From d59295ddccec90d577e38cd99afd18e22318c60e Mon Sep 17 00:00:00 2001 From: Carolina Gonzalez Date: Thu, 23 Jan 2025 17:15:01 -0500 Subject: [PATCH 1/5] feat(cli): add non-studio app template and non-studio dev options --- packages/@sanity/cli/.depcheckrc.json | 3 + .../init-project/bootstrapLocalTemplate.ts | 28 ++++++--- .../init-project/determineStudioTemplate.ts | 13 ++++ .../actions/init-project/templates/coreApp.ts | 14 +++++ .../actions/init-project/templates/index.ts | 2 + packages/@sanity/cli/src/types.ts | 2 + .../cli/templates/core-app/src/App.tsx | 23 +++++++ .../cli/templates/core-app/src/main.tsx | 9 +++ .../_internal/cli/commands/dev/devCommand.ts | 22 +++++++ .../src/_internal/cli/server/devServer.ts | 31 +++++++++- .../src/_internal/cli/server/getViteConfig.ts | 62 +++++++++++-------- .../_internal/cli/server/renderDocument.tsx | 21 +++++-- .../src/_internal/cli/server/runtime.ts | 36 ++++++----- 13 files changed, 210 insertions(+), 56 deletions(-) create mode 100644 packages/@sanity/cli/src/actions/init-project/determineStudioTemplate.ts create mode 100644 packages/@sanity/cli/src/actions/init-project/templates/coreApp.ts create mode 100644 packages/@sanity/cli/templates/core-app/src/App.tsx create mode 100644 packages/@sanity/cli/templates/core-app/src/main.tsx diff --git a/packages/@sanity/cli/.depcheckrc.json b/packages/@sanity/cli/.depcheckrc.json index 90cce94cb1c..aaf2599c229 100644 --- a/packages/@sanity/cli/.depcheckrc.json +++ b/packages/@sanity/cli/.depcheckrc.json @@ -8,11 +8,14 @@ "vite", "@portabletext/toolkit", "react", + "react-dom", "@sanity/ui", "lodash.get", "@portabletext/types", "slug", "@sanity/asset-utils", + "@sanity/sdk", + "@sanity/sdk-react", "styled-components", "sanity-plugin-hotspot-array", "react-icons", diff --git a/packages/@sanity/cli/src/actions/init-project/bootstrapLocalTemplate.ts b/packages/@sanity/cli/src/actions/init-project/bootstrapLocalTemplate.ts index a635d5c792d..edcda565b7b 100644 --- a/packages/@sanity/cli/src/actions/init-project/bootstrapLocalTemplate.ts +++ b/packages/@sanity/cli/src/actions/init-project/bootstrapLocalTemplate.ts @@ -12,6 +12,7 @@ import {resolveLatestVersions} from '../../util/resolveLatestVersions' import {createCliConfig} from './createCliConfig' import {createPackageManifest} from './createPackageManifest' import {createStudioConfig, type GenerateConfigOptions} from './createStudioConfig' +import {determineStudioTemplate} from './determineStudioTemplate' import {type ProjectTemplate} from './initProject' import templates from './templates' import {updateInitialTemplateMetadata} from './updateInitialTemplateMetadata' @@ -36,9 +37,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 isStudioTemplate = determineStudioTemplate(templateName) // Check that we have a template info file (dependencies, plugins etc) const template = templates[templateName] @@ -84,6 +85,7 @@ export async function bootstrapLocalTemplate( ...studioDependencies.dependencies, ...studioDependencies.devDependencies, ...(template.dependencies || {}), + ...(template.devDependencies || {}), }) spinner.succeed() @@ -133,15 +135,21 @@ export async function bootstrapLocalTemplate( // 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( + [ + ...[ + isStudioTemplate + ? writeFileIfNotExists(`sanity.config.${codeExt}`, studioConfig) + : Promise.resolve(null), + ], + 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}`) diff --git a/packages/@sanity/cli/src/actions/init-project/determineStudioTemplate.ts b/packages/@sanity/cli/src/actions/init-project/determineStudioTemplate.ts new file mode 100644 index 00000000000..7135788a8b8 --- /dev/null +++ b/packages/@sanity/cli/src/actions/init-project/determineStudioTemplate.ts @@ -0,0 +1,13 @@ +const nonStudioTemplates = ['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 determineStudioTemplate(templateName: string): boolean { + return !nonStudioTemplates.includes(templateName) +} diff --git a/packages/@sanity/cli/src/actions/init-project/templates/coreApp.ts b/packages/@sanity/cli/src/actions/init-project/templates/coreApp.ts new file mode 100644 index 00000000000..f1479c09435 --- /dev/null +++ b/packages/@sanity/cli/src/actions/init-project/templates/coreApp.ts @@ -0,0 +1,14 @@ +import {type ProjectTemplate} from '../initProject' + +const coreAppTemplate: ProjectTemplate = { + dependencies: { + '@sanity/sdk': '^0.0.0-alpha', + '@sanity/sdk-react': '^0.0.0-alpha', + }, + devDependencies: { + 'vite': '^6', + '@vitejs/plugin-react': '^4.3.4', + }, +} + +export default coreAppTemplate diff --git a/packages/@sanity/cli/src/actions/init-project/templates/index.ts b/packages/@sanity/cli/src/actions/init-project/templates/index.ts index 00f90fae358..1538d1ff6a4 100644 --- a/packages/@sanity/cli/src/actions/init-project/templates/index.ts +++ b/packages/@sanity/cli/src/actions/init-project/templates/index.ts @@ -1,6 +1,7 @@ import {type ProjectTemplate} from '../initProject' import blog from './blog' import clean from './clean' +import coreAppTemplate from './coreApp' import getStartedTemplate from './getStarted' import moviedb from './moviedb' import quickstart from './quickstart' @@ -10,6 +11,7 @@ import shopifyOnline from './shopifyOnline' const templates: Record = { blog, clean, + 'core-app': coreAppTemplate, 'get-started': getStartedTemplate, moviedb, shopify, diff --git a/packages/@sanity/cli/src/types.ts b/packages/@sanity/cli/src/types.ts index 33b2c04f9f9..45262ff18b8 100644 --- a/packages/@sanity/cli/src/types.ts +++ b/packages/@sanity/cli/src/types.ts @@ -344,6 +344,8 @@ export interface CliConfig { autoUpdates?: boolean studioHost?: string + + isStudioApp?: boolean } export type UserViteConfig = diff --git a/packages/@sanity/cli/templates/core-app/src/App.tsx b/packages/@sanity/cli/templates/core-app/src/App.tsx new file mode 100644 index 00000000000..68c7d732bd4 --- /dev/null +++ b/packages/@sanity/cli/templates/core-app/src/App.tsx @@ -0,0 +1,23 @@ +import {SanityApp} from '@sanity/sdk-react/components' + +export function App() { + + const sanityConfig = { + /* + * CORE apps can access several different projects! + * Add the below configuration if you want to connect to a specific project. + */ + // projectId: 'my-project-id', + // dataset: 'my-dataset', + auth: { + authScope: 'org' + } + } + return ( + + Hello world! + + ) +} + +export default App \ No newline at end of file diff --git a/packages/@sanity/cli/templates/core-app/src/main.tsx b/packages/@sanity/cli/templates/core-app/src/main.tsx new file mode 100644 index 00000000000..4aff0256e00 --- /dev/null +++ b/packages/@sanity/cli/templates/core-app/src/main.tsx @@ -0,0 +1,9 @@ +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import App from './App.tsx' + +createRoot(document.getElementById('root')!).render( + + + , +) diff --git a/packages/sanity/src/_internal/cli/commands/dev/devCommand.ts b/packages/sanity/src/_internal/cli/commands/dev/devCommand.ts index 34e3c807203..480b6f0f34f 100644 --- a/packages/sanity/src/_internal/cli/commands/dev/devCommand.ts +++ b/packages/sanity/src/_internal/cli/commands/dev/devCommand.ts @@ -1,10 +1,14 @@ +import path from 'node:path' + import { type CliCommandArguments, type CliCommandContext, type CliCommandDefinition, + type CliConfig, } from '@sanity/cli' import {type StartDevServerCommandFlags} from '../../actions/dev/devAction' +import {startDevServer} from '../../server' const helpText = ` Notes @@ -27,6 +31,24 @@ const devCommand: CliCommandDefinition = { args: CliCommandArguments, context: CliCommandContext, ) => { + const {workDir, cliConfig} = context + + // if not studio app, skip all Studio-specific initialization + if (!(cliConfig && 'isStudioApp' in cliConfig)) { + // non-studio apps were not possible in v2 + const config = cliConfig as CliConfig | undefined + return startDevServer({ + cwd: workDir, + basePath: '/', + staticPath: path.join(workDir, 'static'), + httpPort: Number(args.extOptions?.port) || 3333, + httpHost: args.extOptions?.host, + reactStrictMode: true, + reactCompiler: config?.reactCompiler, + vite: config?.vite, + isStudioApp: false, + }) + } const devAction = await getDevAction() return devAction(args, context) diff --git a/packages/sanity/src/_internal/cli/server/devServer.ts b/packages/sanity/src/_internal/cli/server/devServer.ts index 8d92e89b14b..2919ef34ad8 100644 --- a/packages/sanity/src/_internal/cli/server/devServer.ts +++ b/packages/sanity/src/_internal/cli/server/devServer.ts @@ -1,3 +1,5 @@ +import path from 'node:path' + import {type ReactCompilerConfig, type UserViteConfig} from '@sanity/cli' import chalk from 'chalk' @@ -17,6 +19,7 @@ export interface DevServerOptions { reactStrictMode: boolean reactCompiler: ReactCompilerConfig | undefined vite?: UserViteConfig + isStudioApp?: boolean } export interface DevServer { @@ -32,22 +35,43 @@ export async function startDevServer(options: DevServerOptions): Promise} reactCompiler: ReactCompilerConfig | undefined + isStudioApp?: boolean } /** @@ -72,27 +73,24 @@ export async function getViteConfig(options: ViteOptions): Promise basePath: rawBasePath = '/', importMap, reactCompiler, + isStudioApp = true, // default to true for backwards compatibility } = options - const monorepo = await loadSanityMonorepo(cwd) + // we may eventually load an example sdk app in the monorepo, but there's none right now + const monorepo = isStudioApp ? await loadSanityMonorepo(cwd) : undefined const basePath = normalizeBasePath(rawBasePath) + const runtimeDir = path.join(cwd, '.sanity', 'runtime') - const sanityPkgPath = (await readPkgUp({cwd: __dirname}))?.path - if (!sanityPkgPath) { + const sanityPkgPath = isStudioApp ? (await readPkgUp({cwd: __dirname}))?.path : null + if (isStudioApp && !sanityPkgPath) { throw new Error('Unable to resolve `sanity` module root') } - const customFaviconsPath = path.join(cwd, 'static') - const defaultFaviconsPath = path.join(path.dirname(sanityPkgPath), 'static', 'favicons') - const staticPath = `${basePath}static` - const {default: viteReact} = await import('@vitejs/plugin-react') const viteConfig: InlineConfig = { - // Define a custom cache directory so that sanity's vite cache - // does not conflict with any potential local vite projects - cacheDir: 'node_modules/.sanity/vite', - root: cwd, + root: runtimeDir, base: basePath, + cacheDir: 'node_modules/.sanity/vite', build: { outDir: outputDir || path.resolve(cwd, 'dist'), sourcemap: sourceMap, @@ -105,22 +103,39 @@ export async function getViteConfig(options: ViteOptions): Promise configFile: false, mode, plugins: [ - viteReact( + (await import('@vitejs/plugin-react')).default( reactCompiler ? {babel: {plugins: [['babel-plugin-react-compiler', reactCompiler]]}} : {}, ), + ], + resolve: { + alias: { + // Map /src to the actual source directory + '/src': path.join(cwd, 'src'), + }, + }, + appType: 'spa', + } + + // Add Studio-specific configuration + if (isStudioApp) { + const customFaviconsPath = path.join(cwd, 'static') + const defaultFaviconsPath = path.join(path.dirname(sanityPkgPath!), 'static', 'favicons') + const staticPath = `${basePath}static` + + viteConfig.plugins!.push( sanityFaviconsPlugin({defaultFaviconsPath, customFaviconsPath, staticUrlPath: staticPath}), sanityRuntimeRewritePlugin(), sanityBuildEntries({basePath, cwd, monorepo, importMap}), - ], - envPrefix: 'SANITY_STUDIO_', - logLevel: mode === 'production' ? 'silent' : 'info', - resolve: { + ) + + viteConfig.resolve = { alias: monorepo?.path ? await getMonorepoAliases(monorepo.path) - : getSanityPkgExportAliases(sanityPkgPath), + : getSanityPkgExportAliases(sanityPkgPath!), dedupe: ['styled-components'], - }, - define: { + } + + viteConfig.define = { // eslint-disable-next-line no-process-env '__SANITY_STAGING__': process.env.SANITY_INTERNAL_ENV === 'staging', 'process.env.MODE': JSON.stringify(mode), @@ -135,23 +150,20 @@ export async function getViteConfig(options: ViteOptions): Promise */ 'process.env.SC_DISABLE_SPEEDY': JSON.stringify('false'), ...getStudioEnvironmentVariables({prefix: 'process.env.', jsonEncode: true}), - }, + } } if (mode === 'production') { viteConfig.build = { ...viteConfig.build, - assetsDir: 'static', minify: minify ? 'esbuild' : false, emptyOutDir: false, // Rely on CLI to do this rollupOptions: { onwarn: onRollupWarn, - external: createExternalFromImportMap(importMap), - input: { - sanity: path.join(cwd, '.sanity', 'runtime', 'app.js'), - }, + external: isStudioApp ? createExternalFromImportMap(importMap) : undefined, + input: isStudioApp ? {sanity: path.join(cwd, '.sanity', 'runtime', 'app.js')} : undefined, }, } } diff --git a/packages/sanity/src/_internal/cli/server/renderDocument.tsx b/packages/sanity/src/_internal/cli/server/renderDocument.tsx index 0e1be1c83f9..5738df9d8bc 100644 --- a/packages/sanity/src/_internal/cli/server/renderDocument.tsx +++ b/packages/sanity/src/_internal/cli/server/renderDocument.tsx @@ -48,12 +48,20 @@ interface RenderDocumentOptions { importMap?: { imports?: Record } + isStudioApp?: boolean } export function renderDocument(options: RenderDocumentOptions): Promise { return new Promise((resolve, reject) => { if (!useThreads) { - resolve(getDocumentHtml(options.studioRootPath, options.props, options.importMap)) + resolve( + getDocumentHtml( + options.studioRootPath, + options.props, + options.importMap, + options.isStudioApp, + ), + ) return } @@ -150,7 +158,8 @@ function renderDocumentFromWorkerData() { throw new Error('Must be used as a Worker with a valid options object in worker data') } - const {monorepo, studioRootPath, props, importMap}: RenderDocumentOptions = workerData || {} + const {monorepo, studioRootPath, props, importMap, isStudioApp}: RenderDocumentOptions = + workerData || {} if (workerData?.dev) { // Define `__DEV__` in the worker thread as well @@ -200,7 +209,7 @@ function renderDocumentFromWorkerData() { loader: 'jsx', }) - const html = getDocumentHtml(studioRootPath, props, importMap) + const html = getDocumentHtml(studioRootPath, props, importMap, isStudioApp) parentPort.postMessage({type: 'result', html}) @@ -213,6 +222,7 @@ function getDocumentHtml( studioRootPath: string, props?: DocumentProps, importMap?: {imports?: Record}, + isStudioApp?: boolean, ): string { const Document = getDocumentComponent(studioRootPath) @@ -234,7 +244,10 @@ function getDocumentHtml( importMap, ) - return `${result}` + // Only modify the root element ID for non-Studio apps + const rootElementId = isStudioApp ? 'sanity' : 'root' + // TODO: actually intervene using html methods + return `${result.replace('id="sanity"', `id="${rootElementId}"`)}` } /** diff --git a/packages/sanity/src/_internal/cli/server/runtime.ts b/packages/sanity/src/_internal/cli/server/runtime.ts index 40b491ede45..b2eea42ea07 100644 --- a/packages/sanity/src/_internal/cli/server/runtime.ts +++ b/packages/sanity/src/_internal/cli/server/runtime.ts @@ -20,6 +20,7 @@ export interface RuntimeOptions { reactStrictMode: boolean watch: boolean basePath?: string + isStudioApp?: boolean } /** @@ -34,14 +35,15 @@ export async function writeSanityRuntime({ reactStrictMode, watch, basePath, + isStudioApp = true, }: RuntimeOptions): Promise { - debug('Resolving Sanity monorepo information') - const monorepo = await loadSanityMonorepo(cwd) - const runtimeDir = path.join(cwd, '.sanity', 'runtime') - debug('Making runtime directory') + const runtimeDir = path.join(cwd, '.sanity', 'runtime') await fs.mkdir(runtimeDir, {recursive: true}) + // Only load monorepo info for Studio apps + const monorepo = isStudioApp ? await loadSanityMonorepo(cwd) : undefined + async function renderAndWriteDocument() { debug('Rendering document template') const indexHtml = decorateIndexWithAutoGeneratedWarning( @@ -49,9 +51,12 @@ export async function writeSanityRuntime({ studioRootPath: cwd, monorepo, props: { - entryPath: `/${path.relative(cwd, path.join(runtimeDir, 'app.js'))}`, + entryPath: isStudioApp + ? `/${path.relative(cwd, path.join(runtimeDir, 'app.js'))}` + : '/src/main.tsx', // TODO: change to be more like studio, possibly dyanmic basePath: basePath || '/', }, + isStudioApp, }), ) @@ -67,14 +72,17 @@ export async function writeSanityRuntime({ await renderAndWriteDocument() - debug('Writing app.js to runtime directory') - const studioConfigPath = await getSanityStudioConfigPath(cwd) - const relativeConfigLocation = studioConfigPath - ? path.relative(runtimeDir, studioConfigPath) - : null + // Only generate app.js for Studio apps + if (isStudioApp) { + debug('Writing app.js to runtime directory') + const studioConfigPath = await getSanityStudioConfigPath(cwd) + const relativeConfigLocation = studioConfigPath + ? path.relative(runtimeDir, studioConfigPath) + : null - await fs.writeFile( - path.join(runtimeDir, 'app.js'), - getEntryModule({reactStrictMode, relativeConfigLocation, basePath}), - ) + await fs.writeFile( + path.join(runtimeDir, 'app.js'), + getEntryModule({reactStrictMode, relativeConfigLocation, basePath}), + ) + } } From 9773a369ec2ad7181dd503b9a6d806c17847e524 Mon Sep 17 00:00:00 2001 From: Carolina Gonzalez Date: Tue, 28 Jan 2025 14:40:26 -0500 Subject: [PATCH 2/5] refactor: align core-app structure and start-up more closely with studio --- packages/@sanity/cli/.depcheckrc.json | 1 - .../init-project/bootstrapLocalTemplate.ts | 23 ++++--- .../actions/init-project/createCliConfig.ts | 52 ++------------ .../init-project/createCoreAppCliConfig.ts | 24 +++++++ .../init-project/createStudioConfig.ts | 31 ++------- .../src/actions/init-project/initProject.ts | 23 ++++++- .../actions/init-project/processTemplate.ts | 55 +++++++++++++++ .../actions/init-project/templates/coreApp.ts | 15 ++++- packages/@sanity/cli/src/types.ts | 10 ++- .../cli/templates/core-app/src/App.tsx | 17 +++-- .../cli/templates/core-app/src/main.tsx | 9 --- packages/@sanity/cli/test/init.test.ts | 67 ++++++++++--------- .../cli/actions/build/buildAction.ts | 10 ++- .../_internal/cli/commands/dev/devCommand.ts | 22 ------ .../cli/commands/start/startCommand.ts | 26 ++++--- .../_internal/cli/server/buildStaticFiles.ts | 14 +++- .../src/_internal/cli/server/devServer.ts | 30 ++------- .../_internal/cli/server/getEntryModule.ts | 27 +++++++- .../src/_internal/cli/server/getViteConfig.ts | 64 ++++++++---------- .../src/_internal/cli/server/previewServer.ts | 7 +- .../_internal/cli/server/renderDocument.tsx | 29 +++++--- .../src/_internal/cli/server/runtime.ts | 37 +++++----- .../vite/plugin-sanity-build-entries.ts | 5 +- .../sanity/src/_internal/cli/util/servers.ts | 7 ++ .../src/core/components/BasicDocument.tsx | 49 ++++++++++++++ packages/sanity/src/core/components/index.ts | 1 + 26 files changed, 390 insertions(+), 265 deletions(-) create mode 100644 packages/@sanity/cli/src/actions/init-project/createCoreAppCliConfig.ts create mode 100644 packages/@sanity/cli/src/actions/init-project/processTemplate.ts delete mode 100644 packages/@sanity/cli/templates/core-app/src/main.tsx create mode 100644 packages/sanity/src/core/components/BasicDocument.tsx diff --git a/packages/@sanity/cli/.depcheckrc.json b/packages/@sanity/cli/.depcheckrc.json index aaf2599c229..275fe891d83 100644 --- a/packages/@sanity/cli/.depcheckrc.json +++ b/packages/@sanity/cli/.depcheckrc.json @@ -8,7 +8,6 @@ "vite", "@portabletext/toolkit", "react", - "react-dom", "@sanity/ui", "lodash.get", "@portabletext/types", diff --git a/packages/@sanity/cli/src/actions/init-project/bootstrapLocalTemplate.ts b/packages/@sanity/cli/src/actions/init-project/bootstrapLocalTemplate.ts index edcda565b7b..e2bf0135851 100644 --- a/packages/@sanity/cli/src/actions/init-project/bootstrapLocalTemplate.ts +++ b/packages/@sanity/cli/src/actions/init-project/bootstrapLocalTemplate.ts @@ -10,6 +10,7 @@ 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 {determineStudioTemplate} from './determineStudioTemplate' @@ -82,8 +83,8 @@ 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, + ...(isStudioTemplate ? studioDependencies.dependencies : {}), + ...(isStudioTemplate ? studioDependencies.devDependencies : {}), ...(template.dependencies || {}), ...(template.devDependencies || {}), }) @@ -91,7 +92,7 @@ export async function bootstrapLocalTemplate( // Use the resolved version for the given dependency const dependencies = Object.keys({ - ...studioDependencies.dependencies, + ...(isStudioTemplate ? studioDependencies.dependencies : {}), ...template.dependencies, }).reduce( (deps, dependency) => { @@ -102,7 +103,7 @@ export async function bootstrapLocalTemplate( ) const devDependencies = Object.keys({ - ...studioDependencies.devDependencies, + ...(isStudioTemplate ? studioDependencies.devDependencies : {}), ...template.devDependencies, }).reduce( (deps, dependency) => { @@ -121,17 +122,19 @@ export async function bootstrapLocalTemplate( }) // ...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 = isStudioTemplate + ? createCliConfig({ + projectId: variables.projectId, + dataset: variables.dataset, + autoUpdates: variables.autoUpdates, + }) + : createCoreAppCliConfig({appLocation: template.appLocation!}) // Write non-template files to disc const codeExt = useTypeScript ? 'ts' : 'js' diff --git a/packages/@sanity/cli/src/actions/init-project/createCliConfig.ts b/packages/@sanity/cli/src/actions/init-project/createCliConfig.ts index abd1e5c8c92..caa6bbb30d5 100644 --- a/packages/@sanity/cli/src/actions/init-project/createCliConfig.ts +++ b/packages/@sanity/cli/src/actions/init-project/createCliConfig.ts @@ -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' @@ -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 } diff --git a/packages/@sanity/cli/src/actions/init-project/createCoreAppCliConfig.ts b/packages/@sanity/cli/src/actions/init-project/createCoreAppCliConfig.ts new file mode 100644 index 00000000000..7211efff4c3 --- /dev/null +++ b/packages/@sanity/cli/src/actions/init-project/createCoreAppCliConfig.ts @@ -0,0 +1,24 @@ +import {processTemplate} from './processTemplate' + +const defaultCoreAppTemplate = ` +import {defineCliConfig} from 'sanity/cli' + +export default defineCliConfig({ + __experimental_coreAppConfiguration: { + framework: 'vite', + appLocation: '%appLocation%' + }, +}) +` + +export interface GenerateCliConfigOptions { + organizationId?: string + appLocation: string +} + +export function createCoreAppCliConfig(options: GenerateCliConfigOptions): string { + return processTemplate({ + template: defaultCoreAppTemplate, + variables: options, + }) +} diff --git a/packages/@sanity/cli/src/actions/init-project/createStudioConfig.ts b/packages/@sanity/cli/src/actions/init-project/createStudioConfig.ts index c5d29795ec6..aae87453e9e 100644 --- a/packages/@sanity/cli/src/actions/init-project/createStudioConfig.ts +++ b/packages/@sanity/cli/src/actions/init-project/createStudioConfig.ts @@ -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' @@ -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 } diff --git a/packages/@sanity/cli/src/actions/init-project/initProject.ts b/packages/@sanity/cli/src/actions/init-project/initProject.ts index e100bed2e2a..824b1d3eea0 100644 --- a/packages/@sanity/cli/src/actions/init-project/initProject.ts +++ b/packages/@sanity/cli/src/actions/init-project/initProject.ts @@ -49,6 +49,7 @@ import {createProject} from '../project/createProject' import {bootstrapLocalTemplate} from './bootstrapLocalTemplate' import {bootstrapRemoteTemplate} from './bootstrapRemoteTemplate' import {type GenerateConfigOptions} from './createStudioConfig' +import {determineStudioTemplate} from './determineStudioTemplate' import {absolutify, validateEmptyPath} from './fsUtils' import {tryGitInit} from './git' import {promptForDatasetName} from './promptForDatasetName' @@ -97,6 +98,7 @@ export interface ProjectTemplate { importPrompt?: string configTemplate?: string | ((variables: GenerateConfigOptions['variables']) => string) typescriptOnly?: boolean + appLocation?: string } export interface ProjectOrganization { @@ -271,6 +273,9 @@ export default async function initSanity( print('') const flags = await prepareFlags() + // skip project / dataset prompting + const isStudioTemplate = cliFlags.template ? determineStudioTemplate(cliFlags.template) : true // Default to true + // We're authenticated, now lets select or create a project const {projectId, displayName, isFirstProject, datasetName, schemaUrl} = await getProjectDetails() @@ -655,11 +660,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 ${isStudioTemplate ? 'Sanity Studio' : 'your Sanity application'}\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 ${isStudioTemplate ? 'Sanity Studio' : 'your Sanity application'}\n`, + ) } print(`Other helpful commands`) @@ -720,6 +729,16 @@ export default async function initSanity( return data } + // For non-studio templates, return empty strings but maintain the structure + if (!isStudioTemplate) { + 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`) diff --git a/packages/@sanity/cli/src/actions/init-project/processTemplate.ts b/packages/@sanity/cli/src/actions/init-project/processTemplate.ts new file mode 100644 index 00000000000..9d7705e7c06 --- /dev/null +++ b/packages/@sanity/cli/src/actions/init-project/processTemplate.ts @@ -0,0 +1,55 @@ +import traverse from '@babel/traverse' +import {parse, print} from 'recast' +import * as parser from 'recast/parsers/typescript' + +interface TemplateOptions { + template: string + variables: T + includeBooleanTransform?: boolean +} + +export function processTemplate(options: TemplateOptions): 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 +} diff --git a/packages/@sanity/cli/src/actions/init-project/templates/coreApp.ts b/packages/@sanity/cli/src/actions/init-project/templates/coreApp.ts index f1479c09435..11c89bc99e7 100644 --- a/packages/@sanity/cli/src/actions/init-project/templates/coreApp.ts +++ b/packages/@sanity/cli/src/actions/init-project/templates/coreApp.ts @@ -4,11 +4,24 @@ const coreAppTemplate: ProjectTemplate = { dependencies: { '@sanity/sdk': '^0.0.0-alpha', '@sanity/sdk-react': '^0.0.0-alpha', + 'react': '^19', + 'react-dom': '^19', }, devDependencies: { - 'vite': '^6', + /* + * this will be changed to eslint-config sanity, + * eslint.config generation will be a fast follow + */ + '@sanity/eslint-config-studio': '^5.0.1', + 'sanity': '^3', '@vitejs/plugin-react': '^4.3.4', + '@types/react': '^18.0.25', + 'eslint': '^9.9.0', + 'prettier': '^3.0.2', + 'typescript': '^5.1.6', + 'vite': '^6', }, + appLocation: './src/App.tsx', } export default coreAppTemplate diff --git a/packages/@sanity/cli/src/types.ts b/packages/@sanity/cli/src/types.ts index 45262ff18b8..b277ad89439 100644 --- a/packages/@sanity/cli/src/types.ts +++ b/packages/@sanity/cli/src/types.ts @@ -345,7 +345,15 @@ export interface CliConfig { studioHost?: string - isStudioApp?: boolean + /** + * Parameter used to configure other kinds of applications. + * Signals to `sanity` commands that this is not a studio. + * @internal + */ + __experimental_coreAppConfiguration?: { + framework: 'vite' // add others as we make them available, e.g., 'vite' | 'vue' + appLocation?: string + } } export type UserViteConfig = diff --git a/packages/@sanity/cli/templates/core-app/src/App.tsx b/packages/@sanity/cli/templates/core-app/src/App.tsx index 68c7d732bd4..7608e84a201 100644 --- a/packages/@sanity/cli/templates/core-app/src/App.tsx +++ b/packages/@sanity/cli/templates/core-app/src/App.tsx @@ -1,22 +1,25 @@ -import {SanityApp} from '@sanity/sdk-react/components' +import {createSanityInstance} from '@sanity/sdk' +import {SanityProvider} from '@sanity/sdk-react/context' export function App() { const sanityConfig = { + auth: { + authScope: 'global' + } /* - * CORE apps can access several different projects! + * Apps can access several different projects! * Add the below configuration if you want to connect to a specific project. */ // projectId: 'my-project-id', // dataset: 'my-dataset', - auth: { - authScope: 'org' - } } + + const sanityInstance = createSanityInstance(sanityConfig) return ( - + Hello world! - + ) } diff --git a/packages/@sanity/cli/templates/core-app/src/main.tsx b/packages/@sanity/cli/templates/core-app/src/main.tsx deleted file mode 100644 index 4aff0256e00..00000000000 --- a/packages/@sanity/cli/templates/core-app/src/main.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { StrictMode } from 'react' -import { createRoot } from 'react-dom/client' -import App from './App.tsx' - -createRoot(document.getElementById('root')!).render( - - - , -) diff --git a/packages/@sanity/cli/test/init.test.ts b/packages/@sanity/cli/test/init.test.ts index 08ec7960886..69469bb5b85 100644 --- a/packages/@sanity/cli/test/init.test.ts +++ b/packages/@sanity/cli/test/init.test.ts @@ -3,42 +3,47 @@ import path from 'node:path' import {describe, expect} from 'vitest' +import {determineStudioTemplate} from '../src/actions/init-project/determineStudioTemplate' import templates from '../src/actions/init-project/templates' import {describeCliTest, testConcurrent} from './shared/describe' import {baseTestPath, cliProjectId, getTestRunArgs, runSanityCmdCommand} from './shared/environment' describeCliTest('CLI: `sanity init v3`', () => { - describe.each(Object.keys(templates))('for template %s', (template) => { - testConcurrent('adds autoUpdates: true to cli config', async () => { - const version = 'v3' - const testRunArgs = getTestRunArgs(version) - const outpath = `test-template-${template}-${version}` - - await runSanityCmdCommand(version, [ - 'init', - '--y', - '--project', - cliProjectId, - '--dataset', - testRunArgs.dataset, - '--template', - template, - '--output-path', - `${baseTestPath}/${outpath}`, - '--package-manager', - 'manual', - ]) - - const cliConfig = await fs.readFile( - path.join(baseTestPath, outpath, 'sanity.cli.ts'), - 'utf-8', - ) - - expect(cliConfig).toContain(`projectId: '${cliProjectId}'`) - expect(cliConfig).toContain(`dataset: '${testRunArgs.dataset}'`) - expect(cliConfig).toContain(`autoUpdates: true`) - }) - }) + // filter out non-studio apps for now, until we add things they can auto-update + describe.each(Object.keys(templates).filter(determineStudioTemplate))( + 'for template %s', + (template) => { + testConcurrent('adds autoUpdates: true to cli config', async () => { + const version = 'v3' + const testRunArgs = getTestRunArgs(version) + const outpath = `test-template-${template}-${version}` + + await runSanityCmdCommand(version, [ + 'init', + '--y', + '--project', + cliProjectId, + '--dataset', + testRunArgs.dataset, + '--template', + template, + '--output-path', + `${baseTestPath}/${outpath}`, + '--package-manager', + 'manual', + ]) + + const cliConfig = await fs.readFile( + path.join(baseTestPath, outpath, 'sanity.cli.ts'), + 'utf-8', + ) + + expect(cliConfig).toContain(`projectId: '${cliProjectId}'`) + expect(cliConfig).toContain(`dataset: '${testRunArgs.dataset}'`) + expect(cliConfig).toContain(`autoUpdates: true`) + }) + }, + ) testConcurrent('adds autoUpdates: true to cli config for javascript projects', async () => { const version = 'v3' diff --git a/packages/sanity/src/_internal/cli/actions/build/buildAction.ts b/packages/sanity/src/_internal/cli/actions/build/buildAction.ts index 24e105335b5..d7829c951af 100644 --- a/packages/sanity/src/_internal/cli/actions/build/buildAction.ts +++ b/packages/sanity/src/_internal/cli/actions/build/buildAction.ts @@ -57,6 +57,7 @@ export default async function buildSanityStudio( } const autoUpdatesEnabled = shouldAutoUpdate({flags, cliConfig}) + const isStudioApp = !(cliConfig && '__experimental_coreAppConfiguration' in cliConfig) // Get the version without any tags if any const coercedSanityVersion = semver.coerce(installedSanityVersion)?.version @@ -146,7 +147,7 @@ export default async function buildSanityStudio( spin.succeed() } - spin = output.spinner('Build Sanity Studio').start() + spin = output.spinner(`Build Sanity ${isStudioApp ? 'Studio' : 'application'}`).start() const trace = telemetry.trace(BuildTrace) trace.start() @@ -175,6 +176,11 @@ export default async function buildSanityStudio( importMap, reactCompiler: cliConfig && 'reactCompiler' in cliConfig ? cliConfig.reactCompiler : undefined, + appLocation: + cliConfig && '__experimental_coreAppConfiguration' in cliConfig + ? cliConfig.__experimental_coreAppConfiguration?.appLocation + : undefined, + isStudioApp, }) trace.log({ @@ -184,7 +190,7 @@ export default async function buildSanityStudio( }) const buildDuration = timer.end('bundleStudio') - spin.text = `Build Sanity Studio (${buildDuration.toFixed()}ms)` + spin.text = `Build Sanity ${isStudioApp ? 'Studio' : 'application'} (${buildDuration.toFixed()}ms)` spin.succeed() trace.complete() diff --git a/packages/sanity/src/_internal/cli/commands/dev/devCommand.ts b/packages/sanity/src/_internal/cli/commands/dev/devCommand.ts index 480b6f0f34f..34e3c807203 100644 --- a/packages/sanity/src/_internal/cli/commands/dev/devCommand.ts +++ b/packages/sanity/src/_internal/cli/commands/dev/devCommand.ts @@ -1,14 +1,10 @@ -import path from 'node:path' - import { type CliCommandArguments, type CliCommandContext, type CliCommandDefinition, - type CliConfig, } from '@sanity/cli' import {type StartDevServerCommandFlags} from '../../actions/dev/devAction' -import {startDevServer} from '../../server' const helpText = ` Notes @@ -31,24 +27,6 @@ const devCommand: CliCommandDefinition = { args: CliCommandArguments, context: CliCommandContext, ) => { - const {workDir, cliConfig} = context - - // if not studio app, skip all Studio-specific initialization - if (!(cliConfig && 'isStudioApp' in cliConfig)) { - // non-studio apps were not possible in v2 - const config = cliConfig as CliConfig | undefined - return startDevServer({ - cwd: workDir, - basePath: '/', - staticPath: path.join(workDir, 'static'), - httpPort: Number(args.extOptions?.port) || 3333, - httpHost: args.extOptions?.host, - reactStrictMode: true, - reactCompiler: config?.reactCompiler, - vite: config?.vite, - isStudioApp: false, - }) - } const devAction = await getDevAction() return devAction(args, context) diff --git a/packages/sanity/src/_internal/cli/commands/start/startCommand.ts b/packages/sanity/src/_internal/cli/commands/start/startCommand.ts index dc3504dc5ab..d571c0d3dfe 100644 --- a/packages/sanity/src/_internal/cli/commands/start/startCommand.ts +++ b/packages/sanity/src/_internal/cli/commands/start/startCommand.ts @@ -32,20 +32,24 @@ const startCommand: CliCommandDefinition = { ) => { const {output, chalk, prompt} = context const previewAction = await getPreviewAction() + const {cliConfig} = context + const isStudioApp = !(cliConfig && '__experimental_coreAppConfiguration' in cliConfig) const warn = (msg: string) => output.warn(chalk.yellow.bgBlack(msg)) const error = (msg: string) => output.warn(chalk.red.bgBlack(msg)) - warn('╭───────────────────────────────────────────────────────────╮') - warn('│ │') - warn("│ You're running Sanity Studio v3. In this version the │") - warn('│ [start] command is used to preview static builds. |') - warn('│ │') - warn('│ To run a development server, use the [npm run dev] or |') - warn('│ [npx sanity dev] command instead. For more information, │') - warn('│ see https://www.sanity.io/help/studio-v2-vs-v3 │') - warn('│ │') - warn('╰───────────────────────────────────────────────────────────╯') - warn('') // Newline to separate from other output + if (isStudioApp) { + warn('╭───────────────────────────────────────────────────────────╮') + warn('│ │') + warn("│ You're running Sanity Studio v3. In this version the │") + warn('│ [start] command is used to preview static builds. |') + warn('│ │') + warn('│ To run a development server, use the [npm run dev] or |') + warn('│ [npx sanity dev] command instead. For more information, │') + warn('│ see https://www.sanity.io/help/studio-v2-vs-v3 │') + warn('│ │') + warn('╰───────────────────────────────────────────────────────────╯') + warn('') // Newline to separate from other output + } try { await previewAction(args, context) diff --git a/packages/sanity/src/_internal/cli/server/buildStaticFiles.ts b/packages/sanity/src/_internal/cli/server/buildStaticFiles.ts index 1b500c2327e..1ab61852bc1 100644 --- a/packages/sanity/src/_internal/cli/server/buildStaticFiles.ts +++ b/packages/sanity/src/_internal/cli/server/buildStaticFiles.ts @@ -34,6 +34,8 @@ export interface StaticBuildOptions { vite?: UserViteConfig reactCompiler: ReactCompilerConfig | undefined + appLocation?: string + isStudioApp?: boolean } export async function buildStaticFiles( @@ -48,10 +50,19 @@ export async function buildStaticFiles( vite: extendViteConfig, importMap, reactCompiler, + appLocation, + isStudioApp = true, } = options debug('Writing Sanity runtime files') - await writeSanityRuntime({cwd, reactStrictMode: false, watch: false, basePath}) + await writeSanityRuntime({ + cwd, + reactStrictMode: false, + watch: false, + basePath, + appLocation, + isStudioApp, + }) debug('Resolving vite config') const mode = 'production' @@ -64,6 +75,7 @@ export async function buildStaticFiles( mode, importMap, reactCompiler, + isStudioApp, }) // Extend Vite configuration with user-provided config diff --git a/packages/sanity/src/_internal/cli/server/devServer.ts b/packages/sanity/src/_internal/cli/server/devServer.ts index 2919ef34ad8..fce0cb2f806 100644 --- a/packages/sanity/src/_internal/cli/server/devServer.ts +++ b/packages/sanity/src/_internal/cli/server/devServer.ts @@ -1,5 +1,3 @@ -import path from 'node:path' - import {type ReactCompilerConfig, type UserViteConfig} from '@sanity/cli' import chalk from 'chalk' @@ -19,6 +17,7 @@ export interface DevServerOptions { reactStrictMode: boolean reactCompiler: ReactCompilerConfig | undefined vite?: UserViteConfig + appLocation?: string isStudioApp?: boolean } @@ -35,15 +34,17 @@ export async function startDevServer(options: DevServerOptions): Promise basePath: rawBasePath = '/', importMap, reactCompiler, - isStudioApp = true, // default to true for backwards compatibility + isStudioApp = true, } = options - // we may eventually load an example sdk app in the monorepo, but there's none right now - const monorepo = isStudioApp ? await loadSanityMonorepo(cwd) : undefined + const monorepo = await loadSanityMonorepo(cwd) const basePath = normalizeBasePath(rawBasePath) - const runtimeDir = path.join(cwd, '.sanity', 'runtime') - const sanityPkgPath = isStudioApp ? (await readPkgUp({cwd: __dirname}))?.path : null - if (isStudioApp && !sanityPkgPath) { + const sanityPkgPath = (await readPkgUp({cwd: __dirname}))?.path + if (!sanityPkgPath) { throw new Error('Unable to resolve `sanity` module root') } + const customFaviconsPath = path.join(cwd, 'static') + const defaultFaviconsPath = path.join(path.dirname(sanityPkgPath), 'static', 'favicons') + const staticPath = `${basePath}static` + const {default: viteReact} = await import('@vitejs/plugin-react') const viteConfig: InlineConfig = { - root: runtimeDir, - base: basePath, + // Define a custom cache directory so that sanity's vite cache + // does not conflict with any potential local vite projects cacheDir: 'node_modules/.sanity/vite', + root: cwd, + base: basePath, build: { outDir: outputDir || path.resolve(cwd, 'dist'), sourcemap: sourceMap, @@ -103,39 +107,22 @@ export async function getViteConfig(options: ViteOptions): Promise configFile: false, mode, plugins: [ - (await import('@vitejs/plugin-react')).default( + viteReact( reactCompiler ? {babel: {plugins: [['babel-plugin-react-compiler', reactCompiler]]}} : {}, ), - ], - resolve: { - alias: { - // Map /src to the actual source directory - '/src': path.join(cwd, 'src'), - }, - }, - appType: 'spa', - } - - // Add Studio-specific configuration - if (isStudioApp) { - const customFaviconsPath = path.join(cwd, 'static') - const defaultFaviconsPath = path.join(path.dirname(sanityPkgPath!), 'static', 'favicons') - const staticPath = `${basePath}static` - - viteConfig.plugins!.push( sanityFaviconsPlugin({defaultFaviconsPath, customFaviconsPath, staticUrlPath: staticPath}), sanityRuntimeRewritePlugin(), - sanityBuildEntries({basePath, cwd, monorepo, importMap}), - ) - - viteConfig.resolve = { + sanityBuildEntries({basePath, cwd, monorepo, importMap, isStudioApp}), + ], + envPrefix: isStudioApp ? 'SANITY_STUDIO_' : 'VITE_', + logLevel: mode === 'production' ? 'silent' : 'info', + resolve: { alias: monorepo?.path ? await getMonorepoAliases(monorepo.path) - : getSanityPkgExportAliases(sanityPkgPath!), + : getSanityPkgExportAliases(sanityPkgPath), dedupe: ['styled-components'], - } - - viteConfig.define = { + }, + define: { // eslint-disable-next-line no-process-env '__SANITY_STAGING__': process.env.SANITY_INTERNAL_ENV === 'staging', 'process.env.MODE': JSON.stringify(mode), @@ -150,20 +137,23 @@ export async function getViteConfig(options: ViteOptions): Promise */ 'process.env.SC_DISABLE_SPEEDY': JSON.stringify('false'), ...getStudioEnvironmentVariables({prefix: 'process.env.', jsonEncode: true}), - } + }, } if (mode === 'production') { viteConfig.build = { ...viteConfig.build, + assetsDir: 'static', minify: minify ? 'esbuild' : false, emptyOutDir: false, // Rely on CLI to do this rollupOptions: { onwarn: onRollupWarn, - external: isStudioApp ? createExternalFromImportMap(importMap) : undefined, - input: isStudioApp ? {sanity: path.join(cwd, '.sanity', 'runtime', 'app.js')} : undefined, + external: createExternalFromImportMap(importMap), + input: { + sanity: path.join(cwd, '.sanity', 'runtime', 'app.js'), + }, }, } } diff --git a/packages/sanity/src/_internal/cli/server/previewServer.ts b/packages/sanity/src/_internal/cli/server/previewServer.ts index bc5b23bf554..1fed6386150 100644 --- a/packages/sanity/src/_internal/cli/server/previewServer.ts +++ b/packages/sanity/src/_internal/cli/server/previewServer.ts @@ -24,10 +24,11 @@ export interface PreviewServerOptions { httpHost?: string vite?: UserViteConfig + isStudioApp?: boolean } export async function startPreviewServer(options: PreviewServerOptions): Promise { - const {httpPort, httpHost, root, vite: extendViteConfig} = options + const {httpPort, httpHost, root, vite: extendViteConfig, isStudioApp = true} = options const startTime = Date.now() const indexPath = path.join(root, 'index.html') @@ -41,7 +42,7 @@ export async function startPreviewServer(options: PreviewServerOptions): Promise } const error = new Error( - `Could not find a production build in the '${root}' directory.\nTry building your studio app with 'sanity build' before starting the preview server.`, + `Could not find a production build in the '${root}' directory.\nTry building your ${isStudioApp ? 'studio ' : ''}app with 'sanity build' before starting the preview server.`, ) error.name = 'BUILD_NOT_FOUND' throw error @@ -90,7 +91,7 @@ export async function startPreviewServer(options: PreviewServerOptions): Promise const startupDuration = Date.now() - startTime info( - `Sanity Studio ` + + `Sanity ${isStudioApp ? 'Studio' : 'application'} ` + `using ${chalk.cyan(`vite@${require('vite/package.json').version}`)} ` + `ready in ${chalk.cyan(`${Math.ceil(startupDuration)}ms`)} ` + `and running at ${chalk.cyan(url)} (production preview mode)`, diff --git a/packages/sanity/src/_internal/cli/server/renderDocument.tsx b/packages/sanity/src/_internal/cli/server/renderDocument.tsx index 5738df9d8bc..9049e366d9b 100644 --- a/packages/sanity/src/_internal/cli/server/renderDocument.tsx +++ b/packages/sanity/src/_internal/cli/server/renderDocument.tsx @@ -158,8 +158,13 @@ function renderDocumentFromWorkerData() { throw new Error('Must be used as a Worker with a valid options object in worker data') } - const {monorepo, studioRootPath, props, importMap, isStudioApp}: RenderDocumentOptions = - workerData || {} + const { + monorepo, + studioRootPath, + props, + importMap, + isStudioApp = true, + }: RenderDocumentOptions = workerData || {} if (workerData?.dev) { // Define `__DEV__` in the worker thread as well @@ -222,9 +227,9 @@ function getDocumentHtml( studioRootPath: string, props?: DocumentProps, importMap?: {imports?: Record}, - isStudioApp?: boolean, + isStudioApp = true, ): string { - const Document = getDocumentComponent(studioRootPath) + const Document = getDocumentComponent(studioRootPath, isStudioApp) // NOTE: Validate the list of CSS paths so implementers of `_document.tsx` don't have to // - If the path is not a full URL, check if it starts with `/` @@ -244,10 +249,7 @@ function getDocumentHtml( importMap, ) - // Only modify the root element ID for non-Studio apps - const rootElementId = isStudioApp ? 'sanity' : 'root' - // TODO: actually intervene using html methods - return `${result.replace('id="sanity"', `id="${rootElementId}"`)}` + return `${result}` } /** @@ -283,18 +285,25 @@ export function addTimestampedImportMapScriptToHtml( return root.outerHTML } -function getDocumentComponent(studioRootPath: string) { +function getDocumentComponent(studioRootPath: string, isStudioApp = true) { debug('Loading default document component from `sanity` module') + + const {BasicDocument} = __DEV__ + ? require('../../../core/components/BasicDocument') + : require('sanity') + const {DefaultDocument} = __DEV__ ? require('../../../core/components/DefaultDocument') : require('sanity') + const Document = isStudioApp ? DefaultDocument : BasicDocument + debug('Attempting to load user-defined document component from %s', studioRootPath) const userDefined = tryLoadDocumentComponent(studioRootPath) if (!userDefined) { debug('Using default document component') - return DefaultDocument + return Document } debug('Found user defined document component at %s', userDefined.path) diff --git a/packages/sanity/src/_internal/cli/server/runtime.ts b/packages/sanity/src/_internal/cli/server/runtime.ts index b2eea42ea07..0ca2a251117 100644 --- a/packages/sanity/src/_internal/cli/server/runtime.ts +++ b/packages/sanity/src/_internal/cli/server/runtime.ts @@ -20,6 +20,7 @@ export interface RuntimeOptions { reactStrictMode: boolean watch: boolean basePath?: string + appLocation?: string isStudioApp?: boolean } @@ -35,14 +36,15 @@ export async function writeSanityRuntime({ reactStrictMode, watch, basePath, + appLocation, isStudioApp = true, }: RuntimeOptions): Promise { - debug('Making runtime directory') + debug('Resolving Sanity monorepo information') + const monorepo = await loadSanityMonorepo(cwd) const runtimeDir = path.join(cwd, '.sanity', 'runtime') - await fs.mkdir(runtimeDir, {recursive: true}) - // Only load monorepo info for Studio apps - const monorepo = isStudioApp ? await loadSanityMonorepo(cwd) : undefined + debug('Making runtime directory') + await fs.mkdir(runtimeDir, {recursive: true}) async function renderAndWriteDocument() { debug('Rendering document template') @@ -51,9 +53,7 @@ export async function writeSanityRuntime({ studioRootPath: cwd, monorepo, props: { - entryPath: isStudioApp - ? `/${path.relative(cwd, path.join(runtimeDir, 'app.js'))}` - : '/src/main.tsx', // TODO: change to be more like studio, possibly dyanmic + entryPath: `/${path.relative(cwd, path.join(runtimeDir, 'app.js'))}`, basePath: basePath || '/', }, isStudioApp, @@ -72,17 +72,20 @@ export async function writeSanityRuntime({ await renderAndWriteDocument() - // Only generate app.js for Studio apps + debug('Writing app.js to runtime directory') + let relativeConfigLocation: string | null = null if (isStudioApp) { - debug('Writing app.js to runtime directory') const studioConfigPath = await getSanityStudioConfigPath(cwd) - const relativeConfigLocation = studioConfigPath - ? path.relative(runtimeDir, studioConfigPath) - : null - - await fs.writeFile( - path.join(runtimeDir, 'app.js'), - getEntryModule({reactStrictMode, relativeConfigLocation, basePath}), - ) + relativeConfigLocation = studioConfigPath ? path.relative(runtimeDir, studioConfigPath) : null } + + const relativeAppLocation = cwd ? path.resolve(cwd, appLocation || './src/App') : appLocation + const appJsContent = getEntryModule({ + reactStrictMode, + relativeConfigLocation, + basePath, + appLocation: relativeAppLocation, + isStudioApp, + }) + await fs.writeFile(path.join(runtimeDir, 'app.js'), appJsContent) } diff --git a/packages/sanity/src/_internal/cli/server/vite/plugin-sanity-build-entries.ts b/packages/sanity/src/_internal/cli/server/vite/plugin-sanity-build-entries.ts index 8101f30c02c..e6b6feead0c 100644 --- a/packages/sanity/src/_internal/cli/server/vite/plugin-sanity-build-entries.ts +++ b/packages/sanity/src/_internal/cli/server/vite/plugin-sanity-build-entries.ts @@ -19,6 +19,7 @@ interface ViteRenderedChunk { code: string imports: string[] viteMetadata: ChunkMetadata + isEntry: boolean } const entryChunkId = '.sanity/runtime/app.js' @@ -28,8 +29,9 @@ export function sanityBuildEntries(options: { monorepo: SanityMonorepo | undefined basePath: string importMap?: {imports?: Record} + isStudioApp?: boolean }): Plugin { - const {cwd, monorepo, basePath, importMap} = options + const {cwd, monorepo, basePath, importMap, isStudioApp = true} = options return { name: 'sanity/server/build-entries', @@ -93,6 +95,7 @@ export function sanityBuildEntries(options: { entryPath, css, }, + isStudioApp, }), }) }, diff --git a/packages/sanity/src/_internal/cli/util/servers.ts b/packages/sanity/src/_internal/cli/util/servers.ts index b45933d34ed..9b3daa0907a 100644 --- a/packages/sanity/src/_internal/cli/util/servers.ts +++ b/packages/sanity/src/_internal/cli/util/servers.ts @@ -48,6 +48,8 @@ export function getSharedServerConfig({ httpHost: string basePath: string vite: CliConfig['vite'] + appLocation?: string + isStudioApp: boolean } { // Order of preference: CLI flags, environment variables, user build config, default config const env = process.env // eslint-disable-line no-process-env @@ -64,12 +66,17 @@ export function getSharedServerConfig({ env.SANITY_STUDIO_BASEPATH ?? (cliConfig?.project?.basePath || '/'), ) + const isStudioApp = !(cliConfig && '__experimental_coreAppConfiguration' in cliConfig) + const appLocation = cliConfig?.__experimental_coreAppConfiguration?.appLocation + return { cwd: workDir, httpPort, httpHost, basePath, vite: cliConfig?.vite, + appLocation, + isStudioApp, } } diff --git a/packages/sanity/src/core/components/BasicDocument.tsx b/packages/sanity/src/core/components/BasicDocument.tsx new file mode 100644 index 00000000000..6f62ddadb99 --- /dev/null +++ b/packages/sanity/src/core/components/BasicDocument.tsx @@ -0,0 +1,49 @@ +/* eslint-disable i18next/no-literal-string -- title is literal for now */ +import {Favicons} from './Favicons' +import {GlobalErrorHandler} from './globalErrorHandler' +import {NoJavascript} from './NoJavascript' + +/** + * @hidden + * @beta */ +export interface BasicDocumentProps { + entryPath: string + css?: string[] + // Currently unused, but kept for potential future use + // eslint-disable-next-line react/no-unused-prop-types + basePath?: string +} + +const EMPTY_ARRAY: never[] = [] + +/** + * This is the equivalent of DefaultDocument for non-studio apps. + * @hidden + * @beta */ +export function BasicDocument(props: BasicDocumentProps): React.JSX.Element { + const {entryPath, css = EMPTY_ARRAY} = props + + return ( + + + + + + + + + Sanity CORE App + + + {css.map((href) => ( + + ))} + + +
+