@prosopo/config 3.3.5 → 3.3.7
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 +3 -3
- package/.turbo/turbo-build$colon$tsc.log +4 -4
- package/.turbo/turbo-build.log +4 -4
- package/CHANGELOG.md +13 -0
- 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/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/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.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 +4 -3
- 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/plugins.test.ts +348 -0
- package/src/vite/translations.test.ts +164 -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,348 @@
|
|
|
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, vi } from "vitest";
|
|
19
|
+
import { tsNoCheckPlugin } from "./TsNoCheckPlugin.js";
|
|
20
|
+
import VitePluginCopy from "./vite-plugin-copy.js";
|
|
21
|
+
import VitePluginExternalizeObfuscatorDeadCode from "./vite-plugin-externalize-obfuscator-deadcode.js";
|
|
22
|
+
import VitePluginFixAbsoluteImports from "./vite-plugin-fix-absolute-imports.js";
|
|
23
|
+
import {
|
|
24
|
+
flatten,
|
|
25
|
+
unflatten,
|
|
26
|
+
} from "./vite-plugin-remove-unused-translations.js";
|
|
27
|
+
import VitePluginSourcemapExclude from "./vite-plugin-sourcemap-exclude.js";
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Vite plugin hooks are declared as `hook | { handler }` objects, so they are
|
|
31
|
+
* pulled out through a narrowing helper rather than being called directly.
|
|
32
|
+
*/
|
|
33
|
+
const handlerOf = <T>(hook: T | { handler: T } | undefined): T => {
|
|
34
|
+
if (!hook) {
|
|
35
|
+
throw new Error("plugin does not define the hook");
|
|
36
|
+
}
|
|
37
|
+
return typeof hook === "function" ? hook : (hook as { handler: T }).handler;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
describe("VitePluginSourcemapExclude", () => {
|
|
41
|
+
const transformOf = (opts?: { excludeNodeModules?: boolean }) => {
|
|
42
|
+
const plugin = VitePluginSourcemapExclude(opts);
|
|
43
|
+
return handlerOf(plugin.transform) as (
|
|
44
|
+
code: string,
|
|
45
|
+
id: string,
|
|
46
|
+
) => { code: string; map: { mappings: string } } | undefined;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
it("is named so it can be found in a plugin list", () => {
|
|
50
|
+
expect(VitePluginSourcemapExclude().name).toBe("sourcemap-exclude");
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("blanks the sourcemap for node_modules when asked", () => {
|
|
54
|
+
// The whole point is to stop Vite holding megabytes of vendor
|
|
55
|
+
// sourcemaps in memory.
|
|
56
|
+
const result = transformOf({ excludeNodeModules: true })(
|
|
57
|
+
"code",
|
|
58
|
+
"/repo/node_modules/zod/index.js",
|
|
59
|
+
);
|
|
60
|
+
expect(result).toEqual({ code: "code", map: { mappings: "" } });
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("leaves first-party code alone", () => {
|
|
64
|
+
expect(
|
|
65
|
+
transformOf({ excludeNodeModules: true })("code", "/repo/src/index.ts"),
|
|
66
|
+
).toBeUndefined();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("does nothing without the option, or with no options at all", () => {
|
|
70
|
+
expect(
|
|
71
|
+
transformOf({})("code", "/repo/node_modules/zod/index.js"),
|
|
72
|
+
).toBeUndefined();
|
|
73
|
+
expect(
|
|
74
|
+
transformOf()("code", "/repo/node_modules/zod/index.js"),
|
|
75
|
+
).toBeUndefined();
|
|
76
|
+
expect(
|
|
77
|
+
transformOf({ excludeNodeModules: false })(
|
|
78
|
+
"code",
|
|
79
|
+
"/repo/node_modules/zod/index.js",
|
|
80
|
+
),
|
|
81
|
+
).toBeUndefined();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("matches node_modules anywhere in the id, including nested copies", () => {
|
|
85
|
+
expect(
|
|
86
|
+
transformOf({ excludeNodeModules: true })(
|
|
87
|
+
"code",
|
|
88
|
+
"/repo/node_modules/a/node_modules/b/index.js",
|
|
89
|
+
),
|
|
90
|
+
).toBeDefined();
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe("VitePluginFixAbsoluteImports", () => {
|
|
95
|
+
const transform = handlerOf(VitePluginFixAbsoluteImports().transform) as (
|
|
96
|
+
code: string,
|
|
97
|
+
id: string,
|
|
98
|
+
) => { code: string };
|
|
99
|
+
|
|
100
|
+
it("runs after other plugins have emitted their imports", () => {
|
|
101
|
+
const plugin = VitePluginFixAbsoluteImports();
|
|
102
|
+
expect(plugin.name).toBe("fix-absolute-imports");
|
|
103
|
+
expect(plugin.enforce).toBe("post");
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("rewrites a deep builtin import back to the bare specifier", () => {
|
|
107
|
+
// `from "node:fs/promises/index.js"` is not resolvable at runtime;
|
|
108
|
+
// the bare builtin is.
|
|
109
|
+
expect(transform('import x from "fs/promises/index.js";', "id").code).toBe(
|
|
110
|
+
'import x from "fs";',
|
|
111
|
+
);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("rewrites every occurrence in a file", () => {
|
|
115
|
+
const code = 'from "path/posix/x.js";\nfrom "path/win32/y.js";';
|
|
116
|
+
expect(transform(code, "id").code).toBe('from "path";\nfrom "path";');
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("leaves a bare builtin import untouched", () => {
|
|
120
|
+
const code = 'import fs from "fs";';
|
|
121
|
+
expect(transform(code, "id").code).toBe(code);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("leaves third-party deep imports untouched", () => {
|
|
125
|
+
const code = 'import x from "zod/lib/index.js";';
|
|
126
|
+
expect(transform(code, "id").code).toBe(code);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("returns empty code unchanged", () => {
|
|
130
|
+
expect(transform("", "id").code).toBe("");
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
describe("tsNoCheckPlugin", () => {
|
|
135
|
+
it("prefixes every emitted chunk with a ts-nocheck pragma", () => {
|
|
136
|
+
// Consumers typecheck the bundled output; without this the generated
|
|
137
|
+
// code fails their build.
|
|
138
|
+
const bundle = {
|
|
139
|
+
"index.js": { type: "chunk", code: "export const a = 1;" },
|
|
140
|
+
"other.js": { type: "chunk", code: "export const b = 2;" },
|
|
141
|
+
};
|
|
142
|
+
tsNoCheckPlugin().generateBundle({}, bundle);
|
|
143
|
+
|
|
144
|
+
expect(bundle["index.js"].code).toBe("// @ts-nocheck\nexport const a = 1;");
|
|
145
|
+
expect(bundle["other.js"].code).toBe("// @ts-nocheck\nexport const b = 2;");
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("leaves assets and empty chunks alone", () => {
|
|
149
|
+
const bundle = {
|
|
150
|
+
"style.css": { type: "asset", source: "body{}" },
|
|
151
|
+
"empty.js": { type: "chunk", code: "" },
|
|
152
|
+
};
|
|
153
|
+
tsNoCheckPlugin().generateBundle({}, bundle);
|
|
154
|
+
|
|
155
|
+
expect(bundle["style.css"]).toEqual({ type: "asset", source: "body{}" });
|
|
156
|
+
expect(bundle["empty.js"].code).toBe("");
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("handles an empty bundle", () => {
|
|
160
|
+
expect(() => tsNoCheckPlugin().generateBundle({}, {})).not.toThrow();
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it("is named", () => {
|
|
164
|
+
expect(tsNoCheckPlugin().name).toBe("vite-plugin-ts-nocheck");
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
describe("VitePluginExternalizeObfuscatorDeadCode", () => {
|
|
169
|
+
const resolveId = handlerOf(
|
|
170
|
+
VitePluginExternalizeObfuscatorDeadCode().resolveId,
|
|
171
|
+
) as (source: string) => { id: string; external: boolean } | null;
|
|
172
|
+
|
|
173
|
+
it("runs before resolution so rolldown never sees the specifier", () => {
|
|
174
|
+
const plugin = VitePluginExternalizeObfuscatorDeadCode();
|
|
175
|
+
expect(plugin.name).toBe("externalize-obfuscator-deadcode");
|
|
176
|
+
expect(plugin.enforce).toBe("pre");
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it("externalises each known dead-code specifier", () => {
|
|
180
|
+
// Rolldown treats an unresolved import as a hard error, so missing one
|
|
181
|
+
// of these breaks the obfuscated build outright.
|
|
182
|
+
for (const source of [
|
|
183
|
+
"../../package",
|
|
184
|
+
"../redacted.js",
|
|
185
|
+
"../utils/isStandaloneExecutable",
|
|
186
|
+
"./resolve-local-redacted-path",
|
|
187
|
+
"@redacted/components/package",
|
|
188
|
+
"@redacted/enterprise-plugin",
|
|
189
|
+
"@redacted/enterprise-plugin/package",
|
|
190
|
+
]) {
|
|
191
|
+
expect(resolveId(source), source).toEqual({ id: source, external: true });
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it("defers every other specifier to the normal resolver", () => {
|
|
196
|
+
for (const source of [
|
|
197
|
+
"zod",
|
|
198
|
+
"./real-module.js",
|
|
199
|
+
"",
|
|
200
|
+
"../../package.json",
|
|
201
|
+
]) {
|
|
202
|
+
expect(resolveId(source), source).toBeNull();
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it("matches exactly, not by prefix", () => {
|
|
207
|
+
expect(resolveId("@redacted/enterprise-plugin/extra")).toBeNull();
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
describe("VitePluginCopy", () => {
|
|
212
|
+
let root: string;
|
|
213
|
+
let cwd: string;
|
|
214
|
+
|
|
215
|
+
beforeEach(() => {
|
|
216
|
+
root = fs.mkdtempSync(path.join(os.tmpdir(), "prosopo-copy-test-"));
|
|
217
|
+
cwd = process.cwd();
|
|
218
|
+
process.chdir(root);
|
|
219
|
+
fs.mkdirSync(path.join(root, "src", "nested"), { recursive: true });
|
|
220
|
+
fs.writeFileSync(path.join(root, "src", "a.txt"), "a");
|
|
221
|
+
fs.writeFileSync(path.join(root, "src", "b.md"), "b");
|
|
222
|
+
fs.writeFileSync(path.join(root, "src", "nested", "c.txt"), "c");
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
afterEach(() => {
|
|
226
|
+
process.chdir(cwd);
|
|
227
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
const run = async (options: Parameters<typeof VitePluginCopy>[0]) => {
|
|
231
|
+
const plugin = VitePluginCopy(options);
|
|
232
|
+
const closeBundle = handlerOf(plugin.closeBundle) as () => Promise<void>;
|
|
233
|
+
await closeBundle();
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
it("copies matching files and creates the destination tree", async () => {
|
|
237
|
+
await run({ srcDir: "src", destDir: "dist", include: ["**/*.txt"] });
|
|
238
|
+
|
|
239
|
+
expect(fs.readFileSync(path.join(root, "dist", "a.txt"), "utf8")).toBe("a");
|
|
240
|
+
expect(
|
|
241
|
+
fs.readFileSync(path.join(root, "dist", "nested", "c.txt"), "utf8"),
|
|
242
|
+
).toBe("c");
|
|
243
|
+
expect(fs.existsSync(path.join(root, "dist", "b.md"))).toBe(false);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it("honours the exclude globs", async () => {
|
|
247
|
+
await run({
|
|
248
|
+
srcDir: "src",
|
|
249
|
+
destDir: "dist",
|
|
250
|
+
include: ["**/*.txt"],
|
|
251
|
+
exclude: ["nested/**"],
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
expect(fs.existsSync(path.join(root, "dist", "a.txt"))).toBe(true);
|
|
255
|
+
expect(fs.existsSync(path.join(root, "dist", "nested"))).toBe(false);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it("copies nothing, and says so, when no include glob is given", async () => {
|
|
259
|
+
// Copying the whole tree by default would silently ship sources.
|
|
260
|
+
const info = vi.spyOn(console, "info").mockImplementation(() => undefined);
|
|
261
|
+
await run({ srcDir: "src", destDir: "dist" });
|
|
262
|
+
|
|
263
|
+
expect(fs.existsSync(path.join(root, "dist"))).toBe(false);
|
|
264
|
+
expect(info).toHaveBeenCalledWith(
|
|
265
|
+
"[copy-plugin] No include globs specified, nothing to copy.",
|
|
266
|
+
);
|
|
267
|
+
info.mockRestore();
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it("treats an empty include list as no include list", async () => {
|
|
271
|
+
await run({ srcDir: "src", destDir: "dist", include: [] });
|
|
272
|
+
expect(fs.existsSync(path.join(root, "dist"))).toBe(false);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it("succeeds when nothing matches", async () => {
|
|
276
|
+
await run({ srcDir: "src", destDir: "dist", include: ["**/*.nope"] });
|
|
277
|
+
expect(fs.existsSync(path.join(root, "dist"))).toBe(false);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it("overwrites an existing destination file", async () => {
|
|
281
|
+
fs.mkdirSync(path.join(root, "dist"), { recursive: true });
|
|
282
|
+
fs.writeFileSync(path.join(root, "dist", "a.txt"), "stale");
|
|
283
|
+
await run({ srcDir: "src", destDir: "dist", include: ["a.txt"] });
|
|
284
|
+
expect(fs.readFileSync(path.join(root, "dist", "a.txt"), "utf8")).toBe("a");
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it("is named", () => {
|
|
288
|
+
expect(VitePluginCopy({ srcDir: "s", destDir: "d" }).name).toBe(
|
|
289
|
+
"copy-plugin",
|
|
290
|
+
);
|
|
291
|
+
});
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
describe("translation flatten/unflatten", () => {
|
|
295
|
+
it("flattens nested objects to dotted keys", () => {
|
|
296
|
+
expect(flatten({ a: { b: { c: "x" } }, d: "y" })).toEqual({
|
|
297
|
+
"a.b.c": "x",
|
|
298
|
+
d: "y",
|
|
299
|
+
});
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
it("returns an empty object for an empty input", () => {
|
|
303
|
+
expect(flatten({})).toEqual({});
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
it("applies a prefix", () => {
|
|
307
|
+
expect(flatten({ a: "x" }, "p.")).toEqual({ "p.a": "x" });
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
it("treats arrays as objects, keying by index", () => {
|
|
311
|
+
expect(flatten({ a: ["x", "y"] })).toEqual({ "a.0": "x", "a.1": "y" });
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
it("round trips a nested object", () => {
|
|
315
|
+
const original = { a: { b: "x" }, c: "y" };
|
|
316
|
+
expect(unflatten(flatten(original))).toEqual(original);
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
it("rebuilds arrays when the next key is numeric", () => {
|
|
320
|
+
// Locale files contain arrays of strings; turning them into objects
|
|
321
|
+
// keyed "0", "1" would change the shape the app reads at runtime.
|
|
322
|
+
expect(unflatten({ "a.0": "x", "a.1": "y" })).toEqual({ a: ["x", "y"] });
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
it("keeps object keys that merely look like words", () => {
|
|
326
|
+
expect(unflatten({ "a.b": "x" })).toEqual({ a: { b: "x" } });
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
it("merges sibling keys under one parent", () => {
|
|
330
|
+
expect(unflatten({ "a.b": "x", "a.c": "y" })).toEqual({
|
|
331
|
+
a: { b: "x", c: "y" },
|
|
332
|
+
});
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
it("handles a top-level key with no dots", () => {
|
|
336
|
+
expect(unflatten({ a: "x" })).toEqual({ a: "x" });
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
it("returns an empty object for an empty input", () => {
|
|
340
|
+
expect(unflatten({})).toEqual({});
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
it("preserves non-string leaf values", () => {
|
|
344
|
+
expect(unflatten({ "a.b": 1, "a.c": true })).toEqual({
|
|
345
|
+
a: { b: 1, c: true },
|
|
346
|
+
});
|
|
347
|
+
});
|
|
348
|
+
});
|
|
@@ -0,0 +1,164 @@
|
|
|
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 type { Rollup } from "vite";
|
|
19
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
20
|
+
import VitePluginRemoveUnusedTranslations from "./vite-plugin-remove-unused-translations.js";
|
|
21
|
+
|
|
22
|
+
let root: string;
|
|
23
|
+
let cwd: string;
|
|
24
|
+
|
|
25
|
+
beforeEach(() => {
|
|
26
|
+
root = fs.mkdtempSync(path.join(os.tmpdir(), "prosopo-translations-test-"));
|
|
27
|
+
cwd = process.cwd();
|
|
28
|
+
process.chdir(root);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
afterEach(() => {
|
|
32
|
+
process.chdir(cwd);
|
|
33
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
/** Vite plugin hooks may be a bare function or an object with a `handler`. */
|
|
37
|
+
type Hook<A extends unknown[], R> =
|
|
38
|
+
| ((...args: A) => R)
|
|
39
|
+
| { handler: (...args: A) => R }
|
|
40
|
+
| undefined;
|
|
41
|
+
|
|
42
|
+
const callHook = <A extends unknown[], R>(hook: Hook<A, R>, ...args: A): R => {
|
|
43
|
+
if (!hook) {
|
|
44
|
+
throw new Error("plugin does not define the hook");
|
|
45
|
+
}
|
|
46
|
+
const handler = typeof hook === "function" ? hook : hook.handler;
|
|
47
|
+
return handler(...args);
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const writeJson = (name: string, data: unknown): string => {
|
|
51
|
+
const filePath = path.join(root, name);
|
|
52
|
+
fs.writeFileSync(filePath, JSON.stringify(data));
|
|
53
|
+
return filePath;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const readJson = (filePath: string): unknown =>
|
|
57
|
+
JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
58
|
+
|
|
59
|
+
const run = async (
|
|
60
|
+
keys: string[],
|
|
61
|
+
sources: string[],
|
|
62
|
+
pattern: string,
|
|
63
|
+
): Promise<void> => {
|
|
64
|
+
const plugin = VitePluginRemoveUnusedTranslations(keys, pattern);
|
|
65
|
+
for (const source of sources) {
|
|
66
|
+
callHook(plugin.transform, source, "id.ts");
|
|
67
|
+
}
|
|
68
|
+
await callHook(
|
|
69
|
+
plugin.writeBundle,
|
|
70
|
+
{} as Rollup.NormalizedOutputOptions,
|
|
71
|
+
{} as Rollup.OutputBundle,
|
|
72
|
+
);
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
describe("VitePluginRemoveUnusedTranslations", () => {
|
|
76
|
+
it("runs after the other plugins, once the code is final", () => {
|
|
77
|
+
expect(VitePluginRemoveUnusedTranslations([], "*.json").enforce).toBe(
|
|
78
|
+
"post",
|
|
79
|
+
);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("returns the code it was given, unmodified", () => {
|
|
83
|
+
const plugin = VitePluginRemoveUnusedTranslations(["zz"], "*.json");
|
|
84
|
+
expect(callHook(plugin.transform, "uses zz", "id.ts")).toEqual({
|
|
85
|
+
code: "uses zz",
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// The set of used keys lives at module scope, shared by every plugin
|
|
90
|
+
// instance in the process, so each test below uses key names of its own.
|
|
91
|
+
|
|
92
|
+
it("keeps only the keys the bundle actually references", async () => {
|
|
93
|
+
const file = writeJson("en.json", { kept: "yes", dropped: "no" });
|
|
94
|
+
await run(["kept", "dropped"], ["t('kept')"], `${root}/*.json`);
|
|
95
|
+
expect(readJson(file)).toEqual({ kept: "yes" });
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("restores the nesting of the keys it keeps", async () => {
|
|
99
|
+
const file = writeJson("en.json", {
|
|
100
|
+
widget: { label: "Verify", hint: "Click" },
|
|
101
|
+
});
|
|
102
|
+
await run(
|
|
103
|
+
["widget.label", "widget.hint"],
|
|
104
|
+
["t('widget.label')"],
|
|
105
|
+
`${root}/*.json`,
|
|
106
|
+
);
|
|
107
|
+
expect(readJson(file)).toEqual({ widget: { label: "Verify" } });
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("empties a file whose keys are all unused", async () => {
|
|
111
|
+
const file = writeJson("en.json", { solo: "1" });
|
|
112
|
+
await run(["solo"], ["nothing here"], `${root}/*.json`);
|
|
113
|
+
expect(readJson(file)).toEqual({});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("prunes every matching file", async () => {
|
|
117
|
+
const en = writeJson("en.json", { pa: "en", pb: "en" });
|
|
118
|
+
const fr = writeJson("fr.json", { pa: "fr", pb: "fr" });
|
|
119
|
+
await run(["pa", "pb"], ["t('pa')"], `${root}/*.json`);
|
|
120
|
+
expect(readJson(en)).toEqual({ pa: "en" });
|
|
121
|
+
expect(readJson(fr)).toEqual({ pa: "fr" });
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("does nothing when the pattern matches no file", async () => {
|
|
125
|
+
await expect(
|
|
126
|
+
run(["nk"], ["t('nk')"], `${root}/none/*.json`),
|
|
127
|
+
).resolves.toBeUndefined();
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("leaves a malformed JSON file alone instead of failing the build", async () => {
|
|
131
|
+
// A translation file is not worth aborting a release for; the bundle is
|
|
132
|
+
// already written by the time this hook runs.
|
|
133
|
+
const broken = path.join(root, "en.json");
|
|
134
|
+
fs.writeFileSync(broken, "{ not json");
|
|
135
|
+
await expect(
|
|
136
|
+
run(["mk"], ["t('mk')"], `${root}/*.json`),
|
|
137
|
+
).resolves.toBeUndefined();
|
|
138
|
+
expect(fs.readFileSync(broken, "utf-8")).toBe("{ not json");
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("declares no key used when the key list is empty", async () => {
|
|
142
|
+
const file = writeJson("en.json", { ek: "1" });
|
|
143
|
+
await run([], ["t('ek')"], `${root}/*.json`);
|
|
144
|
+
expect(readJson(file)).toEqual({});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("matches a key as a substring of the emitted code", async () => {
|
|
148
|
+
// The keys are looked for with `includes`, not as whole tokens, so a key
|
|
149
|
+
// that is a prefix of another is kept whenever the longer one is used.
|
|
150
|
+
const file = writeJson("en.json", { sa: "1", sab: "2" });
|
|
151
|
+
await run(["sa", "sab"], ["t('sab')"], `${root}/*.json`);
|
|
152
|
+
expect(readJson(file)).toEqual({ sa: "1", sab: "2" });
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("accumulates used keys across every transformed module", async () => {
|
|
156
|
+
const file = writeJson("en.json", { one: "1", two: "2", three: "3" });
|
|
157
|
+
await run(
|
|
158
|
+
["one", "two", "three"],
|
|
159
|
+
["t('one')", "t('two')"],
|
|
160
|
+
`${root}/*.json`,
|
|
161
|
+
);
|
|
162
|
+
expect(readJson(file)).toEqual({ one: "1", two: "2" });
|
|
163
|
+
});
|
|
164
|
+
});
|
|
@@ -58,6 +58,11 @@ export default function (tsConfigPath?: string) {
|
|
|
58
58
|
"src/**/*.spec.ts",
|
|
59
59
|
"src/**/*.test.tsx",
|
|
60
60
|
"src/**/*.spec.tsx",
|
|
61
|
+
// Type-test files are compiled by the typecheck pass, never
|
|
62
|
+
// executed, so they can only ever report 0% and drag the real
|
|
63
|
+
// figure down. `*.d.ts` above does not match `*.test-d.ts`.
|
|
64
|
+
"src/**/*.test-d.ts",
|
|
65
|
+
"src/**/*.test-d.tsx",
|
|
61
66
|
]
|
|
62
67
|
: [
|
|
63
68
|
"**/tests/**/*",
|