From 7b7dc718456d45109837c8fa6cdc12c4da044da2 Mon Sep 17 00:00:00 2001 From: Alessio Gravili Date: Mon, 10 Jun 2024 18:03:12 -0400 Subject: [PATCH] fix: get auto type-gen to work on turbo, by running type gen in a child process outside turbo/webpack (#6714) Before on turbo: https://github.com/vercel/next.js/issues/66723 --- packages/next/src/utilities/getPayloadHMR.ts | 8 +++- packages/next/src/withPayload.js | 2 - packages/next/webpack.config.js | 1 - packages/payload/bin.js | 9 +++-- packages/payload/src/config/find.ts | 40 ++++++++++---------- packages/payload/src/exports/bin.ts | 5 --- packages/payload/src/index.ts | 39 ++++++++++++++++++- test/testHooks.js | 2 +- 8 files changed, 69 insertions(+), 37 deletions(-) delete mode 100644 packages/payload/src/exports/bin.ts diff --git a/packages/next/src/utilities/getPayloadHMR.ts b/packages/next/src/utilities/getPayloadHMR.ts index 69d660fe23..62a3dbbe4c 100644 --- a/packages/next/src/utilities/getPayloadHMR.ts +++ b/packages/next/src/utilities/getPayloadHMR.ts @@ -2,7 +2,6 @@ import type { GeneratedTypes, Payload } from 'payload' import type { InitOptions, SanitizedConfig } from 'payload/config' import { BasePayload } from 'payload' -import { generateTypes } from 'payload/bin' import WebSocket from 'ws' let cached: { @@ -39,7 +38,12 @@ export const reload = async (config: SanitizedConfig, payload: Payload): Promise // Generate types if (config.typescript.autoGenerate !== false) { - void generateTypes(config, { log: false }) + // We cannot run it directly here, as generate-types imports json-schema-to-typescript, which breaks on turbopack. + // see: https://github.com/vercel/next.js/issues/66723 + void payload.bin({ + args: ['generate:types'], + log: false, + }) } await payload.db.init() diff --git a/packages/next/src/withPayload.js b/packages/next/src/withPayload.js index 1f17f2ff74..a6432addd4 100644 --- a/packages/next/src/withPayload.js +++ b/packages/next/src/withPayload.js @@ -68,7 +68,6 @@ export const withPayload = (nextConfig = {}) => { 'pino', 'pino-pretty', 'graphql', - 'json-schema-to-typescript', ], webpack: (webpackConfig, webpackOptions) => { const incomingWebpackConfig = @@ -84,7 +83,6 @@ export const withPayload = (nextConfig = {}) => { 'drizzle-kit/payload', 'sharp', 'libsql', - 'json-schema-to-typescript', ], ignoreWarnings: [ ...(incomingWebpackConfig?.ignoreWarnings || []), diff --git a/packages/next/webpack.config.js b/packages/next/webpack.config.js index 3c7dedb6d0..8eb051b796 100644 --- a/packages/next/webpack.config.js +++ b/packages/next/webpack.config.js @@ -18,7 +18,6 @@ const componentWebpackConfig = { 'payload/config', 'react-image-crop', 'payload/operations', - 'payload/bin', ], mode: 'production', module: { diff --git a/packages/payload/bin.js b/packages/payload/bin.js index 93e8599b8b..bf72b37386 100755 --- a/packages/payload/bin.js +++ b/packages/payload/bin.js @@ -4,8 +4,6 @@ import { register } from 'node:module' import path from 'node:path' import { fileURLToPath, pathToFileURL } from 'node:url' -import { bin } from './dist/bin/index.js' - // Allow disabling SWC for debugging if (process.env.DISABLE_SWC !== 'true') { const filename = fileURLToPath(import.meta.url) @@ -15,4 +13,9 @@ if (process.env.DISABLE_SWC !== 'true') { register('./dist/bin/loader/index.js', url) } -bin() +const start = async () => { + const { bin } = await import('./dist/bin/index.js') + bin() +} + +void start() diff --git a/packages/payload/src/config/find.ts b/packages/payload/src/config/find.ts index d0f5b13b1a..637a4130c4 100644 --- a/packages/payload/src/config/find.ts +++ b/packages/payload/src/config/find.ts @@ -1,5 +1,5 @@ import { findUpSync, pathExistsSync } from 'find-up' -import fs from 'fs' +import { getTsconfig } from 'get-tsconfig' import path from 'path' /** @@ -12,37 +12,30 @@ const getTSConfigPaths = (): { outPath?: string rootPath?: string srcPath?: string + tsConfigPath?: string } => { - const tsConfigPath = findUpSync('tsconfig.json') - - if (!tsConfigPath) { - return { - rootPath: process.cwd(), - } - } + const tsConfigResult = getTsconfig() + const tsConfig = tsConfigResult.config + const tsConfigDir = path.dirname(tsConfigResult.path) try { - // Read the file as a string and remove trailing commas - const rawTsConfig = fs - .readFileSync(tsConfigPath, 'utf-8') - .replace(/,\s*\]/g, ']') - .replace(/,\s*\}/g, '}') - - const tsConfig = JSON.parse(rawTsConfig) - - const rootPath = process.cwd() + const rootConfigDir = path.resolve(tsConfigDir, tsConfig.compilerOptions.baseUrl || '') const srcPath = tsConfig.compilerOptions?.rootDir || path.resolve(process.cwd(), 'src') const outPath = tsConfig.compilerOptions?.outDir || path.resolve(process.cwd(), 'dist') - const tsConfigDir = path.dirname(tsConfigPath) - let configPath = tsConfig.compilerOptions?.paths?.['@payload-config']?.[0] + let configPath = path.resolve( + rootConfigDir, + tsConfig.compilerOptions?.paths?.['@payload-config']?.[0], + ) + if (configPath) { - configPath = path.resolve(tsConfigDir, configPath) + configPath = path.resolve(rootConfigDir, configPath) } return { configPath, outPath, - rootPath, + rootPath: rootConfigDir, srcPath, + tsConfigPath: tsConfigResult.path, } } catch (error) { console.error(`Error parsing tsconfig.json: ${error}`) // Do not throw the error, as we can still continue with the other config path finding methods @@ -70,6 +63,11 @@ export const findConfig = (): string => { const { configPath, outPath, rootPath, srcPath } = getTSConfigPaths() + // if configPath is absolute file, not folder, return it + if (path.extname(configPath) === '.js' || path.extname(configPath) === '.ts') { + return configPath + } + const searchPaths = process.env.NODE_ENV === 'production' ? [configPath, outPath, srcPath, rootPath] diff --git a/packages/payload/src/exports/bin.ts b/packages/payload/src/exports/bin.ts deleted file mode 100644 index 5cd0b14e92..0000000000 --- a/packages/payload/src/exports/bin.ts +++ /dev/null @@ -1,5 +0,0 @@ -/** - * WARNING: This file contains exports that can only be safely used on the server. - */ - -export { generateTypes } from '../bin/generateTypes.js' diff --git a/packages/payload/src/index.ts b/packages/payload/src/index.ts index 0a88e31eba..5fddbb37f8 100644 --- a/packages/payload/src/index.ts +++ b/packages/payload/src/index.ts @@ -2,7 +2,10 @@ import type { ExecutionResult, GraphQLSchema, ValidationRule } from 'graphql' import type { OperationArgs, Request as graphQLRequest } from 'graphql-http' import type pino from 'pino' +import { spawn } from 'child_process' import crypto from 'crypto' +import { fileURLToPath } from 'node:url' +import path from 'path' import type { AuthArgs } from './auth/operations/auth.js' import type { Result as ForgotPasswordResult } from './auth/operations/forgotPassword.js' @@ -47,7 +50,6 @@ import type { TypeWithVersion } from './versions/types.js' import { decrypt, encrypt } from './auth/crypto.js' import { APIKeyAuthentication } from './auth/strategies/apiKey.js' import { JWTAuthentication } from './auth/strategies/jwt.js' -import { generateTypes } from './bin/generateTypes.js' import localOperations from './collections/operations/local/index.js' import { validateSchema } from './config/validate.js' import { consoleEmailAdapter } from './email/consoleEmailAdapter.js' @@ -57,6 +59,9 @@ import flattenFields from './utilities/flattenTopLevelFields.js' import Logger from './utilities/logger.js' import { serverInit as serverInitTelemetry } from './utilities/telemetry/events/serverInit.js' +const filename = fileURLToPath(import.meta.url) +const dirname = path.dirname(filename) + /** * @description Payload */ @@ -302,6 +307,31 @@ export class BasePayload { [slug: string]: any // TODO: Type this } = {} + async bin({ + args, + cwd, + log, + }: { + args: string[] + cwd?: string + log?: boolean + }): Promise<{ code: number }> { + return new Promise((resolve, reject) => { + const spawned = spawn('node', [path.resolve(dirname, '../bin.js'), ...args], { + cwd, + stdio: log || log === undefined ? 'inherit' : 'ignore', + }) + + spawned.on('exit', (code) => { + resolve({ code }) + }) + + spawned.on('error', (error) => { + reject(error) + }) + }) + } + /** * @description delete one or more documents * @param options @@ -366,7 +396,12 @@ export class BasePayload { // Generate types on startup if (process.env.NODE_ENV !== 'production' && this.config.typescript.autoGenerate !== false) { - void generateTypes(this.config, { log: false }) + // We cannot run it directly here, as generate-types imports json-schema-to-typescript, which breaks on turbopack. + // see: https://github.com/vercel/next.js/issues/66723 + void this.bin({ + args: ['generate:types'], + log: false, + }) } this.db = this.config.db.init({ payload: this }) diff --git a/test/testHooks.js b/test/testHooks.js index c0e29620db..18c450f8d5 100644 --- a/test/testHooks.js +++ b/test/testHooks.js @@ -32,7 +32,7 @@ export const createTestHooks = async (testSuiteName = '_community') => { tsConfig.compilerOptions.paths['@payload-config'] = [`./test/${testSuiteName}/config.ts`] await writeFile(tsConfigPath, stringify(tsConfig, null, 2) + '\n') - process.env.PAYLOAD_CONFIG_PATH = path.resolve(testSuiteName, 'config') + process.env.PAYLOAD_CONFIG_PATH = path.resolve(dirname, testSuiteName, 'config.ts') }, } }