fastscript 0.1.1 → 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.
- package/CHANGELOG.md +7 -3
- package/README.md +15 -0
- package/package.json +10 -3
- package/src/build.mjs +15 -3
- package/src/compat.mjs +10 -2
- package/src/create.mjs +6 -6
- package/src/db-cli.mjs +9 -13
- package/src/fs-normalize.mjs +215 -7
- package/src/language-spec.mjs +58 -0
- package/src/module-loader.mjs +47 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
##
|
|
4
|
-
-
|
|
5
|
-
|
|
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
|
+
|
|
6
8
|
## v0.1.1 - 2026-04-13
|
|
7
9
|
- release prep
|
|
8
10
|
|
|
11
|
+
## v0.1.0 - 2026-04-13
|
|
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,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fastscript",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"description": "JavaScript-first full-stack framework that is simpler and faster.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -40,9 +40,16 @@
|
|
|
40
40
|
"test:validation": "node ./scripts/test-validation.mjs",
|
|
41
41
|
"test:webhook-storage": "node ./scripts/test-webhook-storage.mjs",
|
|
42
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",
|
|
43
49
|
"bench:report": "node ./scripts/bench-report.mjs",
|
|
44
|
-
"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",
|
|
45
|
-
"qa:
|
|
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",
|
|
46
53
|
"release:patch": "node ./scripts/release.mjs patch",
|
|
47
54
|
"release:minor": "node ./scripts/release.mjs minor",
|
|
48
55
|
"release:major": "node ./scripts/release.mjs major",
|
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 {
|
|
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:
|
|
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 {
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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
|
|
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
|
-
|
|
36
|
-
|
|
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
|
|
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
|
|
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
|
-
|
package/src/fs-normalize.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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$<>\[\]
|
|
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$<>\[\]
|
|
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
|
+
}
|