fastscript 2.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.
@@ -0,0 +1,39 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
+
4
+ function assertField(condition, message) {
5
+ if (!condition) throw new Error(message);
6
+ }
7
+
8
+ export async function runBenchmarkDiscipline({ suitePath = resolve("benchmarks", "suite-latest.json") } = {}) {
9
+ assertField(existsSync(suitePath), `missing benchmark suite: ${suitePath}`);
10
+ const suite = JSON.parse(readFileSync(suitePath, "utf8"));
11
+
12
+ assertField(suite.protocol && typeof suite.protocol === "object", "suite protocol missing");
13
+ assertField(Number.isFinite(suite.protocol.runs) && suite.protocol.runs >= 3, "protocol.runs must be >= 3");
14
+ assertField(Number.isFinite(suite.protocol.sampleIntervalMs) && suite.protocol.sampleIntervalMs > 0, "protocol.sampleIntervalMs invalid");
15
+
16
+ assertField(suite.environment && typeof suite.environment === "object", "suite environment missing");
17
+ for (const key of ["node", "platform", "arch", "cpuModel", "cpuCount", "totalMemoryMb"]) {
18
+ assertField(Boolean(suite.environment[key] || suite.environment[key] === 0), `suite environment missing ${key}`);
19
+ }
20
+
21
+ assertField(Array.isArray(suite.corpora) && suite.corpora.length > 0, "suite corpora missing");
22
+
23
+ for (const corpus of suite.corpora) {
24
+ if (corpus.skipped) continue;
25
+ assertField(Boolean(corpus.id), "corpus id missing");
26
+ assertField(Boolean(corpus.root), `corpus root missing for ${corpus.id}`);
27
+ assertField(Number.isFinite(corpus?.timingsMs?.buildCold?.ms), `corpus buildCold invalid for ${corpus.id}`);
28
+ assertField(Number.isFinite(corpus?.timingsMs?.buildWarm?.p95Trimmed), `corpus buildWarm p95Trimmed invalid for ${corpus.id}`);
29
+ assertField(Number.isFinite(corpus?.timingsMs?.typecheck?.p95Trimmed), `corpus typecheck p95Trimmed invalid for ${corpus.id}`);
30
+ assertField(Number.isFinite(corpus?.bundles?.js) && Number.isFinite(corpus?.bundles?.css), `corpus bundle sizes invalid for ${corpus.id}`);
31
+
32
+ const hard = corpus.hardLimitCheck || {};
33
+ for (const key of ["parsePerFileUnder100ms", "typecheckPer100FilesUnder500ms", "warmBuildUnder2000ms", "coldBuildUnder5000ms", "firstLoadJsUnder5kb"]) {
34
+ assertField(typeof hard[key] === "boolean" || hard[key] === null, `corpus hardLimitCheck.${key} invalid for ${corpus.id}`);
35
+ }
36
+ }
37
+
38
+ console.log("benchmark discipline pass");
39
+ }
package/src/cli.mjs CHANGED
@@ -16,6 +16,14 @@ import { runTypeCheck } from "./typecheck.mjs";
16
16
  import { runFormat } from "./fs-formatter.mjs";
17
17
  import { runLint } from "./fs-linter.mjs";
18
18
  import { runMigrationWizard } from "./migration-wizard.mjs";
19
+ import { runProfile } from "./profile.mjs";
20
+ import { runTrace } from "./trace.mjs";
21
+ import { runDiagnostics } from "./diagnostics.mjs";
22
+ import { runMigrateRollback } from "./migrate-rollback.mjs";
23
+ import { runManifest } from "./conversion-manifest.mjs";
24
+ import { runPermissions } from "./permissions-cli.mjs";
25
+ import { runBenchmarkDiscipline } from "./benchmark-discipline.mjs";
26
+ import { runRegressionGuard } from "./regression-guard.mjs";
19
27
 
20
28
  const [, , command, ...args] = process.argv;
21
29
 
