Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/precompile-bundle.md
Original file line number Diff line number Diff line change
@@ -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.
25 changes: 20 additions & 5 deletions src/precompile.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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;
Comment thread
coderabbitai[bot] marked this conversation as resolved.

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",
Expand Down
11 changes: 11 additions & 0 deletions test/fixtures/bundled-precompile.js
Original file line number Diff line number Diff line change
@@ -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")]));
},
);
56 changes: 56 additions & 0 deletions test/precompile.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
Loading