fastscript 0.1.0 → 1.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.
Files changed (71) hide show
  1. package/CHANGELOG.md +9 -2
  2. package/README.md +15 -0
  3. package/package.json +19 -5
  4. package/src/build.mjs +15 -3
  5. package/src/compat.mjs +10 -2
  6. package/src/create.mjs +6 -6
  7. package/src/db-cli.mjs +9 -13
  8. package/src/fs-normalize.mjs +215 -7
  9. package/src/language-spec.mjs +58 -0
  10. package/src/module-loader.mjs +47 -0
  11. package/.github/workflows/ci.yml +0 -17
  12. package/Dockerfile +0 -9
  13. package/app/api/auth.js +0 -10
  14. package/app/api/hello.js +0 -3
  15. package/app/api/upload.js +0 -9
  16. package/app/api/webhook.js +0 -10
  17. package/app/db/migrations/001_init.js +0 -6
  18. package/app/db/seed.js +0 -5
  19. package/app/env.schema.js +0 -6
  20. package/app/middleware.fs +0 -7
  21. package/app/pages/404.fs +0 -3
  22. package/app/pages/_layout.fs +0 -17
  23. package/app/pages/benchmarks.fs +0 -15
  24. package/app/pages/docs/index.fs +0 -16
  25. package/app/pages/index.fs +0 -22
  26. package/app/pages/private.fs +0 -12
  27. package/app/pages/showcase.fs +0 -14
  28. package/app/styles.css +0 -14
  29. package/docs/AI_CONTEXT_PACK_V1.md +0 -25
  30. package/docs/DEPLOY_GUIDE.md +0 -14
  31. package/docs/INCIDENT_PLAYBOOK.md +0 -18
  32. package/docs/INTEROP_RULES.md +0 -7
  33. package/docs/PLUGIN_API_CONTRACT.md +0 -22
  34. package/ecosystem.config.cjs +0 -1
  35. package/examples/fullstack/README.md +0 -10
  36. package/examples/fullstack/app/api/orders.js +0 -8
  37. package/examples/fullstack/app/db/migrations/001_init.js +0 -3
  38. package/examples/fullstack/app/db/seed.js +0 -3
  39. package/examples/fullstack/app/jobs/send-order-email.js +0 -4
  40. package/examples/fullstack/app/pages/_layout.fs +0 -1
  41. package/examples/fullstack/app/pages/index.fs +0 -3
  42. package/examples/startup-mvp/README.md +0 -8
  43. package/examples/startup-mvp/app/api/cart.js +0 -9
  44. package/examples/startup-mvp/app/api/checkout.js +0 -8
  45. package/examples/startup-mvp/app/db/migrations/001_products.js +0 -6
  46. package/examples/startup-mvp/app/jobs/send-receipt.js +0 -4
  47. package/examples/startup-mvp/app/pages/_layout.fs +0 -3
  48. package/examples/startup-mvp/app/pages/dashboard/index.fs +0 -9
  49. package/examples/startup-mvp/app/pages/index.fs +0 -18
  50. package/scripts/bench-report.mjs +0 -36
  51. package/scripts/release.mjs +0 -21
  52. package/scripts/smoke-dev.mjs +0 -78
  53. package/scripts/smoke-start.mjs +0 -41
  54. package/scripts/test-auth.mjs +0 -26
  55. package/scripts/test-db.mjs +0 -31
  56. package/scripts/test-jobs.mjs +0 -15
  57. package/scripts/test-middleware.mjs +0 -37
  58. package/scripts/test-roundtrip.mjs +0 -44
  59. package/scripts/test-validation.mjs +0 -17
  60. package/scripts/test-webhook-storage.mjs +0 -22
  61. package/spec/FASTSCRIPT_1000_BUILD_LIST.md +0 -1090
  62. package/vercel.json +0 -15
  63. package/vscode/fastscript-language/README.md +0 -12
  64. package/vscode/fastscript-language/extension.js +0 -24
  65. package/vscode/fastscript-language/language-configuration.json +0 -6
  66. package/vscode/fastscript-language/lsp/server.cjs +0 -27
  67. package/vscode/fastscript-language/lsp/smoke-test.cjs +0 -1
  68. package/vscode/fastscript-language/package.json +0 -36
  69. package/vscode/fastscript-language/snippets/fastscript.code-snippets +0 -24
  70. package/vscode/fastscript-language/syntaxes/fastscript.tmLanguage.json +0 -21
  71. package/wrangler.toml +0 -5
