fastscript 2.0.0 → 3.0.1

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.
@@ -1,16 +1,42 @@
1
- import { existsSync, readFileSync } from "node:fs";
1
+ import { existsSync } from "node:fs";
2
2
  import { resolve } from "node:path";
3
- import { normalizeFastScript, stripTypeScriptHints } from "./fs-normalize.mjs";
3
+ import { createStrictConversionPlan } from "./migrate.mjs";
4
4
 
5
5
  export async function runMigrationWizard(args = []) {
6
- const pathArg = args[0] || "app/pages/index.js";
7
- const abs = resolve(pathArg);
8
- if (!existsSync(abs)) throw new Error(`migration wizard: file not found (${abs})`);
9
- const raw = readFileSync(abs, "utf8");
10
- let preview = raw;
11
- if (/\.(ts|tsx)$/.test(abs)) preview = stripTypeScriptHints(preview);
12
- preview = normalizeFastScript(preview, { file: abs, mode: "lenient" });
13
- console.log("=== migration preview ===");
14
- console.log(preview);
6
+ const target = args[0] || "app";
7
+ const abs = resolve(target);
8
+ if (!existsSync(abs)) throw new Error(`migration wizard: path not found (${abs})`);
9
+
10
+ const prepared = createStrictConversionPlan([abs, "--dry-run"]);
11
+ const { plan } = prepared;
12
+
13
+ console.log("=== strict conversion preview ===");
14
+ console.log(`target: ${abs}`);
15
+ console.log(`rename files: ${plan.renames.length}`);
16
+ console.log(`rewrite files: ${plan.writes.filter((item) => item.kind === "rewrite").length}`);
17
+ console.log(`import rewrites: ${plan.importRewrites.reduce((sum, item) => sum + item.count, 0)}`);
18
+ console.log(`blocked files: ${plan.blockedFiles.length}`);
19
+
20
+ if (plan.renames.length) {
21
+ console.log("--- renames ---");
22
+ for (const item of plan.renames.slice(0, 200)) {
23
+ console.log(`${item.fromRel} -> ${item.toRel}`);
24
+ }
25
+ }
26
+
27
+ if (plan.importRewrites.length) {
28
+ console.log("--- import rewrites ---");
29
+ for (const item of plan.importRewrites.slice(0, 100)) {
30
+ console.log(`${item.file} (${item.count})`);
31
+ }
32
+ }
33
+
34
+ if (plan.blockedFiles.length) {
35
+ console.log("--- blocked ---");
36
+ for (const item of plan.blockedFiles.slice(0, 200)) {
37
+ console.log(`${item.path}: ${item.reason}`);
38
+ }
39
+ }
40
+
15
41
  console.log("=== end preview ===");
16
42
  }
@@ -3,6 +3,15 @@ import { pathToFileURL } from "node:url";
3
3
  import esbuild from "esbuild";
4
4
  import { normalizeFastScript } from "./fs-normalize.mjs";
5
5
  import { assertFastScript } from "./fs-diagnostics.mjs";
6
+ import { createPermissionRuntime } from "./runtime-permissions.mjs";
7
+
8
+ let cachedPermissionRuntime = null;
9
+
10
+ function getPermissionRuntime() {
11
+ if (process.env.FASTSCRIPT_RUNTIME_PERMISSIONS === "0") return null;
12
+ if (!cachedPermissionRuntime) cachedPermissionRuntime = createPermissionRuntime();
13
+ return cachedPermissionRuntime;
14
+ }
6
15
 
