Skip to content

Commit e9aac90

Browse files
committed
feat: inline cjs index
1 parent 9972041 commit e9aac90

File tree

4 files changed

+524
-0
lines changed

4 files changed

+524
-0
lines changed

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
"@typescript-eslint/parser": "4.30.0",
4040
"chai": "^4.2.0",
4141
"chai-as-promised": "^7.1.1",
42+
"esbuild": "^0.19.9",
4243
"eslint": "7.32.0",
4344
"eslint-config-prettier": "8.3.0",
4445
"eslint-plugin-prettier": "3.4.1",

scripts/compilation/Inliner.js

+242
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,242 @@
1+
const fs = require("fs");
2+
const path = require("path");
3+
const { spawnProcess } = require("./../utils/spawn-process");
4+
const walk = require("./../utils/walk");
5+
const esbuild = require("esbuild");
6+
7+
const root = path.join(__dirname, "..", "..");
8+
9+
/**
10+
*
11+
* Inline a package as one dist file, preserves other files as re-export stubs,
12+
* preserves files with react-native variants as externals.
13+
*
14+
*/
15+
module.exports = class Inliner {
16+
constructor(pkg) {
17+
this.package = pkg;
18+
this.platform = "node";
19+
this.isPackage = fs.existsSync(path.join(root, "packages", pkg));
20+
this.isLib = fs.existsSync(path.join(root, "lib", pkg));
21+
this.isClient = !this.isPackage && !this.isLib;
22+
this.subfolder = this.isPackage ? "packages" : this.isLib ? "lib" : "clients";
23+
24+
this.packageDirectory = path.join(root, this.subfolder, pkg);
25+
26+
this.outfile = path.join(root, this.subfolder, pkg, "dist-cjs", "index.js");
27+
}
28+
29+
/**
30+
* step 0: delete the dist-cjs folder.
31+
*/
32+
async clean() {
33+
await spawnProcess("yarn", ["rimraf", "./dist-cjs", "tsconfig.cjs.tsbuildinfo"], { cwd: this.packageDirectory });
34+
console.log("Deleted ./dist-cjs in " + this.package);
35+
return this;
36+
}
37+
38+
/**
39+
* step 1: build the default tsc dist-cjs output with dispersed files.
40+
* we will need the files to be in place for stubbing.
41+
*/
42+
async tsc() {
43+
await spawnProcess("yarn", ["tsc", "-p", "tsconfig.cjs.json"], { cwd: this.packageDirectory });
44+
console.log("Finished recompiling ./dist-cjs in " + this.package);
45+
return this;
46+
}
47+
48+
/**
49+
* step 2: detect all variant files and their transitive local imports.
50+
* these files will not be inlined, in order to preserve the react-native dist-cjs file replacement behavior.
51+
*/
52+
async discoverVariants() {
53+
const pkgJson = require(path.join(root, this.subfolder, this.package, "package.json"));
54+
this.variantEntries = Object.entries(pkgJson["react-native"] ?? {});
55+
56+
for await (const file of walk(path.join(this.packageDirectory, "dist-cjs"))) {
57+
if (fs.existsSync(file.replace(/\.js$/, ".native.js"))) {
58+
console.log("detected undeclared auto-variant", file);
59+
const canonical = file.replace(/(.*?)dist-cjs\//, "./dist-cjs/").replace(/\.js$/, "");
60+
const variant = canonical.replace(/(.*?)(\.js)?$/, "$1.native$2");
61+
62+
this.variantEntries.push([canonical, variant]);
63+
}
64+
if (fs.existsSync(file.replace(/\.js$/, ".browser.js"))) {
65+
// not applicable to CJS?
66+
}
67+
}
68+
69+
this.transitiveVariants = [];
70+
71+
for (const [k, v] of this.variantEntries) {
72+
for (const variantFile of [k, v]) {
73+
if (!variantFile.includes("dist-cjs/")) {
74+
continue;
75+
}
76+
const keyFile = path.join(
77+
this.packageDirectory,
78+
"dist-cjs",
79+
variantFile.replace(/(.*?)dist-cjs\//, "") + (variantFile.endsWith(".js") ? "" : ".js")
80+
);
81+
const keyFileContents = fs.readFileSync(keyFile, "utf-8");
82+
const requireStatements = keyFileContents.matchAll(/require\("(.*?)"\)/g);
83+
for (const requireStatement of requireStatements) {
84+
if (requireStatement[1]?.startsWith(".")) {
85+
// is relative import.
86+
const key = path
87+
.normalize(path.join(path.dirname(keyFile), requireStatement[1]))
88+
.replace(/(.*?)dist-cjs\//, "./dist-cjs/");
89+
console.log("Transitive variant file:", key);
90+
this.variantEntries.push([key, key]);
91+
this.transitiveVariants.push(key.replace(/(.*?)dist-cjs\//, "").replace(/(\.js)?$/, ""));
92+
}
93+
}
94+
}
95+
}
96+
97+
this.variantExternals = [];
98+
this.variantMap = {};
99+
100+
for (const [k, v] of this.variantEntries) {
101+
const prefix = "dist-cjs/";
102+
const keyPrefixIndex = k.indexOf(prefix);
103+
if (keyPrefixIndex === -1) {
104+
continue;
105+
}
106+
const valuePrefixIndex = v.indexOf(prefix);
107+
const keyRelativePath = k.slice(keyPrefixIndex + prefix.length);
108+
const valueRelativePath = v.slice(valuePrefixIndex + prefix.length);
109+
this.variantExternals.push(
110+
...[keyRelativePath, valueRelativePath].map((file) => (file.endsWith(".js") ? file : file + ".js"))
111+
);
112+
this.variantMap[keyRelativePath] = valueRelativePath;
113+
}
114+
115+
return this;
116+
}
117+
118+
/**
119+
* step 3: rewrite all existing dist-cjs files except the index.js file.
120+
* These now become re-exports of the index to preserve deep-import behavior.
121+
*/
122+
async rewriteStubs() {
123+
for await (const file of walk(path.join(this.packageDirectory, "dist-cjs"))) {
124+
const relativePath = file.replace(path.join(this.packageDirectory, "dist-cjs"), "").slice(1);
125+
if (relativePath === "index.js") {
126+
console.log("Skipping index.js");
127+
continue;
128+
}
129+
130+
if (this.variantExternals.find((external) => relativePath.endsWith(external))) {
131+
console.log("Not rewriting.", relativePath, "is variant.");
132+
continue;
133+
}
134+
135+
console.log("Rewriting", relativePath, "as index re-export stub.");
136+
137+
const depth = relativePath.split("/").length - 1;
138+
const indexRelativePath =
139+
(depth === 0
140+
? "."
141+
: Array.from({ length: depth })
142+
.map(() => "..")
143+
.join("/")) + "/index.js";
144+
145+
fs.writeFileSync(file, `module.exports = require("${indexRelativePath}");`);
146+
}
147+
148+
this.variantExternalsForEsBuild = this.variantExternals.map(
149+
(variant) => "*/" + path.basename(variant).replace(/.js$/, "")
150+
);
151+
152+
return this;
153+
}
154+
155+
/**
156+
* step 4: bundle the package index into dist-cjs/index.js except for node_modules
157+
* and also excluding any local files that have variants for react-native.
158+
*/
159+
async bundle() {
160+
await esbuild.build({
161+
platform: this.platform,
162+
bundle: true,
163+
format: "cjs",
164+
mainFields: ["main"],
165+
entryPoints: [path.join(root, this.subfolder, this.package, "src", "index.ts")],
166+
outfile: this.outfile,
167+
external: [
168+
"tslib",
169+
"@aws-crypto/*",
170+
"@smithy/*",
171+
"@aws-sdk/*",
172+
"typescript",
173+
"vscode-oniguruma",
174+
"pnpapi",
175+
"fast-xml-parser",
176+
"node_modules/*",
177+
...this.variantExternalsForEsBuild,
178+
],
179+
});
180+
return this;
181+
}
182+
183+
/**
184+
* step 5: rewrite variant external imports to correct path.
185+
* these externalized variants use relative imports for transitive variant files
186+
* which need to be rewritten when in the index.js file.
187+
*/
188+
async fixVariantImportPaths() {
189+
this.indexContents = fs.readFileSync(this.outfile, "utf-8");
190+
for (const variant of Object.keys(this.variantMap)) {
191+
const basename = path.basename(variant).replace(/.js$/, "");
192+
const dirname = path.dirname(variant);
193+
194+
const find = new RegExp(`require\\("./(.*?)/${basename}"\\)`);
195+
const replace = `require("./${dirname}/${basename}")`;
196+
197+
this.indexContents = this.indexContents.replace(find, replace);
198+
199+
console.log("replacing", find, "with", replace);
200+
}
201+
202+
fs.writeFileSync(this.outfile, this.indexContents, "utf-8");
203+
return this;
204+
}
205+
206+
/**
207+
* step 6: we validate that the index.js file has a require statement
208+
* for any variant files, to ensure they are not in the inlined (bundled) index.
209+
*/
210+
async validate() {
211+
this.indexContents = fs.readFileSync(this.outfile, "utf-8");
212+
213+
const externalsToCheck = new Set(
214+
Object.keys(this.variantMap)
215+
.filter((variant) => !this.transitiveVariants.includes(variant))
216+
.map((variant) => path.basename(variant).replace(/.js$/, ""))
217+
);
218+
219+
for (const line of this.indexContents.split("\n")) {
220+
// we expect to see a line with require() and the variant external in it
221+
if (line.includes("require(")) {
222+
const checkOrder = [...externalsToCheck].sort().reverse();
223+
for (const external of checkOrder) {
224+
if (line.includes(external)) {
225+
console.log("Inline index confirmed require() for variant external:", external);
226+
externalsToCheck.delete(external);
227+
continue;
228+
}
229+
}
230+
}
231+
}
232+
233+
if (externalsToCheck.size) {
234+
throw new Error(
235+
"require() statements for the following variant externals: " +
236+
[...externalsToCheck].join(", ") +
237+
" were not found in the index."
238+
);
239+
}
240+
return this;
241+
}
242+
};

scripts/inline.js

+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/**
2+
*
3+
* Inline a package as one dist file.
4+
*
5+
*/
6+
7+
const fs = require("fs");
8+
const path = require("path");
9+
const Inliner = require("./compilation/Inliner");
10+
11+
const root = path.join(__dirname, "..");
12+
13+
const package = process.argv[2];
14+
15+
if (!package) {
16+
/**
17+
* If no package is selected, this script sets all build:cjs scripts to
18+
* use this inliner script instead of only tsc.
19+
*/
20+
const packages = fs.readdirSync(path.join(root, "packages"));
21+
for (const pkg of packages) {
22+
const pkgJsonFilePath = path.join(root, "packages", pkg, "package.json");
23+
const pkgJson = require(pkgJsonFilePath);
24+
25+
pkgJson.scripts["build:cjs"] = `node ../../scripts/inline ${pkg}`;
26+
fs.writeFileSync(pkgJsonFilePath, JSON.stringify(pkgJson, null, 2));
27+
}
28+
} else {
29+
(async () => {
30+
const inliner = new Inliner(package);
31+
await inliner.clean();
32+
await inliner.tsc();
33+
await inliner.discoverVariants();
34+
await inliner.rewriteStubs();
35+
await inliner.bundle();
36+
await inliner.fixVariantImportPaths();
37+
await inliner.validate();
38+
})();
39+
}

0 commit comments

Comments
 (0)