@@ -42,7 +50,16 @@ async function main() {
42
50
  await runCheck();
43
51
  break;
44
52
  case "migrate":
45
- await runMigrate(args[0] ?? "app/pages");
53
+ await runMigrate(args.length ? args : ["app"]);
54
+ break;
55
+ case "convert":
56
+ await runMigrate(args.length ? args : ["app"]);
57
+ break;
58
+ case "migrate:rollback":
59
+ await runMigrateRollback(args);
60
+ break;
61
+ case "manifest":
62
+ await runManifest(args);
46
63
  break;
47
64
  case "wizard:migrate":
48
65
  await runMigrationWizard(args);
@@ -50,6 +67,12 @@ async function main() {
50
67
  case "bench":
51
68
  await runBench();
52
69
  break;
70
+ case "bench:discipline":
71
+ await runBenchmarkDiscipline();
72
+ break;
73
+ case "regression":
74
+ await runRegressionGuard(args);
75
+ break;
53
76
  case "export":
54
77
  await runExport(args);
55
78
  break;
@@ -62,6 +85,18 @@ async function main() {
62
85
  case "typecheck":
63
86
  await runTypeCheck(args);
64
87
  break;
88
+ case "profile":
89
+ await runProfile(args);
90
+ break;
91
+ case "trace":
92
+ await runTrace(args);
93
+ break;
94
+ case "diagnostics":
95
+ await runDiagnostics(args);
96
+ break;
97
+ case "permissions":
98
+ await runPermissions(args);
99
+ break;
65
100
  case "format":
66
101
  await runFormat(args);
67
102
  break;
@@ -85,7 +120,7 @@ async function main() {
85
120
  break;
86
121
  default:
87
122
  console.log("FastScript CLI");
88
- console.log("Commands: create, dev, start, build, ssg, check, migrate, wizard:migrate, bench, export, compat, validate, typecheck, format, lint, db:migrate, db:seed, db:rollback, deploy, worker");
123
+ console.log("Commands: create, dev, start, build, ssg, check, migrate, convert, migrate:rollback, manifest, wizard:migrate, bench, bench:discipline, regression, export, compat, validate, typecheck, profile, trace, diagnostics, permissions, format, lint, db:migrate, db:seed, db:rollback, deploy, worker");
89
124
  }
90
125
  }
91
126
 
@@ -0,0 +1,101 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { dirname, relative, resolve } from "node:path";
3
+
4
+ const DEFAULT_MANIFEST = resolve(".fastscript", "conversion", "latest", "conversion-manifest.json");
5
+
6
+ function normalize(path) {
7
+ return String(path || "").replace(/\\/g, "/");
8
+ }
9
+
10
+ function parseArgs(args = []) {
11
+ const options = {
12
+ manifest: DEFAULT_MANIFEST,
13
+ json: false,
14
+ out: "",
15
+ };
16
+
17
+ for (let i = 0; i < args.length; i += 1) {
18
+ const arg = args[i];
19
+ if (arg === "--manifest") {
20
+ options.manifest = resolve(args[i + 1] || options.manifest);
21
+ i += 1;
22
+ continue;
23
+ }
24
+ if (arg === "--json") {
25
+ options.json = true;
26
+ continue;
27
+ }
28
+ if (arg === "--out") {
29
+ options.out = resolve(args[i + 1] || "");
30
+ i += 1;
31
+ continue;
32
+ }
33
+ }
34
+
35
+ return options;
36
+ }
37
+
38
+ export function loadConversionManifest(manifestPath) {
39
+ const path = resolve(manifestPath || DEFAULT_MANIFEST);
40
+ if (!existsSync(path)) throw new Error(`manifest missing: ${path}`);
41
+ return { path, manifest: JSON.parse(readFileSync(path, "utf8")) };
42
+ }
43
+
44
+ export function summarizeConversionManifest(manifest) {
45
+ const converted = Array.isArray(manifest.convertedFiles) ? manifest.convertedFiles.length : 0;
46
+ const importRewrites = Array.isArray(manifest.importRewrites)
47
+ ? manifest.importRewrites.reduce((sum, item) => sum + Number(item.count || 0), 0)
48
+ : 0;
49
+ const blocked = Array.isArray(manifest.blockedFiles) ? manifest.blockedFiles.length : 0;
50
+ const protectedCount = Array.isArray(manifest.protectedFiles) ? manifest.protectedFiles.length : 0;
51
+ const untouched = Array.isArray(manifest.untouchedFiles) ? manifest.untouchedFiles.length : 0;
52
+
53
+ return {
54
+ spec: manifest.spec || "unknown",
55
+ mode: manifest.mode || "unknown",
56
+ runId: manifest.runId || "unknown",
57
+ generatedAt: manifest.generatedAt || "unknown",
58
+ target: manifest.target || ".",
59
+ dryRun: Boolean(manifest.dryRun),
60
+ convertedFiles: converted,
61
+ importRewrites,
62
+ blockedFiles: blocked,
63
+ protectedFiles: protectedCount,
64
+ untouchedFiles: untouched,
65
+ diffRenameOperations: Number(manifest?.diffPreview?.renameOperationCount || 0),
66
+ diffRewriteOperations: Number(manifest?.diffPreview?.rewriteOperationCount || 0),
67
+ diffDeleteOperations: Number(manifest?.diffPreview?.deleteOperationCount || 0),
68
+ validationFailedChecks: manifest?.validation?.failedChecks || [],
69
+ fidelityStatus: manifest?.fidelity?.status || "unknown",
70
+ fidelityFailedChecks: manifest?.fidelity?.failedChecks || [],
71
+ fidelityFailedProbes: manifest?.fidelity?.failedProbes || [],
72
+ };
73
+ }
74
+
75
+ export async function runManifest(args = []) {
76
+ const options = parseArgs(args);
77
+ const { path, manifest } = loadConversionManifest(options.manifest);
78
+ const summary = summarizeConversionManifest(manifest);
79
+
80
+ if (options.out) {
81
+ mkdirSync(dirname(options.out), { recursive: true });
82
+ writeFileSync(options.out, `${JSON.stringify(summary, null, 2)}\n`, "utf8");
83
+ }
84
+
85
+ if (options.json) {
86
+ console.log(JSON.stringify(summary, null, 2));
87
+ return;
88
+ }
89
+
90
+ console.log(`manifest: ${normalize(relative(resolve("."), path))}`);
91
+ console.log(`run: ${summary.runId} (${summary.mode})`);
92
+ console.log(`target: ${summary.target}`);
93
+ console.log(`converted: ${summary.convertedFiles}`);
94
+ console.log(`import rewrites: ${summary.importRewrites}`);
95
+ console.log(`blocked: ${summary.blockedFiles}`);
96
+ console.log(`protected: ${summary.protectedFiles}`);
97
+ console.log(`untouched: ${summary.untouchedFiles}`);
98
+ console.log(`diff preview -> rename:${summary.diffRenameOperations} rewrite:${summary.diffRewriteOperations} delete:${summary.diffDeleteOperations}`);
99
+ console.log(`validation failed checks: ${summary.validationFailedChecks.length}`);
100
+ console.log(`fidelity: ${summary.fidelityStatus}`);
101
+ }
@@ -0,0 +1,100 @@
1
+ import { mkdirSync, readdirSync, readFileSync, statSync, writeFileSync, existsSync } from "node:fs";
2
+ import { dirname, join, relative, resolve } from "node:path";
3
+ import { analyzeFastScript } from "./fs-diagnostics.mjs";
4
+
5
+ function walk(dir) {
6
+ const out = [];
7
+ if (!existsSync(dir)) return out;
8
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
9
+ const full = join(dir, entry.name);
10
+ if (entry.isDirectory()) {
11
+ if (["node_modules", ".git", "dist", ".fastscript"].includes(entry.name)) continue;
12
+ out.push(...walk(full));
13
+ } else if (entry.isFile()) {
14
+ out.push(full);
15
+ }
16
+ }
17
+ return out;
18
+ }
19
+
20
+ function parseArgs(args = []) {
21
+ const options = {
22
+ path: resolve("app"),
23
+ mode: "report",
24
+ out: resolve(".fastscript", "diagnostics-report.json"),
25
+ };
26
+
27
+ for (let i = 0; i < args.length; i += 1) {
28
+ const arg = args[i];
29
+ if (arg === "--path") {
30
+ options.path = resolve(args[i + 1] || options.path);
31
+ i += 1;
32
+ continue;
33
+ }
34
+ if (arg === "--out") {
35
+ options.out = resolve(args[i + 1] || options.out);
36
+ i += 1;
37
+ continue;
38
+ }
39
+ if (arg === "--mode") {
40
+ const next = (args[i + 1] || "report").toLowerCase();
41
+ options.mode = next === "fail" ? "fail" : "report";
42
+ i += 1;
43
+ }
44
+ }
45
+
46
+ return options;
47
+ }
48
+
49
+ export async function runDiagnostics(args = []) {
50
+ const options = parseArgs(args);
51
+ const files = walk(options.path).filter((file) => file.endsWith(".fs"));
52
+ const diagnostics = [];
53
+
54
+ for (const file of files) {
55
+ const source = readFileSync(file, "utf8");
56
+ const issues = analyzeFastScript(source, { file, mode: "lenient" });
57
+ for (const issue of issues) {
58
+ diagnostics.push({
59
+ file: normalizePath(file),
60
+ code: issue.code,
61
+ severity: issue.severity,
62
+ message: issue.message,
63
+ line: issue.line,
64
+ column: issue.column,
65
+ });
66
+ }
67
+ }
68
+
69
+ const summary = diagnostics.reduce(
70
+ (acc, item) => {
71
+ if (item.severity === "warning") acc.warnings += 1;
72
+ else acc.errors += 1;
73
+ acc.byCode[item.code] = (acc.byCode[item.code] || 0) + 1;
74
+ return acc;
75
+ },
76
+ { errors: 0, warnings: 0, byCode: {} },
77
+ );
78
+
79
+ const report = {
80
+ generatedAt: new Date().toISOString(),
81
+ root: normalizePath(options.path),
82
+ filesScanned: files.length,
83
+ summary,
84
+ diagnostics,
85
+ };
86
+
87
+ mkdirSync(dirname(options.out), { recursive: true });
88
+ writeFileSync(options.out, `${JSON.stringify(report, null, 2)}\n`, "utf8");
89
+
90
+ if (options.mode === "fail" && summary.errors > 0) {
91
+ throw new Error(`diagnostics failed: ${summary.errors} blocking issue(s)`);
92
+ }
93
+
94
+ console.log(`diagnostics complete: files=${files.length}, errors=${summary.errors}, warnings=${summary.warnings}`);
95
+ console.log(`diagnostics report: ${normalizePath(relative(resolve("."), options.out))}`);
96
+ }
97
+
98
+ function normalizePath(path) {
99
+ return String(path || "").replace(/\\/g, "/");
100
+ }
@@ -16,8 +16,8 @@ export const FS_ERROR_CODES = Object.freeze({
16
16
  },
17
17
  FS1004: {
18
18
  severity: "error",
19
- message: "Type declarations are not valid runtime FastScript syntax.",
20
- hint: "Move `type`, `interface`, and `enum` definitions to `.d.ts` files or remove them from `.fs` files.",
19
+ message: "Legacy compatibility frontend conflict.",
20
+ hint: "Ordinary TS type-only syntax in `.fs` should parse. If this appears, treat it as a FastScript compatibility bug.",
21
21
  },
22
22
  FS1005: {
23
23
  severity: "error",
@@ -60,28 +60,58 @@ export function stripTypeScriptHints(source) {
60
60
  continue;
61
61
  }
62
62
 
63
+ if (/^\s*declare\s+(global|module|namespace)\b/.test(next)) {
64
+ out.push(`// ${next.trim()} (removed by fastscript migrate)`);
65
+ if (next.includes("{")) {
66
+ skippingBlock = true;
67
+ const opens = (next.match(/{/g) || []).length;
68
+ const closes = (next.match(/}/g) || []).length;
69
+ blockDepth = Math.max(1, opens - closes);
70
+ }
71
+ continue;
72
+ }
73
+
74
+ if (/^\s*declare\s+/.test(next)) {
75
+ out.push(`// ${next.trim()} (removed by fastscript migrate)`);
76
+ continue;
77
+ }
78
+
63
79
  next = next.replace(/\bimport\s+type\b/g, "import");
64
80
  next = next.replace(/\bexport\s+type\b/g, "export");
81
+ next = next.replace(/\btype\s+([A-Za-z_$][\w$]*)\s+as\s+/g, "$1 as ");
82
+ next = next.replace(/\b(?:public|private|protected|readonly|declare|override|abstract)\s+/g, "");
83
+ next = next.replace(/\s+implements\s+[A-Za-z_$][\w$<>\[\]\|&, ?.]+/g, "");
84
+ next = next.replace(/([A-Za-z_$][\w$]*)!([?:;=,\)])/g, "$1$2");
65
85
 
66
86
  next = next.replace(
67
87
  /^(\s*)(const|let|var)\s+([A-Za-z_$][\w$]*)\s*:\s*([^=;]+)([=;].*)$/,
68
88
  "$1$2 $3 $5",
69
89
  );
90
+ next = next.replace(
91
+ /^(\s*)(const|let|var)\s+([A-Za-z_$][\w$]*)\s*<[^>]+>\s*=\s*/,
92
+ "$1$2 $3 = ",
93
+ );
70
94
 
71
- if (/\bfunction\b/.test(next) || /\)\s*=>/.test(next)) {
95
+ if (/\bfunction\b/.test(next) || /\bfn\b/.test(next) || /\)\s*=>/.test(next)) {
72
96
  next = next.replace(/\(([^)]*)\)/, (_, params) => {
73
97
  const cleaned = params.replace(
74
- /([A-Za-z_$][\w$]*)\s*:\s*([A-Za-z_$][\w$<>\[\]\|&, ?.]*)/g,
98
+ /([A-Za-z_$][\w$]*)(\?)?\s*:\s*([A-Za-z_$][\w$<>\[\]\|&, ?.:{}=]*)/g,
75
99
  "$1",
76
100
  );
77
101
  return `(${cleaned})`;
78
102
  });
79
- next = next.replace(/\)\s*:\s*([A-Za-z_$][\w$<>\[\]\|&, ?.]*)\s*\{/g, ") {");
103
+ next = next.replace(/\)\s*:\s*([A-Za-z_$][\w$<>\[\]\|&, ?.:{}=]*)\s*\{/g, ") {");
104
+ next = next.replace(/\)\s*:\s*([A-Za-z_$][\w$<>\[\]\|&, ?.:{}=]*)\s*=>/g, ") =>");
80
105
  next = next.replace(/\bfunction\s+([A-Za-z_$][\w$]*)\s*<[^>]+>\s*\(/g, "function $1(");
106
+ next = next.replace(/\bfn\s+([A-Za-z_$][\w$]*)\s*<[^>]+>\s*\(/g, "fn $1(");
107
+ next = next.replace(/=\s*async\s*<[^>]+>\s*\(/g, "= async (");
108
+ next = next.replace(/=\s*<[^>]+>\s*\(/g, "= (");
81
109
  }
82
110
 
83
111
  next = next.replace(/^\s*<([A-Za-z_$][\w$,\s]*)>\s*\(/, "(");
84
112
  next = next.replace(/\)\s*=>\s*<[A-Za-z_$][\w$<>\[\]\|&, ?.]*>/g, ") =>");
113
+ next = next.replace(/\s+as\s+const\b/g, "");
114
+ next = next.replace(/\s+as\s+[A-Za-z_$][\w$<>\[\]\|&, ?.:{}=]*/g, "");
85
115
  next = next.replace(/\sas\s+const\b/g, "");
86
116
  next = next.replace(/\s+satisfies\s+[A-Za-z_$][\w$<>\[\]\|&, ?.]*/g, "");
87
117
  out.push(next);