Skip to content

Commit

Permalink
feat(cli): add non-studio app template (#8394)
Browse files Browse the repository at this point in the history
  • Loading branch information
cngonzalez authored and pedrobonamin committed Feb 5, 2025
1 parent a6bd3af commit 0d4e40d
Show file tree
Hide file tree
Showing 32 changed files with 641 additions and 154 deletions.
2 changes: 2 additions & 0 deletions packages/@sanity/cli/.depcheckrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
"@portabletext/types",
"slug",
"@sanity/asset-utils",
"@sanity/sdk",
"@sanity/sdk-react",
"styled-components",
"sanity-plugin-hotspot-array",
"react-icons",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@ import {copy} from '../../util/copy'
import {getAndWriteJourneySchemaWorker} from '../../util/journeyConfig'
import {resolveLatestVersions} from '../../util/resolveLatestVersions'
import {createCliConfig} from './createCliConfig'
import {createCoreAppCliConfig} from './createCoreAppCliConfig'
import {createPackageManifest} from './createPackageManifest'
import {createStudioConfig, type GenerateConfigOptions} from './createStudioConfig'
import {determineCoreAppTemplate} from './determineCoreAppTemplate'
import {type ProjectTemplate} from './initProject'
import templates from './templates'
import {updateInitialTemplateMetadata} from './updateInitialTemplateMetadata'
Expand All @@ -36,9 +38,9 @@ export async function bootstrapLocalTemplate(
const {apiClient, cliRoot, output} = context
const templatesDir = path.join(cliRoot, 'templates')
const {outputPath, templateName, useTypeScript, packageName, variables} = opts
const {projectId} = variables
const sourceDir = path.join(templatesDir, templateName)
const sharedDir = path.join(templatesDir, 'shared')
const isCoreAppTemplate = determineCoreAppTemplate(templateName)

// Check that we have a template info file (dependencies, plugins etc)
const template = templates[templateName]
Expand Down Expand Up @@ -81,15 +83,16 @@ export async function bootstrapLocalTemplate(
// Resolve latest versions of Sanity-dependencies
spinner = output.spinner('Resolving latest module versions').start()
const dependencyVersions = await resolveLatestVersions({
...studioDependencies.dependencies,
...studioDependencies.devDependencies,
...(isCoreAppTemplate ? {} : studioDependencies.dependencies),
...(isCoreAppTemplate ? {} : studioDependencies.devDependencies),
...(template.dependencies || {}),
...(template.devDependencies || {}),
})
spinner.succeed()

// Use the resolved version for the given dependency
const dependencies = Object.keys({
...studioDependencies.dependencies,
...(isCoreAppTemplate ? {} : studioDependencies.dependencies),
...template.dependencies,
}).reduce(
(deps, dependency) => {
Expand All @@ -100,7 +103,7 @@ export async function bootstrapLocalTemplate(
)

const devDependencies = Object.keys({
...studioDependencies.devDependencies,
...(isCoreAppTemplate ? {} : studioDependencies.devDependencies),
...template.devDependencies,
}).reduce(
(deps, dependency) => {
Expand All @@ -116,32 +119,41 @@ export async function bootstrapLocalTemplate(
name: packageName,
dependencies,
devDependencies,
scripts: template.scripts,
})

// ...and a studio config (`sanity.config.[ts|js]`)
const studioConfig = await createStudioConfig({
const studioConfig = createStudioConfig({
template: template.configTemplate,
variables,
})

// ...and a CLI config (`sanity.cli.[ts|js]`)
const cliConfig = await createCliConfig({
projectId: variables.projectId,
dataset: variables.dataset,
autoUpdates: variables.autoUpdates,
})
const cliConfig = isCoreAppTemplate
? createCoreAppCliConfig({appLocation: template.appLocation!})
: createCliConfig({
projectId: variables.projectId,
dataset: variables.dataset,
autoUpdates: variables.autoUpdates,
})

// Write non-template files to disc
const codeExt = useTypeScript ? 'ts' : 'js'
await Promise.all([
writeFileIfNotExists(`sanity.config.${codeExt}`, studioConfig),
writeFileIfNotExists(`sanity.cli.${codeExt}`, cliConfig),
writeFileIfNotExists('package.json', packageManifest),
writeFileIfNotExists(
'eslint.config.mjs',
`import studio from '@sanity/eslint-config-studio'\n\nexport default [...studio]\n`,
),
])
await Promise.all(
[
...[
isCoreAppTemplate
? Promise.resolve(null)
: writeFileIfNotExists(`sanity.config.${codeExt}`, studioConfig),
],
writeFileIfNotExists(`sanity.cli.${codeExt}`, cliConfig),
writeFileIfNotExists('package.json', packageManifest),
writeFileIfNotExists(
'eslint.config.mjs',
`import studio from '@sanity/eslint-config-studio'\n\nexport default [...studio]\n`,
),
].filter(Boolean),
)

debug('Updating initial template metadata')
await updateInitialTemplateMetadata(apiClient, variables.projectId, `cli-${templateName}`)
Expand Down
52 changes: 5 additions & 47 deletions packages/@sanity/cli/src/actions/init-project/createCliConfig.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import traverse from '@babel/traverse'
import {parse, print} from 'recast'
import * as parser from 'recast/parsers/typescript'
import {processTemplate} from './processTemplate'

const defaultTemplate = `
import {defineCliConfig} from 'sanity/cli'
Expand All @@ -25,49 +23,9 @@ export interface GenerateCliConfigOptions {
}

export function createCliConfig(options: GenerateCliConfigOptions): string {
const variables = options
const template = defaultTemplate.trimStart()
const ast = parse(template, {parser})

traverse(ast, {
StringLiteral: {
enter({node}) {
const value = node.value
if (!value.startsWith('%') || !value.endsWith('%')) {
return
}
const variableName = value.slice(1, -1) as keyof GenerateCliConfigOptions
if (!(variableName in variables)) {
throw new Error(`Template variable '${value}' not defined`)
}
const newValue = variables[variableName]
/*
* although there are valid non-strings in our config,
* they're not in StringLiteral nodes, so assume undefined
*/
node.value = typeof newValue === 'string' ? newValue : ''
},
},
Identifier: {
enter(path) {
if (!path.node.name.startsWith('__BOOL__')) {
return
}
const variableName = path.node.name.replace(
/^__BOOL__(.+?)__$/,
'$1',
) as keyof GenerateCliConfigOptions
if (!(variableName in variables)) {
throw new Error(`Template variable '${variableName}' not defined`)
}
const value = variables[variableName]
if (typeof value !== 'boolean') {
throw new Error(`Expected boolean value for '${variableName}'`)
}
path.replaceWith({type: 'BooleanLiteral', value})
},
},
return processTemplate({
template: defaultTemplate,
variables: options,
includeBooleanTransform: true,
})

return print(ast, {quote: 'single'}).code
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import {processTemplate} from './processTemplate'

const defaultCoreAppTemplate = `
import {defineCliConfig} from 'sanity/cli'
export default defineCliConfig({
__experimental_coreAppConfiguration: {
appLocation: '%appLocation%'
},
})
`

export interface GenerateCliConfigOptions {
organizationId?: string
appLocation: string
}

export function createCoreAppCliConfig(options: GenerateCliConfigOptions): string {
return processTemplate({
template: defaultCoreAppTemplate,
variables: options,
})
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export function createPackageManifest(

main: 'package.json',
keywords: ['sanity'],
scripts: {
scripts: data.scripts || {
'dev': 'sanity dev',
'start': 'sanity start',
'build': 'sanity build',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import traverse from '@babel/traverse'
import {parse, print} from 'recast'
import * as parser from 'recast/parsers/typescript'
import {processTemplate} from './processTemplate'

const defaultTemplate = `
import {defineConfig} from 'sanity'
Expand Down Expand Up @@ -47,29 +45,8 @@ export function createStudioConfig(options: GenerateConfigOptions): string {
return options.template(variables).trimStart()
}

const template = (options.template || defaultTemplate).trimStart()
const ast = parse(template, {parser})
traverse(ast, {
StringLiteral: {
enter({node}) {
const value = node.value
if (!value.startsWith('%') || !value.endsWith('%')) {
return
}

const variableName = value.slice(1, -1) as keyof GenerateConfigOptions['variables']
if (!(variableName in variables)) {
throw new Error(`Template variable '${value}' not defined`)
}
const newValue = variables[variableName]
/*
* although there are valid non-strings in our config,
* they're not in this template, so assume undefined
*/
node.value = typeof newValue === 'string' ? newValue : ''
},
},
return processTemplate({
template: options.template || defaultTemplate,
variables,
})

return print(ast, {quote: 'single'}).code
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
const coreAppTemplates = ['core-app']

/**
* Determine if a given template is a studio template.
* This function may need to be more robust once we
* introduce remote templates, for example.
*
* @param templateName - Name of the template
* @returns boolean indicating if the template is a studio template
*/
export function determineCoreAppTemplate(templateName: string): boolean {
return coreAppTemplates.includes(templateName)
}
23 changes: 21 additions & 2 deletions packages/@sanity/cli/src/actions/init-project/initProject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ import {createProject} from '../project/createProject'
import {bootstrapLocalTemplate} from './bootstrapLocalTemplate'
import {bootstrapRemoteTemplate} from './bootstrapRemoteTemplate'
import {type GenerateConfigOptions} from './createStudioConfig'
import {determineCoreAppTemplate} from './determineCoreAppTemplate'
import {absolutify, validateEmptyPath} from './fsUtils'
import {tryGitInit} from './git'
import {promptForDatasetName} from './promptForDatasetName'
Expand Down Expand Up @@ -97,6 +98,8 @@ export interface ProjectTemplate {
importPrompt?: string
configTemplate?: string | ((variables: GenerateConfigOptions['variables']) => string)
typescriptOnly?: boolean
appLocation?: string
scripts?: Record<string, string>
}

export interface ProjectOrganization {
Expand Down Expand Up @@ -271,6 +274,9 @@ export default async function initSanity(
print('')

const flags = await prepareFlags()
// skip project / dataset prompting
const isCoreAppTemplate = cliFlags.template ? determineCoreAppTemplate(cliFlags.template) : false // Default to false

// We're authenticated, now lets select or create a project
const {projectId, displayName, isFirstProject, datasetName, schemaUrl} = await getProjectDetails()

Expand Down Expand Up @@ -655,11 +661,15 @@ export default async function initSanity(
const isCurrentDir = outputPath === process.cwd()
if (isCurrentDir) {
print(`\n${chalk.green('Success!')} Now, use this command to continue:\n`)
print(`${chalk.cyan(devCommand)} - to run Sanity Studio\n`)
print(
`${chalk.cyan(devCommand)} - to run ${isCoreAppTemplate ? 'your Sanity application' : 'Sanity Studio'}\n`,
)
} else {
print(`\n${chalk.green('Success!')} Now, use these commands to continue:\n`)
print(`First: ${chalk.cyan(`cd ${outputPath}`)} - to enter project’s directory`)
print(`Then: ${chalk.cyan(devCommand)} - to run Sanity Studio\n`)
print(
`Then: ${chalk.cyan(devCommand)} -to run ${isCoreAppTemplate ? 'your Sanity application' : 'Sanity Studio'}\n`,
)
}

print(`Other helpful commands`)
Expand Down Expand Up @@ -720,6 +730,15 @@ export default async function initSanity(
return data
}

if (isCoreAppTemplate) {
return {
projectId: '',
displayName: '',
isFirstProject: false,
datasetName: '',
}
}

debug('Prompting user to select or create a project')
const project = await getOrCreateProject()
debug(`Project with name ${project.displayName} selected`)
Expand Down
55 changes: 55 additions & 0 deletions packages/@sanity/cli/src/actions/init-project/processTemplate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import traverse from '@babel/traverse'
import {parse, print} from 'recast'
import * as parser from 'recast/parsers/typescript'

interface TemplateOptions<T> {
template: string
variables: T
includeBooleanTransform?: boolean
}

export function processTemplate<T extends object>(options: TemplateOptions<T>): string {
const {template, variables, includeBooleanTransform = false} = options
const ast = parse(template.trimStart(), {parser})

traverse(ast, {
StringLiteral: {
enter({node}) {
const value = node.value
if (!value.startsWith('%') || !value.endsWith('%')) {
return
}
const variableName = value.slice(1, -1) as keyof T
if (!(variableName in variables)) {
throw new Error(`Template variable '${value}' not defined`)
}
const newValue = variables[variableName]
/*
* although there are valid non-strings in our config,
* they're not in StringLiteral nodes, so assume undefined
*/
node.value = typeof newValue === 'string' ? newValue : ''
},
},
...(includeBooleanTransform && {
Identifier: {
enter(path) {
if (!path.node.name.startsWith('__BOOL__')) {
return
}
const variableName = path.node.name.replace(/^__BOOL__(.+?)__$/, '$1') as keyof T
if (!(variableName in variables)) {
throw new Error(`Template variable '${variableName.toString()}' not defined`)
}
const value = variables[variableName]
if (typeof value !== 'boolean') {
throw new Error(`Expected boolean value for '${variableName.toString()}'`)
}
path.replaceWith({type: 'BooleanLiteral', value})
},
},
}),
})

return print(ast, {quote: 'single'}).code
}
Loading

0 comments on commit 0d4e40d

Please sign in to comment.