Skip to content

Commit

Permalink
feat(cli): add non-studio app template and non-studio dev options
Browse files Browse the repository at this point in the history
  • Loading branch information
cngonzalez committed Jan 31, 2025
1 parent 53b8009 commit 56cee8b
Show file tree
Hide file tree
Showing 13 changed files with 210 additions and 56 deletions.
3 changes: 3 additions & 0 deletions packages/@sanity/cli/.depcheckrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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]
Expand Down Expand Up @@ -84,6 +85,7 @@ export async function bootstrapLocalTemplate(
...studioDependencies.dependencies,
...studioDependencies.devDependencies,
...(template.dependencies || {}),
...(template.devDependencies || {}),
})
spinner.succeed()

Expand Down Expand Up @@ -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}`)
Expand Down
Original file line number Diff line number Diff line change
@@ -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)
}
14 changes: 14 additions & 0 deletions packages/@sanity/cli/src/actions/init-project/templates/coreApp.ts
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -10,6 +11,7 @@ import shopifyOnline from './shopifyOnline'
const templates: Record<string, ProjectTemplate | undefined> = {
blog,
clean,
'core-app': coreAppTemplate,
'get-started': getStartedTemplate,
moviedb,
shopify,
Expand Down
2 changes: 2 additions & 0 deletions packages/@sanity/cli/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,8 @@ export interface CliConfig {
autoUpdates?: boolean

studioHost?: string

isStudioApp?: boolean
}

export type UserViteConfig =
Expand Down
23 changes: 23 additions & 0 deletions packages/@sanity/cli/templates/core-app/src/App.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<SanityApp sanityConfig={sanityConfig}>
Hello world!
</SanityApp>
)
}

export default App
9 changes: 9 additions & 0 deletions packages/@sanity/cli/templates/core-app/src/main.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import App from './App.tsx'

createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>,
)
22 changes: 22 additions & 0 deletions packages/sanity/src/_internal/cli/commands/dev/devCommand.ts
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -27,6 +31,24 @@ const devCommand: CliCommandDefinition = {
args: CliCommandArguments<StartDevServerCommandFlags>,
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)
Expand Down
31 changes: 28 additions & 3 deletions packages/sanity/src/_internal/cli/server/devServer.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import path from 'node:path'

import {type ReactCompilerConfig, type UserViteConfig} from '@sanity/cli'
import chalk from 'chalk'

Expand All @@ -17,6 +19,7 @@ export interface DevServerOptions {
reactStrictMode: boolean
reactCompiler: ReactCompilerConfig | undefined
vite?: UserViteConfig
isStudioApp?: boolean
}

export interface DevServer {
Expand All @@ -32,22 +35,43 @@ export async function startDevServer(options: DevServerOptions): Promise<DevServ
reactStrictMode,
vite: extendViteConfig,
reactCompiler,
isStudioApp = true, // default to true for backwards compatibility
} = options

const startTime = Date.now()

debug('Writing Sanity runtime files')
await writeSanityRuntime({cwd, reactStrictMode, watch: true, basePath})
await writeSanityRuntime({cwd, reactStrictMode, watch: true, basePath, isStudioApp})

debug('Resolving vite config')
const mode = 'development'
let viteConfig = await getViteConfig({
basePath,
mode: 'development',
server: {port: httpPort, host: httpHost},
cwd,
reactCompiler,
isStudioApp,
})

if (isStudioApp) {
debug('Writing Sanity runtime files')
await writeSanityRuntime({cwd, reactStrictMode, watch: true, basePath})
} else {
// For non-Studio apps, we need to set the entry point
viteConfig = {
...viteConfig,
build: {
...viteConfig.build,
rollupOptions: {
input: path.join(cwd, 'src', 'main.tsx'),
},
},
}
}

debug('Resolving vite config')
const mode = 'development'

// Extend Vite configuration with user-provided config
if (extendViteConfig) {
viteConfig = await extendViteConfigWithUserConfig(
Expand All @@ -67,8 +91,9 @@ export async function startDevServer(options: DevServerOptions): Promise<DevServ

const startupDuration = Date.now() - startTime
const url = `http://${httpHost || 'localhost'}:${httpPort || '3333'}${basePath}`
const appType = isStudioApp ? 'Sanity Studio' : 'Application'
info(
`Sanity Studio ` +
`${appType} ` +
`using ${chalk.cyan(`vite@${require('vite/package.json').version}`)} ` +
`ready in ${chalk.cyan(`${Math.ceil(startupDuration)}ms`)} ` +
`and running at ${chalk.cyan(url)}`,
Expand Down
62 changes: 37 additions & 25 deletions packages/sanity/src/_internal/cli/server/getViteConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ export interface ViteOptions {

importMap?: {imports?: Record<string, string>}
reactCompiler: ReactCompilerConfig | undefined
isStudioApp?: boolean
}

/**
Expand All @@ -72,27 +73,24 @@ export async function getViteConfig(options: ViteOptions): Promise<InlineConfig>
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,
Expand All @@ -105,22 +103,39 @@ export async function getViteConfig(options: ViteOptions): Promise<InlineConfig>
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),
Expand All @@ -135,23 +150,20 @@ export async function getViteConfig(options: ViteOptions): Promise<InlineConfig>
*/
'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,
},
}
}
Expand Down
Loading

0 comments on commit 56cee8b

Please sign in to comment.