@bananacool467/authtics-host 0.2.6 → 0.2.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/.agents/memory/MEMORY.md +1 -0
- package/.agents/memory/node-child-process-tests.md +10 -0
- package/README.md +168 -6
- package/assets/dev_panel.jpg +0 -0
- package/assets/result.jpg +0 -0
- package/bin/hostjs.js +62 -0
- package/bin/logger.js +29 -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 +408 -58
- package/dist/config.d.ts +5 -0
- package/dist/config.js +15 -1
- package/dist/devPanel.d.ts +11 -0
- package/dist/devPanel.js +358 -56
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/renderEngine.d.ts +4 -3
- package/dist/renderEngine.js +265 -68
- 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 +9 -2
- 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,175 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
const HOSTJS_DIR = ".hostjs";
|
|
4
|
+
function writeJson(filePath, value) {
|
|
5
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
6
|
+
fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`, "utf-8");
|
|
7
|
+
}
|
|
8
|
+
function readJson(filePath) {
|
|
9
|
+
try {
|
|
10
|
+
return JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return {};
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
function toProjectPath(root, filePath) {
|
|
17
|
+
return path.relative(root, filePath).split(path.sep).join("/");
|
|
18
|
+
}
|
|
19
|
+
function walkFiles(dir) {
|
|
20
|
+
if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory())
|
|
21
|
+
return [];
|
|
22
|
+
const result = [];
|
|
23
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
24
|
+
if (entry.name === "node_modules" || entry.name === ".git" || entry.name === ".hostjs")
|
|
25
|
+
continue;
|
|
26
|
+
const fullPath = path.join(dir, entry.name);
|
|
27
|
+
if (entry.isDirectory())
|
|
28
|
+
result.push(...walkFiles(fullPath));
|
|
29
|
+
else if (entry.isFile())
|
|
30
|
+
result.push(fullPath);
|
|
31
|
+
}
|
|
32
|
+
return result;
|
|
33
|
+
}
|
|
34
|
+
function routePathFromFile(relativePath) {
|
|
35
|
+
const withoutExtension = relativePath.replace(/\.(?:tsx?|jsx?|html)$/i, "");
|
|
36
|
+
if (withoutExtension.toLowerCase() === "index")
|
|
37
|
+
return "/";
|
|
38
|
+
return `/${withoutExtension.split(path.sep).join("/")}`;
|
|
39
|
+
}
|
|
40
|
+
function collectRoutes(projectRoot, pagesDir, publicDir) {
|
|
41
|
+
const routes = [];
|
|
42
|
+
if (pagesDir) {
|
|
43
|
+
for (const filePath of walkFiles(pagesDir)) {
|
|
44
|
+
const relativePath = path.relative(pagesDir, filePath);
|
|
45
|
+
const normalized = relativePath.split(path.sep).join("/");
|
|
46
|
+
const lower = normalized.toLowerCase();
|
|
47
|
+
if (lower === "_template.html" || lower === "_paused.tsx")
|
|
48
|
+
continue;
|
|
49
|
+
if (/^api\/.+\.(?:ts|tsx)$/i.test(normalized)) {
|
|
50
|
+
const pathname = `/${normalized.replace(/\.(?:tsx?)$/i, "")}`;
|
|
51
|
+
routes.push({ pathname, kind: "api", source: toProjectPath(projectRoot, filePath) });
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (/\.(?:tsx|html)$/i.test(normalized)) {
|
|
55
|
+
routes.push({
|
|
56
|
+
pathname: routePathFromFile(relativePath),
|
|
57
|
+
kind: "page",
|
|
58
|
+
source: toProjectPath(projectRoot, filePath),
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (publicDir) {
|
|
64
|
+
for (const filePath of walkFiles(publicDir)) {
|
|
65
|
+
routes.push({
|
|
66
|
+
pathname: `/${path.relative(publicDir, filePath).split(path.sep).join("/")}`,
|
|
67
|
+
kind: "static",
|
|
68
|
+
source: toProjectPath(projectRoot, filePath),
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
const unique = new Map();
|
|
73
|
+
for (const route of routes)
|
|
74
|
+
unique.set(`${route.kind}:${route.pathname}`, route);
|
|
75
|
+
return [...unique.values()].sort((a, b) => a.pathname.localeCompare(b.pathname) || a.kind.localeCompare(b.kind));
|
|
76
|
+
}
|
|
77
|
+
function typeUnion(values) {
|
|
78
|
+
const unique = [...new Set(values)].sort();
|
|
79
|
+
return unique.length ? unique.map((value) => JSON.stringify(value)).join(" | ") : "never";
|
|
80
|
+
}
|
|
81
|
+
function resolveDirectories(root, config, pagesDir, publicDir) {
|
|
82
|
+
return {
|
|
83
|
+
pagesDir: config.autoPage === true
|
|
84
|
+
? pagesDir ?? path.join(root, config.autoPageDir || "")
|
|
85
|
+
: undefined,
|
|
86
|
+
publicDir: config.autoFile === true
|
|
87
|
+
? publicDir ?? path.join(root, config.autoFileDir || "")
|
|
88
|
+
: undefined,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
function writeRouteManifestAndTypes(root, outputDir, config, pagesDir, publicDir) {
|
|
92
|
+
const directories = resolveDirectories(root, config, pagesDir, publicDir);
|
|
93
|
+
const routes = collectRoutes(root, directories.pagesDir, directories.publicDir);
|
|
94
|
+
writeJson(path.join(outputDir, "server", "routes-manifest.json"), {
|
|
95
|
+
version: 1,
|
|
96
|
+
generatedBy: "HostJS",
|
|
97
|
+
buildStyle: "super",
|
|
98
|
+
pagesDirectory: directories.pagesDir ? toProjectPath(root, directories.pagesDir) : null,
|
|
99
|
+
publicDirectory: directories.publicDir ? toProjectPath(root, directories.publicDir) : null,
|
|
100
|
+
routes,
|
|
101
|
+
});
|
|
102
|
+
const pageRoutes = routes.filter((route) => route.kind === "page").map((route) => route.pathname);
|
|
103
|
+
const apiRoutes = routes.filter((route) => route.kind === "api").map((route) => route.pathname);
|
|
104
|
+
const staticRoutes = routes.filter((route) => route.kind === "static").map((route) => route.pathname);
|
|
105
|
+
const routeDeclarations = `// Generated by HostJS buildStyle: "super". Do not edit.
|
|
106
|
+
export type HostJSPageRoute = ${typeUnion(pageRoutes)};
|
|
107
|
+
export type HostJSApiRoute = ${typeUnion(apiRoutes)};
|
|
108
|
+
export type HostJSStaticRoute = ${typeUnion(staticRoutes)};
|
|
109
|
+
`;
|
|
110
|
+
fs.mkdirSync(path.join(outputDir, "types", "app"), { recursive: true });
|
|
111
|
+
fs.writeFileSync(path.join(outputDir, "types", "routes.d.ts"), routeDeclarations, "utf-8");
|
|
112
|
+
fs.writeFileSync(path.join(outputDir, "types", "validator.ts"), `// Generated by HostJS buildStyle: "super". Do not edit.
|
|
113
|
+
import type { HostJSPageRoute, HostJSApiRoute, HostJSStaticRoute } from "./routes.js";
|
|
114
|
+
|
|
115
|
+
const pageRoutes = ${JSON.stringify([...new Set(pageRoutes)].sort())} as const;
|
|
116
|
+
const apiRoutes = ${JSON.stringify([...new Set(apiRoutes)].sort())} as const;
|
|
117
|
+
const staticRoutes = ${JSON.stringify([...new Set(staticRoutes)].sort())} as const;
|
|
118
|
+
|
|
119
|
+
export function isHostJSPageRoute(value: string): value is HostJSPageRoute {
|
|
120
|
+
return (pageRoutes as readonly string[]).includes(value);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function isHostJSApiRoute(value: string): value is HostJSApiRoute {
|
|
124
|
+
return (apiRoutes as readonly string[]).includes(value);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function isHostJSStaticRoute(value: string): value is HostJSStaticRoute {
|
|
128
|
+
return (staticRoutes as readonly string[]).includes(value);
|
|
129
|
+
}
|
|
130
|
+
`, "utf-8");
|
|
131
|
+
fs.writeFileSync(path.join(outputDir, "types", "app", "routes.d.ts"), `// Generated by HostJS buildStyle: "super". Do not edit.
|
|
132
|
+
export type { HostJSPageRoute, HostJSApiRoute, HostJSStaticRoute } from "../routes.js";
|
|
133
|
+
`, "utf-8");
|
|
134
|
+
}
|
|
135
|
+
export function refreshSuperRouteManifest(projectRoot, config, pagesDir, publicDir) {
|
|
136
|
+
const root = path.resolve(projectRoot);
|
|
137
|
+
const outputDir = path.join(root, HOSTJS_DIR);
|
|
138
|
+
if (!fs.existsSync(outputDir))
|
|
139
|
+
return;
|
|
140
|
+
writeRouteManifestAndTypes(root, outputDir, config, pagesDir, publicDir);
|
|
141
|
+
}
|
|
142
|
+
export function initializeSuperBuildOutput(projectRoot, config, pagesDir, publicDir) {
|
|
143
|
+
const root = path.resolve(projectRoot);
|
|
144
|
+
const outputDir = path.join(root, HOSTJS_DIR);
|
|
145
|
+
const projectPackage = readJson(path.join(root, "package.json"));
|
|
146
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
147
|
+
writeJson(path.join(outputDir, "package.json"), {
|
|
148
|
+
schemaVersion: 1,
|
|
149
|
+
framework: "@bananacool467/authtics-host",
|
|
150
|
+
frameworkName: "Authtics Host",
|
|
151
|
+
shortName: "HostJS",
|
|
152
|
+
buildStyle: "super",
|
|
153
|
+
project: {
|
|
154
|
+
name: typeof projectPackage.name === "string" ? projectPackage.name : path.basename(root),
|
|
155
|
+
version: typeof projectPackage.version === "string" ? projectPackage.version : null,
|
|
156
|
+
},
|
|
157
|
+
});
|
|
158
|
+
writeJson(path.join(outputDir, "diagnostics", "framework.json"), {
|
|
159
|
+
name: "Authtics Host",
|
|
160
|
+
shortName: "HostJS",
|
|
161
|
+
buildStyle: "super",
|
|
162
|
+
schemaVersion: 1,
|
|
163
|
+
});
|
|
164
|
+
writeJson(path.join(outputDir, "diagnostics", "build-diagnostics.json"), {
|
|
165
|
+
version: 1,
|
|
166
|
+
diagnostics: [],
|
|
167
|
+
});
|
|
168
|
+
writeJson(path.join(outputDir, "server", "middleware-manifest.json"), {
|
|
169
|
+
version: 1,
|
|
170
|
+
middleware: [],
|
|
171
|
+
});
|
|
172
|
+
writeRouteManifestAndTypes(root, outputDir, config, pagesDir, publicDir);
|
|
173
|
+
fs.mkdirSync(path.join(outputDir, "trace"), { recursive: true });
|
|
174
|
+
fs.mkdirSync(path.join(outputDir, "trace-build"), { recursive: true });
|
|
175
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export default function Slope(): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
export default function Slope() {
|
|
3
|
+
return (_jsx("svg", { xmlns: "http://www.w3.org/2000/svg", xmlnsXlink: "http://www.w3.org/1999/xlink", id: "eAC8Baof2Sn1", viewBox: "0 0 300 300", shapeRendering: "geometricPrecision", textRendering: "geometricPrecision", children: _jsx("path", { d: "M38.988202,35.395066v224.168752h226.313907C186.013992,256.687591,95.751962,198.420051,74.383268,65.427243C71.454867,53.642201,60.354042,34.44468,38.988202,35.395066Z", transform: "matrix(1.306635 0 0 1.306641 -48.798185 -42.681458)", fill: "#fff", stroke: "#e6e3e3" }) }));
|
|
4
|
+
}
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bananacool467/authtics-host",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.8",
|
|
4
4
|
"description": "Authtics Host, A Next.js-inspired web framework for Node.js and React with TypeScript and JSX support.",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"sideEffects": false,
|
|
9
9
|
"bin": {
|
|
10
|
-
"
|
|
10
|
+
"hostjs": "./bin/hostjs.js"
|
|
11
11
|
},
|
|
12
12
|
"exports": {
|
|
13
13
|
".": "./dist/index.js"
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
"dev": "tsc --watch",
|
|
18
18
|
"prepublishOnly": "npm run build",
|
|
19
19
|
"test": "npm run build && node dist/index.js",
|
|
20
|
+
"test:source-maps": "npm run build && node --test tests/source-map-*.test.mjs",
|
|
20
21
|
"playgroundTest": "npm run build && cd playground && node --experimental-strip-types index.ts",
|
|
21
22
|
"playgroundTest2": "npm run build && cd playground2 && node --experimental-strip-types index.ts"
|
|
22
23
|
},
|
|
@@ -47,6 +48,7 @@
|
|
|
47
48
|
"author": "bananacool467",
|
|
48
49
|
"license": "MIT",
|
|
49
50
|
"dependencies": {
|
|
51
|
+
"@jridgewell/trace-mapping": "^0.3.31",
|
|
50
52
|
"esbuild": "^0.28.2",
|
|
51
53
|
"jiti": "^2.6.1",
|
|
52
54
|
"lucide-react": "^0.577.0",
|
|
@@ -58,6 +60,7 @@
|
|
|
58
60
|
"react-dom": "^19.2.4"
|
|
59
61
|
},
|
|
60
62
|
"devDependencies": {
|
|
63
|
+
"@bananacool467/ui-tools": "^0.2.2-beta",
|
|
61
64
|
"@babel/core": "^7.29.0",
|
|
62
65
|
"@babel/preset-env": "^7.29.0",
|
|
63
66
|
"@babel/preset-react": "^7.28.5",
|
|
@@ -70,5 +73,9 @@
|
|
|
70
73
|
"@types/react-dom": "^19.2.3",
|
|
71
74
|
"@types/react-syntax-highlighter": "^15.5.13",
|
|
72
75
|
"@types/ws": "^8.18.1"
|
|
76
|
+
},
|
|
77
|
+
"repository": {
|
|
78
|
+
"type": "git",
|
|
79
|
+
"url": "git+https://github.com/bananakitssu/authtics-host"
|
|
73
80
|
}
|
|
74
81
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export default function Slope() {
|
|
2
|
+
return (
|
|
3
|
+
<svg
|
|
4
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
5
|
+
xmlnsXlink="http://www.w3.org/1999/xlink"
|
|
6
|
+
id="eAC8Baof2Sn1"
|
|
7
|
+
viewBox="0 0 300 300"
|
|
8
|
+
shapeRendering="geometricPrecision"
|
|
9
|
+
textRendering="geometricPrecision"
|
|
10
|
+
>
|
|
11
|
+
<path
|
|
12
|
+
d="M38.988202,35.395066v224.168752h226.313907C186.013992,256.687591,95.751962,198.420051,74.383268,65.427243C71.454867,53.642201,60.354042,34.44468,38.988202,35.395066Z"
|
|
13
|
+
transform="matrix(1.306635 0 0 1.306641 -48.798185 -42.681458)"
|
|
14
|
+
fill="#fff"
|
|
15
|
+
stroke="#e6e3e3"
|
|
16
|
+
/>
|
|
17
|
+
</svg>
|
|
18
|
+
);
|
|
19
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
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
|
+
|
|
10
|
+
const projectRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
11
|
+
const cliPath = path.join(projectRoot, "dist", "cli.js");
|
|
12
|
+
const allowedDat = "source-map-test-dat";
|
|
13
|
+
const sourceMarker = "source-map-source-content-test-marker";
|
|
14
|
+
|
|
15
|
+
function childHasExited(child) {
|
|
16
|
+
return child.exitCode !== null || child.signalCode !== null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async function reservePort() {
|
|
20
|
+
const server = net.createServer();
|
|
21
|
+
server.listen(0, "127.0.0.1");
|
|
22
|
+
await once(server, "listening");
|
|
23
|
+
const { port } = server.address();
|
|
24
|
+
await new Promise((resolve, reject) => {
|
|
25
|
+
server.close((error) => (error ? reject(error) : resolve()));
|
|
26
|
+
});
|
|
27
|
+
return port;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function startFixtureServer(dat) {
|
|
31
|
+
const fixtureRoot = await fs.mkdtemp(path.join(projectRoot, ".source-map-access-"));
|
|
32
|
+
const pagesDir = path.join(fixtureRoot, "pages");
|
|
33
|
+
const port = await reservePort();
|
|
34
|
+
await fs.mkdir(pagesDir);
|
|
35
|
+
await fs.writeFile(
|
|
36
|
+
path.join(fixtureRoot, "authtics_host.config.ts"),
|
|
37
|
+
`export default {
|
|
38
|
+
port: ${port},
|
|
39
|
+
renderType: "csr",
|
|
40
|
+
devMode: true,
|
|
41
|
+
autoPage: true,
|
|
42
|
+
autoPageDir: "pages",
|
|
43
|
+
autoFile: false,
|
|
44
|
+
dat: ${dat === undefined ? "undefined" : JSON.stringify(dat)}
|
|
45
|
+
};\n`,
|
|
46
|
+
);
|
|
47
|
+
await fs.writeFile(
|
|
48
|
+
path.join(pagesDir, "index.tsx"),
|
|
49
|
+
`export default function Page() {
|
|
50
|
+
return <main>${sourceMarker}</main>;
|
|
51
|
+
}\n`,
|
|
52
|
+
);
|
|
53
|
+
const serverEntryPath = path.join(fixtureRoot, "server.mjs");
|
|
54
|
+
await fs.writeFile(
|
|
55
|
+
serverEntryPath,
|
|
56
|
+
`import App from ${JSON.stringify(pathToFileURL(cliPath).href)};\nnew App();\n`,
|
|
57
|
+
);
|
|
58
|
+
const child = spawn(process.execPath, [serverEntryPath], {
|
|
59
|
+
cwd: fixtureRoot,
|
|
60
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
let output = "";
|
|
64
|
+
child.stdout.setEncoding("utf8").on("data", (chunk) => (output += chunk));
|
|
65
|
+
child.stderr.setEncoding("utf8").on("data", (chunk) => (output += chunk));
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
const deadline = Date.now() + 20_000;
|
|
69
|
+
let lastFailure = "Server did not become ready.";
|
|
70
|
+
while (Date.now() < deadline) {
|
|
71
|
+
if (childHasExited(child)) {
|
|
72
|
+
throw new Error(
|
|
73
|
+
`Server exited with code ${child.exitCode} and signal ${child.signalCode}.\n${output}`,
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
const response = await fetch(`http://127.0.0.1:${port}/`, {
|
|
79
|
+
headers: dat === undefined ? {} : { cookie: `dat=${dat}` },
|
|
80
|
+
});
|
|
81
|
+
const html = await response.text();
|
|
82
|
+
if (response.ok) {
|
|
83
|
+
const scriptUrl = html.match(/<script src="([^"]+\.js)"><\/script>/)?.[1];
|
|
84
|
+
assert.ok(scriptUrl, `Rendered page did not include its JavaScript bundle.\n${html}`);
|
|
85
|
+
return {
|
|
86
|
+
baseUrl: `http://127.0.0.1:${port}`,
|
|
87
|
+
child,
|
|
88
|
+
fixtureRoot,
|
|
89
|
+
mapUrl: `${scriptUrl}.map`,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
lastFailure = `Page returned HTTP ${response.status}:\n${html}`;
|
|
93
|
+
} catch (error) {
|
|
94
|
+
lastFailure = error instanceof Error ? error.message : String(error);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
throw new Error(`${lastFailure}\n${output}`);
|
|
101
|
+
} catch (error) {
|
|
102
|
+
child.kill("SIGKILL");
|
|
103
|
+
await fs.rm(fixtureRoot, { recursive: true, force: true });
|
|
104
|
+
throw error;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function stopFixtureServer(fixture) {
|
|
109
|
+
if (!childHasExited(fixture.child)) {
|
|
110
|
+
const exited = once(fixture.child, "exit");
|
|
111
|
+
fixture.child.kill("SIGTERM");
|
|
112
|
+
const stopped = await Promise.race([
|
|
113
|
+
exited.then(() => true),
|
|
114
|
+
new Promise((resolve) => setTimeout(() => resolve(false), 3_000)),
|
|
115
|
+
]);
|
|
116
|
+
if (!stopped && !childHasExited(fixture.child)) {
|
|
117
|
+
const forceExit = once(fixture.child, "exit");
|
|
118
|
+
fixture.child.kill("SIGKILL");
|
|
119
|
+
await forceExit;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
await fs.rm(fixture.fixtureRoot, { recursive: true, force: true });
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function assertValidSourceMap(response) {
|
|
126
|
+
assert.equal(response.status, 200);
|
|
127
|
+
assert.match(response.headers.get("content-type") ?? "", /application\/json/i);
|
|
128
|
+
const sourceMap = await response.json();
|
|
129
|
+
assert.equal(sourceMap.version, 3);
|
|
130
|
+
assert.ok(Array.isArray(sourceMap.sourcesContent));
|
|
131
|
+
assert.ok(
|
|
132
|
+
sourceMap.sourcesContent.some((content) => content?.includes(sourceMarker)),
|
|
133
|
+
"The source map should contain the original page source for this regression test.",
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
test("DAT-protected source maps deny missing and incorrect tokens but allow the configured DAT", async () => {
|
|
138
|
+
const fixture = await startFixtureServer(allowedDat);
|
|
139
|
+
try {
|
|
140
|
+
const missingDatResponse = await fetch(`${fixture.baseUrl}${fixture.mapUrl}`);
|
|
141
|
+
assert.equal(missingDatResponse.status, 404);
|
|
142
|
+
assert.equal(await missingDatResponse.text(), "");
|
|
143
|
+
|
|
144
|
+
const incorrectDatResponse = await fetch(`${fixture.baseUrl}${fixture.mapUrl}`, {
|
|
145
|
+
headers: { cookie: "dat=wrong-source-map-test-dat" },
|
|
146
|
+
});
|
|
147
|
+
assert.equal(incorrectDatResponse.status, 404);
|
|
148
|
+
assert.equal(await incorrectDatResponse.text(), "");
|
|
149
|
+
|
|
150
|
+
const matchingDatResponse = await fetch(`${fixture.baseUrl}${fixture.mapUrl}`, {
|
|
151
|
+
headers: { cookie: `dat=${allowedDat}` },
|
|
152
|
+
});
|
|
153
|
+
await assertValidSourceMap(matchingDatResponse);
|
|
154
|
+
} finally {
|
|
155
|
+
await stopFixtureServer(fixture);
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test("unrestricted projects can load valid external source maps without a DAT", async () => {
|
|
160
|
+
const fixture = await startFixtureServer(undefined);
|
|
161
|
+
try {
|
|
162
|
+
const response = await fetch(`${fixture.baseUrl}${fixture.mapUrl}`);
|
|
163
|
+
await assertValidSourceMap(response);
|
|
164
|
+
} finally {
|
|
165
|
+
await stopFixtureServer(fixture);
|
|
166
|
+
}
|
|
167
|
+
});
|
|
@@ -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
|
+
}
|