@prosopo/config 3.3.6 → 3.3.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build$colon$cjs.log +8 -7
- package/.turbo/turbo-build$colon$tsc.log +4 -4
- package/.turbo/turbo-build.log +9 -8
- package/CHANGELOG.md +20 -0
- package/dist/cjs/index.cjs +2 -0
- package/dist/cjs/vite/index.cjs +2 -0
- package/dist/cjs/vite/vite-plugin-modify-exports-files.cjs +43 -0
- package/dist/cjs/vite/vite.frontend.config.cjs +6 -1
- package/dist/cjs/vite/vite.test.config.cjs +3 -1
- package/dist/dependencies.test.d.ts +2 -0
- package/dist/dependencies.test.d.ts.map +1 -0
- package/dist/dependencies.test.js +160 -0
- package/dist/dependencies.test.js.map +1 -0
- package/dist/getDependencies.test.d.ts +2 -0
- package/dist/getDependencies.test.d.ts.map +1 -0
- package/dist/getDependencies.test.js +258 -0
- package/dist/getDependencies.test.js.map +1 -0
- package/dist/index.js +2 -1
- package/dist/vite/bundleConfigs.test.d.ts +2 -0
- package/dist/vite/bundleConfigs.test.d.ts.map +1 -0
- package/dist/vite/bundleConfigs.test.js +252 -0
- package/dist/vite/bundleConfigs.test.js.map +1 -0
- package/dist/vite/configs.test.d.ts +2 -0
- package/dist/vite/configs.test.d.ts.map +1 -0
- package/dist/vite/configs.test.js +163 -0
- package/dist/vite/configs.test.js.map +1 -0
- package/dist/vite/index.d.ts +1 -0
- package/dist/vite/index.d.ts.map +1 -1
- package/dist/vite/index.js +2 -1
- package/dist/vite/index.js.map +1 -1
- package/dist/vite/modifyExportsFiles.test.d.ts +2 -0
- package/dist/vite/modifyExportsFiles.test.d.ts.map +1 -0
- package/dist/vite/modifyExportsFiles.test.js +28 -0
- package/dist/vite/modifyExportsFiles.test.js.map +1 -0
- package/dist/vite/plugins.test.d.ts +2 -0
- package/dist/vite/plugins.test.d.ts.map +1 -0
- package/dist/vite/plugins.test.js +232 -0
- package/dist/vite/plugins.test.js.map +1 -0
- package/dist/vite/translations.test.d.ts +2 -0
- package/dist/vite/translations.test.d.ts.map +1 -0
- package/dist/vite/translations.test.js +96 -0
- package/dist/vite/translations.test.js.map +1 -0
- package/dist/vite/vite-plugin-modify-exports-files.d.ts +4 -0
- package/dist/vite/vite-plugin-modify-exports-files.d.ts.map +1 -0
- package/dist/vite/vite-plugin-modify-exports-files.js +42 -0
- package/dist/vite/vite-plugin-modify-exports-files.js.map +1 -0
- package/dist/vite/vite.frontend.config.d.ts.map +1 -1
- package/dist/vite/vite.frontend.config.js +6 -1
- package/dist/vite/vite.frontend.config.js.map +1 -1
- package/dist/vite/vite.test.config.d.ts.map +1 -1
- package/dist/vite/vite.test.config.js +3 -1
- package/dist/vite/vite.test.config.js.map +1 -1
- package/package.json +3 -2
- package/src/dependencies.test.ts +249 -0
- package/src/getDependencies.test.ts +385 -0
- package/src/vite/bundleConfigs.test.ts +405 -0
- package/src/vite/configs.test.ts +217 -0
- package/src/vite/index.ts +1 -0
- package/src/vite/modifyExportsFiles.test.ts +56 -0
- package/src/vite/plugins.test.ts +348 -0
- package/src/vite/translations.test.ts +164 -0
- package/src/vite/vite-plugin-modify-exports-files.ts +64 -0
- package/src/vite/vite.frontend.config.ts +13 -0
- package/src/vite/vite.test.config.ts +5 -0
- package/tsconfig.tsbuildinfo +1 -1
- package/vite.test.config.ts +37 -0
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
// Copyright 2021-2026 Prosopo (UK) Ltd.
|
|
2
|
+
//
|
|
3
|
+
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
// you may not use this file except in compliance with the License.
|
|
5
|
+
// You may obtain a copy of the License at
|
|
6
|
+
//
|
|
7
|
+
// http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
//
|
|
9
|
+
// Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
// See the License for the specific language governing permissions and
|
|
13
|
+
// limitations under the License.
|
|
14
|
+
|
|
15
|
+
import fs from "node:fs";
|
|
16
|
+
import { builtinModules } from "node:module";
|
|
17
|
+
import os from "node:os";
|
|
18
|
+
import path from "node:path";
|
|
19
|
+
import type { Rollup, UserConfig } from "vite";
|
|
20
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
21
|
+
import { nodejsPolarsNativeFilePlugin } from "./NodejsPolarsNativeFilePlugin.js";
|
|
22
|
+
import VitePluginCloseAndCopy from "./vite-plugin-close-and-copy.js";
|
|
23
|
+
import ViteCommonJSConfig from "./vite.commonjs.config.js";
|
|
24
|
+
import ViteEsmConfig from "./vite.esm.config.js";
|
|
25
|
+
|
|
26
|
+
let root: string;
|
|
27
|
+
let cwd: string;
|
|
28
|
+
|
|
29
|
+
beforeEach(() => {
|
|
30
|
+
root = fs.mkdtempSync(path.join(os.tmpdir(), "prosopo-bundle-config-test-"));
|
|
31
|
+
cwd = process.cwd();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
afterEach(() => {
|
|
35
|
+
process.chdir(cwd);
|
|
36
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
/** A tsconfig with no references, so no npm lookups are needed. */
|
|
40
|
+
const leafTsConfig = (): string => {
|
|
41
|
+
const dir = path.join(root, "pkg");
|
|
42
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
43
|
+
fs.writeFileSync(
|
|
44
|
+
path.join(dir, "package.json"),
|
|
45
|
+
JSON.stringify({ name: "@prosopo/pkg" }),
|
|
46
|
+
);
|
|
47
|
+
const tsConfigPath = path.join(dir, "tsconfig.json");
|
|
48
|
+
fs.writeFileSync(tsConfigPath, JSON.stringify({ references: [] }));
|
|
49
|
+
return tsConfigPath;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
// Hand-rolled rather than `.flat(Infinity)`: a non-literal depth makes tsc
|
|
53
|
+
// expand FlatArray until it gives up with TS2589.
|
|
54
|
+
const flattenDeep = (value: unknown): unknown[] =>
|
|
55
|
+
Array.isArray(value) ? value.flatMap(flattenDeep) : [value];
|
|
56
|
+
|
|
57
|
+
const pluginNames = (config: UserConfig): string[] =>
|
|
58
|
+
flattenDeep(config.plugins ?? []).map((plugin) => {
|
|
59
|
+
if (plugin && typeof plugin === "object" && "name" in plugin) {
|
|
60
|
+
return String(plugin.name);
|
|
61
|
+
}
|
|
62
|
+
return "";
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Drive the `generateBundle` hook of the inline import.meta.url plugin over a
|
|
67
|
+
* fake bundle. Only that one plugin defines the hook.
|
|
68
|
+
*/
|
|
69
|
+
type FakeBundle = Record<string, { type: string; code?: string }>;
|
|
70
|
+
|
|
71
|
+
const generateBundle = async (
|
|
72
|
+
config: UserConfig,
|
|
73
|
+
bundle: FakeBundle,
|
|
74
|
+
): Promise<void> => {
|
|
75
|
+
for (const plugin of flattenDeep(config.plugins ?? [])) {
|
|
76
|
+
if (
|
|
77
|
+
!plugin ||
|
|
78
|
+
typeof plugin !== "object" ||
|
|
79
|
+
!("generateBundle" in plugin)
|
|
80
|
+
) {
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
const hook = plugin.generateBundle;
|
|
84
|
+
if (typeof hook === "function") {
|
|
85
|
+
await callHook(
|
|
86
|
+
hook as (options: object, bundle: FakeBundle) => unknown,
|
|
87
|
+
{},
|
|
88
|
+
bundle,
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const externals = (config: UserConfig): string[] => {
|
|
95
|
+
const external = config.build?.rollupOptions?.external;
|
|
96
|
+
if (!Array.isArray(external)) {
|
|
97
|
+
throw new Error("expected an array of externals");
|
|
98
|
+
}
|
|
99
|
+
return external.map(String);
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
describe("ViteEsmConfig", () => {
|
|
103
|
+
it("emits a single esm library targeting the runtime node version", async () => {
|
|
104
|
+
const config = await ViteEsmConfig("my-lib", leafTsConfig());
|
|
105
|
+
expect(config.build?.lib).toMatchObject({
|
|
106
|
+
name: "my-lib",
|
|
107
|
+
formats: ["es"],
|
|
108
|
+
entry: "src/index.ts",
|
|
109
|
+
});
|
|
110
|
+
expect(config.build?.target).toBe("node24");
|
|
111
|
+
expect(config.build?.outDir).toBe("dist");
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("accepts an explicit entry, including a multi-entry map", async () => {
|
|
115
|
+
const tsConfigPath = leafTsConfig();
|
|
116
|
+
await expect(
|
|
117
|
+
ViteEsmConfig("my-lib", tsConfigPath, "src/other.ts"),
|
|
118
|
+
).resolves.toMatchObject({ build: { lib: { entry: "src/other.ts" } } });
|
|
119
|
+
await expect(
|
|
120
|
+
ViteEsmConfig("my-lib", tsConfigPath, { a: "src/a.ts", b: "src/b.ts" }),
|
|
121
|
+
).resolves.toMatchObject({
|
|
122
|
+
build: { lib: { entry: { a: "src/a.ts", b: "src/b.ts" } } },
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("externalises every node builtin under both bare and node: specifiers", async () => {
|
|
127
|
+
const list = externals(await ViteEsmConfig("my-lib", leafTsConfig()));
|
|
128
|
+
for (const builtin of builtinModules) {
|
|
129
|
+
expect(list).toContain(builtin);
|
|
130
|
+
expect(list).toContain(`node:${builtin}`);
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("externalises the webpack toolchain, which vite must not bundle", async () => {
|
|
135
|
+
const list = externals(await ViteEsmConfig("my-lib", leafTsConfig()));
|
|
136
|
+
expect(list).toContain("webpack");
|
|
137
|
+
expect(list).toContain("html-webpack-plugin");
|
|
138
|
+
expect(list).toContain("babel-loader");
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("uses the same external list for ssr and for rollup", async () => {
|
|
142
|
+
const config = await ViteEsmConfig("my-lib", leafTsConfig());
|
|
143
|
+
expect(config.ssr?.external).toEqual(externals(config));
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("pulls package names out of the referenced projects", async () => {
|
|
147
|
+
const dep = path.join(root, "dep");
|
|
148
|
+
fs.mkdirSync(dep, { recursive: true });
|
|
149
|
+
fs.writeFileSync(
|
|
150
|
+
path.join(dep, "package.json"),
|
|
151
|
+
JSON.stringify({ name: "@prosopo/dep" }),
|
|
152
|
+
);
|
|
153
|
+
fs.writeFileSync(
|
|
154
|
+
path.join(dep, "tsconfig.json"),
|
|
155
|
+
JSON.stringify({ references: [] }),
|
|
156
|
+
);
|
|
157
|
+
const tsConfigPath = leafTsConfig();
|
|
158
|
+
fs.writeFileSync(
|
|
159
|
+
tsConfigPath,
|
|
160
|
+
JSON.stringify({ references: [{ path: "../dep" }] }),
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
expect(externals(await ViteEsmConfig("my-lib", tsConfigPath))).toContain(
|
|
164
|
+
"@prosopo/dep",
|
|
165
|
+
);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it("disables treeshaking and keeps the output directory intact", async () => {
|
|
169
|
+
// Packages emit esm and cjs into the same tree from two separate runs,
|
|
170
|
+
// so emptying the out dir would delete the sibling build.
|
|
171
|
+
const config = await ViteEsmConfig("my-lib", leafTsConfig());
|
|
172
|
+
expect(config.build?.rollupOptions?.treeshake).toBe(false);
|
|
173
|
+
expect(config.build?.emptyOutDir).toBe(false);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it("suppresses the CJS require polyfill so browsers can load the output", async () => {
|
|
177
|
+
const config = await ViteEsmConfig("my-lib", leafTsConfig());
|
|
178
|
+
const output = config.build?.rollupOptions?.output;
|
|
179
|
+
expect(output).toMatchObject({ polyfillRequire: false });
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it("registers the copy and tsconfig-paths plugins", async () => {
|
|
183
|
+
const names = pluginNames(await ViteEsmConfig("my-lib", leafTsConfig()));
|
|
184
|
+
expect(names).toContain("close-plugin");
|
|
185
|
+
expect(names).toContain("vite-tsconfig-paths");
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("uses the automatic jsx runtime", async () => {
|
|
189
|
+
const config = await ViteEsmConfig("my-lib", leafTsConfig());
|
|
190
|
+
expect(config.esbuild).toMatchObject({ jsx: "automatic" });
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it("rejects a tsconfig path that does not exist", async () => {
|
|
194
|
+
await expect(
|
|
195
|
+
ViteEsmConfig("my-lib", path.join(root, "missing.json")),
|
|
196
|
+
).rejects.toBeDefined();
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
describe("ViteCommonJSConfig", () => {
|
|
201
|
+
it("emits cjs into its own subdirectory so it cannot clash with the esm build", async () => {
|
|
202
|
+
const config = await ViteCommonJSConfig("my-lib", leafTsConfig());
|
|
203
|
+
expect(config.build?.lib).toMatchObject({
|
|
204
|
+
name: "my-lib",
|
|
205
|
+
formats: ["cjs"],
|
|
206
|
+
});
|
|
207
|
+
expect(config.build?.outDir).toBe("dist/cjs");
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it("does not externalise the webpack toolchain, unlike the esm config", async () => {
|
|
211
|
+
const tsConfigPath = leafTsConfig();
|
|
212
|
+
expect(
|
|
213
|
+
externals(await ViteCommonJSConfig("my-lib", tsConfigPath)),
|
|
214
|
+
).not.toContain("html-webpack-plugin");
|
|
215
|
+
expect(externals(await ViteEsmConfig("my-lib", tsConfigPath))).toContain(
|
|
216
|
+
"html-webpack-plugin",
|
|
217
|
+
);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it("rewrites import.meta.url in every emitted chunk, since cjs has none", async () => {
|
|
221
|
+
const bundle: Record<string, { type: string; code?: string }> = {
|
|
222
|
+
"a.js": { type: "chunk", code: "const a = import.meta.url;" },
|
|
223
|
+
"b.js": { type: "chunk", code: "const b = import.meta.url + 1;" },
|
|
224
|
+
};
|
|
225
|
+
await generateBundle(
|
|
226
|
+
await ViteCommonJSConfig("my-lib", leafTsConfig()),
|
|
227
|
+
bundle,
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
expect(bundle["a.js"]?.code).toBe(
|
|
231
|
+
"const a = require('url').pathToFileURL(__filename).href;",
|
|
232
|
+
);
|
|
233
|
+
expect(bundle["b.js"]?.code).toBe(
|
|
234
|
+
"const b = require('url').pathToFileURL(__filename).href + 1;",
|
|
235
|
+
);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it("leaves assets and empty chunks alone", async () => {
|
|
239
|
+
// An asset has no `code` property at all, so touching it would throw.
|
|
240
|
+
const bundle: Record<string, { type: string; code?: string }> = {
|
|
241
|
+
"style.css": { type: "asset" },
|
|
242
|
+
"empty.js": { type: "chunk", code: "" },
|
|
243
|
+
};
|
|
244
|
+
await expect(
|
|
245
|
+
generateBundle(
|
|
246
|
+
await ViteCommonJSConfig("my-lib", leafTsConfig()),
|
|
247
|
+
bundle,
|
|
248
|
+
),
|
|
249
|
+
).resolves.toBeUndefined();
|
|
250
|
+
expect(bundle["empty.js"]?.code).toBe("");
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it("honours an explicit entry", async () => {
|
|
254
|
+
await expect(
|
|
255
|
+
ViteCommonJSConfig("my-lib", leafTsConfig(), "src/cli.ts"),
|
|
256
|
+
).resolves.toMatchObject({ build: { lib: { entry: "src/cli.ts" } } });
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
describe("nodejsPolarsNativeFilePlugin", () => {
|
|
261
|
+
it("claims a native file by basename, wherever it is imported from", () => {
|
|
262
|
+
const plugin = nodejsPolarsNativeFilePlugin(["/abs/polars.node"], "dist");
|
|
263
|
+
expect(plugin.resolveId("./polars.node", "/src/a.ts", {})).toBe(
|
|
264
|
+
"./polars.node",
|
|
265
|
+
);
|
|
266
|
+
expect(plugin.resolveId("./other.node", "/src/a.ts", {})).toBeNull();
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it("replaces the module with a createRequire shim", () => {
|
|
270
|
+
const plugin = nodejsPolarsNativeFilePlugin(["polars.node"], "dist");
|
|
271
|
+
const transformed = plugin.transform("ignored", "/abs/polars.node");
|
|
272
|
+
expect(transformed).toContain("createRequire");
|
|
273
|
+
expect(transformed).toContain("'./polars.node'");
|
|
274
|
+
expect(transformed).toContain("export default content");
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it("leaves every other module untouched", () => {
|
|
278
|
+
const plugin = nodejsPolarsNativeFilePlugin(["polars.node"], "dist");
|
|
279
|
+
expect(plugin.transform("const a = 1;", "/src/index.ts")).toBe(
|
|
280
|
+
"const a = 1;",
|
|
281
|
+
);
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it("loads a native file as empty so the bundler never parses it as js", () => {
|
|
285
|
+
const plugin = nodejsPolarsNativeFilePlugin(["polars.node"], "dist");
|
|
286
|
+
expect(plugin.load("/abs/polars.node")).toBe("");
|
|
287
|
+
expect(plugin.load("/src/index.ts")).toBeNull();
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it("copies the native files into the output directory verbatim", () => {
|
|
291
|
+
const src = path.join(root, "polars.node");
|
|
292
|
+
const outDir = path.join(root, "nested", "dist");
|
|
293
|
+
const bytes = Buffer.from([0, 1, 2, 255]);
|
|
294
|
+
fs.writeFileSync(src, bytes);
|
|
295
|
+
|
|
296
|
+
nodejsPolarsNativeFilePlugin([src], outDir).generateBundle({}, {});
|
|
297
|
+
|
|
298
|
+
expect(fs.readFileSync(path.join(outDir, "polars.node"))).toEqual(bytes);
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
it("throws when a declared native file is missing", () => {
|
|
302
|
+
// A silent skip would produce a bundle that dies at runtime with an
|
|
303
|
+
// unhelpful "cannot find module" instead of failing the build.
|
|
304
|
+
const plugin = nodejsPolarsNativeFilePlugin(
|
|
305
|
+
[path.join(root, "absent.node")],
|
|
306
|
+
path.join(root, "dist"),
|
|
307
|
+
);
|
|
308
|
+
expect(() => plugin.generateBundle({}, {})).toThrow();
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
it("does nothing at all when there are no native files", () => {
|
|
312
|
+
const plugin = nodejsPolarsNativeFilePlugin([], "dist");
|
|
313
|
+
expect(plugin.resolveId("anything", undefined, {})).toBeNull();
|
|
314
|
+
expect(plugin.load("anything")).toBeNull();
|
|
315
|
+
expect(() => plugin.generateBundle({}, {})).not.toThrow();
|
|
316
|
+
});
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
describe("VitePluginCloseAndCopy", () => {
|
|
320
|
+
it("is a no-op at close time when no copy options are given", () => {
|
|
321
|
+
const plugin = VitePluginCloseAndCopy();
|
|
322
|
+
expect(plugin.name).toBe("close-plugin");
|
|
323
|
+
expect(() => callHook(plugin.closeBundle)).not.toThrow();
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
it("clears the js already in the destination, then copies the bundle over", () => {
|
|
327
|
+
process.chdir(root);
|
|
328
|
+
fs.mkdirSync(path.join(root, "src"));
|
|
329
|
+
fs.writeFileSync(path.join(root, "src", "new.js"), "new");
|
|
330
|
+
fs.mkdirSync(path.join(root, "dest"));
|
|
331
|
+
fs.writeFileSync(path.join(root, "dest", "stale.js"), "stale");
|
|
332
|
+
fs.writeFileSync(path.join(root, "dest", "keep.css"), "keep");
|
|
333
|
+
|
|
334
|
+
const plugin = VitePluginCloseAndCopy({
|
|
335
|
+
srcDir: "src",
|
|
336
|
+
destDir: ["dest"],
|
|
337
|
+
});
|
|
338
|
+
callHook(plugin.closeBundle);
|
|
339
|
+
|
|
340
|
+
expect(fs.readdirSync(path.join(root, "dest")).sort()).toEqual([
|
|
341
|
+
"keep.css",
|
|
342
|
+
"new.js",
|
|
343
|
+
]);
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
it("copies into every destination", () => {
|
|
347
|
+
process.chdir(root);
|
|
348
|
+
fs.mkdirSync(path.join(root, "src"));
|
|
349
|
+
fs.writeFileSync(path.join(root, "src", "a.js"), "a");
|
|
350
|
+
fs.mkdirSync(path.join(root, "one"));
|
|
351
|
+
fs.mkdirSync(path.join(root, "two"));
|
|
352
|
+
|
|
353
|
+
callHook(
|
|
354
|
+
VitePluginCloseAndCopy({ srcDir: "src", destDir: ["one", "two"] })
|
|
355
|
+
.closeBundle,
|
|
356
|
+
);
|
|
357
|
+
|
|
358
|
+
expect(fs.existsSync(path.join(root, "one", "a.js"))).toBe(true);
|
|
359
|
+
expect(fs.existsSync(path.join(root, "two", "a.js"))).toBe(true);
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
it("throws when a destination does not exist", () => {
|
|
363
|
+
process.chdir(root);
|
|
364
|
+
expect(() =>
|
|
365
|
+
callHook(
|
|
366
|
+
VitePluginCloseAndCopy({ srcDir: "src", destDir: ["absent"] })
|
|
367
|
+
.closeBundle,
|
|
368
|
+
),
|
|
369
|
+
).toThrow();
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
it("logs the build error it is handed rather than swallowing it", () => {
|
|
373
|
+
const plugin = VitePluginCloseAndCopy();
|
|
374
|
+
expect(() =>
|
|
375
|
+
callHook(plugin.buildEnd, new Error("bundle blew up")),
|
|
376
|
+
).not.toThrow();
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
it("announces the start of the build", () => {
|
|
380
|
+
expect(() =>
|
|
381
|
+
callHook(
|
|
382
|
+
VitePluginCloseAndCopy().buildStart,
|
|
383
|
+
{} as Rollup.NormalizedInputOptions,
|
|
384
|
+
),
|
|
385
|
+
).not.toThrow();
|
|
386
|
+
});
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Vite plugin hooks may be a bare function or an object with a `handler`.
|
|
391
|
+
* Neither of these hooks touches the rollup plugin context, so they are safe
|
|
392
|
+
* to call unbound.
|
|
393
|
+
*/
|
|
394
|
+
type Hook<A extends unknown[]> =
|
|
395
|
+
| ((...args: A) => unknown)
|
|
396
|
+
| { handler: (...args: A) => unknown }
|
|
397
|
+
| undefined;
|
|
398
|
+
|
|
399
|
+
const callHook = <A extends unknown[]>(hook: Hook<A>, ...args: A): unknown => {
|
|
400
|
+
if (!hook) {
|
|
401
|
+
throw new Error("plugin does not define the hook");
|
|
402
|
+
}
|
|
403
|
+
const handler = typeof hook === "function" ? hook : hook.handler;
|
|
404
|
+
return handler(...args);
|
|
405
|
+
};
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
// Copyright 2021-2026 Prosopo (UK) Ltd.
|
|
2
|
+
//
|
|
3
|
+
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
// you may not use this file except in compliance with the License.
|
|
5
|
+
// You may obtain a copy of the License at
|
|
6
|
+
//
|
|
7
|
+
// http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
//
|
|
9
|
+
// Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
// See the License for the specific language governing permissions and
|
|
13
|
+
// limitations under the License.
|
|
14
|
+
|
|
15
|
+
import fs from "node:fs";
|
|
16
|
+
import os from "node:os";
|
|
17
|
+
import path from "node:path";
|
|
18
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
19
|
+
import { nodejsPolarsDirnamePlugin } from "./NodejsPolarsDirnamePlugin.js";
|
|
20
|
+
import ViteTestConfig from "./vite.test.config.js";
|
|
21
|
+
import ViteThreadsTestConfig from "./vite.threads.test.config.js";
|
|
22
|
+
|
|
23
|
+
const NATIVE_POLARS = "/repo/node_modules/nodejs-polars/bin/native-polars.js";
|
|
24
|
+
|
|
25
|
+
let root: string;
|
|
26
|
+
let cwd: string;
|
|
27
|
+
let originalTestType: string | undefined;
|
|
28
|
+
|
|
29
|
+
beforeEach(() => {
|
|
30
|
+
root = fs.mkdtempSync(path.join(os.tmpdir(), "prosopo-vite-config-test-"));
|
|
31
|
+
cwd = process.cwd();
|
|
32
|
+
originalTestType = process.env.TEST_TYPE;
|
|
33
|
+
process.env.TEST_TYPE = undefined;
|
|
34
|
+
// biome-ignore lint/performance/noDelete: the config reads presence, not value
|
|
35
|
+
delete process.env.TEST_TYPE;
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
afterEach(() => {
|
|
39
|
+
process.chdir(cwd);
|
|
40
|
+
if (originalTestType === undefined) {
|
|
41
|
+
// biome-ignore lint/performance/noDelete: restore absence, not ""
|
|
42
|
+
delete process.env.TEST_TYPE;
|
|
43
|
+
} else {
|
|
44
|
+
process.env.TEST_TYPE = originalTestType;
|
|
45
|
+
}
|
|
46
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
/** Make `root` look like a package (package.json + src) and chdir into it. */
|
|
50
|
+
const asPackage = (): void => {
|
|
51
|
+
fs.writeFileSync(path.join(root, "package.json"), "{}");
|
|
52
|
+
fs.mkdirSync(path.join(root, "src"));
|
|
53
|
+
process.chdir(root);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/** Make `root` look like a repo root (no src dir) and chdir into it. */
|
|
57
|
+
const asRepoRoot = (): void => {
|
|
58
|
+
fs.writeFileSync(path.join(root, "package.json"), "{}");
|
|
59
|
+
process.chdir(root);
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
describe("ViteTestConfig", () => {
|
|
63
|
+
it("enables coverage and typechecking by default", () => {
|
|
64
|
+
asPackage();
|
|
65
|
+
const config = ViteTestConfig();
|
|
66
|
+
expect(config.test?.coverage?.enabled).toBe(true);
|
|
67
|
+
expect(config.test?.typecheck?.enabled).toBe(true);
|
|
68
|
+
expect(config.test?.watch).toBe(false);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("runs in forks, isolated, because low-level crypto needs it", () => {
|
|
72
|
+
asPackage();
|
|
73
|
+
const config = ViteTestConfig();
|
|
74
|
+
expect(config.test?.pool).toBe("forks");
|
|
75
|
+
expect(config.test?.isolate).toBe(true);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("includes tests with no declared type when no filter is set", () => {
|
|
79
|
+
asPackage();
|
|
80
|
+
expect(config_include(ViteTestConfig())).toBe(
|
|
81
|
+
"src/**/*@(|).@(test|spec).@(mts|cts|mjs|cjs|js|ts|tsx|jsx)",
|
|
82
|
+
);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("filters on a single declared test type", () => {
|
|
86
|
+
asPackage();
|
|
87
|
+
process.env.TEST_TYPE = "unit";
|
|
88
|
+
expect(config_include(ViteTestConfig())).toContain("@(.unit)");
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("filters on several test types", () => {
|
|
92
|
+
asPackage();
|
|
93
|
+
process.env.TEST_TYPE = "unit,integration";
|
|
94
|
+
expect(config_include(ViteTestConfig())).toContain("@(.unit|.integration)");
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("trims whitespace around the test type list", () => {
|
|
98
|
+
asPackage();
|
|
99
|
+
process.env.TEST_TYPE = " unit ";
|
|
100
|
+
expect(config_include(ViteTestConfig())).toContain("@(.unit)");
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("ignores an empty test type, keeping the default glob", () => {
|
|
104
|
+
asPackage();
|
|
105
|
+
process.env.TEST_TYPE = "";
|
|
106
|
+
expect(config_include(ViteTestConfig())).toContain("@(|)");
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("scopes coverage to the local src when run from a package", () => {
|
|
110
|
+
// Workspaces outside `packages/` were reporting 0/0 coverage because
|
|
111
|
+
// the repo-root globs only match `packages/*/src/**`.
|
|
112
|
+
asPackage();
|
|
113
|
+
const config = ViteTestConfig();
|
|
114
|
+
expect(config.test?.coverage?.include).toContain("src/**/*.ts");
|
|
115
|
+
expect(config.test?.coverage?.exclude).toContain("src/**/*.test.ts");
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("falls back to repo-wide globs when there is no src directory", () => {
|
|
119
|
+
asRepoRoot();
|
|
120
|
+
const config = ViteTestConfig();
|
|
121
|
+
expect(config.test?.coverage?.include).toEqual([
|
|
122
|
+
"packages/*/src/**",
|
|
123
|
+
"captcha/packages/*/src/**",
|
|
124
|
+
]);
|
|
125
|
+
expect(config.test?.coverage?.exclude).toContain("**/node_modules/**");
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("adds the tsconfig-paths plugin only when given a tsconfig", () => {
|
|
129
|
+
asPackage();
|
|
130
|
+
const withoutPaths = ViteTestConfig().plugins ?? [];
|
|
131
|
+
const tsConfigPath = path.join(root, "tsconfig.json");
|
|
132
|
+
fs.writeFileSync(tsConfigPath, "{}");
|
|
133
|
+
const withPaths = ViteTestConfig(tsConfigPath).plugins ?? [];
|
|
134
|
+
expect(withPaths.length).toBe(withoutPaths.length + 1);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("always excludes node_modules and dist from the test run", () => {
|
|
138
|
+
asPackage();
|
|
139
|
+
expect(ViteTestConfig().test?.exclude).toEqual([
|
|
140
|
+
"**/node_modules/**",
|
|
141
|
+
"**/dist/**",
|
|
142
|
+
]);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("disables sourcemaps and minification, which only cost time here", () => {
|
|
146
|
+
asPackage();
|
|
147
|
+
const config = ViteTestConfig();
|
|
148
|
+
expect(config.build?.minify).toBe(false);
|
|
149
|
+
expect(config.build?.sourcemap).toBe(false);
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
describe("ViteThreadsTestConfig", () => {
|
|
154
|
+
it("differs from the forks config only in its pool", () => {
|
|
155
|
+
asPackage();
|
|
156
|
+
expect(ViteThreadsTestConfig().test?.pool).toBe("threads");
|
|
157
|
+
expect(ViteTestConfig().test?.pool).toBe("forks");
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it("keeps coverage and typechecking on", () => {
|
|
161
|
+
asPackage();
|
|
162
|
+
const config = ViteThreadsTestConfig();
|
|
163
|
+
expect(config.test?.coverage?.enabled).toBe(true);
|
|
164
|
+
expect(config.test?.typecheck?.enabled).toBe(true);
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
describe("nodejsPolarsDirnamePlugin", () => {
|
|
169
|
+
const plugin = nodejsPolarsDirnamePlugin();
|
|
170
|
+
|
|
171
|
+
it("claims only the native polars entry point", () => {
|
|
172
|
+
expect(plugin.resolveId(NATIVE_POLARS, undefined, {})).toBe(NATIVE_POLARS);
|
|
173
|
+
expect(plugin.resolveId("zod", undefined, {})).toBeNull();
|
|
174
|
+
expect(
|
|
175
|
+
plugin.resolveId("nodejs-polars/bin/other.js", undefined, {}),
|
|
176
|
+
).toBeNull();
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it("rewrites __dirname to a path derived from import.meta.url", () => {
|
|
180
|
+
// The bundled output is a single ESM file, where `__dirname` does not
|
|
181
|
+
// exist; polars uses it to locate its native binary.
|
|
182
|
+
const code = "const a = __dirname; const b = __dirname;";
|
|
183
|
+
const transformed = plugin.transform(code, NATIVE_POLARS);
|
|
184
|
+
expect(transformed).not.toContain("__dirname");
|
|
185
|
+
expect(transformed).toContain("import.meta.url");
|
|
186
|
+
expect(transformed.match(/import\.meta\.url/g)).toHaveLength(2);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it("leaves every other module untouched", () => {
|
|
190
|
+
const code = "const a = __dirname;";
|
|
191
|
+
expect(plugin.transform(code, "/repo/src/index.ts")).toBe(code);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it("is named", () => {
|
|
195
|
+
expect(plugin.name).toBe("nodejs-polars-dirname-plugin");
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
/** The include glob is stored as a single-element array. */
|
|
200
|
+
const config_include = (config: ReturnType<typeof ViteTestConfig>): string => {
|
|
201
|
+
const include = config.test?.include;
|
|
202
|
+
if (!include || !include[0]) {
|
|
203
|
+
throw new Error("config has no test include glob");
|
|
204
|
+
}
|
|
205
|
+
return include[0];
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
describe("coverage excludes", () => {
|
|
209
|
+
it("drops type-test files, which are compiled but never executed", () => {
|
|
210
|
+
// `src/**/*.d.ts` does not match `*.test-d.ts`, so without this every
|
|
211
|
+
// package reported its type tests as 0%-covered source.
|
|
212
|
+
asPackage();
|
|
213
|
+
const exclude = ViteTestConfig().test?.coverage?.exclude ?? [];
|
|
214
|
+
expect(exclude).toContain("src/**/*.test-d.ts");
|
|
215
|
+
expect(exclude).toContain("src/**/*.test-d.tsx");
|
|
216
|
+
});
|
|
217
|
+
});
|
package/src/vite/index.ts
CHANGED
|
@@ -24,3 +24,4 @@ export { default as ViteEsmConfig } from "./vite.esm.config.js";
|
|
|
24
24
|
export { default as VitePluginRemoveUnusedTranslations } from "./vite-plugin-remove-unused-translations.js";
|
|
25
25
|
export * from "./TsNoCheckPlugin.js";
|
|
26
26
|
export { default as VitePluginCopy } from "./vite-plugin-copy.js";
|
|
27
|
+
export { default as VitePluginModifyExportsFiles } from "./vite-plugin-modify-exports-files.js";
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// Copyright 2021-2026 Prosopo (UK) Ltd.
|
|
2
|
+
//
|
|
3
|
+
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
// you may not use this file except in compliance with the License.
|
|
5
|
+
// You may obtain a copy of the License at
|
|
6
|
+
//
|
|
7
|
+
// http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
//
|
|
9
|
+
// Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
// See the License for the specific language governing permissions and
|
|
13
|
+
// limitations under the License.
|
|
14
|
+
|
|
15
|
+
import { describe, expect, test } from "vitest";
|
|
16
|
+
import { emptyExportsFiles } from "./vite-plugin-modify-exports-files.js";
|
|
17
|
+
|
|
18
|
+
describe("emptyExportsFiles", () => {
|
|
19
|
+
test("empties an assignment and keeps the code around it", () => {
|
|
20
|
+
expect(
|
|
21
|
+
emptyExportsFiles('a();\nexports2.FILES = ["a.js", "b.js"];\nb();'),
|
|
22
|
+
).toBe("a();\nexports2.FILES = [];\n\nb();");
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test("empties every assignment, not just the first", () => {
|
|
26
|
+
const out = emptyExportsFiles(
|
|
27
|
+
'exports2.FILES = ["a"];exports2.FILES = ["b"];',
|
|
28
|
+
);
|
|
29
|
+
expect(out).toBe("exports2.FILES = [];\nexports2.FILES = [];\n");
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("spans newlines, as a formatted listing does", () => {
|
|
33
|
+
expect(emptyExportsFiles('exports2.FILES = [\n\t"a.js"\n];')).toBe(
|
|
34
|
+
"exports2.FILES = [];\n",
|
|
35
|
+
);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("leaves code with no assignment untouched", () => {
|
|
39
|
+
expect(emptyExportsFiles("const FILES = [];")).toBe("const FILES = [];");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("leaves an unterminated assignment alone rather than truncating", () => {
|
|
43
|
+
// Cutting to the end of the chunk here would delete real code.
|
|
44
|
+
const code = 'exports2.FILES = ["a.js"';
|
|
45
|
+
expect(emptyExportsFiles(code)).toBe(code);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("stays linear on many unterminated assignments", () => {
|
|
49
|
+
// The regex this replaced backtracked quadratically on exactly this
|
|
50
|
+
// shape, which is what CodeQL flagged.
|
|
51
|
+
const code = "exports2.FILES = [".repeat(20000);
|
|
52
|
+
const started = performance.now();
|
|
53
|
+
expect(emptyExportsFiles(code)).toBe(code);
|
|
54
|
+
expect(performance.now() - started).toBeLessThan(1000);
|
|
55
|
+
});
|
|
56
|
+
});
|