From bbb2462443b6ce74f880b45301dffeec37cffb9b Mon Sep 17 00:00:00 2001 From: Ash Date: Fri, 19 Apr 2024 16:21:08 +0100 Subject: [PATCH 01/49] feat(sanity): allow `extractSchema` worker to emit schemas for all workspaces --- .../cli/actions/schema/extractAction.ts | 6 ++++ .../_internal/cli/threads/extractSchema.ts | 29 ++++++++++--------- 2 files changed, 22 insertions(+), 13 deletions(-) diff --git a/packages/sanity/src/_internal/cli/actions/schema/extractAction.ts b/packages/sanity/src/_internal/cli/actions/schema/extractAction.ts index 5345491c7d3..0daeb1fe5f6 100644 --- a/packages/sanity/src/_internal/cli/actions/schema/extractAction.ts +++ b/packages/sanity/src/_internal/cli/actions/schema/extractAction.ts @@ -33,6 +33,12 @@ export default async function extractAction( throw new Error('Could not find root directory for `sanity` package') } + if (flags.workspace === undefined) { + throw new Error( + `Multiple workspaces found. Please specify which workspace to use with '--workspace'.`, + ) + } + const workerPath = join( dirname(rootPkgPath), 'lib', diff --git a/packages/sanity/src/_internal/cli/threads/extractSchema.ts b/packages/sanity/src/_internal/cli/threads/extractSchema.ts index 8fb02fc00df..0ae1f762b8f 100644 --- a/packages/sanity/src/_internal/cli/threads/extractSchema.ts +++ b/packages/sanity/src/_internal/cli/threads/extractSchema.ts @@ -34,16 +34,24 @@ async function main() { const workspaces = await getStudioWorkspaces({basePath: opts.workDir}) - const workspace = getWorkspace({workspaces, workspaceName: opts.workspaceName}) - - const schema = extractSchema(workspace.schema, { - enforceRequiredFields: opts.enforceRequiredFields, - }) + const postSchema = (workspace: Workspace): void => { + parentPort?.postMessage({ + schema: extractSchema(workspace.schema, { + enforceRequiredFields: opts.enforceRequiredFields, + }), + } satisfies ExtractSchemaWorkerResult) + } - parentPort?.postMessage({ - schema, - } satisfies ExtractSchemaWorkerResult) + if (opts.workspaceName) { + const workspace = getWorkspace({workspaces, workspaceName: opts.workspaceName}) + postSchema(workspace) + } else { + for (const workspace of workspaces) { + postSchema(workspace) + } + } } finally { + parentPort?.close() cleanup() } } @@ -65,11 +73,6 @@ function getWorkspace({ return workspaces[0] } - if (workspaceName === undefined) { - throw new Error( - `Multiple workspaces found. Please specify which workspace to use with '--workspace'.`, - ) - } const workspace = workspaces.find((w) => w.name === workspaceName) if (!workspace) { throw new Error(`Could not find workspace "${workspaceName}"`) From ca8f4c3c4b5e007b77487b61979478efff832a3b Mon Sep 17 00:00:00 2001 From: Ash Date: Mon, 22 Apr 2024 16:59:15 +0100 Subject: [PATCH 02/49] feat(sanity): include workspace and dataset names when extracting schema --- .../sanity/src/_internal/cli/threads/extractSchema.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/sanity/src/_internal/cli/threads/extractSchema.ts b/packages/sanity/src/_internal/cli/threads/extractSchema.ts index 0ae1f762b8f..61608deef35 100644 --- a/packages/sanity/src/_internal/cli/threads/extractSchema.ts +++ b/packages/sanity/src/_internal/cli/threads/extractSchema.ts @@ -15,7 +15,7 @@ export interface ExtractSchemaWorkerData { } /** @internal */ -export interface ExtractSchemaWorkerResult { +export interface ExtractSchemaWorkerResult extends Pick { schema: ReturnType } @@ -34,8 +34,10 @@ async function main() { const workspaces = await getStudioWorkspaces({basePath: opts.workDir}) - const postSchema = (workspace: Workspace): void => { + const postWorkspace = (workspace: Workspace): void => { parentPort?.postMessage({ + name: workspace.name, + dataset: workspace.dataset, schema: extractSchema(workspace.schema, { enforceRequiredFields: opts.enforceRequiredFields, }), @@ -44,10 +46,10 @@ async function main() { if (opts.workspaceName) { const workspace = getWorkspace({workspaces, workspaceName: opts.workspaceName}) - postSchema(workspace) + postWorkspace(workspace) } else { for (const workspace of workspaces) { - postSchema(workspace) + postWorkspace(workspace) } } } finally { From 4b1053bb13b6227450a13d9dfbccfd9fa909e1a4 Mon Sep 17 00:00:00 2001 From: Ash Date: Mon, 22 Apr 2024 16:49:41 +0100 Subject: [PATCH 03/49] feat(cli): add `manifest` commands --- .../@sanity/cli/src/util/noSuchCommandText.ts | 1 + packages/sanity/package.json | 3 +- .../manifest/extractManifestsAction.ts | 151 ++++++++++++++++++ .../actions/manifest/listManifestsAction.ts | 9 ++ .../src/_internal/cli/commands/index.ts | 6 + .../manifest/extractManifestsCommand.ts | 30 ++++ .../commands/manifest/listManifestsCommand.ts | 30 ++++ .../cli/commands/manifest/manifestGroup.ts | 10 ++ pnpm-lock.yaml | 52 ++++++ 9 files changed, 291 insertions(+), 1 deletion(-) create mode 100644 packages/sanity/src/_internal/cli/actions/manifest/extractManifestsAction.ts create mode 100644 packages/sanity/src/_internal/cli/actions/manifest/listManifestsAction.ts create mode 100644 packages/sanity/src/_internal/cli/commands/manifest/extractManifestsCommand.ts create mode 100644 packages/sanity/src/_internal/cli/commands/manifest/listManifestsCommand.ts create mode 100644 packages/sanity/src/_internal/cli/commands/manifest/manifestGroup.ts diff --git a/packages/@sanity/cli/src/util/noSuchCommandText.ts b/packages/@sanity/cli/src/util/noSuchCommandText.ts index 07b94d6b1ce..4aef2271d04 100644 --- a/packages/@sanity/cli/src/util/noSuchCommandText.ts +++ b/packages/@sanity/cli/src/util/noSuchCommandText.ts @@ -17,6 +17,7 @@ const coreCommands = [ 'exec', 'graphql', 'hook', + 'manifest', 'migration', 'preview', 'schema', diff --git a/packages/sanity/package.json b/packages/sanity/package.json index 751e8a109d3..dbac77e2a2a 100644 --- a/packages/sanity/package.json +++ b/packages/sanity/package.json @@ -263,7 +263,8 @@ "use-hot-module-reload": "^2.0.0", "use-sync-external-store": "^1.2.0", "vite": "^4.5.1", - "yargs": "^17.3.0" + "yargs": "^17.3.0", + "zod": "^3.22.4" }, "devDependencies": { "@jest/expect": "^29.7.0", diff --git a/packages/sanity/src/_internal/cli/actions/manifest/extractManifestsAction.ts b/packages/sanity/src/_internal/cli/actions/manifest/extractManifestsAction.ts new file mode 100644 index 00000000000..aa78b837dd9 --- /dev/null +++ b/packages/sanity/src/_internal/cli/actions/manifest/extractManifestsAction.ts @@ -0,0 +1,151 @@ +import {mkdir, writeFile} from 'node:fs/promises' +import {dirname, join, resolve} from 'node:path' +import {Worker} from 'node:worker_threads' + +import {type CliCommandAction} from '@sanity/cli' +import readPkgUp from 'read-pkg-up' +import z from 'zod' + +import { + type ExtractSchemaWorkerData, + type ExtractSchemaWorkerResult, +} from '../../threads/extractSchema' + +const MANIFEST_FILENAME = 'v1.studiomanifest.json' +const SCHEMA_FILENAME_PREFIX = 'schema-' + +const ManifestSchema = z.object({manifestVersion: z.number()}) + +const ManifestV1WorkspaceSchema = z.object({ + name: z.string(), + dataset: z.string(), + schema: z.string(), +}) + +type ManifestV1Workspace = z.infer + +const ManifestV1Schema = ManifestSchema.extend({ + createdAt: z.date(), + workspaces: z.array(ManifestV1WorkspaceSchema), +}) + +type ManifestV1 = z.infer + +const extractManifests: CliCommandAction = async (_args, context) => { + const {output, workDir, chalk} = context + + const defaultOutputDir = resolve(join(workDir, 'dist')) + // const outputDir = resolve(args.argsWithoutOptions[0] || defaultOutputDir) + const outputDir = resolve(defaultOutputDir) + const staticPath = join(outputDir, 'static') + const path = join(staticPath, MANIFEST_FILENAME) + + const rootPkgPath = readPkgUp.sync({cwd: __dirname})?.path + if (!rootPkgPath) { + throw new Error('Could not find root directory for `sanity` package') + } + + const workerPath = join( + dirname(rootPkgPath), + 'lib', + '_internal', + 'cli', + 'threads', + 'extractSchema.js', + ) + + const spinner = output.spinner({}).start('Extracting manifest') + + // const trace = telemetry.trace(SchemaExtractedTrace) + // trace.start() + + const worker = new Worker(workerPath, { + workerData: { + workDir, + enforceRequiredFields: false, + format: 'groq-type-nodes', + } satisfies ExtractSchemaWorkerData, + // eslint-disable-next-line no-process-env + env: process.env, + }) + + try { + const schemas = await new Promise((resolveSchemas, reject) => { + const schemaBuffer: ExtractSchemaWorkerResult[] = [] + worker.addListener('message', (message) => schemaBuffer.push(message)) + worker.addListener('exit', () => resolveSchemas(schemaBuffer)) + worker.addListener('error', reject) + }) + + spinner.text = `Writing manifest to ${chalk.cyan(path)}` + + await mkdir(staticPath, {recursive: true}) + + const manifestWorkspaces = await externalizeSchemas(schemas, staticPath) + + const manifestV1: ManifestV1 = { + manifestVersion: 1, + createdAt: new Date(), + workspaces: manifestWorkspaces, + } + + // trace.log({ + // schemaAllTypesCount: schema.length, + // schemaDocumentTypesCount: schema.filter((type) => type.type === 'document').length, + // schemaTypesCount: schema.filter((type) => type.type === 'type').length, + // enforceRequiredFields, + // schemaFormat: formatFlag, + // }) + + // const path = flags.path || join(process.cwd(), 'schema.json') + // const path = 'test-manifest.json' + + await writeFile(path, JSON.stringify(manifestV1, null, 2)) + + // trace.complete() + + spinner.succeed('Extracted manifest') + } catch (err) { + // trace.error(err) + spinner.fail('Failed to extract manifest') + throw err + } + + output.print(`Extracted manifest to ${chalk.cyan(path)}`) +} + +export default extractManifests + +function externalizeSchemas( + schemas: ExtractSchemaWorkerResult[], + staticPath: string, +): Promise[]> { + const output = schemas.reduce[]>((workspaces, workspace) => { + return [...workspaces, externalizeSchema(workspace, staticPath)] + }, []) + + return Promise.all(output) +} + +async function externalizeSchema( + workspace: ExtractSchemaWorkerResult, + staticPath: string, +): Promise> { + const encoder = new TextEncoder() + const schemaString = JSON.stringify(workspace.schema, null, 2) + const hash = await crypto.subtle.digest('SHA-1', encoder.encode(schemaString)) + const filename = `${SCHEMA_FILENAME_PREFIX}${hexFromBuffer(hash).slice(0, 8)}.json` + + await writeFile(join(staticPath, filename), schemaString) + + return { + ...workspace, + schema: filename, + } +} + +function hexFromBuffer(buffer: ArrayBuffer): string { + return Array.prototype.map + .call(new Uint8Array(buffer), (x) => `00${x.toString(16)}`.slice(-2)) + .join('') +} diff --git a/packages/sanity/src/_internal/cli/actions/manifest/listManifestsAction.ts b/packages/sanity/src/_internal/cli/actions/manifest/listManifestsAction.ts new file mode 100644 index 00000000000..fdec597f09e --- /dev/null +++ b/packages/sanity/src/_internal/cli/actions/manifest/listManifestsAction.ts @@ -0,0 +1,9 @@ +import {type CliCommandAction} from '@sanity/cli' + +const listManifests: CliCommandAction = async (_args, context) => { + const {output} = context + + output.print('Here are the manifests for this project:') +} + +export default listManifests diff --git a/packages/sanity/src/_internal/cli/commands/index.ts b/packages/sanity/src/_internal/cli/commands/index.ts index e27daee4cce..af739380ceb 100644 --- a/packages/sanity/src/_internal/cli/commands/index.ts +++ b/packages/sanity/src/_internal/cli/commands/index.ts @@ -41,6 +41,9 @@ import hookGroup from './hook/hookGroup' import listHookLogsCommand from './hook/listHookLogsCommand' import listHooksCommand from './hook/listHooksCommand' import printHookAttemptCommand from './hook/printHookAttemptCommand' +import extractManifestsCommand from './manifest/extractManifestsCommand' +import listManifestsCommand from './manifest/listManifestsCommand' +import manifestGroup from './manifest/manifestGroup' import createMigrationCommand from './migration/createMigrationCommand' import listMigrationsCommand from './migration/listMigrationsCommand' import migrationGroup from './migration/migrationGroup' @@ -87,6 +90,9 @@ const commands: (CliCommandDefinition | CliCommandGroupDefinition)[] = [ createHookCommand, migrationGroup, createMigrationCommand, + manifestGroup, + extractManifestsCommand, + listManifestsCommand, runMigrationCommand, listMigrationsCommand, deleteHookCommand, diff --git a/packages/sanity/src/_internal/cli/commands/manifest/extractManifestsCommand.ts b/packages/sanity/src/_internal/cli/commands/manifest/extractManifestsCommand.ts new file mode 100644 index 00000000000..cf6b8741c0d --- /dev/null +++ b/packages/sanity/src/_internal/cli/commands/manifest/extractManifestsCommand.ts @@ -0,0 +1,30 @@ +import {type CliCommandDefinition} from '@sanity/cli' + +// TODO: Switch to lazy import. +import mod from '../../actions/manifest/extractManifestsAction' + +const description = 'Extracts a JSON representation of a Sanity schema within a Studio context.' + +const helpText = ` +**Note**: This command is experimental and subject to change. + +Examples + # Extracts manifests + sanity manifest extract +` + +const extractManifestsCommand: CliCommandDefinition = { + name: 'extract', + group: 'manifest', + signature: '', + description, + helpText, + action: async (args, context) => { + // const mod = await import('../../actions/manifest/extractManifestsAction') + // + // return mod.default(args, context) + return mod(args, context) + }, +} + +export default extractManifestsCommand diff --git a/packages/sanity/src/_internal/cli/commands/manifest/listManifestsCommand.ts b/packages/sanity/src/_internal/cli/commands/manifest/listManifestsCommand.ts new file mode 100644 index 00000000000..34b65655129 --- /dev/null +++ b/packages/sanity/src/_internal/cli/commands/manifest/listManifestsCommand.ts @@ -0,0 +1,30 @@ +import {type CliCommandDefinition} from '@sanity/cli' + +// TODO: Switch to lazy import. +import mod from '../../actions/manifest/listManifestsAction' + +const description = 'TODO' + +const helpText = ` +**Note**: This command is experimental and subject to change. + +Examples + # Lists manifests that have been extracted + sanity manifest list +` + +const listManifestsCommand: CliCommandDefinition = { + name: 'list', + group: 'manifest', + signature: '', + description, + helpText, + action: async (args, context) => { + // const mod = await import('../../actions/manifest/listManifestsAction') + // + // return mod.default(args, context) + return mod(args, context) + }, +} + +export default listManifestsCommand diff --git a/packages/sanity/src/_internal/cli/commands/manifest/manifestGroup.ts b/packages/sanity/src/_internal/cli/commands/manifest/manifestGroup.ts new file mode 100644 index 00000000000..2bc68864241 --- /dev/null +++ b/packages/sanity/src/_internal/cli/commands/manifest/manifestGroup.ts @@ -0,0 +1,10 @@ +import {type CliCommandGroupDefinition} from '@sanity/cli' + +const manifestGroup: CliCommandGroupDefinition = { + name: 'manifest', + signature: '[COMMAND]', + isGroupRoot: true, + description: 'TODO', +} + +export default manifestGroup diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cf1d24443e4..aeab975dc88 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1039,6 +1039,55 @@ importers: specifier: ^3.0.2 version: 3.0.2 + packages/@sanity/manifest: + dependencies: + '@sanity/generate-help-url': + specifier: ^3.0.0 + version: 3.0.0 + '@sanity/types': + specifier: 3.38.0 + version: link:../types + arrify: + specifier: ^1.0.1 + version: 1.0.1 + groq-js: + specifier: ^1.7.0 + version: 1.7.0 + humanize-list: + specifier: ^1.0.1 + version: 1.0.1 + leven: + specifier: ^3.1.0 + version: 3.1.0 + lodash: + specifier: ^4.17.21 + version: 4.17.21 + object-inspect: + specifier: ^1.13.1 + version: 1.13.1 + devDependencies: + '@jest/globals': + specifier: ^29.7.0 + version: 29.7.0 + '@repo/package.config': + specifier: workspace:* + version: link:../../@repo/package.config + '@sanity/icons': + specifier: ^2.11.8 + version: 2.11.8(react@18.2.0) + '@types/arrify': + specifier: ^1.0.4 + version: 1.0.4 + '@types/object-inspect': + specifier: ^1.13.0 + version: 1.13.0 + '@types/react': + specifier: ^18.2.78 + version: 18.2.78 + rimraf: + specifier: ^3.0.2 + version: 3.0.2 + packages/@sanity/migrate: dependencies: '@sanity/client': @@ -1695,6 +1744,9 @@ importers: yargs: specifier: ^17.3.0 version: 17.7.2 + zod: + specifier: ^3.22.4 + version: 3.22.4 devDependencies: '@jest/expect': specifier: ^29.7.0 From b3cc8086ef29106bc96de5aa8845404adec05933 Mon Sep 17 00:00:00 2001 From: Ash Date: Mon, 22 Apr 2024 17:29:30 +0100 Subject: [PATCH 04/49] feat(manifest): add `@sanity/manifest` package --- .github/CODEOWNERS | 4 ++ dev/aliases.cjs | 1 + dev/tsconfig.dev.json | 2 + examples/tsconfig.json | 2 + packages/@repo/test-exports/.depcheckrc.json | 1 + packages/@repo/test-exports/package.json | 1 + packages/@sanity/manifest/.depcheckrc.json | 3 ++ packages/@sanity/manifest/.eslintrc.cjs | 11 +++++ packages/@sanity/manifest/.gitignore | 15 ++++++ packages/@sanity/manifest/README.md | 1 + packages/@sanity/manifest/jest.config.cjs | 8 ++++ packages/@sanity/manifest/package.config.ts | 4 ++ packages/@sanity/manifest/package.json | 48 +++++++++++++++++++ .../@sanity/manifest/src/_exports/index.ts | 1 + packages/@sanity/manifest/src/schema/v1.ts | 18 +++++++ packages/@sanity/manifest/tsconfig.json | 10 ++++ packages/@sanity/manifest/tsconfig.lib.json | 8 ++++ packages/@sanity/manifest/turbo.json | 9 ++++ packages/sanity/tsconfig.json | 2 + 19 files changed, 149 insertions(+) create mode 100644 packages/@sanity/manifest/.depcheckrc.json create mode 100644 packages/@sanity/manifest/.eslintrc.cjs create mode 100644 packages/@sanity/manifest/.gitignore create mode 100644 packages/@sanity/manifest/README.md create mode 100644 packages/@sanity/manifest/jest.config.cjs create mode 100644 packages/@sanity/manifest/package.config.ts create mode 100644 packages/@sanity/manifest/package.json create mode 100644 packages/@sanity/manifest/src/_exports/index.ts create mode 100644 packages/@sanity/manifest/src/schema/v1.ts create mode 100644 packages/@sanity/manifest/tsconfig.json create mode 100644 packages/@sanity/manifest/tsconfig.lib.json create mode 100644 packages/@sanity/manifest/turbo.json diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 236179f9fb4..f377c66a3bd 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -98,3 +98,7 @@ packages/sanity/src/core/studio/upsell/ @sanity-io/studio-ex /packages/sanity/src/structure/panes/documentList/PaneContainer.tsx @sanity-io/ecosystem @sanity-io/studio-dx /packages/sanity/src/structure/StructureToolProvider.tsx @sanity-io/ecosystem @sanity-io/studio-dx /packages/sanity/src/structure/types.ts @sanity-io/ecosystem @sanity-io/studio-dx + +# -- Manifest -- +/packages/@sanity/manifest @sanity-io/studio-dx + diff --git a/dev/aliases.cjs b/dev/aliases.cjs index 15f0782ae48..ffc4d50282e 100644 --- a/dev/aliases.cjs +++ b/dev/aliases.cjs @@ -21,6 +21,7 @@ const devAliases = { '@sanity/cli': './packages/@sanity/cli/src', '@sanity/mutator': './packages/@sanity/mutator/src', '@sanity/schema': './packages/@sanity/schema/src/_exports', + '@sanity/manifest': './packages/@sanity/manifrst/src/_exports', '@sanity/migrate': './packages/@sanity/migrate/src/_exports', '@sanity/types': './packages/@sanity/types/src', '@sanity/util': './packages/@sanity/util/src/_exports', diff --git a/dev/tsconfig.dev.json b/dev/tsconfig.dev.json index 5e1ee610513..580b1d60850 100644 --- a/dev/tsconfig.dev.json +++ b/dev/tsconfig.dev.json @@ -10,6 +10,8 @@ "@sanity/cli": ["./packages/@sanity/cli/src/index.ts"], "@sanity/codegen": ["./packages/@sanity/codegen/src/_exports/index.ts"], "@sanity/mutator": ["./packages/@sanity/mutator/src/index.ts"], + "@sanity/manifest/*": ["./packages/@sanity/manifest/src/_exports/*"], + "@sanity/manifest": ["./packages/@sanity/manifest/src/_exports/index.ts"], "@sanity/schema/*": ["./packages/@sanity/schema/src/_exports/*"], "@sanity/schema": ["./packages/@sanity/schema/src/_exports/index.ts"], "@sanity/migrate": ["./packages/@sanity/migrate/src/_exports/index.ts"], diff --git a/examples/tsconfig.json b/examples/tsconfig.json index 5e1ee610513..580b1d60850 100644 --- a/examples/tsconfig.json +++ b/examples/tsconfig.json @@ -10,6 +10,8 @@ "@sanity/cli": ["./packages/@sanity/cli/src/index.ts"], "@sanity/codegen": ["./packages/@sanity/codegen/src/_exports/index.ts"], "@sanity/mutator": ["./packages/@sanity/mutator/src/index.ts"], + "@sanity/manifest/*": ["./packages/@sanity/manifest/src/_exports/*"], + "@sanity/manifest": ["./packages/@sanity/manifest/src/_exports/index.ts"], "@sanity/schema/*": ["./packages/@sanity/schema/src/_exports/*"], "@sanity/schema": ["./packages/@sanity/schema/src/_exports/index.ts"], "@sanity/migrate": ["./packages/@sanity/migrate/src/_exports/index.ts"], diff --git a/packages/@repo/test-exports/.depcheckrc.json b/packages/@repo/test-exports/.depcheckrc.json index 7be5e5048b1..7f5e17b1718 100644 --- a/packages/@repo/test-exports/.depcheckrc.json +++ b/packages/@repo/test-exports/.depcheckrc.json @@ -5,6 +5,7 @@ "@sanity/cli", "@sanity/codegen", "@sanity/diff", + "@sanity/manifest", "@sanity/migrate", "@sanity/mutator", "@sanity/schema", diff --git a/packages/@repo/test-exports/package.json b/packages/@repo/test-exports/package.json index 45d63920fbd..df8de384246 100644 --- a/packages/@repo/test-exports/package.json +++ b/packages/@repo/test-exports/package.json @@ -14,6 +14,7 @@ "@sanity/cli": "workspace:*", "@sanity/codegen": "workspace:*", "@sanity/diff": "workspace:*", + "@sanity/manifest": "workspace:*", "@sanity/migrate": "workspace:*", "@sanity/mutator": "workspace:*", "@sanity/schema": "workspace:*", diff --git a/packages/@sanity/manifest/.depcheckrc.json b/packages/@sanity/manifest/.depcheckrc.json new file mode 100644 index 00000000000..35f1b4badf9 --- /dev/null +++ b/packages/@sanity/manifest/.depcheckrc.json @@ -0,0 +1,3 @@ +{ + "ignores": ["@repo/tsconfig", "@sanity/pkg-utils"] +} diff --git a/packages/@sanity/manifest/.eslintrc.cjs b/packages/@sanity/manifest/.eslintrc.cjs new file mode 100644 index 00000000000..99fd6c69224 --- /dev/null +++ b/packages/@sanity/manifest/.eslintrc.cjs @@ -0,0 +1,11 @@ +'use strict' + +const path = require('path') + +const ROOT_PATH = path.resolve(__dirname, '../../..') + +module.exports = { + rules: { + 'import/no-extraneous-dependencies': ['error', {packageDir: [ROOT_PATH, __dirname]}], + }, +} diff --git a/packages/@sanity/manifest/.gitignore b/packages/@sanity/manifest/.gitignore new file mode 100644 index 00000000000..cd7c1010a8d --- /dev/null +++ b/packages/@sanity/manifest/.gitignore @@ -0,0 +1,15 @@ +# Logs +/logs +*.log + +# Coverage directory used by tools like istanbul +/coverage + +# Dependency directories +/node_modules + +# Compiled code +/lib + +# Legacy exports +/_internal.js diff --git a/packages/@sanity/manifest/README.md b/packages/@sanity/manifest/README.md new file mode 100644 index 00000000000..0532c79425c --- /dev/null +++ b/packages/@sanity/manifest/README.md @@ -0,0 +1 @@ +# Sanity Manifest diff --git a/packages/@sanity/manifest/jest.config.cjs b/packages/@sanity/manifest/jest.config.cjs new file mode 100644 index 00000000000..51ecfb62217 --- /dev/null +++ b/packages/@sanity/manifest/jest.config.cjs @@ -0,0 +1,8 @@ +'use strict' + +const {createJestConfig} = require('../../../test/config.cjs') + +module.exports = createJestConfig({ + displayName: require('./package.json').name, + testEnvironment: 'node', +}) diff --git a/packages/@sanity/manifest/package.config.ts b/packages/@sanity/manifest/package.config.ts new file mode 100644 index 00000000000..c43051dd053 --- /dev/null +++ b/packages/@sanity/manifest/package.config.ts @@ -0,0 +1,4 @@ +import baseConfig from '@repo/package.config' +import {defineConfig} from '@sanity/pkg-utils' + +export default defineConfig(baseConfig) diff --git a/packages/@sanity/manifest/package.json b/packages/@sanity/manifest/package.json new file mode 100644 index 00000000000..354d980af66 --- /dev/null +++ b/packages/@sanity/manifest/package.json @@ -0,0 +1,48 @@ +{ + "name": "@sanity/manifest", + "version": "3.39.1", + "description": "", + "keywords": ["sanity", "cms", "headless", "realtime", "content", "schema"], + "homepage": "https://www.sanity.io/", + "bugs": { + "url": "https://github.com/sanity-io/sanity/issues" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/sanity-io/sanity.git", + "directory": "packages/@sanity/manifest" + }, + "license": "MIT", + "author": "Sanity.io ", + "sideEffects": false, + "exports": { + ".": { + "source": "./src/_exports/index.ts", + "import": "./lib/index.mjs", + "require": "./lib/index.js", + "default": "./lib/index.js" + }, + "./package.json": "./package.json" + }, + "main": "./lib/index.js", + "module": "./lib/index.esm.js", + "types": "./lib/index.d.ts", + "files": ["lib", "src"], + "scripts": { + "build": "pkg-utils build --strict --check --clean", + "check:types": "tsc --project tsconfig.lib.json", + "clean": "rimraf lib", + "lint": "eslint .", + "prepublishOnly": "turbo run build", + "test": "jest", + "test:watch": "jest --watchAll", + "watch": "pkg-utils watch" + }, + "dependencies": { + "zod": "^3.22.4" + }, + "devDependencies": { + "@repo/package.config": "workspace:*", + "rimraf": "^3.0.2" + } +} diff --git a/packages/@sanity/manifest/src/_exports/index.ts b/packages/@sanity/manifest/src/_exports/index.ts new file mode 100644 index 00000000000..3c2ef6c7f95 --- /dev/null +++ b/packages/@sanity/manifest/src/_exports/index.ts @@ -0,0 +1 @@ +export * from '../schema/v1' diff --git a/packages/@sanity/manifest/src/schema/v1.ts b/packages/@sanity/manifest/src/schema/v1.ts new file mode 100644 index 00000000000..9d49c4b77bc --- /dev/null +++ b/packages/@sanity/manifest/src/schema/v1.ts @@ -0,0 +1,18 @@ +import z from 'zod' + +export const ManifestSchema = z.object({manifestVersion: z.number()}) + +export const ManifestV1WorkspaceSchema = z.object({ + name: z.string(), + dataset: z.string(), + schema: z.string(), +}) + +export type ManifestV1Workspace = z.infer + +export const ManifestV1Schema = ManifestSchema.extend({ + createdAt: z.date(), + workspaces: z.array(ManifestV1WorkspaceSchema), +}) + +export type ManifestV1 = z.infer diff --git a/packages/@sanity/manifest/tsconfig.json b/packages/@sanity/manifest/tsconfig.json new file mode 100644 index 00000000000..8d4a1d085ad --- /dev/null +++ b/packages/@sanity/manifest/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "@repo/tsconfig/base.json", + "include": ["./example", "./src", "./test", "./typings", "./node_modules/@sanity/types/src"], + "compilerOptions": { + "rootDir": ".", + "paths": { + "@sanity/types": ["./node_modules/@sanity/types/src"] + } + } +} diff --git a/packages/@sanity/manifest/tsconfig.lib.json b/packages/@sanity/manifest/tsconfig.lib.json new file mode 100644 index 00000000000..18d75ddfa7f --- /dev/null +++ b/packages/@sanity/manifest/tsconfig.lib.json @@ -0,0 +1,8 @@ +{ + "extends": "@repo/tsconfig/build.json", + "include": ["./example", "./src", "./typings"], + "compilerOptions": { + "rootDir": ".", + "outDir": "./lib" + } +} diff --git a/packages/@sanity/manifest/turbo.json b/packages/@sanity/manifest/turbo.json new file mode 100644 index 00000000000..cbf3c2a667c --- /dev/null +++ b/packages/@sanity/manifest/turbo.json @@ -0,0 +1,9 @@ +{ + "$schema": "https://turbo.build/schema.json", + "extends": ["//"], + "pipeline": { + "build": { + "outputs": ["lib/**", "index.js"] + } + } +} diff --git a/packages/sanity/tsconfig.json b/packages/sanity/tsconfig.json index 5c4dc645433..fb888fdd535 100644 --- a/packages/sanity/tsconfig.json +++ b/packages/sanity/tsconfig.json @@ -12,6 +12,7 @@ "./node_modules/@sanity/cli/src", "./node_modules/@sanity/cli/typings/deepSortObject.d.ts", "./node_modules/@sanity/codegen/src", + "./node_modules/@sanity/manifest/src", "./node_modules/@sanity/mutator/src", "./node_modules/@sanity/schema/src", "./node_modules/@sanity/schema/typings", @@ -27,6 +28,7 @@ "@sanity/diff": ["./node_modules/@sanity/diff/src/index.ts"], "@sanity/cli": ["./node_modules/@sanity/cli/src/index.ts"], "@sanity/codegen": ["./node_modules/@sanity/codegen/src/_exports/index.ts"], + "@sanity/manifest": ["./node_modules/@sanity/manifest/src/index.ts"], "@sanity/mutator": ["./node_modules/@sanity/mutator/src/index.ts"], "@sanity/schema/*": ["./node_modules/@sanity/schema/src/_exports/*"], "@sanity/schema": ["./node_modules/@sanity/schema/src/_exports/index.ts"], From 3fbdff481871a4adc1a989062eb936c936fb2882 Mon Sep 17 00:00:00 2001 From: Ash Date: Mon, 22 Apr 2024 17:31:43 +0100 Subject: [PATCH 05/49] refactor(sanity): use manifest schemas from `@sanity/manifest` --- packages/sanity/package.json | 3 +-- .../manifest/extractManifestsAction.ts | 23 +++---------------- 2 files changed, 4 insertions(+), 22 deletions(-) diff --git a/packages/sanity/package.json b/packages/sanity/package.json index dbac77e2a2a..751e8a109d3 100644 --- a/packages/sanity/package.json +++ b/packages/sanity/package.json @@ -263,8 +263,7 @@ "use-hot-module-reload": "^2.0.0", "use-sync-external-store": "^1.2.0", "vite": "^4.5.1", - "yargs": "^17.3.0", - "zod": "^3.22.4" + "yargs": "^17.3.0" }, "devDependencies": { "@jest/expect": "^29.7.0", diff --git a/packages/sanity/src/_internal/cli/actions/manifest/extractManifestsAction.ts b/packages/sanity/src/_internal/cli/actions/manifest/extractManifestsAction.ts index aa78b837dd9..8c12dc193cd 100644 --- a/packages/sanity/src/_internal/cli/actions/manifest/extractManifestsAction.ts +++ b/packages/sanity/src/_internal/cli/actions/manifest/extractManifestsAction.ts @@ -3,8 +3,8 @@ import {dirname, join, resolve} from 'node:path' import {Worker} from 'node:worker_threads' import {type CliCommandAction} from '@sanity/cli' +import {type ManifestV1, type ManifestV1Workspace} from '@sanity/manifest' import readPkgUp from 'read-pkg-up' -import z from 'zod' import { type ExtractSchemaWorkerData, @@ -14,23 +14,6 @@ import { const MANIFEST_FILENAME = 'v1.studiomanifest.json' const SCHEMA_FILENAME_PREFIX = 'schema-' -const ManifestSchema = z.object({manifestVersion: z.number()}) - -const ManifestV1WorkspaceSchema = z.object({ - name: z.string(), - dataset: z.string(), - schema: z.string(), -}) - -type ManifestV1Workspace = z.infer - -const ManifestV1Schema = ManifestSchema.extend({ - createdAt: z.date(), - workspaces: z.array(ManifestV1WorkspaceSchema), -}) - -type ManifestV1 = z.infer - const extractManifests: CliCommandAction = async (_args, context) => { const {output, workDir, chalk} = context @@ -119,7 +102,7 @@ export default extractManifests function externalizeSchemas( schemas: ExtractSchemaWorkerResult[], staticPath: string, -): Promise[]> { +): Promise { const output = schemas.reduce[]>((workspaces, workspace) => { return [...workspaces, externalizeSchema(workspace, staticPath)] }, []) @@ -130,7 +113,7 @@ function externalizeSchemas( async function externalizeSchema( workspace: ExtractSchemaWorkerResult, staticPath: string, -): Promise> { +): Promise { const encoder = new TextEncoder() const schemaString = JSON.stringify(workspace.schema, null, 2) const hash = await crypto.subtle.digest('SHA-1', encoder.encode(schemaString)) From c3d6a57aa6f971996e7fc397daa06cc8d7c1a9da Mon Sep 17 00:00:00 2001 From: Ash Date: Tue, 23 Apr 2024 12:52:41 +0100 Subject: [PATCH 06/49] chore: format files --- packages/@sanity/manifest/package.json | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/@sanity/manifest/package.json b/packages/@sanity/manifest/package.json index 354d980af66..e45c87a0948 100644 --- a/packages/@sanity/manifest/package.json +++ b/packages/@sanity/manifest/package.json @@ -2,7 +2,14 @@ "name": "@sanity/manifest", "version": "3.39.1", "description": "", - "keywords": ["sanity", "cms", "headless", "realtime", "content", "schema"], + "keywords": [ + "sanity", + "cms", + "headless", + "realtime", + "content", + "schema" + ], "homepage": "https://www.sanity.io/", "bugs": { "url": "https://github.com/sanity-io/sanity/issues" @@ -27,7 +34,10 @@ "main": "./lib/index.js", "module": "./lib/index.esm.js", "types": "./lib/index.d.ts", - "files": ["lib", "src"], + "files": [ + "lib", + "src" + ], "scripts": { "build": "pkg-utils build --strict --check --clean", "check:types": "tsc --project tsconfig.lib.json", From 081692511b7a5280193b33f311c01af0289855e7 Mon Sep 17 00:00:00 2001 From: Ash Date: Tue, 23 Apr 2024 17:03:29 +0100 Subject: [PATCH 07/49] feat(schema): include `title`, `description`, and `deprecated` attributes when extracting schema --- .../schema/src/sanity/extractSchema.ts | 14 ++++- .../__snapshots__/extractSchema.test.ts.snap | 63 +++++++++++++++++++ 2 files changed, 76 insertions(+), 1 deletion(-) diff --git a/packages/@sanity/schema/src/sanity/extractSchema.ts b/packages/@sanity/schema/src/sanity/extractSchema.ts index 61d49f4c59c..33f24d58bb0 100644 --- a/packages/@sanity/schema/src/sanity/extractSchema.ts +++ b/packages/@sanity/schema/src/sanity/extractSchema.ts @@ -27,6 +27,9 @@ import { type UnknownTypeNode, } from 'groq-js' +type Metadata = Type & + Pick + const documentDefaultFields = (typeName: string): Record => ({ _id: { type: 'objectAttribute', @@ -93,7 +96,7 @@ export function extractSchema( function convertBaseType( schemaType: SanitySchemaType, - ): DocumentSchemaType | TypeDeclarationSchemaType | null { + ): Metadata | null { let typeName: string | undefined if (schemaType.type) { typeName = schemaType.type.name @@ -101,6 +104,12 @@ export function extractSchema( typeName = schemaType.jsonType } + const metadata: Metadata = { + title: schemaType.title, + description: schemaType.description, + deprecated: schemaType.deprecated, + } + if (typeName === 'document' && isObjectType(schemaType)) { const defaultAttributes = documentDefaultFields(schemaType.name) @@ -110,6 +119,7 @@ export function extractSchema( } return { + ...metadata, name: schemaType.name, type: 'document', attributes: { @@ -125,6 +135,7 @@ export function extractSchema( } if (value.type === 'object') { return { + ...metadata, name: schemaType.name, type: 'type', value: { @@ -144,6 +155,7 @@ export function extractSchema( } return { + ...metadata, name: schemaType.name, type: 'type', value, diff --git a/packages/@sanity/schema/test/extractSchema/__snapshots__/extractSchema.test.ts.snap b/packages/@sanity/schema/test/extractSchema/__snapshots__/extractSchema.test.ts.snap index 30e7c989fc1..290535081d7 100644 --- a/packages/@sanity/schema/test/extractSchema/__snapshots__/extractSchema.test.ts.snap +++ b/packages/@sanity/schema/test/extractSchema/__snapshots__/extractSchema.test.ts.snap @@ -223,7 +223,10 @@ Array [ exports[`Extract schema test Extracts schema general 1`] = ` Array [ Object { + "deprecated": undefined, + "description": undefined, "name": "sanity.imagePaletteSwatch", + "title": "Image palette swatch", "type": "type", "value": Object { "attributes": Object { @@ -267,7 +270,10 @@ Array [ }, }, Object { + "deprecated": undefined, + "description": undefined, "name": "sanity.imagePalette", + "title": "Image palette", "type": "type", "value": Object { "attributes": Object { @@ -339,7 +345,10 @@ Array [ }, }, Object { + "deprecated": undefined, + "description": undefined, "name": "sanity.imageDimensions", + "title": "Image dimensions", "type": "type", "value": Object { "attributes": Object { @@ -376,7 +385,10 @@ Array [ }, }, Object { + "deprecated": undefined, + "description": undefined, "name": "geopoint", + "title": "Geographical Point", "type": "type", "value": Object { "attributes": Object { @@ -413,7 +425,10 @@ Array [ }, }, Object { + "deprecated": undefined, + "description": undefined, "name": "slug", + "title": "Slug", "type": "type", "value": Object { "attributes": Object { @@ -443,7 +458,10 @@ Array [ }, }, Object { + "deprecated": undefined, + "description": undefined, "name": "someTextType", + "title": "Text", "type": "type", "value": Object { "type": "string", @@ -582,11 +600,17 @@ Array [ }, }, }, + "deprecated": undefined, + "description": undefined, "name": "sanity.fileAsset", + "title": "File", "type": "document", }, Object { + "deprecated": undefined, + "description": undefined, "name": "code", + "title": "Code", "type": "type", "value": Object { "attributes": Object { @@ -609,14 +633,20 @@ Array [ }, }, Object { + "deprecated": undefined, + "description": undefined, "name": "customStringType", + "title": "My custom string type", "type": "type", "value": Object { "type": "string", }, }, Object { + "deprecated": undefined, + "description": undefined, "name": "blocksTest", + "title": "Blocks test", "type": "type", "value": Object { "attributes": Object { @@ -3981,7 +4011,10 @@ Array [ }, }, }, + "deprecated": undefined, + "description": undefined, "name": "book", + "title": "Book", "type": "document", }, Object { @@ -4101,11 +4134,17 @@ Array [ }, }, }, + "deprecated": undefined, + "description": undefined, "name": "author", + "title": "Author", "type": "document", }, Object { + "deprecated": undefined, + "description": undefined, "name": "sanity.imageCrop", + "title": "Image crop", "type": "type", "value": Object { "attributes": Object { @@ -4149,7 +4188,10 @@ Array [ }, }, Object { + "deprecated": undefined, + "description": undefined, "name": "sanity.imageHotspot", + "title": "Image hotspot", "type": "type", "value": Object { "attributes": Object { @@ -4333,11 +4375,17 @@ Array [ }, }, }, + "deprecated": undefined, + "description": undefined, "name": "sanity.imageAsset", + "title": "Image", "type": "document", }, Object { + "deprecated": undefined, + "description": undefined, "name": "sanity.assetSourceData", + "title": "Asset Source Data", "type": "type", "value": Object { "attributes": Object { @@ -4374,7 +4422,10 @@ Array [ }, }, Object { + "deprecated": undefined, + "description": undefined, "name": "sanity.imageMetadata", + "title": "Image metadata", "type": "type", "value": Object { "attributes": Object { @@ -4856,7 +4907,10 @@ Array [ }, }, }, + "deprecated": undefined, + "description": undefined, "name": "validDocument", + "title": "Valid document", "type": "document", }, Object { @@ -4900,11 +4954,17 @@ Array [ }, }, }, + "deprecated": undefined, + "description": undefined, "name": "otherValidDocument", + "title": "Other valid document", "type": "document", }, Object { + "deprecated": undefined, + "description": undefined, "name": "manuscript", + "title": "Manuscript", "type": "type", "value": Object { "attributes": Object { @@ -4987,7 +5047,10 @@ Array [ }, }, Object { + "deprecated": undefined, + "description": undefined, "name": "obj", + "title": "Obj", "type": "type", "value": Object { "attributes": Object { From fb860b20344599f0d19febb02f79434686be432f Mon Sep 17 00:00:00 2001 From: Ash Date: Wed, 24 Apr 2024 16:59:51 +0100 Subject: [PATCH 08/49] feat(sanity): add `direct` schema format to schema extractor --- .../manifest/extractManifestsAction.ts | 20 ++++---- .../_internal/cli/threads/extractSchema.ts | 48 +++++++++++++------ 2 files changed, 45 insertions(+), 23 deletions(-) diff --git a/packages/sanity/src/_internal/cli/actions/manifest/extractManifestsAction.ts b/packages/sanity/src/_internal/cli/actions/manifest/extractManifestsAction.ts index 8c12dc193cd..db0f2b73270 100644 --- a/packages/sanity/src/_internal/cli/actions/manifest/extractManifestsAction.ts +++ b/packages/sanity/src/_internal/cli/actions/manifest/extractManifestsAction.ts @@ -46,19 +46,21 @@ const extractManifests: CliCommandAction = async (_args, context) => { workerData: { workDir, enforceRequiredFields: false, - format: 'groq-type-nodes', + format: 'direct', } satisfies ExtractSchemaWorkerData, // eslint-disable-next-line no-process-env env: process.env, }) try { - const schemas = await new Promise((resolveSchemas, reject) => { - const schemaBuffer: ExtractSchemaWorkerResult[] = [] - worker.addListener('message', (message) => schemaBuffer.push(message)) - worker.addListener('exit', () => resolveSchemas(schemaBuffer)) - worker.addListener('error', reject) - }) + const schemas = await new Promise[]>( + (resolveSchemas, reject) => { + const schemaBuffer: ExtractSchemaWorkerResult<'direct'>[] = [] + worker.addListener('message', (message) => schemaBuffer.push(message)) + worker.addListener('exit', () => resolveSchemas(schemaBuffer)) + worker.addListener('error', reject) + }, + ) spinner.text = `Writing manifest to ${chalk.cyan(path)}` @@ -100,7 +102,7 @@ const extractManifests: CliCommandAction = async (_args, context) => { export default extractManifests function externalizeSchemas( - schemas: ExtractSchemaWorkerResult[], + schemas: ExtractSchemaWorkerResult<'direct'>[], staticPath: string, ): Promise { const output = schemas.reduce[]>((workspaces, workspace) => { @@ -111,7 +113,7 @@ function externalizeSchemas( } async function externalizeSchema( - workspace: ExtractSchemaWorkerResult, + workspace: ExtractSchemaWorkerResult<'direct'>, staticPath: string, ): Promise { const encoder = new TextEncoder() diff --git a/packages/sanity/src/_internal/cli/threads/extractSchema.ts b/packages/sanity/src/_internal/cli/threads/extractSchema.ts index 61608deef35..b0dbd2daadd 100644 --- a/packages/sanity/src/_internal/cli/threads/extractSchema.ts +++ b/packages/sanity/src/_internal/cli/threads/extractSchema.ts @@ -1,47 +1,63 @@ import {isMainThread, parentPort, workerData as _workerData} from 'node:worker_threads' import {extractSchema} from '@sanity/schema/_internal' -import {type Workspace} from 'sanity' +import {type SchemaType} from 'groq-js' +import {type SchemaTypeDefinition, type Workspace} from 'sanity' import {getStudioWorkspaces} from '../util/getStudioWorkspaces' import {mockBrowserEnvironment} from '../util/mockBrowserEnvironment' +const formats = ['direct', 'groq-type-nodes'] as const +type Format = (typeof formats)[number] + /** @internal */ export interface ExtractSchemaWorkerData { workDir: string workspaceName?: string enforceRequiredFields?: boolean - format: 'groq-type-nodes' | string + format: Format | string } -/** @internal */ -export interface ExtractSchemaWorkerResult extends Pick { - schema: ReturnType +type WorkspaceTransformer = (workspace: Workspace) => ExtractSchemaWorkerResult + +const workspaceTransformers: Record = { + 'direct': (workspace) => ({ + name: workspace.name, + dataset: workspace.dataset, + schema: JSON.parse(JSON.stringify(workspace.schema._original?.types)), + }), + 'groq-type-nodes': (workspace) => ({ + schema: extractSchema(workspace.schema, { + enforceRequiredFields: opts.enforceRequiredFields, + }), + }), } +/** @internal */ +export type ExtractSchemaWorkerResult = { + 'direct': Pick & {schema: SchemaTypeDefinition[]} + 'groq-type-nodes': {schema: SchemaType} +}[TargetFormat] + if (isMainThread || !parentPort) { throw new Error('This module must be run as a worker thread') } const opts = _workerData as ExtractSchemaWorkerData +const {format} = opts const cleanup = mockBrowserEnvironment(opts.workDir) async function main() { try { - if (opts.format !== 'groq-type-nodes') { - throw new Error(`Unsupported format: "${opts.format}"`) + if (!isFormat(format)) { + throw new Error(`Unsupported format: "${format}"`) } const workspaces = await getStudioWorkspaces({basePath: opts.workDir}) const postWorkspace = (workspace: Workspace): void => { - parentPort?.postMessage({ - name: workspace.name, - dataset: workspace.dataset, - schema: extractSchema(workspace.schema, { - enforceRequiredFields: opts.enforceRequiredFields, - }), - } satisfies ExtractSchemaWorkerResult) + const transformer = workspaceTransformers[format] + parentPort?.postMessage(transformer(workspace)) } if (opts.workspaceName) { @@ -81,3 +97,7 @@ function getWorkspace({ } return workspace } + +function isFormat(maybeFormat: string): maybeFormat is Format { + return formats.includes(maybeFormat as Format) +} From 5b2f8406640cc432741b5aa49867bba0a10f8de0 Mon Sep 17 00:00:00 2001 From: Ash Date: Wed, 24 Apr 2024 17:03:51 +0100 Subject: [PATCH 09/49] Revert "feat(schema): include `title`, `description`, and `deprecated` attributes when extracting schema" This reverts commit 60cb576290e56bde8e688522f2d27ba1cd81e5e7. --- .../schema/src/sanity/extractSchema.ts | 14 +---- .../__snapshots__/extractSchema.test.ts.snap | 63 ------------------- 2 files changed, 1 insertion(+), 76 deletions(-) diff --git a/packages/@sanity/schema/src/sanity/extractSchema.ts b/packages/@sanity/schema/src/sanity/extractSchema.ts index 33f24d58bb0..61d49f4c59c 100644 --- a/packages/@sanity/schema/src/sanity/extractSchema.ts +++ b/packages/@sanity/schema/src/sanity/extractSchema.ts @@ -27,9 +27,6 @@ import { type UnknownTypeNode, } from 'groq-js' -type Metadata = Type & - Pick - const documentDefaultFields = (typeName: string): Record => ({ _id: { type: 'objectAttribute', @@ -96,7 +93,7 @@ export function extractSchema( function convertBaseType( schemaType: SanitySchemaType, - ): Metadata | null { + ): DocumentSchemaType | TypeDeclarationSchemaType | null { let typeName: string | undefined if (schemaType.type) { typeName = schemaType.type.name @@ -104,12 +101,6 @@ export function extractSchema( typeName = schemaType.jsonType } - const metadata: Metadata = { - title: schemaType.title, - description: schemaType.description, - deprecated: schemaType.deprecated, - } - if (typeName === 'document' && isObjectType(schemaType)) { const defaultAttributes = documentDefaultFields(schemaType.name) @@ -119,7 +110,6 @@ export function extractSchema( } return { - ...metadata, name: schemaType.name, type: 'document', attributes: { @@ -135,7 +125,6 @@ export function extractSchema( } if (value.type === 'object') { return { - ...metadata, name: schemaType.name, type: 'type', value: { @@ -155,7 +144,6 @@ export function extractSchema( } return { - ...metadata, name: schemaType.name, type: 'type', value, diff --git a/packages/@sanity/schema/test/extractSchema/__snapshots__/extractSchema.test.ts.snap b/packages/@sanity/schema/test/extractSchema/__snapshots__/extractSchema.test.ts.snap index 290535081d7..30e7c989fc1 100644 --- a/packages/@sanity/schema/test/extractSchema/__snapshots__/extractSchema.test.ts.snap +++ b/packages/@sanity/schema/test/extractSchema/__snapshots__/extractSchema.test.ts.snap @@ -223,10 +223,7 @@ Array [ exports[`Extract schema test Extracts schema general 1`] = ` Array [ Object { - "deprecated": undefined, - "description": undefined, "name": "sanity.imagePaletteSwatch", - "title": "Image palette swatch", "type": "type", "value": Object { "attributes": Object { @@ -270,10 +267,7 @@ Array [ }, }, Object { - "deprecated": undefined, - "description": undefined, "name": "sanity.imagePalette", - "title": "Image palette", "type": "type", "value": Object { "attributes": Object { @@ -345,10 +339,7 @@ Array [ }, }, Object { - "deprecated": undefined, - "description": undefined, "name": "sanity.imageDimensions", - "title": "Image dimensions", "type": "type", "value": Object { "attributes": Object { @@ -385,10 +376,7 @@ Array [ }, }, Object { - "deprecated": undefined, - "description": undefined, "name": "geopoint", - "title": "Geographical Point", "type": "type", "value": Object { "attributes": Object { @@ -425,10 +413,7 @@ Array [ }, }, Object { - "deprecated": undefined, - "description": undefined, "name": "slug", - "title": "Slug", "type": "type", "value": Object { "attributes": Object { @@ -458,10 +443,7 @@ Array [ }, }, Object { - "deprecated": undefined, - "description": undefined, "name": "someTextType", - "title": "Text", "type": "type", "value": Object { "type": "string", @@ -600,17 +582,11 @@ Array [ }, }, }, - "deprecated": undefined, - "description": undefined, "name": "sanity.fileAsset", - "title": "File", "type": "document", }, Object { - "deprecated": undefined, - "description": undefined, "name": "code", - "title": "Code", "type": "type", "value": Object { "attributes": Object { @@ -633,20 +609,14 @@ Array [ }, }, Object { - "deprecated": undefined, - "description": undefined, "name": "customStringType", - "title": "My custom string type", "type": "type", "value": Object { "type": "string", }, }, Object { - "deprecated": undefined, - "description": undefined, "name": "blocksTest", - "title": "Blocks test", "type": "type", "value": Object { "attributes": Object { @@ -4011,10 +3981,7 @@ Array [ }, }, }, - "deprecated": undefined, - "description": undefined, "name": "book", - "title": "Book", "type": "document", }, Object { @@ -4134,17 +4101,11 @@ Array [ }, }, }, - "deprecated": undefined, - "description": undefined, "name": "author", - "title": "Author", "type": "document", }, Object { - "deprecated": undefined, - "description": undefined, "name": "sanity.imageCrop", - "title": "Image crop", "type": "type", "value": Object { "attributes": Object { @@ -4188,10 +4149,7 @@ Array [ }, }, Object { - "deprecated": undefined, - "description": undefined, "name": "sanity.imageHotspot", - "title": "Image hotspot", "type": "type", "value": Object { "attributes": Object { @@ -4375,17 +4333,11 @@ Array [ }, }, }, - "deprecated": undefined, - "description": undefined, "name": "sanity.imageAsset", - "title": "Image", "type": "document", }, Object { - "deprecated": undefined, - "description": undefined, "name": "sanity.assetSourceData", - "title": "Asset Source Data", "type": "type", "value": Object { "attributes": Object { @@ -4422,10 +4374,7 @@ Array [ }, }, Object { - "deprecated": undefined, - "description": undefined, "name": "sanity.imageMetadata", - "title": "Image metadata", "type": "type", "value": Object { "attributes": Object { @@ -4907,10 +4856,7 @@ Array [ }, }, }, - "deprecated": undefined, - "description": undefined, "name": "validDocument", - "title": "Valid document", "type": "document", }, Object { @@ -4954,17 +4900,11 @@ Array [ }, }, }, - "deprecated": undefined, - "description": undefined, "name": "otherValidDocument", - "title": "Other valid document", "type": "document", }, Object { - "deprecated": undefined, - "description": undefined, "name": "manuscript", - "title": "Manuscript", "type": "type", "value": Object { "attributes": Object { @@ -5047,10 +4987,7 @@ Array [ }, }, Object { - "deprecated": undefined, - "description": undefined, "name": "obj", - "title": "Obj", "type": "type", "value": Object { "attributes": Object { From c38fb41a58d5e9c286a0e50a2af8ae6825ef9a5c Mon Sep 17 00:00:00 2001 From: Ash Date: Fri, 26 Apr 2024 15:21:55 +0100 Subject: [PATCH 10/49] feat(sanity): export `ConcreteRuleClass` class --- packages/sanity/src/core/index.ts | 6 +++- packages/sanity/src/core/validation/Rule.ts | 34 ++++++++++++--------- 2 files changed, 24 insertions(+), 16 deletions(-) diff --git a/packages/sanity/src/core/index.ts b/packages/sanity/src/core/index.ts index 56606120212..83140342883 100644 --- a/packages/sanity/src/core/index.ts +++ b/packages/sanity/src/core/index.ts @@ -33,5 +33,9 @@ export * from './templates' export * from './theme' export * from './user-color' export * from './util' -export {validateDocument, type ValidateDocumentOptions} from './validation' +export { + Rule as ConcreteRuleClass, + validateDocument, + type ValidateDocumentOptions, +} from './validation' export * from './version' diff --git a/packages/sanity/src/core/validation/Rule.ts b/packages/sanity/src/core/validation/Rule.ts index 6640cda8a09..15b4ac165a7 100644 --- a/packages/sanity/src/core/validation/Rule.ts +++ b/packages/sanity/src/core/validation/Rule.ts @@ -54,21 +54,25 @@ const ruleConstraintTypes: RuleTypeConstraint[] = [ 'String', ] -// Note: `RuleClass` and `Rule` are split to fit the current `@sanity/types` -// setup. Classes are a bit weird in the `@sanity/types` package because classes -// create an actual javascript class while simultaneously creating a type -// definition. -// -// This implicitly creates two types: -// 1. the instance type — `Rule` and -// 2. the static/class type - `RuleClass` -// -// The `RuleClass` type contains the static methods and the `Rule` instance -// contains the instance methods. -// -// This package exports the RuleClass as a value without implicitly exporting -// an instance definition. This should help reminder downstream users to import -// from the `@sanity/types` package. +/** + * Note: `RuleClass` and `Rule` are split to fit the current `@sanity/types` + * setup. Classes are a bit weird in the `@sanity/types` package because classes + * create an actual javascript class while simultaneously creating a type + * definition. + * + * This implicitly creates two types: + * 1. the instance type — `Rule` and + * 2. the static/class type - `RuleClass` + * + * The `RuleClass` type contains the static methods and the `Rule` instance + * contains the instance methods. + * + * This package exports the RuleClass as a value without implicitly exporting + * an instance definition. This should help reminder downstream users to import + * from the `@sanity/types` package. + * + * @internal + */ export const Rule: RuleClass = class Rule implements IRule { static readonly FIELD_REF = FIELD_REF static array = (def?: SchemaType): Rule => new Rule(def).type('Array') From 9be3d9005f70469a1c838da85ef23f1bc0dfccb9 Mon Sep 17 00:00:00 2001 From: Ash Date: Fri, 26 Apr 2024 17:05:08 +0100 Subject: [PATCH 11/49] feat(sanity): include validation rules in manifests --- .../_internal/cli/threads/extractSchema.ts | 74 +++++++++++++++++-- 1 file changed, 68 insertions(+), 6 deletions(-) diff --git a/packages/sanity/src/_internal/cli/threads/extractSchema.ts b/packages/sanity/src/_internal/cli/threads/extractSchema.ts index b0dbd2daadd..ef46f17e540 100644 --- a/packages/sanity/src/_internal/cli/threads/extractSchema.ts +++ b/packages/sanity/src/_internal/cli/threads/extractSchema.ts @@ -2,7 +2,12 @@ import {isMainThread, parentPort, workerData as _workerData} from 'node:worker_t import {extractSchema} from '@sanity/schema/_internal' import {type SchemaType} from 'groq-js' -import {type SchemaTypeDefinition, type Workspace} from 'sanity' +import { + ConcreteRuleClass, + type SchemaTypeDefinition, + type SchemaValidationValue, + type Workspace, +} from 'sanity' import {getStudioWorkspaces} from '../util/getStudioWorkspaces' import {mockBrowserEnvironment} from '../util/mockBrowserEnvironment' @@ -21,11 +26,20 @@ export interface ExtractSchemaWorkerData { type WorkspaceTransformer = (workspace: Workspace) => ExtractSchemaWorkerResult const workspaceTransformers: Record = { - 'direct': (workspace) => ({ - name: workspace.name, - dataset: workspace.dataset, - schema: JSON.parse(JSON.stringify(workspace.schema._original?.types)), - }), + 'direct': (workspace) => { + return { + name: workspace.name, + dataset: workspace.dataset, + schema: JSON.parse( + JSON.stringify(workspace.schema._original, (key, value) => { + if (key === 'validation' && isSchemaValidationValue(value)) { + return serializeValidation(value) + } + return value + }), + ), + } + }, 'groq-type-nodes': (workspace) => ({ schema: extractSchema(workspace.schema, { enforceRequiredFields: opts.enforceRequiredFields, @@ -101,3 +115,51 @@ function getWorkspace({ function isFormat(maybeFormat: string): maybeFormat is Format { return formats.includes(maybeFormat as Format) } + +// TODO: Simplify output format. +function serializeValidation(validation: SchemaValidationValue): SchemaValidationValue[] { + const validationArray = Array.isArray(validation) ? validation : [validation] + + return validationArray + .reduce((output, validationValue) => { + if (typeof validationValue === 'function') { + const rule = new ConcreteRuleClass() + const applied = validationValue(rule) + + // TODO: Deduplicate by flag. + // TODO: Handle merging of validation rules for array items. + return [...output, applied] + } + return output + }, []) + .flat() +} + +function isSchemaValidationValue( + maybeSchemaValidationValue: unknown, +): maybeSchemaValidationValue is SchemaValidationValue { + if (Array.isArray(maybeSchemaValidationValue)) { + return maybeSchemaValidationValue.every(isSchemaValidationValue) + } + + // TODO: Errors with `fields() can only be called on an object type` when it encounters + // the `fields` validation rule on a type that is not directly an `object`. This mayb be + // because the validation rules aren't normalized. + try { + return ( + maybeSchemaValidationValue === false || + typeof maybeSchemaValidationValue === 'undefined' || + maybeSchemaValidationValue instanceof ConcreteRuleClass || + (typeof maybeSchemaValidationValue === 'function' && + isSchemaValidationValue(maybeSchemaValidationValue(new ConcreteRuleClass()))) + ) + } catch (error) { + const hasMessage = 'message' in error + + if (!hasMessage || error.message !== 'fields() can only be called on an object type') { + throw error + } + } + + return false +} From 4e7aabf0a6dd1d5c9c7609c96fb11b8b42f78345 Mon Sep 17 00:00:00 2001 From: Ash Date: Sat, 27 Apr 2024 17:56:11 +0100 Subject: [PATCH 12/49] refactor(sanity): move manifest extraction code --- .../_internal/cli/threads/extractSchema.ts | 72 +---------------- .../src/_internal/manifest/extractManifest.ts | 77 +++++++++++++++++++ 2 files changed, 81 insertions(+), 68 deletions(-) create mode 100644 packages/sanity/src/_internal/manifest/extractManifest.ts diff --git a/packages/sanity/src/_internal/cli/threads/extractSchema.ts b/packages/sanity/src/_internal/cli/threads/extractSchema.ts index ef46f17e540..c6482fc9fbd 100644 --- a/packages/sanity/src/_internal/cli/threads/extractSchema.ts +++ b/packages/sanity/src/_internal/cli/threads/extractSchema.ts @@ -2,13 +2,9 @@ import {isMainThread, parentPort, workerData as _workerData} from 'node:worker_t import {extractSchema} from '@sanity/schema/_internal' import {type SchemaType} from 'groq-js' -import { - ConcreteRuleClass, - type SchemaTypeDefinition, - type SchemaValidationValue, - type Workspace, -} from 'sanity' +import {type SchemaTypeDefinition, type Workspace} from 'sanity' +import {extractWorkspace} from '../../manifest/extractManifest' import {getStudioWorkspaces} from '../util/getStudioWorkspaces' import {mockBrowserEnvironment} from '../util/mockBrowserEnvironment' @@ -26,20 +22,8 @@ export interface ExtractSchemaWorkerData { type WorkspaceTransformer = (workspace: Workspace) => ExtractSchemaWorkerResult const workspaceTransformers: Record = { - 'direct': (workspace) => { - return { - name: workspace.name, - dataset: workspace.dataset, - schema: JSON.parse( - JSON.stringify(workspace.schema._original, (key, value) => { - if (key === 'validation' && isSchemaValidationValue(value)) { - return serializeValidation(value) - } - return value - }), - ), - } - }, + // @ts-expect-error FIXME + 'direct': extractWorkspace, 'groq-type-nodes': (workspace) => ({ schema: extractSchema(workspace.schema, { enforceRequiredFields: opts.enforceRequiredFields, @@ -115,51 +99,3 @@ function getWorkspace({ function isFormat(maybeFormat: string): maybeFormat is Format { return formats.includes(maybeFormat as Format) } - -// TODO: Simplify output format. -function serializeValidation(validation: SchemaValidationValue): SchemaValidationValue[] { - const validationArray = Array.isArray(validation) ? validation : [validation] - - return validationArray - .reduce((output, validationValue) => { - if (typeof validationValue === 'function') { - const rule = new ConcreteRuleClass() - const applied = validationValue(rule) - - // TODO: Deduplicate by flag. - // TODO: Handle merging of validation rules for array items. - return [...output, applied] - } - return output - }, []) - .flat() -} - -function isSchemaValidationValue( - maybeSchemaValidationValue: unknown, -): maybeSchemaValidationValue is SchemaValidationValue { - if (Array.isArray(maybeSchemaValidationValue)) { - return maybeSchemaValidationValue.every(isSchemaValidationValue) - } - - // TODO: Errors with `fields() can only be called on an object type` when it encounters - // the `fields` validation rule on a type that is not directly an `object`. This mayb be - // because the validation rules aren't normalized. - try { - return ( - maybeSchemaValidationValue === false || - typeof maybeSchemaValidationValue === 'undefined' || - maybeSchemaValidationValue instanceof ConcreteRuleClass || - (typeof maybeSchemaValidationValue === 'function' && - isSchemaValidationValue(maybeSchemaValidationValue(new ConcreteRuleClass()))) - ) - } catch (error) { - const hasMessage = 'message' in error - - if (!hasMessage || error.message !== 'fields() can only be called on an object type') { - throw error - } - } - - return false -} diff --git a/packages/sanity/src/_internal/manifest/extractManifest.ts b/packages/sanity/src/_internal/manifest/extractManifest.ts new file mode 100644 index 00000000000..f79fd1a89fa --- /dev/null +++ b/packages/sanity/src/_internal/manifest/extractManifest.ts @@ -0,0 +1,77 @@ +import {type ManifestV1Workspace} from '@sanity/manifest' +import {ConcreteRuleClass, type SchemaValidationValue, type Workspace} from 'sanity' + +export function extractWorkspace(workspace: Workspace): ManifestV1Workspace { + return { + name: workspace.name, + dataset: workspace.dataset, + schema: JSON.parse( + JSON.stringify(workspace.schema._original, (key, value) => { + if (key === 'validation' && isSchemaValidationValue(value)) { + return serializeValidation(value) + } + return value + }), + ), + } +} + +// TODO: Type output. +export function extractSchema(workspace: Workspace): any { + return JSON.parse( + JSON.stringify(workspace.schema._original, (key, value) => { + if (key === 'validation' && isSchemaValidationValue(value)) { + return serializeValidation(value) + } + return value + }), + ) +} + +// TODO: Simplify output format. +function serializeValidation(validation: SchemaValidationValue): SchemaValidationValue[] { + const validationArray = Array.isArray(validation) ? validation : [validation] + + return validationArray + .reduce((output, validationValue) => { + if (typeof validationValue === 'function') { + const rule = new ConcreteRuleClass() + const applied = validationValue(rule) + + // TODO: Deduplicate by flag. + // TODO: Handle merging of validation rules for array items. + return [...output, applied] + } + return output + }, []) + .flat() +} + +function isSchemaValidationValue( + maybeSchemaValidationValue: unknown, +): maybeSchemaValidationValue is SchemaValidationValue { + if (Array.isArray(maybeSchemaValidationValue)) { + return maybeSchemaValidationValue.every(isSchemaValidationValue) + } + + // TODO: Errors with `fields() can only be called on an object type` when it encounters + // the `fields` validation rule on a type that is not directly an `object`. This mayb be + // because the validation rules aren't normalized. + try { + return ( + maybeSchemaValidationValue === false || + typeof maybeSchemaValidationValue === 'undefined' || + maybeSchemaValidationValue instanceof ConcreteRuleClass || + (typeof maybeSchemaValidationValue === 'function' && + isSchemaValidationValue(maybeSchemaValidationValue(new ConcreteRuleClass()))) + ) + } catch (error) { + const hasMessage = 'message' in error + + if (!hasMessage || error.message !== 'fields() can only be called on an object type') { + throw error + } + } + + return false +} From 43beb969e08ef6b2cb9467d4616d94e145c6b95f Mon Sep 17 00:00:00 2001 From: Ash Date: Sun, 28 Apr 2024 10:35:13 +0100 Subject: [PATCH 13/49] feat(sanity): extract manifest during build --- .../src/_internal/cli/actions/build/buildAction.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/packages/sanity/src/_internal/cli/actions/build/buildAction.ts b/packages/sanity/src/_internal/cli/actions/build/buildAction.ts index 62b9b8fe8d2..d1b8dbd940c 100644 --- a/packages/sanity/src/_internal/cli/actions/build/buildAction.ts +++ b/packages/sanity/src/_internal/cli/actions/build/buildAction.ts @@ -17,6 +17,8 @@ import {buildVendorDependencies} from '../../server/buildVendorDependencies' import {compareStudioDependencyVersions} from '../../util/compareStudioDependencyVersions' import {getAutoUpdateImportMap} from '../../util/getAutoUpdatesImportMap' import {shouldAutoUpdate} from '../../util/shouldAutoUpdate' +import extractManifest from '../manifest/extractManifestsAction' +import {pick} from 'lodash' const rimraf = promisify(rimrafCallback) @@ -186,6 +188,16 @@ export default async function buildSanityStudio( const buildDuration = timer.end('bundleStudio') spin.text = `Build Sanity Studio (${buildDuration.toFixed()}ms)` + + await extractManifest( + { + ...pick(args, ['argsWithoutOptions', 'argv', 'groupOrCommand']), + extOptions: {}, + extraArguments: [], + }, + context, + ) + spin.succeed() trace.complete() if (flags.stats) { From d35982cf387082be7b0edd98415c3e076a44efc6 Mon Sep 17 00:00:00 2001 From: Ash Date: Sun, 28 Apr 2024 17:46:06 +0100 Subject: [PATCH 14/49] feat(sanity): adopt `.studioschema.json` filename suffix for manifest schemas --- .../_internal/cli/actions/manifest/extractManifestsAction.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/sanity/src/_internal/cli/actions/manifest/extractManifestsAction.ts b/packages/sanity/src/_internal/cli/actions/manifest/extractManifestsAction.ts index db0f2b73270..a6dbdf0e9b9 100644 --- a/packages/sanity/src/_internal/cli/actions/manifest/extractManifestsAction.ts +++ b/packages/sanity/src/_internal/cli/actions/manifest/extractManifestsAction.ts @@ -12,7 +12,7 @@ import { } from '../../threads/extractSchema' const MANIFEST_FILENAME = 'v1.studiomanifest.json' -const SCHEMA_FILENAME_PREFIX = 'schema-' +const SCHEMA_FILENAME_SUFFIX = '.studioschema.json' const extractManifests: CliCommandAction = async (_args, context) => { const {output, workDir, chalk} = context @@ -119,7 +119,7 @@ async function externalizeSchema( const encoder = new TextEncoder() const schemaString = JSON.stringify(workspace.schema, null, 2) const hash = await crypto.subtle.digest('SHA-1', encoder.encode(schemaString)) - const filename = `${SCHEMA_FILENAME_PREFIX}${hexFromBuffer(hash).slice(0, 8)}.json` + const filename = `${hexFromBuffer(hash).slice(0, 8)}${SCHEMA_FILENAME_SUFFIX}` await writeFile(join(staticPath, filename), schemaString) From aa8cf02aa9068090cd104b0cfa48ed780a0b2e54 Mon Sep 17 00:00:00 2001 From: Ash Date: Mon, 29 Apr 2024 09:21:39 +0100 Subject: [PATCH 15/49] refactor(sanity): rename manifest extraction functions (remove plural) --- ...extractManifestsAction.ts => extractManifestAction.ts} | 4 ++-- packages/sanity/src/_internal/cli/commands/index.ts | 4 ++-- ...tractManifestsCommand.ts => extractManifestCommand.ts} | 8 ++++---- 3 files changed, 8 insertions(+), 8 deletions(-) rename packages/sanity/src/_internal/cli/actions/manifest/{extractManifestsAction.ts => extractManifestAction.ts} (97%) rename packages/sanity/src/_internal/cli/commands/manifest/{extractManifestsCommand.ts => extractManifestCommand.ts} (77%) diff --git a/packages/sanity/src/_internal/cli/actions/manifest/extractManifestsAction.ts b/packages/sanity/src/_internal/cli/actions/manifest/extractManifestAction.ts similarity index 97% rename from packages/sanity/src/_internal/cli/actions/manifest/extractManifestsAction.ts rename to packages/sanity/src/_internal/cli/actions/manifest/extractManifestAction.ts index a6dbdf0e9b9..50093cb0915 100644 --- a/packages/sanity/src/_internal/cli/actions/manifest/extractManifestsAction.ts +++ b/packages/sanity/src/_internal/cli/actions/manifest/extractManifestAction.ts @@ -14,7 +14,7 @@ import { const MANIFEST_FILENAME = 'v1.studiomanifest.json' const SCHEMA_FILENAME_SUFFIX = '.studioschema.json' -const extractManifests: CliCommandAction = async (_args, context) => { +const extractManifest: CliCommandAction = async (_args, context) => { const {output, workDir, chalk} = context const defaultOutputDir = resolve(join(workDir, 'dist')) @@ -99,7 +99,7 @@ const extractManifests: CliCommandAction = async (_args, context) => { output.print(`Extracted manifest to ${chalk.cyan(path)}`) } -export default extractManifests +export default extractManifest function externalizeSchemas( schemas: ExtractSchemaWorkerResult<'direct'>[], diff --git a/packages/sanity/src/_internal/cli/commands/index.ts b/packages/sanity/src/_internal/cli/commands/index.ts index af739380ceb..4b41a210e52 100644 --- a/packages/sanity/src/_internal/cli/commands/index.ts +++ b/packages/sanity/src/_internal/cli/commands/index.ts @@ -41,7 +41,7 @@ import hookGroup from './hook/hookGroup' import listHookLogsCommand from './hook/listHookLogsCommand' import listHooksCommand from './hook/listHooksCommand' import printHookAttemptCommand from './hook/printHookAttemptCommand' -import extractManifestsCommand from './manifest/extractManifestsCommand' +import extractManifestCommand from './manifest/extractManifestCommand' import listManifestsCommand from './manifest/listManifestsCommand' import manifestGroup from './manifest/manifestGroup' import createMigrationCommand from './migration/createMigrationCommand' @@ -91,7 +91,7 @@ const commands: (CliCommandDefinition | CliCommandGroupDefinition)[] = [ migrationGroup, createMigrationCommand, manifestGroup, - extractManifestsCommand, + extractManifestCommand, listManifestsCommand, runMigrationCommand, listMigrationsCommand, diff --git a/packages/sanity/src/_internal/cli/commands/manifest/extractManifestsCommand.ts b/packages/sanity/src/_internal/cli/commands/manifest/extractManifestCommand.ts similarity index 77% rename from packages/sanity/src/_internal/cli/commands/manifest/extractManifestsCommand.ts rename to packages/sanity/src/_internal/cli/commands/manifest/extractManifestCommand.ts index cf6b8741c0d..ec381d701a3 100644 --- a/packages/sanity/src/_internal/cli/commands/manifest/extractManifestsCommand.ts +++ b/packages/sanity/src/_internal/cli/commands/manifest/extractManifestCommand.ts @@ -1,7 +1,7 @@ import {type CliCommandDefinition} from '@sanity/cli' // TODO: Switch to lazy import. -import mod from '../../actions/manifest/extractManifestsAction' +import mod from '../../actions/manifest/extractManifestAction' const description = 'Extracts a JSON representation of a Sanity schema within a Studio context.' @@ -13,18 +13,18 @@ Examples sanity manifest extract ` -const extractManifestsCommand: CliCommandDefinition = { +const extractManifestCommand: CliCommandDefinition = { name: 'extract', group: 'manifest', signature: '', description, helpText, action: async (args, context) => { - // const mod = await import('../../actions/manifest/extractManifestsAction') + // const mod = await import('../../actions/manifest/extractManifestAction') // // return mod.default(args, context) return mod(args, context) }, } -export default extractManifestsCommand +export default extractManifestCommand From cc3c3d37b6c2299fd19eb2c198e8aa8bb58dae79 Mon Sep 17 00:00:00 2001 From: Ash Date: Mon, 29 Apr 2024 10:02:00 +0100 Subject: [PATCH 16/49] fix(sanity): remove redundant success message --- .../_internal/cli/actions/manifest/extractManifestAction.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/sanity/src/_internal/cli/actions/manifest/extractManifestAction.ts b/packages/sanity/src/_internal/cli/actions/manifest/extractManifestAction.ts index 50093cb0915..69f0bff11f9 100644 --- a/packages/sanity/src/_internal/cli/actions/manifest/extractManifestAction.ts +++ b/packages/sanity/src/_internal/cli/actions/manifest/extractManifestAction.ts @@ -89,14 +89,12 @@ const extractManifest: CliCommandAction = async (_args, context) => { // trace.complete() - spinner.succeed('Extracted manifest') + spinner.succeed(`Extracted manifest to ${chalk.cyan(path)}`) } catch (err) { // trace.error(err) spinner.fail('Failed to extract manifest') throw err } - - output.print(`Extracted manifest to ${chalk.cyan(path)}`) } export default extractManifest From d96fe765fc1930017f023a3f2266845289431865 Mon Sep 17 00:00:00 2001 From: Ash Date: Mon, 29 Apr 2024 12:11:47 +0100 Subject: [PATCH 17/49] fix(sanity): stop build spinner before starting manifest extraction --- packages/sanity/src/_internal/cli/actions/build/buildAction.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/sanity/src/_internal/cli/actions/build/buildAction.ts b/packages/sanity/src/_internal/cli/actions/build/buildAction.ts index d1b8dbd940c..55aa097e98a 100644 --- a/packages/sanity/src/_internal/cli/actions/build/buildAction.ts +++ b/packages/sanity/src/_internal/cli/actions/build/buildAction.ts @@ -188,6 +188,7 @@ export default async function buildSanityStudio( const buildDuration = timer.end('bundleStudio') spin.text = `Build Sanity Studio (${buildDuration.toFixed()}ms)` + spin.succeed() await extractManifest( { @@ -198,7 +199,6 @@ export default async function buildSanityStudio( context, ) - spin.succeed() trace.complete() if (flags.stats) { output.print('\nLargest module files:') From 116ca595d65ec51e01e074e6d7cd8147e4c5f601 Mon Sep 17 00:00:00 2001 From: Ash Date: Mon, 29 Apr 2024 12:44:19 +0100 Subject: [PATCH 18/49] feat(sanity): add `unstable_extractManifestOnBuild` CLI config option --- packages/@sanity/cli/src/types.ts | 7 +++++++ .../_internal/cli/actions/build/buildAction.ts | 18 ++++++++++-------- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/packages/@sanity/cli/src/types.ts b/packages/@sanity/cli/src/types.ts index ac85b2d5666..fb8e1f85fde 100644 --- a/packages/@sanity/cli/src/types.ts +++ b/packages/@sanity/cli/src/types.ts @@ -314,6 +314,13 @@ export interface CliConfig { autoUpdates?: boolean studioHost?: string + + /** + * Extracts a Studio manifest to the `dist/static` directory when building (or deploying) the project. + * + * Optional, defaults to `false`. + */ + unstable_extractManifestOnBuild?: boolean } export type UserViteConfig = diff --git a/packages/sanity/src/_internal/cli/actions/build/buildAction.ts b/packages/sanity/src/_internal/cli/actions/build/buildAction.ts index 55aa097e98a..1ed9af3efd3 100644 --- a/packages/sanity/src/_internal/cli/actions/build/buildAction.ts +++ b/packages/sanity/src/_internal/cli/actions/build/buildAction.ts @@ -190,14 +190,16 @@ export default async function buildSanityStudio( spin.text = `Build Sanity Studio (${buildDuration.toFixed()}ms)` spin.succeed() - await extractManifest( - { - ...pick(args, ['argsWithoutOptions', 'argv', 'groupOrCommand']), - extOptions: {}, - extraArguments: [], - }, - context, - ) + if (context.cliConfig && 'unstable_extractManifestOnBuild' in context.cliConfig && context.cliConfig.unstable_extractManifestOnBuild) { + await extractManifest( + { + ...pick(args, ['argsWithoutOptions', 'argv', 'groupOrCommand']), + extOptions: {}, + extraArguments: [], + }, + context, + ) + } trace.complete() if (flags.stats) { From 41b985b3926ad32f14920d2b72918f689af04f5f Mon Sep 17 00:00:00 2001 From: Ash Date: Mon, 29 Apr 2024 12:45:02 +0100 Subject: [PATCH 19/49] feat(test-studio): enable `unstable_extractManifestOnBuild` --- dev/test-studio/sanity.cli.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dev/test-studio/sanity.cli.ts b/dev/test-studio/sanity.cli.ts index 085d8f34600..c01d5b1c250 100644 --- a/dev/test-studio/sanity.cli.ts +++ b/dev/test-studio/sanity.cli.ts @@ -9,6 +9,8 @@ export default defineCliConfig({ dataset: 'test', }, + unstable_extractManifestOnBuild: true, + // Can be overriden by: // A) `SANITY_STUDIO_REACT_STRICT_MODE=false pnpm dev` // B) creating a `.env` file locally that sets the same env variable as above From afa7e47e9c25308c98638708e21d4e7bf710b480 Mon Sep 17 00:00:00 2001 From: Ash Date: Mon, 29 Apr 2024 13:54:41 +0100 Subject: [PATCH 20/49] fix(sanity): switch to node crypto for node 18 compatibility --- .../cli/actions/manifest/extractManifestAction.ts | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/packages/sanity/src/_internal/cli/actions/manifest/extractManifestAction.ts b/packages/sanity/src/_internal/cli/actions/manifest/extractManifestAction.ts index 69f0bff11f9..f7867ef8672 100644 --- a/packages/sanity/src/_internal/cli/actions/manifest/extractManifestAction.ts +++ b/packages/sanity/src/_internal/cli/actions/manifest/extractManifestAction.ts @@ -1,3 +1,4 @@ +import {createHash} from 'node:crypto' import {mkdir, writeFile} from 'node:fs/promises' import {dirname, join, resolve} from 'node:path' import {Worker} from 'node:worker_threads' @@ -114,10 +115,9 @@ async function externalizeSchema( workspace: ExtractSchemaWorkerResult<'direct'>, staticPath: string, ): Promise { - const encoder = new TextEncoder() const schemaString = JSON.stringify(workspace.schema, null, 2) - const hash = await crypto.subtle.digest('SHA-1', encoder.encode(schemaString)) - const filename = `${hexFromBuffer(hash).slice(0, 8)}${SCHEMA_FILENAME_SUFFIX}` + const hash = createHash('sha1').update(schemaString).digest('hex') + const filename = `${hash.slice(0, 8)}${SCHEMA_FILENAME_SUFFIX}` await writeFile(join(staticPath, filename), schemaString) @@ -126,9 +126,3 @@ async function externalizeSchema( schema: filename, } } - -function hexFromBuffer(buffer: ArrayBuffer): string { - return Array.prototype.map - .call(new Uint8Array(buffer), (x) => `00${x.toString(16)}`.slice(-2)) - .join('') -} From cf4cb7e8566c983e390b8280949d345e19c8d23e Mon Sep 17 00:00:00 2001 From: Ash Date: Mon, 29 Apr 2024 16:43:00 +0100 Subject: [PATCH 21/49] feat(cli): add `unstable_staticAssetsPath` CLI configuration option --- packages/@sanity/cli/src/types.ts | 12 ++++++++++++ .../cli/actions/manifest/extractManifestAction.ts | 12 ++++++++++-- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/packages/@sanity/cli/src/types.ts b/packages/@sanity/cli/src/types.ts index fb8e1f85fde..78d381bd740 100644 --- a/packages/@sanity/cli/src/types.ts +++ b/packages/@sanity/cli/src/types.ts @@ -321,6 +321,18 @@ export interface CliConfig { * Optional, defaults to `false`. */ unstable_extractManifestOnBuild?: boolean + + /** + * The path Sanity CLI will write static assets (such as the Studio Manifest) to when used inside + * an embedded Studio project. Relative to the CLI config file. + * + * Sanity CLI will attempt to create this path if it does not exist. + * + * You should not define a value if your Studio is not embedded. + * + * Optional, defaults to `dist/static`. + */ + unstable_staticAssetsPath?: string } export type UserViteConfig = diff --git a/packages/sanity/src/_internal/cli/actions/manifest/extractManifestAction.ts b/packages/sanity/src/_internal/cli/actions/manifest/extractManifestAction.ts index f7867ef8672..bc5ecd7c205 100644 --- a/packages/sanity/src/_internal/cli/actions/manifest/extractManifestAction.ts +++ b/packages/sanity/src/_internal/cli/actions/manifest/extractManifestAction.ts @@ -16,12 +16,20 @@ const MANIFEST_FILENAME = 'v1.studiomanifest.json' const SCHEMA_FILENAME_SUFFIX = '.studioschema.json' const extractManifest: CliCommandAction = async (_args, context) => { - const {output, workDir, chalk} = context + const {output, workDir, chalk, cliConfig} = context const defaultOutputDir = resolve(join(workDir, 'dist')) // const outputDir = resolve(args.argsWithoutOptions[0] || defaultOutputDir) const outputDir = resolve(defaultOutputDir) - const staticPath = join(outputDir, 'static') + const defaultStaticPath = join(outputDir, 'static') + + const staticPath = + cliConfig && + 'unstable_staticAssetsPath' in cliConfig && + typeof cliConfig.unstable_staticAssetsPath !== 'undefined' + ? resolve(join(workDir, cliConfig.unstable_staticAssetsPath)) + : defaultStaticPath + const path = join(staticPath, MANIFEST_FILENAME) const rootPkgPath = readPkgUp.sync({cwd: __dirname})?.path From 3d8e0703611608211f78c11b7d1078f881c5ee6f Mon Sep 17 00:00:00 2001 From: Ash Date: Mon, 29 Apr 2024 16:45:23 +0100 Subject: [PATCH 22/49] chore(cli): refine `unstable_extractManifestOnBuild` CLI configuration option description --- packages/@sanity/cli/src/types.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/@sanity/cli/src/types.ts b/packages/@sanity/cli/src/types.ts index 78d381bd740..f5733745a60 100644 --- a/packages/@sanity/cli/src/types.ts +++ b/packages/@sanity/cli/src/types.ts @@ -316,7 +316,8 @@ export interface CliConfig { studioHost?: string /** - * Extracts a Studio manifest to the `dist/static` directory when building (or deploying) the project. + * Whether or not to extract a Studio Manifest to the static assets directory when building + * (or deploying) the project. * * Optional, defaults to `false`. */ From 7019f26e21f75584eb7d48b447edf4e91e393b72 Mon Sep 17 00:00:00 2001 From: Ash Date: Tue, 30 Apr 2024 11:26:37 +0100 Subject: [PATCH 23/49] feat(sanity): remove extraneous `types` wrapper from manifests --- packages/sanity/src/_internal/manifest/extractManifest.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/sanity/src/_internal/manifest/extractManifest.ts b/packages/sanity/src/_internal/manifest/extractManifest.ts index f79fd1a89fa..4ca7e0b76f9 100644 --- a/packages/sanity/src/_internal/manifest/extractManifest.ts +++ b/packages/sanity/src/_internal/manifest/extractManifest.ts @@ -6,7 +6,7 @@ export function extractWorkspace(workspace: Workspace): ManifestV1Workspace { name: workspace.name, dataset: workspace.dataset, schema: JSON.parse( - JSON.stringify(workspace.schema._original, (key, value) => { + JSON.stringify(workspace.schema._original?.types, (key, value) => { if (key === 'validation' && isSchemaValidationValue(value)) { return serializeValidation(value) } From c4be77f009f0aa4216102a0e929269baa2b451f3 Mon Sep 17 00:00:00 2001 From: Ash Date: Thu, 23 May 2024 08:08:30 +0100 Subject: [PATCH 24/49] debug(test-studio): remove Mux plugin to unblock typegen --- dev/test-studio/sanity.config.ts | 2 -- dev/test-studio/schema/index.ts | 2 -- 2 files changed, 4 deletions(-) diff --git a/dev/test-studio/sanity.config.ts b/dev/test-studio/sanity.config.ts index 43eefde3dc4..1908f7e29db 100644 --- a/dev/test-studio/sanity.config.ts +++ b/dev/test-studio/sanity.config.ts @@ -137,8 +137,6 @@ const sharedSettings = definePlugin({ visionTool({ defaultApiVersion: '2022-08-08', }), - // eslint-disable-next-line camelcase - muxInput({mp4_support: 'standard'}), imageHotspotArrayPlugin(), routerDebugTool(), errorReportingTestPlugin(), diff --git a/dev/test-studio/schema/index.ts b/dev/test-studio/schema/index.ts index b579539513f..7cfc3538b07 100644 --- a/dev/test-studio/schema/index.ts +++ b/dev/test-studio/schema/index.ts @@ -79,7 +79,6 @@ import {virtualizationDebug} from './debug/virtualizationDebug' import {virtualizationInObject} from './debug/virtualizationInObject' import {v3docs} from './docs/v3' import markdown from './externalPlugins/markdown' -import mux from './externalPlugins/mux' import playlist from './playlist' import playlistTrack from './playlistTrack' import code from './plugins/code' @@ -268,7 +267,6 @@ export const schemaTypes = [ // Test documents with 3rd party plugin inputs markdown, - mux, // Other documents author, From 94b58443edacee95cdc399055fd5d58a0e0ae324 Mon Sep 17 00:00:00 2001 From: Ash Date: Thu, 23 May 2024 08:10:04 +0100 Subject: [PATCH 25/49] feat(embedded-studio): enable manifest extraction --- dev/embedded-studio/package.json | 2 +- dev/embedded-studio/sanity.cli.ts | 9 ++++++ dev/embedded-studio/sanity.config.ts | 34 ++++++++++++++++++++++ dev/embedded-studio/src/App.tsx | 42 ++-------------------------- 4 files changed, 46 insertions(+), 41 deletions(-) create mode 100644 dev/embedded-studio/sanity.cli.ts create mode 100644 dev/embedded-studio/sanity.config.ts diff --git a/dev/embedded-studio/package.json b/dev/embedded-studio/package.json index 745518d08d2..ca346158fad 100644 --- a/dev/embedded-studio/package.json +++ b/dev/embedded-studio/package.json @@ -3,7 +3,7 @@ "version": "3.57.1", "private": true, "scripts": { - "build": "tsc && vite build", + "build": "tsc && vite build && sanity manifest extract", "dev": "vite", "preview": "vite preview" }, diff --git a/dev/embedded-studio/sanity.cli.ts b/dev/embedded-studio/sanity.cli.ts new file mode 100644 index 00000000000..5f8416de33d --- /dev/null +++ b/dev/embedded-studio/sanity.cli.ts @@ -0,0 +1,9 @@ +import {defineCliConfig} from 'sanity/cli' + +export default defineCliConfig({ + api: { + projectId: 'ppsg7ml5', + dataset: 'test', + }, + unstable_staticAssetsPath: './dist/assets', +}) diff --git a/dev/embedded-studio/sanity.config.ts b/dev/embedded-studio/sanity.config.ts new file mode 100644 index 00000000000..c49026536a2 --- /dev/null +++ b/dev/embedded-studio/sanity.config.ts @@ -0,0 +1,34 @@ +import {defineConfig, defineType} from 'sanity' +import {structureTool} from 'sanity/structure' + +const BLOG_POST_SCHEMA = defineType({ + type: 'document', + name: 'blogPost', + title: 'Blog post', + fields: [ + { + type: 'string', + name: 'title', + title: 'Title', + }, + ], +}) + +export const SCHEMA_TYPES = [BLOG_POST_SCHEMA] + +export default defineConfig({ + projectId: 'ppsg7ml5', + dataset: 'test', + + document: { + unstable_comments: { + enabled: true, + }, + }, + + schema: { + types: SCHEMA_TYPES, + }, + + plugins: [structureTool()], +}) diff --git a/dev/embedded-studio/src/App.tsx b/dev/embedded-studio/src/App.tsx index d07913d0206..7ee792a216b 100644 --- a/dev/embedded-studio/src/App.tsx +++ b/dev/embedded-studio/src/App.tsx @@ -1,46 +1,8 @@ import {Button, Card, Flex, studioTheme, ThemeProvider, usePrefersDark} from '@sanity/ui' import {useCallback, useMemo, useState} from 'react' -import { - defineConfig, - defineType, - Studio, - StudioLayout, - StudioProvider, - type StudioThemeColorSchemeKey, -} from 'sanity' -import {structureTool} from 'sanity/structure' +import {Studio, StudioLayout, StudioProvider, type StudioThemeColorSchemeKey} from 'sanity' -const BLOG_POST_SCHEMA = defineType({ - type: 'document', - name: 'blogPost', - title: 'Blog post', - fields: [ - { - type: 'string', - name: 'title', - title: 'Title', - }, - ], -}) - -const SCHEMA_TYPES = [BLOG_POST_SCHEMA] - -const config = defineConfig({ - projectId: 'ppsg7ml5', - dataset: 'test', - - document: { - unstable_comments: { - enabled: true, - }, - }, - - schema: { - types: SCHEMA_TYPES, - }, - - plugins: [structureTool()], -}) +import config from '../sanity.config' export function App() { const prefersDark = usePrefersDark() From 720dcd814f0584d04efae4b5b012a33a0f00e3e1 Mon Sep 17 00:00:00 2001 From: Ash Date: Thu, 23 May 2024 08:11:36 +0100 Subject: [PATCH 26/49] feat(starter-next-studio): enable manifest extraction --- dev/starter-next-studio/components/Studio.tsx | 36 +++---------------- dev/starter-next-studio/package.json | 2 +- dev/starter-next-studio/sanity.cli.ts | 8 +++++ dev/starter-next-studio/sanity.config.ts | 25 +++++++++++++ 4 files changed, 38 insertions(+), 33 deletions(-) create mode 100644 dev/starter-next-studio/sanity.cli.ts create mode 100644 dev/starter-next-studio/sanity.config.ts diff --git a/dev/starter-next-studio/components/Studio.tsx b/dev/starter-next-studio/components/Studio.tsx index 00557ec43c7..11aa0e3ce65 100644 --- a/dev/starter-next-studio/components/Studio.tsx +++ b/dev/starter-next-studio/components/Studio.tsx @@ -1,41 +1,13 @@ -import {useMemo} from 'react' -import {defineConfig, Studio} from 'sanity' -import {structureTool} from 'sanity/structure' +import {Studio} from 'sanity' + +import config from '../sanity.config' const wrapperStyles = {height: '100vh', width: '100vw'} export default function StudioRoot({basePath}: {basePath: string}) { - const config = useMemo( - () => - defineConfig({ - basePath, - plugins: [structureTool()], - title: 'Next.js Starter', - projectId: 'ppsg7ml5', - dataset: 'test', - schema: { - types: [ - { - type: 'document', - name: 'post', - title: 'Post', - fields: [ - { - type: 'string', - name: 'title', - title: 'Title', - }, - ], - }, - ], - }, - }), - [basePath], - ) - return (
- +
) } diff --git a/dev/starter-next-studio/package.json b/dev/starter-next-studio/package.json index e7c9ecd9c85..a511780da5e 100644 --- a/dev/starter-next-studio/package.json +++ b/dev/starter-next-studio/package.json @@ -5,7 +5,7 @@ "license": "MIT", "author": "Sanity.io ", "scripts": { - "build": "next build", + "build": "sanity manifest extract && next build", "dev": "next dev", "start": "next start" }, diff --git a/dev/starter-next-studio/sanity.cli.ts b/dev/starter-next-studio/sanity.cli.ts new file mode 100644 index 00000000000..fac247bf8cf --- /dev/null +++ b/dev/starter-next-studio/sanity.cli.ts @@ -0,0 +1,8 @@ +import {defineCliConfig} from 'sanity/cli' + +export default defineCliConfig({ + api: { + projectId: 'ppsg7ml5', + dataset: 'test', + }, +}) diff --git a/dev/starter-next-studio/sanity.config.ts b/dev/starter-next-studio/sanity.config.ts new file mode 100644 index 00000000000..102cbb15f94 --- /dev/null +++ b/dev/starter-next-studio/sanity.config.ts @@ -0,0 +1,25 @@ +import {defineConfig} from 'sanity' +import {structureTool} from 'sanity/structure' + +export default defineConfig({ + plugins: [structureTool()], + title: 'Next.js Starter', + projectId: 'ppsg7ml5', + dataset: 'test', + schema: { + types: [ + { + type: 'document', + name: 'post', + title: 'Post', + fields: [ + { + type: 'string', + name: 'title', + title: 'Title', + }, + ], + }, + ], + }, +}) From 0ba3f72695c234ee8f87d4b763636b08f2c46ac4 Mon Sep 17 00:00:00 2001 From: Ash Date: Thu, 23 May 2024 08:20:34 +0100 Subject: [PATCH 27/49] wip --- packages/@sanity/manifest/src/schema/v1.ts | 18 - .../@sanity/manifest/src/schema/v1/index.ts | 101 +++++ .../actions/manifest/extractManifestAction.ts | 6 +- .../commands/schema/extractSchemaCommand.ts | 10 +- .../src/_internal/manifest/extractManifest.ts | 349 +++++++++++++++--- 5 files changed, 407 insertions(+), 77 deletions(-) delete mode 100644 packages/@sanity/manifest/src/schema/v1.ts create mode 100644 packages/@sanity/manifest/src/schema/v1/index.ts diff --git a/packages/@sanity/manifest/src/schema/v1.ts b/packages/@sanity/manifest/src/schema/v1.ts deleted file mode 100644 index 9d49c4b77bc..00000000000 --- a/packages/@sanity/manifest/src/schema/v1.ts +++ /dev/null @@ -1,18 +0,0 @@ -import z from 'zod' - -export const ManifestSchema = z.object({manifestVersion: z.number()}) - -export const ManifestV1WorkspaceSchema = z.object({ - name: z.string(), - dataset: z.string(), - schema: z.string(), -}) - -export type ManifestV1Workspace = z.infer - -export const ManifestV1Schema = ManifestSchema.extend({ - createdAt: z.date(), - workspaces: z.array(ManifestV1WorkspaceSchema), -}) - -export type ManifestV1 = z.infer diff --git a/packages/@sanity/manifest/src/schema/v1/index.ts b/packages/@sanity/manifest/src/schema/v1/index.ts new file mode 100644 index 00000000000..158e08e6eec --- /dev/null +++ b/packages/@sanity/manifest/src/schema/v1/index.ts @@ -0,0 +1,101 @@ +import z from 'zod' + +export const manifestV1Deprecation = z.object({ + reason: z.string(), +}) + +export const manifestV1TypeValidationRule = z.object({ + flag: z.literal('type'), + constraint: z.string(), // xxx make me precise +}) + +// TOOD: Constraints +export const manifestV1UriValidationRule = z.object({ + flag: z.literal('uri'), +}) + +// TODO +// export const manifestV1ValidationRule = z.any() +export const manifestV1ValidationRule = z.union([ + manifestV1TypeValidationRule, + manifestV1UriValidationRule, + // TODO: Remove + z.any(), +]) + +export const manifestV1ValidationGroup = z.object({ + rules: z.array(manifestV1ValidationRule), + message: z.string().optional(), + level: z.union([z.literal('error'), z.literal('warning'), z.literal('info')]).optional(), +}) + +export type ManifestV1ValidationGroup = z.infer + +export const manifestV1Reference = z.object({ + type: z.string(), +}) + +export type ManifestV1Reference = z.infer + +export const manifestV1ReferenceGroup = z.array(manifestV1Reference) + +export type ManifestV1ReferenceGroup = z.infer + +const _base = z.object({ + type: z.string(), + name: z.string(), + title: z.string().optional(), + description: z.string().optional(), + deprecated: manifestV1Deprecation.optional(), + readOnly: z.boolean().optional(), // xxx + hidden: z.boolean().optional(), // xxx + validation: z.array(manifestV1ValidationGroup).optional(), + to: manifestV1ReferenceGroup.optional(), + of: z.any(), + preview: z + .object({ + select: z.record(z.string(), z.string()), + }) + .optional(), +}) + +const manifestV1TypeBase: z.ZodType = _base.extend({ + fields: z.array(z.lazy(() => manifestV1Field)).optional(), +}) + +export const manifestV1Field = manifestV1TypeBase + +export type ManifestV1Field = z.infer + +// export const ManifestV1TypeSchema = ManifestV1TypeBaseSchema.extend({ +// readOnly: z.boolean().optional(), +// hidden: z.boolean().optional(), +// preview: z +// .object({ +// select: z.record(z.string(), z.string()), +// }) +// .optional(), +// }) + +export type ManifestV1Type = z.infer & { + fields?: ManifestV1Field[] +} + +export const manifestV1Schema = z.array(manifestV1TypeBase) + +export const ManifestSchema = z.object({manifestVersion: z.number()}) + +export const manifestV1Workspace = z.object({ + name: z.string(), + dataset: z.string(), + schema: z.union([manifestV1Schema, z.string()]), // xxx don't actually want string here, but allows us to replace with filename +}) + +export type ManifestV1Workspace = z.infer + +export const manifestV1 = ManifestSchema.extend({ + createdAt: z.date(), + workspaces: z.array(manifestV1Workspace), +}) + +export type ManifestV1 = z.infer diff --git a/packages/sanity/src/_internal/cli/actions/manifest/extractManifestAction.ts b/packages/sanity/src/_internal/cli/actions/manifest/extractManifestAction.ts index bc5ecd7c205..cdef7d7a6c8 100644 --- a/packages/sanity/src/_internal/cli/actions/manifest/extractManifestAction.ts +++ b/packages/sanity/src/_internal/cli/actions/manifest/extractManifestAction.ts @@ -100,9 +100,10 @@ const extractManifest: CliCommandAction = async (_args, context) => { spinner.succeed(`Extracted manifest to ${chalk.cyan(path)}`) } catch (err) { + console.error('[ERR]', err) // trace.error(err) spinner.fail('Failed to extract manifest') - throw err + // throw err } } @@ -125,7 +126,8 @@ async function externalizeSchema( ): Promise { const schemaString = JSON.stringify(workspace.schema, null, 2) const hash = createHash('sha1').update(schemaString).digest('hex') - const filename = `${hash.slice(0, 8)}${SCHEMA_FILENAME_SUFFIX}` + // const filename = `${hash.slice(0, 8)}${SCHEMA_FILENAME_SUFFIX}` + const filename = `${workspace.name}${SCHEMA_FILENAME_SUFFIX}` await writeFile(join(staticPath, filename), schemaString) diff --git a/packages/sanity/src/_internal/cli/commands/schema/extractSchemaCommand.ts b/packages/sanity/src/_internal/cli/commands/schema/extractSchemaCommand.ts index f6867346c39..aa0a3ef4600 100644 --- a/packages/sanity/src/_internal/cli/commands/schema/extractSchemaCommand.ts +++ b/packages/sanity/src/_internal/cli/commands/schema/extractSchemaCommand.ts @@ -1,5 +1,8 @@ import {type CliCommandDefinition} from '@sanity/cli' +// xxx tmp +import mod from '../../actions/schema/extractAction' + const description = 'Extracts a JSON representation of a Sanity schema within a Studio context.' const helpText = ` @@ -23,9 +26,10 @@ const extractSchemaCommand: CliCommandDefinition = { description, helpText, action: async (args, context) => { - const mod = await import('../../actions/schema/extractAction') - - return mod.default(args, context) + // const mod = await import('../../actions/schema/extractAction') + // + // return mod.default(args, context) + return mod(args, context) }, } satisfies CliCommandDefinition diff --git a/packages/sanity/src/_internal/manifest/extractManifest.ts b/packages/sanity/src/_internal/manifest/extractManifest.ts index 4ca7e0b76f9..3c6291cd3f5 100644 --- a/packages/sanity/src/_internal/manifest/extractManifest.ts +++ b/packages/sanity/src/_internal/manifest/extractManifest.ts @@ -1,77 +1,318 @@ -import {type ManifestV1Workspace} from '@sanity/manifest' -import {ConcreteRuleClass, type SchemaValidationValue, type Workspace} from 'sanity' +import { + type ManifestV1Field, + manifestV1Schema, + type ManifestV1Type, + type ManifestV1ValidationGroup, + type ManifestV1Workspace, +} from '@sanity/manifest' +import { + type ArraySchemaType, + ConcreteRuleClass, + type ObjectField, + type ReferenceSchemaType, + type Rule, + type SchemaType, + type SchemaValidationValue, + type Workspace, +} from 'sanity' + +interface Context { + workspace: Workspace +} + +// type WithWarnings = Base & { +// warnings: { +// path: string[] +// warning: string +// }[] +// } + +type MaybeCustomized = Type & { + isCustomized?: boolean +} + +type Customized = Type & { + isCustomized: true +} + +type Validation = + | { + validation: ManifestV1ValidationGroup[] + } + | Record + +type ObjectFields = + | { + fields: ManifestV1Field[] + } + | Record export function extractWorkspace(workspace: Workspace): ManifestV1Workspace { + const typeNames = workspace.schema.getTypeNames() + const context = {workspace} + + const schema = typeNames + .map((typeName) => workspace.schema.get(typeName)) + .filter((type): type is SchemaType => typeof type !== 'undefined') + .map((type) => transformType(type, context)) + return { name: workspace.name, dataset: workspace.dataset, - schema: JSON.parse( - JSON.stringify(workspace.schema._original?.types, (key, value) => { - if (key === 'validation' && isSchemaValidationValue(value)) { - return serializeValidation(value) + schema: manifestV1Schema.parse(schema), + } +} + +function transformType(type: SchemaType, context: Context): ManifestV1Type { + const typeName = type.type ? type.type.name : type.jsonType + + if (type.jsonType === 'object') { + return { + name: type.name, + type: typeName, + deprecated: type.deprecated, + fields: (type.fields ?? []).map((field) => transformField(field, context)), + validation: transformValidation(type.validation), + ...ensureString('title', type.title), + ...ensureString('description', type.description), + ...ensureBoolean('readOnly', type.readOnly), + ...ensureBoolean('hidden', type.hidden), + } + } + + return { + name: type.name, + type: typeName, + deprecated: type.deprecated, + validation: transformValidation(type.validation), + ...ensureString('title', type.title), + ...ensureString('description', type.description), + ...ensureBoolean('readOnly', type.readOnly), + ...ensureBoolean('hidden', type.hidden), + } +} + +function transformField(field: MaybeCustomized, context: Context): ManifestV1Field { + const shouldCreateDefinition = + !context.workspace.schema.get(field.type.name) || isCustomized(field) + + const arrayProperties = + field.type.jsonType === 'array' ? transformArrayMember(field.type, context) : {} + + const referenceProperties = isReferenceSchemaType(field.type) + ? transformReference(field.type) + : {} + + const validation: Validation = shouldCreateDefinition + ? { + validation: transformValidation(field.type.validation), + } + : {} + + const objectFields: ObjectFields = + field.type.jsonType === 'object' && field.type.type && shouldCreateDefinition + ? { + fields: field.type.fields.map((objectField) => transformField(objectField, context)), } - return value - }), - ), + : {} + + return { + name: field.name, + type: field.type.name, + deprecated: field.type.deprecated, + ...validation, + ...objectFields, + ...ensureString('title', field.type.title), + ...ensureString('description', field.type.description), + ...arrayProperties, + ...referenceProperties, + ...ensureBoolean('readOnly', field.type.readOnly), + ...ensureBoolean('hidden', field.type.hidden), } } -// TODO: Type output. -export function extractSchema(workspace: Workspace): any { - return JSON.parse( - JSON.stringify(workspace.schema._original, (key, value) => { - if (key === 'validation' && isSchemaValidationValue(value)) { - return serializeValidation(value) +function transformArrayMember( + arrayMember: ArraySchemaType, + context: Context, +): Pick { + return { + of: arrayMember.of.map((type) => { + const shouldCreateDefinition = !context.workspace.schema.get(type.name) || isCustomized(type) + + if (shouldCreateDefinition) { + return transformType(type, context) + } + + // TODO: Deduplicate process taken from `transformField`. + // return transformField(type, context) + + const arrayProperties = + type.type?.jsonType === 'array' ? transformArrayMember(type.type, context) : {} + + const referenceProperties = isReferenceSchemaType(type.type) + ? transformReference(type.type) + : {} + + const validation: Validation = shouldCreateDefinition + ? { + validation: transformValidation(type.type?.validation), + } + : {} + + const objectFields: ObjectFields = + type.type?.jsonType === 'object' && type.type.type && shouldCreateDefinition + ? { + fields: type.type.fields.map((objectField) => transformField(objectField, context)), + } + : {} + + return { + name: type.name, + type: type.type?.name, + deprecated: type.type?.deprecated, + ...validation, + ...objectFields, + ...ensureString('title', type.type?.title), + ...ensureString('description', type.type?.description), + ...arrayProperties, + ...referenceProperties, + ...ensureBoolean('readOnly', type.type?.readOnly), + ...ensureBoolean('hidden', type.type?.hidden), } - return value }), + } +} + +function transformReference(reference: ReferenceSchemaType): Pick { + return { + to: (reference.to ?? []).map((type) => ({ + type: type.name, + })), + } +} + +function transformValidation(validation: SchemaValidationValue): ManifestV1ValidationGroup[] { + const validationArray = (Array.isArray(validation) ? validation : [validation]).filter( + (value): value is Rule => typeof value === 'object' && '_type' in value, ) + + // Custom validation rules cannot be serialized. + const disallowedFlags = ['custom'] + + // Validation rules that refer to other fields use symbols, which cannot be serialized. It would + // be possible to transform these to a serializable type, but we haven't implemented that for now. + const disallowedConstraintTypes: (symbol | unknown)[] = [ConcreteRuleClass.FIELD_REF] + + return validationArray.map(({_rules, _message, _level}) => { + // TODO: Handle insances of `LocalizedValidationMessages`. + const message: Partial> = + typeof _message === 'string' ? {message: _message} : {} + + return { + rules: _rules.filter((rule) => { + if (!('constraint' in rule)) { + return false + } + + const {flag, constraint} = rule + + if (disallowedFlags.includes(flag)) { + return false + } + + if ( + typeof constraint === 'object' && + 'type' in constraint && + disallowedConstraintTypes.includes(constraint.type) + ) { + return false + } + + return true + }), + level: _level, + ...message, + } + }) } -// TODO: Simplify output format. -function serializeValidation(validation: SchemaValidationValue): SchemaValidationValue[] { - const validationArray = Array.isArray(validation) ? validation : [validation] +function ensureString< + Key extends string, + const Value, + const DefaultValue extends string | undefined, +>( + key: Key, + value: Value, + defaultValue?: DefaultValue, +): Value extends string + ? Record + : [DefaultValue] extends [string] + ? Record + : Record { + if (typeof value === 'string') { + return { + [key]: value, + } as any // eslint-disable-line @typescript-eslint/no-explicit-any + } - return validationArray - .reduce((output, validationValue) => { - if (typeof validationValue === 'function') { - const rule = new ConcreteRuleClass() - const applied = validationValue(rule) + if (typeof defaultValue === 'string') { + return { + [key]: defaultValue, + } as any // eslint-disable-line @typescript-eslint/no-explicit-any + } - // TODO: Deduplicate by flag. - // TODO: Handle merging of validation rules for array items. - return [...output, applied] - } - return output - }, []) - .flat() + return {} as any // eslint-disable-line @typescript-eslint/no-explicit-any } -function isSchemaValidationValue( - maybeSchemaValidationValue: unknown, -): maybeSchemaValidationValue is SchemaValidationValue { - if (Array.isArray(maybeSchemaValidationValue)) { - return maybeSchemaValidationValue.every(isSchemaValidationValue) +function ensureBoolean< + Key extends string, + const Value, + const DefaultValue extends boolean | undefined, +>( + key: Key, + value: Value, + defaultValue?: DefaultValue, +): Value extends boolean + ? Record + : [DefaultValue] extends [boolean] + ? Record + : Record { + if (typeof value === 'boolean') { + return { + [key]: value, + } as any // eslint-disable-line @typescript-eslint/no-explicit-any } - // TODO: Errors with `fields() can only be called on an object type` when it encounters - // the `fields` validation rule on a type that is not directly an `object`. This mayb be - // because the validation rules aren't normalized. - try { - return ( - maybeSchemaValidationValue === false || - typeof maybeSchemaValidationValue === 'undefined' || - maybeSchemaValidationValue instanceof ConcreteRuleClass || - (typeof maybeSchemaValidationValue === 'function' && - isSchemaValidationValue(maybeSchemaValidationValue(new ConcreteRuleClass()))) - ) - } catch (error) { - const hasMessage = 'message' in error - - if (!hasMessage || error.message !== 'fields() can only be called on an object type') { - throw error - } + if (typeof defaultValue === 'boolean') { + return { + [key]: defaultValue, + } as any // eslint-disable-line @typescript-eslint/no-explicit-any } - return false + return {} as any // eslint-disable-line @typescript-eslint/no-explicit-any +} + +function isReferenceSchemaType(type: unknown): type is ReferenceSchemaType { + return typeof type === 'object' && type !== null && 'name' in type && type.name === 'reference' +} + +function isObjectField(maybeOjectField: unknown): maybeOjectField is MaybeCustomized { + return ( + typeof maybeOjectField === 'object' && maybeOjectField !== null && 'name' in maybeOjectField + ) +} + +function isMaybeCustomized( + maybeCustomized: unknown, +): maybeCustomized is MaybeCustomized { + return isObjectField(maybeCustomized) +} + +function isCustomized(maybeCustomized: Type): maybeCustomized is Customized { + return ( + isObjectField(maybeCustomized) && + 'fields' in maybeCustomized.type && + isObjectField(maybeCustomized.type) && + maybeCustomized.type.fields.some((field) => isMaybeCustomized(field) && field.isCustomized) + ) } From e7bcde1510735f38e032f3295ecb8373fed2ee76 Mon Sep 17 00:00:00 2001 From: Ash Date: Fri, 24 May 2024 15:44:57 +0100 Subject: [PATCH 28/49] feat(sanity): normalize type constraints in manifest validation --- .../@sanity/manifest/src/schema/v1/index.ts | 13 +++- .../src/_internal/manifest/extractManifest.ts | 60 +++++++++++++------ 2 files changed, 55 insertions(+), 18 deletions(-) diff --git a/packages/@sanity/manifest/src/schema/v1/index.ts b/packages/@sanity/manifest/src/schema/v1/index.ts index 158e08e6eec..1563d874603 100644 --- a/packages/@sanity/manifest/src/schema/v1/index.ts +++ b/packages/@sanity/manifest/src/schema/v1/index.ts @@ -6,9 +6,18 @@ export const manifestV1Deprecation = z.object({ export const manifestV1TypeValidationRule = z.object({ flag: z.literal('type'), - constraint: z.string(), // xxx make me precise + constraint: z.union([ + z.literal('array'), + z.literal('boolean'), + z.literal('date'), + z.literal('number'), + z.literal('object'), + z.literal('string'), + ]), }) +export type ManifestV1TypeValidationRule = z.infer + // TOOD: Constraints export const manifestV1UriValidationRule = z.object({ flag: z.literal('uri'), @@ -23,6 +32,8 @@ export const manifestV1ValidationRule = z.union([ z.any(), ]) +export type ManifestV1ValidationRule = z.infer + export const manifestV1ValidationGroup = z.object({ rules: z.array(manifestV1ValidationRule), message: z.string().optional(), diff --git a/packages/sanity/src/_internal/manifest/extractManifest.ts b/packages/sanity/src/_internal/manifest/extractManifest.ts index 3c6291cd3f5..f620e9bae24 100644 --- a/packages/sanity/src/_internal/manifest/extractManifest.ts +++ b/packages/sanity/src/_internal/manifest/extractManifest.ts @@ -2,7 +2,9 @@ import { type ManifestV1Field, manifestV1Schema, type ManifestV1Type, + type ManifestV1TypeValidationRule, type ManifestV1ValidationGroup, + type ManifestV1ValidationRule, type ManifestV1Workspace, } from '@sanity/manifest' import { @@ -11,6 +13,7 @@ import { type ObjectField, type ReferenceSchemaType, type Rule, + type RuleSpec, type SchemaType, type SchemaValidationValue, type Workspace, @@ -191,6 +194,23 @@ function transformReference(reference: ReferenceSchemaType): Pick = (rule: RuleSpec & {flag: Flag}) => ManifestV1ValidationRule & {flag: Flag} + +const transformTypeValidationRule: ValidationRuleTransformer<'type'> = (rule) => { + return { + ...rule, + constraint: rule.constraint.toLowerCase() as ManifestV1TypeValidationRule['constraint'], // xxx + } +} + +const validationRuleTransformers: Partial< + Record +> = { + type: transformTypeValidationRule, +} + function transformValidation(validation: SchemaValidationValue): ManifestV1ValidationGroup[] { const validationArray = (Array.isArray(validation) ? validation : [validation]).filter( (value): value is Rule => typeof value === 'object' && '_type' in value, @@ -209,27 +229,33 @@ function transformValidation(validation: SchemaValidationValue): ManifestV1Valid typeof _message === 'string' ? {message: _message} : {} return { - rules: _rules.filter((rule) => { - if (!('constraint' in rule)) { - return false - } + rules: _rules + .filter((rule) => { + if (!('constraint' in rule)) { + return false + } - const {flag, constraint} = rule + const {flag, constraint} = rule - if (disallowedFlags.includes(flag)) { - return false - } + if (disallowedFlags.includes(flag)) { + return false + } - if ( - typeof constraint === 'object' && - 'type' in constraint && - disallowedConstraintTypes.includes(constraint.type) - ) { - return false - } + if ( + typeof constraint === 'object' && + 'type' in constraint && + disallowedConstraintTypes.includes(constraint.type) + ) { + return false + } - return true - }), + return true + }) + .reduce((rules, rule) => { + const transformer: ValidationRuleTransformer = + validationRuleTransformers[rule.flag] ?? ((_) => _) + return [...rules, transformer(rule)] + }, []), level: _level, ...message, } From 09a0aa064530b24791694df409c1225e38c6a040 Mon Sep 17 00:00:00 2001 From: Ash Date: Thu, 6 Jun 2024 09:25:41 -0700 Subject: [PATCH 29/49] wip --- .../cli/actions/manifest/extractManifestAction.ts | 4 ++-- .../src/_internal/cli/commands/deploy/deployCommand.ts | 10 ++++++---- .../sanity/src/_internal/cli/threads/extractSchema.ts | 2 +- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/packages/sanity/src/_internal/cli/actions/manifest/extractManifestAction.ts b/packages/sanity/src/_internal/cli/actions/manifest/extractManifestAction.ts index cdef7d7a6c8..66adb86b131 100644 --- a/packages/sanity/src/_internal/cli/actions/manifest/extractManifestAction.ts +++ b/packages/sanity/src/_internal/cli/actions/manifest/extractManifestAction.ts @@ -126,8 +126,8 @@ async function externalizeSchema( ): Promise { const schemaString = JSON.stringify(workspace.schema, null, 2) const hash = createHash('sha1').update(schemaString).digest('hex') - // const filename = `${hash.slice(0, 8)}${SCHEMA_FILENAME_SUFFIX}` - const filename = `${workspace.name}${SCHEMA_FILENAME_SUFFIX}` + const filename = `${hash.slice(0, 8)}${SCHEMA_FILENAME_SUFFIX}` + // const filename = `${workspace.name}${SCHEMA_FILENAME_SUFFIX}` await writeFile(join(staticPath, filename), schemaString) diff --git a/packages/sanity/src/_internal/cli/commands/deploy/deployCommand.ts b/packages/sanity/src/_internal/cli/commands/deploy/deployCommand.ts index 2b126289788..eee0fe75ee9 100644 --- a/packages/sanity/src/_internal/cli/commands/deploy/deployCommand.ts +++ b/packages/sanity/src/_internal/cli/commands/deploy/deployCommand.ts @@ -4,7 +4,8 @@ import { type CliCommandDefinition, } from '@sanity/cli' -import {type DeployStudioActionFlags} from '../../actions/deploy/deployAction' +// xxx tmp +import deployAction, {type DeployStudioActionFlags} from '../../actions/deploy/deployAction' const helpText = ` Options @@ -25,9 +26,10 @@ const deployCommand: CliCommandDefinition = { args: CliCommandArguments, context: CliCommandContext, ) => { - const mod = await import('../../actions/deploy/deployAction') - - return mod.default(args, context) + // const mod = await import('../../actions/deploy/deployAction') + // + // return mod.default(args, context) + return deployAction(args, context) }, helpText, } diff --git a/packages/sanity/src/_internal/cli/threads/extractSchema.ts b/packages/sanity/src/_internal/cli/threads/extractSchema.ts index c6482fc9fbd..b2edcce9aaf 100644 --- a/packages/sanity/src/_internal/cli/threads/extractSchema.ts +++ b/packages/sanity/src/_internal/cli/threads/extractSchema.ts @@ -33,7 +33,7 @@ const workspaceTransformers: Record = { /** @internal */ export type ExtractSchemaWorkerResult = { - 'direct': Pick & {schema: SchemaTypeDefinition[]} + 'direct': Pick & {schema: SchemaTypeDefinition[]} // xxx 'groq-type-nodes': {schema: SchemaType} }[TargetFormat] From 3ba96fe638c21995c08e5ed00fec6c3202186ec6 Mon Sep 17 00:00:00 2001 From: Snorre Eskeland Brekke Date: Thu, 22 Aug 2024 12:53:01 +0200 Subject: [PATCH 30/49] chore: merge fix --- packages/@sanity/manifest/turbo.json | 2 +- .../sanity/src/_internal/cli/actions/build/buildAction.ts | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/@sanity/manifest/turbo.json b/packages/@sanity/manifest/turbo.json index cbf3c2a667c..19b4a243ca2 100644 --- a/packages/@sanity/manifest/turbo.json +++ b/packages/@sanity/manifest/turbo.json @@ -1,7 +1,7 @@ { "$schema": "https://turbo.build/schema.json", "extends": ["//"], - "pipeline": { + "tasks": { "build": { "outputs": ["lib/**", "index.js"] } diff --git a/packages/sanity/src/_internal/cli/actions/build/buildAction.ts b/packages/sanity/src/_internal/cli/actions/build/buildAction.ts index 1ed9af3efd3..c0dfe51fe9e 100644 --- a/packages/sanity/src/_internal/cli/actions/build/buildAction.ts +++ b/packages/sanity/src/_internal/cli/actions/build/buildAction.ts @@ -17,7 +17,7 @@ import {buildVendorDependencies} from '../../server/buildVendorDependencies' import {compareStudioDependencyVersions} from '../../util/compareStudioDependencyVersions' import {getAutoUpdateImportMap} from '../../util/getAutoUpdatesImportMap' import {shouldAutoUpdate} from '../../util/shouldAutoUpdate' -import extractManifest from '../manifest/extractManifestsAction' +import extractManifest from '../manifest/extractManifestAction' import {pick} from 'lodash' const rimraf = promisify(rimrafCallback) @@ -190,7 +190,11 @@ export default async function buildSanityStudio( spin.text = `Build Sanity Studio (${buildDuration.toFixed()}ms)` spin.succeed() - if (context.cliConfig && 'unstable_extractManifestOnBuild' in context.cliConfig && context.cliConfig.unstable_extractManifestOnBuild) { + if ( + context.cliConfig && + 'unstable_extractManifestOnBuild' in context.cliConfig && + context.cliConfig.unstable_extractManifestOnBuild + ) { await extractManifest( { ...pick(args, ['argsWithoutOptions', 'argv', 'groupOrCommand']), From f421749b47f5878a59d4b02b1693273776c51d05 Mon Sep 17 00:00:00 2001 From: Snorre Eskeland Brekke Date: Wed, 28 Aug 2024 23:36:14 +0200 Subject: [PATCH 31/49] feat: serialize userland properties and validation rules in manifest --- .../@sanity/manifest/src/_exports/index.ts | 4 +- .../src/_internal/manifest/extractManifest.ts | 533 +++++++----- .../_internal/manifest/manifestTypeHelpers.ts | 60 ++ .../src/_internal/manifest/manifestTypes.ts | 66 ++ .../test/manifest/extractManifest.test.ts | 773 ++++++++++++++++++ .../extractManifestValidation.test.ts | 515 ++++++++++++ 6 files changed, 1738 insertions(+), 213 deletions(-) create mode 100644 packages/sanity/src/_internal/manifest/manifestTypeHelpers.ts create mode 100644 packages/sanity/src/_internal/manifest/manifestTypes.ts create mode 100644 packages/sanity/test/manifest/extractManifest.test.ts create mode 100644 packages/sanity/test/manifest/extractManifestValidation.test.ts diff --git a/packages/@sanity/manifest/src/_exports/index.ts b/packages/@sanity/manifest/src/_exports/index.ts index 3c2ef6c7f95..31e9b807fc8 100644 --- a/packages/@sanity/manifest/src/_exports/index.ts +++ b/packages/@sanity/manifest/src/_exports/index.ts @@ -1 +1,3 @@ -export * from '../schema/v1' +//export * from '../schema/v1' + +export const DUMMY = '' diff --git a/packages/sanity/src/_internal/manifest/extractManifest.ts b/packages/sanity/src/_internal/manifest/extractManifest.ts index f620e9bae24..191c7ceccc9 100644 --- a/packages/sanity/src/_internal/manifest/extractManifest.ts +++ b/packages/sanity/src/_internal/manifest/extractManifest.ts @@ -1,235 +1,279 @@ -import { - type ManifestV1Field, - manifestV1Schema, - type ManifestV1Type, - type ManifestV1TypeValidationRule, - type ManifestV1ValidationGroup, - type ManifestV1ValidationRule, - type ManifestV1Workspace, -} from '@sanity/manifest' +import startCase from 'lodash/startCase' import { type ArraySchemaType, + type BlockDefinition, + type BooleanSchemaType, ConcreteRuleClass, + createSchema, + type FileSchemaType, + type NumberSchemaType, type ObjectField, + type ObjectSchemaType, type ReferenceSchemaType, type Rule, type RuleSpec, + type Schema, type SchemaType, type SchemaValidationValue, + type SpanSchemaType, + type StringSchemaType, type Workspace, } from 'sanity' -interface Context { - workspace: Workspace -} - -// type WithWarnings = Base & { -// warnings: { -// path: string[] -// warning: string -// }[] -// } - -type MaybeCustomized = Type & { - isCustomized?: boolean -} +import { + getCustomFields, + isDefined, + isPrimitive, + isRecord, + isString, + isType, +} from './manifestTypeHelpers' +import { + type ManifestField, + type ManifestSchemaType, + type ManifestSerializable, + type ManifestValidationGroup, + type ManifestValidationRule, + type ManifestWorkspace, + type TitledValue, +} from './manifestTypes' -type Customized = Type & { - isCustomized: true +interface Context { + schema: Schema } -type Validation = - | { - validation: ManifestV1ValidationGroup[] - } - | Record +type SchemaTypeKey = + | keyof ArraySchemaType + | keyof BooleanSchemaType + | keyof FileSchemaType + | keyof NumberSchemaType + | keyof ObjectSchemaType + | keyof StringSchemaType + | keyof ReferenceSchemaType + | keyof BlockDefinition -type ObjectFields = - | { - fields: ManifestV1Field[] - } - | Record +type Validation = {validation: ManifestValidationGroup[]} | Record +type ObjectFields = {fields: ManifestField[]} | Record +type SerializableProp = ManifestSerializable | ManifestSerializable[] | undefined +type ManifestValidationFlag = ManifestValidationRule['flag'] +type ValidationRuleTransformer = (rule: RuleSpec) => ManifestValidationRule | undefined -export function extractWorkspace(workspace: Workspace): ManifestV1Workspace { - const typeNames = workspace.schema.getTypeNames() - const context = {workspace} +const MAX_CUSTOM_PROPERTY_DEPTH = 5 - const schema = typeNames - .map((typeName) => workspace.schema.get(typeName)) - .filter((type): type is SchemaType => typeof type !== 'undefined') - .map((type) => transformType(type, context)) +export function extractWorkspace(workspace: Workspace): ManifestWorkspace { + const serializedSchema = extractManifestSchemaTypes(workspace.schema) return { name: workspace.name, dataset: workspace.dataset, - schema: manifestV1Schema.parse(schema), + schema: serializedSchema, } } -function transformType(type: SchemaType, context: Context): ManifestV1Type { - const typeName = type.type ? type.type.name : type.jsonType +/** + * Extracts all serializable properties from userland schema types, + * so they best-effort can be used as definitions for Schema.compile +. */ +export function extractManifestSchemaTypes(schema: Schema): ManifestSchemaType[] { + const typeNames = schema.getTypeNames() + const context = {schema} - if (type.jsonType === 'object') { - return { - name: type.name, - type: typeName, - deprecated: type.deprecated, - fields: (type.fields ?? []).map((field) => transformField(field, context)), - validation: transformValidation(type.validation), - ...ensureString('title', type.title), - ...ensureString('description', type.description), - ...ensureBoolean('readOnly', type.readOnly), - ...ensureBoolean('hidden', type.hidden), - } - } + const studioDefaultTypeNames = createSchema({name: 'default', types: []}).getTypeNames() - return { - name: type.name, - type: typeName, - deprecated: type.deprecated, - validation: transformValidation(type.validation), - ...ensureString('title', type.title), - ...ensureString('description', type.description), - ...ensureBoolean('readOnly', type.readOnly), - ...ensureBoolean('hidden', type.hidden), - } + return typeNames + .filter((typeName) => !studioDefaultTypeNames.includes(typeName)) + .map((typeName) => schema.get(typeName)) + .filter((type): type is SchemaType => typeof type !== 'undefined') + .map((type) => transformType(type, context)) } -function transformField(field: MaybeCustomized, context: Context): ManifestV1Field { - const shouldCreateDefinition = - !context.workspace.schema.get(field.type.name) || isCustomized(field) +function transformCommonTypeFields(type: SchemaType, context: Context) { + const shouldCreateDefinition = !context.schema.get(type.name) || isCustomized(type) - const arrayProperties = - field.type.jsonType === 'array' ? transformArrayMember(field.type, context) : {} + const arrayProperties = type.jsonType === 'array' ? transformArrayMember(type, context) : {} - const referenceProperties = isReferenceSchemaType(field.type) - ? transformReference(field.type) - : {} - - const validation: Validation = shouldCreateDefinition - ? { - validation: transformValidation(field.type.validation), - } - : {} + const referenceProperties = isReferenceSchemaType(type) ? transformReference(type) : {} const objectFields: ObjectFields = - field.type.jsonType === 'object' && field.type.type && shouldCreateDefinition + type.jsonType === 'object' && type.type && shouldCreateDefinition ? { - fields: field.type.fields.map((objectField) => transformField(objectField, context)), + fields: getCustomFields(type).map((objectField) => transformField(objectField, context)), } : {} return { - name: field.name, - type: field.type.name, - deprecated: field.type.deprecated, - ...validation, + ...retainUserlandTypeProps(type), + ...transformValidation(type.validation), ...objectFields, - ...ensureString('title', field.type.title), - ...ensureString('description', field.type.description), + ...ensureCustomTitle(type.name, type.title), + ...ensureString('description', type.description), ...arrayProperties, ...referenceProperties, - ...ensureBoolean('readOnly', field.type.readOnly), - ...ensureBoolean('hidden', field.type.hidden), + ...ensureConditional('readOnly', type.readOnly), + ...ensureConditional('hidden', type.hidden), + ...transformBlockType(type, context), } } -function transformArrayMember( - arrayMember: ArraySchemaType, - context: Context, -): Pick { +function transformType(type: SchemaType, context: Context): ManifestSchemaType { + const typeName = type.type ? type.type.name : type.jsonType + return { - of: arrayMember.of.map((type) => { - const shouldCreateDefinition = !context.workspace.schema.get(type.name) || isCustomized(type) + ...transformCommonTypeFields(type, context), + name: type.name, + type: typeName, + } +} - if (shouldCreateDefinition) { - return transformType(type, context) - } +function retainUserlandTypeProps(type: SchemaType): Record { + const manuallySerializedFields: SchemaTypeKey[] = [ + 'name', + 'title', + 'description', + 'readOnly', + 'hidden', + 'deprecated', + 'type', + 'jsonType', + '__experimental_actions', + '__experimental_formPreviewTitle', + '__experimental_omnisearch_visibility', + '__experimental_search', + 'groups', + 'components', + 'icon', + 'orderings', + 'validation', + 'fieldsets', + 'preview', + 'fields', + 'to', + 'of', + // we know about these, but let them be generically handled + // deprecated + // rows + // initialValue + // options + // reference stuff + ] + const typeWithoutManuallyHandledFields = Object.fromEntries( + Object.entries(type).filter( + ([key]) => !manuallySerializedFields.includes(key as unknown as SchemaTypeKey), + ), + ) + return retainSerializableProperties(typeWithoutManuallyHandledFields) as Record< + string, + SerializableProp + > +} - // TODO: Deduplicate process taken from `transformField`. - // return transformField(type, context) +function retainSerializableProperties(maybeSerializable: unknown, depth = 0): SerializableProp { + if (depth > MAX_CUSTOM_PROPERTY_DEPTH) { + return undefined + } - const arrayProperties = - type.type?.jsonType === 'array' ? transformArrayMember(type.type, context) : {} + if (!isDefined(maybeSerializable)) { + return undefined + } - const referenceProperties = isReferenceSchemaType(type.type) - ? transformReference(type.type) - : {} + if (isPrimitive(maybeSerializable)) { + // cull empty strings + if (maybeSerializable === '') { + return undefined + } + return maybeSerializable + } - const validation: Validation = shouldCreateDefinition - ? { - validation: transformValidation(type.type?.validation), - } - : {} + // url-schemes ect.. + if (maybeSerializable instanceof RegExp) { + return maybeSerializable.toString() + } + + if (Array.isArray(maybeSerializable)) { + const arrayItems = maybeSerializable + .map((item) => retainSerializableProperties(item, depth + 1)) + .filter((item): item is ManifestSerializable => isDefined(item)) + return arrayItems.length ? arrayItems : undefined + } + + if (isRecord(maybeSerializable)) { + const serializableEntries = Object.entries(maybeSerializable) + .map(([key, value]) => { + return [key, retainSerializableProperties(value, depth + 1)] + }) + .filter(([, value]) => isDefined(value)) + return serializableEntries.length ? Object.fromEntries(serializableEntries) : undefined + } + + return undefined +} - const objectFields: ObjectFields = - type.type?.jsonType === 'object' && type.type.type && shouldCreateDefinition - ? { - fields: type.type.fields.map((objectField) => transformField(objectField, context)), - } - : {} +function transformField(field: ObjectField, context: Context): ManifestField { + return { + ...transformCommonTypeFields(field.type, context), + name: field.name, + type: field.type.name, + } +} +function transformArrayMember( + arrayMember: ArraySchemaType, + context: Context, +): Pick { + return { + of: arrayMember.of.map((type) => { return { - name: type.name, - type: type.type?.name, - deprecated: type.type?.deprecated, - ...validation, - ...objectFields, - ...ensureString('title', type.type?.title), - ...ensureString('description', type.type?.description), - ...arrayProperties, - ...referenceProperties, - ...ensureBoolean('readOnly', type.type?.readOnly), - ...ensureBoolean('hidden', type.type?.hidden), + ...transformCommonTypeFields(type, context), + type: type.name, } }), } } -function transformReference(reference: ReferenceSchemaType): Pick { +function transformReference(reference: ReferenceSchemaType): Pick { return { to: (reference.to ?? []).map((type) => ({ + ...retainUserlandTypeProps(type), type: type.name, })), } } -type ValidationRuleTransformer< - Flag extends ManifestV1ValidationRule['flag'] = ManifestV1ValidationRule['flag'], -> = (rule: RuleSpec & {flag: Flag}) => ManifestV1ValidationRule & {flag: Flag} - -const transformTypeValidationRule: ValidationRuleTransformer<'type'> = (rule) => { +const transformTypeValidationRule: ValidationRuleTransformer = (rule) => { return { ...rule, - constraint: rule.constraint.toLowerCase() as ManifestV1TypeValidationRule['constraint'], // xxx + constraint: + 'constraint' in rule && + (typeof rule.constraint === 'string' + ? rule.constraint.toLowerCase() + : retainSerializableProperties(rule.constraint)), } } const validationRuleTransformers: Partial< - Record + Record > = { type: transformTypeValidationRule, } -function transformValidation(validation: SchemaValidationValue): ManifestV1ValidationGroup[] { +function transformValidation(validation: SchemaValidationValue): Validation { const validationArray = (Array.isArray(validation) ? validation : [validation]).filter( (value): value is Rule => typeof value === 'object' && '_type' in value, ) - // Custom validation rules cannot be serialized. - const disallowedFlags = ['custom'] + // we dont want type in the output as that is implicitly given by the typedef itself an will only bloat the payload + const disallowedFlags = ['type'] // Validation rules that refer to other fields use symbols, which cannot be serialized. It would // be possible to transform these to a serializable type, but we haven't implemented that for now. const disallowedConstraintTypes: (symbol | unknown)[] = [ConcreteRuleClass.FIELD_REF] - return validationArray.map(({_rules, _message, _level}) => { - // TODO: Handle insances of `LocalizedValidationMessages`. - const message: Partial> = - typeof _message === 'string' ? {message: _message} : {} + const serializedValidation = validationArray + .map(({_rules, _message, _level}) => { + const message: Partial> = + typeof _message === 'string' ? {message: _message} : {} - return { - rules: _rules + const serializedRules = _rules .filter((rule) => { if (!('constraint' in rule)) { return false @@ -241,104 +285,169 @@ function transformValidation(validation: SchemaValidationValue): ManifestV1Valid return false } - if ( + return !( typeof constraint === 'object' && 'type' in constraint && disallowedConstraintTypes.includes(constraint.type) - ) { - return false - } - - return true + ) }) - .reduce((rules, rule) => { + .reduce((rules, rule) => { const transformer: ValidationRuleTransformer = - validationRuleTransformers[rule.flag] ?? ((_) => _) - return [...rules, transformer(rule)] - }, []), - level: _level, - ...message, - } - }) + validationRuleTransformers[rule.flag] ?? + ((spec) => retainSerializableProperties(spec) as ManifestValidationRule) + + const transformedRule = transformer(rule) + if (!transformedRule) { + return rules + } + return [...rules, transformedRule] + }, []) + + return { + rules: serializedRules, + level: _level, + ...message, + } + }) + .filter((group) => !!group.rules.length) + + return serializedValidation.length ? {validation: serializedValidation} : {} } -function ensureString< - Key extends string, - const Value, - const DefaultValue extends string | undefined, ->( - key: Key, - value: Value, - defaultValue?: DefaultValue, -): Value extends string - ? Record - : [DefaultValue] extends [string] - ? Record - : Record { - if (typeof value === 'string') { - return { - [key]: value, - } as any // eslint-disable-line @typescript-eslint/no-explicit-any +function ensureCustomTitle(typeName: string, value: Value) { + const titleObject = ensureString('title', value) + + const defaultTitle = startCase(typeName) + + // omit title if its the same as default, to reduce payload + if (titleObject.title === defaultTitle) { + return {} } + return titleObject +} - if (typeof defaultValue === 'string') { +function ensureString(key: Key, value: Value) { + if (typeof value === 'string') { return { - [key]: defaultValue, - } as any // eslint-disable-line @typescript-eslint/no-explicit-any + [key]: value, + } } - return {} as any // eslint-disable-line @typescript-eslint/no-explicit-any + return {} } -function ensureBoolean< - Key extends string, - const Value, - const DefaultValue extends boolean | undefined, ->( - key: Key, - value: Value, - defaultValue?: DefaultValue, -): Value extends boolean - ? Record - : [DefaultValue] extends [boolean] - ? Record - : Record { +function ensureConditional(key: Key, value: Value) { if (typeof value === 'boolean') { return { [key]: value, - } as any // eslint-disable-line @typescript-eslint/no-explicit-any + } } - if (typeof defaultValue === 'boolean') { + if (typeof value === 'function') { return { - [key]: defaultValue, - } as any // eslint-disable-line @typescript-eslint/no-explicit-any + [key]: 'conditional', + } } - return {} as any // eslint-disable-line @typescript-eslint/no-explicit-any + return {} } function isReferenceSchemaType(type: unknown): type is ReferenceSchemaType { return typeof type === 'object' && type !== null && 'name' in type && type.name === 'reference' } -function isObjectField(maybeOjectField: unknown): maybeOjectField is MaybeCustomized { +function isObjectField(maybeOjectField: unknown) { return ( typeof maybeOjectField === 'object' && maybeOjectField !== null && 'name' in maybeOjectField ) } -function isMaybeCustomized( - maybeCustomized: unknown, -): maybeCustomized is MaybeCustomized { - return isObjectField(maybeCustomized) +function isCustomized(maybeCustomized: SchemaType) { + const hasFieldsArray = + isObjectField(maybeCustomized) && + !isType(maybeCustomized, 'reference') && + !isType(maybeCustomized, 'crossDatasetReference') && + 'fields' in maybeCustomized && + Array.isArray(maybeCustomized.fields) + + if (!hasFieldsArray) { + return false + } + + const fields = getCustomFields(maybeCustomized) + return !!fields.length } -function isCustomized(maybeCustomized: Type): maybeCustomized is Customized { - return ( - isObjectField(maybeCustomized) && - 'fields' in maybeCustomized.type && - isObjectField(maybeCustomized.type) && - maybeCustomized.type.fields.some((field) => isMaybeCustomized(field) && field.isCustomized) - ) +export function transformBlockType( + blockType: SchemaType, + context: Context, +): Pick | Record { + if (blockType.jsonType !== 'object' || !isType(blockType, 'block')) { + return {} + } + + const childrenField = blockType.fields?.find((field) => field.name === 'children') as + | {type: ArraySchemaType} + | undefined + + if (!childrenField) { + return {} + } + const ofType = childrenField.type.of + if (!ofType) { + return {} + } + const spanType = ofType.find((memberType) => memberType.name === 'span') as + | ObjectSchemaType + | undefined + if (!spanType) { + return {} + } + const inlineObjectTypes = (ofType.filter((memberType) => memberType.name !== 'span') || + []) as ObjectSchemaType[] + + return { + marks: { + annotations: (spanType as SpanSchemaType).annotations.map((t) => transformType(t, context)), + decorators: resolveEnabledDecorators(spanType), + }, + lists: resolveEnabledListItems(blockType), + styles: resolveEnabledStyles(blockType), + of: inlineObjectTypes.map((t) => transformType(t, context)), + } +} + +function resolveEnabledStyles(blockType: ObjectSchemaType): TitledValue[] | undefined { + const styleField = blockType.fields?.find((btField) => btField.name === 'style') + return resolveTitleValueArray(styleField?.type.options.list) +} + +function resolveEnabledDecorators(spanType: ObjectSchemaType): TitledValue[] | undefined { + return 'decorators' in spanType ? resolveTitleValueArray(spanType.decorators) : undefined +} + +function resolveEnabledListItems(blockType: ObjectSchemaType): TitledValue[] | undefined { + const listField = blockType.fields?.find((btField) => btField.name === 'listItem') + return resolveTitleValueArray(listField?.type?.options.list) +} + +function resolveTitleValueArray(possibleArray: unknown): TitledValue[] | undefined { + if (!possibleArray || !Array.isArray(possibleArray)) { + return undefined + } + const titledValues = possibleArray + .filter( + (d): d is {value: string; title?: string} => isRecord(d) && !!d.value && isString(d.value), + ) + .map((item) => { + return { + value: item.value, + ...ensureString('title', item.title), + } satisfies TitledValue + }) + if (!titledValues?.length) { + return undefined + } + + return titledValues } diff --git a/packages/sanity/src/_internal/manifest/manifestTypeHelpers.ts b/packages/sanity/src/_internal/manifest/manifestTypeHelpers.ts new file mode 100644 index 00000000000..80fd9026071 --- /dev/null +++ b/packages/sanity/src/_internal/manifest/manifestTypeHelpers.ts @@ -0,0 +1,60 @@ +import {type ObjectField, type ObjectSchemaType, type SchemaType} from '@sanity/types' + +const DEFAULT_IMAGE_FIELDS = ['asset', 'hotspot', 'crop'] +const DEFAULT_FILE_FIELDS = ['asset'] +const DEFAULT_GEOPOINT_FIELDS = ['lat', 'lng', 'alt'] +const DEFAULT_SLUG_FIELDS = ['current', 'source'] + +export function getCustomFields(type: ObjectSchemaType): ObjectField[] { + const fields = type.fields + if (isType(type, 'block')) { + return [] + } + if (isType(type, 'slug')) { + return fields.filter((f) => !DEFAULT_SLUG_FIELDS.includes(f.name)) + } + if (isType(type, 'geopoint')) { + return fields.filter((f) => !DEFAULT_GEOPOINT_FIELDS.includes(f.name)) + } + if (isType(type, 'image')) { + return fields.filter((f) => !DEFAULT_IMAGE_FIELDS.includes(f.name)) + } + if (isType(type, 'file')) { + return fields.filter((f) => !DEFAULT_FILE_FIELDS.includes(f.name)) + } + return fields +} + +export function isType(schemaType: SchemaType, typeName: string): boolean { + if (schemaType.name === typeName) { + return true + } + if (!schemaType.type) { + return false + } + return isType(schemaType.type, typeName) +} + +export function isDefined(value: T | null | undefined): value is T { + return value !== null && value !== undefined +} + +export function isRecord(value: unknown): value is Record { + return !!value && typeof value === 'object' +} + +export function isPrimitive(value: unknown): value is string | boolean | number { + return isString(value) || isBoolean(value) || isNumber(value) +} + +export function isString(value: unknown): value is string { + return typeof value === 'string' +} + +function isNumber(value: unknown): value is number { + return typeof value === 'boolean' +} + +function isBoolean(value: unknown): value is boolean { + return typeof value === 'number' +} diff --git a/packages/sanity/src/_internal/manifest/manifestTypes.ts b/packages/sanity/src/_internal/manifest/manifestTypes.ts new file mode 100644 index 00000000000..e1d9dbfcf73 --- /dev/null +++ b/packages/sanity/src/_internal/manifest/manifestTypes.ts @@ -0,0 +1,66 @@ +export type ManifestSerializable = + | string + | number + | boolean + | {[k: string]: ManifestSerializable} + | ManifestSerializable[] + +export interface ManifestV1 { + version: 1 + createdAt: string + workspaces: ManifestWorkspace[] +} + +export interface ManifestWorkspace { + name: string + dataset: string + schema: ManifestSchemaType[] +} + +export interface ManifestSchemaType { + type: string + name: string + title?: string + deprecated?: { + reason: string + } + readOnly?: boolean | 'function' + hidden?: boolean | 'function' + validation?: ManifestValidationGroup[] + fields?: ManifestField[] + to?: ManifestReferenceMember[] + of?: ManifestArrayMember[] + preview?: { + select: Record + } + options?: Record + + //portable text + marks?: { + annotations?: ManifestArrayMember[] + decorators?: TitledValue[] + } + lists?: TitledValue[] + styles?: TitledValue[] +} + +export interface TitledValue { + value: string + title?: string +} + +export type ManifestField = ManifestSchemaType +export type ManifestArrayMember = Omit & {name?: string} +export type ManifestReferenceMember = Omit & {name?: string} + +export interface ManifestValidationGroup { + rules: ManifestValidationRule[] + message?: string + level?: 'error' | 'warning' | 'info' +} + +export type ManifestValidationRule = { + flag: string + constraint?: ManifestSerializable + [index: string]: ManifestSerializable | undefined +} diff --git a/packages/sanity/test/manifest/extractManifest.test.ts b/packages/sanity/test/manifest/extractManifest.test.ts new file mode 100644 index 00000000000..da1e5e84435 --- /dev/null +++ b/packages/sanity/test/manifest/extractManifest.test.ts @@ -0,0 +1,773 @@ +/* eslint-disable camelcase */ +import {describe, expect, test} from '@jest/globals' +import {defineArrayMember, defineField, defineType} from '@sanity/types' + +import {extractManifestSchemaTypes} from '../../src/_internal/manifest/extractManifest' +import {createSchema} from '../../src/core' + +describe('Extract studio manifest', () => { + describe('serialize schema for manifest', () => { + test('extracted schema should only include user defined types (and no built-in types)', () => { + const documentType = 'basic' + const schema = createSchema({ + name: 'test', + types: [ + defineType({ + name: documentType, + type: 'document', + fields: [defineField({name: 'title', type: 'string'})], + }), + ], + }) + + const extracted = extractManifestSchemaTypes(schema) + expect(extracted.map((v) => v.name)).toStrictEqual([documentType]) + }) + + test('indicate conditional for function values on hidden and readOnly fields', () => { + const documentType = 'basic' + + const schema = createSchema({ + name: 'test', + types: [ + defineType({ + name: documentType, + type: 'document', + readOnly: true, + hidden: false, + fields: [ + defineField({ + name: 'string', + type: 'string', + hidden: () => true, + readOnly: () => false, + }), + ], + }), + ], + }) + + const extracted = extractManifestSchemaTypes(schema) + const serializedDoc = extracted.find((serialized) => serialized.name === documentType) + expect(serializedDoc).toEqual({ + type: 'document', + name: 'basic', + readOnly: true, + hidden: false, + fields: [ + { + name: 'string', + type: 'string', + hidden: 'conditional', + readOnly: 'conditional', + }, + ], + }) + }) + + test('should omit known non-serializable schema props ', () => { + const documentType = 'remove-props' + + const schema = createSchema({ + name: 'test', + types: [ + defineType({ + name: documentType, + type: 'document', + icon: () => 'remove-icon', + groups: [{name: 'groups-are-removed'}], + __experimental_omnisearch_visibility: true, + __experimental_search: [ + { + path: 'title', + weight: 100, + }, + ], + __experimental_formPreviewTitle: true, + components: { + field: () => 'remove-components', + }, + orderings: [ + {name: 'remove-orderings', title: '', by: [{field: 'title', direction: 'desc'}]}, + ], + fields: [ + defineField({ + name: 'string', + type: 'string', + }), + ], + preview: { + select: {title: 'remove-preview'}, + }, + }), + ], + }) + + const extracted = extractManifestSchemaTypes(schema) + const serializedDoc = extracted.find((serialized) => serialized.name === documentType) + expect(serializedDoc).toEqual({ + type: 'document', + name: documentType, + fields: [ + { + name: 'string', + type: 'string', + }, + ], + }) + }) + + test('schema should include most userland properties', () => { + const documentType = 'basic' + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const recursiveObject: any = { + repeat: 'string', + } + recursiveObject.recurse = recursiveObject + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const customization: any = { + recursiveObject, // this one will be cut off at max-depth + serializableProp: 'dummy', + nonSerializableProp: () => {}, + options: { + serializableOption: true, + nonSerializableOption: () => {}, + nested: { + serializableOption: 1, + nonSerializableOption: () => {}, + }, + }, + } + + const schema = createSchema({ + name: 'test', + types: [ + defineType({ + name: documentType, + type: 'document', + fields: [ + defineField({ + title: 'Nested', + name: 'nested', + type: 'object', + fields: [ + defineField({ + title: 'Nested inline string', + name: 'nestedString', + type: 'string', + ...customization, + }), + ], + ...customization, + }), + ], + ...customization, + }), + ], + }) + + const extracted = extractManifestSchemaTypes(schema) + + const expectedCustomProps = { + serializableProp: 'dummy', + options: { + serializableOption: true, + nested: { + serializableOption: 1, + }, + }, + recursiveObject: { + recurse: { + recurse: { + recurse: { + repeat: 'string', + }, + repeat: 'string', + }, + repeat: 'string', + }, + repeat: 'string', + }, + } + + const serializedDoc = extracted.find((serialized) => serialized.name === documentType) + expect(serializedDoc).toEqual({ + type: 'document', + name: 'basic', + fields: [ + { + name: 'nested', + title: 'Nested', + type: 'object', + fields: [ + { + name: 'nestedString', + title: 'Nested inline string', + type: 'string', + ...expectedCustomProps, + }, + ], + ...expectedCustomProps, + }, + ], + ...expectedCustomProps, + }) + }) + + test('should serialize fieldset config', () => { + const documentType = 'fieldsets' + + const schema = createSchema({ + name: 'test', + types: [ + defineType({ + name: documentType, + type: 'document', + fields: [ + defineField({ + name: 'string', + type: 'string', + }), + ], + preview: { + select: {title: 'title'}, + prepare: () => ({ + title: 'remove-prepare', + }), + }, + }), + ], + }) + + const extracted = extractManifestSchemaTypes(schema) + const serializedDoc = extracted.find((serialized) => serialized.name === documentType) + expect(serializedDoc).toEqual({ + type: 'document', + name: documentType, + fields: [ + { + name: 'string', + type: 'string', + }, + ], + }) + }) + + test('serialize fieldless types', () => { + const documentType = 'fieldless-types' + const schema = createSchema({ + name: 'test', + types: [ + defineType({ + title: 'Some document', + name: documentType, + type: 'document', + fields: [ + defineField({title: 'String field', name: 'string', type: 'string'}), + defineField({title: 'Text field', name: 'text', type: 'text'}), + defineField({title: 'Number field', name: 'number', type: 'number'}), + defineField({title: 'Boolean field', name: 'boolean', type: 'boolean'}), + defineField({title: 'Date field', name: 'date', type: 'date'}), + defineField({title: 'Datetime field', name: 'datetime', type: 'datetime'}), + defineField({title: 'Geopoint field', name: 'geopoint', type: 'geopoint'}), + defineField({title: 'Basic image field', name: 'image', type: 'image'}), + defineField({title: 'Basic file field', name: 'file', type: 'file'}), + defineField({title: 'Slug field', name: 'slug', type: 'slug'}), + defineField({title: 'URL field', name: 'url', type: 'url'}), + ], + }), + ], + }) + + const extracted = extractManifestSchemaTypes(schema) + + const serializedDoc = extracted.find((serialized) => serialized.name === documentType) + expect(serializedDoc).toEqual({ + fields: [ + {name: 'string', title: 'String field', type: 'string'}, + {name: 'text', title: 'Text field', type: 'text'}, + {name: 'number', title: 'Number field', type: 'number'}, + {name: 'boolean', title: 'Boolean field', type: 'boolean'}, + {name: 'date', title: 'Date field', type: 'date'}, + {name: 'datetime', title: 'Datetime field', type: 'datetime'}, + {name: 'geopoint', title: 'Geopoint field', type: 'geopoint'}, + {name: 'image', title: 'Basic image field', type: 'image'}, + {name: 'file', title: 'Basic file field', type: 'file'}, + { + name: 'slug', + title: 'Slug field', + type: 'slug', + validation: [{level: 'error', rules: [{flag: 'custom'}]}], + }, + { + name: 'url', + title: 'URL field', + type: 'url', + validation: [ + { + level: 'error', + rules: [ + { + constraint: { + options: { + allowCredentials: false, + allowRelative: false, + relativeOnly: false, + scheme: ['/^http$/', '/^https$/'], + }, + }, + flag: 'uri', + }, + ], + }, + ], + }, + ], + name: documentType, + title: 'Some document', + type: 'document', + }) + }) + + test('serialize types with fields', () => { + const documentType = 'field-types' + const schema = createSchema({ + name: 'test', + types: [ + defineType({ + fields: [ + { + fields: [ + { + name: 'nestedString', + title: 'Nested inline string', + type: 'string', + }, + { + fields: [ + { + name: 'inner', + title: 'Inner', + type: 'number', + }, + ], + name: 'nestedTwice', + title: 'Child object', + type: 'object', + }, + ], + name: 'nested', + title: 'Nested', + type: 'object', + }, + { + fields: [ + { + name: 'title', + title: 'Image title', + type: 'string', + }, + ], + name: 'image', + type: 'image', + }, + { + fields: [ + { + name: 'title', + title: 'File title', + type: 'string', + }, + ], + name: 'file', + type: 'file', + }, + ], + name: documentType, + type: 'document', + }), + ], + }) + + const extracted = extractManifestSchemaTypes(schema) + + const serializedDoc = extracted.find((serialized) => serialized.name === documentType) + expect(serializedDoc).toEqual({ + fields: [ + { + fields: [ + { + name: 'nestedString', + title: 'Nested inline string', + type: 'string', + }, + { + fields: [ + { + name: 'inner', + title: 'Inner', + type: 'number', + }, + ], + name: 'nestedTwice', + title: 'Child object', + type: 'object', + }, + ], + name: 'nested', + title: 'Nested', + type: 'object', + }, + { + fields: [ + { + name: 'title', + title: 'Image title', + type: 'string', + }, + ], + name: 'image', + type: 'image', + }, + { + fields: [ + { + name: 'title', + title: 'File title', + type: 'string', + }, + ], + name: 'file', + type: 'file', + }, + ], + name: documentType, + type: 'document', + }) + }) + + test('serialize array-like fields (portable text tested separately)', () => { + const documentType = 'all-types' + const schema = createSchema({ + name: 'test', + types: [ + defineType({ + title: 'Basic doc', + name: documentType, + type: 'document', + fields: [ + defineField({ + title: 'String array', + name: 'stringArray', + type: 'array', + of: [{type: 'string'}], + }), + defineField({ + title: 'Number array', + name: 'numberArray', + type: 'array', + of: [{type: 'number'}], + }), + defineField({ + title: 'Boolean array', + name: 'booleanArray', + type: 'array', + of: [{type: 'boolean'}], + }), + defineField({ + title: 'Object array', + name: 'objectArray', + type: 'array', + of: [ + defineArrayMember({ + title: 'Array item', + name: 'item', + type: 'object', + fields: [ + defineField({ + title: 'Item title', + name: 'itemTitle', + type: 'string', + }), + ], + }), + ], + }), + ], + }), + ], + }) + + const extracted = extractManifestSchemaTypes(schema) + + const serializedDoc = extracted.find((serialized) => serialized.name === documentType) + expect(serializedDoc).toEqual({ + fields: [ + { + name: 'stringArray', + of: [{type: 'string'}], + title: 'String array', + type: 'array', + }, + { + name: 'numberArray', + of: [{type: 'number'}], + title: 'Number array', + type: 'array', + }, + { + name: 'booleanArray', + of: [{type: 'boolean'}], + title: 'Boolean array', + type: 'array', + }, + { + name: 'objectArray', + of: [ + { + fields: [{name: 'itemTitle', title: 'Item title', type: 'string'}], + title: 'Array item', + type: 'item', + }, + ], + title: 'Object array', + type: 'array', + }, + ], + name: 'all-types', + title: 'Basic doc', + type: 'document', + }) + }) + + test('serialize array fields with typename override', () => { + const arrayType = 'someArray' + const objectBaseType = 'someObject' + const schema = createSchema({ + name: 'test', + types: [ + defineType({ + name: objectBaseType, + type: 'object', + fields: [ + defineField({ + name: 'title', + type: 'string', + }), + ], + }), + defineType({ + name: arrayType, + type: 'array', + of: [{type: objectBaseType, name: 'override'}], + }), + ], + }) + + const extracted = extractManifestSchemaTypes(schema) + + const serializedDoc = extracted.find((serialized) => serialized.name === arrayType) + expect(serializedDoc).toEqual({ + name: arrayType, + of: [ + { + fields: [{name: 'title', title: 'Title', type: 'string'}], + title: 'Some Object', + type: 'override', + }, + ], + type: 'array', + }) + }) + + test('serialize portable text field', () => { + const documentType = 'pt' + const schema = createSchema({ + name: 'test', + types: [ + defineType({ + name: documentType, + type: 'document', + fields: [ + defineField({ + title: 'Portable text', + name: 'pt', + type: 'array', + of: [ + defineArrayMember({ + title: 'Block', + name: 'block', + type: 'block', + of: [ + defineField({ + title: 'Inline block', + name: 'inlineBlock', + type: 'object', + fields: [ + defineField({ + title: 'Inline value', + name: 'value', + type: 'string', + }), + ], + }), + ], + marks: { + annotations: [ + defineField({ + title: 'Annotation', + name: 'annotation', + type: 'object', + fields: [ + defineField({ + title: 'Annotation value', + name: 'value', + type: 'string', + }), + ], + }), + ], + decorators: [{title: 'Custom mark', value: 'custom'}], + }, + lists: [{value: 'bullet', title: 'Bullet list'}], + styles: [{value: 'customStyle', title: 'Custom style'}], + }), + ], + }), + ], + }), + ], + }) + + const extracted = extractManifestSchemaTypes(schema) + + const serializedDoc = extracted.find((serialized) => serialized.name === documentType) + expect(serializedDoc).toEqual({ + fields: [ + { + name: 'pt', + of: [ + { + lists: [{title: 'Bullet list', value: 'bullet'}], + marks: { + annotations: [ + { + fields: [{name: 'value', title: 'Annotation value', type: 'string'}], + name: 'annotation', + type: 'object', + }, + ], + decorators: [{title: 'Custom mark', value: 'custom'}], + }, + of: [ + { + fields: [{name: 'value', title: 'Inline value', type: 'string'}], + name: 'inlineBlock', + title: 'Inline block', + type: 'object', + }, + ], + styles: [ + {title: 'Normal', value: 'normal'}, + {title: 'Custom style', value: 'customStyle'}, + ], + type: 'block', + }, + ], + title: 'Portable text', + type: 'array', + }, + ], + name: 'pt', + type: 'document', + }) + }) + + test('serialize fields with references', () => { + const documentType = 'ref-types' + const schema = createSchema({ + name: 'test', + types: [ + defineType({ + name: documentType, + type: 'document', + fields: [ + defineField({ + title: 'Reference to', + name: 'reference', + type: 'reference', + to: [{type: documentType}], + }), + defineField({ + title: 'Cross dataset ref', + name: 'crossDatasetReference', + type: 'crossDatasetReference', + dataset: 'production', + studioUrl: () => 'cannot serialize studioUrl function', + to: [ + { + type: documentType, + preview: { + select: {title: 'title'}, + prepare: () => ({ + title: 'cannot serialize prepare function', + }), + }, + }, + ], + }), + defineField({ + title: 'Reference array', + name: 'objectArray', + type: 'array', + of: [ + defineArrayMember({ + title: 'Reference to', + name: 'reference', + type: 'reference', + to: [{type: documentType}], + }), + ], + }), + ], + }), + ], + }) + + const extracted = extractManifestSchemaTypes(schema) + + const serializedDoc = extracted.find((serialized) => serialized.name === documentType) + expect(serializedDoc).toEqual({ + fields: [ + { + name: 'reference', + title: 'Reference to', + to: [{type: documentType}], + type: 'reference', + }, + { + dataset: 'production', + name: 'crossDatasetReference', + title: 'Cross dataset ref', + type: 'crossDatasetReference', + }, + { + name: 'objectArray', + of: [ + { + title: 'Reference to', + to: [{type: documentType}], + type: 'reference', + }, + ], + title: 'Reference array', + type: 'array', + }, + ], + name: documentType, + type: 'document', + }) + }) + }) +}) diff --git a/packages/sanity/test/manifest/extractManifestValidation.test.ts b/packages/sanity/test/manifest/extractManifestValidation.test.ts new file mode 100644 index 00000000000..6af4f4aedb2 --- /dev/null +++ b/packages/sanity/test/manifest/extractManifestValidation.test.ts @@ -0,0 +1,515 @@ +/* eslint-disable camelcase */ +import {describe, expect, test} from '@jest/globals' +import {defineField, defineType} from '@sanity/types' + +import {extractManifestSchemaTypes} from '../../src/_internal/manifest/extractManifest' +import {createSchema} from '../../src/core' + +describe('Extract studio manifest', () => { + describe('serialize validation rules', () => { + test('object validation rules', () => { + const docType = 'some-doc' + const schema = createSchema({ + name: 'test', + types: [ + defineType({ + name: docType, + type: 'document', + fields: [defineField({name: 'title', type: 'string'})], + validation: (rule) => [ + rule + .required() + .custom(() => 'doesnt-matter') + .warning('custom-warning'), + rule.custom(() => 'doesnt-matter').error('custom-error'), + rule.custom(() => 'doesnt-matter').info('custom-info'), + ], + }), + ], + }) + + const extracted = extractManifestSchemaTypes(schema) + const validation = extracted.find((e) => e.name === docType)?.validation + expect(validation).toEqual([ + { + level: 'warning', + message: 'custom-warning', + rules: [{constraint: 'required', flag: 'presence'}, {flag: 'custom'}], + }, + { + level: 'error', + message: 'custom-error', + rules: [{flag: 'custom'}], + }, + { + level: 'info', + message: 'custom-info', + rules: [{flag: 'custom'}], + }, + ]) + }) + + test('array validation rules', () => { + const type = 'someArray' + const schema = createSchema({ + name: 'test', + types: [ + defineType({ + name: type, + type: 'array', + of: [{type: 'string'}], + validation: (rule) => [ + rule + .required() + .unique() + .min(1) + .max(10) + .length(10) + .custom(() => 'doesnt-matter'), + ], + }), + ], + }) + + const extracted = extractManifestSchemaTypes(schema) + const validation = extracted.find((e) => e.name === type)?.validation + expect(validation).toEqual([ + { + level: 'error', + rules: [ + {constraint: 'required', flag: 'presence'}, + {constraint: 1, flag: 'min'}, + {constraint: 10, flag: 'max'}, + {constraint: 10, flag: 'length'}, + {flag: 'custom'}, + ], + }, + ]) + }) + + test('boolean validation rules', () => { + const type = 'someArray' + const schema = createSchema({ + name: 'test', + types: [ + defineType({ + name: type, + type: 'boolean', + validation: (rule) => [rule.required().custom(() => 'doesnt-matter')], + }), + ], + }) + + const extracted = extractManifestSchemaTypes(schema) + const validation = extracted.find((e) => e.name === type)?.validation + expect(validation).toEqual([ + { + level: 'error', + rules: [{constraint: 'required', flag: 'presence'}, {flag: 'custom'}], + }, + ]) + }) + + test('date validation rules', () => { + const type = 'someDate' + const schema = createSchema({ + name: 'test', + types: [ + defineType({ + name: type, + type: 'date', + validation: (rule) => [ + rule + .required() + .min('2022-01-01') + .max('2022-01-02') + .custom(() => 'doesnt-matter'), + ], + }), + ], + }) + + const extracted = extractManifestSchemaTypes(schema) + const validation = extracted.find((e) => e.name === type)?.validation + expect(validation).toEqual([ + { + level: 'error', + rules: [ + {constraint: 'required', flag: 'presence'}, + {constraint: '2022-01-01', flag: 'min'}, + {constraint: '2022-01-02', flag: 'max'}, + {flag: 'custom'}, + ], + }, + ]) + }) + + test('image validation rules', () => { + const type = 'someImage' + const schema = createSchema({ + name: 'test', + types: [ + defineType({ + name: type, + type: 'image', + validation: (rule) => [ + rule + .required() + .assetRequired() + .custom(() => 'doesnt-matter'), + ], + }), + ], + }) + + const extracted = extractManifestSchemaTypes(schema) + const validation = extracted.find((e) => e.name === type)?.validation + expect(validation).toEqual([ + { + level: 'error', + rules: [ + {constraint: 'required', flag: 'presence'}, + {constraint: {assetType: 'image'}, flag: 'assetRequired'}, + {flag: 'custom'}, + ], + }, + ]) + }) + + test('file validation rules', () => { + const type = 'someFile' + const schema = createSchema({ + name: 'test', + types: [ + defineType({ + name: type, + type: 'file', + validation: (rule) => [ + rule + .required() + .assetRequired() + .custom(() => 'doesnt-matter'), + ], + }), + ], + }) + + const extracted = extractManifestSchemaTypes(schema) + const validation = extracted.find((e) => e.name === type)?.validation + expect(validation).toEqual([ + { + level: 'error', + rules: [ + {constraint: 'required', flag: 'presence'}, + {constraint: {assetType: 'file'}, flag: 'assetRequired'}, + {flag: 'custom'}, + ], + }, + ]) + }) + + test('number validation rules', () => { + const type = 'someNumber' + const schema = createSchema({ + name: 'test', + types: [ + defineType({ + name: type, + type: 'number', + validation: (rule) => [ + rule + .custom(() => 'doesnt-matter') + .required() + .min(1) + .max(2), + rule.integer().positive(), + rule.greaterThan(-4).negative(), + rule.precision(2).lessThan(5), + ], + }), + ], + }) + + const extracted = extractManifestSchemaTypes(schema) + const validation = extracted.find((e) => e.name === type)?.validation + expect(validation).toEqual([ + { + level: 'error', + rules: [ + {flag: 'custom'}, + {constraint: 'required', flag: 'presence'}, + {constraint: 1, flag: 'min'}, + {constraint: 2, flag: 'max'}, + ], + }, + { + level: 'error', + rules: [{constraint: 0, flag: 'min'}], + }, + { + level: 'error', + rules: [ + {constraint: -4, flag: 'greaterThan'}, + {constraint: 0, flag: 'lessThan'}, + ], + }, + { + level: 'error', + rules: [ + {constraint: 2, flag: 'precision'}, + {constraint: 5, flag: 'lessThan'}, + ], + }, + ]) + }) + + test('reference validation rules', () => { + const type = 'someRef' + const docType = 'doc' + const schema = createSchema({ + name: 'test', + types: [ + defineType({ + type: 'document', + name: docType, + fields: [ + defineField({ + type: 'string', + name: 'title', + }), + ], + }), + defineType({ + name: type, + type: 'reference', + to: [{type: docType}], + validation: (rule) => rule.required().custom(() => 'doesnt-matter'), + }), + ], + }) + + const extracted = extractManifestSchemaTypes(schema) + const validation = extracted.find((e) => e.name === type)?.validation + expect(validation).toEqual([ + { + level: 'error', + rules: [{constraint: 'required', flag: 'presence'}, {flag: 'custom'}], + }, + ]) + }) + + test('slug validation rules', () => { + const type = 'someSlug' + const schema = createSchema({ + name: 'test', + types: [ + defineType({ + name: type, + type: 'slug', + validation: (rule) => rule.required().custom(() => 'doesnt-matter'), + }), + ], + }) + + const extracted = extractManifestSchemaTypes(schema) + const validation = extracted.find((e) => e.name === type)?.validation + expect(validation).toEqual([ + { + level: 'error', + rules: [ + { + flag: 'custom', // this is the default unique checking rule + }, + { + constraint: 'required', + flag: 'presence', + }, + { + flag: 'custom', + }, + ], + }, + ]) + }) + + test('string validation rules', () => { + const type = 'someString' + const schema = createSchema({ + name: 'test', + types: [ + defineType({ + name: type, + type: 'string', + validation: (rule) => [ + rule + .required() + .max(50) + .min(5) + .length(10) + .uppercase() + .lowercase() + .regex(/a+/, 'test', {name: 'yeah', invert: true}) + .regex(/a+/, {name: 'yeah', invert: true}) + .regex(/a+/, 'test') + .regex(/a+/) + .email() + .custom(() => 'doesnt-matter'), + ], + }), + ], + }) + + const extracted = extractManifestSchemaTypes(schema) + const validation = extracted.find((e) => e.name === type)?.validation + expect(validation).toEqual([ + { + level: 'error', + rules: [ + {constraint: 'required', flag: 'presence'}, + {constraint: 50, flag: 'max'}, + {constraint: 5, flag: 'min'}, + {constraint: 10, flag: 'length'}, + {constraint: 'uppercase', flag: 'stringCasing'}, + {constraint: 'lowercase', flag: 'stringCasing'}, + { + constraint: { + invert: false, + name: 'test', + pattern: '/a+/', + }, + flag: 'regex', + }, + { + constraint: { + invert: true, + name: 'yeah', + pattern: '/a+/', + }, + flag: 'regex', + }, + { + constraint: { + invert: false, + name: 'test', + pattern: '/a+/', + }, + flag: 'regex', + }, + { + constraint: { + invert: false, + pattern: '/a+/', + }, + flag: 'regex', + }, + { + flag: 'custom', + }, + ], + }, + ]) + }) + + test('url validation rules', () => { + const type = 'someUrl' + const schema = createSchema({ + name: 'test', + types: [ + defineType({ + name: type, + type: 'url', + validation: (rule) => [ + rule.required().custom(() => 'doesnt-matter'), + rule.uri({scheme: 'ftp'}), + rule.uri({ + scheme: ['https'], + allowCredentials: true, + allowRelative: true, + relativeOnly: false, + }), + rule.uri({ + scheme: /^custom-protocol.*$/g, + }), + ], + }), + ], + }) + + const extracted = extractManifestSchemaTypes(schema) + const validation = extracted.find((e) => e.name === type)?.validation + expect(validation).toEqual([ + { + level: 'error', + rules: [ + { + constraint: { + options: { + allowCredentials: false, + allowRelative: false, + relativeOnly: false, + scheme: ['/^http$/', '/^https$/'], + }, + }, + flag: 'uri', + }, + { + constraint: 'required', + flag: 'presence', + }, + { + flag: 'custom', + }, + ], + }, + { + level: 'error', + rules: [ + { + constraint: { + options: { + allowCredentials: false, + allowRelative: false, + relativeOnly: false, + scheme: ['/^ftp$/'], + }, + }, + flag: 'uri', + }, + ], + }, + { + level: 'error', + rules: [ + { + constraint: { + options: { + allowCredentials: true, + allowRelative: true, + relativeOnly: false, + scheme: ['/^https$/'], + }, + }, + flag: 'uri', + }, + ], + }, + { + level: 'error', + rules: [ + { + constraint: { + options: { + allowCredentials: false, + allowRelative: false, + relativeOnly: false, + scheme: ['/^custom-protocol.*$/g'], + }, + }, + flag: 'uri', + }, + ], + }, + ]) + }) + }) +}) From 71db59af7bce55713885cce05892724822fb6179 Mon Sep 17 00:00:00 2001 From: Snorre Eskeland Brekke Date: Thu, 29 Aug 2024 00:48:09 +0200 Subject: [PATCH 32/49] fix: remove @sanity/manifest package --- .github/CODEOWNERS | 4 - dev/aliases.cjs | 1 - dev/embedded-studio/package.json | 2 +- dev/embedded-studio/sanity.cli.ts | 1 - dev/starter-next-studio/.gitignore | 3 + dev/starter-next-studio/package.json | 2 +- dev/test-studio/sanity.cli.ts | 2 - dev/tsconfig.dev.json | 2 - examples/tsconfig.json | 2 - packages/@repo/test-exports/.depcheckrc.json | 1 - packages/@repo/test-exports/package.json | 1 - packages/@sanity/cli/src/types.ts | 20 ---- packages/@sanity/manifest/.depcheckrc.json | 3 - packages/@sanity/manifest/.eslintrc.cjs | 11 -- packages/@sanity/manifest/.gitignore | 15 --- packages/@sanity/manifest/README.md | 1 - packages/@sanity/manifest/jest.config.cjs | 8 -- packages/@sanity/manifest/package.config.ts | 4 - packages/@sanity/manifest/package.json | 58 --------- .../@sanity/manifest/src/_exports/index.ts | 3 - .../@sanity/manifest/src/schema/v1/index.ts | 112 ------------------ packages/@sanity/manifest/tsconfig.json | 10 -- packages/@sanity/manifest/tsconfig.lib.json | 8 -- packages/@sanity/manifest/turbo.json | 9 -- .../cli/actions/build/buildAction.ts | 25 ++-- .../actions/manifest/extractManifestAction.ts | 92 +++++++------- .../cli/actions/schema/extractAction.ts | 11 +- .../src/_internal/cli/commands/index.ts | 6 - .../manifest/extractManifestCommand.ts | 30 ----- .../commands/manifest/listManifestsCommand.ts | 30 ----- .../cli/commands/manifest/manifestGroup.ts | 10 -- .../commands/schema/extractSchemaCommand.ts | 9 +- .../_internal/cli/threads/extractSchema.ts | 10 +- .../src/_internal/manifest/manifestTypes.ts | 8 +- packages/sanity/tsconfig.json | 2 - 35 files changed, 83 insertions(+), 433 deletions(-) delete mode 100644 packages/@sanity/manifest/.depcheckrc.json delete mode 100644 packages/@sanity/manifest/.eslintrc.cjs delete mode 100644 packages/@sanity/manifest/.gitignore delete mode 100644 packages/@sanity/manifest/README.md delete mode 100644 packages/@sanity/manifest/jest.config.cjs delete mode 100644 packages/@sanity/manifest/package.config.ts delete mode 100644 packages/@sanity/manifest/package.json delete mode 100644 packages/@sanity/manifest/src/_exports/index.ts delete mode 100644 packages/@sanity/manifest/src/schema/v1/index.ts delete mode 100644 packages/@sanity/manifest/tsconfig.json delete mode 100644 packages/@sanity/manifest/tsconfig.lib.json delete mode 100644 packages/@sanity/manifest/turbo.json delete mode 100644 packages/sanity/src/_internal/cli/commands/manifest/extractManifestCommand.ts delete mode 100644 packages/sanity/src/_internal/cli/commands/manifest/listManifestsCommand.ts delete mode 100644 packages/sanity/src/_internal/cli/commands/manifest/manifestGroup.ts diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index f377c66a3bd..236179f9fb4 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -98,7 +98,3 @@ packages/sanity/src/core/studio/upsell/ @sanity-io/studio-ex /packages/sanity/src/structure/panes/documentList/PaneContainer.tsx @sanity-io/ecosystem @sanity-io/studio-dx /packages/sanity/src/structure/StructureToolProvider.tsx @sanity-io/ecosystem @sanity-io/studio-dx /packages/sanity/src/structure/types.ts @sanity-io/ecosystem @sanity-io/studio-dx - -# -- Manifest -- -/packages/@sanity/manifest @sanity-io/studio-dx - diff --git a/dev/aliases.cjs b/dev/aliases.cjs index ffc4d50282e..15f0782ae48 100644 --- a/dev/aliases.cjs +++ b/dev/aliases.cjs @@ -21,7 +21,6 @@ const devAliases = { '@sanity/cli': './packages/@sanity/cli/src', '@sanity/mutator': './packages/@sanity/mutator/src', '@sanity/schema': './packages/@sanity/schema/src/_exports', - '@sanity/manifest': './packages/@sanity/manifrst/src/_exports', '@sanity/migrate': './packages/@sanity/migrate/src/_exports', '@sanity/types': './packages/@sanity/types/src', '@sanity/util': './packages/@sanity/util/src/_exports', diff --git a/dev/embedded-studio/package.json b/dev/embedded-studio/package.json index ca346158fad..8713d2bc533 100644 --- a/dev/embedded-studio/package.json +++ b/dev/embedded-studio/package.json @@ -3,7 +3,7 @@ "version": "3.57.1", "private": true, "scripts": { - "build": "tsc && vite build && sanity manifest extract", + "build": "tsc && vite build && sanity schema extract --format manifest", "dev": "vite", "preview": "vite preview" }, diff --git a/dev/embedded-studio/sanity.cli.ts b/dev/embedded-studio/sanity.cli.ts index 5f8416de33d..fac247bf8cf 100644 --- a/dev/embedded-studio/sanity.cli.ts +++ b/dev/embedded-studio/sanity.cli.ts @@ -5,5 +5,4 @@ export default defineCliConfig({ projectId: 'ppsg7ml5', dataset: 'test', }, - unstable_staticAssetsPath: './dist/assets', }) diff --git a/dev/starter-next-studio/.gitignore b/dev/starter-next-studio/.gitignore index a680367ef56..28d9bc42059 100644 --- a/dev/starter-next-studio/.gitignore +++ b/dev/starter-next-studio/.gitignore @@ -1 +1,4 @@ .next + +public/static/*.studioschema.json +public/static/v1.studiomanifest.json diff --git a/dev/starter-next-studio/package.json b/dev/starter-next-studio/package.json index a511780da5e..a5d82cbe412 100644 --- a/dev/starter-next-studio/package.json +++ b/dev/starter-next-studio/package.json @@ -5,7 +5,7 @@ "license": "MIT", "author": "Sanity.io ", "scripts": { - "build": "sanity manifest extract && next build", + "build": "sanity schema extract --format manifest --path public/static && next build", "dev": "next dev", "start": "next start" }, diff --git a/dev/test-studio/sanity.cli.ts b/dev/test-studio/sanity.cli.ts index c01d5b1c250..085d8f34600 100644 --- a/dev/test-studio/sanity.cli.ts +++ b/dev/test-studio/sanity.cli.ts @@ -9,8 +9,6 @@ export default defineCliConfig({ dataset: 'test', }, - unstable_extractManifestOnBuild: true, - // Can be overriden by: // A) `SANITY_STUDIO_REACT_STRICT_MODE=false pnpm dev` // B) creating a `.env` file locally that sets the same env variable as above diff --git a/dev/tsconfig.dev.json b/dev/tsconfig.dev.json index 580b1d60850..5e1ee610513 100644 --- a/dev/tsconfig.dev.json +++ b/dev/tsconfig.dev.json @@ -10,8 +10,6 @@ "@sanity/cli": ["./packages/@sanity/cli/src/index.ts"], "@sanity/codegen": ["./packages/@sanity/codegen/src/_exports/index.ts"], "@sanity/mutator": ["./packages/@sanity/mutator/src/index.ts"], - "@sanity/manifest/*": ["./packages/@sanity/manifest/src/_exports/*"], - "@sanity/manifest": ["./packages/@sanity/manifest/src/_exports/index.ts"], "@sanity/schema/*": ["./packages/@sanity/schema/src/_exports/*"], "@sanity/schema": ["./packages/@sanity/schema/src/_exports/index.ts"], "@sanity/migrate": ["./packages/@sanity/migrate/src/_exports/index.ts"], diff --git a/examples/tsconfig.json b/examples/tsconfig.json index 580b1d60850..5e1ee610513 100644 --- a/examples/tsconfig.json +++ b/examples/tsconfig.json @@ -10,8 +10,6 @@ "@sanity/cli": ["./packages/@sanity/cli/src/index.ts"], "@sanity/codegen": ["./packages/@sanity/codegen/src/_exports/index.ts"], "@sanity/mutator": ["./packages/@sanity/mutator/src/index.ts"], - "@sanity/manifest/*": ["./packages/@sanity/manifest/src/_exports/*"], - "@sanity/manifest": ["./packages/@sanity/manifest/src/_exports/index.ts"], "@sanity/schema/*": ["./packages/@sanity/schema/src/_exports/*"], "@sanity/schema": ["./packages/@sanity/schema/src/_exports/index.ts"], "@sanity/migrate": ["./packages/@sanity/migrate/src/_exports/index.ts"], diff --git a/packages/@repo/test-exports/.depcheckrc.json b/packages/@repo/test-exports/.depcheckrc.json index 7f5e17b1718..7be5e5048b1 100644 --- a/packages/@repo/test-exports/.depcheckrc.json +++ b/packages/@repo/test-exports/.depcheckrc.json @@ -5,7 +5,6 @@ "@sanity/cli", "@sanity/codegen", "@sanity/diff", - "@sanity/manifest", "@sanity/migrate", "@sanity/mutator", "@sanity/schema", diff --git a/packages/@repo/test-exports/package.json b/packages/@repo/test-exports/package.json index df8de384246..45d63920fbd 100644 --- a/packages/@repo/test-exports/package.json +++ b/packages/@repo/test-exports/package.json @@ -14,7 +14,6 @@ "@sanity/cli": "workspace:*", "@sanity/codegen": "workspace:*", "@sanity/diff": "workspace:*", - "@sanity/manifest": "workspace:*", "@sanity/migrate": "workspace:*", "@sanity/mutator": "workspace:*", "@sanity/schema": "workspace:*", diff --git a/packages/@sanity/cli/src/types.ts b/packages/@sanity/cli/src/types.ts index f5733745a60..ac85b2d5666 100644 --- a/packages/@sanity/cli/src/types.ts +++ b/packages/@sanity/cli/src/types.ts @@ -314,26 +314,6 @@ export interface CliConfig { autoUpdates?: boolean studioHost?: string - - /** - * Whether or not to extract a Studio Manifest to the static assets directory when building - * (or deploying) the project. - * - * Optional, defaults to `false`. - */ - unstable_extractManifestOnBuild?: boolean - - /** - * The path Sanity CLI will write static assets (such as the Studio Manifest) to when used inside - * an embedded Studio project. Relative to the CLI config file. - * - * Sanity CLI will attempt to create this path if it does not exist. - * - * You should not define a value if your Studio is not embedded. - * - * Optional, defaults to `dist/static`. - */ - unstable_staticAssetsPath?: string } export type UserViteConfig = diff --git a/packages/@sanity/manifest/.depcheckrc.json b/packages/@sanity/manifest/.depcheckrc.json deleted file mode 100644 index 35f1b4badf9..00000000000 --- a/packages/@sanity/manifest/.depcheckrc.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "ignores": ["@repo/tsconfig", "@sanity/pkg-utils"] -} diff --git a/packages/@sanity/manifest/.eslintrc.cjs b/packages/@sanity/manifest/.eslintrc.cjs deleted file mode 100644 index 99fd6c69224..00000000000 --- a/packages/@sanity/manifest/.eslintrc.cjs +++ /dev/null @@ -1,11 +0,0 @@ -'use strict' - -const path = require('path') - -const ROOT_PATH = path.resolve(__dirname, '../../..') - -module.exports = { - rules: { - 'import/no-extraneous-dependencies': ['error', {packageDir: [ROOT_PATH, __dirname]}], - }, -} diff --git a/packages/@sanity/manifest/.gitignore b/packages/@sanity/manifest/.gitignore deleted file mode 100644 index cd7c1010a8d..00000000000 --- a/packages/@sanity/manifest/.gitignore +++ /dev/null @@ -1,15 +0,0 @@ -# Logs -/logs -*.log - -# Coverage directory used by tools like istanbul -/coverage - -# Dependency directories -/node_modules - -# Compiled code -/lib - -# Legacy exports -/_internal.js diff --git a/packages/@sanity/manifest/README.md b/packages/@sanity/manifest/README.md deleted file mode 100644 index 0532c79425c..00000000000 --- a/packages/@sanity/manifest/README.md +++ /dev/null @@ -1 +0,0 @@ -# Sanity Manifest diff --git a/packages/@sanity/manifest/jest.config.cjs b/packages/@sanity/manifest/jest.config.cjs deleted file mode 100644 index 51ecfb62217..00000000000 --- a/packages/@sanity/manifest/jest.config.cjs +++ /dev/null @@ -1,8 +0,0 @@ -'use strict' - -const {createJestConfig} = require('../../../test/config.cjs') - -module.exports = createJestConfig({ - displayName: require('./package.json').name, - testEnvironment: 'node', -}) diff --git a/packages/@sanity/manifest/package.config.ts b/packages/@sanity/manifest/package.config.ts deleted file mode 100644 index c43051dd053..00000000000 --- a/packages/@sanity/manifest/package.config.ts +++ /dev/null @@ -1,4 +0,0 @@ -import baseConfig from '@repo/package.config' -import {defineConfig} from '@sanity/pkg-utils' - -export default defineConfig(baseConfig) diff --git a/packages/@sanity/manifest/package.json b/packages/@sanity/manifest/package.json deleted file mode 100644 index e45c87a0948..00000000000 --- a/packages/@sanity/manifest/package.json +++ /dev/null @@ -1,58 +0,0 @@ -{ - "name": "@sanity/manifest", - "version": "3.39.1", - "description": "", - "keywords": [ - "sanity", - "cms", - "headless", - "realtime", - "content", - "schema" - ], - "homepage": "https://www.sanity.io/", - "bugs": { - "url": "https://github.com/sanity-io/sanity/issues" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/sanity-io/sanity.git", - "directory": "packages/@sanity/manifest" - }, - "license": "MIT", - "author": "Sanity.io ", - "sideEffects": false, - "exports": { - ".": { - "source": "./src/_exports/index.ts", - "import": "./lib/index.mjs", - "require": "./lib/index.js", - "default": "./lib/index.js" - }, - "./package.json": "./package.json" - }, - "main": "./lib/index.js", - "module": "./lib/index.esm.js", - "types": "./lib/index.d.ts", - "files": [ - "lib", - "src" - ], - "scripts": { - "build": "pkg-utils build --strict --check --clean", - "check:types": "tsc --project tsconfig.lib.json", - "clean": "rimraf lib", - "lint": "eslint .", - "prepublishOnly": "turbo run build", - "test": "jest", - "test:watch": "jest --watchAll", - "watch": "pkg-utils watch" - }, - "dependencies": { - "zod": "^3.22.4" - }, - "devDependencies": { - "@repo/package.config": "workspace:*", - "rimraf": "^3.0.2" - } -} diff --git a/packages/@sanity/manifest/src/_exports/index.ts b/packages/@sanity/manifest/src/_exports/index.ts deleted file mode 100644 index 31e9b807fc8..00000000000 --- a/packages/@sanity/manifest/src/_exports/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -//export * from '../schema/v1' - -export const DUMMY = '' diff --git a/packages/@sanity/manifest/src/schema/v1/index.ts b/packages/@sanity/manifest/src/schema/v1/index.ts deleted file mode 100644 index 1563d874603..00000000000 --- a/packages/@sanity/manifest/src/schema/v1/index.ts +++ /dev/null @@ -1,112 +0,0 @@ -import z from 'zod' - -export const manifestV1Deprecation = z.object({ - reason: z.string(), -}) - -export const manifestV1TypeValidationRule = z.object({ - flag: z.literal('type'), - constraint: z.union([ - z.literal('array'), - z.literal('boolean'), - z.literal('date'), - z.literal('number'), - z.literal('object'), - z.literal('string'), - ]), -}) - -export type ManifestV1TypeValidationRule = z.infer - -// TOOD: Constraints -export const manifestV1UriValidationRule = z.object({ - flag: z.literal('uri'), -}) - -// TODO -// export const manifestV1ValidationRule = z.any() -export const manifestV1ValidationRule = z.union([ - manifestV1TypeValidationRule, - manifestV1UriValidationRule, - // TODO: Remove - z.any(), -]) - -export type ManifestV1ValidationRule = z.infer - -export const manifestV1ValidationGroup = z.object({ - rules: z.array(manifestV1ValidationRule), - message: z.string().optional(), - level: z.union([z.literal('error'), z.literal('warning'), z.literal('info')]).optional(), -}) - -export type ManifestV1ValidationGroup = z.infer - -export const manifestV1Reference = z.object({ - type: z.string(), -}) - -export type ManifestV1Reference = z.infer - -export const manifestV1ReferenceGroup = z.array(manifestV1Reference) - -export type ManifestV1ReferenceGroup = z.infer - -const _base = z.object({ - type: z.string(), - name: z.string(), - title: z.string().optional(), - description: z.string().optional(), - deprecated: manifestV1Deprecation.optional(), - readOnly: z.boolean().optional(), // xxx - hidden: z.boolean().optional(), // xxx - validation: z.array(manifestV1ValidationGroup).optional(), - to: manifestV1ReferenceGroup.optional(), - of: z.any(), - preview: z - .object({ - select: z.record(z.string(), z.string()), - }) - .optional(), -}) - -const manifestV1TypeBase: z.ZodType = _base.extend({ - fields: z.array(z.lazy(() => manifestV1Field)).optional(), -}) - -export const manifestV1Field = manifestV1TypeBase - -export type ManifestV1Field = z.infer - -// export const ManifestV1TypeSchema = ManifestV1TypeBaseSchema.extend({ -// readOnly: z.boolean().optional(), -// hidden: z.boolean().optional(), -// preview: z -// .object({ -// select: z.record(z.string(), z.string()), -// }) -// .optional(), -// }) - -export type ManifestV1Type = z.infer & { - fields?: ManifestV1Field[] -} - -export const manifestV1Schema = z.array(manifestV1TypeBase) - -export const ManifestSchema = z.object({manifestVersion: z.number()}) - -export const manifestV1Workspace = z.object({ - name: z.string(), - dataset: z.string(), - schema: z.union([manifestV1Schema, z.string()]), // xxx don't actually want string here, but allows us to replace with filename -}) - -export type ManifestV1Workspace = z.infer - -export const manifestV1 = ManifestSchema.extend({ - createdAt: z.date(), - workspaces: z.array(manifestV1Workspace), -}) - -export type ManifestV1 = z.infer diff --git a/packages/@sanity/manifest/tsconfig.json b/packages/@sanity/manifest/tsconfig.json deleted file mode 100644 index 8d4a1d085ad..00000000000 --- a/packages/@sanity/manifest/tsconfig.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "extends": "@repo/tsconfig/base.json", - "include": ["./example", "./src", "./test", "./typings", "./node_modules/@sanity/types/src"], - "compilerOptions": { - "rootDir": ".", - "paths": { - "@sanity/types": ["./node_modules/@sanity/types/src"] - } - } -} diff --git a/packages/@sanity/manifest/tsconfig.lib.json b/packages/@sanity/manifest/tsconfig.lib.json deleted file mode 100644 index 18d75ddfa7f..00000000000 --- a/packages/@sanity/manifest/tsconfig.lib.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "@repo/tsconfig/build.json", - "include": ["./example", "./src", "./typings"], - "compilerOptions": { - "rootDir": ".", - "outDir": "./lib" - } -} diff --git a/packages/@sanity/manifest/turbo.json b/packages/@sanity/manifest/turbo.json deleted file mode 100644 index 19b4a243ca2..00000000000 --- a/packages/@sanity/manifest/turbo.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "$schema": "https://turbo.build/schema.json", - "extends": ["//"], - "tasks": { - "build": { - "outputs": ["lib/**", "index.js"] - } - } -} diff --git a/packages/sanity/src/_internal/cli/actions/build/buildAction.ts b/packages/sanity/src/_internal/cli/actions/build/buildAction.ts index c0dfe51fe9e..c157c3ef63c 100644 --- a/packages/sanity/src/_internal/cli/actions/build/buildAction.ts +++ b/packages/sanity/src/_internal/cli/actions/build/buildAction.ts @@ -16,8 +16,7 @@ import {BuildTrace} from './build.telemetry' import {buildVendorDependencies} from '../../server/buildVendorDependencies' import {compareStudioDependencyVersions} from '../../util/compareStudioDependencyVersions' import {getAutoUpdateImportMap} from '../../util/getAutoUpdatesImportMap' -import {shouldAutoUpdate} from '../../util/shouldAutoUpdate' -import extractManifest from '../manifest/extractManifestAction' +import {extractManifest} from '../manifest/extractManifestAction' import {pick} from 'lodash' const rimraf = promisify(rimrafCallback) @@ -190,20 +189,14 @@ export default async function buildSanityStudio( spin.text = `Build Sanity Studio (${buildDuration.toFixed()}ms)` spin.succeed() - if ( - context.cliConfig && - 'unstable_extractManifestOnBuild' in context.cliConfig && - context.cliConfig.unstable_extractManifestOnBuild - ) { - await extractManifest( - { - ...pick(args, ['argsWithoutOptions', 'argv', 'groupOrCommand']), - extOptions: {}, - extraArguments: [], - }, - context, - ) - } + await extractManifest( + { + ...pick(args, ['argsWithoutOptions', 'argv', 'groupOrCommand']), + extOptions: {}, + extraArguments: [], + }, + context, + ) trace.complete() if (flags.stats) { diff --git a/packages/sanity/src/_internal/cli/actions/manifest/extractManifestAction.ts b/packages/sanity/src/_internal/cli/actions/manifest/extractManifestAction.ts index 66adb86b131..19a4156ce01 100644 --- a/packages/sanity/src/_internal/cli/actions/manifest/extractManifestAction.ts +++ b/packages/sanity/src/_internal/cli/actions/manifest/extractManifestAction.ts @@ -3,10 +3,10 @@ import {mkdir, writeFile} from 'node:fs/promises' import {dirname, join, resolve} from 'node:path' import {Worker} from 'node:worker_threads' -import {type CliCommandAction} from '@sanity/cli' -import {type ManifestV1, type ManifestV1Workspace} from '@sanity/manifest' +import {type CliCommandArguments, type CliCommandContext} from '@sanity/cli' import readPkgUp from 'read-pkg-up' +import {type ManifestV1, type SerializedManifestWorkspace} from '../../../manifest/manifestTypes' import { type ExtractSchemaWorkerData, type ExtractSchemaWorkerResult, @@ -15,20 +15,40 @@ import { const MANIFEST_FILENAME = 'v1.studiomanifest.json' const SCHEMA_FILENAME_SUFFIX = '.studioschema.json' -const extractManifest: CliCommandAction = async (_args, context) => { - const {output, workDir, chalk, cliConfig} = context +interface ExtractFlags { + workspace?: string + path?: string + level?: string | 'trace' +} + +export async function extractManifest( + args: CliCommandArguments, + context: CliCommandContext, +): Promise { + try { + await extractManifestInner(args, context) + } catch (err) { + // best-effort extraction + context.output.print('Complicated schema detected.') + if (process.env.SANITY_MANIFEST_LOG_ERROR) { + context.output.error(err) + } + } +} + +async function extractManifestInner( + args: CliCommandArguments, + context: CliCommandContext, +): Promise { + const {output, workDir, chalk} = context + const flags = args.extOptions const defaultOutputDir = resolve(join(workDir, 'dist')) - // const outputDir = resolve(args.argsWithoutOptions[0] || defaultOutputDir) + const outputDir = resolve(defaultOutputDir) const defaultStaticPath = join(outputDir, 'static') - const staticPath = - cliConfig && - 'unstable_staticAssetsPath' in cliConfig && - typeof cliConfig.unstable_staticAssetsPath !== 'undefined' - ? resolve(join(workDir, cliConfig.unstable_staticAssetsPath)) - : defaultStaticPath + const staticPath = flags.path ?? defaultStaticPath const path = join(staticPath, MANIFEST_FILENAME) @@ -46,25 +66,22 @@ const extractManifest: CliCommandAction = async (_args, context) => { 'extractSchema.js', ) - const spinner = output.spinner({}).start('Extracting manifest') - - // const trace = telemetry.trace(SchemaExtractedTrace) - // trace.start() + const spinner = output.spinner({}).start('Analyzing schema') const worker = new Worker(workerPath, { workerData: { workDir, enforceRequiredFields: false, - format: 'direct', + format: 'manifest', } satisfies ExtractSchemaWorkerData, // eslint-disable-next-line no-process-env env: process.env, }) try { - const schemas = await new Promise[]>( + const schemas = await new Promise[]>( (resolveSchemas, reject) => { - const schemaBuffer: ExtractSchemaWorkerResult<'direct'>[] = [] + const schemaBuffer: ExtractSchemaWorkerResult<'manifest'>[] = [] worker.addListener('message', (message) => schemaBuffer.push(message)) worker.addListener('exit', () => resolveSchemas(schemaBuffer)) worker.addListener('error', reject) @@ -78,42 +95,25 @@ const extractManifest: CliCommandAction = async (_args, context) => { const manifestWorkspaces = await externalizeSchemas(schemas, staticPath) const manifestV1: ManifestV1 = { - manifestVersion: 1, - createdAt: new Date(), + version: 1, + createdAt: new Date().toISOString(), workspaces: manifestWorkspaces, } - // trace.log({ - // schemaAllTypesCount: schema.length, - // schemaDocumentTypesCount: schema.filter((type) => type.type === 'document').length, - // schemaTypesCount: schema.filter((type) => type.type === 'type').length, - // enforceRequiredFields, - // schemaFormat: formatFlag, - // }) - - // const path = flags.path || join(process.cwd(), 'schema.json') - // const path = 'test-manifest.json' - await writeFile(path, JSON.stringify(manifestV1, null, 2)) - // trace.complete() - - spinner.succeed(`Extracted manifest to ${chalk.cyan(path)}`) + //spinner.succeed(`Extracted manifest to ${chalk.cyan(path)}`) } catch (err) { - console.error('[ERR]', err) - // trace.error(err) spinner.fail('Failed to extract manifest') - // throw err + throw err } } -export default extractManifest - function externalizeSchemas( - schemas: ExtractSchemaWorkerResult<'direct'>[], + schemas: ExtractSchemaWorkerResult<'manifest'>[], staticPath: string, -): Promise { - const output = schemas.reduce[]>((workspaces, workspace) => { +): Promise { + const output = schemas.reduce[]>((workspaces, workspace) => { return [...workspaces, externalizeSchema(workspace, staticPath)] }, []) @@ -121,14 +121,12 @@ function externalizeSchemas( } async function externalizeSchema( - workspace: ExtractSchemaWorkerResult<'direct'>, + workspace: ExtractSchemaWorkerResult<'manifest'>, staticPath: string, -): Promise { +): Promise { const schemaString = JSON.stringify(workspace.schema, null, 2) const hash = createHash('sha1').update(schemaString).digest('hex') - const filename = `${hash.slice(0, 8)}${SCHEMA_FILENAME_SUFFIX}` - // const filename = `${workspace.name}${SCHEMA_FILENAME_SUFFIX}` - + const filename = `${hash.slice(0, 8)}.${workspace.name}.${SCHEMA_FILENAME_SUFFIX}` await writeFile(join(staticPath, filename), schemaString) return { diff --git a/packages/sanity/src/_internal/cli/actions/schema/extractAction.ts b/packages/sanity/src/_internal/cli/actions/schema/extractAction.ts index 0daeb1fe5f6..f2b43f6fcb4 100644 --- a/packages/sanity/src/_internal/cli/actions/schema/extractAction.ts +++ b/packages/sanity/src/_internal/cli/actions/schema/extractAction.ts @@ -9,6 +9,7 @@ import { type ExtractSchemaWorkerData, type ExtractSchemaWorkerResult, } from '../../threads/extractSchema' +import {extractManifest} from '../manifest/extractManifestAction' import {SchemaExtractedTrace} from './extractSchema.telemetry' interface ExtractFlags { @@ -22,10 +23,16 @@ export type SchemaValidationFormatter = (result: ExtractSchemaWorkerResult) => s export default async function extractAction( args: CliCommandArguments, - {workDir, output, telemetry}: CliCommandContext, + context: CliCommandContext, ): Promise { const flags = args.extOptions const formatFlag = flags.format || 'groq-type-nodes' + const {workDir, output, telemetry} = context + + if (formatFlag === 'manifest') { + return extractManifest(args, context) + } + const enforceRequiredFields = flags['enforce-required-fields'] || false const rootPkgPath = readPkgUp.sync({cwd: __dirname})?.path @@ -106,4 +113,6 @@ export default async function extractAction( ) throw err } + + return undefined } diff --git a/packages/sanity/src/_internal/cli/commands/index.ts b/packages/sanity/src/_internal/cli/commands/index.ts index 4b41a210e52..e27daee4cce 100644 --- a/packages/sanity/src/_internal/cli/commands/index.ts +++ b/packages/sanity/src/_internal/cli/commands/index.ts @@ -41,9 +41,6 @@ import hookGroup from './hook/hookGroup' import listHookLogsCommand from './hook/listHookLogsCommand' import listHooksCommand from './hook/listHooksCommand' import printHookAttemptCommand from './hook/printHookAttemptCommand' -import extractManifestCommand from './manifest/extractManifestCommand' -import listManifestsCommand from './manifest/listManifestsCommand' -import manifestGroup from './manifest/manifestGroup' import createMigrationCommand from './migration/createMigrationCommand' import listMigrationsCommand from './migration/listMigrationsCommand' import migrationGroup from './migration/migrationGroup' @@ -90,9 +87,6 @@ const commands: (CliCommandDefinition | CliCommandGroupDefinition)[] = [ createHookCommand, migrationGroup, createMigrationCommand, - manifestGroup, - extractManifestCommand, - listManifestsCommand, runMigrationCommand, listMigrationsCommand, deleteHookCommand, diff --git a/packages/sanity/src/_internal/cli/commands/manifest/extractManifestCommand.ts b/packages/sanity/src/_internal/cli/commands/manifest/extractManifestCommand.ts deleted file mode 100644 index ec381d701a3..00000000000 --- a/packages/sanity/src/_internal/cli/commands/manifest/extractManifestCommand.ts +++ /dev/null @@ -1,30 +0,0 @@ -import {type CliCommandDefinition} from '@sanity/cli' - -// TODO: Switch to lazy import. -import mod from '../../actions/manifest/extractManifestAction' - -const description = 'Extracts a JSON representation of a Sanity schema within a Studio context.' - -const helpText = ` -**Note**: This command is experimental and subject to change. - -Examples - # Extracts manifests - sanity manifest extract -` - -const extractManifestCommand: CliCommandDefinition = { - name: 'extract', - group: 'manifest', - signature: '', - description, - helpText, - action: async (args, context) => { - // const mod = await import('../../actions/manifest/extractManifestAction') - // - // return mod.default(args, context) - return mod(args, context) - }, -} - -export default extractManifestCommand diff --git a/packages/sanity/src/_internal/cli/commands/manifest/listManifestsCommand.ts b/packages/sanity/src/_internal/cli/commands/manifest/listManifestsCommand.ts deleted file mode 100644 index 34b65655129..00000000000 --- a/packages/sanity/src/_internal/cli/commands/manifest/listManifestsCommand.ts +++ /dev/null @@ -1,30 +0,0 @@ -import {type CliCommandDefinition} from '@sanity/cli' - -// TODO: Switch to lazy import. -import mod from '../../actions/manifest/listManifestsAction' - -const description = 'TODO' - -const helpText = ` -**Note**: This command is experimental and subject to change. - -Examples - # Lists manifests that have been extracted - sanity manifest list -` - -const listManifestsCommand: CliCommandDefinition = { - name: 'list', - group: 'manifest', - signature: '', - description, - helpText, - action: async (args, context) => { - // const mod = await import('../../actions/manifest/listManifestsAction') - // - // return mod.default(args, context) - return mod(args, context) - }, -} - -export default listManifestsCommand diff --git a/packages/sanity/src/_internal/cli/commands/manifest/manifestGroup.ts b/packages/sanity/src/_internal/cli/commands/manifest/manifestGroup.ts deleted file mode 100644 index 2bc68864241..00000000000 --- a/packages/sanity/src/_internal/cli/commands/manifest/manifestGroup.ts +++ /dev/null @@ -1,10 +0,0 @@ -import {type CliCommandGroupDefinition} from '@sanity/cli' - -const manifestGroup: CliCommandGroupDefinition = { - name: 'manifest', - signature: '[COMMAND]', - isGroupRoot: true, - description: 'TODO', -} - -export default manifestGroup diff --git a/packages/sanity/src/_internal/cli/commands/schema/extractSchemaCommand.ts b/packages/sanity/src/_internal/cli/commands/schema/extractSchemaCommand.ts index aa0a3ef4600..8cd7fc4f1b9 100644 --- a/packages/sanity/src/_internal/cli/commands/schema/extractSchemaCommand.ts +++ b/packages/sanity/src/_internal/cli/commands/schema/extractSchemaCommand.ts @@ -1,8 +1,5 @@ import {type CliCommandDefinition} from '@sanity/cli' -// xxx tmp -import mod from '../../actions/schema/extractAction' - const description = 'Extracts a JSON representation of a Sanity schema within a Studio context.' const helpText = ` @@ -26,10 +23,8 @@ const extractSchemaCommand: CliCommandDefinition = { description, helpText, action: async (args, context) => { - // const mod = await import('../../actions/schema/extractAction') - // - // return mod.default(args, context) - return mod(args, context) + const mod = await import('../../actions/schema/extractAction') + return mod.default(args, context) }, } satisfies CliCommandDefinition diff --git a/packages/sanity/src/_internal/cli/threads/extractSchema.ts b/packages/sanity/src/_internal/cli/threads/extractSchema.ts index b2edcce9aaf..3be7ad03db8 100644 --- a/packages/sanity/src/_internal/cli/threads/extractSchema.ts +++ b/packages/sanity/src/_internal/cli/threads/extractSchema.ts @@ -2,13 +2,14 @@ import {isMainThread, parentPort, workerData as _workerData} from 'node:worker_t import {extractSchema} from '@sanity/schema/_internal' import {type SchemaType} from 'groq-js' -import {type SchemaTypeDefinition, type Workspace} from 'sanity' +import {type Workspace} from 'sanity' import {extractWorkspace} from '../../manifest/extractManifest' +import {type ManifestWorkspace} from '../../manifest/manifestTypes' import {getStudioWorkspaces} from '../util/getStudioWorkspaces' import {mockBrowserEnvironment} from '../util/mockBrowserEnvironment' -const formats = ['direct', 'groq-type-nodes'] as const +const formats = ['manifest', 'groq-type-nodes'] as const type Format = (typeof formats)[number] /** @internal */ @@ -22,8 +23,7 @@ export interface ExtractSchemaWorkerData { type WorkspaceTransformer = (workspace: Workspace) => ExtractSchemaWorkerResult const workspaceTransformers: Record = { - // @ts-expect-error FIXME - 'direct': extractWorkspace, + 'manifest': extractWorkspace, 'groq-type-nodes': (workspace) => ({ schema: extractSchema(workspace.schema, { enforceRequiredFields: opts.enforceRequiredFields, @@ -33,7 +33,7 @@ const workspaceTransformers: Record = { /** @internal */ export type ExtractSchemaWorkerResult = { - 'direct': Pick & {schema: SchemaTypeDefinition[]} // xxx + 'manifest': ManifestWorkspace 'groq-type-nodes': {schema: SchemaType} }[TargetFormat] diff --git a/packages/sanity/src/_internal/manifest/manifestTypes.ts b/packages/sanity/src/_internal/manifest/manifestTypes.ts index e1d9dbfcf73..2de03da9656 100644 --- a/packages/sanity/src/_internal/manifest/manifestTypes.ts +++ b/packages/sanity/src/_internal/manifest/manifestTypes.ts @@ -8,7 +8,13 @@ export type ManifestSerializable = export interface ManifestV1 { version: 1 createdAt: string - workspaces: ManifestWorkspace[] + workspaces: SerializedManifestWorkspace[] +} + +export interface SerializedManifestWorkspace { + name: string + dataset: string + schema: string // filename } export interface ManifestWorkspace { diff --git a/packages/sanity/tsconfig.json b/packages/sanity/tsconfig.json index fb888fdd535..5c4dc645433 100644 --- a/packages/sanity/tsconfig.json +++ b/packages/sanity/tsconfig.json @@ -12,7 +12,6 @@ "./node_modules/@sanity/cli/src", "./node_modules/@sanity/cli/typings/deepSortObject.d.ts", "./node_modules/@sanity/codegen/src", - "./node_modules/@sanity/manifest/src", "./node_modules/@sanity/mutator/src", "./node_modules/@sanity/schema/src", "./node_modules/@sanity/schema/typings", @@ -28,7 +27,6 @@ "@sanity/diff": ["./node_modules/@sanity/diff/src/index.ts"], "@sanity/cli": ["./node_modules/@sanity/cli/src/index.ts"], "@sanity/codegen": ["./node_modules/@sanity/codegen/src/_exports/index.ts"], - "@sanity/manifest": ["./node_modules/@sanity/manifest/src/index.ts"], "@sanity/mutator": ["./node_modules/@sanity/mutator/src/index.ts"], "@sanity/schema/*": ["./node_modules/@sanity/schema/src/_exports/*"], "@sanity/schema": ["./node_modules/@sanity/schema/src/_exports/index.ts"], From 09a2efc4902d24506cbd3756351b3acadde27506 Mon Sep 17 00:00:00 2001 From: Snorre Eskeland Brekke Date: Thu, 29 Aug 2024 10:15:59 +0200 Subject: [PATCH 33/49] chore: cleanup --- dev/test-studio/sanity.config.ts | 2 ++ dev/test-studio/schema/index.ts | 2 ++ .../@sanity/cli/src/util/noSuchCommandText.ts | 1 - .../cli/actions/build/buildAction.ts | 8 ++++--- .../actions/manifest/extractManifestAction.ts | 21 +++++++++++-------- .../actions/manifest/listManifestsAction.ts | 9 -------- .../cli/actions/schema/extractAction.ts | 4 ++-- .../cli/commands/deploy/deployCommand.ts | 10 ++++----- 8 files changed, 27 insertions(+), 30 deletions(-) delete mode 100644 packages/sanity/src/_internal/cli/actions/manifest/listManifestsAction.ts diff --git a/dev/test-studio/sanity.config.ts b/dev/test-studio/sanity.config.ts index 1908f7e29db..43eefde3dc4 100644 --- a/dev/test-studio/sanity.config.ts +++ b/dev/test-studio/sanity.config.ts @@ -137,6 +137,8 @@ const sharedSettings = definePlugin({ visionTool({ defaultApiVersion: '2022-08-08', }), + // eslint-disable-next-line camelcase + muxInput({mp4_support: 'standard'}), imageHotspotArrayPlugin(), routerDebugTool(), errorReportingTestPlugin(), diff --git a/dev/test-studio/schema/index.ts b/dev/test-studio/schema/index.ts index 7cfc3538b07..b579539513f 100644 --- a/dev/test-studio/schema/index.ts +++ b/dev/test-studio/schema/index.ts @@ -79,6 +79,7 @@ import {virtualizationDebug} from './debug/virtualizationDebug' import {virtualizationInObject} from './debug/virtualizationInObject' import {v3docs} from './docs/v3' import markdown from './externalPlugins/markdown' +import mux from './externalPlugins/mux' import playlist from './playlist' import playlistTrack from './playlistTrack' import code from './plugins/code' @@ -267,6 +268,7 @@ export const schemaTypes = [ // Test documents with 3rd party plugin inputs markdown, + mux, // Other documents author, diff --git a/packages/@sanity/cli/src/util/noSuchCommandText.ts b/packages/@sanity/cli/src/util/noSuchCommandText.ts index 4aef2271d04..07b94d6b1ce 100644 --- a/packages/@sanity/cli/src/util/noSuchCommandText.ts +++ b/packages/@sanity/cli/src/util/noSuchCommandText.ts @@ -17,7 +17,6 @@ const coreCommands = [ 'exec', 'graphql', 'hook', - 'manifest', 'migration', 'preview', 'schema', diff --git a/packages/sanity/src/_internal/cli/actions/build/buildAction.ts b/packages/sanity/src/_internal/cli/actions/build/buildAction.ts index c157c3ef63c..81f937e88b5 100644 --- a/packages/sanity/src/_internal/cli/actions/build/buildAction.ts +++ b/packages/sanity/src/_internal/cli/actions/build/buildAction.ts @@ -16,7 +16,7 @@ import {BuildTrace} from './build.telemetry' import {buildVendorDependencies} from '../../server/buildVendorDependencies' import {compareStudioDependencyVersions} from '../../util/compareStudioDependencyVersions' import {getAutoUpdateImportMap} from '../../util/getAutoUpdatesImportMap' -import {extractManifest} from '../manifest/extractManifestAction' +import {extractManifestSafe} from '../manifest/extractManifestAction' import {pick} from 'lodash' const rimraf = promisify(rimrafCallback) @@ -189,10 +189,12 @@ export default async function buildSanityStudio( spin.text = `Build Sanity Studio (${buildDuration.toFixed()}ms)` spin.succeed() - await extractManifest( + await extractManifestSafe( { ...pick(args, ['argsWithoutOptions', 'argv', 'groupOrCommand']), - extOptions: {}, + extOptions: { + silent: true, + }, extraArguments: [], }, context, diff --git a/packages/sanity/src/_internal/cli/actions/manifest/extractManifestAction.ts b/packages/sanity/src/_internal/cli/actions/manifest/extractManifestAction.ts index 19a4156ce01..0c040983edb 100644 --- a/packages/sanity/src/_internal/cli/actions/manifest/extractManifestAction.ts +++ b/packages/sanity/src/_internal/cli/actions/manifest/extractManifestAction.ts @@ -18,25 +18,25 @@ const SCHEMA_FILENAME_SUFFIX = '.studioschema.json' interface ExtractFlags { workspace?: string path?: string - level?: string | 'trace' + silent?: boolean } -export async function extractManifest( +export async function extractManifestSafe( args: CliCommandArguments, context: CliCommandContext, ): Promise { try { - await extractManifestInner(args, context) + await extractManifest(args, context) } catch (err) { // best-effort extraction context.output.print('Complicated schema detected.') - if (process.env.SANITY_MANIFEST_LOG_ERROR) { + if (!args.extOptions.silent) { context.output.error(err) } } } -async function extractManifestInner( +async function extractManifest( args: CliCommandArguments, context: CliCommandContext, ): Promise { @@ -66,7 +66,8 @@ async function extractManifestInner( 'extractSchema.js', ) - const spinner = output.spinner({}).start('Analyzing schema') + const start = Date.now() + const spinner = output.spinner({}).start('Extracting manifest') const worker = new Worker(workerPath, { workerData: { @@ -102,9 +103,9 @@ async function extractManifestInner( await writeFile(path, JSON.stringify(manifestV1, null, 2)) - //spinner.succeed(`Extracted manifest to ${chalk.cyan(path)}`) + spinner.succeed(`Extracted manifest to ${chalk.cyan(path)}: (${Date.now() - start}ms)`) } catch (err) { - spinner.fail('Failed to extract manifest') + spinner.fail(`Failed to extract manifest (${Date.now() - start}ms)`) throw err } } @@ -126,7 +127,9 @@ async function externalizeSchema( ): Promise { const schemaString = JSON.stringify(workspace.schema, null, 2) const hash = createHash('sha1').update(schemaString).digest('hex') - const filename = `${hash.slice(0, 8)}.${workspace.name}.${SCHEMA_FILENAME_SUFFIX}` + const filename = `${hash.slice(0, 8)}.${SCHEMA_FILENAME_SUFFIX}` + + // workspaces with identical schemas will overwrite each others schema file. This is ok, since they are identical and can be shared await writeFile(join(staticPath, filename), schemaString) return { diff --git a/packages/sanity/src/_internal/cli/actions/manifest/listManifestsAction.ts b/packages/sanity/src/_internal/cli/actions/manifest/listManifestsAction.ts deleted file mode 100644 index fdec597f09e..00000000000 --- a/packages/sanity/src/_internal/cli/actions/manifest/listManifestsAction.ts +++ /dev/null @@ -1,9 +0,0 @@ -import {type CliCommandAction} from '@sanity/cli' - -const listManifests: CliCommandAction = async (_args, context) => { - const {output} = context - - output.print('Here are the manifests for this project:') -} - -export default listManifests diff --git a/packages/sanity/src/_internal/cli/actions/schema/extractAction.ts b/packages/sanity/src/_internal/cli/actions/schema/extractAction.ts index f2b43f6fcb4..0f4c6cbfd21 100644 --- a/packages/sanity/src/_internal/cli/actions/schema/extractAction.ts +++ b/packages/sanity/src/_internal/cli/actions/schema/extractAction.ts @@ -9,7 +9,7 @@ import { type ExtractSchemaWorkerData, type ExtractSchemaWorkerResult, } from '../../threads/extractSchema' -import {extractManifest} from '../manifest/extractManifestAction' +import {extractManifestSafe} from '../manifest/extractManifestAction' import {SchemaExtractedTrace} from './extractSchema.telemetry' interface ExtractFlags { @@ -30,7 +30,7 @@ export default async function extractAction( const {workDir, output, telemetry} = context if (formatFlag === 'manifest') { - return extractManifest(args, context) + return extractManifestSafe(args, context) } const enforceRequiredFields = flags['enforce-required-fields'] || false diff --git a/packages/sanity/src/_internal/cli/commands/deploy/deployCommand.ts b/packages/sanity/src/_internal/cli/commands/deploy/deployCommand.ts index eee0fe75ee9..2b126289788 100644 --- a/packages/sanity/src/_internal/cli/commands/deploy/deployCommand.ts +++ b/packages/sanity/src/_internal/cli/commands/deploy/deployCommand.ts @@ -4,8 +4,7 @@ import { type CliCommandDefinition, } from '@sanity/cli' -// xxx tmp -import deployAction, {type DeployStudioActionFlags} from '../../actions/deploy/deployAction' +import {type DeployStudioActionFlags} from '../../actions/deploy/deployAction' const helpText = ` Options @@ -26,10 +25,9 @@ const deployCommand: CliCommandDefinition = { args: CliCommandArguments, context: CliCommandContext, ) => { - // const mod = await import('../../actions/deploy/deployAction') - // - // return mod.default(args, context) - return deployAction(args, context) + const mod = await import('../../actions/deploy/deployAction') + + return mod.default(args, context) }, helpText, } From 974433260ffe5b37143a972b11081ae8dc1fa920 Mon Sep 17 00:00:00 2001 From: Snorre Eskeland Brekke Date: Thu, 29 Aug 2024 11:19:49 +0200 Subject: [PATCH 34/49] fix: serialize fieldsets --- .../src/_internal/manifest/extractManifest.ts | 92 ++++++++++++------ .../_internal/manifest/manifestTypeHelpers.ts | 15 ++- .../src/_internal/manifest/manifestTypes.ts | 20 ++-- .../test/manifest/extractManifest.test.ts | 96 +++++++++++++++++++ 4 files changed, 186 insertions(+), 37 deletions(-) diff --git a/packages/sanity/src/_internal/manifest/extractManifest.ts b/packages/sanity/src/_internal/manifest/extractManifest.ts index 191c7ceccc9..453690fb8ab 100644 --- a/packages/sanity/src/_internal/manifest/extractManifest.ts +++ b/packages/sanity/src/_internal/manifest/extractManifest.ts @@ -6,6 +6,7 @@ import { ConcreteRuleClass, createSchema, type FileSchemaType, + type MultiFieldSet, type NumberSchemaType, type ObjectField, type ObjectSchemaType, @@ -30,12 +31,13 @@ import { } from './manifestTypeHelpers' import { type ManifestField, + type ManifestFieldset, type ManifestSchemaType, type ManifestSerializable, + type ManifestTitledValue, type ManifestValidationGroup, type ManifestValidationRule, type ManifestWorkspace, - type TitledValue, } from './manifestTypes' interface Context { @@ -51,6 +53,7 @@ type SchemaTypeKey = | keyof StringSchemaType | keyof ReferenceSchemaType | keyof BlockDefinition + | 'group' // we strip this from fields type Validation = {validation: ManifestValidationGroup[]} | Record type ObjectFields = {fields: ManifestField[]} | Record @@ -87,7 +90,7 @@ export function extractManifestSchemaTypes(schema: Schema): ManifestSchemaType[] .map((type) => transformType(type, context)) } -function transformCommonTypeFields(type: SchemaType, context: Context) { +function transformCommonTypeFields(type: SchemaType & {fieldset?: string}, context: Context) { const shouldCreateDefinition = !context.schema.get(type.name) || isCustomized(type) const arrayProperties = type.jsonType === 'array' ? transformArrayMember(type, context) : {} @@ -102,19 +105,45 @@ function transformCommonTypeFields(type: SchemaType, context: Context) { : {} return { - ...retainUserlandTypeProps(type), + ...retainCustomTypeProps(type), ...transformValidation(type.validation), - ...objectFields, ...ensureCustomTitle(type.name, type.title), ...ensureString('description', type.description), + ...objectFields, ...arrayProperties, ...referenceProperties, ...ensureConditional('readOnly', type.readOnly), ...ensureConditional('hidden', type.hidden), + ...transformFieldsets(type), + // fieldset prop gets instrumented via getCustomFields + ...ensureString('fieldset', type.fieldset), ...transformBlockType(type, context), } } +function transformFieldsets( + type: SchemaType, +): {fieldsets: ManifestFieldset[]} | Record { + if (type.jsonType !== 'object') { + return {} + } + const fieldsets = type.fieldsets + ?.filter((fs): fs is MultiFieldSet => !fs.single) + .map((fs) => { + const options = isRecord(fs.options) ? {options: retainSerializableProps(fs.options)} : {} + return { + name: fs.name, + ...ensureCustomTitle(fs.name, fs.title), + ...ensureString('description', fs.description), + ...ensureConditional('readOnly', fs.readOnly), + ...ensureConditional('hidden', fs.hidden), + ...options, + } + }) + + return fieldsets?.length ? {fieldsets} : {} +} + function transformType(type: SchemaType, context: Context): ManifestSchemaType { const typeName = type.type ? type.type.name : type.jsonType @@ -125,49 +154,52 @@ function transformType(type: SchemaType, context: Context): ManifestSchemaType { } } -function retainUserlandTypeProps(type: SchemaType): Record { +function retainCustomTypeProps(type: SchemaType): Record { const manuallySerializedFields: SchemaTypeKey[] = [ + //explicitly added 'name', 'title', 'description', 'readOnly', 'hidden', - 'deprecated', + 'validation', + 'fieldsets', + 'fields', + 'to', + 'of', + // not serialized 'type', 'jsonType', '__experimental_actions', '__experimental_formPreviewTitle', '__experimental_omnisearch_visibility', '__experimental_search', - 'groups', 'components', 'icon', 'orderings', - 'validation', - 'fieldsets', 'preview', - 'fields', - 'to', - 'of', + 'groups', + //only exists on fields + 'group', // we know about these, but let them be generically handled // deprecated - // rows + // rows (from text) // initialValue // options - // reference stuff + // crossDatasetReference props ] const typeWithoutManuallyHandledFields = Object.fromEntries( Object.entries(type).filter( ([key]) => !manuallySerializedFields.includes(key as unknown as SchemaTypeKey), ), ) - return retainSerializableProperties(typeWithoutManuallyHandledFields) as Record< + return retainSerializableProps(typeWithoutManuallyHandledFields) as Record< string, SerializableProp > } -function retainSerializableProperties(maybeSerializable: unknown, depth = 0): SerializableProp { +function retainSerializableProps(maybeSerializable: unknown, depth = 0): SerializableProp { if (depth > MAX_CUSTOM_PROPERTY_DEPTH) { return undefined } @@ -191,7 +223,7 @@ function retainSerializableProperties(maybeSerializable: unknown, depth = 0): Se if (Array.isArray(maybeSerializable)) { const arrayItems = maybeSerializable - .map((item) => retainSerializableProperties(item, depth + 1)) + .map((item) => retainSerializableProps(item, depth + 1)) .filter((item): item is ManifestSerializable => isDefined(item)) return arrayItems.length ? arrayItems : undefined } @@ -199,7 +231,7 @@ function retainSerializableProperties(maybeSerializable: unknown, depth = 0): Se if (isRecord(maybeSerializable)) { const serializableEntries = Object.entries(maybeSerializable) .map(([key, value]) => { - return [key, retainSerializableProperties(value, depth + 1)] + return [key, retainSerializableProps(value, depth + 1)] }) .filter(([, value]) => isDefined(value)) return serializableEntries.length ? Object.fromEntries(serializableEntries) : undefined @@ -208,11 +240,13 @@ function retainSerializableProperties(maybeSerializable: unknown, depth = 0): Se return undefined } -function transformField(field: ObjectField, context: Context): ManifestField { +function transformField(field: ObjectField & {fieldset?: string}, context: Context): ManifestField { return { ...transformCommonTypeFields(field.type, context), name: field.name, type: field.type.name, + // this prop gets added synthetically via getCustomFields + ...ensureString('fieldset', field.fieldset), } } @@ -233,7 +267,7 @@ function transformArrayMember( function transformReference(reference: ReferenceSchemaType): Pick { return { to: (reference.to ?? []).map((type) => ({ - ...retainUserlandTypeProps(type), + ...retainCustomTypeProps(type), type: type.name, })), } @@ -246,7 +280,7 @@ const transformTypeValidationRule: ValidationRuleTransformer = (rule) => { 'constraint' in rule && (typeof rule.constraint === 'string' ? rule.constraint.toLowerCase() - : retainSerializableProperties(rule.constraint)), + : retainSerializableProps(rule.constraint)), } } @@ -294,7 +328,7 @@ function transformValidation(validation: SchemaValidationValue): Validation { .reduce((rules, rule) => { const transformer: ValidationRuleTransformer = validationRuleTransformers[rule.flag] ?? - ((spec) => retainSerializableProperties(spec) as ManifestValidationRule) + ((spec) => retainSerializableProps(spec) as ManifestValidationRule) const transformedRule = transformer(rule) if (!transformedRule) { @@ -326,7 +360,7 @@ function ensureCustomTitle(typeName: string, value: Value) { return titleObject } -function ensureString(key: Key, value: Value) { +function ensureString(key: Key, value: unknown) { if (typeof value === 'string') { return { [key]: value, @@ -336,7 +370,7 @@ function ensureString(key: Key, value: Value) { return {} } -function ensureConditional(key: Key, value: Value) { +function ensureConditional(key: Key, value: unknown) { if (typeof value === 'boolean') { return { [key]: value, @@ -417,21 +451,21 @@ export function transformBlockType( } } -function resolveEnabledStyles(blockType: ObjectSchemaType): TitledValue[] | undefined { +function resolveEnabledStyles(blockType: ObjectSchemaType): ManifestTitledValue[] | undefined { const styleField = blockType.fields?.find((btField) => btField.name === 'style') return resolveTitleValueArray(styleField?.type.options.list) } -function resolveEnabledDecorators(spanType: ObjectSchemaType): TitledValue[] | undefined { +function resolveEnabledDecorators(spanType: ObjectSchemaType): ManifestTitledValue[] | undefined { return 'decorators' in spanType ? resolveTitleValueArray(spanType.decorators) : undefined } -function resolveEnabledListItems(blockType: ObjectSchemaType): TitledValue[] | undefined { +function resolveEnabledListItems(blockType: ObjectSchemaType): ManifestTitledValue[] | undefined { const listField = blockType.fields?.find((btField) => btField.name === 'listItem') return resolveTitleValueArray(listField?.type?.options.list) } -function resolveTitleValueArray(possibleArray: unknown): TitledValue[] | undefined { +function resolveTitleValueArray(possibleArray: unknown): ManifestTitledValue[] | undefined { if (!possibleArray || !Array.isArray(possibleArray)) { return undefined } @@ -443,7 +477,7 @@ function resolveTitleValueArray(possibleArray: unknown): TitledValue[] | undefin return { value: item.value, ...ensureString('title', item.title), - } satisfies TitledValue + } satisfies ManifestTitledValue }) if (!titledValues?.length) { return undefined diff --git a/packages/sanity/src/_internal/manifest/manifestTypeHelpers.ts b/packages/sanity/src/_internal/manifest/manifestTypeHelpers.ts index 80fd9026071..52fab6d1aa0 100644 --- a/packages/sanity/src/_internal/manifest/manifestTypeHelpers.ts +++ b/packages/sanity/src/_internal/manifest/manifestTypeHelpers.ts @@ -5,8 +5,19 @@ const DEFAULT_FILE_FIELDS = ['asset'] const DEFAULT_GEOPOINT_FIELDS = ['lat', 'lng', 'alt'] const DEFAULT_SLUG_FIELDS = ['current', 'source'] -export function getCustomFields(type: ObjectSchemaType): ObjectField[] { - const fields = type.fields +export function getCustomFields(type: ObjectSchemaType): (ObjectField & {fieldset?: string})[] { + const fields = type.fieldsets + ? type.fieldsets.flatMap((fs) => { + if (fs.single) { + return fs.field + } + return fs.fields.map((field) => ({ + ...field, + fieldset: fs.name, + })) + }) + : type.fields + if (isType(type, 'block')) { return [] } diff --git a/packages/sanity/src/_internal/manifest/manifestTypes.ts b/packages/sanity/src/_internal/manifest/manifestTypes.ts index 2de03da9656..bfbadb6a892 100644 --- a/packages/sanity/src/_internal/manifest/manifestTypes.ts +++ b/packages/sanity/src/_internal/manifest/manifestTypes.ts @@ -39,23 +39,31 @@ export interface ManifestSchemaType { preview?: { select: Record } + fieldsets?: ManifestFieldset[] options?: Record - //portable text marks?: { annotations?: ManifestArrayMember[] - decorators?: TitledValue[] + decorators?: ManifestTitledValue[] } - lists?: TitledValue[] - styles?: TitledValue[] + lists?: ManifestTitledValue[] + styles?: ManifestTitledValue[] + // userland (assignable to ManifestSerializable | undefined) + [index: string]: unknown +} + +export interface ManifestFieldset { + name: string + title?: string + [index: string]: ManifestSerializable | undefined } -export interface TitledValue { +export interface ManifestTitledValue { value: string title?: string } -export type ManifestField = ManifestSchemaType +export type ManifestField = ManifestSchemaType & {fieldset?: string} export type ManifestArrayMember = Omit & {name?: string} export type ManifestReferenceMember = Omit & {name?: string} diff --git a/packages/sanity/test/manifest/extractManifest.test.ts b/packages/sanity/test/manifest/extractManifest.test.ts index da1e5e84435..1b6d599792a 100644 --- a/packages/sanity/test/manifest/extractManifest.test.ts +++ b/packages/sanity/test/manifest/extractManifest.test.ts @@ -72,8 +72,21 @@ describe('Extract studio manifest', () => { name: 'test', types: [ defineType({ + //include name: documentType, type: 'document', + title: 'My document', + description: 'Stuff', + deprecated: { + reason: 'old', + }, + options: { + custom: 'value', + }, + initialValue: {title: 'Default'}, + liveEdit: true, + + //omit icon: () => 'remove-icon', groups: [{name: 'groups-are-removed'}], __experimental_omnisearch_visibility: true, @@ -94,6 +107,7 @@ describe('Extract studio manifest', () => { defineField({ name: 'string', type: 'string', + group: 'groups-are-removed', }), ], preview: { @@ -108,6 +122,16 @@ describe('Extract studio manifest', () => { expect(serializedDoc).toEqual({ type: 'document', name: documentType, + title: 'My document', + description: 'Stuff', + deprecated: { + reason: 'old', + }, + options: { + custom: 'value', + }, + initialValue: {title: 'Default'}, + liveEdit: true, fields: [ { name: 'string', @@ -769,5 +793,77 @@ describe('Extract studio manifest', () => { type: 'document', }) }) + + test('fieldsets and fieldset on fields is serialized', () => { + const documentType = 'basic' + const schema = createSchema({ + name: 'test', + types: [ + defineType({ + name: documentType, + type: 'document', + fieldsets: [ + { + name: 'test', + title: 'Test fieldset', + hidden: false, + readOnly: true, + options: { + collapsed: true, + }, + description: 'my fieldset', + }, + { + name: 'conditional', + hidden: () => true, + readOnly: () => true, + }, + ], + fields: [ + defineField({name: 'title', type: 'string', fieldset: 'test'}), + defineField({name: 'other', type: 'string', fieldset: 'conditional'}), + ], + }), + ], + }) + + const extracted = extractManifestSchemaTypes(schema) + const serializedDoc = extracted.find((serialized) => serialized.name === documentType) + expect(serializedDoc).toEqual({ + fields: [ + { + fieldset: 'test', + name: 'title', + title: 'Title', + type: 'string', + }, + { + fieldset: 'conditional', + name: 'other', + title: 'Other', + type: 'string', + }, + ], + fieldsets: [ + { + description: 'my fieldset', + hidden: false, + name: 'test', + options: { + collapsed: true, + }, + readOnly: true, + title: 'Test fieldset', + }, + { + hidden: 'conditional', + name: 'conditional', + readOnly: 'conditional', + }, + ], + name: 'basic', + type: 'document', + }) + }) }) }) From 8fada7beded51070b11594f384d6e4c6065af8d5 Mon Sep 17 00:00:00 2001 From: Snorre Eskeland Brekke Date: Thu, 29 Aug 2024 11:31:54 +0200 Subject: [PATCH 35/49] fix: omit default titles on fields and array-members --- .../src/_internal/manifest/extractManifest.ts | 7 +-- .../test/manifest/extractManifest.test.ts | 45 ++++++++++++++++--- 2 files changed, 43 insertions(+), 9 deletions(-) diff --git a/packages/sanity/src/_internal/manifest/extractManifest.ts b/packages/sanity/src/_internal/manifest/extractManifest.ts index 453690fb8ab..a861cd0977e 100644 --- a/packages/sanity/src/_internal/manifest/extractManifest.ts +++ b/packages/sanity/src/_internal/manifest/extractManifest.ts @@ -107,7 +107,6 @@ function transformCommonTypeFields(type: SchemaType & {fieldset?: string}, conte return { ...retainCustomTypeProps(type), ...transformValidation(type.validation), - ...ensureCustomTitle(type.name, type.title), ...ensureString('description', type.description), ...objectFields, ...arrayProperties, @@ -151,6 +150,7 @@ function transformType(type: SchemaType, context: Context): ManifestSchemaType { ...transformCommonTypeFields(type, context), name: type.name, type: typeName, + ...ensureCustomTitle(type.name, type.title), } } @@ -245,6 +245,7 @@ function transformField(field: ObjectField & {fieldset?: string}, context: Conte ...transformCommonTypeFields(field.type, context), name: field.name, type: field.type.name, + ...ensureCustomTitle(field.name, field.type.title), // this prop gets added synthetically via getCustomFields ...ensureString('fieldset', field.fieldset), } @@ -259,6 +260,7 @@ function transformArrayMember( return { ...transformCommonTypeFields(type, context), type: type.name, + ...ensureCustomTitle(type.name, type.title), } }), } @@ -348,11 +350,10 @@ function transformValidation(validation: SchemaValidationValue): Validation { return serializedValidation.length ? {validation: serializedValidation} : {} } -function ensureCustomTitle(typeName: string, value: Value) { +function ensureCustomTitle(typeName: string, value: unknown) { const titleObject = ensureString('title', value) const defaultTitle = startCase(typeName) - // omit title if its the same as default, to reduce payload if (titleObject.title === defaultTitle) { return {} diff --git a/packages/sanity/test/manifest/extractManifest.test.ts b/packages/sanity/test/manifest/extractManifest.test.ts index 1b6d599792a..df8d0509301 100644 --- a/packages/sanity/test/manifest/extractManifest.test.ts +++ b/packages/sanity/test/manifest/extractManifest.test.ts @@ -222,7 +222,6 @@ describe('Extract studio manifest', () => { fields: [ { name: 'nested', - title: 'Nested', type: 'object', fields: [ { @@ -430,7 +429,6 @@ describe('Extract studio manifest', () => { fields: [ { name: 'inner', - title: 'Inner', type: 'number', }, ], @@ -440,7 +438,6 @@ describe('Extract studio manifest', () => { }, ], name: 'nested', - title: 'Nested', type: 'object', }, { @@ -596,7 +593,7 @@ describe('Extract studio manifest', () => { name: arrayType, of: [ { - fields: [{name: 'title', title: 'Title', type: 'string'}], + fields: [{name: 'title', type: 'string'}], title: 'Some Object', type: 'override', }, @@ -834,13 +831,11 @@ describe('Extract studio manifest', () => { { fieldset: 'test', name: 'title', - title: 'Title', type: 'string', }, { fieldset: 'conditional', name: 'other', - title: 'Other', type: 'string', }, ], @@ -865,5 +860,43 @@ describe('Extract studio manifest', () => { type: 'document', }) }) + + test('do not serialize default titles (default titles added by Schema.compile based on type/field name)', () => { + const documentType = 'basic-document' + const schema = createSchema({ + name: 'test', + types: [ + defineType({ + name: documentType, + type: 'document', + fieldsets: [ + {name: 'someFieldset'}, + { + name: 'conditional', + hidden: () => true, + readOnly: () => true, + }, + ], + fields: [ + defineField({name: 'title', type: 'string'}), + defineField({name: 'someField', type: 'array', of: [{type: 'string'}]}), + defineField({name: 'customTitleField', type: 'string', title: 'Custom'}), + ], + }), + ], + }) + + const extracted = extractManifestSchemaTypes(schema) + const serializedDoc = extracted.find((serialized) => serialized.name === documentType) + expect(serializedDoc).toEqual({ + fields: [ + {name: 'title', type: 'string'}, + {name: 'someField', of: [{type: 'string'}], type: 'array'}, + {name: 'customTitleField', type: 'string', title: 'Custom'}, + ], + name: 'basic-document', + type: 'document', + }) + }) }) }) From 3db46c3c2a577912211f59ff7f6dedcb52ddd90b Mon Sep 17 00:00:00 2001 From: Snorre Eskeland Brekke Date: Thu, 29 Aug 2024 12:38:08 +0200 Subject: [PATCH 36/49] fix: ensure manifest schema is restoreble and supports cross dataset references --- .../commands/schema/extractSchemaCommand.ts | 2 +- .../src/_internal/manifest/extractManifest.ts | 69 ++++--- .../_internal/manifest/manifestTypeHelpers.ts | 38 +++- .../test/manifest/extractManifest.test.ts | 15 +- .../manifest/extractManifestRestore.test.ts | 195 ++++++++++++++++++ 5 files changed, 279 insertions(+), 40 deletions(-) create mode 100644 packages/sanity/test/manifest/extractManifestRestore.test.ts diff --git a/packages/sanity/src/_internal/cli/commands/schema/extractSchemaCommand.ts b/packages/sanity/src/_internal/cli/commands/schema/extractSchemaCommand.ts index 8cd7fc4f1b9..e9701e7015c 100644 --- a/packages/sanity/src/_internal/cli/commands/schema/extractSchemaCommand.ts +++ b/packages/sanity/src/_internal/cli/commands/schema/extractSchemaCommand.ts @@ -9,7 +9,7 @@ Options --workspace The name of the workspace to generate a schema for --path Optional path to specify destination of the schema file --enforce-required-fields Makes the schema generated treat fields marked as required as non-optional. Defaults to false. - --format=[groq-type-nodes] Format the schema as GROQ type nodes. Only available format at the moment. + --format=[groq-type-nodes, manifest] Format the schema as GROQ type nodes, or as a serialized manifest of the full studio schema Examples # Extracts schema types in a Sanity project with more than one workspace diff --git a/packages/sanity/src/_internal/manifest/extractManifest.ts b/packages/sanity/src/_internal/manifest/extractManifest.ts index a861cd0977e..33e32f8b16f 100644 --- a/packages/sanity/src/_internal/manifest/extractManifest.ts +++ b/packages/sanity/src/_internal/manifest/extractManifest.ts @@ -5,6 +5,7 @@ import { type BooleanSchemaType, ConcreteRuleClass, createSchema, + type CrossDatasetReferenceSchemaType, type FileSchemaType, type MultiFieldSet, type NumberSchemaType, @@ -23,9 +24,12 @@ import { import { getCustomFields, + isCrossDatasetReference, + isCustomized, isDefined, isPrimitive, isRecord, + isReference, isString, isType, } from './manifestTypeHelpers' @@ -93,9 +97,12 @@ export function extractManifestSchemaTypes(schema: Schema): ManifestSchemaType[] function transformCommonTypeFields(type: SchemaType & {fieldset?: string}, context: Context) { const shouldCreateDefinition = !context.schema.get(type.name) || isCustomized(type) - const arrayProperties = type.jsonType === 'array' ? transformArrayMember(type, context) : {} + const arrayProps = type.jsonType === 'array' ? transformArrayMember(type, context) : {} - const referenceProperties = isReferenceSchemaType(type) ? transformReference(type) : {} + const referenceProps = isReference(type) ? transformReference(type) : {} + const crossDatasetRefProps = isCrossDatasetReference(type) + ? transformCrossDatasetReference(type) + : {} const objectFields: ObjectFields = type.jsonType === 'object' && type.type && shouldCreateDefinition @@ -109,8 +116,9 @@ function transformCommonTypeFields(type: SchemaType & {fieldset?: string}, conte ...transformValidation(type.validation), ...ensureString('description', type.description), ...objectFields, - ...arrayProperties, - ...referenceProperties, + ...arrayProps, + ...referenceProps, + ...crossDatasetRefProps, ...ensureConditional('readOnly', type.readOnly), ...ensureConditional('hidden', type.hidden), ...transformFieldsets(type), @@ -268,10 +276,29 @@ function transformArrayMember( function transformReference(reference: ReferenceSchemaType): Pick { return { - to: (reference.to ?? []).map((type) => ({ - ...retainCustomTypeProps(type), - type: type.name, - })), + to: (reference.to ?? []).map((type) => { + return { + ...retainCustomTypeProps(type), + type: type.name, + } + }), + } +} + +function transformCrossDatasetReference( + reference: CrossDatasetReferenceSchemaType, +): Pick { + return { + to: (reference.to ?? []).map((crossDataset) => { + const preview = crossDataset.preview?.select + ? {preview: {select: crossDataset.preview.select}} + : {} + return { + type: crossDataset.type, + ...ensureCustomTitle(crossDataset.type, crossDataset.title), + ...preview, + } + }), } } @@ -387,32 +414,6 @@ function ensureConditional(key: Key, value: unknown) { return {} } -function isReferenceSchemaType(type: unknown): type is ReferenceSchemaType { - return typeof type === 'object' && type !== null && 'name' in type && type.name === 'reference' -} - -function isObjectField(maybeOjectField: unknown) { - return ( - typeof maybeOjectField === 'object' && maybeOjectField !== null && 'name' in maybeOjectField - ) -} - -function isCustomized(maybeCustomized: SchemaType) { - const hasFieldsArray = - isObjectField(maybeCustomized) && - !isType(maybeCustomized, 'reference') && - !isType(maybeCustomized, 'crossDatasetReference') && - 'fields' in maybeCustomized && - Array.isArray(maybeCustomized.fields) - - if (!hasFieldsArray) { - return false - } - - const fields = getCustomFields(maybeCustomized) - return !!fields.length -} - export function transformBlockType( blockType: SchemaType, context: Context, diff --git a/packages/sanity/src/_internal/manifest/manifestTypeHelpers.ts b/packages/sanity/src/_internal/manifest/manifestTypeHelpers.ts index 52fab6d1aa0..e9b366e978a 100644 --- a/packages/sanity/src/_internal/manifest/manifestTypeHelpers.ts +++ b/packages/sanity/src/_internal/manifest/manifestTypeHelpers.ts @@ -1,4 +1,10 @@ -import {type ObjectField, type ObjectSchemaType, type SchemaType} from '@sanity/types' +import { + type CrossDatasetReferenceSchemaType, + type ObjectField, + type ObjectSchemaType, + type ReferenceSchemaType, + type SchemaType, +} from '@sanity/types' const DEFAULT_IMAGE_FIELDS = ['asset', 'hotspot', 'crop'] const DEFAULT_FILE_FIELDS = ['asset'] @@ -36,6 +42,36 @@ export function getCustomFields(type: ObjectSchemaType): (ObjectField & {fieldse return fields } +export function isReference(type: SchemaType): type is ReferenceSchemaType { + return isType(type, 'reference') +} + +export function isCrossDatasetReference(type: SchemaType): type is CrossDatasetReferenceSchemaType { + return isType(type, 'crossDatasetReference') +} + +export function isObjectField(maybeOjectField: unknown): boolean { + return ( + typeof maybeOjectField === 'object' && maybeOjectField !== null && 'name' in maybeOjectField + ) +} + +export function isCustomized(maybeCustomized: SchemaType): boolean { + const hasFieldsArray = + isObjectField(maybeCustomized) && + !isType(maybeCustomized, 'reference') && + !isType(maybeCustomized, 'crossDatasetReference') && + 'fields' in maybeCustomized && + Array.isArray(maybeCustomized.fields) + + if (!hasFieldsArray) { + return false + } + + const fields = getCustomFields(maybeCustomized) + return !!fields.length +} + export function isType(schemaType: SchemaType, typeName: string): boolean { if (schemaType.name === typeName) { return true diff --git a/packages/sanity/test/manifest/extractManifest.test.ts b/packages/sanity/test/manifest/extractManifest.test.ts index df8d0509301..efba15dd731 100644 --- a/packages/sanity/test/manifest/extractManifest.test.ts +++ b/packages/sanity/test/manifest/extractManifest.test.ts @@ -503,7 +503,6 @@ describe('Extract studio manifest', () => { of: [ defineArrayMember({ title: 'Array item', - name: 'item', type: 'object', fields: [ defineField({ @@ -549,7 +548,7 @@ describe('Extract studio manifest', () => { { fields: [{name: 'itemTitle', title: 'Item title', type: 'string'}], title: 'Array item', - type: 'item', + type: 'object', }, ], title: 'Object array', @@ -740,7 +739,7 @@ describe('Extract studio manifest', () => { }), defineField({ title: 'Reference array', - name: 'objectArray', + name: 'refArray', type: 'array', of: [ defineArrayMember({ @@ -772,9 +771,17 @@ describe('Extract studio manifest', () => { name: 'crossDatasetReference', title: 'Cross dataset ref', type: 'crossDatasetReference', + to: [ + { + type: documentType, + preview: { + select: {title: 'title'}, + }, + }, + ], }, { - name: 'objectArray', + name: 'refArray', of: [ { title: 'Reference to', diff --git a/packages/sanity/test/manifest/extractManifestRestore.test.ts b/packages/sanity/test/manifest/extractManifestRestore.test.ts new file mode 100644 index 00000000000..fd5445f14fe --- /dev/null +++ b/packages/sanity/test/manifest/extractManifestRestore.test.ts @@ -0,0 +1,195 @@ +import {describe, expect, test} from '@jest/globals' +import { + defineArrayMember, + defineField, + defineType, + type ObjectSchemaType, + type SchemaType, +} from '@sanity/types' +import pick from 'lodash/pick' + +import {extractManifestSchemaTypes} from '../../src/_internal/manifest/extractManifest' +import {createSchema} from '../../src/core' + +describe('Extract studio manifest', () => { + test('extracted schema types should be mappable to a createSchema compatible version', () => { + const documentType = 'basic' + const sourceSchema = createSchema({ + name: 'test', + types: [ + defineType({ + name: documentType, + type: 'document', + fields: [ + defineField({name: 'string', type: 'string'}), + defineField({name: 'text', type: 'text'}), + defineField({name: 'number', type: 'number'}), + defineField({name: 'boolean', type: 'boolean'}), + defineField({name: 'date', type: 'date'}), + defineField({name: 'datetime', type: 'datetime'}), + defineField({name: 'geopoint', type: 'geopoint'}), + defineField({name: 'image', type: 'image'}), + defineField({name: 'file', type: 'file'}), + defineField({name: 'slug', type: 'slug'}), + defineField({name: 'url', type: 'url'}), + defineField({ + type: 'object', + name: 'nestedObject', + fields: [{name: 'nestedString', type: 'string'}], + }), + defineField({ + type: 'image', + name: 'customImage', + fields: [{name: 'title', type: 'string'}], + }), + defineField({ + type: 'file', + name: 'customFile', + fields: [{name: 'title', type: 'string'}], + options: {storeOriginalFilename: true}, + }), + defineField({ + name: 'stringArray', + type: 'array', + of: [{type: 'string'}], + }), + defineField({ + name: 'numberArray', + type: 'array', + of: [{type: 'number'}], + }), + defineField({ + name: 'booleanArray', + type: 'array', + of: [{type: 'boolean'}], + }), + defineField({ + name: 'objectArray', + type: 'array', + of: [ + defineArrayMember({ + type: 'object', + fields: [defineField({name: 'itemTitle', type: 'string'})], + }), + ], + }), + defineField({ + name: 'reference', + type: 'reference', + to: [{type: documentType}], + }), + defineField({ + name: 'crossDatasetReference', + type: 'crossDatasetReference', + dataset: 'production', + to: [ + { + type: documentType, + preview: {select: {title: 'title'}}, + }, + ], + }), + defineField({ + name: 'refArray', + type: 'array', + of: [ + defineArrayMember({ + name: 'reference', + type: 'reference', + to: [{type: documentType}], + }), + ], + }), + defineField({ + name: 'pt', + type: 'array', + of: [ + defineArrayMember({ + name: 'block', + type: 'block', + of: [ + defineField({ + name: 'inlineBlock', + type: 'object', + fields: [ + defineField({ + name: 'value', + type: 'string', + }), + ], + }), + ], + marks: { + annotations: [ + defineField({ + name: 'annotation', + type: 'object', + fields: [ + defineField({ + name: 'value', + type: 'string', + }), + ], + }), + ], + decorators: [{title: 'Custom mark', value: 'custom'}], + }, + lists: [{value: 'bullet', title: 'Bullet list'}], + styles: [{value: 'customStyle', title: 'Custom style'}], + }), + ], + }), + ], + }), + ], + }) + + const extracted = extractManifestSchemaTypes(sourceSchema) + + const restoredSchema = createSchema({ + name: 'test', + types: extracted, + }) + + expect(restoredSchema._validation).toEqual([]) + expect(restoredSchema.getTypeNames().sort()).toEqual(sourceSchema.getTypeNames().sort()) + + const restoredDocument = restoredSchema.get(documentType) as ObjectSchemaType + const sourceDocument = sourceSchema.get(documentType) as ObjectSchemaType + + // this is not an exhaustive test (requires additional mapping to make validation, readOnly ect schema def compliant); + // it just asserts that a basic schema can be restored without crashing + expect(typeForComparison(restoredDocument)).toEqual(typeForComparison(sourceDocument)) + }) +}) + +function typeForComparison(_type: SchemaType): unknown { + const type = pick(_type, 'jsonType', 'name', 'title', 'fields', 'of', 'to') + + if ('to' in type) { + return { + ...type, + to: (type.to as SchemaType[]).map((item) => ({ + type: item.name, + })), + } + } + + if (type.jsonType === 'object' && type.fields) { + return { + ...type, + fields: type.fields.map((field) => ({ + ...field, + type: typeForComparison(field.type), + })), + } + } + if (type.jsonType === 'array' && 'of' in type) { + return { + ...type, + of: (type.of as SchemaType[]).map((item) => typeForComparison(item)), + } + } + + return type +} From 0eb86c5fd006fc012e30aaaa7f2fbcc391dcd0b6 Mon Sep 17 00:00:00 2001 From: Snorre Eskeland Brekke Date: Thu, 29 Aug 2024 12:47:17 +0200 Subject: [PATCH 37/49] chore: mergefix --- .../cli/actions/build/buildAction.ts | 1 + pnpm-lock.yaml | 52 ------------------- 2 files changed, 1 insertion(+), 52 deletions(-) diff --git a/packages/sanity/src/_internal/cli/actions/build/buildAction.ts b/packages/sanity/src/_internal/cli/actions/build/buildAction.ts index 81f937e88b5..c3540efd647 100644 --- a/packages/sanity/src/_internal/cli/actions/build/buildAction.ts +++ b/packages/sanity/src/_internal/cli/actions/build/buildAction.ts @@ -18,6 +18,7 @@ import {compareStudioDependencyVersions} from '../../util/compareStudioDependenc import {getAutoUpdateImportMap} from '../../util/getAutoUpdatesImportMap' import {extractManifestSafe} from '../manifest/extractManifestAction' import {pick} from 'lodash' +import {shouldAutoUpdate} from '../../util/shouldAutoUpdate' const rimraf = promisify(rimrafCallback) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index aeab975dc88..cf1d24443e4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1039,55 +1039,6 @@ importers: specifier: ^3.0.2 version: 3.0.2 - packages/@sanity/manifest: - dependencies: - '@sanity/generate-help-url': - specifier: ^3.0.0 - version: 3.0.0 - '@sanity/types': - specifier: 3.38.0 - version: link:../types - arrify: - specifier: ^1.0.1 - version: 1.0.1 - groq-js: - specifier: ^1.7.0 - version: 1.7.0 - humanize-list: - specifier: ^1.0.1 - version: 1.0.1 - leven: - specifier: ^3.1.0 - version: 3.1.0 - lodash: - specifier: ^4.17.21 - version: 4.17.21 - object-inspect: - specifier: ^1.13.1 - version: 1.13.1 - devDependencies: - '@jest/globals': - specifier: ^29.7.0 - version: 29.7.0 - '@repo/package.config': - specifier: workspace:* - version: link:../../@repo/package.config - '@sanity/icons': - specifier: ^2.11.8 - version: 2.11.8(react@18.2.0) - '@types/arrify': - specifier: ^1.0.4 - version: 1.0.4 - '@types/object-inspect': - specifier: ^1.13.0 - version: 1.13.0 - '@types/react': - specifier: ^18.2.78 - version: 18.2.78 - rimraf: - specifier: ^3.0.2 - version: 3.0.2 - packages/@sanity/migrate: dependencies: '@sanity/client': @@ -1744,9 +1695,6 @@ importers: yargs: specifier: ^17.3.0 version: 17.7.2 - zod: - specifier: ^3.22.4 - version: 3.22.4 devDependencies: '@jest/expect': specifier: ^29.7.0 From 859b98230b49b4eb67313b3d5cfe6aac31dc0729 Mon Sep 17 00:00:00 2001 From: Snorre Eskeland Brekke Date: Thu, 29 Aug 2024 16:59:51 +0200 Subject: [PATCH 38/49] fix: serialization of type aliases no longer inlines fields and of props --- .../src/_internal/manifest/extractManifest.ts | 34 ++++-- .../src/_internal/manifest/manifestTypes.ts | 8 +- .../test/manifest/extractManifest.test.ts | 109 +++++++++++++++--- .../manifest/extractManifestRestore.test.ts | 16 ++- 4 files changed, 135 insertions(+), 32 deletions(-) diff --git a/packages/sanity/src/_internal/manifest/extractManifest.ts b/packages/sanity/src/_internal/manifest/extractManifest.ts index 33e32f8b16f..f3fc18f64fd 100644 --- a/packages/sanity/src/_internal/manifest/extractManifest.ts +++ b/packages/sanity/src/_internal/manifest/extractManifest.ts @@ -66,6 +66,7 @@ type ManifestValidationFlag = ManifestValidationRule['flag'] type ValidationRuleTransformer = (rule: RuleSpec) => ManifestValidationRule | undefined const MAX_CUSTOM_PROPERTY_DEPTH = 5 +const INLINE_TYPES = ['document', 'object', 'image', 'file'] export function extractWorkspace(workspace: Workspace): ManifestWorkspace { const serializedSchema = extractManifestSchemaTypes(workspace.schema) @@ -94,10 +95,13 @@ export function extractManifestSchemaTypes(schema: Schema): ManifestSchemaType[] .map((type) => transformType(type, context)) } -function transformCommonTypeFields(type: SchemaType & {fieldset?: string}, context: Context) { - const shouldCreateDefinition = !context.schema.get(type.name) || isCustomized(type) - - const arrayProps = type.jsonType === 'array' ? transformArrayMember(type, context) : {} +function transformCommonTypeFields( + type: SchemaType & {fieldset?: string}, + typeName: string, + context: Context, +): Omit { + const arrayProps = + typeName === 'array' && type.jsonType === 'array' ? transformArrayMember(type, context) : {} const referenceProps = isReference(type) ? transformReference(type) : {} const crossDatasetRefProps = isCrossDatasetReference(type) @@ -105,7 +109,7 @@ function transformCommonTypeFields(type: SchemaType & {fieldset?: string}, conte : {} const objectFields: ObjectFields = - type.jsonType === 'object' && type.type && shouldCreateDefinition + type.jsonType === 'object' && type.type && INLINE_TYPES.includes(typeName) && isCustomized(type) ? { fields: getCustomFields(type).map((objectField) => transformField(objectField, context)), } @@ -155,7 +159,7 @@ function transformType(type: SchemaType, context: Context): ManifestSchemaType { const typeName = type.type ? type.type.name : type.jsonType return { - ...transformCommonTypeFields(type, context), + ...transformCommonTypeFields(type, typeName, context), name: type.name, type: typeName, ...ensureCustomTitle(type.name, type.title), @@ -249,11 +253,14 @@ function retainSerializableProps(maybeSerializable: unknown, depth = 0): Seriali } function transformField(field: ObjectField & {fieldset?: string}, context: Context): ManifestField { + const fieldType = field.type + const typeNameExists = !!context.schema.get(fieldType.name) + const typeName = typeNameExists ? fieldType.name : (fieldType.type?.name ?? fieldType.name) return { - ...transformCommonTypeFields(field.type, context), + ...transformCommonTypeFields(fieldType, typeName, context), name: field.name, - type: field.type.name, - ...ensureCustomTitle(field.name, field.type.title), + type: typeName, + ...ensureCustomTitle(field.name, fieldType.title), // this prop gets added synthetically via getCustomFields ...ensureString('fieldset', field.fieldset), } @@ -265,9 +272,12 @@ function transformArrayMember( ): Pick { return { of: arrayMember.of.map((type) => { + const typeNameExists = !!context.schema.get(type.name) + const typeName = typeNameExists ? type.name : (type.type?.name ?? type.name) return { - ...transformCommonTypeFields(type, context), - type: type.name, + ...transformCommonTypeFields(type, typeName, context), + type: typeName, + ...(typeName === type.name ? {} : {name: type.name}), ...ensureCustomTitle(type.name, type.title), } }), @@ -398,7 +408,7 @@ function ensureString(key: Key, value: unknown) { return {} } -function ensureConditional(key: Key, value: unknown) { +function ensureConditional(key: Key, value: unknown) { if (typeof value === 'boolean') { return { [key]: value, diff --git a/packages/sanity/src/_internal/manifest/manifestTypes.ts b/packages/sanity/src/_internal/manifest/manifestTypes.ts index bfbadb6a892..9437a88cc1e 100644 --- a/packages/sanity/src/_internal/manifest/manifestTypes.ts +++ b/packages/sanity/src/_internal/manifest/manifestTypes.ts @@ -30,8 +30,8 @@ export interface ManifestSchemaType { deprecated?: { reason: string } - readOnly?: boolean | 'function' - hidden?: boolean | 'function' + readOnly?: boolean | 'conditional' + hidden?: boolean | 'conditional' validation?: ManifestValidationGroup[] fields?: ManifestField[] to?: ManifestReferenceMember[] @@ -48,8 +48,10 @@ export interface ManifestSchemaType { } lists?: ManifestTitledValue[] styles?: ManifestTitledValue[] + // userland (assignable to ManifestSerializable | undefined) - [index: string]: unknown + // not included to add some typesafty to extractManifest + // [index: string]: unknown } export interface ManifestFieldset { diff --git a/packages/sanity/test/manifest/extractManifest.test.ts b/packages/sanity/test/manifest/extractManifest.test.ts index efba15dd731..fd4f109d414 100644 --- a/packages/sanity/test/manifest/extractManifest.test.ts +++ b/packages/sanity/test/manifest/extractManifest.test.ts @@ -360,6 +360,10 @@ describe('Extract studio manifest', () => { types: [ defineType({ fields: [ + { + name: 'existingType', + type: documentType, + }, { fields: [ { @@ -418,6 +422,11 @@ describe('Extract studio manifest', () => { const serializedDoc = extracted.find((serialized) => serialized.name === documentType) expect(serializedDoc).toEqual({ fields: [ + { + name: 'existingType', + type: 'field-types', + }, + { fields: [ { @@ -497,21 +506,34 @@ describe('Extract studio manifest', () => { of: [{type: 'boolean'}], }), defineField({ - title: 'Object array', name: 'objectArray', type: 'array', of: [ defineArrayMember({ - title: 'Array item', + title: 'Anonymous object item', type: 'object', fields: [ defineField({ - title: 'Item title', name: 'itemTitle', type: 'string', }), ], }), + defineArrayMember({ + type: 'object', + title: 'Inline named object item', + name: 'item', + fields: [ + defineField({ + name: 'otherTitle', + type: 'string', + }), + ], + }), + defineArrayMember({ + title: 'Existing type object item', + type: documentType, + }), ], }), ], @@ -546,12 +568,21 @@ describe('Extract studio manifest', () => { name: 'objectArray', of: [ { - fields: [{name: 'itemTitle', title: 'Item title', type: 'string'}], - title: 'Array item', + title: 'Anonymous object item', + type: 'object', + fields: [{name: 'itemTitle', type: 'string'}], + }, + { + fields: [{name: 'otherTitle', type: 'string'}], + title: 'Inline named object item', type: 'object', + name: 'item', + }, + { + title: 'Existing type object item', + type: 'all-types', }, ], - title: 'Object array', type: 'array', }, ], @@ -561,7 +592,7 @@ describe('Extract studio manifest', () => { }) }) - test('serialize array fields with typename override', () => { + test('serialize array with type reference and overridden typename', () => { const arrayType = 'someArray' const objectBaseType = 'someObject' const schema = createSchema({ @@ -590,17 +621,67 @@ describe('Extract studio manifest', () => { const serializedDoc = extracted.find((serialized) => serialized.name === arrayType) expect(serializedDoc).toEqual({ name: arrayType, - of: [ - { - fields: [{name: 'title', type: 'string'}], - title: 'Some Object', - type: 'override', - }, - ], + of: [{title: 'Some Object', type: objectBaseType, name: 'override'}], type: 'array', }) }) + test('serialize schema with indirectly recursive structure', () => { + const arrayType = 'someArray' + const objectBaseType = 'someObject' + const otherObjectType = 'other' + const schema = createSchema({ + name: 'test', + types: [ + defineType({ + name: objectBaseType, + type: 'object', + fields: [ + defineField({ + name: 'recurse', + type: otherObjectType, + }), + ], + }), + defineType({ + name: otherObjectType, + type: 'object', + fields: [ + defineField({ + name: 'recurse2', + type: arrayType, + }), + ], + }), + defineType({ + name: arrayType, + type: 'array', + of: [{type: objectBaseType}], + }), + ], + }) + + const extracted = extractManifestSchemaTypes(schema) + + expect(extracted).toEqual([ + { + fields: [{name: 'recurse', type: 'other'}], + name: 'someObject', + type: 'object', + }, + { + fields: [{name: 'recurse2', type: 'someArray'}], + name: 'other', + type: 'object', + }, + { + name: 'someArray', + of: [{type: 'someObject'}], + type: 'array', + }, + ]) + }) + test('serialize portable text field', () => { const documentType = 'pt' const schema = createSchema({ diff --git a/packages/sanity/test/manifest/extractManifestRestore.test.ts b/packages/sanity/test/manifest/extractManifestRestore.test.ts index fd5445f14fe..5b3048047fa 100644 --- a/packages/sanity/test/manifest/extractManifestRestore.test.ts +++ b/packages/sanity/test/manifest/extractManifestRestore.test.ts @@ -32,6 +32,7 @@ describe('Extract studio manifest', () => { defineField({name: 'file', type: 'file'}), defineField({name: 'slug', type: 'slug'}), defineField({name: 'url', type: 'url'}), + defineField({name: 'object', type: documentType}), defineField({ type: 'object', name: 'nestedObject', @@ -48,6 +49,11 @@ describe('Extract studio manifest', () => { fields: [{name: 'title', type: 'string'}], options: {storeOriginalFilename: true}, }), + defineField({ + name: 'typeAliasArray', + type: 'array', + of: [{type: documentType}], + }), defineField({ name: 'stringArray', type: 'array', @@ -163,9 +169,13 @@ describe('Extract studio manifest', () => { }) }) -function typeForComparison(_type: SchemaType): unknown { +function typeForComparison(_type: SchemaType, depth = 0): unknown { const type = pick(_type, 'jsonType', 'name', 'title', 'fields', 'of', 'to') + if (depth > 10) { + return undefined + } + if ('to' in type) { return { ...type, @@ -180,14 +190,14 @@ function typeForComparison(_type: SchemaType): unknown { ...type, fields: type.fields.map((field) => ({ ...field, - type: typeForComparison(field.type), + type: typeForComparison(field.type, depth + 1), })), } } if (type.jsonType === 'array' && 'of' in type) { return { ...type, - of: (type.of as SchemaType[]).map((item) => typeForComparison(item)), + of: (type.of as SchemaType[]).map((item) => typeForComparison(item, depth + 1)), } } From bb811a9a658c3f406f737b3ffd397197b74293b2 Mon Sep 17 00:00:00 2001 From: Snorre Eskeland Brekke Date: Thu, 29 Aug 2024 17:02:59 +0200 Subject: [PATCH 39/49] fix: removes double dot in filename --- .../src/_internal/cli/actions/manifest/extractManifestAction.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/sanity/src/_internal/cli/actions/manifest/extractManifestAction.ts b/packages/sanity/src/_internal/cli/actions/manifest/extractManifestAction.ts index 0c040983edb..06dce8bb014 100644 --- a/packages/sanity/src/_internal/cli/actions/manifest/extractManifestAction.ts +++ b/packages/sanity/src/_internal/cli/actions/manifest/extractManifestAction.ts @@ -127,7 +127,7 @@ async function externalizeSchema( ): Promise { const schemaString = JSON.stringify(workspace.schema, null, 2) const hash = createHash('sha1').update(schemaString).digest('hex') - const filename = `${hash.slice(0, 8)}.${SCHEMA_FILENAME_SUFFIX}` + const filename = `${hash.slice(0, 8)}${SCHEMA_FILENAME_SUFFIX}` // workspaces with identical schemas will overwrite each others schema file. This is ok, since they are identical and can be shared await writeFile(join(staticPath, filename), schemaString) From 3ef8f692d1bfbf3368416161a5adba360d5e4cb9 Mon Sep 17 00:00:00 2001 From: Snorre Eskeland Brekke Date: Tue, 3 Sep 2024 22:03:25 +0200 Subject: [PATCH 40/49] feat: manifest command --- dev/starter-next-studio/.gitignore | 4 +- .../@sanity/cli/src/util/noSuchCommandText.ts | 1 + packages/sanity/package.config.ts | 5 ++ .../cli/actions/build/buildAction.ts | 13 --- .../cli/actions/deploy/deployAction.ts | 22 +++-- .../actions/manifest/extractManifestAction.ts | 87 +++++++++++-------- .../cli/actions/schema/extractAction.ts | 6 -- .../src/_internal/cli/commands/index.ts | 2 + .../manifest/extractManifestCommand.ts | 31 +++++++ .../commands/schema/extractSchemaCommand.ts | 2 +- .../_internal/cli/threads/extractManifest.ts | 33 +++++++ .../_internal/cli/threads/extractSchema.ts | 61 ++++--------- ...anifest.ts => extractWorkspaceManifest.ts} | 7 +- .../src/_internal/manifest/manifestTypes.ts | 11 ++- .../test/manifest/extractManifest.test.ts | 2 +- .../manifest/extractManifestRestore.test.ts | 2 +- .../extractManifestValidation.test.ts | 2 +- 17 files changed, 175 insertions(+), 116 deletions(-) create mode 100644 packages/sanity/src/_internal/cli/commands/manifest/extractManifestCommand.ts create mode 100644 packages/sanity/src/_internal/cli/threads/extractManifest.ts rename packages/sanity/src/_internal/manifest/{extractManifest.ts => extractWorkspaceManifest.ts} (98%) diff --git a/dev/starter-next-studio/.gitignore b/dev/starter-next-studio/.gitignore index 28d9bc42059..f0f5197150f 100644 --- a/dev/starter-next-studio/.gitignore +++ b/dev/starter-next-studio/.gitignore @@ -1,4 +1,4 @@ .next -public/static/*.studioschema.json -public/static/v1.studiomanifest.json +public/static/*.create-schema.json +public/static/create-manifest.json diff --git a/packages/@sanity/cli/src/util/noSuchCommandText.ts b/packages/@sanity/cli/src/util/noSuchCommandText.ts index 07b94d6b1ce..3c35f5dd65d 100644 --- a/packages/@sanity/cli/src/util/noSuchCommandText.ts +++ b/packages/@sanity/cli/src/util/noSuchCommandText.ts @@ -18,6 +18,7 @@ const coreCommands = [ 'graphql', 'hook', 'migration', + 'manifest', 'preview', 'schema', 'start', diff --git a/packages/sanity/package.config.ts b/packages/sanity/package.config.ts index 08aa9c96444..a2e7757e57a 100644 --- a/packages/sanity/package.config.ts +++ b/packages/sanity/package.config.ts @@ -41,6 +41,11 @@ export default defineConfig({ require: './lib/_internal/cli/threads/extractSchema.js', runtime: 'node', }, + { + source: './src/_internal/cli/threads/extractManifest.ts', + require: './lib/_internal/cli/threads/extractManifest.js', + runtime: 'node', + }, ], extract: { diff --git a/packages/sanity/src/_internal/cli/actions/build/buildAction.ts b/packages/sanity/src/_internal/cli/actions/build/buildAction.ts index c3540efd647..24cb65957bc 100644 --- a/packages/sanity/src/_internal/cli/actions/build/buildAction.ts +++ b/packages/sanity/src/_internal/cli/actions/build/buildAction.ts @@ -16,8 +16,6 @@ import {BuildTrace} from './build.telemetry' import {buildVendorDependencies} from '../../server/buildVendorDependencies' import {compareStudioDependencyVersions} from '../../util/compareStudioDependencyVersions' import {getAutoUpdateImportMap} from '../../util/getAutoUpdatesImportMap' -import {extractManifestSafe} from '../manifest/extractManifestAction' -import {pick} from 'lodash' import {shouldAutoUpdate} from '../../util/shouldAutoUpdate' const rimraf = promisify(rimrafCallback) @@ -190,17 +188,6 @@ export default async function buildSanityStudio( spin.text = `Build Sanity Studio (${buildDuration.toFixed()}ms)` spin.succeed() - await extractManifestSafe( - { - ...pick(args, ['argsWithoutOptions', 'argv', 'groupOrCommand']), - extOptions: { - silent: true, - }, - extraArguments: [], - }, - context, - ) - trace.complete() if (flags.stats) { output.print('\nLargest module files:') diff --git a/packages/sanity/src/_internal/cli/actions/deploy/deployAction.ts b/packages/sanity/src/_internal/cli/actions/deploy/deployAction.ts index 364aec55735..042fb803190 100644 --- a/packages/sanity/src/_internal/cli/actions/deploy/deployAction.ts +++ b/packages/sanity/src/_internal/cli/actions/deploy/deployAction.ts @@ -7,6 +7,7 @@ import tar from 'tar-fs' import {shouldAutoUpdate} from '../../util/shouldAutoUpdate' import buildSanityStudio, {type BuildSanityStudioCommandFlags} from '../build/buildAction' +import {extractManifestSafe} from '../manifest/extractManifestAction' import { checkDir, createDeployment, @@ -101,16 +102,25 @@ export default async function deployStudioAction( // Always build the project, unless --no-build is passed const shouldBuild = flags.build if (shouldBuild) { - const buildArgs = [customSourceDir].filter(Boolean) - const {didCompile} = await buildSanityStudio( - {...args, extOptions: flags, argsWithoutOptions: buildArgs}, - context, - {basePath: '/'}, - ) + const buildArgs = { + ...args, + extOptions: flags, + argsWithoutOptions: [customSourceDir].filter(Boolean), + } + const {didCompile} = await buildSanityStudio(buildArgs, context, {basePath: '/'}) if (!didCompile) { return } + + await extractManifestSafe( + { + ...buildArgs, + extOptions: {}, + extraArguments: [], + }, + context, + ) } // Ensure that the directory exists, is a directory and seems to have valid content diff --git a/packages/sanity/src/_internal/cli/actions/manifest/extractManifestAction.ts b/packages/sanity/src/_internal/cli/actions/manifest/extractManifestAction.ts index 06dce8bb014..fd40c4715f4 100644 --- a/packages/sanity/src/_internal/cli/actions/manifest/extractManifestAction.ts +++ b/packages/sanity/src/_internal/cli/actions/manifest/extractManifestAction.ts @@ -6,31 +6,44 @@ import {Worker} from 'node:worker_threads' import {type CliCommandArguments, type CliCommandContext} from '@sanity/cli' import readPkgUp from 'read-pkg-up' -import {type ManifestV1, type SerializedManifestWorkspace} from '../../../manifest/manifestTypes' import { - type ExtractSchemaWorkerData, - type ExtractSchemaWorkerResult, -} from '../../threads/extractSchema' + type CreateManifest, + type CreateWorkspaceManifest, + type ManifestWorkspaceFile, +} from '../../../manifest/manifestTypes' +import {type ExtractManifestWorkerData} from '../../threads/extractManifest' +import {getTimer} from '../../util/timing' -const MANIFEST_FILENAME = 'v1.studiomanifest.json' -const SCHEMA_FILENAME_SUFFIX = '.studioschema.json' +const MANIFEST_FILENAME = 'create-manifest.json' +const SCHEMA_FILENAME_SUFFIX = '.create-schema.json' + +/** Escape-hatch env flags to change action behavior */ +const EXTRACT_MANIFEST_DISABLED = process.env.SANITY_CLI_EXTRACT_CREATE_MANIFEST_ENABLED === 'false' +const EXTRACT_MANIFEST_LOG_ERRORS = + process.env.SANITY_CLI_EXTRACT_CREATE_MANIFEST_LOG_ERRORS === 'true' + +const CREATE_TIMER = 'create-manifest' interface ExtractFlags { - workspace?: string path?: string - silent?: boolean } export async function extractManifestSafe( args: CliCommandArguments, context: CliCommandContext, ): Promise { + if (EXTRACT_MANIFEST_DISABLED) { + return + } + try { await extractManifest(args, context) } catch (err) { // best-effort extraction - context.output.print('Complicated schema detected.') - if (!args.extOptions.silent) { + context.output.print( + 'Unable to extract manifest. Certain features like Sanity Create will not work with this studio.', + ) + if (EXTRACT_MANIFEST_LOG_ERRORS) { context.output.error(err) } } @@ -63,28 +76,25 @@ async function extractManifest( '_internal', 'cli', 'threads', - 'extractSchema.js', + 'extractManifest.js', ) - const start = Date.now() + const timer = getTimer() + timer.start(CREATE_TIMER) const spinner = output.spinner({}).start('Extracting manifest') const worker = new Worker(workerPath, { - workerData: { - workDir, - enforceRequiredFields: false, - format: 'manifest', - } satisfies ExtractSchemaWorkerData, + workerData: {workDir} satisfies ExtractManifestWorkerData, // eslint-disable-next-line no-process-env env: process.env, }) try { - const schemas = await new Promise[]>( - (resolveSchemas, reject) => { - const schemaBuffer: ExtractSchemaWorkerResult<'manifest'>[] = [] - worker.addListener('message', (message) => schemaBuffer.push(message)) - worker.addListener('exit', () => resolveSchemas(schemaBuffer)) + const workspaceManifests = await new Promise( + (resolveWorkspaces, reject) => { + const buffer: CreateWorkspaceManifest[] = [] + worker.addListener('message', (message) => buffer.push(message)) + worker.addListener('exit', () => resolveWorkspaces(buffer)) worker.addListener('error', reject) }, ) @@ -93,38 +103,41 @@ async function extractManifest( await mkdir(staticPath, {recursive: true}) - const manifestWorkspaces = await externalizeSchemas(schemas, staticPath) + const workspaceFiles = await writeWorkspaceFiles(workspaceManifests, staticPath) - const manifestV1: ManifestV1 = { + const manifestV1: CreateManifest = { version: 1, createdAt: new Date().toISOString(), - workspaces: manifestWorkspaces, + workspaces: workspaceFiles, } await writeFile(path, JSON.stringify(manifestV1, null, 2)) + const manifestDuration = timer.end(CREATE_TIMER) - spinner.succeed(`Extracted manifest to ${chalk.cyan(path)}: (${Date.now() - start}ms)`) + spinner.succeed(`Extracted manifest (${manifestDuration.toFixed()}ms)`) } catch (err) { - spinner.fail(`Failed to extract manifest (${Date.now() - start}ms)`) + spinner.fail() throw err } } -function externalizeSchemas( - schemas: ExtractSchemaWorkerResult<'manifest'>[], +function writeWorkspaceFiles( + manifestWorkspaces: CreateWorkspaceManifest[], staticPath: string, -): Promise { - const output = schemas.reduce[]>((workspaces, workspace) => { - return [...workspaces, externalizeSchema(workspace, staticPath)] - }, []) - +): Promise { + const output = manifestWorkspaces.reduce[]>( + (workspaces, workspace) => { + return [...workspaces, writeWorkspaceSchemaFile(workspace, staticPath)] + }, + [], + ) return Promise.all(output) } -async function externalizeSchema( - workspace: ExtractSchemaWorkerResult<'manifest'>, +async function writeWorkspaceSchemaFile( + workspace: CreateWorkspaceManifest, staticPath: string, -): Promise { +): Promise { const schemaString = JSON.stringify(workspace.schema, null, 2) const hash = createHash('sha1').update(schemaString).digest('hex') const filename = `${hash.slice(0, 8)}${SCHEMA_FILENAME_SUFFIX}` diff --git a/packages/sanity/src/_internal/cli/actions/schema/extractAction.ts b/packages/sanity/src/_internal/cli/actions/schema/extractAction.ts index 0f4c6cbfd21..96304df63e8 100644 --- a/packages/sanity/src/_internal/cli/actions/schema/extractAction.ts +++ b/packages/sanity/src/_internal/cli/actions/schema/extractAction.ts @@ -40,12 +40,6 @@ export default async function extractAction( throw new Error('Could not find root directory for `sanity` package') } - if (flags.workspace === undefined) { - throw new Error( - `Multiple workspaces found. Please specify which workspace to use with '--workspace'.`, - ) - } - const workerPath = join( dirname(rootPkgPath), 'lib', diff --git a/packages/sanity/src/_internal/cli/commands/index.ts b/packages/sanity/src/_internal/cli/commands/index.ts index e27daee4cce..80cb2d19da2 100644 --- a/packages/sanity/src/_internal/cli/commands/index.ts +++ b/packages/sanity/src/_internal/cli/commands/index.ts @@ -41,6 +41,7 @@ import hookGroup from './hook/hookGroup' import listHookLogsCommand from './hook/listHookLogsCommand' import listHooksCommand from './hook/listHooksCommand' import printHookAttemptCommand from './hook/printHookAttemptCommand' +import extractManifestCommand from './manifest/extractManifestCommand' import createMigrationCommand from './migration/createMigrationCommand' import listMigrationsCommand from './migration/listMigrationsCommand' import migrationGroup from './migration/migrationGroup' @@ -110,6 +111,7 @@ const commands: (CliCommandDefinition | CliCommandGroupDefinition)[] = [ previewCommand, uninstallCommand, execCommand, + extractManifestCommand, ] /** diff --git a/packages/sanity/src/_internal/cli/commands/manifest/extractManifestCommand.ts b/packages/sanity/src/_internal/cli/commands/manifest/extractManifestCommand.ts new file mode 100644 index 00000000000..76d053dcbb2 --- /dev/null +++ b/packages/sanity/src/_internal/cli/commands/manifest/extractManifestCommand.ts @@ -0,0 +1,31 @@ +import {type CliCommandDefinition} from '@sanity/cli' + +const description = 'Extracts studio configuration as on more JSON manifest files.' + +const helpText = ` +**Note**: This command is experimental and subject to change. + +Options + --path Optional path to specify destination directory of the manifest files. Default: /dist/static + +Examples + # Extracts manifests + sanity manifest extract + + # Extracts manifests into /public/static + sanity manifest extract --path /public/static +` + +const extractManifestCommand: CliCommandDefinition = { + name: 'extract', + group: 'manifest', + signature: '', + description, + helpText, + action: async (args, context) => { + const {extractManifestSafe} = await import('../../actions/manifest/extractManifestAction') + return extractManifestSafe(args, context) + }, +} + +export default extractManifestCommand diff --git a/packages/sanity/src/_internal/cli/commands/schema/extractSchemaCommand.ts b/packages/sanity/src/_internal/cli/commands/schema/extractSchemaCommand.ts index e9701e7015c..8cd7fc4f1b9 100644 --- a/packages/sanity/src/_internal/cli/commands/schema/extractSchemaCommand.ts +++ b/packages/sanity/src/_internal/cli/commands/schema/extractSchemaCommand.ts @@ -9,7 +9,7 @@ Options --workspace The name of the workspace to generate a schema for --path Optional path to specify destination of the schema file --enforce-required-fields Makes the schema generated treat fields marked as required as non-optional. Defaults to false. - --format=[groq-type-nodes, manifest] Format the schema as GROQ type nodes, or as a serialized manifest of the full studio schema + --format=[groq-type-nodes] Format the schema as GROQ type nodes. Only available format at the moment. Examples # Extracts schema types in a Sanity project with more than one workspace diff --git a/packages/sanity/src/_internal/cli/threads/extractManifest.ts b/packages/sanity/src/_internal/cli/threads/extractManifest.ts new file mode 100644 index 00000000000..e3080dce09f --- /dev/null +++ b/packages/sanity/src/_internal/cli/threads/extractManifest.ts @@ -0,0 +1,33 @@ +import {isMainThread, parentPort, workerData as _workerData} from 'node:worker_threads' + +import {extractCreateWorkspaceManifest} from '../../manifest/extractWorkspaceManifest' +import {getStudioWorkspaces} from '../util/getStudioWorkspaces' +import {mockBrowserEnvironment} from '../util/mockBrowserEnvironment' + +/** @internal */ +export interface ExtractManifestWorkerData { + workDir: string +} + +if (isMainThread || !parentPort) { + throw new Error('This module must be run as a worker thread') +} + +const opts = _workerData as ExtractManifestWorkerData + +const cleanup = mockBrowserEnvironment(opts.workDir) + +async function main() { + try { + const workspaces = await getStudioWorkspaces({basePath: opts.workDir}) + + for (const workspace of workspaces) { + parentPort?.postMessage(extractCreateWorkspaceManifest(workspace)) + } + } finally { + parentPort?.close() + cleanup() + } +} + +main() diff --git a/packages/sanity/src/_internal/cli/threads/extractSchema.ts b/packages/sanity/src/_internal/cli/threads/extractSchema.ts index 3be7ad03db8..8fb02fc00df 100644 --- a/packages/sanity/src/_internal/cli/threads/extractSchema.ts +++ b/packages/sanity/src/_internal/cli/threads/extractSchema.ts @@ -1,73 +1,49 @@ import {isMainThread, parentPort, workerData as _workerData} from 'node:worker_threads' import {extractSchema} from '@sanity/schema/_internal' -import {type SchemaType} from 'groq-js' import {type Workspace} from 'sanity' -import {extractWorkspace} from '../../manifest/extractManifest' -import {type ManifestWorkspace} from '../../manifest/manifestTypes' import {getStudioWorkspaces} from '../util/getStudioWorkspaces' import {mockBrowserEnvironment} from '../util/mockBrowserEnvironment' -const formats = ['manifest', 'groq-type-nodes'] as const -type Format = (typeof formats)[number] - /** @internal */ export interface ExtractSchemaWorkerData { workDir: string workspaceName?: string enforceRequiredFields?: boolean - format: Format | string -} - -type WorkspaceTransformer = (workspace: Workspace) => ExtractSchemaWorkerResult - -const workspaceTransformers: Record = { - 'manifest': extractWorkspace, - 'groq-type-nodes': (workspace) => ({ - schema: extractSchema(workspace.schema, { - enforceRequiredFields: opts.enforceRequiredFields, - }), - }), + format: 'groq-type-nodes' | string } /** @internal */ -export type ExtractSchemaWorkerResult = { - 'manifest': ManifestWorkspace - 'groq-type-nodes': {schema: SchemaType} -}[TargetFormat] +export interface ExtractSchemaWorkerResult { + schema: ReturnType +} if (isMainThread || !parentPort) { throw new Error('This module must be run as a worker thread') } const opts = _workerData as ExtractSchemaWorkerData -const {format} = opts const cleanup = mockBrowserEnvironment(opts.workDir) async function main() { try { - if (!isFormat(format)) { - throw new Error(`Unsupported format: "${format}"`) + if (opts.format !== 'groq-type-nodes') { + throw new Error(`Unsupported format: "${opts.format}"`) } const workspaces = await getStudioWorkspaces({basePath: opts.workDir}) - const postWorkspace = (workspace: Workspace): void => { - const transformer = workspaceTransformers[format] - parentPort?.postMessage(transformer(workspace)) - } + const workspace = getWorkspace({workspaces, workspaceName: opts.workspaceName}) - if (opts.workspaceName) { - const workspace = getWorkspace({workspaces, workspaceName: opts.workspaceName}) - postWorkspace(workspace) - } else { - for (const workspace of workspaces) { - postWorkspace(workspace) - } - } + const schema = extractSchema(workspace.schema, { + enforceRequiredFields: opts.enforceRequiredFields, + }) + + parentPort?.postMessage({ + schema, + } satisfies ExtractSchemaWorkerResult) } finally { - parentPort?.close() cleanup() } } @@ -89,13 +65,14 @@ function getWorkspace({ return workspaces[0] } + if (workspaceName === undefined) { + throw new Error( + `Multiple workspaces found. Please specify which workspace to use with '--workspace'.`, + ) + } const workspace = workspaces.find((w) => w.name === workspaceName) if (!workspace) { throw new Error(`Could not find workspace "${workspaceName}"`) } return workspace } - -function isFormat(maybeFormat: string): maybeFormat is Format { - return formats.includes(maybeFormat as Format) -} diff --git a/packages/sanity/src/_internal/manifest/extractManifest.ts b/packages/sanity/src/_internal/manifest/extractWorkspaceManifest.ts similarity index 98% rename from packages/sanity/src/_internal/manifest/extractManifest.ts rename to packages/sanity/src/_internal/manifest/extractWorkspaceManifest.ts index f3fc18f64fd..4e7ec05e8ab 100644 --- a/packages/sanity/src/_internal/manifest/extractManifest.ts +++ b/packages/sanity/src/_internal/manifest/extractWorkspaceManifest.ts @@ -34,6 +34,7 @@ import { isType, } from './manifestTypeHelpers' import { + type CreateWorkspaceManifest, type ManifestField, type ManifestFieldset, type ManifestSchemaType, @@ -41,7 +42,6 @@ import { type ManifestTitledValue, type ManifestValidationGroup, type ManifestValidationRule, - type ManifestWorkspace, } from './manifestTypes' interface Context { @@ -68,11 +68,14 @@ type ValidationRuleTransformer = (rule: RuleSpec) => ManifestValidationRule | un const MAX_CUSTOM_PROPERTY_DEPTH = 5 const INLINE_TYPES = ['document', 'object', 'image', 'file'] -export function extractWorkspace(workspace: Workspace): ManifestWorkspace { +export function extractCreateWorkspaceManifest(workspace: Workspace): CreateWorkspaceManifest { const serializedSchema = extractManifestSchemaTypes(workspace.schema) return { name: workspace.name, + title: workspace.title, + subtitle: workspace.subtitle, + basePath: workspace.basePath, dataset: workspace.dataset, schema: serializedSchema, } diff --git a/packages/sanity/src/_internal/manifest/manifestTypes.ts b/packages/sanity/src/_internal/manifest/manifestTypes.ts index 9437a88cc1e..7ce29c9ba7e 100644 --- a/packages/sanity/src/_internal/manifest/manifestTypes.ts +++ b/packages/sanity/src/_internal/manifest/manifestTypes.ts @@ -5,20 +5,23 @@ export type ManifestSerializable = | {[k: string]: ManifestSerializable} | ManifestSerializable[] -export interface ManifestV1 { +export interface CreateManifest { version: 1 createdAt: string - workspaces: SerializedManifestWorkspace[] + workspaces: ManifestWorkspaceFile[] } -export interface SerializedManifestWorkspace { +export interface ManifestWorkspaceFile { name: string dataset: string schema: string // filename } -export interface ManifestWorkspace { +export interface CreateWorkspaceManifest { name: string + title?: string + subtitle?: string + basePath: string dataset: string schema: ManifestSchemaType[] } diff --git a/packages/sanity/test/manifest/extractManifest.test.ts b/packages/sanity/test/manifest/extractManifest.test.ts index fd4f109d414..36cabadb2e9 100644 --- a/packages/sanity/test/manifest/extractManifest.test.ts +++ b/packages/sanity/test/manifest/extractManifest.test.ts @@ -2,7 +2,7 @@ import {describe, expect, test} from '@jest/globals' import {defineArrayMember, defineField, defineType} from '@sanity/types' -import {extractManifestSchemaTypes} from '../../src/_internal/manifest/extractManifest' +import {extractManifestSchemaTypes} from '../../src/_internal/manifest/extractWorkspaceManifest' import {createSchema} from '../../src/core' describe('Extract studio manifest', () => { diff --git a/packages/sanity/test/manifest/extractManifestRestore.test.ts b/packages/sanity/test/manifest/extractManifestRestore.test.ts index 5b3048047fa..a64ab1e8699 100644 --- a/packages/sanity/test/manifest/extractManifestRestore.test.ts +++ b/packages/sanity/test/manifest/extractManifestRestore.test.ts @@ -8,7 +8,7 @@ import { } from '@sanity/types' import pick from 'lodash/pick' -import {extractManifestSchemaTypes} from '../../src/_internal/manifest/extractManifest' +import {extractManifestSchemaTypes} from '../../src/_internal/manifest/extractWorkspaceManifest' import {createSchema} from '../../src/core' describe('Extract studio manifest', () => { diff --git a/packages/sanity/test/manifest/extractManifestValidation.test.ts b/packages/sanity/test/manifest/extractManifestValidation.test.ts index 6af4f4aedb2..9709c8ea554 100644 --- a/packages/sanity/test/manifest/extractManifestValidation.test.ts +++ b/packages/sanity/test/manifest/extractManifestValidation.test.ts @@ -2,7 +2,7 @@ import {describe, expect, test} from '@jest/globals' import {defineField, defineType} from '@sanity/types' -import {extractManifestSchemaTypes} from '../../src/_internal/manifest/extractManifest' +import {extractManifestSchemaTypes} from '../../src/_internal/manifest/extractWorkspaceManifest' import {createSchema} from '../../src/core' describe('Extract studio manifest', () => { From 8da232cf69016ba62ae7a2a6916d8618097212f9 Mon Sep 17 00:00:00 2001 From: Snorre Eskeland Brekke Date: Wed, 4 Sep 2024 11:48:49 +0200 Subject: [PATCH 41/49] chore: tweaks --- dev/embedded-studio/package.json | 2 +- dev/starter-next-studio/package.json | 2 +- .../actions/manifest/extractManifestAction.ts | 16 ++++++++-------- .../commands/manifest/extractManifestCommand.ts | 4 ++-- .../cli/commands/schema/extractSchemaCommand.ts | 1 + 5 files changed, 13 insertions(+), 12 deletions(-) diff --git a/dev/embedded-studio/package.json b/dev/embedded-studio/package.json index 8713d2bc533..ca346158fad 100644 --- a/dev/embedded-studio/package.json +++ b/dev/embedded-studio/package.json @@ -3,7 +3,7 @@ "version": "3.57.1", "private": true, "scripts": { - "build": "tsc && vite build && sanity schema extract --format manifest", + "build": "tsc && vite build && sanity manifest extract", "dev": "vite", "preview": "vite preview" }, diff --git a/dev/starter-next-studio/package.json b/dev/starter-next-studio/package.json index a5d82cbe412..6aff3a3fec1 100644 --- a/dev/starter-next-studio/package.json +++ b/dev/starter-next-studio/package.json @@ -5,7 +5,7 @@ "license": "MIT", "author": "Sanity.io ", "scripts": { - "build": "sanity schema extract --format manifest --path public/static && next build", + "build": "sanity manifest extract --path public/static && next build", "dev": "next dev", "start": "next start" }, diff --git a/packages/sanity/src/_internal/cli/actions/manifest/extractManifestAction.ts b/packages/sanity/src/_internal/cli/actions/manifest/extractManifestAction.ts index fd40c4715f4..77e5786cf76 100644 --- a/packages/sanity/src/_internal/cli/actions/manifest/extractManifestAction.ts +++ b/packages/sanity/src/_internal/cli/actions/manifest/extractManifestAction.ts @@ -18,9 +18,8 @@ const MANIFEST_FILENAME = 'create-manifest.json' const SCHEMA_FILENAME_SUFFIX = '.create-schema.json' /** Escape-hatch env flags to change action behavior */ -const EXTRACT_MANIFEST_DISABLED = process.env.SANITY_CLI_EXTRACT_CREATE_MANIFEST_ENABLED === 'false' -const EXTRACT_MANIFEST_LOG_ERRORS = - process.env.SANITY_CLI_EXTRACT_CREATE_MANIFEST_LOG_ERRORS === 'true' +const EXTRACT_MANIFEST_DISABLED = process.env.SANITY_CLI_EXTRACT_MANIFEST_ENABLED === 'false' +const EXTRACT_MANIFEST_LOG_ERRORS = process.env.SANITY_CLI_EXTRACT_MANIFEST_LOG_ERRORS === 'true' const CREATE_TIMER = 'create-manifest' @@ -28,6 +27,9 @@ interface ExtractFlags { path?: string } +/** + * This method will never throw + */ export async function extractManifestSafe( args: CliCommandArguments, context: CliCommandContext, @@ -53,7 +55,7 @@ async function extractManifest( args: CliCommandArguments, context: CliCommandContext, ): Promise { - const {output, workDir, chalk} = context + const {output, workDir} = context const flags = args.extOptions const defaultOutputDir = resolve(join(workDir, 'dist')) @@ -99,19 +101,17 @@ async function extractManifest( }, ) - spinner.text = `Writing manifest to ${chalk.cyan(path)}` - await mkdir(staticPath, {recursive: true}) const workspaceFiles = await writeWorkspaceFiles(workspaceManifests, staticPath) - const manifestV1: CreateManifest = { + const manifest: CreateManifest = { version: 1, createdAt: new Date().toISOString(), workspaces: workspaceFiles, } - await writeFile(path, JSON.stringify(manifestV1, null, 2)) + await writeFile(path, JSON.stringify(manifest, null, 2)) const manifestDuration = timer.end(CREATE_TIMER) spinner.succeed(`Extracted manifest (${manifestDuration.toFixed()}ms)`) diff --git a/packages/sanity/src/_internal/cli/commands/manifest/extractManifestCommand.ts b/packages/sanity/src/_internal/cli/commands/manifest/extractManifestCommand.ts index 76d053dcbb2..1675cccdbcd 100644 --- a/packages/sanity/src/_internal/cli/commands/manifest/extractManifestCommand.ts +++ b/packages/sanity/src/_internal/cli/commands/manifest/extractManifestCommand.ts @@ -1,9 +1,9 @@ import {type CliCommandDefinition} from '@sanity/cli' -const description = 'Extracts studio configuration as on more JSON manifest files.' +const description = 'Extracts the studio configuration as one more JSON manifest files.' const helpText = ` -**Note**: This command is experimental and subject to change. +**Note**: This command is experimental and subject to change. It is intended for use with Create only. Options --path Optional path to specify destination directory of the manifest files. Default: /dist/static diff --git a/packages/sanity/src/_internal/cli/commands/schema/extractSchemaCommand.ts b/packages/sanity/src/_internal/cli/commands/schema/extractSchemaCommand.ts index 8cd7fc4f1b9..f6867346c39 100644 --- a/packages/sanity/src/_internal/cli/commands/schema/extractSchemaCommand.ts +++ b/packages/sanity/src/_internal/cli/commands/schema/extractSchemaCommand.ts @@ -24,6 +24,7 @@ const extractSchemaCommand: CliCommandDefinition = { helpText, action: async (args, context) => { const mod = await import('../../actions/schema/extractAction') + return mod.default(args, context) }, } satisfies CliCommandDefinition From f07e4e7cc40d6d3ff53f57d939afd9d601c3b84a Mon Sep 17 00:00:00 2001 From: Snorre Eskeland Brekke Date: Fri, 6 Sep 2024 16:31:03 +0200 Subject: [PATCH 42/49] chore: revert redundant changes --- .../src/_internal/cli/actions/schema/extractAction.ts | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/packages/sanity/src/_internal/cli/actions/schema/extractAction.ts b/packages/sanity/src/_internal/cli/actions/schema/extractAction.ts index 96304df63e8..5345491c7d3 100644 --- a/packages/sanity/src/_internal/cli/actions/schema/extractAction.ts +++ b/packages/sanity/src/_internal/cli/actions/schema/extractAction.ts @@ -9,7 +9,6 @@ import { type ExtractSchemaWorkerData, type ExtractSchemaWorkerResult, } from '../../threads/extractSchema' -import {extractManifestSafe} from '../manifest/extractManifestAction' import {SchemaExtractedTrace} from './extractSchema.telemetry' interface ExtractFlags { @@ -23,16 +22,10 @@ export type SchemaValidationFormatter = (result: ExtractSchemaWorkerResult) => s export default async function extractAction( args: CliCommandArguments, - context: CliCommandContext, + {workDir, output, telemetry}: CliCommandContext, ): Promise { const flags = args.extOptions const formatFlag = flags.format || 'groq-type-nodes' - const {workDir, output, telemetry} = context - - if (formatFlag === 'manifest') { - return extractManifestSafe(args, context) - } - const enforceRequiredFields = flags['enforce-required-fields'] || false const rootPkgPath = readPkgUp.sync({cwd: __dirname})?.path @@ -107,6 +100,4 @@ export default async function extractAction( ) throw err } - - return undefined } From b5890360a820882c6eef4813d1b7f1e85ec7d8f5 Mon Sep 17 00:00:00 2001 From: Snorre Eskeland Brekke Date: Fri, 6 Sep 2024 16:34:26 +0200 Subject: [PATCH 43/49] fix: adds manifest group to CLI --- packages/sanity/src/_internal/cli/commands/index.ts | 2 ++ .../src/_internal/cli/commands/manifest/manifestGroup.ts | 6 ++++++ 2 files changed, 8 insertions(+) create mode 100644 packages/sanity/src/_internal/cli/commands/manifest/manifestGroup.ts diff --git a/packages/sanity/src/_internal/cli/commands/index.ts b/packages/sanity/src/_internal/cli/commands/index.ts index 80cb2d19da2..1a6801e8a82 100644 --- a/packages/sanity/src/_internal/cli/commands/index.ts +++ b/packages/sanity/src/_internal/cli/commands/index.ts @@ -42,6 +42,7 @@ import listHookLogsCommand from './hook/listHookLogsCommand' import listHooksCommand from './hook/listHooksCommand' import printHookAttemptCommand from './hook/printHookAttemptCommand' import extractManifestCommand from './manifest/extractManifestCommand' +import manifestGroup from './manifest/manifestGroup' import createMigrationCommand from './migration/createMigrationCommand' import listMigrationsCommand from './migration/listMigrationsCommand' import migrationGroup from './migration/migrationGroup' @@ -111,6 +112,7 @@ const commands: (CliCommandDefinition | CliCommandGroupDefinition)[] = [ previewCommand, uninstallCommand, execCommand, + manifestGroup, extractManifestCommand, ] diff --git a/packages/sanity/src/_internal/cli/commands/manifest/manifestGroup.ts b/packages/sanity/src/_internal/cli/commands/manifest/manifestGroup.ts new file mode 100644 index 00000000000..edc01315c42 --- /dev/null +++ b/packages/sanity/src/_internal/cli/commands/manifest/manifestGroup.ts @@ -0,0 +1,6 @@ +export default { + name: 'manifest', + signature: '[COMMAND]', + isGroupRoot: true, + description: 'Extracts the studio configuration as one more JSON manifest files.', +} From 4bad06ecb145ff39b0481660ea86d1625d9ed2e7 Mon Sep 17 00:00:00 2001 From: Snorre Eskeland Brekke Date: Mon, 9 Sep 2024 13:29:58 +0200 Subject: [PATCH 44/49] chore: wording change --- .../_internal/cli/commands/manifest/extractManifestCommand.ts | 4 ++-- .../src/_internal/cli/commands/manifest/manifestGroup.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/sanity/src/_internal/cli/commands/manifest/extractManifestCommand.ts b/packages/sanity/src/_internal/cli/commands/manifest/extractManifestCommand.ts index 1675cccdbcd..e3d3922622f 100644 --- a/packages/sanity/src/_internal/cli/commands/manifest/extractManifestCommand.ts +++ b/packages/sanity/src/_internal/cli/commands/manifest/extractManifestCommand.ts @@ -1,9 +1,9 @@ import {type CliCommandDefinition} from '@sanity/cli' -const description = 'Extracts the studio configuration as one more JSON manifest files.' +const description = 'Extracts the studio configuration as one or more JSON manifest files.' const helpText = ` -**Note**: This command is experimental and subject to change. It is intended for use with Create only. +**Note**: This command is experimental and subject to change. It is currently intended for use with Create only. Options --path Optional path to specify destination directory of the manifest files. Default: /dist/static diff --git a/packages/sanity/src/_internal/cli/commands/manifest/manifestGroup.ts b/packages/sanity/src/_internal/cli/commands/manifest/manifestGroup.ts index edc01315c42..ba086d91672 100644 --- a/packages/sanity/src/_internal/cli/commands/manifest/manifestGroup.ts +++ b/packages/sanity/src/_internal/cli/commands/manifest/manifestGroup.ts @@ -2,5 +2,5 @@ export default { name: 'manifest', signature: '[COMMAND]', isGroupRoot: true, - description: 'Extracts the studio configuration as one more JSON manifest files.', + description: 'Interacts with the studio configuration.', } From 82cd3277d706962d4d37f130e293920cceb67094 Mon Sep 17 00:00:00 2001 From: Snorre Eskeland Brekke Date: Wed, 18 Sep 2024 21:13:29 +0200 Subject: [PATCH 45/49] fix: adds a 2-minute timeout to manifest extract --- .../actions/manifest/extractManifestAction.ts | 80 +++++++++++++------ 1 file changed, 54 insertions(+), 26 deletions(-) diff --git a/packages/sanity/src/_internal/cli/actions/manifest/extractManifestAction.ts b/packages/sanity/src/_internal/cli/actions/manifest/extractManifestAction.ts index 77e5786cf76..fd3b65ae934 100644 --- a/packages/sanity/src/_internal/cli/actions/manifest/extractManifestAction.ts +++ b/packages/sanity/src/_internal/cli/actions/manifest/extractManifestAction.ts @@ -4,6 +4,7 @@ import {dirname, join, resolve} from 'node:path' import {Worker} from 'node:worker_threads' import {type CliCommandArguments, type CliCommandContext} from '@sanity/cli' +import {minutesToMilliseconds} from 'date-fns' import readPkgUp from 'read-pkg-up' import { @@ -18,11 +19,14 @@ const MANIFEST_FILENAME = 'create-manifest.json' const SCHEMA_FILENAME_SUFFIX = '.create-schema.json' /** Escape-hatch env flags to change action behavior */ -const EXTRACT_MANIFEST_DISABLED = process.env.SANITY_CLI_EXTRACT_MANIFEST_ENABLED === 'false' +const FEATURE_ENABLED_ENV_NAME = 'SANITY_CLI_EXTRACT_MANIFEST_ENABLED' +const EXTRACT_MANIFEST_DISABLED = process.env[FEATURE_ENABLED_ENV_NAME] === 'false' const EXTRACT_MANIFEST_LOG_ERRORS = process.env.SANITY_CLI_EXTRACT_MANIFEST_LOG_ERRORS === 'true' const CREATE_TIMER = 'create-manifest' +const EXTRACT_TASK_TIMEOUT_MS = minutesToMilliseconds(2) + interface ExtractFlags { path?: string } @@ -43,7 +47,8 @@ export async function extractManifestSafe( } catch (err) { // best-effort extraction context.output.print( - 'Unable to extract manifest. Certain features like Sanity Create will not work with this studio.', + 'Unable to extract manifest. Certain features like Sanity Create will not work with this studio.\n' + + `To disable manifest extraction set ${FEATURE_ENABLED_ENV_NAME}=false`, ) if (EXTRACT_MANIFEST_LOG_ERRORS) { context.output.error(err) @@ -72,35 +77,12 @@ async function extractManifest( throw new Error('Could not find root directory for `sanity` package') } - const workerPath = join( - dirname(rootPkgPath), - 'lib', - '_internal', - 'cli', - 'threads', - 'extractManifest.js', - ) - const timer = getTimer() timer.start(CREATE_TIMER) const spinner = output.spinner({}).start('Extracting manifest') - const worker = new Worker(workerPath, { - workerData: {workDir} satisfies ExtractManifestWorkerData, - // eslint-disable-next-line no-process-env - env: process.env, - }) - try { - const workspaceManifests = await new Promise( - (resolveWorkspaces, reject) => { - const buffer: CreateWorkspaceManifest[] = [] - worker.addListener('message', (message) => buffer.push(message)) - worker.addListener('exit', () => resolveWorkspaces(buffer)) - worker.addListener('error', reject) - }, - ) - + const workspaceManifests = await getWorkspaceManifests({rootPkgPath, workDir}) await mkdir(staticPath, {recursive: true}) const workspaceFiles = await writeWorkspaceFiles(workspaceManifests, staticPath) @@ -121,6 +103,52 @@ async function extractManifest( } } +async function getWorkspaceManifests({ + rootPkgPath, + workDir, +}: { + rootPkgPath: string + workDir: string +}): Promise { + const workerPath = join( + dirname(rootPkgPath), + 'lib', + '_internal', + 'cli', + 'threads', + 'extractManifest.js', + ) + + const worker = new Worker(workerPath, { + workerData: {workDir} satisfies ExtractManifestWorkerData, + // eslint-disable-next-line no-process-env + env: process.env, + }) + + let timeout = false + const timeoutId = setTimeout(() => { + timeout = true + worker.terminate() + }, EXTRACT_TASK_TIMEOUT_MS) + + try { + return await new Promise((resolveWorkspaces, reject) => { + const buffer: CreateWorkspaceManifest[] = [] + worker.addListener('message', (message) => buffer.push(message)) + worker.addListener('exit', (exitCode) => { + if (exitCode === 0) { + resolveWorkspaces(buffer) + } else if (timeout) { + reject(new Error(`Extract manifest was aborted after ${EXTRACT_TASK_TIMEOUT_MS}ms`)) + } + }) + worker.addListener('error', reject) + }) + } finally { + clearTimeout(timeoutId) + } +} + function writeWorkspaceFiles( manifestWorkspaces: CreateWorkspaceManifest[], staticPath: string, From 0246fa6e86d13669d16c487d1667baf36981ed98 Mon Sep 17 00:00:00 2001 From: Snorre Eskeland Brekke Date: Thu, 26 Sep 2024 12:59:18 +0200 Subject: [PATCH 46/49] fix: ensures error code when mainfest extract fails and changes failed spinner message to info --- .../actions/manifest/extractManifestAction.ts | 21 +++++++++++-------- .../manifest/extractManifestCommand.ts | 6 +++++- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/packages/sanity/src/_internal/cli/actions/manifest/extractManifestAction.ts b/packages/sanity/src/_internal/cli/actions/manifest/extractManifestAction.ts index fd3b65ae934..caaa00f8196 100644 --- a/packages/sanity/src/_internal/cli/actions/manifest/extractManifestAction.ts +++ b/packages/sanity/src/_internal/cli/actions/manifest/extractManifestAction.ts @@ -27,32 +27,35 @@ const CREATE_TIMER = 'create-manifest' const EXTRACT_TASK_TIMEOUT_MS = minutesToMilliseconds(2) +const EXTRACT_FAILURE_MESSAGE = + 'Unable to extract manifest. Certain features like Sanity Create will not work with this studio.\n' + + //TODO: replace this link + 'For more information, see: https://www.sanity.io/docs/cli' + interface ExtractFlags { path?: string } /** - * This method will never throw + * This function will never throw. + * @returns `undefined` if extract succeeded - caught error if it failed */ export async function extractManifestSafe( args: CliCommandArguments, context: CliCommandContext, -): Promise { +): Promise { if (EXTRACT_MANIFEST_DISABLED) { - return + return undefined } try { await extractManifest(args, context) + return undefined } catch (err) { - // best-effort extraction - context.output.print( - 'Unable to extract manifest. Certain features like Sanity Create will not work with this studio.\n' + - `To disable manifest extraction set ${FEATURE_ENABLED_ENV_NAME}=false`, - ) if (EXTRACT_MANIFEST_LOG_ERRORS) { context.output.error(err) } + return err } } @@ -98,7 +101,7 @@ async function extractManifest( spinner.succeed(`Extracted manifest (${manifestDuration.toFixed()}ms)`) } catch (err) { - spinner.fail() + spinner.info(EXTRACT_FAILURE_MESSAGE) throw err } } diff --git a/packages/sanity/src/_internal/cli/commands/manifest/extractManifestCommand.ts b/packages/sanity/src/_internal/cli/commands/manifest/extractManifestCommand.ts index e3d3922622f..7422524e2a4 100644 --- a/packages/sanity/src/_internal/cli/commands/manifest/extractManifestCommand.ts +++ b/packages/sanity/src/_internal/cli/commands/manifest/extractManifestCommand.ts @@ -24,7 +24,11 @@ const extractManifestCommand: CliCommandDefinition = { helpText, action: async (args, context) => { const {extractManifestSafe} = await import('../../actions/manifest/extractManifestAction') - return extractManifestSafe(args, context) + const extractError = await extractManifestSafe(args, context) + if (extractError) { + throw extractError + } + return extractError }, } From 011610cddc46a65b1fe7f0717d8904bf0c2ed7d7 Mon Sep 17 00:00:00 2001 From: Snorre Eskeland Brekke Date: Tue, 1 Oct 2024 14:07:30 +0200 Subject: [PATCH 47/49] chore: use *ENABLED instead of *DISABLED for constant --- .../_internal/cli/actions/manifest/extractManifestAction.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/sanity/src/_internal/cli/actions/manifest/extractManifestAction.ts b/packages/sanity/src/_internal/cli/actions/manifest/extractManifestAction.ts index caaa00f8196..be52dbe7e29 100644 --- a/packages/sanity/src/_internal/cli/actions/manifest/extractManifestAction.ts +++ b/packages/sanity/src/_internal/cli/actions/manifest/extractManifestAction.ts @@ -20,7 +20,7 @@ const SCHEMA_FILENAME_SUFFIX = '.create-schema.json' /** Escape-hatch env flags to change action behavior */ const FEATURE_ENABLED_ENV_NAME = 'SANITY_CLI_EXTRACT_MANIFEST_ENABLED' -const EXTRACT_MANIFEST_DISABLED = process.env[FEATURE_ENABLED_ENV_NAME] === 'false' +const EXTRACT_MANIFEST_ENABLED = process.env[FEATURE_ENABLED_ENV_NAME] !== 'false' const EXTRACT_MANIFEST_LOG_ERRORS = process.env.SANITY_CLI_EXTRACT_MANIFEST_LOG_ERRORS === 'true' const CREATE_TIMER = 'create-manifest' @@ -44,7 +44,7 @@ export async function extractManifestSafe( args: CliCommandArguments, context: CliCommandContext, ): Promise { - if (EXTRACT_MANIFEST_DISABLED) { + if (!EXTRACT_MANIFEST_ENABLED) { return undefined } From a150e86af775c0a1d64e617f594919e55d863813 Mon Sep 17 00:00:00 2001 From: Snorre Eskeland Brekke Date: Tue, 1 Oct 2024 14:09:01 +0200 Subject: [PATCH 48/49] chore: defensive optional chaining for option extraction --- .../sanity/src/_internal/manifest/extractWorkspaceManifest.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/sanity/src/_internal/manifest/extractWorkspaceManifest.ts b/packages/sanity/src/_internal/manifest/extractWorkspaceManifest.ts index 4e7ec05e8ab..427d4b83a7b 100644 --- a/packages/sanity/src/_internal/manifest/extractWorkspaceManifest.ts +++ b/packages/sanity/src/_internal/manifest/extractWorkspaceManifest.ts @@ -468,7 +468,7 @@ export function transformBlockType( function resolveEnabledStyles(blockType: ObjectSchemaType): ManifestTitledValue[] | undefined { const styleField = blockType.fields?.find((btField) => btField.name === 'style') - return resolveTitleValueArray(styleField?.type.options.list) + return resolveTitleValueArray(styleField?.type?.options?.list) } function resolveEnabledDecorators(spanType: ObjectSchemaType): ManifestTitledValue[] | undefined { @@ -477,7 +477,7 @@ function resolveEnabledDecorators(spanType: ObjectSchemaType): ManifestTitledVal function resolveEnabledListItems(blockType: ObjectSchemaType): ManifestTitledValue[] | undefined { const listField = blockType.fields?.find((btField) => btField.name === 'listItem') - return resolveTitleValueArray(listField?.type?.options.list) + return resolveTitleValueArray(listField?.type?.options?.list) } function resolveTitleValueArray(possibleArray: unknown): ManifestTitledValue[] | undefined { From de125a7235550487b436568f2f1676a2facc9b83 Mon Sep 17 00:00:00 2001 From: Snorre Eskeland Brekke Date: Tue, 1 Oct 2024 21:32:17 +0200 Subject: [PATCH 49/49] chore: reworded EXTRACT_FAILURE_MESSAGE --- .../_internal/cli/actions/manifest/extractManifestAction.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/sanity/src/_internal/cli/actions/manifest/extractManifestAction.ts b/packages/sanity/src/_internal/cli/actions/manifest/extractManifestAction.ts index be52dbe7e29..509110cf775 100644 --- a/packages/sanity/src/_internal/cli/actions/manifest/extractManifestAction.ts +++ b/packages/sanity/src/_internal/cli/actions/manifest/extractManifestAction.ts @@ -28,9 +28,8 @@ const CREATE_TIMER = 'create-manifest' const EXTRACT_TASK_TIMEOUT_MS = minutesToMilliseconds(2) const EXTRACT_FAILURE_MESSAGE = - 'Unable to extract manifest. Certain features like Sanity Create will not work with this studio.\n' + - //TODO: replace this link - 'For more information, see: https://www.sanity.io/docs/cli' + "Couldn't extract manifest file. Sanity Create will not be available for the studio.\n" + + `Disable this message with ${FEATURE_ENABLED_ENV_NAME}=false` interface ExtractFlags { path?: string