@bananacool467/authtics-host 0.2.5 → 0.2.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/.agents/memory/MEMORY.md +1 -0
- package/.agents/memory/node-child-process-tests.md +10 -0
- package/README.md +145 -36
- package/assets/dev_panel.jpg +0 -0
- package/assets/result.jpg +0 -0
- package/dist/BrowserAPIs.d.ts +10 -0
- package/dist/BrowserAPIs.js +53 -0
- package/dist/cli.d.ts +5 -3
- package/dist/cli.js +427 -67
- package/dist/config.d.ts +8 -1
- package/dist/config.js +20 -0
- package/dist/devPanel.d.ts +11 -0
- package/dist/devPanel.js +415 -59
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/renderEngine.d.ts +5 -1
- package/dist/renderEngine.js +429 -79
- package/dist/rendererTempFiles.d.ts +10 -1
- package/dist/rendererTempFiles.js +101 -17
- package/dist/superBuildOutput.d.ts +9 -0
- package/dist/superBuildOutput.js +175 -0
- package/dist/svg_assets/slope_top_left.d.ts +1 -0
- package/dist/svg_assets/slope_top_left.js +4 -0
- package/package.json +23 -11
- package/scripts/post-merge.sh +5 -0
- package/svg_assets/slope_top_left.tsx +19 -0
- package/tests/source-map-access.test.mjs +167 -0
- package/tests/source-map-location.test.mjs +237 -0
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { once } from "node:events";
|
|
4
|
+
import fs from "node:fs/promises";
|
|
5
|
+
import net from "node:net";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
8
|
+
import test from "node:test";
|
|
9
|
+
import { mapGeneratedLocation } from "../dist/devPanel.js";
|
|
10
|
+
|
|
11
|
+
const projectRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
12
|
+
const cliPath = path.join(projectRoot, "dist", "cli.js");
|
|
13
|
+
const errorMarker = "cached-tsx-source-map-error";
|
|
14
|
+
const pageSource = `export default function Page() {
|
|
15
|
+
return (
|
|
16
|
+
<main>
|
|
17
|
+
{(() => {
|
|
18
|
+
throw new Error("${errorMarker}");
|
|
19
|
+
})()}
|
|
20
|
+
</main>
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
`;
|
|
24
|
+
|
|
25
|
+
function childHasExited(child) {
|
|
26
|
+
return child.exitCode !== null || child.signalCode !== null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function reservePort() {
|
|
30
|
+
const server = net.createServer();
|
|
31
|
+
server.listen(0, "127.0.0.1");
|
|
32
|
+
await once(server, "listening");
|
|
33
|
+
const { port } = server.address();
|
|
34
|
+
await new Promise((resolve, reject) => {
|
|
35
|
+
server.close((error) => (error ? reject(error) : resolve()));
|
|
36
|
+
});
|
|
37
|
+
return port;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function createFixture(buildStyle) {
|
|
41
|
+
const fixtureRoot = await fs.mkdtemp(path.join(projectRoot, ".source-map-location-"));
|
|
42
|
+
const pagesDir = path.join(fixtureRoot, "pages");
|
|
43
|
+
const port = await reservePort();
|
|
44
|
+
await fs.mkdir(pagesDir);
|
|
45
|
+
await fs.writeFile(path.join(pagesDir, "index.tsx"), pageSource);
|
|
46
|
+
await fs.writeFile(
|
|
47
|
+
path.join(fixtureRoot, "authtics_host.config.ts"),
|
|
48
|
+
`export default {
|
|
49
|
+
port: ${port},
|
|
50
|
+
renderType: "csr",
|
|
51
|
+
devMode: true,
|
|
52
|
+
autoPage: true,
|
|
53
|
+
autoPageDir: "pages",
|
|
54
|
+
autoFile: false,
|
|
55
|
+
buildStyle: ${JSON.stringify(buildStyle)}
|
|
56
|
+
};\n`,
|
|
57
|
+
);
|
|
58
|
+
const serverEntryPath = path.join(fixtureRoot, "server.mjs");
|
|
59
|
+
await fs.writeFile(
|
|
60
|
+
serverEntryPath,
|
|
61
|
+
`import App from ${JSON.stringify(pathToFileURL(cliPath).href)};\nnew App();\n`,
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
return { fixtureRoot, port, serverEntryPath };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function startFixtureServer(fixture) {
|
|
68
|
+
const child = spawn(process.execPath, [fixture.serverEntryPath], {
|
|
69
|
+
cwd: fixture.fixtureRoot,
|
|
70
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
71
|
+
});
|
|
72
|
+
let output = "";
|
|
73
|
+
child.stdout.setEncoding("utf8").on("data", (chunk) => (output += chunk));
|
|
74
|
+
child.stderr.setEncoding("utf8").on("data", (chunk) => (output += chunk));
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
const baseUrl = `http://127.0.0.1:${fixture.port}`;
|
|
78
|
+
const deadline = Date.now() + 20_000;
|
|
79
|
+
let lastFailure = "Server did not become ready.";
|
|
80
|
+
while (Date.now() < deadline) {
|
|
81
|
+
if (childHasExited(child)) {
|
|
82
|
+
throw new Error(
|
|
83
|
+
`Server exited with code ${child.exitCode} and signal ${child.signalCode}.\n${output}`,
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
const response = await fetch(`${baseUrl}/`);
|
|
89
|
+
const html = await response.text();
|
|
90
|
+
if (response.ok) {
|
|
91
|
+
const scriptUrl = html.match(/<script src="([^"]+\.js)"><\/script>/)?.[1];
|
|
92
|
+
assert.ok(scriptUrl, `Rendered page did not include its JavaScript bundle.\n${html}`);
|
|
93
|
+
return { child, baseUrl, scriptUrl, output };
|
|
94
|
+
}
|
|
95
|
+
lastFailure = `Page returned HTTP ${response.status}:\n${html}`;
|
|
96
|
+
} catch (error) {
|
|
97
|
+
lastFailure = error instanceof Error ? error.message : String(error);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
throw new Error(`${lastFailure}\n${output}`);
|
|
104
|
+
} catch (error) {
|
|
105
|
+
await stopFixtureServer({ child });
|
|
106
|
+
throw error;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function stopFixtureServer(server) {
|
|
111
|
+
if (!childHasExited(server.child)) {
|
|
112
|
+
const exited = once(server.child, "exit");
|
|
113
|
+
server.child.kill("SIGTERM");
|
|
114
|
+
const stopped = await Promise.race([
|
|
115
|
+
exited.then(() => true),
|
|
116
|
+
new Promise((resolve) => setTimeout(() => resolve(false), 3_000)),
|
|
117
|
+
]);
|
|
118
|
+
if (!stopped && !childHasExited(server.child)) {
|
|
119
|
+
const forceExit = once(server.child, "exit");
|
|
120
|
+
server.child.kill("SIGKILL");
|
|
121
|
+
await forceExit;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async function findClientMapPath(fixtureRoot, buildStyle) {
|
|
127
|
+
const cacheRoot = buildStyle === "default"
|
|
128
|
+
? path.join(fixtureRoot, ".authtics_host", "cache")
|
|
129
|
+
: path.join(fixtureRoot, ".hostjs", "cache", "bundler", "client-production");
|
|
130
|
+
const mapFiles = [];
|
|
131
|
+
async function visit(directory) {
|
|
132
|
+
for (const entry of await fs.readdir(directory, { withFileTypes: true })) {
|
|
133
|
+
const entryPath = path.join(directory, entry.name);
|
|
134
|
+
if (entry.isDirectory()) await visit(entryPath);
|
|
135
|
+
else if (entry.name.endsWith(".js.map")) mapFiles.push(entryPath);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
await visit(cacheRoot);
|
|
139
|
+
assert.equal(mapFiles.length, 1, "Expected one cached client source map.");
|
|
140
|
+
return mapFiles[0];
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async function verifyBrowserErrorMapping(server, expectedSourceLine = 5) {
|
|
144
|
+
const scriptResponse = await fetch(new URL(server.scriptUrl, server.baseUrl));
|
|
145
|
+
assert.equal(scriptResponse.status, 200);
|
|
146
|
+
const generatedCode = await scriptResponse.text();
|
|
147
|
+
|
|
148
|
+
assert.match(
|
|
149
|
+
generatedCode,
|
|
150
|
+
/\/\/[#@]\s*sourceMappingURL=(?!data:)[^\s]+/,
|
|
151
|
+
"The generated JavaScript should reference an external source map.",
|
|
152
|
+
);
|
|
153
|
+
assert.doesNotMatch(
|
|
154
|
+
generatedCode,
|
|
155
|
+
/sourceMappingURL=data:|data:application\/json[^,\s]*;base64,/i,
|
|
156
|
+
"The generated JavaScript must not embed its source map as base64.",
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
const markerIndex = generatedCode.indexOf(errorMarker);
|
|
160
|
+
assert.notEqual(markerIndex, -1, "The browser bundle should contain the deliberate error.");
|
|
161
|
+
const generatedThrowIndex = generatedCode.lastIndexOf("throw", markerIndex);
|
|
162
|
+
assert.notEqual(generatedThrowIndex, -1, "Could not locate the generated throw statement.");
|
|
163
|
+
const prefix = generatedCode.slice(0, generatedThrowIndex);
|
|
164
|
+
const generatedLine = prefix.split("\n").length;
|
|
165
|
+
const generatedColumn = generatedThrowIndex - prefix.lastIndexOf("\n");
|
|
166
|
+
|
|
167
|
+
const mappedLocation = await mapGeneratedLocation({
|
|
168
|
+
file: scriptResponse.url,
|
|
169
|
+
line: generatedLine,
|
|
170
|
+
column: generatedColumn,
|
|
171
|
+
mapped: false,
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
assert.equal(mappedLocation.mapped, true, "The DevPanel should map the browser error.");
|
|
175
|
+
assert.match(mappedLocation.file, /pages\/index\.tsx$/);
|
|
176
|
+
assert.equal(mappedLocation.line, expectedSourceLine);
|
|
177
|
+
assert.equal(mappedLocation.column, 9);
|
|
178
|
+
assert.match(mappedLocation.sourceText, /throw new Error\("cached-tsx-source-map-error"\)/);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
for (const buildStyle of ["default", "super"]) {
|
|
182
|
+
test(`${buildStyle} build maps browser errors after a fresh build and a disk-cache hit`, async () => {
|
|
183
|
+
const fixture = await createFixture(buildStyle);
|
|
184
|
+
let server;
|
|
185
|
+
try {
|
|
186
|
+
server = await startFixtureServer(fixture);
|
|
187
|
+
await verifyBrowserErrorMapping(server);
|
|
188
|
+
const mapPath = await findClientMapPath(fixture.fixtureRoot, buildStyle);
|
|
189
|
+
const initialMapMtime = (await fs.stat(mapPath, { bigint: true })).mtimeNs;
|
|
190
|
+
|
|
191
|
+
// A new process has no in-memory asset routes; it must reuse the cached
|
|
192
|
+
// bundle and restore its map from disk without rebuilding either file.
|
|
193
|
+
await stopFixtureServer(server);
|
|
194
|
+
server = await startFixtureServer(fixture);
|
|
195
|
+
await verifyBrowserErrorMapping(server);
|
|
196
|
+
assert.equal(
|
|
197
|
+
(await fs.stat(mapPath, { bigint: true })).mtimeNs,
|
|
198
|
+
initialMapMtime,
|
|
199
|
+
"The cache-hit request should reuse the existing source map.",
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
// A missing sidecar must invalidate the otherwise-valid JavaScript
|
|
203
|
+
// cache and produce a matching map before the asset is served.
|
|
204
|
+
await stopFixtureServer(server);
|
|
205
|
+
await fs.unlink(mapPath);
|
|
206
|
+
server = await startFixtureServer(fixture);
|
|
207
|
+
await verifyBrowserErrorMapping(server);
|
|
208
|
+
const restoredMapMtime = (await fs.stat(mapPath, { bigint: true })).mtimeNs;
|
|
209
|
+
assert.notEqual(
|
|
210
|
+
restoredMapMtime,
|
|
211
|
+
initialMapMtime,
|
|
212
|
+
"A missing cached map should be rebuilt rather than silently reused.",
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
// Changing the TSX source must rebuild the bundle and map together so a
|
|
216
|
+
// valid JavaScript URL cannot keep pointing at an outdated map.
|
|
217
|
+
await stopFixtureServer(server);
|
|
218
|
+
const sourcePath = path.join(fixture.fixtureRoot, "pages", "index.tsx");
|
|
219
|
+
await fs.writeFile(
|
|
220
|
+
sourcePath,
|
|
221
|
+
pageSource.replace(" throw new Error", " // Shift the mapped error down one line.\n throw new Error"),
|
|
222
|
+
);
|
|
223
|
+
const future = new Date(Date.now() + 2_000);
|
|
224
|
+
await fs.utimes(sourcePath, future, future);
|
|
225
|
+
server = await startFixtureServer(fixture);
|
|
226
|
+
await verifyBrowserErrorMapping(server, 6);
|
|
227
|
+
assert.notEqual(
|
|
228
|
+
(await fs.stat(mapPath, { bigint: true })).mtimeNs,
|
|
229
|
+
restoredMapMtime,
|
|
230
|
+
"A changed TSX source should replace its cached source map.",
|
|
231
|
+
);
|
|
232
|
+
} finally {
|
|
233
|
+
if (server) await stopFixtureServer(server);
|
|
234
|
+
await fs.rm(fixture.fixtureRoot, { recursive: true, force: true });
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
}
|