diff --git a/packages/quicktype-core/package.json b/packages/quicktype-core/package.json index 3fd669c73..463b80236 100644 --- a/packages/quicktype-core/package.json +++ b/packages/quicktype-core/package.json @@ -48,6 +48,7 @@ }, "files": ["dist"], "browser": { - "fs": false + "fs": false, + "path": false } } diff --git a/packages/quicktype-core/src/input/io/NodeIO.ts b/packages/quicktype-core/src/input/io/NodeIO.ts index 2b3995c3b..0f5095dc4 100644 --- a/packages/quicktype-core/src/input/io/NodeIO.ts +++ b/packages/quicktype-core/src/input/io/NodeIO.ts @@ -1,5 +1,5 @@ -import * as fs from "node:fs"; -import * as path from "node:path"; +import * as fs from "fs"; +import * as path from "path"; import { defined, exceptionToString } from "@glideapps/ts-necessities"; import { isNode } from "browser-or-node"; diff --git a/test/check-no-node-imports.ts b/test/check-no-node-imports.ts new file mode 100644 index 000000000..3e727b9ec --- /dev/null +++ b/test/check-no-node-imports.ts @@ -0,0 +1,76 @@ +// This is the Node-only test harness, so "node:" imports are fine *here* — +// the guard below only restricts packages/quicktype-core/src. +import * as fs from "node:fs"; +import * as path from "node:path"; + +// Guard: quicktype-core must stay bundleable for the browser. +// +// quicktype-core's package.json declares `"browser": { "fs": false }`, which +// tells web bundlers (webpack, browserify, ...) to stub out the `fs` module. +// That mapping only matches the *bare* specifier "fs" — an import of +// "node:fs" (or any other "node:"-prefixed builtin) is NOT remapped, so it +// breaks web bundles of quicktype-core even though the code behaves the same +// under Node. This regression already happened once between 23.2.0 and +// 23.2.5: see https://github.com/glideapps/quicktype/issues/2763. +// +// This check fails the test run if any "node:"-prefixed import sneaks back +// into packages/quicktype-core/src. It is scoped to quicktype-core only: the +// CLI in the root `src/` directory is Node-only and may use "node:" imports +// freely. + +const coreSrcDir = path.join( + __dirname, + "..", + "packages", + "quicktype-core", + "src", +); + +// Matches static imports/re-exports (`from "node:fs"`), dynamic imports +// (`import("node:fs")`), and CommonJS requires (`require("node:fs")`). +const nodeImportPattern = /(?:from\s+|import\s*\(\s*|require\s*\(\s*)["']node:/; + +function findNodeImports(dir: string): string[] { + const offenders: string[] = []; + for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { + const fullPath = path.join(dir, entry.name); + if (entry.isDirectory()) { + offenders.push(...findNodeImports(fullPath)); + } else if (entry.isFile() && entry.name.endsWith(".ts")) { + const lines = fs.readFileSync(fullPath, "utf8").split("\n"); + for (let i = 0; i < lines.length; i++) { + if (nodeImportPattern.test(lines[i])) { + offenders.push( + `${path.relative(path.join(__dirname, ".."), fullPath)}:${i + 1}: ${lines[i].trim()}`, + ); + } + } + } + } + + return offenders; +} + +export function checkCoreHasNoNodePrefixedImports(): void { + const offenders = findNodeImports(coreSrcDir); + if (offenders.length > 0) { + const offenderList = offenders.map((o) => ` ${o}`).join("\n"); + console.error( + `error: found "node:"-prefixed imports in packages/quicktype-core/src: + +${offenderList} + +quicktype-core must use bare builtin specifiers (e.g. "fs", not "node:fs"): +its package.json's "browser" field only stubs bare specifiers, so "node:" +imports break web bundlers. See https://github.com/glideapps/quicktype/issues/2763`, + ); + process.exit(1); + } +} + +// Allow running the check standalone: +// npx ts-node --project test/tsconfig.json test/check-no-node-imports.ts +if (require.main === module) { + checkCoreHasNoNodePrefixedImports(); + console.error('* quicktype-core has no "node:"-prefixed imports'); +} diff --git a/test/test.ts b/test/test.ts index f6015eafa..f9f4e316a 100755 --- a/test/test.ts +++ b/test/test.ts @@ -5,6 +5,7 @@ import { inParallel } from "./lib/multicore"; import { execAsync, type Sample } from "./utils"; import { type Fixture, allFixtures } from "./fixtures"; import { affectedFixtures, divideParallelJobs } from "./buildkite"; +import { checkCoreHasNoNodePrefixedImports } from "./check-no-node-imports"; const exit = require("exit"); const CPUs = Number.parseInt(process.env.CPUs || "0", 10) || os.cpus().length; @@ -16,6 +17,10 @@ const CPUs = Number.parseInt(process.env.CPUs || "0", 10) || os.cpus().length; export type WorkItem = { sample: Sample; fixtureName: string }; async function main(sources: string[]) { + // Cheap sanity check, run before any fixture: quicktype-core must not + // use "node:"-prefixed imports or it breaks web bundlers (issue #2763). + checkCoreHasNoNodePrefixedImports(); + let fixtures = affectedFixtures(); const fixturesFromCmdline = process.env.FIXTURE; if (fixturesFromCmdline) {