package/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## v1.0.0 - 2026-04-14
4
+ - close v1 tranche 1-44 (language spec + lenient parser/normalizer hardening)
5
+ - add deterministic/runtime-contract/parser-fuzz/stress/spec tests and CI cross-platform gates
6
+ - add language diagnostics/telemetry APIs and benchmark baseline docs
7
+
8
+ ## v0.1.1 - 2026-04-13
9
+ - release prep
10
+
3
11
  ## v0.1.0 - 2026-04-13
4
- - FastScript JS-first full-stack core
5
-
12
+ - FastScript JS-first full-stack core
package/README.md CHANGED
@@ -27,7 +27,14 @@ npm run db:seed
27
27
  npm run smoke:dev
28
28
  npm run smoke:start
29
29
  npm run test:core
30
+ npm run test:language-spec
31
+ npm run test:parser-fuzz
32
+ npm run test:normalizer-stress
33
+ npm run test:determinism
34
+ npm run test:runtime-contract
35
+ npm run bench:language
30
36
  npm run bench:report
37
+ npm run qa:gate
31
38
  npm run qa:all
32
39
  npm run worker
33
40
  npm run deploy:node
@@ -48,7 +55,14 @@ npm run pack:check
48
55
  - `npm run smoke:dev`: automated SSR/API/auth/middleware smoke test
49
56
  - `npm run smoke:start`: production `fastscript start` smoke test
50
57
  - `npm run test:core`: middleware/auth/db/migration round-trip tests
58
+ - `npm run test:language-spec`: validates language spec API and strict diagnostics
59
+ - `npm run test:parser-fuzz`: randomized parser/normalizer robustness sweep
60
+ - `npm run test:normalizer-stress`: load/stress test for lenient parser pipeline
61
+ - `npm run test:determinism`: build determinism check
62
+ - `npm run test:runtime-contract`: route/runtime manifest contract test
63
+ - `npm run bench:language`: writes parser/normalizer benchmark baseline
51
64
  - `npm run bench:report`: writes benchmark report to `benchmarks/latest-report.md`
65
+ - `npm run qa:gate`: validation + full test suite for merge gating
52
66
  - `npm run qa:all`: full quality sweep in one command
53
67
  - `npm run worker`: run queue worker runtime
54
68
  - `npm run deploy:*`: generate deploy adapters for node/vercel/cloudflare
@@ -61,6 +75,7 @@ npm run pack:check
61
75
  - `docs/PLUGIN_API_CONTRACT.md`
62
76
  - `docs/INCIDENT_PLAYBOOK.md`
63
77
  - `docs/DEPLOY_GUIDE.md`
78
+ - `docs/V1_TRANCHE_1_44.md`
64
79
 
65
80
  ## Project layout
66
81
 
package/package.json CHANGED
@@ -1,11 +1,18 @@
1
1
  {
2
2
  "name": "fastscript",
3
- "version": "0.1.0",
3
+ "version": "1.0.0",
4
4
  "description": "JavaScript-first full-stack framework that is simpler and faster.",
5
+ "license": "MIT",
5
6
  "type": "module",
6
7
  "bin": {
7
- "fastscript": "./src/cli.mjs"
8
+ "fastscript": "src/cli.mjs"
8
9
  },
10
+ "files": [
11
+ "src",
12
+ "README.md",
13
+ "LICENSE",
14
+ "CHANGELOG.md"
15
+ ],
9
16
  "scripts": {
10
17
  "dev": "node ./src/cli.mjs dev",
11
18
  "start": "node ./src/cli.mjs start",
@@ -33,9 +40,16 @@
33
40
  "test:validation": "node ./scripts/test-validation.mjs",
34
41
  "test:webhook-storage": "node ./scripts/test-webhook-storage.mjs",
35
42
  "test:jobs": "node ./scripts/test-jobs.mjs",
43
+ "test:language-spec": "node ./scripts/test-language-spec.mjs",
44
+ "test:parser-fuzz": "node ./scripts/test-parser-fuzz.mjs",
45
+ "test:normalizer-stress": "node ./scripts/test-normalizer-stress.mjs",
46
+ "test:determinism": "node ./scripts/test-determinism.mjs",
47
+ "test:runtime-contract": "node ./scripts/test-runtime-contract.mjs",
48
+ "bench:language": "node ./scripts/bench-language-normalize.mjs",
36
49
  "bench:report": "node ./scripts/bench-report.mjs",
37
- "test:core": "npm run test:middleware && npm run test:auth && npm run test:db && npm run test:validation && npm run test:webhook-storage && npm run test:jobs && npm run test:roundtrip",
38
- "qa:all": "npm run validate && npm run test:core && npm run smoke:dev && npm run smoke:start && npm run bench:report",
50
+ "test:core": "npm run test:middleware && npm run test:auth && npm run test:db && npm run test:validation && npm run test:webhook-storage && npm run test:jobs && npm run test:roundtrip && npm run test:language-spec && npm run test:parser-fuzz && npm run test:normalizer-stress && npm run test:determinism && npm run test:runtime-contract",
51
+ "qa:gate": "npm run validate && npm run test:core",
52
+ "qa:all": "npm run qa:gate && npm run smoke:dev && npm run smoke:start && npm run bench:language && npm run bench:report",
39
53
  "release:patch": "node ./scripts/release.mjs patch",
40
54
  "release:minor": "node ./scripts/release.mjs minor",
41
55
  "release:major": "node ./scripts/release.mjs major",
@@ -47,4 +61,4 @@
47
61
  "dependencies": {
48
62
  "esbuild": "^0.25.11"
49
63
  }
50
- }
64
+ }
package/src/build.mjs CHANGED
@@ -1,7 +1,10 @@
1
1
  import { existsSync, mkdirSync, readdirSync, rmSync, writeFileSync, copyFileSync } from "node:fs";