7
16
  function fsLoaderPlugin() {
8
17
  const compilerMode = (process.env.FASTSCRIPT_COMPILER_MODE || "strict").toLowerCase() === "lenient" ? "lenient" : "strict";
@@ -24,6 +33,8 @@ function fsLoaderPlugin() {
24
33
 
25
34
  export async function importSourceModule(filePath, { platform = "node" } = {}) {
26
35
  const ext = extname(filePath).toLowerCase();
36
+ const permissionRuntime = getPermissionRuntime();
37
+ permissionRuntime?.assert({ kind: "dynamicImportAccess", resource: filePath, details: { platform } });
27
38
  if (ext !== ".fs") {
28
39
  return import(`${pathToFileURL(filePath).href}?t=${Date.now()}`);
29
40
  }
@@ -42,5 +53,6 @@ export async function importSourceModule(filePath, { platform = "node" } = {}) {
42
53
 
43
54
  const code = result.outputFiles[0].text;
44
55
  const dataUrl = `data:text/javascript;base64,${Buffer.from(code).toString("base64")}`;
56
+ permissionRuntime?.assert({ kind: "dynamicImportAccess", resource: dataUrl, details: { platform, source: filePath } });
45
57
  return import(dataUrl);
46
- }
58
+ }
@@ -0,0 +1,112 @@
1
+ import { mkdirSync, writeFileSync } from "node:fs";
2
+ import { dirname, relative, resolve } from "node:path";
3
+ import { createPermissionRuntime } from "./runtime-permissions.mjs";
4
+
5
+ function normalize(path) {
6
+ return String(path || "").replace(/\\/g, "/");
7
+ }
8
+
9
+ function parseArgs(args = []) {
10
+ const options = {
11
+ policy: process.env.FASTSCRIPT_PERMISSION_POLICY_PATH || "fastscript.permissions.json",
12
+ mode: "report",
13
+ kind: "",
14
+ resource: "",
15
+ out: "",
16
+ json: false,
17
+ };
18
+
19
+ for (let i = 0; i < args.length; i += 1) {
20
+ const arg = args[i];
21
+ if (arg === "--policy") {
22
+ options.policy = args[i + 1] || options.policy;
23
+ i += 1;
24
+ continue;
25
+ }
26
+ if (arg === "--mode") {
27
+ const next = String(args[i + 1] || "report").toLowerCase();
28
+ options.mode = next === "assert" ? "assert" : "report";
29
+ i += 1;
30
+ continue;
31
+ }
32
+ if (arg === "--kind") {
33
+ options.kind = String(args[i + 1] || "");
34
+ i += 1;
35
+ continue;
36
+ }
37
+ if (arg === "--resource") {
38
+ options.resource = String(args[i + 1] || "");
39
+ i += 1;
40
+ continue;
41
+ }
42
+ if (arg === "--out") {
43
+ options.out = resolve(args[i + 1] || "");
44
+ i += 1;
45
+ continue;
46
+ }
47
+ if (arg === "--json") {
48
+ options.json = true;
49
+ continue;
50
+ }
51
+ }
52
+
53
+ return options;
54
+ }
55
+
56
+ function summarizePolicy(policyRuntime) {
57
+ const policy = policyRuntime.policy;
58
+ return {
59
+ policyPath: normalize(relative(resolve("."), policyRuntime.policyPath)),
60
+ preset: policy.preset,
61
+ boundaries: {
62
+ fileAccess: policy.fileAccess.mode,
63
+ envAccess: policy.envAccess.mode,
64
+ networkAccess: policy.networkAccess.mode,
65
+ subprocessExecution: policy.subprocessExecution.mode,
66
+ dynamicImports: policy.dynamicImports.mode,
67
+ pluginAccess: policy.pluginAccess.mode,
68
+ },
69
+ };
70
+ }
71
+
72
+ export async function runPermissions(args = []) {
73
+ const options = parseArgs(args);
74
+ const runtime = createPermissionRuntime({ policyPath: options.policy });
75
+
76
+ const summary = summarizePolicy(runtime);
77
+ let decision = null;
78
+
79
+ if (options.kind && options.resource) {
80
+ if (options.mode === "assert") {
81
+ decision = runtime.assert({ kind: options.kind, resource: options.resource });
82
+ } else {
83
+ decision = runtime.check({ kind: options.kind, resource: options.resource });
84
+ }
85
+ }
86
+
87
+ const report = {
88
+ generatedAt: new Date().toISOString(),
89
+ summary,
90
+ decision,
91
+ };
92
+
93
+ if (options.out) {
94
+ mkdirSync(dirname(options.out), { recursive: true });
95
+ writeFileSync(options.out, `${JSON.stringify(report, null, 2)}\n`, "utf8");
96
+ }
97
+
98
+ if (options.json) {
99
+ console.log(JSON.stringify(report, null, 2));
100
+ return;
101
+ }
102
+
103
+ console.log(`permissions policy: ${summary.policyPath}`);
104
+ console.log(`preset: ${summary.preset}`);
105
+ for (const [boundary, mode] of Object.entries(summary.boundaries)) {
106
+ console.log(`${boundary}: ${mode}`);
107
+ }
108
+ if (decision) {
109
+ console.log(`decision: ${decision.allowed ? "allow" : "deny"} (${decision.boundary})`);
110
+ console.log(`reason: ${decision.reason}`);
111
+ }
112
+ }
@@ -0,0 +1,95 @@
1
+ import { mkdirSync, writeFileSync } from "node:fs";
2
+ import { dirname, relative, resolve } from "node:path";
3
+ import { performance } from "node:perf_hooks";
4
+ import { runBuild } from "./build.mjs";
5
+ import { runTypeCheck } from "./typecheck.mjs";
6
+ import { runBench } from "./bench.mjs";
7
+ import { runCompat } from "./compat.mjs";
8
+
9
+ function parseArgs(args = []) {
10
+ const options = {
11
+ command: "build",
12
+ runs: Math.max(1, Number(process.env.FASTSCRIPT_PROFILE_RUNS || 1)),
13
+ out: resolve(".fastscript", "profile-latest.json"),
14
+ };
15
+
16
+ for (let i = 0; i < args.length; i += 1) {
17
+ const arg = args[i];
18
+ if (arg === "--command") {
19
+ options.command = String(args[i + 1] || options.command).toLowerCase();
20
+ i += 1;
21
+ continue;
22
+ }
23
+ if (arg === "--runs") {
24
+ options.runs = Math.max(1, Number(args[i + 1] || options.runs));
25
+ i += 1;
26
+ continue;
27
+ }
28
+ if (arg === "--out") {
29
+ options.out = resolve(args[i + 1] || options.out);
30
+ i += 1;
31
+ continue;
32
+ }
33
+ }
34
+
35
+ return options;
36
+ }
37
+
38
+ function createRunner(command) {
39
+ switch (command) {
40
+ case "build":
41
+ return () => runBuild({ mode: "build" });
42
+ case "typecheck":
43
+ return () => runTypeCheck(["--mode", "pass"]);
44
+ case "bench":
45
+ return () => runBench();
46
+ case "compat":
47
+ return () => runCompat();
48
+ default:
49
+ throw new Error(`profile: unsupported command '${command}'`);
50
+ }
51
+ }
52
+
53
+ export async function runProfile(args = []) {
54
+ const options = parseArgs(args);
55
+ const runner = createRunner(options.command);
56
+ const runs = [];
57
+
58
+ for (let i = 0; i < options.runs; i += 1) {
59
+ const rssBefore = process.memoryUsage().rss;
60
+ const t0 = performance.now();
61
+ await runner();
62
+ const t1 = performance.now();
63
+ const rssAfter = process.memoryUsage().rss;
64
+
65
+ runs.push({
66
+ run: i + 1,
67
+ ms: Number((t1 - t0).toFixed(2)),
68
+ rssBeforeMb: Number((rssBefore / (1024 * 1024)).toFixed(2)),
69
+ rssAfterMb: Number((rssAfter / (1024 * 1024)).toFixed(2)),
70
+ rssDeltaMb: Number(((rssAfter - rssBefore) / (1024 * 1024)).toFixed(2)),
71
+ });
72
+ }
73
+
74
+ const times = runs.map((row) => row.ms).sort((a, b) => a - b);
75
+ const mean = times.reduce((sum, value) => sum + value, 0) / Math.max(1, times.length);
76
+
77
+ const report = {
78
+ generatedAt: new Date().toISOString(),
79
+ command: options.command,
80
+ runs,
81
+ summary: {
82
+ count: runs.length,
83
+ minMs: times[0] || 0,
84
+ medianMs: times[Math.floor(times.length / 2)] || 0,
85
+ maxMs: times[times.length - 1] || 0,
86
+ meanMs: Number(mean.toFixed(2)),
87
+ },
88
+ };
89
+
90
+ mkdirSync(dirname(options.out), { recursive: true });
91
+ writeFileSync(options.out, `${JSON.stringify(report, null, 2)}\n`, "utf8");
92
+
93
+ console.log(`profile complete: command=${options.command}, runs=${runs.length}, mean=${report.summary.meanMs}ms`);
94
+ console.log(`profile report: ${String(relative(resolve("."), options.out)).replace(/\\/g, "/")}`);
95
+ }
@@ -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
+ }