Skip to content
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
6 changes: 6 additions & 0 deletions .changeset/pr-968.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@sanity/cli': minor
---

- Add project and dataset selection prompts to `sanity init` for app templates
- Fix crash when selecting "no" for TypeScript on app templates, which only ship `.tsx` files
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import {mkdtemp, readFile, rm} from 'node:fs/promises'
import {tmpdir} from 'node:os'
import path from 'node:path'

import {afterEach, beforeEach, describe, expect, test, vi} from 'vitest'

import {bootstrapLocalTemplate} from '../bootstrapLocalTemplate.js'

vi.mock('../../../util/resolveLatestVersions.js', () => ({
resolveLatestVersions: vi.fn().mockImplementation(async (deps: Record<string, string>) => {
const resolved: Record<string, string> = {}
for (const key of Object.keys(deps)) resolved[key] = '1.0.0'
return resolved
}),
}))

vi.mock('../updateInitialTemplateMetadata.js', () => ({
updateInitialTemplateMetadata: vi.fn().mockResolvedValue(undefined),
}))

function makeOutput() {
return {
clear: vi.fn(),
error: vi.fn(),
log: vi.fn(),
print: vi.fn(),
spinner: vi.fn(() => ({
start: () => ({fail: vi.fn(), succeed: vi.fn()}),
})),
warn: vi.fn(),
} as any
}

