From d2089947ca83cd00de299645516d77b8dd0fd8ca Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Sat, 23 Nov 2024 14:04:34 +0100 Subject: [PATCH] feat(core): allow creation of static imports from a transform function Export createDefaultImport and createNamedImport functions to create static imports from within a transform function. --- .changeset/healthy-ducks-kick.md | 5 ++ packages/core/src/config.ts | 18 ++++- packages/core/src/import.ts | 28 +++++++ packages/core/src/index.ts | 1 + packages/core/src/serializer.test.ts | 76 ++++++++++++++++++ packages/core/src/serializer.ts | 40 +++++++++- packages/core/src/types.test.ts | 114 ++++++++++++++++++++++++++- packages/core/tsconfig.json | 3 +- 8 files changed, 280 insertions(+), 5 deletions(-) create mode 100644 .changeset/healthy-ducks-kick.md create mode 100644 packages/core/src/import.ts diff --git a/.changeset/healthy-ducks-kick.md b/.changeset/healthy-ducks-kick.md new file mode 100644 index 00000000..b44b9c46 --- /dev/null +++ b/.changeset/healthy-ducks-kick.md @@ -0,0 +1,5 @@ +--- +"@content-collections/core": minor +--- + +Allow creation of static imports from a transform function diff --git a/packages/core/src/config.ts b/packages/core/src/config.ts index fffb8633..3008a642 100644 --- a/packages/core/src/config.ts +++ b/packages/core/src/config.ts @@ -1,5 +1,6 @@ import { ZodObject, ZodRawShape, ZodString, ZodTypeAny, z } from "zod"; import { CacheFn } from "./cache"; +import { GetTypeOfImport, Import } from "./import"; import { Parser, Parsers } from "./parser"; import { NotSerializableError, Serializable } from "./serializer"; import { generateTypeName } from "./utils"; @@ -111,6 +112,21 @@ type InvalidReturnType = { object: TObject; }; +type ResolveImports = + TTransformResult extends Import + ? GetTypeOfImport + : TTransformResult extends Array + ? Array> + : TTransformResult extends (...args: any[]) => any + ? TTransformResult + : TTransformResult extends object + ? { + [K in keyof TTransformResult]: ResolveImports< + TTransformResult[K] + >; + } + : TTransformResult; + export function defineCollection< TName extends string, TShape extends ZodRawShape, @@ -121,7 +137,7 @@ export function defineCollection< ? Schema : Awaited, TResult = TDocument extends Serializable - ? Collection + ? Collection> : InvalidReturnType, >( collection: CollectionRequest< diff --git a/packages/core/src/import.ts b/packages/core/src/import.ts new file mode 100644 index 00000000..f0b6a33b --- /dev/null +++ b/packages/core/src/import.ts @@ -0,0 +1,28 @@ +const importSymbol = Symbol("import"); + +export type Import = { + [importSymbol]: true; + path: string; + name?: string; +}; + +export type GetTypeOfImport = T extends Import ? U : never; + +export function isImport(value: any): value is Import { + return value && value[importSymbol]; +} + +export function createDefaultImport(path: string): Import { + return { + [importSymbol]: true, + path, + }; +} + +export function createNamedImport(name: string, path: string): Import { + return { + [importSymbol]: true, + path, + name, + }; +} diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 94ab7b50..22254060 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -4,5 +4,6 @@ export type { Document, GetTypeByName, Modification } from "./types"; export { CollectError } from "./collector"; export { ConfigurationError } from "./configurationReader"; +export { createDefaultImport, createNamedImport } from "./import"; export { TransformError } from "./transformer"; export { type Watcher } from "./watcher"; diff --git a/packages/core/src/serializer.test.ts b/packages/core/src/serializer.test.ts index 58b31ac5..9c4dd9ac 100644 --- a/packages/core/src/serializer.test.ts +++ b/packages/core/src/serializer.test.ts @@ -2,6 +2,7 @@ import fs from "node:fs/promises"; import path from "node:path"; import { describe, expect, it } from "vitest"; import { tmpdirTest } from "./__tests__/tmpdir"; +import { createDefaultImport, createNamedImport } from "./import"; import { extension, serializableSchema, serialize } from "./serializer"; describe("serializer", () => { @@ -149,5 +150,80 @@ describe("serializer", () => { expect(imported[0].bigint).toBeTypeOf("bigint"); }, ); + + tmpdirTest( + "should serialize an object with an import", + async ({ tmpdir }) => { + const object = { + name: "sample", + fso: createDefaultImport("node:fs/promises"), + }; + + const imported = await writeAndImport(tmpdir, object); + expect(imported).toEqual([{ name: "sample", fso: fs }]); + }, + ); + + tmpdirTest( + "should serialize an object with an a nested import", + async ({ tmpdir }) => { + const object = { + name: "sample", + imports: { + fso: createDefaultImport("node:fs/promises"), + }, + }; + + const [imported] = await writeAndImport(tmpdir, object); + expect(imported.imports.fso).toEqual(fs); + }, + ); + + tmpdirTest( + "should serialize an object with multiple imports", + async ({ tmpdir }) => { + const object = { + name: "sample", + imports: { + fs: createDefaultImport("node:fs/promises"), + path: createDefaultImport("node:path"), + }, + }; + + const [imported] = await writeAndImport(tmpdir, object); + expect(imported.imports.fs).toEqual(fs); + expect(imported.imports.path).toEqual(path); + }, + ); + + tmpdirTest( + "should serialize an object with an array of imports", + async ({ tmpdir }) => { + const object = { + name: "sample", + imports: [ + createDefaultImport("node:fs/promises"), + createDefaultImport("node:path"), + ], + }; + + const [imported] = await writeAndImport(tmpdir, object); + expect(imported.imports[0]).toEqual(fs); + expect(imported.imports[1]).toEqual(path); + }, + ); + + tmpdirTest( + "should serialize an object with an name import", + async ({ tmpdir }) => { + const object = { + name: "sample", + join: createNamedImport("join", "node:path"), + }; + + const [imported] = await writeAndImport(tmpdir, object); + expect(imported.join).toEqual(path.join); + }, + ); }); }); diff --git a/packages/core/src/serializer.ts b/packages/core/src/serializer.ts index 3b6427df..140ba9fa 100644 --- a/packages/core/src/serializer.ts +++ b/packages/core/src/serializer.ts @@ -1,5 +1,6 @@ import serializeJs from "serialize-javascript"; import z from "zod"; +import { Import, isImport } from "./import"; const literalSchema = z.union([ // json @@ -35,11 +36,46 @@ export const serializableSchema = z.record(schema); export type Serializable = z.infer; +function createImport(imp: Import, variableName: string): string { + const variableDeclaration = imp.name + ? `{ ${imp.name} as ${variableName} }` + : variableName; + + return `import ${variableDeclaration} from "${imp.path}";\n`; +} + export function serialize(value: Array): string { - const serializedValue = serializeJs(value, { + let serializedValue = ""; + let counter = 0; + + function handleImports(item: any) { + if (item instanceof Object) { + Object.entries(item).forEach(([key, value]) => { + if (isImport(value)) { + counter++; + const variableName = `__v_${counter}`; + serializedValue += createImport(value, variableName); + item[key] = variableName; + } else if (value instanceof Object) { + handleImports(value); + } + }); + } + } + + value.forEach(handleImports); + + serializedValue += "\n"; + + const js = serializeJs(value, { space: 2, unsafe: true, ignoreFunction: true, + }).replace(/"__v_(\d+)"/g, (_, index) => { + return `__v_${index}`; }); - return `export default ${serializedValue};`; + + serializedValue += "export default " + js; + + return serializedValue; } diff --git a/packages/core/src/types.test.ts b/packages/core/src/types.test.ts index 1ad5d0b7..14018ca4 100644 --- a/packages/core/src/types.test.ts +++ b/packages/core/src/types.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it } from "vitest"; import { defineCollection, defineConfig } from "./config"; +import { createDefaultImport } from "./import"; import { GetTypeByName } from "./types"; describe("types", () => { @@ -324,11 +325,11 @@ describe("types", () => { directory: "./persons", include: "*.md", schema: (z) => ({ - // date is not a valid json date: z.string(), }), transform: (data) => { return { + // functions are not serial fn: () => {}, }; }, @@ -337,4 +338,115 @@ describe("types", () => { // @ts-expect-error content is not a valid json object expect(collection.name).toBeDefined(); }); + + it("should return the generic of the import", () => { + type Content = { + mdx: string; + }; + + const collection = defineCollection({ + name: "posts", + directory: "./posts", + include: "*.mdx", + schema: (z) => ({ + title: z.string(), + }), + transform: ({ _meta, ...rest }) => { + const content = createDefaultImport("./content"); + return { + ...rest, + content, + }; + }, + }); + + const config = defineConfig({ + collections: [collection], + }); + + type Post = GetTypeByName; + + const post: Post = { + title: "Hello World", + content: { + mdx: "# MDX Content", + }, + }; + + expect(post).toBeTruthy(); + }); + + it("should return the generic of a nested import", () => { + type Content = { + mdx: string; + }; + + const collection = defineCollection({ + name: "posts", + directory: "./posts", + include: "*.mdx", + schema: (z) => ({ + title: z.string(), + }), + transform: ({ _meta, content: _, ...rest }) => { + const content = createDefaultImport("./content"); + return { + ...rest, + props: { + content, + }, + }; + }, + }); + + const config = defineConfig({ + collections: [collection], + }); + + type Post = GetTypeByName; + + const post: Post = { + title: "Hello World", + props: { + content: { + mdx: "# MDX Content", + }, + } + }; + + expect(post).toBeTruthy(); + }); + + it("should return a function from a resolve import", () => { + type Content = () => string; + + const collection = defineCollection({ + name: "posts", + directory: "./posts", + include: "*.mdx", + schema: (z) => ({ + title: z.string(), + }), + transform: ({ _meta, ...rest }) => { + const content = createDefaultImport("./content"); + return { + ...rest, + content, + }; + }, + }); + + const config = defineConfig({ + collections: [collection], + }); + + type Post = GetTypeByName; + + const post: Post = { + title: "Hello World", + content: () => "# MDX Content", + }; + + expect(post).toBeTruthy(); + }); }); diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json index d48da4a8..145ed560 100644 --- a/packages/core/tsconfig.json +++ b/packages/core/tsconfig.json @@ -23,5 +23,6 @@ "@content-collections/core": ["./src"] } }, - "include": ["src/**/*"] + "include": ["src/**/*"], + "exclude": ["src/__tests__/**/*"] }