diff --git a/.changeset/precompile-bundle.md b/.changeset/precompile-bundle.md new file mode 100644 index 0000000..a8fa678 --- /dev/null +++ b/.changeset/precompile-bundle.md @@ -0,0 +1,5 @@ +--- +"schema-utils": patch +--- + +Bundle `schema-utils` without errors or warnings: `precompile` requires the `ajv` helpers it inlines by name and loads webpack's minifier with Node.js's own `require`, so `minify` works from a webpack bundle too. diff --git a/src/precompile.js b/src/precompile.js index 73c9947..843c970 100644 --- a/src/precompile.js +++ b/src/precompile.js @@ -130,6 +130,13 @@ function addKeywords(ajv, hoist) { }); } +// Each helper is required by its own name: a request built from the name would make a bundler +// that bundles `schema-utils` parse every file in `ajv/dist/runtime` +const INLINE_HELPERS = { + equal: () => require("ajv/dist/runtime/equal").default, + ucs2length: () => require("ajv/dist/runtime/ucs2length").default, +}; + /** * @param {string} code generated code * @param {boolean} esm whether the code is an ES module @@ -147,7 +154,7 @@ function useRuntime(code, esm, inline) { // WHY: loading a helper through `schema-utils` costs about 1ms, more than the check itself // takes to run for most options - both functions are small and self-contained, `uri` is not if (inline && name !== "uri") { - return `(${require(`ajv/dist/runtime/${name}`).default.toString()})`; + return `(${INLINE_HELPERS[/** @type {"equal" | "ucs2length"} */ (name)]().toString()})`; } // ajv keeps `require` for its helpers in an ES module too @@ -262,12 +269,20 @@ async function minify(code, esm) { let minifier; // webpack's own minifier, which writes what terser writes without loading it - the packages - // shipping a check already have webpack. Required by name so neither TypeScript nor a bundler - // reaches for it. - const WEBPACK = "webpack"; + // shipping a check already have webpack. A bundler that bundles `schema-utils` must neither + // bundle webpack nor warn about a request it cannot resolve, so webpack is loaded with Node.js's + // own `require`: the one webpack keeps as `__non_webpack_require__` inside a webpack bundle, and + // `module.require` through an alias of `module` outside one. + /* global __non_webpack_require__ */ + // prettier-ignore + // @ts-expect-error a free variable only a webpack bundle defines + const nodeRequire = typeof __non_webpack_require__ === "function" ? __non_webpack_require__ : undefined; + const self = module; try { - minifier = await require(WEBPACK).javascript.syntax.printer.load(); + minifier = await (nodeRequire || self.require.bind(self))( + "webpack", + ).javascript.syntax.printer.load(); } catch (err) { throw new Error( "Install webpack 5.112.0 or later to minify a precompiled check", diff --git a/test/fixtures/bundled-precompile.js b/test/fixtures/bundled-precompile.js new file mode 100644 index 0000000..4df00e8 --- /dev/null +++ b/test/fixtures/bundled-precompile.js @@ -0,0 +1,11 @@ +const { precompile } = require("../../src"); + +precompile({ type: "string", minLength: 1 }, { minify: true }).then( + (code) => { + const check = { exports: {} }; + + // eslint-disable-next-line no-new-func + new Function("module", code)(check); + process.stdout.write(JSON.stringify([check.exports(""), check.exports("a")])); + }, +); diff --git a/test/precompile.test.js b/test/precompile.test.js index 8e6cb64..25a1509 100644 --- a/test/precompile.test.js +++ b/test/precompile.test.js @@ -598,3 +598,59 @@ describe("precompile", () => { }); }); }); + +describe("bundled", () => { + // `schema-utils` is bundled by tools that ship it inside their own code, which reaches + // `precompile` through the `precompile` export even though it only runs at build time + it("should bundle without errors or warnings and minify from the bundle", (done) => { + const webpack = require("webpack"); + + // Inside the package, so the bundle finds webpack the way any installed bundle does + const localPath = path.resolve(__dirname, "../local"); + + fs.mkdirSync(localPath, { recursive: true }); + + const outputPath = fs.mkdtempSync(path.join(localPath, "bundle-")); + + webpack( + { + mode: "none", + target: "node", + context: __dirname, + entry: "./fixtures/bundled-precompile.js", + output: { path: outputPath }, + }, + (err, stats) => { + if (err) { + remove(outputPath); + done(err); + + return; + } + + try { + const { errors, warnings } = stats.toJson({ + all: false, + errors: true, + warnings: true, + }); + + expect(errors.map(({ message }) => message)).toEqual([]); + expect(warnings.map(({ message }) => message)).toEqual([]); + expect( + JSON.parse( + execFileSync(process.execPath, [ + path.join(outputPath, "main.js"), + ]).toString(), + ), + ).toEqual([false, true]); + done(); + } catch (error) { + done(error); + } finally { + remove(outputPath); + } + }, + ); + }, 60000); +});