fastscript 1.0.0 → 3.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 +38 -7
- package/LICENSE +33 -21
- package/README.md +605 -73
- 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 +121 -0
- package/node_modules/@fastscript/core-private/src/fs-parser.mjs +1120 -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 +1466 -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 +108 -14
- 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/benchmark-discipline.mjs +39 -0
- package/src/build.mjs +1 -234
- package/src/cache.mjs +210 -20
- package/src/cli.mjs +65 -6
- package/src/compat.mjs +8 -10
- package/src/conversion-manifest.mjs +101 -0
- package/src/create.mjs +71 -17
- package/src/csp.mjs +26 -0
- package/src/db-cli.mjs +152 -8
- package/src/db-postgres-collection.mjs +110 -0
- package/src/deploy.mjs +1 -65
- package/src/diagnostics.mjs +100 -0
- 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 +52 -239
- package/src/fs-parser.mjs +1 -0
- package/src/generated/docs-search-index.mjs +3591 -0
- package/src/i18n.mjs +25 -0
- package/src/jobs.mjs +283 -32
- package/src/metrics.mjs +45 -0
- package/src/migrate-rollback.mjs +144 -0
- package/src/migrate.mjs +1275 -47
- package/src/migration-wizard.mjs +42 -0
- package/src/module-loader.mjs +22 -11
- package/src/oauth-providers.mjs +103 -0
- package/src/permissions-cli.mjs +112 -0
- package/src/plugins.mjs +194 -0
- package/src/profile.mjs +95 -0
- package/src/regression-guard.mjs +245 -0
- package/src/retention.mjs +57 -0
- package/src/routes.mjs +178 -0
- package/src/runtime-permissions.mjs +299 -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/trace.mjs +95 -0
- package/src/typecheck.mjs +1 -0
- package/src/validate.mjs +13 -1
- package/src/validation.mjs +14 -5
- package/src/webhook.mjs +1 -71
- package/src/worker.mjs +23 -4
- package/src/language-spec.mjs +0 -58
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname, relative, resolve } from "node:path";
|
|
3
|
+
|
|
4
|
+
function normalize(path) {
|
|
5
|
+
return String(path || "").replace(/\\/g, "/");
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function readJson(path, fallback = null) {
|
|
9
|
+
if (!existsSync(path)) return fallback;
|
|
10
|
+
try {
|
|
11
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
12
|
+
} catch {
|
|
13
|
+
return fallback;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function writeJson(path, value) {
|
|
18
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
19
|
+
writeFileSync(path, `${JSON.stringify(value, null, 2)}\n`, "utf8");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function parseArgs(args = []) {
|
|
23
|
+
const options = {
|
|
24
|
+
mode: "all",
|
|
25
|
+
autoBaseline: false,
|
|
26
|
+
failOnRegression: true,
|
|
27
|
+
benchmarkLatest: resolve("benchmarks", "suite-latest.json"),
|
|
28
|
+
benchmarkBaseline: resolve("benchmarks", "suite-baseline.json"),
|
|
29
|
+
fidelityLatest: resolve(".fastscript", "conversion", "latest", "fidelity-report.json"),
|
|
30
|
+
fidelityBaseline: resolve(".fastscript", "baselines", "fidelity-baseline.json"),
|
|
31
|
+
out: resolve(".fastscript", "regression", "latest.json"),
|
|
32
|
+
warmBuildBudgetPct: Number(process.env.FASTSCRIPT_REGRESSION_WARM_PCT || 0.15),
|
|
33
|
+
coldBuildBudgetPct: Number(process.env.FASTSCRIPT_REGRESSION_COLD_PCT || 0.2),
|
|
34
|
+
typecheckBudgetPct: Number(process.env.FASTSCRIPT_REGRESSION_TYPECHECK_PCT || 0.2),
|
|
35
|
+
jsBudgetPct: Number(process.env.FASTSCRIPT_REGRESSION_JS_PCT || 0.1),
|
|
36
|
+
cssBudgetPct: Number(process.env.FASTSCRIPT_REGRESSION_CSS_PCT || 0.1),
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
40
|
+
const arg = args[i];
|
|
41
|
+
if (arg === "--mode") {
|
|
42
|
+
const value = String(args[i + 1] || options.mode).toLowerCase();
|
|
43
|
+
options.mode = ["all", "benchmark", "fidelity"].includes(value) ? value : "all";
|
|
44
|
+
i += 1;
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
if (arg === "--auto-baseline") {
|
|
48
|
+
options.autoBaseline = true;
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
if (arg === "--no-fail") {
|
|
52
|
+
options.failOnRegression = false;
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
if (arg === "--benchmark-latest") {
|
|
56
|
+
options.benchmarkLatest = resolve(args[i + 1] || options.benchmarkLatest);
|
|
57
|
+
i += 1;
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (arg === "--benchmark-baseline") {
|
|
61
|
+
options.benchmarkBaseline = resolve(args[i + 1] || options.benchmarkBaseline);
|
|
62
|
+
i += 1;
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (arg === "--fidelity-latest") {
|
|
66
|
+
options.fidelityLatest = resolve(args[i + 1] || options.fidelityLatest);
|
|
67
|
+
i += 1;
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (arg === "--fidelity-baseline") {
|
|
71
|
+
options.fidelityBaseline = resolve(args[i + 1] || options.fidelityBaseline);
|
|
72
|
+
i += 1;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (arg === "--out") {
|
|
76
|
+
options.out = resolve(args[i + 1] || options.out);
|
|
77
|
+
i += 1;
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return options;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function budgetExceeded(current, baseline, pct) {
|
|
86
|
+
const max = baseline * (1 + pct);
|
|
87
|
+
return { exceeded: current > max, max };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function compareBenchmark(latest, baseline, options) {
|
|
91
|
+
const findings = [];
|
|
92
|
+
const byId = new Map((baseline?.corpora || []).filter((item) => !item.skipped).map((item) => [item.id, item]));
|
|
93
|
+
|
|
94
|
+
for (const corpus of latest.corpora || []) {
|
|
95
|
+
if (corpus.skipped) continue;
|
|
96
|
+
const base = byId.get(corpus.id);
|
|
97
|
+
if (!base) continue;
|
|
98
|
+
|
|
99
|
+
const checks = [
|
|
100
|
+
{
|
|
101
|
+
metric: "warmBuildP95TrimmedMs",
|
|
102
|
+
current: Number(corpus?.timingsMs?.buildWarm?.p95Trimmed || 0),
|
|
103
|
+
baseline: Number(base?.timingsMs?.buildWarm?.p95Trimmed || 0),
|
|
104
|
+
budgetPct: options.warmBuildBudgetPct,
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
metric: "coldBuildMs",
|
|
108
|
+
current: Number(corpus?.timingsMs?.buildCold?.ms || 0),
|
|
109
|
+
baseline: Number(base?.timingsMs?.buildCold?.ms || 0),
|
|
110
|
+
budgetPct: options.coldBuildBudgetPct,
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
metric: "typecheckP95TrimmedMs",
|
|
114
|
+
current: Number(corpus?.timingsMs?.typecheck?.p95Trimmed || 0),
|
|
115
|
+
baseline: Number(base?.timingsMs?.typecheck?.p95Trimmed || 0),
|
|
116
|
+
budgetPct: options.typecheckBudgetPct,
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
metric: "firstLoadJsGzipBytes",
|
|
120
|
+
current: Number(corpus?.bundles?.js || 0),
|
|
121
|
+
baseline: Number(base?.bundles?.js || 0),
|
|
122
|
+
budgetPct: options.jsBudgetPct,
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
metric: "firstLoadCssGzipBytes",
|
|
126
|
+
current: Number(corpus?.bundles?.css || 0),
|
|
127
|
+
baseline: Number(base?.bundles?.css || 0),
|
|
128
|
+
budgetPct: options.cssBudgetPct,
|
|
129
|
+
},
|
|
130
|
+
];
|
|
131
|
+
|
|
132
|
+
for (const check of checks) {
|
|
133
|
+
if (!Number.isFinite(check.current) || !Number.isFinite(check.baseline) || check.baseline <= 0) continue;
|
|
134
|
+
const status = budgetExceeded(check.current, check.baseline, check.budgetPct);
|
|
135
|
+
if (status.exceeded) {
|
|
136
|
+
findings.push({
|
|
137
|
+
domain: "benchmark",
|
|
138
|
+
corpus: corpus.id,
|
|
139
|
+
metric: check.metric,
|
|
140
|
+
current: check.current,
|
|
141
|
+
baseline: check.baseline,
|
|
142
|
+
maxAllowed: Number(status.max.toFixed(2)),
|
|
143
|
+
budgetPct: check.budgetPct,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
status: findings.length ? "fail" : "pass",
|
|
151
|
+
findings,
|
|
152
|
+
corporaCompared: (latest.corpora || []).filter((item) => !item.skipped).length,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function compareFidelity(latest, baseline) {
|
|
157
|
+
const findings = [];
|
|
158
|
+
|
|
159
|
+
const latestStatus = String(latest?.status || "unknown");
|
|
160
|
+
const baselineStatus = String(baseline?.status || "unknown");
|
|
161
|
+
if (latestStatus !== "pass") {
|
|
162
|
+
findings.push({ domain: "fidelity", metric: "status", current: latestStatus, baseline: baselineStatus, expected: "pass" });
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const latestChecksFail = (latest?.checks || []).filter((item) => item.status !== "pass").map((item) => item.id);
|
|
166
|
+
if (latestChecksFail.length) {
|
|
167
|
+
findings.push({ domain: "fidelity", metric: "checks", current: latestChecksFail, baseline: [] });
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const latestProbesFail = (latest?.probes || []).filter((item) => item.status !== "pass").map((item) => item.id);
|
|
171
|
+
if (latestProbesFail.length) {
|
|
172
|
+
findings.push({ domain: "fidelity", metric: "probes", current: latestProbesFail, baseline: [] });
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const baselineRequired = new Set((baseline?.required || []).map((item) => String(item || "")));
|
|
176
|
+
const latestRequired = new Set((latest?.required || []).map((item) => String(item || "")));
|
|
177
|
+
for (const id of baselineRequired) {
|
|
178
|
+
if (!latestRequired.has(id)) {
|
|
179
|
+
findings.push({ domain: "fidelity", metric: "requiredProbeMissing", probe: id });
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return {
|
|
184
|
+
status: findings.length ? "fail" : "pass",
|
|
185
|
+
findings,
|
|
186
|
+
requiredCount: latestRequired.size,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function ensureBaselineOrLoad(path, latest, options) {
|
|
191
|
+
const baseline = readJson(path, null);
|
|
192
|
+
if (baseline) return { baseline, created: false };
|
|
193
|
+
if (!options.autoBaseline) {
|
|
194
|
+
throw new Error(`regression baseline missing: ${normalize(relative(resolve("."), path))}`);
|
|
195
|
+
}
|
|
196
|
+
writeJson(path, latest);
|
|
197
|
+
return { baseline: latest, created: true };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export async function runRegressionGuard(args = []) {
|
|
201
|
+
const options = parseArgs(args);
|
|
202
|
+
const report = {
|
|
203
|
+
generatedAt: new Date().toISOString(),
|
|
204
|
+
mode: options.mode,
|
|
205
|
+
benchmark: null,
|
|
206
|
+
fidelity: null,
|
|
207
|
+
baselineCreated: {
|
|
208
|
+
benchmark: false,
|
|
209
|
+
fidelity: false,
|
|
210
|
+
},
|
|
211
|
+
status: "pass",
|
|
212
|
+
findings: [],
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
if (options.mode === "all" || options.mode === "benchmark") {
|
|
216
|
+
const latest = readJson(options.benchmarkLatest, null);
|
|
217
|
+
if (!latest) throw new Error(`benchmark latest missing: ${normalize(relative(resolve("."), options.benchmarkLatest))}`);
|
|
218
|
+
|
|
219
|
+
const baselineState = ensureBaselineOrLoad(options.benchmarkBaseline, latest, options);
|
|
220
|
+
report.baselineCreated.benchmark = baselineState.created;
|
|
221
|
+
report.benchmark = compareBenchmark(latest, baselineState.baseline, options);
|
|
222
|
+
report.findings.push(...report.benchmark.findings);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (options.mode === "all" || options.mode === "fidelity") {
|
|
226
|
+
const latest = readJson(options.fidelityLatest, null);
|
|
227
|
+
if (!latest) throw new Error(`fidelity latest missing: ${normalize(relative(resolve("."), options.fidelityLatest))}`);
|
|
228
|
+
|
|
229
|
+
const baselineState = ensureBaselineOrLoad(options.fidelityBaseline, latest, options);
|
|
230
|
+
report.baselineCreated.fidelity = baselineState.created;
|
|
231
|
+
report.fidelity = compareFidelity(latest, baselineState.baseline);
|
|
232
|
+
report.findings.push(...report.fidelity.findings);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (report.findings.length) report.status = "fail";
|
|
236
|
+
|
|
237
|
+
writeJson(options.out, report);
|
|
238
|
+
|
|
239
|
+
console.log(`regression guard: ${report.status}`);
|
|
240
|
+
console.log(`regression report: ${normalize(relative(resolve("."), options.out))}`);
|
|
241
|
+
|
|
242
|
+
if (options.failOnRegression && report.status === "fail") {
|
|
243
|
+
throw new Error(`regression guard failed with ${report.findings.length} finding(s)`);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
|
|
4
|
+
function readJson(path, fallback) {
|
|
5
|
+
if (!existsSync(path)) return fallback;
|
|
6
|
+
try {
|
|
7
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
8
|
+
} catch {
|
|
9
|
+
return fallback;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function applyJsonArrayRetention(path, { timestampField = "ts", maxAgeMs = 1000 * 60 * 60 * 24 * 30 } = {}) {
|
|
14
|
+
const rows = readJson(path, []);
|
|
15
|
+
if (!Array.isArray(rows)) return { kept: 0, removed: 0 };
|
|
16
|
+
const cutoff = Date.now() - maxAgeMs;
|
|
17
|
+
const kept = rows.filter((row) => {
|
|
18
|
+
const raw = row?.[timestampField] || row?.failedAt || row?.createdAt || null;
|
|
19
|
+
const n = typeof raw === "number" ? raw : Date.parse(raw || "");
|
|
20
|
+
return Number.isFinite(n) ? n >= cutoff : true;
|
|
21
|
+
});
|
|
22
|
+
const removed = rows.length - kept.length;
|
|
23
|
+
if (removed > 0) writeFileSync(path, JSON.stringify(kept, null, 2), "utf8");
|
|
24
|
+
return { kept: kept.length, removed };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function sweepFileAges(dir, { maxAgeMs = 1000 * 60 * 60 * 24 * 30 } = {}) {
|
|
28
|
+
const root = resolve(dir);
|
|
29
|
+
if (!existsSync(root)) return { removed: 0 };
|
|
30
|
+
const cutoff = Date.now() - maxAgeMs;
|
|
31
|
+
let removed = 0;
|
|
32
|
+
for (const name of readdirSync(root)) {
|
|
33
|
+
const full = join(root, name);
|
|
34
|
+
const st = statSync(full);
|
|
35
|
+
if (st.isDirectory()) continue;
|
|
36
|
+
if (st.mtimeMs < cutoff) {
|
|
37
|
+
rmSync(full, { force: true });
|
|
38
|
+
removed += 1;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return { removed };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function runRetentionSweep({ root = ".fastscript" } = {}) {
|
|
45
|
+
const resolved = resolve(root);
|
|
46
|
+
const cacheResult = sweepFileAges(join(resolved, "cache"), {
|
|
47
|
+
maxAgeMs: Number(process.env.RETENTION_CACHE_MS || 1000 * 60 * 60 * 24 * 14),
|
|
48
|
+
});
|
|
49
|
+
const jobsResult = applyJsonArrayRetention(join(resolved, "jobs-dead-letter.json"), {
|
|
50
|
+
timestampField: "failedAt",
|
|
51
|
+
maxAgeMs: Number(process.env.RETENTION_DEAD_LETTER_MS || 1000 * 60 * 60 * 24 * 30),
|
|
52
|
+
});
|
|
53
|
+
return {
|
|
54
|
+
cache: cacheResult,
|
|
55
|
+
deadLetter: jobsResult,
|
|
56
|
+
};
|
|
57
|
+
}
|
package/src/routes.mjs
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { dirname, extname, relative } from "node:path";
|
|
2
|
+
|
|
3
|
+
function cleanSegments(file, pagesDir) {
|
|
4
|
+
const rel = relative(pagesDir, file).replace(/\\/g, "/").replace(extname(file), "");
|
|
5
|
+
return rel.split("/").filter(Boolean);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function isGroupSegment(segment) {
|
|
9
|
+
return segment.startsWith("(") && segment.endsWith(")");
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function isParallelSegment(segment) {
|
|
13
|
+
return segment.startsWith("@");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function normalizeParamType(type) {
|
|
17
|
+
const raw = String(type || "string").toLowerCase();
|
|
18
|
+
if (raw === "int" || raw === "float" || raw === "number") return "number";
|
|
19
|
+
if (raw === "bool" || raw === "boolean") return "boolean";
|
|
20
|
+
if (raw === "str" || raw === "string") return "string";
|
|
21
|
+
return "string";
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function segmentPriority(segment) {
|
|
25
|
+
if (!segment) return 0;
|
|
26
|
+
const token = /^:([A-Za-z_$][\w$]*)(\*)?(\?)?$/.exec(segment);
|
|
27
|
+
if (!token) return 40;
|
|
28
|
+
const catchAll = Boolean(token[2]);
|
|
29
|
+
const optional = Boolean(token[3]);
|
|
30
|
+
if (catchAll && optional) return 5;
|
|
31
|
+
if (catchAll) return 10;
|
|
32
|
+
if (optional) return 20;
|
|
33
|
+
return 30;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function routePriority(pathname) {
|
|
37
|
+
const parts = String(pathname || "/").split("/").filter(Boolean);
|
|
38
|
+
if (!parts.length) return 1000;
|
|
39
|
+
return parts.reduce((sum, part) => sum + segmentPriority(part), 0) + parts.length;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function routeSegmentValue(segment) {
|
|
43
|
+
if (!segment) return null;
|
|
44
|
+
if (isGroupSegment(segment)) return null;
|
|
45
|
+
if (isParallelSegment(segment)) return null;
|
|
46
|
+
if (segment === "index") return null;
|
|
47
|
+
const optionalCatchAll = /^\[\[\.\.\.([A-Za-z_$][\w$]*)\]\]$/.exec(segment);
|
|
48
|
+
if (optionalCatchAll) {
|
|
49
|
+
return {
|
|
50
|
+
value: `:${optionalCatchAll[1]}*?`,
|
|
51
|
+
param: { name: optionalCatchAll[1], type: "string[] | undefined", optional: true, catchAll: true },
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
const requiredCatchAll = /^\[\.\.\.([A-Za-z_$][\w$]*)\]$/.exec(segment);
|
|
55
|
+
if (requiredCatchAll) {
|
|
56
|
+
return {
|
|
57
|
+
value: `:${requiredCatchAll[1]}*`,
|
|
58
|
+
param: { name: requiredCatchAll[1], type: "string[]", optional: false, catchAll: true },
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
const optionalTyped = /^\[\[([A-Za-z_$][\w$]*)(?::([A-Za-z]+))?\]\]$/.exec(segment);
|
|
62
|
+
if (optionalTyped) {
|
|
63
|
+
const type = normalizeParamType(optionalTyped[2]);
|
|
64
|
+
return {
|
|
65
|
+
value: `:${optionalTyped[1]}?`,
|
|
66
|
+
param: { name: optionalTyped[1], type: `${type} | undefined`, optional: true, catchAll: false },
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
const simpleTyped = /^\[([A-Za-z_$][\w$]*)(?::([A-Za-z]+))?\]$/.exec(segment);
|
|
70
|
+
if (simpleTyped) {
|
|
71
|
+
const type = normalizeParamType(simpleTyped[2]);
|
|
72
|
+
return {
|
|
73
|
+
value: `:${simpleTyped[1]}`,
|
|
74
|
+
param: { name: simpleTyped[1], type, optional: false, catchAll: false },
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
return { value: segment, param: null };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function inferRouteMeta(file, pagesDir) {
|
|
81
|
+
const parts = cleanSegments(file, pagesDir);
|
|
82
|
+
const pageFile = parts[parts.length - 1] || "index";
|
|
83
|
+
const parallel = parts.find((segment) => isParallelSegment(segment));
|
|
84
|
+
const slot = parallel ? parallel.slice(1) : null;
|
|
85
|
+
const routeParts = [];
|
|
86
|
+
const params = [];
|
|
87
|
+
const paramTypes = {};
|
|
88
|
+
|
|
89
|
+
for (const part of parts) {
|
|
90
|
+
const mapped = routeSegmentValue(part);
|
|
91
|
+
if (!mapped?.value) continue;
|
|
92
|
+
routeParts.push(mapped.value);
|
|
93
|
+
if (mapped.param) {
|
|
94
|
+
params.push(mapped.param.name);
|
|
95
|
+
paramTypes[mapped.param.name] = mapped.param.type;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const routePath = routeParts.length ? `/${routeParts.join("/")}` : "/";
|
|
100
|
+
const normalizedParts = parts.filter((segment) => !isGroupSegment(segment) && !isParallelSegment(segment));
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
file,
|
|
104
|
+
pageFile,
|
|
105
|
+
routePath,
|
|
106
|
+
params,
|
|
107
|
+
paramTypes,
|
|
108
|
+
slot,
|
|
109
|
+
segments: normalizedParts,
|
|
110
|
+
directory: dirname(relative(pagesDir, file).replace(/\\/g, "/")),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function inferRouteParamTypes(routePath, paramTypes = null) {
|
|
115
|
+
if (paramTypes && typeof paramTypes === "object" && Object.keys(paramTypes).length) {
|
|
116
|
+
return { ...paramTypes };
|
|
117
|
+
}
|
|
118
|
+
const params = {};
|
|
119
|
+
for (const segment of String(routePath || "").split("/")) {
|
|
120
|
+
const token = /^:([A-Za-z_$][\w$]*)(\*)?(\?)?$/.exec(segment);
|
|
121
|
+
if (!token) continue;
|
|
122
|
+
const name = token[1];
|
|
123
|
+
const catchAll = Boolean(token[2]);
|
|
124
|
+
const optional = Boolean(token[3]);
|
|
125
|
+
if (catchAll && optional) params[name] = "string[] | undefined";
|
|
126
|
+
else if (catchAll) params[name] = "string[]";
|
|
127
|
+
else if (optional) params[name] = "string | undefined";
|
|
128
|
+
else params[name] = "string";
|
|
129
|
+
}
|
|
130
|
+
return params;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function inferRouteLayouts(file, pagesDir, layoutFiles = new Set()) {
|
|
134
|
+
const relDir = dirname(relative(pagesDir, file).replace(/\\/g, "/"));
|
|
135
|
+
const chain = [];
|
|
136
|
+
const dirs = relDir === "." ? [] : relDir.split("/").filter(Boolean);
|
|
137
|
+
const probes = [""];
|
|
138
|
+
for (const dir of dirs) {
|
|
139
|
+
probes.push(probes[probes.length - 1] ? `${probes[probes.length - 1]}/${dir}` : dir);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
for (const probe of probes) {
|
|
143
|
+
const fsLayout = probe ? `${probe}/_layout.fs` : "_layout.fs";
|
|
144
|
+
const jsLayout = probe ? `${probe}/_layout.js` : "_layout.js";
|
|
145
|
+
if (layoutFiles.has(fsLayout)) chain.push(fsLayout);
|
|
146
|
+
else if (layoutFiles.has(jsLayout)) chain.push(jsLayout);
|
|
147
|
+
}
|
|
148
|
+
return chain;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function compareRoutePriority(a, b) {
|
|
152
|
+
const scoreA = routePriority(a?.path || a?.routePath || "/");
|
|
153
|
+
const scoreB = routePriority(b?.path || b?.routePath || "/");
|
|
154
|
+
if (scoreA !== scoreB) return scoreB - scoreA;
|
|
155
|
+
|
|
156
|
+
const lenA = String(a?.path || a?.routePath || "/").split("/").filter(Boolean).length;
|
|
157
|
+
const lenB = String(b?.path || b?.routePath || "/").split("/").filter(Boolean).length;
|
|
158
|
+
if (lenA !== lenB) return lenB - lenA;
|
|
159
|
+
|
|
160
|
+
const slotA = String(a?.slot || "default");
|
|
161
|
+
const slotB = String(b?.slot || "default");
|
|
162
|
+
if (slotA !== slotB) return slotA.localeCompare(slotB);
|
|
163
|
+
return String(a?.path || a?.routePath || "/").localeCompare(String(b?.path || b?.routePath || "/"));
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function sortRoutesByPriority(routes = []) {
|
|
167
|
+
return [...routes].sort(compareRoutePriority);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function isLayoutFile(file, pagesDir) {
|
|
171
|
+
const rel = relative(pagesDir, file).replace(/\\/g, "/");
|
|
172
|
+
return rel.endsWith("/_layout.fs") || rel.endsWith("/_layout.js") || rel === "_layout.fs" || rel === "_layout.js";
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function isNotFoundFile(file, pagesDir) {
|
|
176
|
+
const rel = relative(pagesDir, file).replace(/\\/g, "/");
|
|
177
|
+
return rel.endsWith("/404.fs") || rel.endsWith("/404.js") || rel === "404.fs" || rel === "404.js";
|
|
178
|
+
}
|