@riebeckite/test 0.0.6
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/LICENSE +201 -0
- package/README.md +88 -0
- package/README_ja.md +69 -0
- package/dist/e2e/assertions.d.ts +63 -0
- package/dist/e2e/cli.d.ts +23 -0
- package/dist/e2e/files.d.ts +7 -0
- package/dist/e2e/format.d.ts +5 -0
- package/dist/e2e/html.d.ts +10 -0
- package/dist/e2e/index.d.ts +20 -0
- package/dist/e2e/index.js +573 -0
- package/dist/e2e/logger.d.ts +18 -0
- package/dist/e2e/pack.d.ts +30 -0
- package/dist/e2e/process.d.ts +34 -0
- package/dist/e2e/run.d.ts +100 -0
- package/dist/e2e/stage.d.ts +55 -0
- package/dist/golden.d.ts +22 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +40 -0
- package/package.json +59 -0
|
@@ -0,0 +1,573 @@
|
|
|
1
|
+
// e2e/assertions.ts
|
|
2
|
+
import fs2 from "node:fs";
|
|
3
|
+
import path2 from "node:path";
|
|
4
|
+
|
|
5
|
+
// e2e/files.ts
|
|
6
|
+
import fs from "node:fs";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
function walkFiles(root, predicate = () => true) {
|
|
9
|
+
const found = [];
|
|
10
|
+
if (!fs.existsSync(root)) return found;
|
|
11
|
+
const stack = [root];
|
|
12
|
+
while (stack.length > 0) {
|
|
13
|
+
const current = stack.pop();
|
|
14
|
+
if (current === void 0) break;
|
|
15
|
+
for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
|
|
16
|
+
const full = path.join(current, entry.name);
|
|
17
|
+
if (entry.isDirectory()) {
|
|
18
|
+
stack.push(full);
|
|
19
|
+
} else if (predicate(full)) {
|
|
20
|
+
found.push(full);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return found;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// e2e/process.ts
|
|
28
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
29
|
+
var MAX_BUFFER = 128 * 1024 * 1024;
|
|
30
|
+
function quote(value) {
|
|
31
|
+
const text = String(value);
|
|
32
|
+
return /[\s"]/.test(text) ? `"${text.replace(/"/g, '\\"')}"` : text;
|
|
33
|
+
}
|
|
34
|
+
function failureDetail(stdout, stderr) {
|
|
35
|
+
return `--- stdout ---
|
|
36
|
+
${stdout}
|
|
37
|
+
--- stderr ---
|
|
38
|
+
${stderr}`;
|
|
39
|
+
}
|
|
40
|
+
function run(command, args, options = {}) {
|
|
41
|
+
const line = [command, ...args].map(quote).join(" ");
|
|
42
|
+
const result = spawnSync(line, {
|
|
43
|
+
shell: true,
|
|
44
|
+
cwd: options.cwd ?? process.cwd(),
|
|
45
|
+
env: { ...process.env, ...options.env },
|
|
46
|
+
encoding: "utf8",
|
|
47
|
+
maxBuffer: MAX_BUFFER
|
|
48
|
+
});
|
|
49
|
+
if (result.error) {
|
|
50
|
+
throw new Error(`Failed to run: ${line}
|
|
51
|
+
${result.error.message}`);
|
|
52
|
+
}
|
|
53
|
+
const stdout = result.stdout ?? "";
|
|
54
|
+
const stderr = result.stderr ?? "";
|
|
55
|
+
if (result.status !== 0 && !options.allowFailure) {
|
|
56
|
+
throw new Error(
|
|
57
|
+
`Command failed (exit ${result.status}): ${line}
|
|
58
|
+
` + failureDetail(stdout, stderr)
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
return { status: result.status, stdout, stderr };
|
|
62
|
+
}
|
|
63
|
+
function runAsync(command, args, options = {}) {
|
|
64
|
+
const line = [command, ...args].map(quote).join(" ");
|
|
65
|
+
return new Promise((resolve, reject) => {
|
|
66
|
+
const child = spawn(line, {
|
|
67
|
+
shell: true,
|
|
68
|
+
cwd: options.cwd ?? process.cwd(),
|
|
69
|
+
env: { ...process.env, ...options.env },
|
|
70
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
71
|
+
});
|
|
72
|
+
let settled = false;
|
|
73
|
+
let exceeded = false;
|
|
74
|
+
let stdout = "";
|
|
75
|
+
let stderr = "";
|
|
76
|
+
const stdoutStream = child.stdout;
|
|
77
|
+
const stderrStream = child.stderr;
|
|
78
|
+
if (!stdoutStream || !stderrStream) {
|
|
79
|
+
child.kill("SIGKILL");
|
|
80
|
+
reject(new Error(`Failed to run: ${line}
|
|
81
|
+
no stdio streams available`));
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
const onData = (kind, chunk) => {
|
|
85
|
+
const length = kind === "stdout" ? stdout.length : stderr.length;
|
|
86
|
+
if (length + chunk.length > MAX_BUFFER) {
|
|
87
|
+
exceeded = true;
|
|
88
|
+
child.kill("SIGKILL");
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
if (kind === "stdout") stdout += chunk;
|
|
92
|
+
else stderr += chunk;
|
|
93
|
+
};
|
|
94
|
+
stdoutStream.setEncoding("utf8");
|
|
95
|
+
stderrStream.setEncoding("utf8");
|
|
96
|
+
stdoutStream.on("data", (chunk) => onData("stdout", chunk));
|
|
97
|
+
stderrStream.on("data", (chunk) => onData("stderr", chunk));
|
|
98
|
+
child.on("error", (error) => {
|
|
99
|
+
if (settled) return;
|
|
100
|
+
settled = true;
|
|
101
|
+
reject(new Error(`Failed to run: ${line}
|
|
102
|
+
${error.message}`));
|
|
103
|
+
});
|
|
104
|
+
child.on("close", (status) => {
|
|
105
|
+
if (settled) return;
|
|
106
|
+
settled = true;
|
|
107
|
+
if (exceeded) {
|
|
108
|
+
reject(
|
|
109
|
+
new Error(`Command output exceeded ${MAX_BUFFER} bytes: ${line}`)
|
|
110
|
+
);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
if (status !== 0 && !options.allowFailure) {
|
|
114
|
+
reject(
|
|
115
|
+
new Error(
|
|
116
|
+
`Command failed (exit ${status}): ${line}
|
|
117
|
+
` + failureDetail(stdout, stderr)
|
|
118
|
+
)
|
|
119
|
+
);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
resolve({ status, stdout, stderr });
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// e2e/assertions.ts
|
|
128
|
+
function isInside(parent, child) {
|
|
129
|
+
const relative = path2.relative(parent, child);
|
|
130
|
+
return relative === "" || !relative.startsWith("..") && !path2.isAbsolute(relative);
|
|
131
|
+
}
|
|
132
|
+
function assertIsolatedInstall(siteDir, tempRoot, options) {
|
|
133
|
+
const { repoRoot, requiredPaths = [], logger } = options;
|
|
134
|
+
logger.step("verifying standalone install (no pnpm/monorepo inheritance)");
|
|
135
|
+
if (isInside(repoRoot, tempRoot)) {
|
|
136
|
+
logger.fail("the external workspace must live outside the repository");
|
|
137
|
+
}
|
|
138
|
+
const forbiddenState = [
|
|
139
|
+
path2.join(tempRoot, "pnpm-workspace.yaml"),
|
|
140
|
+
path2.join(tempRoot, "pnpm-lock.yaml"),
|
|
141
|
+
path2.join(siteDir, "pnpm-workspace.yaml"),
|
|
142
|
+
path2.join(siteDir, "pnpm-lock.yaml"),
|
|
143
|
+
path2.join(siteDir, "node_modules", ".pnpm")
|
|
144
|
+
];
|
|
145
|
+
for (const file of forbiddenState) {
|
|
146
|
+
if (fs2.existsSync(file)) {
|
|
147
|
+
logger.fail(
|
|
148
|
+
`external site must not inherit pnpm workspace state: ${path2.relative(
|
|
149
|
+
tempRoot,
|
|
150
|
+
file
|
|
151
|
+
)}`
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
const nodeModules = path2.join(siteDir, "node_modules");
|
|
156
|
+
if (!fs2.existsSync(nodeModules)) {
|
|
157
|
+
logger.fail("the isolated install did not create site/node_modules");
|
|
158
|
+
}
|
|
159
|
+
const probes = [];
|
|
160
|
+
for (const required of requiredPaths) {
|
|
161
|
+
const absolute = path2.join(siteDir, required.relativePath);
|
|
162
|
+
if (!fs2.existsSync(absolute)) {
|
|
163
|
+
logger.fail(required.missingMessage);
|
|
164
|
+
}
|
|
165
|
+
if (required.report ?? true) {
|
|
166
|
+
probes.push(`${required.label} -> ${path2.relative(siteDir, absolute)}`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
const nodeModulesLabel = path2.relative(path2.dirname(siteDir), nodeModules).split(path2.sep).join("/");
|
|
170
|
+
const suffix = probes.length > 0 ? `; ${probes.join("; ")}` : "";
|
|
171
|
+
console.log(` ${nodeModulesLabel} is local${suffix}`);
|
|
172
|
+
}
|
|
173
|
+
function reachesDirectory(contents, directory, minimumTraversals) {
|
|
174
|
+
const needle = `${directory}/`;
|
|
175
|
+
const parent = `${"."}${"."}/`;
|
|
176
|
+
let index = contents.indexOf(needle);
|
|
177
|
+
while (index !== -1) {
|
|
178
|
+
let cursor = index;
|
|
179
|
+
let traversals = 0;
|
|
180
|
+
while (cursor - parent.length >= 0 && contents.slice(cursor - parent.length, cursor) === parent) {
|
|
181
|
+
traversals += 1;
|
|
182
|
+
cursor -= parent.length;
|
|
183
|
+
}
|
|
184
|
+
if (traversals >= minimumTraversals) return true;
|
|
185
|
+
index = contents.indexOf(needle, index + needle.length);
|
|
186
|
+
}
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
189
|
+
function assertNoMonorepoEscapeHatches(siteDir, options) {
|
|
190
|
+
const { monorepoPackagesDirectory = "packages", logger } = options;
|
|
191
|
+
logger.step("checking the site for monorepo escape hatches");
|
|
192
|
+
const manifest = JSON.parse(
|
|
193
|
+
fs2.readFileSync(path2.join(siteDir, "package.json"), "utf8")
|
|
194
|
+
);
|
|
195
|
+
if (JSON.stringify(manifest).includes("workspace:")) {
|
|
196
|
+
logger.fail(
|
|
197
|
+
"site/package.json must not contain `workspace:` protocol references"
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
const sourceFiles = walkFiles(siteDir, (full) => {
|
|
201
|
+
const relative = path2.relative(siteDir, full);
|
|
202
|
+
if (relative.startsWith(`node_modules${path2.sep}`)) return false;
|
|
203
|
+
if (relative.startsWith(`dist${path2.sep}`)) return false;
|
|
204
|
+
return /\.(ts|tsx|mts|cts|js|mjs|json)$/.test(full);
|
|
205
|
+
});
|
|
206
|
+
for (const file of sourceFiles) {
|
|
207
|
+
const contents = fs2.readFileSync(file, "utf8");
|
|
208
|
+
if (contents.includes("workspace:")) {
|
|
209
|
+
logger.fail(`${path2.relative(siteDir, file)} contains \`workspace:\``);
|
|
210
|
+
}
|
|
211
|
+
if (reachesDirectory(contents, monorepoPackagesDirectory, 2)) {
|
|
212
|
+
logger.fail(
|
|
213
|
+
`${path2.relative(
|
|
214
|
+
siteDir,
|
|
215
|
+
file
|
|
216
|
+
)} reaches into the monorepo \`${monorepoPackagesDirectory}/\``
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
if (fs2.existsSync(path2.join(siteDir, monorepoPackagesDirectory))) {
|
|
221
|
+
logger.fail(
|
|
222
|
+
`site must not contain a \`${monorepoPackagesDirectory}/\` directory`
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
function assertPublishedArtifacts(siteDir, options) {
|
|
227
|
+
const { repoRoot, scope, logger } = options;
|
|
228
|
+
logger.step(`verifying ${scope}/* resolves to installed tarballs only`);
|
|
229
|
+
const scopeDir = path2.join(siteDir, "node_modules", ...scope.split("/"));
|
|
230
|
+
if (!fs2.existsSync(scopeDir)) {
|
|
231
|
+
logger.fail(`no ${scope}/* packages were installed`);
|
|
232
|
+
}
|
|
233
|
+
for (const entry of fs2.readdirSync(scopeDir)) {
|
|
234
|
+
const full = path2.join(scopeDir, entry);
|
|
235
|
+
const real = fs2.realpathSync(full);
|
|
236
|
+
if (isInside(repoRoot, real)) {
|
|
237
|
+
logger.fail(
|
|
238
|
+
`${scope}/${entry} resolves into the repository (${real}); the fixture must use installed tarballs only`
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
if (fs2.lstatSync(full).isSymbolicLink()) {
|
|
242
|
+
logger.fail(
|
|
243
|
+
`${scope}/${entry} is a symlink; expected a real installed directory`
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
console.log(` ${scope}/${entry} -> ${real}`);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
function assertDeclaredDependencies(siteDir, options) {
|
|
250
|
+
const { dependencyCheckScript, cwd, logger } = options;
|
|
251
|
+
logger.step("checking the site for undeclared (hoisted) dependencies");
|
|
252
|
+
run(process.execPath, [dependencyCheckScript, siteDir], { cwd });
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// e2e/cli.ts
|
|
256
|
+
import path3 from "node:path";
|
|
257
|
+
function runCli(args, options) {
|
|
258
|
+
const { cliEntry, cwd, cliName, logger } = options;
|
|
259
|
+
logger.step(`${cliName} ${args.join(" ")}`);
|
|
260
|
+
const result = run(process.execPath, [cliEntry, ...args], { cwd });
|
|
261
|
+
const output = cliText(result).trim();
|
|
262
|
+
if (output) console.log(output);
|
|
263
|
+
return result;
|
|
264
|
+
}
|
|
265
|
+
function cliText(result) {
|
|
266
|
+
return `${result.stdout}${result.stderr}`;
|
|
267
|
+
}
|
|
268
|
+
function runTypecheck(siteDir, project, logger) {
|
|
269
|
+
logger.step(`tsc --noEmit -p ${project}`);
|
|
270
|
+
const tsc = path3.join(siteDir, "node_modules", "typescript", "bin", "tsc");
|
|
271
|
+
const result = run(process.execPath, [tsc, "--noEmit", "-p", project], {
|
|
272
|
+
cwd: siteDir
|
|
273
|
+
});
|
|
274
|
+
const output = cliText(result).trim();
|
|
275
|
+
if (output) console.log(output);
|
|
276
|
+
return result;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// e2e/format.ts
|
|
280
|
+
function formatBytes(bytes) {
|
|
281
|
+
const units = ["B", "KB", "MB", "GB"];
|
|
282
|
+
let value = bytes;
|
|
283
|
+
let unitIndex = 0;
|
|
284
|
+
while (value >= 1024 && unitIndex < units.length - 1) {
|
|
285
|
+
value /= 1024;
|
|
286
|
+
unitIndex += 1;
|
|
287
|
+
}
|
|
288
|
+
const digits = value >= 10 || unitIndex === 0 ? 0 : 1;
|
|
289
|
+
return `${value.toFixed(digits)} ${units[unitIndex]}`;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// e2e/html.ts
|
|
293
|
+
function escapeRegExp(value) {
|
|
294
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
295
|
+
}
|
|
296
|
+
function extractScriptPayloads(html, attribute) {
|
|
297
|
+
const pattern = new RegExp(
|
|
298
|
+
`<script[^>]*${escapeRegExp(attribute)}[^>]*>([\\s\\S]*?)</script>`,
|
|
299
|
+
"g"
|
|
300
|
+
);
|
|
301
|
+
return [...html.matchAll(pattern)].map((match) => match[1] ?? "").join("\n");
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// e2e/logger.ts
|
|
305
|
+
function createLogger(label = "e2e") {
|
|
306
|
+
return {
|
|
307
|
+
step(message) {
|
|
308
|
+
console.log(`
|
|
309
|
+
[${label}] ${message}`);
|
|
310
|
+
},
|
|
311
|
+
fail(message) {
|
|
312
|
+
throw new Error(message);
|
|
313
|
+
}
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// e2e/pack.ts
|
|
318
|
+
import fs3 from "node:fs";
|
|
319
|
+
import path4 from "node:path";
|
|
320
|
+
async function packPackages(tarballDir, options) {
|
|
321
|
+
const { repoRoot, packages, logger, concurrency = 4 } = options;
|
|
322
|
+
logger.step(
|
|
323
|
+
`packing ${packages.length} packages with pnpm pack (concurrency ${concurrency})`
|
|
324
|
+
);
|
|
325
|
+
const packOne = async (pkg) => {
|
|
326
|
+
const packageDir = path4.join(repoRoot, pkg.directory);
|
|
327
|
+
if (!fs3.existsSync(packageDir)) {
|
|
328
|
+
logger.fail(`Package directory is missing: ${pkg.directory}`);
|
|
329
|
+
}
|
|
330
|
+
const manifest = JSON.parse(
|
|
331
|
+
fs3.readFileSync(path4.join(packageDir, "package.json"), "utf8")
|
|
332
|
+
);
|
|
333
|
+
await runAsync("pnpm", ["pack", "--pack-destination", tarballDir], {
|
|
334
|
+
cwd: packageDir
|
|
335
|
+
});
|
|
336
|
+
const sanitized = pkg.name.replace(/^@/, "").replace(/\//g, "-");
|
|
337
|
+
const tarball = path4.join(
|
|
338
|
+
tarballDir,
|
|
339
|
+
`${sanitized}-${manifest.version}.tgz`
|
|
340
|
+
);
|
|
341
|
+
if (!fs3.existsSync(tarball)) {
|
|
342
|
+
logger.fail(`pnpm pack did not produce the expected tarball: ${tarball}`);
|
|
343
|
+
}
|
|
344
|
+
console.log(` packed ${pkg.name} -> ${path4.basename(tarball)}`);
|
|
345
|
+
return { pkg, version: manifest.version, tarball };
|
|
346
|
+
};
|
|
347
|
+
const results = new Array(packages.length);
|
|
348
|
+
let nextIndex = 0;
|
|
349
|
+
const worker = async () => {
|
|
350
|
+
while (nextIndex < packages.length) {
|
|
351
|
+
const index = nextIndex;
|
|
352
|
+
nextIndex += 1;
|
|
353
|
+
const pkg = packages[index];
|
|
354
|
+
if (pkg === void 0) continue;
|
|
355
|
+
results[index] = await packOne(pkg);
|
|
356
|
+
}
|
|
357
|
+
};
|
|
358
|
+
const batch = await Promise.allSettled(
|
|
359
|
+
Array.from(
|
|
360
|
+
{ length: Math.min(concurrency, packages.length) },
|
|
361
|
+
() => worker()
|
|
362
|
+
)
|
|
363
|
+
);
|
|
364
|
+
const failure = batch.find(
|
|
365
|
+
(result) => result.status === "rejected"
|
|
366
|
+
);
|
|
367
|
+
if (failure) {
|
|
368
|
+
throw failure.reason;
|
|
369
|
+
}
|
|
370
|
+
const packed = /* @__PURE__ */ new Map();
|
|
371
|
+
for (const result of results) {
|
|
372
|
+
packed.set(result.pkg.name, {
|
|
373
|
+
tarball: result.tarball,
|
|
374
|
+
version: result.version
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
return packed;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// e2e/run.ts
|
|
381
|
+
import fs5 from "node:fs";
|
|
382
|
+
import os from "node:os";
|
|
383
|
+
import path6 from "node:path";
|
|
384
|
+
|
|
385
|
+
// e2e/stage.ts
|
|
386
|
+
import fs4 from "node:fs";
|
|
387
|
+
import path5 from "node:path";
|
|
388
|
+
function stageIsolatedSite(options) {
|
|
389
|
+
fs4.cpSync(options.fixtureSite, options.siteDir, { recursive: true });
|
|
390
|
+
fs4.cpSync(options.fixtureVault, options.vaultDir, { recursive: true });
|
|
391
|
+
}
|
|
392
|
+
function writeFileDependencies(siteDir, packed) {
|
|
393
|
+
const manifestPath = path5.join(siteDir, "package.json");
|
|
394
|
+
const manifest = JSON.parse(
|
|
395
|
+
fs4.readFileSync(manifestPath, "utf8")
|
|
396
|
+
);
|
|
397
|
+
const dependencies = { ...manifest.dependencies };
|
|
398
|
+
const devDependencies = { ...manifest.devDependencies };
|
|
399
|
+
const overrides = { ...manifest.overrides };
|
|
400
|
+
for (const [name, info] of packed) {
|
|
401
|
+
const fileSpec = `file:${path5.relative(siteDir, info.tarball).split(path5.sep).join("/")}`;
|
|
402
|
+
if (name in devDependencies) {
|
|
403
|
+
devDependencies[name] = fileSpec;
|
|
404
|
+
} else {
|
|
405
|
+
dependencies[name] = fileSpec;
|
|
406
|
+
}
|
|
407
|
+
overrides[name] = fileSpec;
|
|
408
|
+
}
|
|
409
|
+
manifest.dependencies = dependencies;
|
|
410
|
+
manifest.devDependencies = devDependencies;
|
|
411
|
+
manifest.overrides = overrides;
|
|
412
|
+
fs4.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}
|
|
413
|
+
`);
|
|
414
|
+
}
|
|
415
|
+
function layerExternalConsumerTypes(siteDir, options) {
|
|
416
|
+
const { additionalTypeLibraries, exclude, nodeNext, logger } = options;
|
|
417
|
+
logger.step(
|
|
418
|
+
`layering the external-consumer type libraries (${additionalTypeLibraries.join(", ")})`
|
|
419
|
+
);
|
|
420
|
+
const configPath = path5.join(siteDir, "tsconfig.json");
|
|
421
|
+
const config = JSON.parse(
|
|
422
|
+
fs4.readFileSync(configPath, "utf8")
|
|
423
|
+
);
|
|
424
|
+
const types = new Set(config.compilerOptions?.types ?? []);
|
|
425
|
+
types.add("node");
|
|
426
|
+
for (const library of additionalTypeLibraries) types.add(library);
|
|
427
|
+
config.compilerOptions = { ...config.compilerOptions, types: [...types] };
|
|
428
|
+
if (exclude !== void 0) config.exclude = [...exclude];
|
|
429
|
+
fs4.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}
|
|
430
|
+
`);
|
|
431
|
+
if (nodeNext) {
|
|
432
|
+
const nodeNextConfigPath = path5.join(siteDir, nodeNext.config);
|
|
433
|
+
const nodeNextConfig = JSON.parse(
|
|
434
|
+
fs4.readFileSync(nodeNextConfigPath, "utf8")
|
|
435
|
+
);
|
|
436
|
+
nodeNextConfig.include = [...nodeNext.include];
|
|
437
|
+
fs4.writeFileSync(
|
|
438
|
+
nodeNextConfigPath,
|
|
439
|
+
`${JSON.stringify(nodeNextConfig, null, 2)}
|
|
440
|
+
`
|
|
441
|
+
);
|
|
442
|
+
}
|
|
443
|
+
const written = JSON.parse(
|
|
444
|
+
fs4.readFileSync(configPath, "utf8")
|
|
445
|
+
);
|
|
446
|
+
const effective = new Set(written.compilerOptions?.types ?? []);
|
|
447
|
+
for (const library of additionalTypeLibraries) {
|
|
448
|
+
if (!effective.has(library)) {
|
|
449
|
+
logger.fail(
|
|
450
|
+
`external-consumer tsconfig is missing the ${library} type library`
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
const label = path5.relative(path5.dirname(siteDir), configPath).split(path5.sep).join("/");
|
|
455
|
+
console.log(` ${label} types: ${[...effective].join(", ")}`);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// e2e/run.ts
|
|
459
|
+
async function runExternalSiteE2E(config) {
|
|
460
|
+
const logger = config.logger ?? createLogger(config.stepLabel ?? "e2e");
|
|
461
|
+
const tempRoot = fs5.mkdtempSync(
|
|
462
|
+
path6.join(os.tmpdir(), config.tempPrefix ?? "riebeckite-e2e-")
|
|
463
|
+
);
|
|
464
|
+
const siteDir = path6.join(tempRoot, "site");
|
|
465
|
+
const vaultDir = path6.join(tempRoot, "vault");
|
|
466
|
+
const tarballDir = path6.join(tempRoot, "tarballs");
|
|
467
|
+
fs5.mkdirSync(tarballDir, { recursive: true });
|
|
468
|
+
logger.step(`temporary workspace: ${tempRoot}`);
|
|
469
|
+
try {
|
|
470
|
+
const packed = await packPackages(tarballDir, {
|
|
471
|
+
repoRoot: config.repoRoot,
|
|
472
|
+
packages: config.packages,
|
|
473
|
+
concurrency: config.concurrency,
|
|
474
|
+
logger
|
|
475
|
+
});
|
|
476
|
+
logger.step(
|
|
477
|
+
"copying fixture site and vault (content stays outside the site root)"
|
|
478
|
+
);
|
|
479
|
+
stageIsolatedSite({
|
|
480
|
+
fixtureSite: config.fixture.site,
|
|
481
|
+
fixtureVault: config.fixture.vault,
|
|
482
|
+
siteDir,
|
|
483
|
+
vaultDir
|
|
484
|
+
});
|
|
485
|
+
layerExternalConsumerTypes(siteDir, {
|
|
486
|
+
additionalTypeLibraries: config.additionalTypeLibraries ?? [],
|
|
487
|
+
exclude: config.bundlerTypeExclude,
|
|
488
|
+
nodeNext: config.nodeNextProject,
|
|
489
|
+
logger
|
|
490
|
+
});
|
|
491
|
+
writeFileDependencies(siteDir, packed);
|
|
492
|
+
logger.step("npm install (tarballs + normal registry dependencies)");
|
|
493
|
+
run("npm", ["install", "--no-audit", "--no-fund", "--loglevel=error"], {
|
|
494
|
+
cwd: siteDir
|
|
495
|
+
});
|
|
496
|
+
assertIsolatedInstall(siteDir, tempRoot, {
|
|
497
|
+
repoRoot: config.repoRoot,
|
|
498
|
+
requiredPaths: config.requiredInstallPaths,
|
|
499
|
+
logger
|
|
500
|
+
});
|
|
501
|
+
assertNoMonorepoEscapeHatches(siteDir, {
|
|
502
|
+
monorepoPackagesDirectory: config.monorepoPackagesDirectory,
|
|
503
|
+
logger
|
|
504
|
+
});
|
|
505
|
+
assertDeclaredDependencies(siteDir, {
|
|
506
|
+
dependencyCheckScript: config.dependencyCheckScript,
|
|
507
|
+
cwd: config.repoRoot,
|
|
508
|
+
logger
|
|
509
|
+
});
|
|
510
|
+
assertPublishedArtifacts(siteDir, {
|
|
511
|
+
repoRoot: config.repoRoot,
|
|
512
|
+
scope: config.scope,
|
|
513
|
+
logger
|
|
514
|
+
});
|
|
515
|
+
const workspace = {
|
|
516
|
+
repoRoot: config.repoRoot,
|
|
517
|
+
tempRoot,
|
|
518
|
+
siteDir,
|
|
519
|
+
vaultDir,
|
|
520
|
+
tarballDir,
|
|
521
|
+
packed
|
|
522
|
+
};
|
|
523
|
+
for (const check of config.siteChecks ?? []) check(workspace, logger);
|
|
524
|
+
const cliCwd = config.cliWorkingDirectory ? path6.join(siteDir, config.cliWorkingDirectory) : siteDir;
|
|
525
|
+
const cliEntry = config.resolveCliEntry(siteDir);
|
|
526
|
+
for (const command of config.cliCommands) {
|
|
527
|
+
const result = runCli(command.args, {
|
|
528
|
+
cliEntry,
|
|
529
|
+
cwd: cliCwd,
|
|
530
|
+
cliName: config.cliName,
|
|
531
|
+
logger
|
|
532
|
+
});
|
|
533
|
+
command.assertOutput?.(cliText(result));
|
|
534
|
+
}
|
|
535
|
+
config.assertions.buildOutput(siteDir, vaultDir);
|
|
536
|
+
for (const project of config.typecheckProjects ?? []) {
|
|
537
|
+
runTypecheck(siteDir, project, logger);
|
|
538
|
+
}
|
|
539
|
+
await config.afterSiteChecks?.(workspace);
|
|
540
|
+
logger.step(
|
|
541
|
+
`PASS: ${config.passMessage ?? "external site built from published artifacts"}`
|
|
542
|
+
);
|
|
543
|
+
} finally {
|
|
544
|
+
if (process.env[config.keepEnv]) {
|
|
545
|
+
logger.step(`kept workspace: ${tempRoot}`);
|
|
546
|
+
} else {
|
|
547
|
+
fs5.rmSync(tempRoot, { recursive: true, force: true });
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
export {
|
|
552
|
+
MAX_BUFFER,
|
|
553
|
+
assertDeclaredDependencies,
|
|
554
|
+
assertIsolatedInstall,
|
|
555
|
+
assertNoMonorepoEscapeHatches,
|
|
556
|
+
assertPublishedArtifacts,
|
|
557
|
+
cliText,
|
|
558
|
+
createLogger,
|
|
559
|
+
escapeRegExp,
|
|
560
|
+
extractScriptPayloads,
|
|
561
|
+
formatBytes,
|
|
562
|
+
layerExternalConsumerTypes,
|
|
563
|
+
packPackages,
|
|
564
|
+
quote,
|
|
565
|
+
run,
|
|
566
|
+
runAsync,
|
|
567
|
+
runCli,
|
|
568
|
+
runExternalSiteE2E,
|
|
569
|
+
runTypecheck,
|
|
570
|
+
stageIsolatedSite,
|
|
571
|
+
walkFiles,
|
|
572
|
+
writeFileDependencies
|
|
573
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal console logger shared by the external-site engine.
|
|
3
|
+
*
|
|
4
|
+
* The engine is published as the shared test package's e2e entry point and
|
|
5
|
+
* must not assume any repository layout, so every phase receives a logger
|
|
6
|
+
* instead of reading a module-level one. `createLogger` reproduces the `\n[label] message` format
|
|
7
|
+
* the repo-local driver has always printed.
|
|
8
|
+
*/
|
|
9
|
+
export interface Logger {
|
|
10
|
+
/** Print a phase heading. */
|
|
11
|
+
step(message: string): void;
|
|
12
|
+
/** Abort the run with `message`. */
|
|
13
|
+
fail(message: string): never;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Create a logger whose `step` lines are prefixed with `\n[label] `.
|
|
17
|
+
*/
|
|
18
|
+
export declare function createLogger(label?: string): Logger;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { Logger } from "./logger.js";
|
|
2
|
+
/** A package that should be packed and injected into the consumer project. */
|
|
3
|
+
export interface PackageSpec {
|
|
4
|
+
/** Package directory, relative to the repository root. */
|
|
5
|
+
directory: string;
|
|
6
|
+
/** Published package name, e.g. `@scope/name` or `create-thing`. */
|
|
7
|
+
name: string;
|
|
8
|
+
}
|
|
9
|
+
/** A tarball produced for one package. */
|
|
10
|
+
export interface PackedPackage {
|
|
11
|
+
tarball: string;
|
|
12
|
+
version: string;
|
|
13
|
+
}
|
|
14
|
+
/** Packed packages keyed by package name. */
|
|
15
|
+
export type PackedPackages = Map<string, PackedPackage>;
|
|
16
|
+
export interface PackPackagesOptions {
|
|
17
|
+
/** Absolute repository root the package directories are resolved against. */
|
|
18
|
+
repoRoot: string;
|
|
19
|
+
/** Packages to pack. */
|
|
20
|
+
packages: readonly PackageSpec[];
|
|
21
|
+
/** Maximum number of `pnpm pack` processes in flight. Defaults to 4. */
|
|
22
|
+
concurrency?: number;
|
|
23
|
+
logger: Logger;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Pack every package with `pnpm pack` into `tarballDir`, using a
|
|
27
|
+
* limited-concurrency pool. Returns a map from package name to its tarball and
|
|
28
|
+
* version. The first failure rejects the returned promise.
|
|
29
|
+
*/
|
|
30
|
+
export declare function packPackages(tarballDir: string, options: PackPackagesOptions): Promise<PackedPackages>;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/** Upper bound on captured stdout/stderr per command. */
|
|
2
|
+
export declare const MAX_BUFFER: number;
|
|
3
|
+
/**
|
|
4
|
+
* Quote a value for the shell command line. Values containing whitespace or a
|
|
5
|
+
* double quote are wrapped and their quotes escaped; everything else is passed
|
|
6
|
+
* through unchanged.
|
|
7
|
+
*/
|
|
8
|
+
export declare function quote(value: unknown): string;
|
|
9
|
+
export interface RunOptions {
|
|
10
|
+
/** Working directory. Defaults to `process.cwd()`. */
|
|
11
|
+
cwd?: string;
|
|
12
|
+
/** Extra environment variables merged over `process.env`. */
|
|
13
|
+
env?: Record<string, string | undefined>;
|
|
14
|
+
/** Return the result instead of throwing on a non-zero exit. */
|
|
15
|
+
allowFailure?: boolean;
|
|
16
|
+
}
|
|
17
|
+
export interface RunResult {
|
|
18
|
+
/** Process exit status, or `null` when the process was signalled. */
|
|
19
|
+
status: number | null;
|
|
20
|
+
stdout: string;
|
|
21
|
+
stderr: string;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Run a command through the shell, synchronously. Throws on spawn errors and,
|
|
25
|
+
* unless `allowFailure` is set, on a non-zero exit status. The command and its
|
|
26
|
+
* arguments are quoted and joined into a single shell line.
|
|
27
|
+
*/
|
|
28
|
+
export declare function run(command: string, args: readonly string[], options?: RunOptions): RunResult;
|
|
29
|
+
/**
|
|
30
|
+
* Asynchronous counterpart of {@link run}, suitable for parallel jobs. Mirrors
|
|
31
|
+
* the synchronous semantics: shell execution, the same buffer cap, and a
|
|
32
|
+
* rejection on spawn error or (unless `allowFailure` is set) non-zero exit.
|
|
33
|
+
*/
|
|
34
|
+
export declare function runAsync(command: string, args: readonly string[], options?: RunOptions): Promise<RunResult>;
|