2
2
  import { dirname, extname, join, relative, resolve } from "node:path";
3
3
  import esbuild from "esbuild";
4
- import { normalizeFastScript } from "./fs-normalize.mjs";
4
+ import {
5
+ createFastScriptDiagnosticError,
6
+ normalizeFastScriptWithTelemetry,
7
+ } from "./fs-normalize.mjs";
5
8
 
6
9
  const APP_DIR = resolve("app");
7
10
  const PAGES_DIR = join(APP_DIR, "pages");
@@ -26,8 +29,18 @@ function fsLoaderPlugin() {
26
29
  build.onLoad({ filter: /\.fs$/ }, async (args) => {
27
30
  const { readFile } = await import("node:fs/promises");
28
31
  const raw = await readFile(args.path, "utf8");
32
+ const result = normalizeFastScriptWithTelemetry(raw, { filename: args.path, strict: false });
33
+ const hardErrors = result.diagnostics.filter((diag) => diag.severity === "error");
34
+ if (hardErrors.length > 0) {
35
+ throw createFastScriptDiagnosticError(hardErrors, { filename: args.path });
36
+ }
37
+ if (process.env.FASTSCRIPT_DEBUG_NORMALIZE === "1") {
38
+ console.log(
39
+ `normalize ${args.path} lines=${result.stats.lineCount} ms=${result.stats.durationMs.toFixed(2)} rx=${result.stats.reactiveToLet} st=${result.stats.stateToLet} fn=${result.stats.fnToFunction}`,
40
+ );
41
+ }
29
42
  return {
30
- contents: normalizeFastScript(raw),
43
+ contents: result.code,
31
44
  loader: "js",
32
45
  };
33
46
  });
@@ -219,4 +232,3 @@ window.addEventListener("popstate", () => render(location.pathname, true));
219
232
  render(location.pathname, false);
220
233
  `;
221
234
  }
222
-
package/src/compat.mjs CHANGED
@@ -1,7 +1,10 @@
1
1
  import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
2
2
  import { join, resolve } from "node:path";
3
3
  import esbuild from "esbuild";
4
- import { normalizeFastScript } from "./fs-normalize.mjs";
4
+ import {
5
+ createFastScriptDiagnosticError,
6
+ normalizeFastScriptWithTelemetry,
7
+ } from "./fs-normalize.mjs";
5
8
 
6
9
  const TMP_DIR = resolve(".fastscript-tmp-compat");
7
10
 
@@ -12,7 +15,12 @@ function fsLoaderPlugin() {
12
15
  build.onLoad({ filter: /\.fs$/ }, async (args) => {
13
16
  const { readFile } = await import("node:fs/promises");
14
17
  const raw = await readFile(args.path, "utf8");
15
- return { contents: normalizeFastScript(raw), loader: "js" };
18
+ const result = normalizeFastScriptWithTelemetry(raw, { filename: args.path, strict: false });
19
+ const hardErrors = result.diagnostics.filter((diag) => diag.severity === "error");
20
+ if (hardErrors.length > 0) {
21
+ throw createFastScriptDiagnosticError(hardErrors, { filename: args.path });
22
+ }
23
+ return { contents: result.code, loader: "js" };
16
24
  });
17
25
  },
18
26
  };
package/src/create.mjs CHANGED
@@ -97,14 +97,14 @@ body { margin: 0; font: 16px/1.6 ui-sans-serif, system-ui; background: #050505;
97
97
  `,
98
98
  },
