fastscript 0.1.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 (96) hide show
  1. package/.github/workflows/ci.yml +17 -0
  2. package/CHANGELOG.md +5 -0
  3. package/Dockerfile +9 -0
  4. package/LICENSE +21 -0
  5. package/README.md +102 -0
  6. package/app/api/auth.js +10 -0
  7. package/app/api/hello.js +3 -0
  8. package/app/api/upload.js +9 -0
  9. package/app/api/webhook.js +10 -0
  10. package/app/db/migrations/001_init.js +6 -0
  11. package/app/db/seed.js +5 -0
  12. package/app/env.schema.js +6 -0
  13. package/app/middleware.fs +7 -0
  14. package/app/pages/404.fs +3 -0
  15. package/app/pages/_layout.fs +17 -0
  16. package/app/pages/benchmarks.fs +15 -0
  17. package/app/pages/docs/index.fs +16 -0
  18. package/app/pages/index.fs +22 -0
  19. package/app/pages/private.fs +12 -0
  20. package/app/pages/showcase.fs +14 -0
  21. package/app/styles.css +14 -0
  22. package/docs/AI_CONTEXT_PACK_V1.md +25 -0
  23. package/docs/DEPLOY_GUIDE.md +14 -0
  24. package/docs/INCIDENT_PLAYBOOK.md +18 -0
  25. package/docs/INTEROP_RULES.md +7 -0
  26. package/docs/PLUGIN_API_CONTRACT.md +22 -0
  27. package/ecosystem.config.cjs +1 -0
  28. package/examples/fullstack/README.md +10 -0
  29. package/examples/fullstack/app/api/orders.js +8 -0
  30. package/examples/fullstack/app/db/migrations/001_init.js +3 -0
  31. package/examples/fullstack/app/db/seed.js +3 -0
  32. package/examples/fullstack/app/jobs/send-order-email.js +4 -0
  33. package/examples/fullstack/app/pages/_layout.fs +1 -0
  34. package/examples/fullstack/app/pages/index.fs +3 -0
  35. package/examples/startup-mvp/README.md +8 -0
  36. package/examples/startup-mvp/app/api/cart.js +9 -0
  37. package/examples/startup-mvp/app/api/checkout.js +8 -0
  38. package/examples/startup-mvp/app/db/migrations/001_products.js +6 -0
  39. package/examples/startup-mvp/app/jobs/send-receipt.js +4 -0
  40. package/examples/startup-mvp/app/pages/_layout.fs +3 -0
  41. package/examples/startup-mvp/app/pages/dashboard/index.fs +9 -0
  42. package/examples/startup-mvp/app/pages/index.fs +18 -0
  43. package/package.json +50 -0
  44. package/scripts/bench-report.mjs +36 -0
  45. package/scripts/release.mjs +21 -0
  46. package/scripts/smoke-dev.mjs +78 -0
  47. package/scripts/smoke-start.mjs +41 -0
  48. package/scripts/test-auth.mjs +26 -0
  49. package/scripts/test-db.mjs +31 -0
  50. package/scripts/test-jobs.mjs +15 -0
  51. package/scripts/test-middleware.mjs +37 -0
  52. package/scripts/test-roundtrip.mjs +44 -0
  53. package/scripts/test-validation.mjs +17 -0
  54. package/scripts/test-webhook-storage.mjs +22 -0
  55. package/spec/FASTSCRIPT_1000_BUILD_LIST.md +1090 -0
  56. package/src/auth-flows.mjs +29 -0
  57. package/src/auth.mjs +115 -0
  58. package/src/bench.mjs +46 -0
  59. package/src/build.mjs +222 -0
  60. package/src/cache.mjs +58 -0
  61. package/src/check.mjs +22 -0
  62. package/src/cli.mjs +71 -0
  63. package/src/compat.mjs +122 -0
  64. package/src/create.mjs +190 -0
  65. package/src/db-cli.mjs +45 -0
  66. package/src/db-postgres.mjs +40 -0
  67. package/src/db.mjs +103 -0
  68. package/src/deploy.mjs +65 -0
  69. package/src/dev.mjs +5 -0
  70. package/src/env.mjs +89 -0
  71. package/src/export.mjs +83 -0
  72. package/src/fs-normalize.mjs +100 -0
  73. package/src/interop.mjs +16 -0
  74. package/src/jobs.mjs +127 -0
  75. package/src/logger.mjs +27 -0
  76. package/src/middleware.mjs +14 -0
  77. package/src/migrate.mjs +81 -0
  78. package/src/observability.mjs +21 -0
  79. package/src/security.mjs +55 -0
  80. package/src/server-runtime.mjs +339 -0
  81. package/src/start.mjs +10 -0
  82. package/src/storage.mjs +56 -0
  83. package/src/validate.mjs +18 -0
  84. package/src/validation.mjs +79 -0
  85. package/src/webhook.mjs +71 -0
  86. package/src/worker.mjs +5 -0
  87. package/vercel.json +15 -0
  88. package/vscode/fastscript-language/README.md +12 -0
  89. package/vscode/fastscript-language/extension.js +24 -0
  90. package/vscode/fastscript-language/language-configuration.json +6 -0
  91. package/vscode/fastscript-language/lsp/server.cjs +27 -0
  92. package/vscode/fastscript-language/lsp/smoke-test.cjs +1 -0
  93. package/vscode/fastscript-language/package.json +36 -0
  94. package/vscode/fastscript-language/snippets/fastscript.code-snippets +24 -0
  95. package/vscode/fastscript-language/syntaxes/fastscript.tmLanguage.json +21 -0
  96. package/wrangler.toml +5 -0