describe('bootstrapLocalTemplate (app templates)', () => {
let tmp: string
beforeEach(async () => {
tmp = await mkdtemp(path.join(tmpdir(), 'cli-bootstrap-'))
})
afterEach(async () => {
await rm(tmp, {force: true, recursive: true})
vi.clearAllMocks()
})

test('renders projectId and dataset into App.tsx when provided', async () => {
await bootstrapLocalTemplate({
output: makeOutput(),
outputPath: tmp,
packageName: 'my-app',
templateName: 'app-quickstart',
useTypeScript: true,
variables: {
autoUpdates: false,
dataset: 'production',
organizationId: 'org1',
projectId: 'abc123',
projectName: 'my-app',
},
})

const appTsx = await readFile(path.join(tmp, 'src', 'App.tsx'), 'utf8')
expect(appTsx).toContain(`projectId: 'abc123'`)
expect(appTsx).toContain(`dataset: 'production'`)
expect(appTsx).not.toContain('%projectId%')
expect(appTsx).not.toContain('%dataset%')
})

test('renders empty strings into App.tsx when user skipped project selection', async () => {
await bootstrapLocalTemplate({
output: makeOutput(),
outputPath: tmp,
packageName: 'my-app',
templateName: 'app-sanity-ui',
useTypeScript: true,
variables: {
autoUpdates: false,
dataset: '',
organizationId: 'org1',
projectId: '',
projectName: 'my-app',
},
})

const appTsx = await readFile(path.join(tmp, 'src', 'App.tsx'), 'utf8')
expect(appTsx).toContain(`projectId: ''`)
expect(appTsx).toContain(`dataset: ''`)
expect(appTsx).not.toContain('%projectId%')
expect(appTsx).not.toContain('%dataset%')
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {createCliConfig} from './createCliConfig.js'
import {createPackageManifest} from './createPackageManifest.js'
import {createStudioConfig, type GenerateConfigOptions} from './createStudioConfig.js'
import {determineAppTemplate} from './determineAppTemplate.js'
import {processTemplate} from './processTemplate.js'
import {sdkAppDependencies} from './sdkAppDependencies.js'
import {studioDependencies} from './studioDependencies.js'
import templates from './templates/index.js'
Expand Down Expand Up @@ -66,6 +67,19 @@ export async function bootstrapLocalTemplate(

spin.succeed()

if (isAppTemplate) {
const appEntryPath = path.join(outputPath, 'src', 'App.tsx')
const raw = await fs.readFile(appEntryPath, 'utf8')
const rendered = processTemplate({
template: raw,
variables: {
dataset: variables.dataset ?? '',
projectId: variables.projectId ?? '',
},
})
await fs.writeFile(appEntryPath, rendered)
}

// Merge global and template-specific plugins and dependencies

// Resolve latest versions of Sanity-dependencies
Expand Down Expand Up @@ -153,7 +167,9 @@ export async function bootstrapLocalTemplate(
)

debug('Updating initial template metadata')
await updateInitialTemplateMetadata(variables.projectId, `cli-${templateName}`)
if (variables.projectId) {
await updateInitialTemplateMetadata(variables.projectId, `cli-${templateName}`)
}

// Finish up by providing init process with template-specific info
spin.succeed()
Expand Down
30 changes: 22 additions & 8 deletions packages/@sanity/cli/src/actions/init/initApp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,18 @@ import {scaffoldAndInstall, selectTemplate} from './scaffoldTemplate.js'

export async function initApp({
autoUpdates,
datasetName,
defaults,
error,
git,
noGit,
mcpConfigured,
noGit,
organizationId,
output,
outputPath,
overwriteFiles,
packageManager,
projectId,
remoteTemplateInfo,
sluggedName,
template,
Expand All @@ -32,16 +34,18 @@ export async function initApp({
workDir,
}: {
autoUpdates: boolean
datasetName: string
defaults: {projectName: string}
error: Output['error']
git?: boolean | string
noGit?: boolean
mcpConfigured: EditorName[]
noGit?: boolean
organizationId: string | undefined
output: Output
outputPath: string
overwriteFiles?: boolean
packageManager?: string
projectId: string
remoteTemplateInfo: RepoInfo | undefined
sluggedName: string
template?: string
Expand Down Expand Up @@ -69,7 +73,7 @@ export async function initApp({

await scaffoldAndInstall({
autoUpdates,
datasetName: '',
datasetName,
defaults,
displayName: '',
git,
Expand All @@ -79,7 +83,7 @@ export async function initApp({
outputPath,
overwriteFiles,
packageManager,
projectId: '',
projectId,
remoteTemplateInfo,
sluggedName,
templateName,
Expand All @@ -98,10 +102,20 @@ export async function initApp({
`${logSymbols.success} ${styleText(['green', 'bold'], 'Success!')} Your custom app has been scaffolded.`,
)
if (!isCurrentDir) output.log(goToProjectDir)
output.log(
`\n${styleText('bold', 'Next')}, configure the project(s) and dataset(s) your app should work with.`,
)
output.log('\nGet started in `src/App.tsx`, or refer to our documentation for a walkthrough:')

if (projectId && datasetName) {
output.log(
`\nConfigured with project ${styleText('cyan', projectId)} and dataset ${styleText('cyan', datasetName)}.`,
)
output.log(
'Edit `src/App.tsx` to change these values or add more project / dataset pairs to your config.',
)
} else {
output.log(
`\n${styleText('bold', 'Next')}, configure the project(s) and dataset(s) your app should work with in \`src/App.tsx\`.`,
)
}
output.log('\nRefer to our documentation for a walkthrough:')
output.log(
styleText(['blue', 'underline'], 'https://www.sanity.io/docs/app-sdk/sdk-configuration'),
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {type ProjectTemplate} from '../types.js'
const appTemplate: ProjectTemplate = {
entry: './src/App.tsx',
type: 'module',
typescriptOnly: true,
}

export default appTemplate
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const appSanityUiTemplate: ProjectTemplate = {
},
entry: './src/App.tsx',
type: 'module',
typescriptOnly: true,
}

export default appSanityUiTemplate
Original file line number Diff line number Diff line change
Expand Up @@ -239,16 +239,14 @@ describe('#init: bootstrap-app-initialization', () => {
// Reset select mock to clear any unconsumed mockResolvedValueOnce from prior tests
mocks.select.mockReset()

// Mock organizations endpoint with app-specific query params
mockApi({
apiVersion: ORGANIZATIONS_API_VERSION,
method: 'get',
query: {includeImplicitMemberships: 'true', includeMembers: 'true'},
uri: '/organizations',
}).reply(200, [{id: 'org-1', name: 'Org 1', slug: 'org-1'}])

// select is called once for organization selection (template comes from --template flag)
mocks.select.mockResolvedValueOnce('org-1') // organization
mocks.select.mockResolvedValueOnce('__skip__') // promptForAppTemplateSetup

mockApi({
apiVersion: MCP_JOURNEY_API_VERSION,
Expand Down
Loading
Loading