Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(cli): add non-studio app template #8394

Merged
merged 5 commits into from
Feb 5, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
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: {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Confirm this is the key we want to use

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
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
Loading