diff --git a/.changeset/curly-cats-build.md b/.changeset/curly-cats-build.md new file mode 100644 index 00000000000..95f24ce1f97 --- /dev/null +++ b/.changeset/curly-cats-build.md @@ -0,0 +1,5 @@ +--- +'@keystone-6/core': patch +--- + +Adds support for Keystone projects using `"type": "module"` in their `package.json` by generating an empty `package.json` in `.keystone` so Node continues inferring module types in `.keystone` diff --git a/examples/custom-output-paths/my-types.ts b/examples/custom-output-paths/my-types.ts index 0c1217b9291..6a1ea60f927 100644 --- a/examples/custom-output-paths/my-types.ts +++ b/examples/custom-output-paths/my-types.ts @@ -119,22 +119,22 @@ export type KeystoneAdminUISortDirection = | 'DESC' type ResolvedPostCreateInput = { - id?: import('./generated/custom-prisma/client').Prisma.PostCreateInput['id'] - title?: import('./generated/custom-prisma/client').Prisma.PostCreateInput['title'] - content?: import('./generated/custom-prisma/client').Prisma.PostCreateInput['content'] - publishDate?: import('./generated/custom-prisma/client').Prisma.PostCreateInput['publishDate'] + id?: import('./generated/custom-prisma/client.js').Prisma.PostCreateInput['id'] + title?: import('./generated/custom-prisma/client.js').Prisma.PostCreateInput['title'] + content?: import('./generated/custom-prisma/client.js').Prisma.PostCreateInput['content'] + publishDate?: import('./generated/custom-prisma/client.js').Prisma.PostCreateInput['publishDate'] } type ResolvedPostUpdateInput = { id?: undefined - title?: import('./generated/custom-prisma/client').Prisma.PostUpdateInput['title'] - content?: import('./generated/custom-prisma/client').Prisma.PostUpdateInput['content'] - publishDate?: import('./generated/custom-prisma/client').Prisma.PostUpdateInput['publishDate'] + title?: import('./generated/custom-prisma/client.js').Prisma.PostUpdateInput['title'] + content?: import('./generated/custom-prisma/client.js').Prisma.PostUpdateInput['content'] + publishDate?: import('./generated/custom-prisma/client.js').Prisma.PostUpdateInput['publishDate'] } export declare namespace Lists { export type Post = import('@keystone-6/core/types').ListConfig> namespace Post { - export type Item = import('./generated/custom-prisma/client').Post + export type Item = import('./generated/custom-prisma/client.js').Post export type TypeInfo = { key: 'Post' isSingleton: false @@ -163,8 +163,8 @@ export type TypeInfo = { lists: { readonly Post: Lists.Post.TypeInfo } - prisma: import('./generated/custom-prisma/client').PrismaClient - prismaClientOptions: import('./generated/custom-prisma/client').Prisma.PrismaClientOptions + prisma: import('./generated/custom-prisma/client.js').PrismaClient + prismaClientOptions: import('./generated/custom-prisma/client.js').Prisma.PrismaClientOptions session: Session dbProvider: 'sqlite' } diff --git a/examples/extend-graphql-schema-nexus/keystone-types.ts b/examples/extend-graphql-schema-nexus/keystone-types.ts index 75804ed500f..f6df5f05059 100644 --- a/examples/extend-graphql-schema-nexus/keystone-types.ts +++ b/examples/extend-graphql-schema-nexus/keystone-types.ts @@ -200,36 +200,36 @@ export type KeystoneAdminUISortDirection = | 'DESC' type ResolvedPostCreateInput = { - id?: import('./generated/prisma/client').Prisma.PostCreateInput['id'] - title?: import('./generated/prisma/client').Prisma.PostCreateInput['title'] - status?: import('./generated/prisma/client').Prisma.PostCreateInput['status'] - content?: import('./generated/prisma/client').Prisma.PostCreateInput['content'] - publishDate?: import('./generated/prisma/client').Prisma.PostCreateInput['publishDate'] - author?: import('./generated/prisma/client').Prisma.PostCreateInput['author'] + id?: import('./generated/prisma/client.js').Prisma.PostCreateInput['id'] + title?: import('./generated/prisma/client.js').Prisma.PostCreateInput['title'] + status?: import('./generated/prisma/client.js').Prisma.PostCreateInput['status'] + content?: import('./generated/prisma/client.js').Prisma.PostCreateInput['content'] + publishDate?: import('./generated/prisma/client.js').Prisma.PostCreateInput['publishDate'] + author?: import('./generated/prisma/client.js').Prisma.PostCreateInput['author'] } type ResolvedPostUpdateInput = { id?: undefined - title?: import('./generated/prisma/client').Prisma.PostUpdateInput['title'] - status?: import('./generated/prisma/client').Prisma.PostUpdateInput['status'] - content?: import('./generated/prisma/client').Prisma.PostUpdateInput['content'] - publishDate?: import('./generated/prisma/client').Prisma.PostUpdateInput['publishDate'] - author?: import('./generated/prisma/client').Prisma.PostUpdateInput['author'] + title?: import('./generated/prisma/client.js').Prisma.PostUpdateInput['title'] + status?: import('./generated/prisma/client.js').Prisma.PostUpdateInput['status'] + content?: import('./generated/prisma/client.js').Prisma.PostUpdateInput['content'] + publishDate?: import('./generated/prisma/client.js').Prisma.PostUpdateInput['publishDate'] + author?: import('./generated/prisma/client.js').Prisma.PostUpdateInput['author'] } type ResolvedAuthorCreateInput = { - id?: import('./generated/prisma/client').Prisma.AuthorCreateInput['id'] - name?: import('./generated/prisma/client').Prisma.AuthorCreateInput['name'] - posts?: import('./generated/prisma/client').Prisma.AuthorCreateInput['posts'] + id?: import('./generated/prisma/client.js').Prisma.AuthorCreateInput['id'] + name?: import('./generated/prisma/client.js').Prisma.AuthorCreateInput['name'] + posts?: import('./generated/prisma/client.js').Prisma.AuthorCreateInput['posts'] } type ResolvedAuthorUpdateInput = { id?: undefined - name?: import('./generated/prisma/client').Prisma.AuthorUpdateInput['name'] - posts?: import('./generated/prisma/client').Prisma.AuthorUpdateInput['posts'] + name?: import('./generated/prisma/client.js').Prisma.AuthorUpdateInput['name'] + posts?: import('./generated/prisma/client.js').Prisma.AuthorUpdateInput['posts'] } export declare namespace Lists { export type Post = import('@keystone-6/core/types').ListConfig> namespace Post { - export type Item = import('./generated/prisma/client').Post + export type Item = import('./generated/prisma/client.js').Post export type TypeInfo = { key: 'Post' isSingleton: false @@ -252,7 +252,7 @@ export declare namespace Lists { } export type Author = import('@keystone-6/core/types').ListConfig> namespace Author { - export type Item = import('./generated/prisma/client').Author + export type Item = import('./generated/prisma/client.js').Author export type TypeInfo = { key: 'Author' isSingleton: false @@ -282,8 +282,8 @@ export type TypeInfo = { readonly Post: Lists.Post.TypeInfo readonly Author: Lists.Author.TypeInfo } - prisma: import('./generated/prisma/client').PrismaClient - prismaClientOptions: import('./generated/prisma/client').Prisma.PrismaClientOptions + prisma: import('./generated/prisma/client.js').PrismaClient + prismaClientOptions: import('./generated/prisma/client.js').Prisma.PrismaClientOptions session: Session dbProvider: 'sqlite' } diff --git a/packages/core/src/lib/system.ts b/packages/core/src/lib/system.ts index 2f2b0357b0c..6b4ac3bf25e 100644 --- a/packages/core/src/lib/system.ts +++ b/packages/core/src/lib/system.ts @@ -35,7 +35,12 @@ function getSystemPaths(cwd: string, config: KeystoneConfig) { const builtPrismaPath = path.resolve(cwd, config.db.prismaSchemaPath) - const relativePrismaPath = relativeModulePath(path.dirname(builtTypesPath), prismaClientPath) + // note using .js is the best way to generically import TypeScript types since TS: + // - allows .js imports to .ts files in all module resolution modes with any options + // - doesn't allow bare imports to .ts files in with moduleResolution: nodenext + "type": "module" (like Node) + // - only allows .ts imports to .ts files when using allowImportingTsExtensions: true + const relativePrismaPath = + relativeModulePath(path.dirname(builtTypesPath), prismaClientPath) + '.js' const builtGraphqlPath = config.graphql?.schemaPath ? path.join(cwd, config.graphql.schemaPath) // TODO: enforce initConfig before getSystemPaths diff --git a/packages/core/src/scripts/esbuild.ts b/packages/core/src/scripts/esbuild.ts index cd9c2d1c757..35faf87cfe6 100644 --- a/packages/core/src/scripts/esbuild.ts +++ b/packages/core/src/scripts/esbuild.ts @@ -9,6 +9,18 @@ function identity(x: BuildOptions) { return x } +async function ensureKeystoneDirectory(cwd: string) { + const directory = nodePath.join(cwd, '.keystone') + await fs.mkdir(directory, { recursive: true }) + // this is to reset the module type to be ambiguous in `.keystone` + // so that Keystone users can use "type": "module" in their package.json + // we use an empty object instead of "type": "commonjs" + // because inside `.keystone/admin` the ambiguous behaviour is expected + // e.g. next.config.js is commonjs but pages are ESM + // even though Node doesn't load the pages, Turbopack errors on the ESM pages if "type": "commonjs" is set + await fs.writeFile(nodePath.join(directory, 'package.json'), '{}\n') +} + async function getEsbuildConfigFn( cwd: string ): Promise<((x: BuildOptions) => BuildOptions | Promise) | undefined> { @@ -40,15 +52,10 @@ async function getEsbuildConfigForEntry( outfile: string, define?: BuildOptions['define'] ): Promise { + await ensureKeystoneDirectory(cwd) const esbuildFn = (await getEsbuildConfigFn(cwd)) ?? identity const resolveDir = nodePath.join(cwd, '.keystone') const importer = outfile - // we need the .keystone directory to exist so when we resolve from it below, it actually exists - await fs.mkdir(resolveDir, { - // while we don't need to actually make this recursive, - // this will make mkdir not error when the directory already exists - recursive: true, - }) return esbuildFn({ entryPoints: [entryPoint], absWorkingDir: cwd, diff --git a/tests/cli-tests/__snapshots__/artifacts.test.ts.snap b/tests/cli-tests/__snapshots__/artifacts.test.ts.snap index 17fee7e84ab..c51e52eaefe 100644 --- a/tests/cli-tests/__snapshots__/artifacts.test.ts.snap +++ b/tests/cli-tests/__snapshots__/artifacts.test.ts.snap @@ -104,18 +104,18 @@ export type KeystoneAdminUISortDirection = | 'DESC' type ResolvedTodoCreateInput = { - id?: import('../prisma/client').Prisma.TodoCreateInput['id'] - title?: import('../prisma/client').Prisma.TodoCreateInput['title'] + id?: import('../prisma/client.js').Prisma.TodoCreateInput['id'] + title?: import('../prisma/client.js').Prisma.TodoCreateInput['title'] } type ResolvedTodoUpdateInput = { id?: undefined - title?: import('../prisma/client').Prisma.TodoUpdateInput['title'] + title?: import('../prisma/client.js').Prisma.TodoUpdateInput['title'] } export declare namespace Lists { export type Todo = import('@keystone-6/core/types').ListConfig> namespace Todo { - export type Item = import('../prisma/client').Todo + export type Item = import('../prisma/client.js').Todo export type TypeInfo = { key: 'Todo' isSingleton: false @@ -144,8 +144,8 @@ export type TypeInfo = { lists: { readonly Todo: Lists.Todo.TypeInfo } - prisma: import('../prisma/client').PrismaClient - prismaClientOptions: import('../prisma/client').Prisma.PrismaClientOptions + prisma: import('../prisma/client.js').PrismaClient + prismaClientOptions: import('../prisma/client.js').Prisma.PrismaClientOptions session: Session dbProvider: 'sqlite' } diff --git a/tests/cli-tests/build.test.ts b/tests/cli-tests/build.test.ts index 024361d97cd..5d1405d588c 100644 --- a/tests/cli-tests/build.test.ts +++ b/tests/cli-tests/build.test.ts @@ -22,10 +22,13 @@ test("start errors when a build hasn't happened", async () => { await expect(cliMock(cwd, 'start')).rejects.toEqual(new Error('You need to run "keystone build"')) }) -test('build works with typescript without the user defining a babel config', async () => { +test('build works in a Node ESM project without the user defining a babel config', async () => { const cwd = await testdir({ ...symlinkKeystoneDeps, ...schemas, + 'package.json': JSON.stringify({ type: 'module' }), + // this is just to show it gets overridden + '.keystone/package.json': JSON.stringify({ type: 'module' }), 'keystone.ts': await readFile(`${__dirname}/fixtures/with-ts.ts`, 'utf8'), }) const result = await execa('node', [cliBinPath, 'build'], { @@ -36,10 +39,11 @@ test('build works with typescript without the user defining a babel config', asy NEXT_TELEMETRY_DISABLED: '1', } as any, }) + expect(result.exitCode, result.all).toBe(0) expect(result.stdout.includes('Compiled successfully')).toBe(true) expect(result.stdout.includes('Generating static pages')).toBe(true) expect(result.stdout.includes('Finalizing page optimization')).toBe(true) - expect(result.exitCode).toBe(0) + expect(await readFile(`${cwd}/.keystone/package.json`, 'utf8')).toEqual('{}\n') expect(require(`${cwd}/.keystone/config.js`)).toEqual( expect.objectContaining({ default: expect.any(Object) }) )