|
| 1 | +import { |
| 2 | + appendFileSync, |
| 3 | + copyFileSync, |
| 4 | + mkdirSync, |
| 5 | + mkdtempSync, |
| 6 | + readFileSync, |
| 7 | + readdirSync, |
| 8 | + rmSync, |
| 9 | + writeFileSync, |
| 10 | +} from "node:fs"; |
| 11 | +import { createRequire } from "node:module"; |
| 12 | +import path from "node:path"; |
| 13 | +import { after, before, describe, it } from "node:test"; |
| 14 | +import { fileURLToPath } from "node:url"; |
| 15 | +import { transformFileAsync } from "@babel/core"; |
| 16 | +import { expect } from "expect"; |
| 17 | +import Server from "../lib/Server.js"; |
| 18 | + |
| 19 | +const __dirname = path.dirname(fileURLToPath(import.meta.url)); |
| 20 | +const rootDir = path.join(__dirname, ".."); |
| 21 | +const libDir = path.join(rootDir, "lib"); |
| 22 | + |
| 23 | +const require = createRequire(import.meta.url); |
| 24 | + |
| 25 | +/** |
| 26 | + * Collect every file under `dir` recursively. |
| 27 | + * @param {string} dir directory to walk |
| 28 | + * @returns {string[]} absolute file paths |
| 29 | + */ |
| 30 | +function walk(dir) { |
| 31 | + const files = []; |
| 32 | + |
| 33 | + for (const entry of readdirSync(dir, { withFileTypes: true })) { |
| 34 | + const full = path.join(dir, entry.name); |
| 35 | + |
| 36 | + if (entry.isDirectory()) { |
| 37 | + files.push(...walk(full)); |
| 38 | + } else { |
| 39 | + files.push(full); |
| 40 | + } |
| 41 | + } |
| 42 | + |
| 43 | + return files; |
| 44 | +} |
| 45 | + |
| 46 | +/** |
| 47 | + * Reproduce the `build:cjs` pipeline against `lib/` into `outDir`: transpile |
| 48 | + * every `.js` file with Babel's `cjs` env, copy other assets, drop the |
| 49 | + * `package.json` CommonJS marker and append the `module.exports = exports.default` |
| 50 | + * unwrap that `scripts/finalize-cjs-build.mjs` writes. The result can then be |
| 51 | + * `require()`d directly. |
| 52 | + * @param {string} outDir target directory |
| 53 | + */ |
| 54 | +async function buildCjsBundle(outDir) { |
| 55 | + for (const source of walk(libDir)) { |
| 56 | + const target = path.join(outDir, path.relative(libDir, source)); |
| 57 | + |
| 58 | + mkdirSync(path.dirname(target), { recursive: true }); |
| 59 | + |
| 60 | + if (source.endsWith(".js")) { |
| 61 | + const result = await transformFileAsync(source, { envName: "cjs" }); |
| 62 | + |
| 63 | + writeFileSync(target, result.code); |
| 64 | + } else { |
| 65 | + copyFileSync(source, target); |
| 66 | + } |
| 67 | + } |
| 68 | + |
| 69 | + writeFileSync( |
| 70 | + path.join(outDir, "package.json"), |
| 71 | + `${JSON.stringify({ type: "commonjs" }, null, 2)}\n`, |
| 72 | + ); |
| 73 | + |
| 74 | + appendFileSync( |
| 75 | + path.join(outDir, "Server.js"), |
| 76 | + "module.exports = exports.default;\nmodule.exports.default = exports.default;\n", |
| 77 | + ); |
| 78 | +} |
| 79 | + |
| 80 | +describe("cjs", () => { |
| 81 | + let bundleDir; |
| 82 | + let serverPath; |
| 83 | + let serverSource; |
| 84 | + let bundlePackage; |
| 85 | + let cjsServer; |
| 86 | + |
| 87 | + before(async () => { |
| 88 | + // Build inside `node_modules` so the transpiled bare `require()`s |
| 89 | + // (`schema-utils`, `graceful-fs`, ...) resolve against the project deps. |
| 90 | + bundleDir = mkdtempSync(path.join(rootDir, "node_modules", ".wds-cjs-")); |
| 91 | + |
| 92 | + await buildCjsBundle(bundleDir); |
| 93 | + |
| 94 | + serverPath = path.join(bundleDir, "Server.js"); |
| 95 | + serverSource = readFileSync(serverPath, "utf8"); |
| 96 | + bundlePackage = JSON.parse( |
| 97 | + readFileSync(path.join(bundleDir, "package.json"), "utf8"), |
| 98 | + ); |
| 99 | + cjsServer = require(serverPath); |
| 100 | + }); |
| 101 | + |
| 102 | + after(() => { |
| 103 | + if (bundleDir) { |
| 104 | + rmSync(bundleDir, { recursive: true, force: true }); |
| 105 | + } |
| 106 | + }); |
| 107 | + |
| 108 | + it("should produce a require()-able CommonJS bundle", () => { |
| 109 | + expect(bundlePackage.type).toBe("commonjs"); |
| 110 | + |
| 111 | + // Babel's strict-mode prologue, the `exports.default` assignment and the |
| 112 | + // unwrap appended by the finalize step. |
| 113 | + expect(serverSource).toMatch(/^"use strict";/); |
| 114 | + expect(serverSource).toMatch(/exports\.default = /); |
| 115 | + expect(serverSource).toMatch(/module\.exports = exports\.default;/); |
| 116 | + |
| 117 | + // No executable dynamic `import()` or `import.meta` survives in the CJS |
| 118 | + // build. Strip comments first since JSDoc type annotations legitimately |
| 119 | + // reference `import("pkg")` and `[S=import("http").Server]`. |
| 120 | + const executableCode = serverSource |
| 121 | + .replaceAll(/\/\*[\s\S]*?\*\//g, "") |
| 122 | + .replaceAll(/^\s*\/\/.*$/gm, ""); |
| 123 | + |
| 124 | + expect(executableCode).not.toMatch(/import\(/); |
| 125 | + expect(executableCode).not.toMatch(/import\.meta/); |
| 126 | + }); |
| 127 | + |
| 128 | + it("should expose the Server class through `require` (pre-ESM shape)", () => { |
| 129 | + // `require("webpack-dev-server")` returns the class directly, with |
| 130 | + // `.default` pointing back at it for ESM interop. |
| 131 | + expect(typeof cjsServer).toBe("function"); |
| 132 | + expect(cjsServer.default).toBe(cjsServer); |
| 133 | + expect(cjsServer.name).toBe(Server.name); |
| 134 | + expect(cjsServer.length).toBe(Server.length); |
| 135 | + }); |
| 136 | + |
| 137 | + it("should match the ESM default export", () => { |
| 138 | + expect(typeof Server).toBe("function"); |
| 139 | + expect(cjsServer.name).toBe("Server"); |
| 140 | + // Same public statics/prototype surface as the ESM build. |
| 141 | + expect(Object.getOwnPropertyNames(cjsServer.prototype)).toEqual( |
| 142 | + Object.getOwnPropertyNames(Server.prototype), |
| 143 | + ); |
| 144 | + }); |
| 145 | +}); |
0 commit comments