99
99
  {
100
- path: join(appRoot, "api", "hello.js"),
100
+ path: join(appRoot, "api", "hello.fs"),
101
101
  content: `export async function GET() {
102
102
  return { status: 200, json: { ok: true, message: "Hello from FastScript API" } };
103
103
  }
104
104
  `,
105
105
  },
106
106
  {
107
- path: join(appRoot, "api", "auth.js"),
107
+ path: join(appRoot, "api", "auth.fs"),
108
108
  content: `export const schemas = {
109
109
  POST: { name: "string?" }
110
110
  };
@@ -123,7 +123,7 @@ export async function DELETE(ctx) {
123
123
  `,
124
124
  },
125
125
  {
126
- path: join(appRoot, "api", "upload.js"),
126
+ path: join(appRoot, "api", "upload.fs"),
127
127
  content: `export const schemas = {
128
128
  POST: { key: "string", content: "string" }
129
129
  };
@@ -136,7 +136,7 @@ export async function POST(ctx) {
136
136
  `,
137
137
  },
138
138
  {
139
- path: join(appRoot, "api", "webhook.js"),
139
+ path: join(appRoot, "api", "webhook.fs"),
140
140
  content: `import { verifyWebhookRequest } from "../../src/webhook.mjs";
141
141
 
142
142
  export async function POST(ctx) {
@@ -161,7 +161,7 @@ export async function POST(ctx) {
161
161
  `,
162
162
  },
163
163
  {
164
- path: join(appRoot, "db", "migrations", "001_init.js"),
164
+ path: join(appRoot, "db", "migrations", "001_init.fs"),
165
165
  content: `export async function up(db) {
166
166
  const users = db.collection("users");
167
167
  if (!users.get("u_1")) {
@@ -171,7 +171,7 @@ export async function POST(ctx) {
171
171
  `,
172
172
  },
