Skip to content

Commit 0d4e40d

Browse files
cngonzalezpedrobonamin
authored andcommitted
feat(cli): add non-studio app template (#8394)
1 parent a6bd3af commit 0d4e40d

32 files changed

+641
-154
lines changed

packages/@sanity/cli/.depcheckrc.json

+2
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
"@portabletext/types",
1414
"slug",
1515
"@sanity/asset-utils",
16+
"@sanity/sdk",
17+
"@sanity/sdk-react",
1618
"styled-components",
1719
"sanity-plugin-hotspot-array",
1820
"react-icons",

packages/@sanity/cli/src/actions/init-project/bootstrapLocalTemplate.ts

+32-20
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,10 @@ import {copy} from '../../util/copy'
1010
import {getAndWriteJourneySchemaWorker} from '../../util/journeyConfig'
1111
import {resolveLatestVersions} from '../../util/resolveLatestVersions'
1212
import {createCliConfig} from './createCliConfig'
13+
import {createCoreAppCliConfig} from './createCoreAppCliConfig'
1314
import {createPackageManifest} from './createPackageManifest'
1415
import {createStudioConfig, type GenerateConfigOptions} from './createStudioConfig'
16+
import {determineCoreAppTemplate} from './determineCoreAppTemplate'
1517
import {type ProjectTemplate} from './initProject'
1618
import templates from './templates'
1719
import {updateInitialTemplateMetadata} from './updateInitialTemplateMetadata'
@@ -36,9 +38,9 @@ export async function bootstrapLocalTemplate(
3638
const {apiClient, cliRoot, output} = context
3739
const templatesDir = path.join(cliRoot, 'templates')
3840
const {outputPath, templateName, useTypeScript, packageName, variables} = opts
39-
const {projectId} = variables
4041
const sourceDir = path.join(templatesDir, templateName)
4142
const sharedDir = path.join(templatesDir, 'shared')
43+
const isCoreAppTemplate = determineCoreAppTemplate(templateName)
4244

4345
// Check that we have a template info file (dependencies, plugins etc)
4446
const template = templates[templateName]
@@ -81,15 +83,16 @@ export async function bootstrapLocalTemplate(
8183
// Resolve latest versions of Sanity-dependencies
8284
spinner = output.spinner('Resolving latest module versions').start()
8385
const dependencyVersions = await resolveLatestVersions({
84-
...studioDependencies.dependencies,
85-
...studioDependencies.devDependencies,
86+
...(isCoreAppTemplate ? {} : studioDependencies.dependencies),
87+
...(isCoreAppTemplate ? {} : studioDependencies.devDependencies),
8688
...(template.dependencies || {}),
89+
...(template.devDependencies || {}),
8790
})
8891
spinner.succeed()
8992

9093
// Use the resolved version for the given dependency
9194
const dependencies = Object.keys({
92-
...studioDependencies.dependencies,
95+
...(isCoreAppTemplate ? {} : studioDependencies.dependencies),
9396
...template.dependencies,
9497
}).reduce(
9598
(deps, dependency) => {
@@ -100,7 +103,7 @@ export async function bootstrapLocalTemplate(
100103
)
101104

102105
const devDependencies = Object.keys({
103-
...studioDependencies.devDependencies,
106+
...(isCoreAppTemplate ? {} : studioDependencies.devDependencies),
104107
...template.devDependencies,
105108
}).reduce(
106109
(deps, dependency) => {
@@ -116,32 +119,41 @@ export async function bootstrapLocalTemplate(
116119
name: packageName,
117120
dependencies,
118121
devDependencies,
122+
scripts: template.scripts,
119123
})
120124

121125
// ...and a studio config (`sanity.config.[ts|js]`)
122-
const studioConfig = await createStudioConfig({
126+
const studioConfig = createStudioConfig({
123127
template: template.configTemplate,
124128
variables,
125129
})
126130

127131
// ...and a CLI config (`sanity.cli.[ts|js]`)
128-
const cliConfig = await createCliConfig({
129-
projectId: variables.projectId,
130-
dataset: variables.dataset,
131-
autoUpdates: variables.autoUpdates,
132-
})
132+
const cliConfig = isCoreAppTemplate
133+
? createCoreAppCliConfig({appLocation: template.appLocation!})
134+
: createCliConfig({
135+
projectId: variables.projectId,
136+
dataset: variables.dataset,
137+
autoUpdates: variables.autoUpdates,
138+
})
133139

134140
// Write non-template files to disc
135141
const codeExt = useTypeScript ? 'ts' : 'js'
136-
await Promise.all([
137-
writeFileIfNotExists(`sanity.config.${codeExt}`, studioConfig),
138-
writeFileIfNotExists(`sanity.cli.${codeExt}`, cliConfig),
139-
writeFileIfNotExists('package.json', packageManifest),
140-
writeFileIfNotExists(
141-
'eslint.config.mjs',
142-
`import studio from '@sanity/eslint-config-studio'\n\nexport default [...studio]\n`,
143-
),
144-
])
142+
await Promise.all(
143+
[
144+
...[
145+
isCoreAppTemplate
146+
? Promise.resolve(null)
147+
: writeFileIfNotExists(`sanity.config.${codeExt}`, studioConfig),
148+
],
149+
writeFileIfNotExists(`sanity.cli.${codeExt}`, cliConfig),
150+
writeFileIfNotExists('package.json', packageManifest),
151+
writeFileIfNotExists(
152+
'eslint.config.mjs',
153+
`import studio from '@sanity/eslint-config-studio'\n\nexport default [...studio]\n`,
154+
),
155+
].filter(Boolean),
156+
)
145157

146158
debug('Updating initial template metadata')
147159
await updateInitialTemplateMetadata(apiClient, variables.projectId, `cli-${templateName}`)
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
1-
import traverse from '@babel/traverse'
2-
import {parse, print} from 'recast'
3-
import * as parser from 'recast/parsers/typescript'
1+
import {processTemplate} from './processTemplate'
42

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

2725
export function createCliConfig(options: GenerateCliConfigOptions): string {
28-
const variables = options
29-
const template = defaultTemplate.trimStart()
30-
const ast = parse(template, {parser})
31-
32-
traverse(ast, {
33-
StringLiteral: {
34-
enter({node}) {
35-
const value = node.value
36-
if (!value.startsWith('%') || !value.endsWith('%')) {
37-
return
38-
}
39-
const variableName = value.slice(1, -1) as keyof GenerateCliConfigOptions
40-
if (!(variableName in variables)) {
41-
throw new Error(`Template variable '${value}' not defined`)
42-
}
43-
const newValue = variables[variableName]
44-
/*
45-
* although there are valid non-strings in our config,
46-
* they're not in StringLiteral nodes, so assume undefined
47-
*/
48-
node.value = typeof newValue === 'string' ? newValue : ''
49-
},
50-
},
51-
Identifier: {
52-
enter(path) {
53-
if (!path.node.name.startsWith('__BOOL__')) {
54-
return
55-
}
56-
const variableName = path.node.name.replace(
57-
/^__BOOL__(.+?)__$/,
58-
'$1',
59-
) as keyof GenerateCliConfigOptions
60-
if (!(variableName in variables)) {
61-
throw new Error(`Template variable '${variableName}' not defined`)
62-
}
63-
const value = variables[variableName]
64-
if (typeof value !== 'boolean') {
65-
throw new Error(`Expected boolean value for '${variableName}'`)
66-
}
67-
path.replaceWith({type: 'BooleanLiteral', value})
68-
},
69-
},
26+
return processTemplate({
27+
template: defaultTemplate,
28+
variables: options,
29+
includeBooleanTransform: true,
7030
})
71-
72-
return print(ast, {quote: 'single'}).code
7331
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import {processTemplate} from './processTemplate'
2+
3+
const defaultCoreAppTemplate = `
4+
import {defineCliConfig} from 'sanity/cli'
5+
6+
export default defineCliConfig({
7+
__experimental_coreAppConfiguration: {
8+
appLocation: '%appLocation%'
9+
},
10+
})
11+
`
12+
13+
export interface GenerateCliConfigOptions {
14+
organizationId?: string
15+
appLocation: string
16+
}
17+
18+
export function createCoreAppCliConfig(options: GenerateCliConfigOptions): string {
19+
return processTemplate({
20+
template: defaultCoreAppTemplate,
21+
variables: options,
22+
})
23+
}

packages/@sanity/cli/src/actions/init-project/createPackageManifest.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ export function createPackageManifest(
2929

3030
main: 'package.json',
3131
keywords: ['sanity'],
32-
scripts: {
32+
scripts: data.scripts || {
3333
'dev': 'sanity dev',
3434
'start': 'sanity start',
3535
'build': 'sanity build',

packages/@sanity/cli/src/actions/init-project/createStudioConfig.ts

+4-27
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
1-
import traverse from '@babel/traverse'
2-
import {parse, print} from 'recast'
3-
import * as parser from 'recast/parsers/typescript'
1+
import {processTemplate} from './processTemplate'
42

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

50-
const template = (options.template || defaultTemplate).trimStart()
51-
const ast = parse(template, {parser})
52-
traverse(ast, {
53-
StringLiteral: {
54-
enter({node}) {
55-
const value = node.value
56-
if (!value.startsWith('%') || !value.endsWith('%')) {
57-
return
58-
}
59-
60-
const variableName = value.slice(1, -1) as keyof GenerateConfigOptions['variables']
61-
if (!(variableName in variables)) {
62-
throw new Error(`Template variable '${value}' not defined`)
63-
}
64-
const newValue = variables[variableName]
65-
/*
66-
* although there are valid non-strings in our config,
67-
* they're not in this template, so assume undefined
68-
*/
69-
node.value = typeof newValue === 'string' ? newValue : ''
70-
},
71-
},
48+
return processTemplate({
49+
template: options.template || defaultTemplate,
50+
variables,
7251
})
73-
74-
return print(ast, {quote: 'single'}).code
7552
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
const coreAppTemplates = ['core-app']
2+
3+
/**
4+
* Determine if a given template is a studio template.
5+
* This function may need to be more robust once we
6+
* introduce remote templates, for example.
7+
*
8+
* @param templateName - Name of the template
9+
* @returns boolean indicating if the template is a studio template
10+
*/
11+
export function determineCoreAppTemplate(templateName: string): boolean {
12+
return coreAppTemplates.includes(templateName)
13+
}

packages/@sanity/cli/src/actions/init-project/initProject.ts

+21-2
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ import {createProject} from '../project/createProject'
4949
import {bootstrapLocalTemplate} from './bootstrapLocalTemplate'
5050
import {bootstrapRemoteTemplate} from './bootstrapRemoteTemplate'
5151
import {type GenerateConfigOptions} from './createStudioConfig'
52+
import {determineCoreAppTemplate} from './determineCoreAppTemplate'
5253
import {absolutify, validateEmptyPath} from './fsUtils'
5354
import {tryGitInit} from './git'
5455
import {promptForDatasetName} from './promptForDatasetName'
@@ -97,6 +98,8 @@ export interface ProjectTemplate {
9798
importPrompt?: string
9899
configTemplate?: string | ((variables: GenerateConfigOptions['variables']) => string)
99100
typescriptOnly?: boolean
101+
appLocation?: string
102+
scripts?: Record<string, string>
100103
}
101104

102105
export interface ProjectOrganization {
@@ -271,6 +274,9 @@ export default async function initSanity(
271274
print('')
272275

273276
const flags = await prepareFlags()
277+
// skip project / dataset prompting
278+
const isCoreAppTemplate = cliFlags.template ? determineCoreAppTemplate(cliFlags.template) : false // Default to false
279+
274280
// We're authenticated, now lets select or create a project
275281
const {projectId, displayName, isFirstProject, datasetName, schemaUrl} = await getProjectDetails()
276282

@@ -655,11 +661,15 @@ export default async function initSanity(
655661
const isCurrentDir = outputPath === process.cwd()
656662
if (isCurrentDir) {
657663
print(`\n${chalk.green('Success!')} Now, use this command to continue:\n`)
658-
print(`${chalk.cyan(devCommand)} - to run Sanity Studio\n`)
664+
print(
665+
`${chalk.cyan(devCommand)} - to run ${isCoreAppTemplate ? 'your Sanity application' : 'Sanity Studio'}\n`,
666+
)
659667
} else {
660668
print(`\n${chalk.green('Success!')} Now, use these commands to continue:\n`)
661669
print(`First: ${chalk.cyan(`cd ${outputPath}`)} - to enter project’s directory`)
662-
print(`Then: ${chalk.cyan(devCommand)} - to run Sanity Studio\n`)
670+
print(
671+
`Then: ${chalk.cyan(devCommand)} -to run ${isCoreAppTemplate ? 'your Sanity application' : 'Sanity Studio'}\n`,
672+
)
663673
}
664674

665675
print(`Other helpful commands`)
@@ -720,6 +730,15 @@ export default async function initSanity(
720730
return data
721731
}
722732

733+
if (isCoreAppTemplate) {
734+
return {
735+
projectId: '',
736+
displayName: '',
737+
isFirstProject: false,
738+
datasetName: '',
739+
}
740+
}
741+
723742
debug('Prompting user to select or create a project')
724743
const project = await getOrCreateProject()
725744
debug(`Project with name ${project.displayName} selected`)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import traverse from '@babel/traverse'
2+
import {parse, print} from 'recast'
3+
import * as parser from 'recast/parsers/typescript'
4+
5+
interface TemplateOptions<T> {
6+
template: string
7+
variables: T
8+
includeBooleanTransform?: boolean
9+
}
10+
11+
export function processTemplate<T extends object>(options: TemplateOptions<T>): string {
12+
const {template, variables, includeBooleanTransform = false} = options
13+
const ast = parse(template.trimStart(), {parser})
14+
15+
traverse(ast, {
16+
StringLiteral: {
17+
enter({node}) {
18+
const value = node.value
19+
if (!value.startsWith('%') || !value.endsWith('%')) {
20+
return
21+
}
22+
const variableName = value.slice(1, -1) as keyof T
23+
if (!(variableName in variables)) {
24+
throw new Error(`Template variable '${value}' not defined`)
25+
}
26+
const newValue = variables[variableName]
27+
/*
28+
* although there are valid non-strings in our config,
29+
* they're not in StringLiteral nodes, so assume undefined
30+
*/
31+
node.value = typeof newValue === 'string' ? newValue : ''
32+
},
33+
},
34+
...(includeBooleanTransform && {
35+
Identifier: {
36+
enter(path) {
37+
if (!path.node.name.startsWith('__BOOL__')) {
38+
return
39+
}
40+
const variableName = path.node.name.replace(/^__BOOL__(.+?)__$/, '$1') as keyof T
41+
if (!(variableName in variables)) {
42+
throw new Error(`Template variable '${variableName.toString()}' not defined`)
43+
}
44+
const value = variables[variableName]
45+
if (typeof value !== 'boolean') {
46+
throw new Error(`Expected boolean value for '${variableName.toString()}'`)
47+
}
48+
path.replaceWith({type: 'BooleanLiteral', value})
49+
},
50+
},
51+
}),
52+
})
53+
54+
return print(ast, {quote: 'single'}).code
55+
}

0 commit comments

Comments
 (0)