@@ -0,0 +1,29 @@
1
+ import { randomBytes, scryptSync, timingSafeEqual } from "node:crypto";
2
+
3
+ export function hashPassword(password, salt = randomBytes(16).toString("hex")) {
4
+ const key = scryptSync(password, salt, 64).toString("hex");
5
+ return `${salt}:${key}`;
6
+ }
7
+
8
+ export function verifyPassword(password, hashed) {
9
+ const [salt, keyHex] = String(hashed || "").split(":");
10
+ if (!salt || !keyHex) return false;
11
+ const calc = scryptSync(password, salt, 64);
12
+ const key = Buffer.from(keyHex, "hex");
13
+ if (calc.length !== key.length) return false;
14
+ return timingSafeEqual(calc, key);
15
+ }
16
+
17
+ export function createOAuthState() {
18
+ return randomBytes(20).toString("hex");
19
+ }
20
+
21
+ export function buildOAuthAuthorizeUrl({ authorizeUrl, clientId, redirectUri, scope = "openid profile email", state }) {
22
+ const u = new URL(authorizeUrl);
23
+ u.searchParams.set("response_type", "code");
24
+ u.searchParams.set("client_id", clientId);
25
+ u.searchParams.set("redirect_uri", redirectUri);
26
+ u.searchParams.set("scope", scope);
27
+ u.searchParams.set("state", state || createOAuthState());
28
+ return u.toString();
29
+ }
package/src/auth.mjs ADDED
@@ -0,0 +1,115 @@
1
+ import { createHmac, randomBytes } from "node:crypto";
2
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+
5
+ function readJson(path, fallback) {
6
+ if (!existsSync(path)) return fallback;
7
+ try {
8
+ return JSON.parse(readFileSync(path, "utf8"));
9
+ } catch {
10
+ return fallback;
11
+ }
12
+ }
13
+
14
+ function writeJson(path, value) {
15
+ writeFileSync(path, JSON.stringify(value, null, 2), "utf8");
16
+ }
17
+
18
+ export function parseCookies(header) {
19
+ const out = {};
20
+ if (!header) return out;
21
+ for (const part of header.split(";")) {
22
+ const [k, ...rest] = part.trim().split("=");
23
+ if (!k) continue;
24
+ out[k] = decodeURIComponent(rest.join("=") || "");
25
+ }
26
+ return out;
27
+ }
28
+
29
+ export function serializeCookie(name, value, opts = {}) {
30
+ const bits = [`${name}=${encodeURIComponent(value)}`];
31
+ bits.push(`Path=${opts.path ?? "/"}`);
32
+ if (opts.httpOnly !== false) bits.push("HttpOnly");
33
+ if (opts.sameSite) bits.push(`SameSite=${opts.sameSite}`);
34
+ else bits.push("SameSite=Lax");
35
+ if (opts.secure) bits.push("Secure");
36
+ if (typeof opts.maxAge === "number") bits.push(`Max-Age=${opts.maxAge}`);
37
+ return bits.join("; ");
38
+ }
39
+
40
+ function sign(payload, secret) {
41
+ return createHmac("sha256", secret).update(payload).digest("hex");
42
+ }
43
+
44
+ export function createSessionManager({ dir = ".fastscript", cookieName = "fs_session", secret = "fastscript-dev-secret" } = {}) {
45
+ mkdirSync(dir, { recursive: true });
46
+ const path = join(dir, "sessions.json");
47
+ const state = readJson(path, { sessions: {} });
48
+
49
+ function persist() {
50
+ writeJson(path, state);
51
+ }
52
+
53
+ return {
54
+ cookieName,
55
+ sweepExpired() {
56
+ let removed = 0;
57
+ const now = Date.now();
58
+ for (const [id, row] of Object.entries(state.sessions)) {
59
+ if (!row || row.exp < now) {
60
+ delete state.sessions[id];
61
+ removed += 1;
62
+ }
63
+ }
64
+ if (removed) persist();
65
+ return removed;
66
+ },
67
+ create(user, maxAgeSec = 60 * 60 * 24 * 7) {
68
+ const id = randomBytes(12).toString("hex");
69
+ const exp = Date.now() + maxAgeSec * 1000;
70
+ state.sessions[id] = { user, exp };
71
+ persist();
72
+ const sig = sign(`${id}.${exp}`, secret);
73
+ return `${id}.${exp}.${sig}`;
74
+ },
75
+ read(token) {
76
+ if (!token) return null;
77
+ const [id, expStr, sig] = token.split(".");
78
+ if (!id || !expStr || !sig) return null;
79
+ const expected = sign(`${id}.${expStr}`, secret);
80
+ if (expected !== sig) return null;
81
+ const exp = Number(expStr);
82
+ if (!Number.isFinite(exp) || exp < Date.now()) return null;
83
+ const row = state.sessions[id];
84
+ if (!row || row.exp < Date.now()) {
85
+ if (row) {
86
+ delete state.sessions[id];
87
+ persist();
88
+ }
89
+ return null;
90
+ }
91
+ return row;
92
+ },
93
+ delete(token) {
94
+ const [id] = String(token || "").split(".");
95
+ if (!id) return;
96
+ delete state.sessions[id];
97
+ persist();
98
+ },
99
+ rotate(token, maxAgeSec = 60 * 60 * 24 * 7) {
100
+ const row = this.read(token);
101
+ if (!row?.user) return null;
102
+ this.delete(token);
103
+ return this.create(row.user, maxAgeSec);
104
+ },
105
+ };
106
+ }
107
+
108
+ export function requireUser(user) {
109
+ if (!user) {
110
+ const error = new Error("Unauthorized");
111
+ error.status = 401;
112
+ throw error;
113
+ }
114
+ return user;
115
+ }
package/src/bench.mjs ADDED
@@ -0,0 +1,46 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { gzipSync } from "node:zlib";
3
+ import { join, resolve } from "node:path";
4
+
5
+ const DIST = resolve("dist");
6
+ const JS_BUDGET_BYTES = 30 * 1024;
7
+ const CSS_BUDGET_BYTES = 10 * 1024;
8
+
9
+ function gzipSize(path) {
10
+ if (!existsSync(path)) return 0;
11
+ const raw = readFileSync(path);
12
+ return gzipSync(raw, { level: 9 }).byteLength;
13
+ }
14
+
15
+ function kb(bytes) {
16
+ return `${(bytes / 1024).toFixed(2)}KB`;
17
+ }
18
+
19
+ export async function runBench() {
20
+ const manifestPath = join(DIST, "fastscript-manifest.json");
21
+ if (!existsSync(manifestPath)) {
22
+ throw new Error("Missing dist build output. Run: fastscript build");
23
+ }
24
+
25
+ const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
26
+ const jsAssets = [join(DIST, "router.js")];
27
+ const cssAssets = [join(DIST, "styles.css")];
28
+
29
+ if (manifest.layout) jsAssets.push(join(DIST, manifest.layout.replace(/^\.\//, "")));
30
+ const root = manifest.routes.find((r) => r.path === "/");
31
+ if (root?.module) jsAssets.push(join(DIST, root.module.replace(/^\.\//, "")));
32
+
33
+ const totalJs = jsAssets.reduce((sum, p) => sum + gzipSize(p), 0);
34
+ const totalCss = cssAssets.reduce((sum, p) => sum + gzipSize(p), 0);
35
+
36
+ console.log(`3G budget check -> JS: ${kb(totalJs)} / 30.00KB, CSS: ${kb(totalCss)} / 10.00KB`);
37
+
38
+ const errors = [];
39
+ if (totalJs > JS_BUDGET_BYTES) errors.push(`JS budget exceeded by ${kb(totalJs - JS_BUDGET_BYTES)}`);
40
+ if (totalCss > CSS_BUDGET_BYTES) errors.push(`CSS budget exceeded by ${kb(totalCss - CSS_BUDGET_BYTES)}`);
41
+
42
+ if (errors.length > 0) {
43
+ throw new Error(errors.join("\n"));
44
+ }
45
+ }
46
+
package/src/build.mjs ADDED
@@ -0,0 +1,222 @@
1
+ import { existsSync, mkdirSync, readdirSync, rmSync, writeFileSync, copyFileSync } from "node:fs";
2
+ import { dirname, extname, join, relative, resolve } from "node:path";
3
+ import esbuild from "esbuild";
4
+ import { normalizeFastScript } from "./fs-normalize.mjs";
5
+
6
+ const APP_DIR = resolve("app");
7
+ const PAGES_DIR = join(APP_DIR, "pages");
8
+ const API_DIR = join(APP_DIR, "api");
9
+ const DIST_DIR = resolve("dist");
10
+
11
+ function walk(dir) {
12
+ const out = [];
13
+ if (!existsSync(dir)) return out;
14
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
15
+ const full = join(dir, entry.name);
16
+ if (entry.isDirectory()) out.push(...walk(full));
17
+ else out.push(full);
18
+ }
19
+ return out;
20
+ }
21
+
22
+ function fsLoaderPlugin() {
23
+ return {
24
+ name: "fastscript-fs-loader",
25
+ setup(build) {
26
+ build.onLoad({ filter: /\.fs$/ }, async (args) => {
27
+ const { readFile } = await import("node:fs/promises");
28
+ const raw = await readFile(args.path, "utf8");
29
+ return {
30
+ contents: normalizeFastScript(raw),
31
+ loader: "js",
32
+ };
33
+ });
34
+ },
35
+ };
36
+ }
37
+
38
+ function routeFromPageFile(file) {
39
+ const rel = relative(PAGES_DIR, file).replace(/\\/g, "/").replace(/\.(js|fs)$/, "");
40
+ if (rel === "index") return "/";
41
+ const segs = rel.split("/").filter(Boolean);
42
+ if (segs.at(-1) === "index") segs.pop();
43
+ return "/" + segs.map((s) => (s.startsWith("[") && s.endsWith("]") ? `:${s.slice(1, -1)}` : s)).join("/");
44
+ }
45
+
46
+ function routeFromApiFile(file) {
47
+ const rel = relative(API_DIR, file).replace(/\\/g, "/").replace(/\.(js|fs)$/, "");
48
+ const segs = rel.split("/").filter(Boolean);
49
+ if (segs.at(-1) === "index") segs.pop();
50
+ return "/api/" + segs.join("/");
51
+ }
52
+
53
+ async function compileFile(file, out, platform) {
54
+ mkdirSync(dirname(out), { recursive: true });
55
+ await esbuild.build({
56
+ entryPoints: [file],
57
+ outfile: out,
58
+ bundle: true,
59
+ format: "esm",
60
+ platform,
61
+ sourcemap: true,
62
+ minify: platform === "browser",
63
+ logLevel: "silent",
64
+ resolveExtensions: [".fs", ".js", ".mjs", ".cjs", ".json"],
65
+ plugins: [fsLoaderPlugin()],
66
+ loader: { ".fs": "js" },
67
+ });
68
+ }
69
+
70
+ export async function runBuild() {
71
+ if (!existsSync(PAGES_DIR)) throw new Error("Missing app/pages directory. Run: fastscript create app");
72
+
73
+ rmSync(DIST_DIR, { recursive: true, force: true });
74
+ mkdirSync(DIST_DIR, { recursive: true });
75
+
76
+ const manifest = { routes: [], apiRoutes: [], layout: null, notFound: null, middleware: null };
77
+ const pageFiles = walk(PAGES_DIR).filter((f) => [".js", ".fs"].includes(extname(f)));
78
+
79
+ for (const file of pageFiles) {
80
+ const rel = relative(APP_DIR, file).replace(/\\/g, "/");
81
+ const relModule = rel.replace(/\.fs$/, ".js");
82
+ const relFromPages = relative(PAGES_DIR, file).replace(/\\/g, "/").replace(/\.(js|fs)$/, "");
83
+ const out = join(DIST_DIR, relModule);
84
+
85
+ await compileFile(file, out, "browser");
86
+
87
+ if (relFromPages === "_layout") manifest.layout = `./${relModule}`;
88
+ else if (relFromPages === "404") manifest.notFound = `./${relModule}`;
89
+ else if (!relFromPages.startsWith("_")) manifest.routes.push({ path: routeFromPageFile(file), module: `./${relModule}` });
90
+ }
91
+
92
+ if (existsSync(API_DIR)) {
93
+ const apiFiles = walk(API_DIR).filter((f) => [".js", ".fs"].includes(extname(f)));
94
+ for (const file of apiFiles) {
95
+ const rel = relative(APP_DIR, file).replace(/\\/g, "/");
96
+ const relModule = rel.replace(/\.fs$/, ".js");
97
+ const out = join(DIST_DIR, relModule);
98
+ await compileFile(file, out, "node");
99
+ manifest.apiRoutes.push({ path: routeFromApiFile(file), module: `./${relModule}` });
100
+ }
101
+ }
102
+
103
+ const middlewareSource = [join(APP_DIR, "middleware.fs"), join(APP_DIR, "middleware.js")].find((p) => existsSync(p));
104
+ if (middlewareSource) {
105
+ const rel = relative(APP_DIR, middlewareSource).replace(/\\/g, "/").replace(/\.fs$/, ".js");
106
+ const out = join(DIST_DIR, rel);
107
+ await compileFile(middlewareSource, out, "node");
108
+ manifest.middleware = `./${rel}`;
109
+ }
110
+
111
+ const stylesSrc = join(APP_DIR, "styles.css");
112
+ if (existsSync(stylesSrc)) copyFileSync(stylesSrc, join(DIST_DIR, "styles.css"));
113
+
114
+ writeFileSync(join(DIST_DIR, "fastscript-manifest.json"), JSON.stringify(manifest, null, 2), "utf8");
115
+ writeFileSync(join(DIST_DIR, "router.js"), buildRouterRuntime(), "utf8");
116
+ writeFileSync(join(DIST_DIR, "index.html"), buildIndexHtml(existsSync(stylesSrc)), "utf8");
117
+
118
+ console.log(`built FastScript app with ${manifest.routes.length} page route(s) and ${manifest.apiRoutes.length} api route(s)`);
119
+ }
120
+
121
+ function buildIndexHtml(hasStyles) {
122
+ return `<!doctype html>
123
+ <html>
124
+ <head>
125
+ <meta charset="utf-8" />
126
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
127
+ <title>FastScript</title>
128
+ ${hasStyles ? '<link rel="stylesheet" href="/styles.css" />' : ""}
129
+ </head>
130
+ <body>
131
+ <div id="app"></div>
132
+ <script type="module" src="/router.js"></script>
133
+ </body>
134
+ </html>`;
135
+ }
136
+
137
+ function buildRouterRuntime() {
138
+ return `
139
+ const app = document.getElementById("app");
140
+ const manifest = await fetch("/fastscript-manifest.json").then((r) => r.json());
141
+
142
+ function match(routePath, pathname) {
143
+ const a = routePath.split("/").filter(Boolean);
144
+ const b = pathname.split("/").filter(Boolean);
145
+ if (a.length !== b.length) return null;
146
+ const params = {};
147
+ for (let i = 0; i < a.length; i += 1) {
148
+ if (a[i].startsWith(":")) params[a[i].slice(1)] = b[i];
149
+ else if (a[i] !== b[i]) return null;
150
+ }
151
+ return params;
152
+ }
153
+
154
+ function findRoute(pathname) {
155
+ for (const route of manifest.routes) {
156
+ const params = match(route.path, pathname);
157
+ if (params) return { route, params };
158
+ }
159
+ return null;
160
+ }
161
+
162
+ async function hydrate(mod, ctx) {
163
+ if (typeof mod.hydrate === "function") {
164
+ await mod.hydrate({ ...ctx, root: app });
165
+ }
166
+ }
167
+
168
+ async function render(pathname, force = false) {
169
+ const path = pathname || "/";
170
+ const ssr = globalThis.__FASTSCRIPT_SSR;
171
+ const initialHit = ssr && ssr.pathname === path;
172
+
173
+ const matched = findRoute(path);
174
+ let mod = null;
175
+ let params = {};
176
+ let data = {};
177
+ let html = "";
178
+
179
+ if (matched) {
180
+ params = matched.params;
181
+ mod = await import(matched.route.module);
182
+ }
183
+
184
+ if (initialHit && !force) {
185
+ html = app.innerHTML;
186
+ if (ssr?.data) data = ssr.data;
187
+ } else if (!matched && manifest.notFound) {
188
+ const nfMod = await import(manifest.notFound);
189
+ html = (nfMod.default ? nfMod.default({ pathname: path }) : "<h1>404</h1>") || "";
190
+ } else if (matched) {
191
+ if (typeof mod.load === "function") data = (await mod.load({ params, pathname: path })) || {};
192
+ html = (mod.default ? mod.default({ ...data, params, pathname: path }) : "") || "";
193
+ if (manifest.layout) {
194
+ const layout = await import(manifest.layout);
195
+ html = layout.default ? layout.default({ content: html, pathname: path }) : html;
196
+ }
197
+ app.innerHTML = html;
198
+ }
199
+
200
+ bindLinks();
201
+ if (mod) await hydrate(mod, { pathname: path, params, data });
202
+ globalThis.__FASTSCRIPT_SSR = null;
203
+ }
204
+
205
+ function bindLinks() {
206
+ for (const a of app.querySelectorAll('a[href^="/"]')) {
207
+ if (a.dataset.fsBound === "1") continue;
208
+ a.dataset.fsBound = "1";
209
+ a.addEventListener("click", (e) => {
210
+ e.preventDefault();
211
+ const href = a.getAttribute("href");
212
+ history.pushState({}, "", href);
213
+ render(location.pathname, true);
214
+ });
215
+ }
216
+ }
217
+
218
+ window.addEventListener("popstate", () => render(location.pathname, true));
219
+ render(location.pathname, false);
220
+ `;
221
+ }
222
+
package/src/cache.mjs ADDED
@@ -0,0 +1,58 @@
1
+ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
2
+ import { join, resolve } from "node:path";
3
+
4
+ export function createMemoryCache() {
5
+ const m = new Map();
6
+ return {
7
+ async get(key) {
8
+ const row = m.get(key);
9
+ if (!row) return null;
10
+ if (row.exp && row.exp < Date.now()) {
11
+ m.delete(key);
12
+ return null;
13
+ }
14
+ return row.value;
15
+ },
16
+ async set(key, value, ttlMs = 0) {
17
+ m.set(key, { value, exp: ttlMs ? Date.now() + ttlMs : 0 });
18
+ },
19
+ async del(key) { m.delete(key); },
20
+ async clear() { m.clear(); },
21
+ };
22
+ }
23
+
24
+ export function createFileCache({ dir = ".fastscript/cache" } = {}) {
25
+ const root = resolve(dir);
26
+ mkdirSync(root, { recursive: true });
27
+ const p = (key) => join(root, `${encodeURIComponent(key)}.json`);
28
+ return {
29
+ async get(key) {
30
+ const file = p(key);
31
+ if (!existsSync(file)) return null;
32
+ const row = JSON.parse(readFileSync(file, "utf8"));
33
+ if (row.exp && row.exp < Date.now()) { rmSync(file, { force: true }); return null; }
34
+ return row.value;
35
+ },
36
+ async set(key, value, ttlMs = 0) {
37
+ writeFileSync(p(key), JSON.stringify({ value, exp: ttlMs ? Date.now() + ttlMs : 0 }), "utf8");
38
+ },
39
+ async del(key) { rmSync(p(key), { force: true }); },
40
+ async clear() { rmSync(root, { recursive: true, force: true }); mkdirSync(root, { recursive: true }); },
41
+ };
42
+ }
43
+
44
+ export async function createRedisCache({ url = process.env.REDIS_URL } = {}) {
45
+ const mod = await import("redis");
46
+ const client = mod.createClient({ url });
47
+ await client.connect();
48
+ return {
49
+ async get(key) { return client.get(key); },
50
+ async set(key, value, ttlMs = 0) {
51
+ if (ttlMs > 0) await client.set(key, value, { PX: ttlMs });
52
+ else await client.set(key, value);
53
+ },
54
+ async del(key) { await client.del(key); },
55
+ async clear() { await client.flushDb(); },
56
+ async close() { await client.quit(); },
57
+ };
58
+ }
package/src/check.mjs ADDED
@@ -0,0 +1,22 @@
1
+ import { existsSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
+
4
+ export async function runCheck() {
5
+ const indexExists = existsSync(resolve("app/pages/index.fs")) || existsSync(resolve("app/pages/index.js"));
6
+ const layoutExists = existsSync(resolve("app/pages/_layout.fs")) || existsSync(resolve("app/pages/_layout.js"));
7
+
8
+ const required = [
9
+ resolve("app"),
10
+ resolve("app/pages"),
11
+ ];
12
+
13
+ const missing = required.filter((path) => !existsSync(path));
14
+ if (!indexExists) missing.push("app/pages/index.fs (or index.js)");
15
+ if (!layoutExists) missing.push("app/pages/_layout.fs (or _layout.js)");
16
+
17
+ if (missing.length > 0) {
18
+ throw new Error(`Missing required FastScript files:\n${missing.join("\n")}`);
19
+ }
20
+
21
+ console.log("check passed: FastScript app structure is valid");
22
+ }
package/src/cli.mjs ADDED
@@ -0,0 +1,71 @@
1
+ #!/usr/bin/env node
2
+ import { createApp } from "./create.mjs";
3
+ import { runDev } from "./dev.mjs";
4
+ import { runBuild } from "./build.mjs";
5
+ import { runCheck } from "./check.mjs";
6
+ import { runMigrate } from "./migrate.mjs";
7
+ import { runBench } from "./bench.mjs";
8
+ import { runExport } from "./export.mjs";
9
+ import { runCompat } from "./compat.mjs";
10
+ import { runValidate } from "./validate.mjs";
11
+ import { runDbMigrate, runDbSeed } from "./db-cli.mjs";
12
+ import { runStart } from "./start.mjs";
13
+ import { runDeploy } from "./deploy.mjs";
14
+ import { runWorkerCommand } from "./worker.mjs";
15
+
16
+ const [, , command, ...args] = process.argv;
17
+
18
+ async function main() {
19
+ switch (command) {
20
+ case "create":
21
+ await createApp(args[0] ?? "app");
22
+ break;
23
+ case "dev":
24
+ await runDev();
25
+ break;
26
+ case "start":
27
+ await runStart();
28
+ break;
29
+ case "build":
30
+ await runBuild();
31
+ break;
32
+ case "check":
33
+ await runCheck();
34
+ break;
35
+ case "migrate":
36
+ await runMigrate(args[0] ?? "app/pages");
37
+ break;
38
+ case "bench":
39
+ await runBench();
40
+ break;
41
+ case "export":
42
+ await runExport(args);
43
+ break;
44
+ case "compat":
45
+ await runCompat();
46
+ break;
47
+ case "validate":
48
+ await runValidate();
49
+ break;
50
+ case "db:migrate":
51
+ await runDbMigrate();
52
+ break;
53
+ case "db:seed":
54
+ await runDbSeed();
55
+ break;
56
+ case "deploy":
57
+ await runDeploy(args);
58
+ break;
59
+ case "worker":
60
+ await runWorkerCommand();
61
+ break;
62
+ default:
63
+ console.log("FastScript CLI");
64
+ console.log("Commands: create, dev, start, build, check, migrate, bench, export, compat, validate, db:migrate, db:seed, deploy, worker");
65
+ }
66
+ }
67
+
68
+ main().catch((error) => {
69
+ console.error("fastscript error:", error.message);
70
+ process.exit(1);
71
+ });
package/src/compat.mjs ADDED
@@ -0,0 +1,122 @@
1
+ import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
2
+ import { join, resolve } from "node:path";
3
+ import esbuild from "esbuild";
4
+ import { normalizeFastScript } from "./fs-normalize.mjs";
5
+
6
+ const TMP_DIR = resolve(".fastscript-tmp-compat");
7
+
8
+ function fsLoaderPlugin() {
9
+ return {
10
+ name: "fastscript-fs-loader",
11
+ setup(build) {
12
+ build.onLoad({ filter: /\.fs$/ }, async (args) => {
13
+ const { readFile } = await import("node:fs/promises");
14
+ const raw = await readFile(args.path, "utf8");
15
+ return { contents: normalizeFastScript(raw), loader: "js" };
16
+ });
17
+ },
18
+ };
19
+ }
20
+
21
+ async function bundle(entry) {
22
+ await esbuild.build({
23
+ entryPoints: [entry],
24
+ bundle: true,
25
+ platform: "browser",
26
+ format: "esm",
27
+ write: false,
28
+ logLevel: "silent",
29
+ plugins: [fsLoaderPlugin()],
30
+ loader: { ".fs": "js" },
31
+ resolveExtensions: [".fs", ".js", ".mjs", ".cjs", ".json"],
32
+ });
33
+ }
34
+
35
+ export async function runCompat() {
36
+ rmSync(TMP_DIR, { recursive: true, force: true });
37
+ mkdirSync(TMP_DIR, { recursive: true });
38
+
39
+ // CJS fixture
40
+ writeFileSync(
41
+ join(TMP_DIR, "cjs-lib.cjs"),
42
+ `module.exports = { answer: 42, greet(name){ return "hi " + name; } };`,
43
+ "utf8",
44
+ );
45
+
46
+ // ESM fixture
47
+ writeFileSync(
48
+ join(TMP_DIR, "esm-lib.js"),
49
+ `export const value = 7; export default function twice(n){ return n*2; }`,
50
+ "utf8",
51
+ );
52
+
53
+ // FS fixture using lenient syntax + mixed imports.
54
+ writeFileSync(
55
+ join(TMP_DIR, "entry.fs"),
56
+ `import cjs from "./cjs-lib.cjs"
57
+ import twice, { value } from "./esm-lib.js"
58
+
59
+ state name = "fastscript"
60
+ fn run(n) {
61
+ return cjs.greet(name) + ":" + String(twice(n) + value + cjs.answer)
62
+ }
63
+
64
+ export default run
65
+ `,
66
+ "utf8",
67
+ );
68
+
69
+ // JS fixture importing FS module.
70
+ writeFileSync(
71
+ join(TMP_DIR, "entry-js.js"),
72
+ `import run from "./entry.fs"; export default run(1);`,
73
+ "utf8",
74
+ );
75
+
76
+ // Dynamic import + JSON compatibility fixture.
77
+ writeFileSync(join(TMP_DIR, "data.json"), JSON.stringify({ ok: true, n: 3 }), "utf8");
78
+ writeFileSync(
79
+ join(TMP_DIR, "dynamic.js"),
80
+ `export async function getN(){ const mod = await import("./data.json"); return mod.default.n; }`,
81
+ "utf8",
82
+ );
83
+ writeFileSync(
84
+ join(TMP_DIR, "dynamic-entry.fs"),
85
+ `fn boot() { return "ok" }
86
+ export async function run() {
87
+ const mod = await import("./dynamic.js");
88
+ return mod.getN();
89
+ }
90
+ export default boot
91
+ `,
92
+ "utf8",
93
+ );
94
+
95
+ // TS-like migrated syntax fixture.
96
+ writeFileSync(
97
+ join(TMP_DIR, "ts-like.fs"),
98
+ `state total = 0
99
+ fn add(n) { total = total + n; return total }
100
+ export default add
101
+ `,
102
+ "utf8",
103
+ );
104
+
105
+ const checks = [
106
+ { name: "fs-entry-bundle", entry: join(TMP_DIR, "entry.fs") },
107
+ { name: "js-imports-fs-bundle", entry: join(TMP_DIR, "entry-js.js") },
108
+ { name: "dynamic-json-import-bundle", entry: join(TMP_DIR, "dynamic-entry.fs") },
109
+ { name: "ts-like-fs-bundle", entry: join(TMP_DIR, "ts-like.fs") },
110
+ ];
111
+
112
+ for (const check of checks) {
113
+ try {
114
+ await bundle(check.entry);
115
+ console.log(`compat pass: ${check.name}`);
116
+ } catch (error) {
117
+ throw new Error(`compat fail: ${check.name}\n${error.message}`);
118
+ }
119
+ }
120
+
121
+ console.log("compat summary: core ESM/CJS/FS interop checks passed");
122
+ }