173
173
  {
174
- path: join(appRoot, "db", "seed.js"),
174
+ path: join(appRoot, "db", "seed.fs"),
175
175
  content: `export async function seed(db) {
176
176
  db.transaction((tx) => {
177
177
  tx.collection("posts").set("hello", { id: "hello", title: "First Post", published: true });
package/src/db-cli.mjs CHANGED
@@ -1,14 +1,10 @@
1
1
  import { existsSync, readdirSync } from "node:fs";
2
2
  import { join, resolve } from "node:path";
3
- import { pathToFileURL } from "node:url";
4
3
  import { createFileDatabase } from "./db.mjs";
4
+ import { importSourceModule } from "./module-loader.mjs";
5
5
 
6
6
  const MIGRATIONS_DIR = resolve("app/db/migrations");
7
- const SEED_FILE = resolve("app/db/seed.js");
8
-
9
- async function importFresh(path) {
10
- return import(`${pathToFileURL(path).href}?t=${Date.now()}`);
11
- }
7
+ const SEED_FILES = [resolve("app/db/seed.fs"), resolve("app/db/seed.js"), resolve("app/db/seed.mjs"), resolve("app/db/seed.cjs")];
12
8
 
13
9
  export async function runDbMigrate() {
14
10
  const db = createFileDatabase({ dir: ".fastscript", name: "appdb" });
@@ -16,10 +12,10 @@ export async function runDbMigrate() {
16
12
  console.log("db migrate: no app/db/migrations directory");
17
13
  return;
18
14
  }
19
- const files = readdirSync(MIGRATIONS_DIR).filter((f) => /\.(js|mjs|cjs)$/.test(f)).sort();
15
+ const files = readdirSync(MIGRATIONS_DIR).filter((f) => /\.(fs|js|mjs|cjs)$/.test(f)).sort();
20
16
  let count = 0;
21
17
  for (const file of files) {
22
- const mod = await importFresh(join(MIGRATIONS_DIR, file));
18
+ const mod = await importSourceModule(join(MIGRATIONS_DIR, file), { platform: "node" });
23
19
  const fn = mod.up ?? mod.default;
24
20
  if (typeof fn === "function") {
25
21
  await fn(db);
@@ -32,14 +28,14 @@ export async function runDbMigrate() {
32
28
 
33
29
  export async function runDbSeed() {
34
30
  const db = createFileDatabase({ dir: ".fastscript", name: "appdb" });
35
- if (!existsSync(SEED_FILE)) {
36
- console.log("db seed: no app/db/seed.js file");
31
+ const seedFile = SEED_FILES.find((p) => existsSync(p));
32
+ if (!seedFile) {
33
+ console.log("db seed: no app/db/seed file");
37
34
  return;
38
35
  }
39
- const mod = await importFresh(SEED_FILE);
36
+ const mod = await importSourceModule(seedFile, { platform: "node" });
40
37
  const fn = mod.seed ?? mod.default;
41
- if (typeof fn !== "function") throw new Error("app/db/seed.js must export seed(db) or default(db)");
38
+ if (typeof fn !== "function") throw new Error("app/db/seed must export seed(db) or default(db)");
42
39
  await fn(db);
43
40
  console.log("db seed complete");
44
41
  }
45
-
@@ -1,24 +1,43 @@
1
1
  const reactiveDeclPattern = /^(\s*)~([A-Za-z_$][\w$]*)(\s*=\s*.*)$/;
2
2
  const stateDeclPattern = /^(\s*)state\s+([A-Za-z_$][\w$]*)(\s*=\s*.*)$/;
3
3
  const fnDeclPattern = /^(\s*)(export\s+)?fn\s+([A-Za-z_$][\w$]*)\s*\(/;
4
+ const reactiveStartPattern = /^\s*~/;
5
+ const stateStartPattern = /^\s*state\b/;
6
+ const emptyImportClausePattern = /^\s*import\s*\{\s*\}\s*from\s*["'][^"']+["']/;
7
+ const invalidFnPattern = /^\s*(export\s+)?fn\b(?!\s+[A-Za-z_$][\w$]*\s*\()/;
8
+ const relativeJsImportPattern = /from\s+["'](\.?\.?\/[^"']+)\.(js|jsx|ts|tsx)["']/;
4
9
 
5
- export function normalizeFastScript(source) {
10
+ function nowMs() {
11
+ if (typeof performance !== "undefined" && typeof performance.now === "function") {
12
+ return performance.now();
13
+ }
14
+ return Date.now();
15
+ }
16
+
17
+ function toDiag(severity, line, col, code, message, fix = null) {
18
+ return { severity, line, col, code, message, fix };
19
+ }
20
+
21
+ function normalizeFastScriptInternal(source, stats) {
6
22
  const lines = source.split(/\r?\n/);
7
23
  const out = [];
8
24
 
9
25
  for (const line of lines) {
10
26
  const m = line.match(reactiveDeclPattern);
11
27
  if (m) {
28
+ stats.reactiveToLet += 1;
12
29
  out.push(`${m[1]}let ${m[2]}${m[3]}`);
13
30
  continue;
14
31
  }
15
32
  const s = line.match(stateDeclPattern);
16
33
  if (s) {
34
+ stats.stateToLet += 1;
17
35
  out.push(`${s[1]}let ${s[2]}${s[3]}`);
18
36
  continue;
19
37
  }
20
38
  const f = line.match(fnDeclPattern);
21
39
  if (f) {
40
+ stats.fnToFunction += 1;
22
41
  out.push(line.replace(fnDeclPattern, `${f[1]}${f[2] ?? ""}function ${f[3]}(`));
23
42
  continue;
24
43
  }
@@ -28,6 +47,189 @@ export function normalizeFastScript(source) {
28
47
  return out.join("\n");
29
48
  }
30
49
 
50
+ export function analyzeFastScriptSource(source, { filename = "input.fs" } = {}) {
51
+ const lines = source.split(/\r?\n/);
52
+ const diagnostics = [];
53
+ const declaredState = new Map();
54
+ const isFs = /\.fs$/i.test(filename);
55
+
56
+ for (let i = 0; i < lines.length; i += 1) {
57
+ const lineNo = i + 1;
58
+ const line = lines[i];
59
+
60
+ if (!line.trim()) continue;
61
+
62
+ const reactive = line.match(reactiveDeclPattern);
63
+ if (reactive) {
64
+ const name = reactive[2];
65
+ const first = declaredState.get(name);
66
+ if (first !== undefined) {
67
+ diagnostics.push(
68
+ toDiag(
69
+ "warning",
70
+ lineNo,
71
+ Math.max(1, line.indexOf(name) + 1),
72
+ "FS_DUP_STATE",
73
+ `Duplicate reactive/state declaration "${name}" (first seen on line ${first})`,
74
+ "Rename one declaration to avoid shadowing confusion.",
75
+ ),
76
+ );
77
+ } else {
78
+ declaredState.set(name, lineNo);
79
+ }
80
+ continue;
81
+ }
82
+
83
+ const state = line.match(stateDeclPattern);
84
+ if (state) {
85
+ const name = state[2];
86
+ const first = declaredState.get(name);
87
+ if (first !== undefined) {
88
+ diagnostics.push(
89
+ toDiag(
90
+ "warning",
91
+ lineNo,
92
+ Math.max(1, line.indexOf(name) + 1),
93
+ "FS_DUP_STATE",
94
+ `Duplicate reactive/state declaration "${name}" (first seen on line ${first})`,
95
+ "Rename one declaration to avoid shadowing confusion.",
96
+ ),
97
+ );
98
+ } else {
99
+ declaredState.set(name, lineNo);
100
+ }
101
+ continue;
102
+ }
103
+
104
+ if (reactiveStartPattern.test(line)) {
105
+ diagnostics.push(
106
+ toDiag(
107
+ "warning",
108
+ lineNo,
109
+ Math.max(1, line.indexOf("~") + 1),
110
+ "FS_BAD_REACTIVE",
111
+ "Reactive declaration expected format: ~name = value",
112
+ "Use: ~count = 0",
113
+ ),
114
+ );
115
+ }
116
+
117
+ if (stateStartPattern.test(line) && !stateDeclPattern.test(line)) {
118
+ diagnostics.push(
119
+ toDiag(
120
+ "warning",
121
+ lineNo,
122
+ Math.max(1, line.indexOf("state") + 1),
123
+ "FS_BAD_STATE",
124
+ "State declaration expected format: state name = value",
125
+ "Use: state count = 0",
126
+ ),
127
+ );
128
+ }
129
+
130
+ if (emptyImportClausePattern.test(line)) {
131
+ diagnostics.push(
132
+ toDiag(
133
+ "error",
134
+ lineNo,
135
+ Math.max(1, line.indexOf("import") + 1),
136
+ "FS_EMPTY_IMPORT",
137
+ "Empty import clause is invalid.",
138
+ 'Remove import or import at least one symbol: import { x } from "./mod.fs"',
139
+ ),
140
+ );
141
+ }
142
+
143
+ if (invalidFnPattern.test(line)) {
144
+ diagnostics.push(
145
+ toDiag(
146
+ "warning",
147
+ lineNo,
148
+ Math.max(1, line.indexOf("fn") + 1),
149
+ "FS_BAD_FN",
150
+ 'Function shorthand expected format: fn name(args) { ... }',
151
+ "Use: fn run(input) { ... }",
152
+ ),
153
+ );
154
+ }
155
+
156
+ if (isFs && relativeJsImportPattern.test(line)) {
157
+ diagnostics.push(
158
+ toDiag(
159
+ "warning",
160
+ lineNo,
161
+ Math.max(1, line.indexOf("from") + 1),
162
+ "FS_RELATIVE_JS_IMPORT",
163
+ "Relative .js/.ts import inside .fs can hinder migration portability.",
164
+ "Prefer extensionless imports or .fs extension for local modules.",
165
+ ),
166
+ );
167
+ }
168
+ }
169
+
170
+ return diagnostics;
171
+ }
172
+
173
+ export function formatFastScriptDiagnostics(
174
+ diagnostics,
175
+ { filename = "input.fs", includeWarnings = true } = {},
176
+ ) {
177
+ const filtered = includeWarnings ? diagnostics : diagnostics.filter((d) => d.severity === "error");
178
+ if (filtered.length === 0) return "";
179
+ const lines = filtered.map((d) => {
180
+ const sev = d.severity.toUpperCase();
181
+ const fix = d.fix ? `\n fix: ${d.fix}` : "";
182
+ return `[${sev}] ${d.code} ${filename}:${d.line}:${d.col}\n ${d.message}${fix}`;
183
+ });
184
+ return lines.join("\n");
185
+ }
186
+
187
+ export function createFastScriptDiagnosticError(
188
+ diagnostics,
189
+ { filename = "input.fs", includeWarnings = false } = {},
190
+ ) {
191
+ const printable = formatFastScriptDiagnostics(diagnostics, { filename, includeWarnings });
192
+ const error = new Error(printable || `FastScript diagnostics failed for ${filename}`);
193
+ error.code = "FASTSCRIPT_DIAGNOSTICS";
194
+ error.diagnostics = diagnostics;
195
+ return error;
196
+ }
197
+
198
+ export function normalizeFastScriptWithTelemetry(source, { filename = "input.fs", strict = false } = {}) {
199
+ const startedAt = nowMs();
200
+ const diagnostics = analyzeFastScriptSource(source, { filename });
201
+ const errors = diagnostics.filter((d) => d.severity === "error");
202
+ if (strict && errors.length > 0) {
203
+ throw createFastScriptDiagnosticError(errors, { filename, includeWarnings: false });
204
+ }
205
+
206
+ const transformStats = {
207
+ reactiveToLet: 0,
208
+ stateToLet: 0,
209
+ fnToFunction: 0,
210
+ };
211
+ const code = normalizeFastScriptInternal(source, transformStats);
212
+ const durationMs = nowMs() - startedAt;
213
+
214
+ return {
215
+ code,
216
+ diagnostics,
217
+ stats: {
218
+ lineCount: source.split(/\r?\n/).length,
219
+ durationMs,
220
+ ...transformStats,
221
+ },
222
+ };
223
+ }
224
+
225
+ export function normalizeFastScript(source) {
226
+ return normalizeFastScriptInternal(source, {
227
+ reactiveToLet: 0,
228
+ stateToLet: 0,
229
+ fnToFunction: 0,
230
+ });
231
+ }
232
+
31
233
  export function stripTypeScriptHints(source) {
32
234
  const lines = source.split(/\r?\n/);
33
235
  const out = [];
@@ -51,20 +253,26 @@ export function stripTypeScriptHints(source) {
51
253
 
52
254
  if (/^\s*interface\s+[A-Za-z_$][\w$]*\s*[{]/.test(next) || /^\s*enum\s+[A-Za-z_$][\w$]*\s*[{]/.test(next)) {
53
255
  out.push(`// ${next.trim()} (removed by fastscript migrate)`);
54
- skippingBlock = true;
55
256
  const opens = (next.match(/{/g) || []).length;
56
257
  const closes = (next.match(/}/g) || []).length;
57
- blockDepth = Math.max(1, opens - closes);
258
+ const depth = opens - closes;
259
+ if (depth > 0) {
260
+ skippingBlock = true;
261
+ blockDepth = depth;
262
+ }
58
263
  continue;
59
264
  }
60
265
 
61
266
  if (/^\s*type\s+[A-Za-z_$][\w$]*\s*=/.test(next)) {
62
267
  out.push(`// ${next.trim()} (removed by fastscript migrate)`);
63
268
  if (!next.includes(";") && next.includes("{")) {
64
- skippingBlock = true;
65
269
  const opens = (next.match(/{/g) || []).length;
66
270
  const closes = (next.match(/}/g) || []).length;
67
- blockDepth = Math.max(1, opens - closes);
271
+ const depth = opens - closes;
272
+ if (depth > 0) {
273
+ skippingBlock = true;
274
+ blockDepth = depth;
275
+ }
68
276
  }
69
277
  continue;
70
278
  }
@@ -80,12 +288,12 @@ export function stripTypeScriptHints(source) {
80
288
  if (/\bfunction\b/.test(next) || /\)\s*=>/.test(next)) {
81
289
  next = next.replace(/\(([^)]*)\)/, (_, params) => {
82
290
  const cleaned = params.replace(
83
- /([A-Za-z_$][\w$]*)\s*:\s*([A-Za-z_$][\w$<>\[\]\|&, ?.]*)/g,
291
+ /([A-Za-z_$][\w$]*)\s*:\s*([A-Za-z_$][\w$<>\[\]\|& ?. ]*)/g,
84
292
  "$1",
85
293
  );
86
294
  return `(${cleaned})`;
87
295
  });
88
- next = next.replace(/\)\s*:\s*([A-Za-z_$][\w$<>\[\]\|&, ?.]*)\s*\{/g, ") {");
296
+ next = next.replace(/\)\s*:\s*([A-Za-z_$][\w$<>\[\]\|& ?. ]*)\s*\{/g, ") {");
89
297
  next = next.replace(/\bfunction\s+([A-Za-z_$][\w$]*)\s*<[^>]+>\s*\(/g, "function $1(");
90
298
  }
91
299
 
@@ -0,0 +1,58 @@
1
+ import {
2
+ analyzeFastScriptSource,
3
+ createFastScriptDiagnosticError,
4
+ formatFastScriptDiagnostics,
5
+ normalizeFastScriptWithTelemetry,
6
+ } from "./fs-normalize.mjs";
7
+
8
+ export const FASTSCRIPT_LANGUAGE_SPEC_VERSION = "v1.0";
9
+
10
+ export const FASTSCRIPT_LANGUAGE_SPEC = Object.freeze({
11
+ version: FASTSCRIPT_LANGUAGE_SPEC_VERSION,
12
+ goals: [
13
+ "Lenient syntax that compiles to JavaScript quickly",
14
+ "JS ecosystem compatibility first",
15
+ ".fs primary surface with .js zero-friction support",
16
+ ],
17
+ sugar: [
18
+ "~name = value -> let name = value",
19
+ "state name = value -> let name = value",
20
+ "fn add(a, b) -> function add(a, b)",
21
+ ],
22
+ diagnostics: {
23
+ errors: ["FS_EMPTY_IMPORT"],
24
+ warnings: [
25
+ "FS_BAD_REACTIVE",
26
+ "FS_BAD_STATE",
27
+ "FS_BAD_FN",
28
+ "FS_DUP_STATE",
29
+ "FS_RELATIVE_JS_IMPORT",
30
+ ],
31
+ },
32
+ });
33
+
34
+ export function getLanguageSpec() {
35
+ return FASTSCRIPT_LANGUAGE_SPEC;
36
+ }
37
+
38
+ export function inspectFastScriptSource(source, { filename = "input.fs" } = {}) {
39
+ const diagnostics = analyzeFastScriptSource(source, { filename });
40
+ const printable = formatFastScriptDiagnostics(diagnostics, { filename, includeWarnings: true });
41
+ return {
42
+ ok: diagnostics.every((diag) => diag.severity !== "error"),
43
+ diagnostics,
44
+ printable,
45
+ };
46
+ }
47
+
48
+ export function compileFastScriptSource(source, { filename = "input.fs", strict = false } = {}) {
49
+ return normalizeFastScriptWithTelemetry(source, { filename, strict });
50
+ }
51
+
52
+ export function assertFastScriptSource(source, { filename = "input.fs" } = {}) {
53
+ const diagnostics = analyzeFastScriptSource(source, { filename });
54
+ const errors = diagnostics.filter((diag) => diag.severity === "error");
55
+ if (errors.length > 0) throw createFastScriptDiagnosticError(errors, { filename, includeWarnings: false });
56
+ return diagnostics;
57
+ }
58
+
@@ -0,0 +1,47 @@
1
+ import { extname } from "node:path";
2
+ import { pathToFileURL } from "node:url";
3
+ import esbuild from "esbuild";
4
+ import {
5
+ createFastScriptDiagnosticError,
6
+ normalizeFastScriptWithTelemetry,
7
+ } from "./fs-normalize.mjs";
8
+
9
+ function fsLoaderPlugin() {
10
+ return {
11
+ name: "fastscript-fs-loader",
12
+ setup(build) {
13
+ build.onLoad({ filter: /\.fs$/ }, async (args) => {
14
+ const { readFile } = await import("node:fs/promises");
15
+ const raw = await readFile(args.path, "utf8");
16
+ const result = normalizeFastScriptWithTelemetry(raw, { filename: args.path, strict: false });
17
+ const hardErrors = result.diagnostics.filter((diag) => diag.severity === "error");
18
+ if (hardErrors.length > 0) {
19
+ throw createFastScriptDiagnosticError(hardErrors, { filename: args.path });
20
+ }
21
+ return { contents: result.code, loader: "js" };
22
+ });
23
+ },
24
+ };
25
+ }
26
+
27
+ export async function importSourceModule(filePath, { platform = "node" } = {}) {
28
+ const ext = extname(filePath).toLowerCase();
29
+ if (ext !== ".fs") {
30
+ return import(`${pathToFileURL(filePath).href}?t=${Date.now()}`);
31
+ }
32
+
33
+ const result = await esbuild.build({
34
+ entryPoints: [filePath],
35
+ bundle: true,
36
+ platform,
37
+ format: "esm",
38
+ write: false,
39
+ logLevel: "silent",
40
+ resolveExtensions: [".fs", ".js", ".mjs", ".cjs", ".json"],
41
+ plugins: [fsLoaderPlugin()],
42
+ loader: { ".fs": "js" },
43
+ });
44
+ const code = result.outputFiles[0].text;
45
+ const dataUrl = `data:text/javascript;base64,${Buffer.from(code).toString("base64")}`;
46
+ return import(dataUrl);
47
+ }
@@ -1,17 +0,0 @@
1
- name: ci
2
-
3
- on:
4
- push:
5
- pull_request:
6
-
7
- jobs:
8
- test:
9
- runs-on: ubuntu-latest
10
- steps:
11
- - uses: actions/checkout@v4
12
- - uses: actions/setup-node@v4
13
- with:
14
- node-version: 20
15
- cache: npm
16
- - run: npm ci
17
- - run: npm run qa:all
package/Dockerfile DELETED
@@ -1,9 +0,0 @@
1
- FROM node:20-alpine
2
- WORKDIR /app
3
- COPY package*.json ./
4
- RUN npm ci --omit=dev
5
- COPY . .
6
- RUN npm run build
7
- ENV NODE_ENV=production
8
- EXPOSE 4173
9
- CMD ["node","./src/cli.mjs","start"]