diff --git a/src/commands/gen-types.ts b/src/commands/gen-types.ts new file mode 100644 index 0000000..a53cc07 --- /dev/null +++ b/src/commands/gen-types.ts @@ -0,0 +1,35 @@ +import { getAdapter } from "../adapters"; +import { generateAndWriteTypes } from "../lib/codegen"; +import { createCommand, type CommandConfig } from "../lib/command"; +import { relativePathname } from "../lib/url"; +import { findProjectRoot } from "../project"; + +const FILENAME = "prismicio-types.d.ts"; + +const config = { + name: "prismic gen types", + description: ` + Generate TypeScript types from local custom type and slice models. + + Reads models from the customtypes/ and slices directories, then writes + a prismicio-types.d.ts file at the project root. + `, +} satisfies CommandConfig; + +export default createCommand(config, async () => { + const adapter = await getAdapter(); + const slices = await adapter.getSlices(); + const customTypes = await adapter.getCustomTypes(); + const projectRoot = await findProjectRoot(); + + const output = new URL(FILENAME, projectRoot); + const relativeOutput = relativePathname(projectRoot, output); + + await generateAndWriteTypes({ + customTypes: customTypes.map((customType) => customType.model), + slices: slices.map((slice) => slice.model), + output, + }); + + console.info(`Generated types at ${relativeOutput}`); +}); diff --git a/src/commands/gen.ts b/src/commands/gen.ts new file mode 100644 index 0000000..6d4f4e7 --- /dev/null +++ b/src/commands/gen.ts @@ -0,0 +1,13 @@ +import { createCommandRouter } from "../lib/command"; +import genTypes from "./gen-types"; + +export default createCommandRouter({ + name: "prismic gen", + description: "Generate files from local Prismic models.", + commands: { + types: { + handler: genTypes, + description: "Generate TypeScript types from local models", + }, + }, +}); diff --git a/src/commands/init.ts b/src/commands/init.ts index a535347..36bd085 100644 --- a/src/commands/init.ts +++ b/src/commands/init.ts @@ -150,10 +150,11 @@ export default createCommand(config, async ({ values }) => { const slices = await adapter.getSlices(); const customTypes = await adapter.getCustomTypes(); const projectRoot = await findProjectRoot(); + const output = new URL("prismicio-types.d.ts", projectRoot); await generateAndWriteTypes({ customTypes: customTypes.map((customType) => customType.model), slices: slices.map((slice) => slice.model), - projectRoot, + output, }); console.info(`\nInitialized Prismic for repository "${repo}".`); diff --git a/src/commands/sync.ts b/src/commands/sync.ts index 46213ff..565e5ce 100644 --- a/src/commands/sync.ts +++ b/src/commands/sync.ts @@ -227,9 +227,10 @@ async function regenerateTypes(adapter: Adapter): Promise { const slices = await adapter.getSlices(); const customTypes = await adapter.getCustomTypes(); const projectRoot = await findProjectRoot(); + const output = new URL("prismicio-types.d.ts", projectRoot); await generateAndWriteTypes({ customTypes: customTypes.map((customType) => customType.model), slices: slices.map((slice) => slice.model), - projectRoot, + output, }); } diff --git a/src/index.ts b/src/index.ts index 6d9c213..b616da9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,6 +6,7 @@ import packageJson from "../package.json" with { type: "json" }; import { getAdapter, NoSupportedFrameworkError } from "./adapters"; import { getHost, refreshToken } from "./auth"; import { getProfile } from "./clients/user"; +import gen from "./commands/gen"; import init from "./commands/init"; import locale from "./commands/locale"; import login from "./commands/login"; @@ -45,6 +46,10 @@ const router = createCommandRouter({ handler: init, description: "Initialize a Prismic project", }, + gen: { + handler: gen, + description: "Generate files from local models", + }, sync: { handler: sync, description: "Sync types and slices from Prismic", diff --git a/src/lib/codegen.ts b/src/lib/codegen.ts index b21fc5e..839eb73 100644 --- a/src/lib/codegen.ts +++ b/src/lib/codegen.ts @@ -6,11 +6,13 @@ import { generateTypes } from "prismic-ts-codegen"; export async function generateAndWriteTypes(args: { customTypes: CustomType[]; slices: SharedSlice[]; - projectRoot: URL; + output: URL; }): Promise { + const { customTypes, slices, output } = args; + const types = generateTypes({ - customTypeModels: args.customTypes, - sharedSliceModels: args.slices, + customTypeModels: customTypes, + sharedSliceModels: slices, clientIntegration: { includeContentNamespace: true, includeCreateClientInterface: true, @@ -18,6 +20,5 @@ export async function generateAndWriteTypes(args: { cache: true, typesProvider: "@prismicio/client", }); - const outputPath = new URL("prismicio-types.d.ts", args.projectRoot); - await writeFile(outputPath, types); + await writeFile(output, types); } diff --git a/src/lib/url.ts b/src/lib/url.ts index ec029ff..32b8d33 100644 --- a/src/lib/url.ts +++ b/src/lib/url.ts @@ -1,5 +1,12 @@ +import { relative } from "node:path"; +import { fileURLToPath } from "node:url"; + export function appendTrailingSlash(url: string | URL): URL { const newURL = new URL(url); if (!newURL.pathname.endsWith("/")) newURL.pathname += "/"; return newURL; } + +export function relativePathname(a: URL, b: URL): string { + return relative(fileURLToPath(a), fileURLToPath(b)); +} diff --git a/test/gen-types.test.ts b/test/gen-types.test.ts new file mode 100644 index 0000000..f0d2fb3 --- /dev/null +++ b/test/gen-types.test.ts @@ -0,0 +1,69 @@ +import type { CustomType, SharedSlice } from "@prismicio/types-internal/lib/customtypes"; + +import { mkdir, writeFile } from "node:fs/promises"; + +import { it } from "./it"; + +it("supports --help", async ({ expect, prismic }) => { + const { stdout, exitCode } = await prismic("gen", ["types", "--help"]); + expect(exitCode).toBe(0); + expect(stdout).toContain("prismic gen types [options]"); +}); + +it("generates types from local models", async ({ expect, project, prismic }) => { + const customType = buildCustomType(); + const customTypePath = new URL(`customtypes/${customType.id}/index.json`, project); + await mkdir(new URL(".", customTypePath), { recursive: true }); + await writeFile(customTypePath, JSON.stringify(customType)); + + const slice = buildSlice(); + const slicePath = new URL(`slices/${slice.name}/model.json`, project); + await mkdir(new URL(".", slicePath), { recursive: true }); + await writeFile(slicePath, JSON.stringify(slice)); + + const { exitCode, stdout } = await prismic("gen", ["types"]); + expect(exitCode).toBe(0); + expect(stdout).toContain("Generated types"); + + await expect(project).toHaveFile("prismicio-types.d.ts", { + contains: customType.id, + }); +}); + +it("generates types with no models", async ({ expect, project, prismic }) => { + const { exitCode, stdout } = await prismic("gen", ["types"]); + expect(exitCode).toBe(0); + expect(stdout).toContain("Generated types"); + + await expect(project).toHaveFile("prismicio-types.d.ts"); +}); + +function buildCustomType(): CustomType { + const id = crypto.randomUUID().split("-")[0]; + return { + id: `type-T${id}`, + label: `TypeT${id}`, + repeatable: true, + status: true, + json: {}, + }; +} + +function buildSlice(): SharedSlice { + const id = crypto.randomUUID().split("-")[0]; + return { + id: `slice-S${id}`, + type: "SharedSlice", + name: `SliceS${id}`, + variations: [ + { + id: "default", + name: "Default", + docURL: "", + version: "initial", + description: "Default", + imageUrl: "", + }, + ], + }; +} diff --git a/test/gen.test.ts b/test/gen.test.ts new file mode 100644 index 0000000..c0699b9 --- /dev/null +++ b/test/gen.test.ts @@ -0,0 +1,13 @@ +import { it } from "./it"; + +it("supports --help", async ({ expect, prismic }) => { + const { stdout, exitCode } = await prismic("gen", ["--help"]); + expect(exitCode).toBe(0); + expect(stdout).toContain("prismic gen [options]"); +}); + +it("prints help by default", async ({ expect, prismic }) => { + const { stdout, exitCode } = await prismic("gen"); + expect(exitCode).toBe(0); + expect(stdout).toContain("prismic gen [options]"); +}); diff --git a/vitest.config.ts b/vitest.config.ts index ad634eb..3ade042 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -5,7 +5,6 @@ export default defineConfig({ globalSetup: ["./test/setup.global.ts"], forceRerunTriggers: ["**/dist/index.mjs"], typecheck: { enabled: true }, - testTimeout: 10_000, retry: 1, projects: [ { @@ -15,6 +14,7 @@ export default defineConfig({ include: ["./test/**/*.test.ts"], exclude: ["./test/*.serial.test.ts"], sequence: { concurrent: true }, + testTimeout: 10_000, }, }, { @@ -24,6 +24,7 @@ export default defineConfig({ include: ["./test/*.serial.test.ts"], sequence: { concurrent: false }, fileParallelism: false, + testTimeout: 10_000, }, }, ],