fastscript 1.0.0 → 2.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 +32 -7
- package/LICENSE +33 -21
- package/README.md +567 -73
- package/node_modules/@fastscript/core-private/BOUNDARY.json +15 -0
- package/node_modules/@fastscript/core-private/README.md +5 -0
- package/node_modules/@fastscript/core-private/package.json +34 -0
- package/node_modules/@fastscript/core-private/src/asset-optimizer.mjs +67 -0
- package/node_modules/@fastscript/core-private/src/audit-log.mjs +50 -0
- package/node_modules/@fastscript/core-private/src/auth-flows.mjs +29 -0
- package/node_modules/@fastscript/core-private/src/auth.mjs +115 -0
- package/node_modules/@fastscript/core-private/src/bench.mjs +45 -0
- package/node_modules/@fastscript/core-private/src/build.mjs +670 -0
- package/node_modules/@fastscript/core-private/src/cache.mjs +248 -0
- package/node_modules/@fastscript/core-private/src/check.mjs +22 -0
- package/node_modules/@fastscript/core-private/src/cli.mjs +95 -0
- package/node_modules/@fastscript/core-private/src/compat.mjs +128 -0
- package/node_modules/@fastscript/core-private/src/create.mjs +278 -0
- package/node_modules/@fastscript/core-private/src/csp.mjs +26 -0
- package/node_modules/@fastscript/core-private/src/db-cli.mjs +185 -0
- package/node_modules/@fastscript/core-private/src/db-postgres-collection.mjs +110 -0
- package/node_modules/@fastscript/core-private/src/db-postgres.mjs +40 -0
- package/node_modules/@fastscript/core-private/src/db.mjs +103 -0
- package/node_modules/@fastscript/core-private/src/deploy.mjs +662 -0
- package/node_modules/@fastscript/core-private/src/dev.mjs +5 -0
- package/node_modules/@fastscript/core-private/src/docs-search.mjs +35 -0
- package/node_modules/@fastscript/core-private/src/env.mjs +118 -0
- package/node_modules/@fastscript/core-private/src/export.mjs +83 -0
- package/node_modules/@fastscript/core-private/src/fs-diagnostics.mjs +70 -0
- package/node_modules/@fastscript/core-private/src/fs-error-codes.mjs +141 -0
- package/node_modules/@fastscript/core-private/src/fs-formatter.mjs +66 -0
- package/node_modules/@fastscript/core-private/src/fs-linter.mjs +274 -0
- package/node_modules/@fastscript/core-private/src/fs-normalize.mjs +91 -0
- package/node_modules/@fastscript/core-private/src/fs-parser.mjs +980 -0
- package/node_modules/@fastscript/core-private/src/generated/docs-search-index.mjs +3182 -0
- package/node_modules/@fastscript/core-private/src/i18n.mjs +25 -0
- package/node_modules/@fastscript/core-private/src/interop.mjs +16 -0
- package/node_modules/@fastscript/core-private/src/jobs.mjs +378 -0
- package/node_modules/@fastscript/core-private/src/logger.mjs +27 -0
- package/node_modules/@fastscript/core-private/src/metrics.mjs +45 -0
- package/node_modules/@fastscript/core-private/src/middleware.mjs +14 -0
- package/node_modules/@fastscript/core-private/src/migrate.mjs +81 -0
- package/node_modules/@fastscript/core-private/src/migration-wizard.mjs +16 -0
- package/node_modules/@fastscript/core-private/src/module-loader.mjs +46 -0
- package/node_modules/@fastscript/core-private/src/oauth-providers.mjs +103 -0
- package/node_modules/@fastscript/core-private/src/observability.mjs +21 -0
- package/node_modules/@fastscript/core-private/src/plugins.mjs +194 -0
- package/node_modules/@fastscript/core-private/src/retention.mjs +57 -0
- package/node_modules/@fastscript/core-private/src/routes.mjs +178 -0
- package/node_modules/@fastscript/core-private/src/scheduler.mjs +104 -0
- package/node_modules/@fastscript/core-private/src/security.mjs +233 -0
- package/node_modules/@fastscript/core-private/src/server-runtime.mjs +849 -0
- package/node_modules/@fastscript/core-private/src/serverless-handler.mjs +20 -0
- package/node_modules/@fastscript/core-private/src/session-policy.mjs +38 -0
- package/node_modules/@fastscript/core-private/src/start.mjs +10 -0
- package/node_modules/@fastscript/core-private/src/storage.mjs +155 -0
- package/node_modules/@fastscript/core-private/src/style-primitives.mjs +538 -0
- package/node_modules/@fastscript/core-private/src/style-system.mjs +461 -0
- package/node_modules/@fastscript/core-private/src/tenant.mjs +55 -0
- package/node_modules/@fastscript/core-private/src/typecheck.mjs +1464 -0
- package/node_modules/@fastscript/core-private/src/validate.mjs +22 -0
- package/node_modules/@fastscript/core-private/src/validation.mjs +88 -0
- package/node_modules/@fastscript/core-private/src/webhook.mjs +81 -0
- package/node_modules/@fastscript/core-private/src/worker.mjs +24 -0
- package/package.json +86 -13
- package/src/asset-optimizer.mjs +67 -0
- package/src/audit-log.mjs +50 -0
- package/src/auth.mjs +1 -115
- package/src/bench.mjs +20 -7
- package/src/build.mjs +1 -234
- package/src/cache.mjs +210 -20
- package/src/cli.mjs +29 -5
- package/src/compat.mjs +8 -10
- package/src/create.mjs +71 -17
- package/src/csp.mjs +26 -0
- package/src/db-cli.mjs +152 -8
- package/src/db-postgres-collection.mjs +110 -0
- package/src/deploy.mjs +1 -65
- package/src/docs-search.mjs +35 -0
- package/src/env.mjs +34 -5
- package/src/fs-diagnostics.mjs +70 -0
- package/src/fs-error-codes.mjs +126 -0
- package/src/fs-formatter.mjs +66 -0
- package/src/fs-linter.mjs +274 -0
- package/src/fs-normalize.mjs +21 -238
- package/src/fs-parser.mjs +1 -0
- package/src/generated/docs-search-index.mjs +3220 -0
- package/src/i18n.mjs +25 -0
- package/src/jobs.mjs +283 -32
- package/src/metrics.mjs +45 -0
- package/src/migration-wizard.mjs +16 -0
- package/src/module-loader.mjs +11 -12
- package/src/oauth-providers.mjs +103 -0
- package/src/plugins.mjs +194 -0
- package/src/retention.mjs +57 -0
- package/src/routes.mjs +178 -0
- package/src/scheduler.mjs +104 -0
- package/src/security.mjs +197 -19
- package/src/server-runtime.mjs +1 -339
- package/src/serverless-handler.mjs +20 -0
- package/src/session-policy.mjs +38 -0
- package/src/storage.mjs +1 -56
- package/src/style-system.mjs +461 -0
- package/src/tenant.mjs +55 -0
- package/src/typecheck.mjs +1 -0
- package/src/validate.mjs +5 -1
- package/src/validation.mjs +14 -5
- package/src/webhook.mjs +1 -71
- package/src/worker.mjs +23 -4
- package/src/language-spec.mjs +0 -58
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { appendFileSync, existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { pathToFileURL } from "node:url";
|
|
4
|
+
import { importSourceModule } from "./module-loader.mjs";
|
|
5
|
+
|
|
6
|
+
function parseDotEnv(raw) {
|
|
7
|
+
const out = {};
|
|
8
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
9
|
+
const trimmed = line.trim();
|
|
10
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
11
|
+
const idx = trimmed.indexOf("=");
|
|
12
|
+
if (idx <= 0) continue;
|
|
13
|
+
const key = trimmed.slice(0, idx).trim();
|
|
14
|
+
let val = trimmed.slice(idx + 1).trim();
|
|
15
|
+
if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) val = val.slice(1, -1);
|
|
16
|
+
out[key] = val;
|
|
17
|
+
}
|
|
18
|
+
return out;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function loadEnv({ root = process.cwd(), mode = process.env.NODE_ENV || "development" } = {}) {
|
|
22
|
+
const profile = process.env.FASTSCRIPT_PROFILE || mode;
|
|
23
|
+
const files = [
|
|
24
|
+
".env",
|
|
25
|
+
`.env.${mode}`,
|
|
26
|
+
profile !== mode ? `.env.${profile}` : null,
|
|
27
|
+
mode === "production" ? ".env.local" : null,
|
|
28
|
+
].filter(Boolean);
|
|
29
|
+
const merged = {};
|
|
30
|
+
const configPath = join(root, "fastscript.config.json");
|
|
31
|
+
if (existsSync(configPath)) {
|
|
32
|
+
try {
|
|
33
|
+
const config = JSON.parse(readFileSync(configPath, "utf8"));
|
|
34
|
+
Object.assign(merged, config?.profiles?.[profile] || {});
|
|
35
|
+
} catch {}
|
|
36
|
+
}
|
|
37
|
+
for (const file of files) {
|
|
38
|
+
const path = join(root, file);
|
|
39
|
+
if (!existsSync(path)) continue;
|
|
40
|
+
Object.assign(merged, parseDotEnv(readFileSync(path, "utf8")));
|
|
41
|
+
}
|
|
42
|
+
for (const [k, v] of Object.entries(merged)) if (process.env[k] === undefined) process.env[k] = v;
|
|
43
|
+
return { mode, profile, values: merged };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function validateEnv(schema = {}, env = process.env) {
|
|
47
|
+
const errors = [];
|
|
48
|
+
const out = {};
|
|
49
|
+
for (const [key, ruleRaw] of Object.entries(schema || {})) {
|
|
50
|
+
const rule = typeof ruleRaw === "string" ? { type: ruleRaw } : { ...(ruleRaw || {}) };
|
|
51
|
+
const type = String(rule.type || "string").replace(/\?$/, "");
|
|
52
|
+
const optional = String(rule.type || "").endsWith("?") || rule.optional === true;
|
|
53
|
+
const value = env[key];
|
|
54
|
+
if (value === undefined || value === "") {
|
|
55
|
+
if (!optional) errors.push(`${key} is required`);
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (type === "int") {
|
|
59
|
+
const n = Number(value);
|
|
60
|
+
if (!Number.isInteger(n)) errors.push(`${key} must be int`); else out[key] = n;
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
if (type === "float" || type === "number") {
|
|
64
|
+
const n = Number(value);
|
|
65
|
+
if (!Number.isFinite(n)) errors.push(`${key} must be number`); else out[key] = n;
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
if (type === "bool" || type === "boolean") {
|
|
69
|
+
if (!["true", "false", true, false].includes(value)) errors.push(`${key} must be boolean`);
|
|
70
|
+
else out[key] = value === true || value === "true";
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
out[key] = String(value);
|
|
74
|
+
}
|
|
75
|
+
if (errors.length) {
|
|
76
|
+
const error = new Error(`Env validation failed: ${errors.join("; ")}`);
|
|
77
|
+
error.status = 500;
|
|
78
|
+
error.details = errors;
|
|
79
|
+
throw error;
|
|
80
|
+
}
|
|
81
|
+
return out;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export async function validateAppEnv({ root = process.cwd() } = {}) {
|
|
85
|
+
const fsSchema = join(root, "app", "env.schema.fs");
|
|
86
|
+
const jsSchema = join(root, "app", "env.schema.js");
|
|
87
|
+
const schemaPath = existsSync(fsSchema) ? fsSchema : jsSchema;
|
|
88
|
+
if (!existsSync(schemaPath)) return null;
|
|
89
|
+
const mod = schemaPath.endsWith('.fs')
|
|
90
|
+
? await importSourceModule(schemaPath, { platform: "node" })
|
|
91
|
+
: await import(`${pathToFileURL(schemaPath).href}?t=${Date.now()}`);
|
|
92
|
+
const schema = mod.schema || mod.default || {};
|
|
93
|
+
return validateEnv(schema, process.env);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function ensureEnvExample() {
|
|
97
|
+
const example = join(process.cwd(), ".env.example");
|
|
98
|
+
if (existsSync(example)) return;
|
|
99
|
+
writeFileSync(example, "# FastScript env template\nSESSION_SECRET=change_me\n", "utf8");
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function appendEnvIfMissing(key, value) {
|
|
103
|
+
const path = join(process.cwd(), ".env");
|
|
104
|
+
const current = existsSync(path) ? readFileSync(path, "utf8") : "";
|
|
105
|
+
if (current.includes(`${key}=`)) return;
|
|
106
|
+
appendFileSync(path, `${key}=${value}\n`, "utf8");
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function listConfigProfiles({ root = process.cwd() } = {}) {
|
|
110
|
+
const path = join(root, "fastscript.config.json");
|
|
111
|
+
if (!existsSync(path)) return [];
|
|
112
|
+
try {
|
|
113
|
+
const config = JSON.parse(readFileSync(path, "utf8"));
|
|
114
|
+
return Object.keys(config.profiles || {});
|
|
115
|
+
} catch {
|
|
116
|
+
return [];
|
|
117
|
+
}
|
|
118
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname, extname, join, relative, resolve } from "node:path";
|
|
3
|
+
import { normalizeFastScript } from "./fs-normalize.mjs";
|
|
4
|
+
|
|
5
|
+
const VALID_TARGETS = new Set(["js", "ts"]);
|
|
6
|
+
|
|
7
|
+
function walk(dir) {
|
|
8
|
+
const out = [];
|
|
9
|
+
if (!existsSync(dir)) return out;
|
|
10
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
11
|
+
const full = join(dir, entry.name);
|
|
12
|
+
if (entry.isDirectory()) out.push(...walk(full));
|
|
13
|
+
else if (entry.isFile()) out.push(full);
|
|
14
|
+
}
|
|
15
|
+
return out;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function rewriteImportsForTarget(source, to) {
|
|
19
|
+
let out = source
|
|
20
|
+
.replace(/from\s+["'](\.\/[^"']+)\.fs["']/g, `from "$1.${to}"`)
|
|
21
|
+
.replace(/from\s+["'](\.\.\/[^"']+)\.fs["']/g, `from "$1.${to}"`)
|
|
22
|
+
.replace(/import\(\s*["'](\.\/[^"']+)\.fs["']\s*\)/g, `import("$1.${to}")`)
|
|
23
|
+
.replace(/import\(\s*["'](\.\.\/[^"']+)\.fs["']\s*\)/g, `import("$1.${to}")`);
|
|
24
|
+
out = out
|
|
25
|
+
.replace(/require\(\s*["'](\.\/[^"']+)\.fs["']\s*\)/g, `require("$1.${to}")`)
|
|
26
|
+
.replace(/require\(\s*["'](\.\.\/[^"']+)\.fs["']\s*\)/g, `require("$1.${to}")`);
|
|
27
|
+
return out;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function toTargetPath(file, to) {
|
|
31
|
+
if (extname(file) === ".fs") return file.replace(/\.fs$/, `.${to}`);
|
|
32
|
+
return file;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function runExport(args = []) {
|
|
36
|
+
let to = "js";
|
|
37
|
+
let out = "exported-app";
|
|
38
|
+
|
|
39
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
40
|
+
const arg = args[i];
|
|
41
|
+
if (arg === "--to") to = (args[i + 1] || "").toLowerCase();
|
|
42
|
+
if (arg === "--out") out = args[i + 1] || out;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (!VALID_TARGETS.has(to)) {
|
|
46
|
+
throw new Error('Invalid export target. Use: fastscript export --to js|ts');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const appDir = resolve("app");
|
|
50
|
+
const outDir = resolve(out);
|
|
51
|
+
if (!existsSync(appDir)) throw new Error("Missing app directory.");
|
|
52
|
+
|
|
53
|
+
rmSync(outDir, { recursive: true, force: true });
|
|
54
|
+
mkdirSync(outDir, { recursive: true });
|
|
55
|
+
|
|
56
|
+
// Copy non-page assets/directories first.
|
|
57
|
+
for (const entry of readdirSync(appDir, { withFileTypes: true })) {
|
|
58
|
+
if (entry.name === "pages") continue;
|
|
59
|
+
const src = join(appDir, entry.name);
|
|
60
|
+
const dest = join(outDir, entry.name);
|
|
61
|
+
if (entry.isDirectory()) cpSync(src, dest, { recursive: true });
|
|
62
|
+
else cpSync(src, dest);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const pageRoot = join(appDir, "pages");
|
|
66
|
+
const pageFiles = walk(pageRoot);
|
|
67
|
+
for (const file of pageFiles) {
|
|
68
|
+
const rel = relative(appDir, file);
|
|
69
|
+
const nextPath = toTargetPath(rel, to);
|
|
70
|
+
const outFile = join(outDir, nextPath);
|
|
71
|
+
mkdirSync(dirname(outFile), { recursive: true });
|
|
72
|
+
|
|
73
|
+
const ext = extname(file);
|
|
74
|
+
let source = readFileSync(file, "utf8");
|
|
75
|
+
if (ext === ".fs") source = normalizeFastScript(source);
|
|
76
|
+
source = rewriteImportsForTarget(source, to);
|
|
77
|
+
if (to === "ts") source = `// @ts-nocheck\n${source}`;
|
|
78
|
+
|
|
79
|
+
writeFileSync(outFile, source, "utf8");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
console.log(`export complete: app -> ${out} (target: ${to})`);
|
|
83
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { formatDiagnostic, parseFastScript } from "./fs-parser.mjs";
|
|
2
|
+
|
|
3
|
+
function displayPath(file) {
|
|
4
|
+
return file || "<memory>";
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function safeLine(lines, line) {
|
|
8
|
+
const index = Math.max(0, Math.min(lines.length - 1, line - 1));
|
|
9
|
+
return lines[index] ?? "";
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function renderPrimarySnippet(diagnostic, source) {
|
|
13
|
+
if (!source || !diagnostic) return "";
|
|
14
|
+
const lines = String(source).split(/\r?\n/);
|
|
15
|
+
const lineText = safeLine(lines, diagnostic.line);
|
|
16
|
+
const markerStart = Math.max(1, diagnostic.column || 1);
|
|
17
|
+
const markerEnd = Math.max(markerStart, diagnostic.endColumn || markerStart);
|
|
18
|
+
const width = Math.max(1, markerEnd - markerStart);
|
|
19
|
+
const pointer = `${" ".repeat(markerStart - 1)}${"^".repeat(width)}`;
|
|
20
|
+
return `${String(diagnostic.line).padStart(4, " ")} | ${lineText}\n | ${pointer}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function formatDiagnosticsReport(diagnostics, { source = "" } = {}) {
|
|
24
|
+
if (!diagnostics?.length) return "";
|
|
25
|
+
const ordered = [...diagnostics].sort((a, b) => {
|
|
26
|
+
const startA = a.span?.start ?? 0;
|
|
27
|
+
const startB = b.span?.start ?? 0;
|
|
28
|
+
if (startA !== startB) return startA - startB;
|
|
29
|
+
return a.code.localeCompare(b.code);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
return ordered
|
|
33
|
+
.map((diagnostic) => {
|
|
34
|
+
const details = [formatDiagnostic(diagnostic)];
|
|
35
|
+
const snippet = renderPrimarySnippet(diagnostic, source);
|
|
36
|
+
if (snippet) details.push(snippet);
|
|
37
|
+
for (const related of diagnostic.related || []) {
|
|
38
|
+
details.push(
|
|
39
|
+
` related ${displayPath(related.file)}:${related.line}:${related.column} ${related.message}`,
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
for (const fix of diagnostic.fixes || []) {
|
|
43
|
+
const text = String(fix.text ?? "").replace(/\n/g, "\\n");
|
|
44
|
+
details.push(` fix ${fix.message || "Apply fix"} -> ${text}`);
|
|
45
|
+
}
|
|
46
|
+
return details.join("\n");
|
|
47
|
+
})
|
|
48
|
+
.join("\n\n");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function analyzeFastScript(source, { file = "", mode = "lenient" } = {}) {
|
|
52
|
+
const ast = parseFastScript(source, { file, mode, recover: true });
|
|
53
|
+
return ast.diagnostics;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function assertFastScript(source, { file = "", mode = "strict" } = {}) {
|
|
57
|
+
const diagnostics = analyzeFastScript(source, { file, mode: "lenient" });
|
|
58
|
+
if (!diagnostics.length) return;
|
|
59
|
+
const blocking = diagnostics.filter((diagnostic) => diagnostic.severity !== "warning");
|
|
60
|
+
if (!blocking.length && mode !== "strict") return;
|
|
61
|
+
|
|
62
|
+
const primary = (blocking.length ? blocking : diagnostics)[0];
|
|
63
|
+
const report = formatDiagnosticsReport(diagnostics, { source });
|
|
64
|
+
const path = displayPath(primary.file);
|
|
65
|
+
const error = new Error(`${path}:${primary.line}:${primary.column} ${primary.code} ${primary.message}\n${report}`);
|
|
66
|
+
error.status = 1;
|
|
67
|
+
error.details = diagnostics;
|
|
68
|
+
error.report = report;
|
|
69
|
+
throw error;
|
|
70
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
export const FS_ERROR_CODES = Object.freeze({
|
|
2
|
+
FS1001: {
|
|
3
|
+
severity: "error",
|
|
4
|
+
message: "Invalid reactive declaration.",
|
|
5
|
+
hint: "Reactive declarations must be `~name = expression`.",
|
|
6
|
+
},
|
|
7
|
+
FS1002: {
|
|
8
|
+
severity: "error",
|
|
9
|
+
message: "Invalid state declaration.",
|
|
10
|
+
hint: "State declarations must be `state name = expression`.",
|
|
11
|
+
},
|
|
12
|
+
FS1003: {
|
|
13
|
+
severity: "error",
|
|
14
|
+
message: "Invalid function declaration.",
|
|
15
|
+
hint: "Function declarations must use `fn name(...) { ... }` or `export fn ...`.",
|
|
16
|
+
},
|
|
17
|
+
FS1004: {
|
|
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.",
|
|
21
|
+
},
|
|
22
|
+
FS1005: {
|
|
23
|
+
severity: "error",
|
|
24
|
+
message: "FastScript parse error.",
|
|
25
|
+
hint: "Check punctuation and statement structure near the reported location.",
|
|
26
|
+
},
|
|
27
|
+
FS1006: {
|
|
28
|
+
severity: "error",
|
|
29
|
+
message: "Invalid identifier.",
|
|
30
|
+
hint: "Identifiers must start with a letter, `_`, or `$`.",
|
|
31
|
+
},
|
|
32
|
+
FS1007: {
|
|
33
|
+
severity: "warning",
|
|
34
|
+
message: "Suspicious `TODO_ERROR` token found.",
|
|
35
|
+
hint: "Remove placeholder tokens before shipping.",
|
|
36
|
+
},
|
|
37
|
+
FS1010: {
|
|
38
|
+
severity: "error",
|
|
39
|
+
message: "Unterminated token.",
|
|
40
|
+
hint: "Close the string, template, regex, or comment that starts near this location.",
|
|
41
|
+
},
|
|
42
|
+
FS1101: {
|
|
43
|
+
severity: "warning",
|
|
44
|
+
message: "Unsupported language directive.",
|
|
45
|
+
hint: "Use directives listed in the FastScript language specification.",
|
|
46
|
+
},
|
|
47
|
+
FS2001: {
|
|
48
|
+
severity: "warning",
|
|
49
|
+
message: "Prefer `const` when a binding is never reassigned.",
|
|
50
|
+
hint: "Use lint autofix to rewrite `let` to `const` when safe.",
|
|
51
|
+
},
|
|
52
|
+
FS2002: {
|
|
53
|
+
severity: "warning",
|
|
54
|
+
message: "Avoid inline `<script>` tags in templates.",
|
|
55
|
+
hint: "Move script behavior into module functions or hydration handlers.",
|
|
56
|
+
},
|
|
57
|
+
FS3001: {
|
|
58
|
+
severity: "error",
|
|
59
|
+
message: "`TODO_ERROR` token is not allowed in committed code.",
|
|
60
|
+
hint: "Delete placeholder tokens before merging.",
|
|
61
|
+
},
|
|
62
|
+
FS3002: {
|
|
63
|
+
severity: "warning",
|
|
64
|
+
message: "`var` is discouraged in FastScript modules.",
|
|
65
|
+
hint: "Replace `var` with `let` or `const`.",
|
|
66
|
+
},
|
|
67
|
+
FS3003: {
|
|
68
|
+
severity: "warning",
|
|
69
|
+
message: "Inline `<script>` tag detected in template literal.",
|
|
70
|
+
hint: "Use external module code instead of embedding scripts in markup strings.",
|
|
71
|
+
},
|
|
72
|
+
FS3004: {
|
|
73
|
+
severity: "warning",
|
|
74
|
+
message: "Binding can be `const`.",
|
|
75
|
+
hint: "Apply the autofix to preserve intent and reduce accidental reassignment.",
|
|
76
|
+
},
|
|
77
|
+
FS4001: {
|
|
78
|
+
severity: "error",
|
|
79
|
+
message: "Duplicate route mapping detected.",
|
|
80
|
+
hint: "Ensure each route path+slot pair is unique.",
|
|
81
|
+
},
|
|
82
|
+
FS4002: {
|
|
83
|
+
severity: "warning",
|
|
84
|
+
message: "Route param is declared but not referenced.",
|
|
85
|
+
hint: "Use `params.<name>` or remove the dynamic segment.",
|
|
86
|
+
},
|
|
87
|
+
FS4101: {
|
|
88
|
+
severity: "error",
|
|
89
|
+
message: "Unknown symbol.",
|
|
90
|
+
hint: "Declare the symbol before use or import it from another module.",
|
|
91
|
+
},
|
|
92
|
+
FS4102: {
|
|
93
|
+
severity: "error",
|
|
94
|
+
message: "Cannot reassign `const` binding.",
|
|
95
|
+
hint: "Change the declaration to `let` or remove the assignment.",
|
|
96
|
+
},
|
|
97
|
+
FS4103: {
|
|
98
|
+
severity: "error",
|
|
99
|
+
message: "Type mismatch in assignment.",
|
|
100
|
+
hint: "Align assigned expression type with the declared/inferred variable type.",
|
|
101
|
+
},
|
|
102
|
+
FS4104: {
|
|
103
|
+
severity: "error",
|
|
104
|
+
message: "Incorrect function argument count.",
|
|
105
|
+
hint: "Pass the required number of arguments or update the function signature.",
|
|
106
|
+
},
|
|
107
|
+
FS4105: {
|
|
108
|
+
severity: "error",
|
|
109
|
+
message: "Incompatible return type.",
|
|
110
|
+
hint: "Ensure all return paths in the function resolve to compatible types.",
|
|
111
|
+
},
|
|
112
|
+
FS4106: {
|
|
113
|
+
severity: "error",
|
|
114
|
+
message: "Attempted to call a non-function value.",
|
|
115
|
+
hint: "Ensure the callee resolves to a function.",
|
|
116
|
+
},
|
|
117
|
+
FS4107: {
|
|
118
|
+
severity: "error",
|
|
119
|
+
message: "Invalid operand types for operator.",
|
|
120
|
+
hint: "Use operands compatible with the operator semantics.",
|
|
121
|
+
},
|
|
122
|
+
FS4108: {
|
|
123
|
+
severity: "error",
|
|
124
|
+
message: "Browser-only symbol used in non-browser runtime context.",
|
|
125
|
+
hint: "Move this code to a browser module or gate it behind runtime-aware logic.",
|
|
126
|
+
},
|
|
127
|
+
FS4109: {
|
|
128
|
+
severity: "error",
|
|
129
|
+
message: "Server-only symbol used in browser runtime context.",
|
|
130
|
+
hint: "Move this code to server/runtime modules or use a shared-safe abstraction.",
|
|
131
|
+
},
|
|
132
|
+
FS4201: {
|
|
133
|
+
severity: "error",
|
|
134
|
+
message: "Symbol is not available in this runtime context.",
|
|
135
|
+
hint: "Use a symbol available in the current context or move code to the correct runtime module.",
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
export function resolveErrorMeta(code) {
|
|
140
|
+
return FS_ERROR_CODES[code] || { severity: "error", message: "FastScript compiler error." };
|
|
141
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { existsSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { extname, join, resolve } from "node:path";
|
|
3
|
+
import { generate } from "astring";
|
|
4
|
+
import { parseFastScript } from "./fs-parser.mjs";
|
|
5
|
+
|
|
6
|
+
function walk(dir) {
|
|
7
|
+
if (!existsSync(dir)) return [];
|
|
8
|
+
const out = [];
|
|
9
|
+
for (const entry of readdirSync(dir)) {
|
|
10
|
+
const full = join(dir, entry);
|
|
11
|
+
const st = statSync(full);
|
|
12
|
+
if (st.isDirectory()) out.push(...walk(full));
|
|
13
|
+
else if (st.isFile()) out.push(full);
|
|
14
|
+
}
|
|
15
|
+
return out;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function shouldThrowForFormat(diagnostics) {
|
|
19
|
+
return diagnostics.some((diagnostic) => diagnostic.severity !== "warning");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function formatFastScriptSource(source, { file = "" } = {}) {
|
|
23
|
+
const parsed = parseFastScript(source, { file, mode: "lenient", recover: true });
|
|
24
|
+
if (!parsed.estree || shouldThrowForFormat(parsed.diagnostics)) {
|
|
25
|
+
return String(source ?? "");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const formatted = generate(parsed.estree, {
|
|
29
|
+
comments: false,
|
|
30
|
+
indent: " ",
|
|
31
|
+
lineEnd: "\n",
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
return formatted.endsWith("\n") ? formatted : `${formatted}\n`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function runFormat(args = []) {
|
|
38
|
+
let target = "app";
|
|
39
|
+
let write = true;
|
|
40
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
41
|
+
if (args[i] === "--path") target = args[i + 1] || target;
|
|
42
|
+
if (args[i] === "--check") write = false;
|
|
43
|
+
if (args[i] === "--write") write = true;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const base = resolve(target);
|
|
47
|
+
const files = walk(base).filter((file) => extname(file) === ".fs");
|
|
48
|
+
let changed = 0;
|
|
49
|
+
|
|
50
|
+
for (const file of files) {
|
|
51
|
+
const current = readFileSync(file, "utf8");
|
|
52
|
+
const next = formatFastScriptSource(current, { file });
|
|
53
|
+
if (current !== next) {
|
|
54
|
+
changed += 1;
|
|
55
|
+
if (write) writeFileSync(file, next, "utf8");
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (!write && changed > 0) {
|
|
60
|
+
const error = new Error(`format check failed: ${changed} file(s) need formatting`);
|
|
61
|
+
error.status = 1;
|
|
62
|
+
throw error;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
console.log(`format ${write ? "write" : "check"} complete: ${files.length} file(s), ${changed} changed`);
|
|
66
|
+
}
|