fastscript 0.1.1 → 2.0.0
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/CHANGELOG.md +31 -2
- package/LICENSE +33 -21
- package/README.md +568 -59
- package/node_modules/@fastscript/core-private/BOUNDARY.json +15 -0
- package/node_modules/@fastscript/core-private/README.md +5 -0
- package/node_modules/@fastscript/core-private/package.json +34 -0
- package/node_modules/@fastscript/core-private/src/asset-optimizer.mjs +67 -0
- package/node_modules/@fastscript/core-private/src/audit-log.mjs +50 -0
- package/node_modules/@fastscript/core-private/src/auth-flows.mjs +29 -0
- package/node_modules/@fastscript/core-private/src/auth.mjs +115 -0
- package/node_modules/@fastscript/core-private/src/bench.mjs +45 -0
- package/node_modules/@fastscript/core-private/src/build.mjs +670 -0
- package/node_modules/@fastscript/core-private/src/cache.mjs +248 -0
- package/node_modules/@fastscript/core-private/src/check.mjs +22 -0
- package/node_modules/@fastscript/core-private/src/cli.mjs +95 -0
- package/node_modules/@fastscript/core-private/src/compat.mjs +128 -0
- package/node_modules/@fastscript/core-private/src/create.mjs +278 -0
- package/node_modules/@fastscript/core-private/src/csp.mjs +26 -0
- package/node_modules/@fastscript/core-private/src/db-cli.mjs +185 -0
- package/node_modules/@fastscript/core-private/src/db-postgres-collection.mjs +110 -0
- package/node_modules/@fastscript/core-private/src/db-postgres.mjs +40 -0
- package/node_modules/@fastscript/core-private/src/db.mjs +103 -0
- package/node_modules/@fastscript/core-private/src/deploy.mjs +662 -0
- package/node_modules/@fastscript/core-private/src/dev.mjs +5 -0
- package/node_modules/@fastscript/core-private/src/docs-search.mjs +35 -0
- package/node_modules/@fastscript/core-private/src/env.mjs +118 -0
- package/node_modules/@fastscript/core-private/src/export.mjs +83 -0
- package/node_modules/@fastscript/core-private/src/fs-diagnostics.mjs +70 -0
- package/node_modules/@fastscript/core-private/src/fs-error-codes.mjs +141 -0
- package/node_modules/@fastscript/core-private/src/fs-formatter.mjs +66 -0
- package/node_modules/@fastscript/core-private/src/fs-linter.mjs +274 -0
- package/node_modules/@fastscript/core-private/src/fs-normalize.mjs +91 -0
- package/node_modules/@fastscript/core-private/src/fs-parser.mjs +980 -0
- package/node_modules/@fastscript/core-private/src/generated/docs-search-index.mjs +3182 -0
- package/node_modules/@fastscript/core-private/src/i18n.mjs +25 -0
- package/node_modules/@fastscript/core-private/src/interop.mjs +16 -0
- package/node_modules/@fastscript/core-private/src/jobs.mjs +378 -0
- package/node_modules/@fastscript/core-private/src/logger.mjs +27 -0
- package/node_modules/@fastscript/core-private/src/metrics.mjs +45 -0
- package/node_modules/@fastscript/core-private/src/middleware.mjs +14 -0
- package/node_modules/@fastscript/core-private/src/migrate.mjs +81 -0
- package/node_modules/@fastscript/core-private/src/migration-wizard.mjs +16 -0
- package/node_modules/@fastscript/core-private/src/module-loader.mjs +46 -0
- package/node_modules/@fastscript/core-private/src/oauth-providers.mjs +103 -0
- package/node_modules/@fastscript/core-private/src/observability.mjs +21 -0
- package/node_modules/@fastscript/core-private/src/plugins.mjs +194 -0
- package/node_modules/@fastscript/core-private/src/retention.mjs +57 -0
- package/node_modules/@fastscript/core-private/src/routes.mjs +178 -0
- package/node_modules/@fastscript/core-private/src/scheduler.mjs +104 -0
- package/node_modules/@fastscript/core-private/src/security.mjs +233 -0
- package/node_modules/@fastscript/core-private/src/server-runtime.mjs +849 -0
- package/node_modules/@fastscript/core-private/src/serverless-handler.mjs +20 -0
- package/node_modules/@fastscript/core-private/src/session-policy.mjs +38 -0
- package/node_modules/@fastscript/core-private/src/start.mjs +10 -0
- package/node_modules/@fastscript/core-private/src/storage.mjs +155 -0
- package/node_modules/@fastscript/core-private/src/style-primitives.mjs +538 -0
- package/node_modules/@fastscript/core-private/src/style-system.mjs +461 -0
- package/node_modules/@fastscript/core-private/src/tenant.mjs +55 -0
- package/node_modules/@fastscript/core-private/src/typecheck.mjs +1464 -0
- package/node_modules/@fastscript/core-private/src/validate.mjs +22 -0
- package/node_modules/@fastscript/core-private/src/validation.mjs +88 -0
- package/node_modules/@fastscript/core-private/src/webhook.mjs +81 -0
- package/node_modules/@fastscript/core-private/src/worker.mjs +24 -0
- package/package.json +88 -8
- package/src/asset-optimizer.mjs +67 -0
- package/src/audit-log.mjs +50 -0
- package/src/auth.mjs +1 -115
- package/src/bench.mjs +20 -7
- package/src/build.mjs +1 -222
- package/src/cache.mjs +210 -20
- package/src/cli.mjs +29 -5
- package/src/compat.mjs +7 -1
- package/src/create.mjs +65 -11
- package/src/csp.mjs +26 -0
- package/src/db-cli.mjs +158 -18
- package/src/db-postgres-collection.mjs +110 -0
- package/src/deploy.mjs +1 -65
- package/src/docs-search.mjs +35 -0
- package/src/env.mjs +34 -5
- package/src/fs-diagnostics.mjs +70 -0
- package/src/fs-error-codes.mjs +126 -0
- package/src/fs-formatter.mjs +66 -0
- package/src/fs-linter.mjs +274 -0
- package/src/fs-normalize.mjs +17 -26
- package/src/fs-parser.mjs +1 -0
- package/src/generated/docs-search-index.mjs +3220 -0
- package/src/i18n.mjs +25 -0
- package/src/jobs.mjs +283 -32
- package/src/metrics.mjs +45 -0
- package/src/migration-wizard.mjs +16 -0
- package/src/module-loader.mjs +46 -0
- package/src/oauth-providers.mjs +103 -0
- package/src/plugins.mjs +194 -0
- package/src/retention.mjs +57 -0
- package/src/routes.mjs +178 -0
- package/src/scheduler.mjs +104 -0
- package/src/security.mjs +197 -19
- package/src/server-runtime.mjs +1 -339
- package/src/serverless-handler.mjs +20 -0
- package/src/session-policy.mjs +38 -0
- package/src/storage.mjs +1 -56
- package/src/style-system.mjs +461 -0
- package/src/tenant.mjs +55 -0
- package/src/typecheck.mjs +1 -0
- package/src/validate.mjs +5 -1
- package/src/validation.mjs +14 -5
- package/src/webhook.mjs +1 -71
- package/src/worker.mjs +23 -4
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@fastscript/core-private",
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"description": "Protected FastScript compiler/runtime/platform core",
|
|
7
|
+
"license": "UNLICENSED",
|
|
8
|
+
"files": [
|
|
9
|
+
"src",
|
|
10
|
+
"package.json",
|
|
11
|
+
"README.md",
|
|
12
|
+
"BOUNDARY.json"
|
|
13
|
+
],
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=20.0.0"
|
|
16
|
+
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"acorn": "^8.16.0",
|
|
19
|
+
"acorn-walk": "^8.3.5",
|
|
20
|
+
"astring": "^1.9.0",
|
|
21
|
+
"esbuild": "^0.25.11"
|
|
22
|
+
},
|
|
23
|
+
"exports": {
|
|
24
|
+
"./build": "./src/build.mjs",
|
|
25
|
+
"./deploy": "./src/deploy.mjs",
|
|
26
|
+
"./server-runtime": "./src/server-runtime.mjs",
|
|
27
|
+
"./parser": "./src/fs-parser.mjs",
|
|
28
|
+
"./typecheck": "./src/typecheck.mjs",
|
|
29
|
+
"./style-primitives": "./src/style-primitives.mjs",
|
|
30
|
+
"./auth": "./src/auth.mjs",
|
|
31
|
+
"./storage": "./src/storage.mjs",
|
|
32
|
+
"./webhook": "./src/webhook.mjs"
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { dirname, extname, join, relative, resolve } from "node:path";
|
|
4
|
+
|
|
5
|
+
function walk(dir) {
|
|
6
|
+
if (!existsSync(dir)) return [];
|
|
7
|
+
const out = [];
|
|
8
|
+
for (const entry of readdirSync(dir)) {
|
|
9
|
+
const full = join(dir, entry);
|
|
10
|
+
const st = statSync(full);
|
|
11
|
+
if (st.isDirectory()) out.push(...walk(full));
|
|
12
|
+
else out.push(full);
|
|
13
|
+
}
|
|
14
|
+
return out;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function hashFile(path) {
|
|
18
|
+
return createHash("sha1").update(readFileSync(path)).digest("hex").slice(0, 8);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function optimizeImageAssets({ appDir = "app", distDir = "dist" } = {}) {
|
|
22
|
+
const sourceRoot = resolve(appDir, "assets", "images");
|
|
23
|
+
const targetRoot = resolve(distDir, "assets", "images");
|
|
24
|
+
mkdirSync(targetRoot, { recursive: true });
|
|
25
|
+
const files = walk(sourceRoot).filter((file) => [".png", ".jpg", ".jpeg", ".webp", ".svg", ".gif"].includes(extname(file).toLowerCase()));
|
|
26
|
+
const manifest = {};
|
|
27
|
+
|
|
28
|
+
for (const file of files) {
|
|
29
|
+
const rel = relative(sourceRoot, file).replace(/\\/g, "/");
|
|
30
|
+
const ext = extname(rel);
|
|
31
|
+
const base = rel.slice(0, -ext.length);
|
|
32
|
+
const hashed = `${base}.${hashFile(file)}${ext}`;
|
|
33
|
+
const out = join(targetRoot, hashed);
|
|
34
|
+
mkdirSync(dirname(out), { recursive: true });
|
|
35
|
+
cpSync(file, out);
|
|
36
|
+
manifest[`/assets/images/${rel}`] = `/assets/images/${hashed}`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
writeFileSync(resolve(distDir, "image-manifest.json"), JSON.stringify(manifest, null, 2), "utf8");
|
|
40
|
+
return manifest;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function optimizeFontAssets({ appDir = "app", distDir = "dist" } = {}) {
|
|
44
|
+
const sourceRoot = resolve(appDir, "assets", "fonts");
|
|
45
|
+
const targetRoot = resolve(distDir, "assets", "fonts");
|
|
46
|
+
mkdirSync(targetRoot, { recursive: true });
|
|
47
|
+
const files = walk(sourceRoot).filter((file) => [".woff2", ".woff", ".ttf", ".otf"].includes(extname(file).toLowerCase()));
|
|
48
|
+
const manifest = {};
|
|
49
|
+
const css = [];
|
|
50
|
+
|
|
51
|
+
for (const file of files) {
|
|
52
|
+
const rel = relative(sourceRoot, file).replace(/\\/g, "/");
|
|
53
|
+
const ext = extname(rel);
|
|
54
|
+
const base = rel.slice(0, -ext.length);
|
|
55
|
+
const hashed = `${base}.${hashFile(file)}${ext}`;
|
|
56
|
+
const out = join(targetRoot, hashed);
|
|
57
|
+
mkdirSync(dirname(out), { recursive: true });
|
|
58
|
+
cpSync(file, out);
|
|
59
|
+
manifest[`/assets/fonts/${rel}`] = `/assets/fonts/${hashed}`;
|
|
60
|
+
const family = base.split("/").pop().replace(/[-_]/g, " ");
|
|
61
|
+
css.push(`@font-face{font-family:"${family}";src:url('/assets/fonts/${hashed}') format('${ext.replace(".", "")}');font-display:swap;}`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
writeFileSync(resolve(distDir, "font-manifest.json"), JSON.stringify(manifest, null, 2), "utf8");
|
|
65
|
+
if (css.length) writeFileSync(resolve(distDir, "fonts.generated.css"), `${css.join("\n")}\n`, "utf8");
|
|
66
|
+
return manifest;
|
|
67
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { dirname, resolve } from "node:path";
|
|
4
|
+
|
|
5
|
+
function sha(input) {
|
|
6
|
+
return createHash("sha256").update(input).digest("hex");
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function readLines(path) {
|
|
10
|
+
if (!existsSync(path)) return [];
|
|
11
|
+
return readFileSync(path, "utf8").split(/\r?\n/).filter(Boolean);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function createAuditLog({ file = ".fastscript/audit.log" } = {}) {
|
|
15
|
+
const path = resolve(file);
|
|
16
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
17
|
+
|
|
18
|
+
function append(event) {
|
|
19
|
+
const lines = readLines(path);
|
|
20
|
+
let prevHash = "genesis";
|
|
21
|
+
if (lines.length) {
|
|
22
|
+
const last = JSON.parse(lines[lines.length - 1]);
|
|
23
|
+
prevHash = last.hash || "genesis";
|
|
24
|
+
}
|
|
25
|
+
const record = {
|
|
26
|
+
ts: new Date().toISOString(),
|
|
27
|
+
prevHash,
|
|
28
|
+
...event,
|
|
29
|
+
};
|
|
30
|
+
record.hash = sha(JSON.stringify({ ...record, hash: undefined }));
|
|
31
|
+
lines.push(JSON.stringify(record));
|
|
32
|
+
writeFileSync(path, `${lines.join("\n")}\n`, "utf8");
|
|
33
|
+
return record;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function verify() {
|
|
37
|
+
const lines = readLines(path);
|
|
38
|
+
let prevHash = "genesis";
|
|
39
|
+
for (const line of lines) {
|
|
40
|
+
const row = JSON.parse(line);
|
|
41
|
+
if (row.prevHash !== prevHash) return { ok: false, row };
|
|
42
|
+
const expected = sha(JSON.stringify({ ...row, hash: undefined }));
|
|
43
|
+
if (row.hash !== expected) return { ok: false, row };
|
|
44
|
+
prevHash = row.hash;
|
|
45
|
+
}
|
|
46
|
+
return { ok: true, count: lines.length };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return { append, verify, file: path };
|
|
50
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { randomBytes, scryptSync, timingSafeEqual } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
export function hashPassword(password, salt = randomBytes(16).toString("hex")) {
|
|
4
|
+
const key = scryptSync(password, salt, 64).toString("hex");
|
|
5
|
+
return `${salt}:${key}`;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function verifyPassword(password, hashed) {
|
|
9
|
+
const [salt, keyHex] = String(hashed || "").split(":");
|
|
10
|
+
if (!salt || !keyHex) return false;
|
|
11
|
+
const calc = scryptSync(password, salt, 64);
|
|
12
|
+
const key = Buffer.from(keyHex, "hex");
|
|
13
|
+
if (calc.length !== key.length) return false;
|
|
14
|
+
return timingSafeEqual(calc, key);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function createOAuthState() {
|
|
18
|
+
return randomBytes(20).toString("hex");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function buildOAuthAuthorizeUrl({ authorizeUrl, clientId, redirectUri, scope = "openid profile email", state }) {
|
|
22
|
+
const u = new URL(authorizeUrl);
|
|
23
|
+
u.searchParams.set("response_type", "code");
|
|
24
|
+
u.searchParams.set("client_id", clientId);
|
|
25
|
+
u.searchParams.set("redirect_uri", redirectUri);
|
|
26
|
+
u.searchParams.set("scope", scope);
|
|
27
|
+
u.searchParams.set("state", state || createOAuthState());
|
|
28
|
+
return u.toString();
|
|
29
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { createHmac, randomBytes } from "node:crypto";
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
|
|
5
|
+
function readJson(path, fallback) {
|
|
6
|
+
if (!existsSync(path)) return fallback;
|
|
7
|
+
try {
|
|
8
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
9
|
+
} catch {
|
|
10
|
+
return fallback;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function writeJson(path, value) {
|
|
15
|
+
writeFileSync(path, JSON.stringify(value, null, 2), "utf8");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function parseCookies(header) {
|
|
19
|
+
const out = {};
|
|
20
|
+
if (!header) return out;
|
|
21
|
+
for (const part of header.split(";")) {
|
|
22
|
+
const [k, ...rest] = part.trim().split("=");
|
|
23
|
+
if (!k) continue;
|
|
24
|
+
out[k] = decodeURIComponent(rest.join("=") || "");
|
|
25
|
+
}
|
|
26
|
+
return out;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function serializeCookie(name, value, opts = {}) {
|
|
30
|
+
const bits = [`${name}=${encodeURIComponent(value)}`];
|
|
31
|
+
bits.push(`Path=${opts.path ?? "/"}`);
|
|
32
|
+
if (opts.httpOnly !== false) bits.push("HttpOnly");
|
|
33
|
+
if (opts.sameSite) bits.push(`SameSite=${opts.sameSite}`);
|
|
34
|
+
else bits.push("SameSite=Lax");
|
|
35
|
+
if (opts.secure) bits.push("Secure");
|
|
36
|
+
if (typeof opts.maxAge === "number") bits.push(`Max-Age=${opts.maxAge}`);
|
|
37
|
+
return bits.join("; ");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function sign(payload, secret) {
|
|
41
|
+
return createHmac("sha256", secret).update(payload).digest("hex");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function createSessionManager({ dir = ".fastscript", cookieName = "fs_session", secret = "fastscript-dev-secret" } = {}) {
|
|
45
|
+
mkdirSync(dir, { recursive: true });
|
|
46
|
+
const path = join(dir, "sessions.json");
|
|
47
|
+
const state = readJson(path, { sessions: {} });
|
|
48
|
+
|
|
49
|
+
function persist() {
|
|
50
|
+
writeJson(path, state);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
cookieName,
|
|
55
|
+
sweepExpired() {
|
|
56
|
+
let removed = 0;
|
|
57
|
+
const now = Date.now();
|
|
58
|
+
for (const [id, row] of Object.entries(state.sessions)) {
|
|
59
|
+
if (!row || row.exp < now) {
|
|
60
|
+
delete state.sessions[id];
|
|
61
|
+
removed += 1;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (removed) persist();
|
|
65
|
+
return removed;
|
|
66
|
+
},
|
|
67
|
+
create(user, maxAgeSec = 60 * 60 * 24 * 7) {
|
|
68
|
+
const id = randomBytes(12).toString("hex");
|
|
69
|
+
const exp = Date.now() + maxAgeSec * 1000;
|
|
70
|
+
state.sessions[id] = { user, exp };
|
|
71
|
+
persist();
|
|
72
|
+
const sig = sign(`${id}.${exp}`, secret);
|
|
73
|
+
return `${id}.${exp}.${sig}`;
|
|
74
|
+
},
|
|
75
|
+
read(token) {
|
|
76
|
+
if (!token) return null;
|
|
77
|
+
const [id, expStr, sig] = token.split(".");
|
|
78
|
+
if (!id || !expStr || !sig) return null;
|
|
79
|
+
const expected = sign(`${id}.${expStr}`, secret);
|
|
80
|
+
if (expected !== sig) return null;
|
|
81
|
+
const exp = Number(expStr);
|
|
82
|
+
if (!Number.isFinite(exp) || exp < Date.now()) return null;
|
|
83
|
+
const row = state.sessions[id];
|
|
84
|
+
if (!row || row.exp < Date.now()) {
|
|
85
|
+
if (row) {
|
|
86
|
+
delete state.sessions[id];
|
|
87
|
+
persist();
|
|
88
|
+
}
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
return row;
|
|
92
|
+
},
|
|
93
|
+
delete(token) {
|
|
94
|
+
const [id] = String(token || "").split(".");
|
|
95
|
+
if (!id) return;
|
|
96
|
+
delete state.sessions[id];
|
|
97
|
+
persist();
|
|
98
|
+
},
|
|
99
|
+
rotate(token, maxAgeSec = 60 * 60 * 24 * 7) {
|
|
100
|
+
const row = this.read(token);
|
|
101
|
+
if (!row?.user) return null;
|
|
102
|
+
this.delete(token);
|
|
103
|
+
return this.create(row.user, maxAgeSec);
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function requireUser(user) {
|
|
109
|
+
if (!user) {
|
|
110
|
+
const error = new Error("Unauthorized");
|
|
111
|
+
error.status = 401;
|
|
112
|
+
throw error;
|
|
113
|
+
}
|
|
114
|
+
return user;
|
|
115
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { gzipSync } from "node:zlib";
|
|
3
|
+
import { join, resolve } from "node:path";
|
|
4
|
+
|
|
5
|
+
const DIST = resolve("dist");
|
|
6
|
+
const JS_BUDGET_BYTES = Number(process.env.FASTSCRIPT_JS_BUDGET_KB || 30) * 1024;
|
|
7
|
+
const CSS_BUDGET_BYTES = Number(process.env.FASTSCRIPT_CSS_BUDGET_KB || 15) * 1024;
|
|
8
|
+
|
|
9
|
+
function gzipSize(path) {
|
|
10
|
+
if (!existsSync(path)) return 0;
|
|
11
|
+
const raw = readFileSync(path);
|
|
12
|
+
return gzipSync(raw, { level: 9 }).byteLength;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function kb(bytes) {
|
|
16
|
+
return `${(bytes / 1024).toFixed(2)}KB`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function runBench() {
|
|
20
|
+
const manifestPath = join(DIST, "fastscript-manifest.json");
|
|
21
|
+
if (!existsSync(manifestPath)) {
|
|
22
|
+
throw new Error("Missing dist build output. Run: fastscript build");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
|
|
26
|
+
const jsAssets = [join(DIST, "router.js")];
|
|
27
|
+
const cssAssets = [join(DIST, "styles.css")];
|
|
28
|
+
|
|
29
|
+
if (manifest.layout) jsAssets.push(join(DIST, manifest.layout.replace(/^\.\//, "")));
|
|
30
|
+
const root = manifest.routes.find((r) => r.path === "/");
|
|
31
|
+
if (root?.module) jsAssets.push(join(DIST, root.module.replace(/^\.\//, "")));
|
|
32
|
+
|
|
33
|
+
const totalJs = jsAssets.reduce((sum, p) => sum + gzipSize(p), 0);
|
|
34
|
+
const totalCss = cssAssets.reduce((sum, p) => sum + gzipSize(p), 0);
|
|
35
|
+
|
|
36
|
+
console.log(`3G budget check -> JS: ${kb(totalJs)} / ${kb(JS_BUDGET_BYTES)}, CSS: ${kb(totalCss)} / ${kb(CSS_BUDGET_BYTES)}`);
|
|
37
|
+
|
|
38
|
+
const errors = [];
|
|
39
|
+
if (totalJs > JS_BUDGET_BYTES) errors.push(`JS budget exceeded by ${kb(totalJs - JS_BUDGET_BYTES)}`);
|
|
40
|
+
if (totalCss > CSS_BUDGET_BYTES) errors.push(`CSS budget exceeded by ${kb(totalCss - CSS_BUDGET_BYTES)}`);
|
|
41
|
+
|
|
42
|
+
if (errors.length > 0) {
|
|
43
|
+
throw new Error(errors.join("\n"));
|
|
44
|
+
}
|
|
45
|
+
}
|