Skip to content

Commit b163f32

Browse files
committed
test: add CJS/ESM dual-package compatibility tests
Mirrors the cjs.test.js suite from webpack/sass-loader: builds a CommonJS bundle from src/ at test time via @babel/core and asserts the shape promised by the build:cjs pipeline (commonjs package marker, strict-mode prologue, exports.default unwrap) so require() returns the loader function with .default pointing back at itself, matching the ESM default export.
1 parent d6a6fea commit b163f32

1 file changed

Lines changed: 117 additions & 0 deletions

File tree

test/cjs.test.js

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import assert from "node:assert";
2+
import {
3+
copyFileSync,
4+
mkdtempSync,
5+
readFileSync,
6+
readdirSync,
7+
rmSync,
8+
writeFileSync,
9+
} from "node:fs";
10+
import { createRequire } from "node:module";
11+
import { tmpdir } from "node:os";
12+
import path from "node:path";
13+
import { after, before, describe, it } from "node:test";
14+
import { fileURLToPath } from "node:url";
15+
16+
import { transformFileAsync } from "@babel/core";
17+
18+
// eslint-disable-next-line n/no-unpublished-import
19+
import src from "../src/index.js";
20+
21+
const require = createRequire(import.meta.url);
22+
23+
const srcDir = fileURLToPath(new URL("../src", import.meta.url));
24+
25+
/**
26+
* Transpile every `.js` file under `src/` to CommonJS in `outDir`, copy
27+
* non-JS assets, then drop the `dist/cjs/package.json` type marker and
28+
* the same `module.exports = exports.default` post-build append the
29+
* `build:cjs` npm script writes. The test then `require()`s the result.
30+
* @param {string} outDir target directory
31+
*/
32+
async function buildCjsBundle(outDir) {
33+
for (const entry of readdirSync(srcDir, { withFileTypes: true })) {
34+
const source = path.join(srcDir, entry.name);
35+
const target = path.join(outDir, entry.name);
36+
37+
if (entry.name.endsWith(".js")) {
38+
const result = await transformFileAsync(source, { envName: "cjs" });
39+
40+
writeFileSync(target, result.code);
41+
} else {
42+
copyFileSync(source, target);
43+
}
44+
}
45+
46+
writeFileSync(
47+
path.join(outDir, "package.json"),
48+
`${JSON.stringify({ type: "commonjs" })}\n`,
49+
);
50+
51+
const indexPath = path.join(outDir, "index.js");
52+
53+
writeFileSync(
54+
indexPath,
55+
`${readFileSync(indexPath, "utf8")}module.exports = exports.default;\nmodule.exports.default = exports.default;\n`,
56+
);
57+
}
58+
59+
describe("cjs", () => {
60+
let cjsDir;
61+
let cjsIndexPath;
62+
let cjsIndexSource;
63+
let cjsPackage;
64+
let cjsLoader;
65+
66+
before(async () => {
67+
cjsDir = mkdtempSync(path.join(tmpdir(), "less-loader-cjs-"));
68+
69+
await buildCjsBundle(cjsDir);
70+
71+
cjsIndexPath = path.join(cjsDir, "index.js");
72+
cjsIndexSource = readFileSync(cjsIndexPath, "utf8");
73+
cjsPackage = JSON.parse(
74+
readFileSync(path.join(cjsDir, "package.json"), "utf8"),
75+
);
76+
cjsLoader = require(cjsIndexPath);
77+
});
78+
79+
after(() => {
80+
if (cjsDir) rmSync(cjsDir, { recursive: true, force: true });
81+
});
82+
83+
it("should expose the loader as the default export of the ESM entry", () => {
84+
assert.strictEqual(typeof src, "function");
85+
});
86+
87+
// Run Babel against `src/` directly (no dependency on the published
88+
// `dist/`) and assert the resulting CommonJS bundle has the shape the
89+
// `build:cjs` pipeline promises: it's marked CommonJS, opens with
90+
// Babel's strict-mode prologue and `exports.default = lessLoader`, and
91+
// ends with the `module.exports = exports.default;` unwrap so
92+
// `require()` returns the loader function directly.
93+
it("should produce a require()-able CommonJS bundle via @babel/core", () => {
94+
assert.strictEqual(cjsPackage.type, "commonjs");
95+
96+
assert.match(cjsIndexSource, /^"use strict";/);
97+
assert.match(cjsIndexSource, /exports\.default = lessLoader/);
98+
assert.match(cjsIndexSource, /module\.exports = exports\.default;/);
99+
100+
assert.strictEqual(typeof cjsLoader, "function");
101+
assert.strictEqual(cjsLoader.default, cjsLoader);
102+
});
103+
104+
// Pre-refactor consumers loaded the loader two ways:
105+
// const loader = require("less-loader"); // function
106+
// import loader from "less-loader"; // function
107+
// Both surfaces must continue to return a callable function, with a
108+
// `.default` property pointing back at the same function so transitional
109+
// `require("less-loader").default` calls keep working.
110+
it("should expose the loader through `require` as a callable function (pre-refactor shape)", () => {
111+
assert.strictEqual(typeof cjsLoader, "function");
112+
assert.strictEqual(typeof src, "function");
113+
assert.strictEqual(cjsLoader.default, cjsLoader);
114+
assert.strictEqual(cjsLoader.name, src.name);
115+
assert.strictEqual(cjsLoader.length, src.length);
116+
});
117+
});

0 commit comments

Comments
 (0)