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
package/src/build.mjs
CHANGED
|
@@ -1,234 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { dirname, extname, join, relative, resolve } from "node:path";
|
|
3
|
-
import esbuild from "esbuild";
|
|
4
|
-
import {
|
|
5
|
-
createFastScriptDiagnosticError,
|
|
6
|
-
normalizeFastScriptWithTelemetry,
|
|
7
|
-
} from "./fs-normalize.mjs";
|
|
8
|
-
|
|
9
|
-
const APP_DIR = resolve("app");
|
|
10
|
-
const PAGES_DIR = join(APP_DIR, "pages");
|
|
11
|
-
const API_DIR = join(APP_DIR, "api");
|
|
12
|
-
const DIST_DIR = resolve("dist");
|
|
13
|
-
|
|
14
|
-
function walk(dir) {
|
|
15
|
-
const out = [];
|
|
16
|
-
if (!existsSync(dir)) return out;
|
|
17
|
-
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
18
|
-
const full = join(dir, entry.name);
|
|
19
|
-
if (entry.isDirectory()) out.push(...walk(full));
|
|
20
|
-
else out.push(full);
|
|
21
|
-
}
|
|
22
|
-
return out;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function fsLoaderPlugin() {
|
|
26
|
-
return {
|
|
27
|
-
name: "fastscript-fs-loader",
|
|
28
|
-
setup(build) {
|
|
29
|
-
build.onLoad({ filter: /\.fs$/ }, async (args) => {
|
|
30
|
-
const { readFile } = await import("node:fs/promises");
|
|
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
|
-
}
|
|
42
|
-
return {
|
|
43
|
-
contents: result.code,
|
|
44
|
-
loader: "js",
|
|
45
|
-
};
|
|
46
|
-
});
|
|
47
|
-
},
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function routeFromPageFile(file) {
|
|
52
|
-
const rel = relative(PAGES_DIR, file).replace(/\\/g, "/").replace(/\.(js|fs)$/, "");
|
|
53
|
-
if (rel === "index") return "/";
|
|
54
|
-
const segs = rel.split("/").filter(Boolean);
|
|
55
|
-
if (segs.at(-1) === "index") segs.pop();
|
|
56
|
-
return "/" + segs.map((s) => (s.startsWith("[") && s.endsWith("]") ? `:${s.slice(1, -1)}` : s)).join("/");
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function routeFromApiFile(file) {
|
|
60
|
-
const rel = relative(API_DIR, file).replace(/\\/g, "/").replace(/\.(js|fs)$/, "");
|
|
61
|
-
const segs = rel.split("/").filter(Boolean);
|
|
62
|
-
if (segs.at(-1) === "index") segs.pop();
|
|
63
|
-
return "/api/" + segs.join("/");
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
async function compileFile(file, out, platform) {
|
|
67
|
-
mkdirSync(dirname(out), { recursive: true });
|
|
68
|
-
await esbuild.build({
|
|
69
|
-
entryPoints: [file],
|
|
70
|
-
outfile: out,
|
|
71
|
-
bundle: true,
|
|
72
|
-
format: "esm",
|
|
73
|
-
platform,
|
|
74
|
-
sourcemap: true,
|
|
75
|
-
minify: platform === "browser",
|
|
76
|
-
logLevel: "silent",
|
|
77
|
-
resolveExtensions: [".fs", ".js", ".mjs", ".cjs", ".json"],
|
|
78
|
-
plugins: [fsLoaderPlugin()],
|
|
79
|
-
loader: { ".fs": "js" },
|
|
80
|
-
});
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
export async function runBuild() {
|
|
84
|
-
if (!existsSync(PAGES_DIR)) throw new Error("Missing app/pages directory. Run: fastscript create app");
|
|
85
|
-
|
|
86
|
-
rmSync(DIST_DIR, { recursive: true, force: true });
|
|
87
|
-
mkdirSync(DIST_DIR, { recursive: true });
|
|
88
|
-
|
|
89
|
-
const manifest = { routes: [], apiRoutes: [], layout: null, notFound: null, middleware: null };
|
|
90
|
-
const pageFiles = walk(PAGES_DIR).filter((f) => [".js", ".fs"].includes(extname(f)));
|
|
91
|
-
|
|
92
|
-
for (const file of pageFiles) {
|
|
93
|
-
const rel = relative(APP_DIR, file).replace(/\\/g, "/");
|
|
94
|
-
const relModule = rel.replace(/\.fs$/, ".js");
|
|
95
|
-
const relFromPages = relative(PAGES_DIR, file).replace(/\\/g, "/").replace(/\.(js|fs)$/, "");
|
|
96
|
-
const out = join(DIST_DIR, relModule);
|
|
97
|
-
|
|
98
|
-
await compileFile(file, out, "browser");
|
|
99
|
-
|
|
100
|
-
if (relFromPages === "_layout") manifest.layout = `./${relModule}`;
|
|
101
|
-
else if (relFromPages === "404") manifest.notFound = `./${relModule}`;
|
|
102
|
-
else if (!relFromPages.startsWith("_")) manifest.routes.push({ path: routeFromPageFile(file), module: `./${relModule}` });
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
if (existsSync(API_DIR)) {
|
|
106
|
-
const apiFiles = walk(API_DIR).filter((f) => [".js", ".fs"].includes(extname(f)));
|
|
107
|
-
for (const file of apiFiles) {
|
|
108
|
-
const rel = relative(APP_DIR, file).replace(/\\/g, "/");
|
|
109
|
-
const relModule = rel.replace(/\.fs$/, ".js");
|
|
110
|
-
const out = join(DIST_DIR, relModule);
|
|
111
|
-
await compileFile(file, out, "node");
|
|
112
|
-
manifest.apiRoutes.push({ path: routeFromApiFile(file), module: `./${relModule}` });
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
const middlewareSource = [join(APP_DIR, "middleware.fs"), join(APP_DIR, "middleware.js")].find((p) => existsSync(p));
|
|
117
|
-
if (middlewareSource) {
|
|
118
|
-
const rel = relative(APP_DIR, middlewareSource).replace(/\\/g, "/").replace(/\.fs$/, ".js");
|
|
119
|
-
const out = join(DIST_DIR, rel);
|
|
120
|
-
await compileFile(middlewareSource, out, "node");
|
|
121
|
-
manifest.middleware = `./${rel}`;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
const stylesSrc = join(APP_DIR, "styles.css");
|
|
125
|
-
if (existsSync(stylesSrc)) copyFileSync(stylesSrc, join(DIST_DIR, "styles.css"));
|
|
126
|
-
|
|
127
|
-
writeFileSync(join(DIST_DIR, "fastscript-manifest.json"), JSON.stringify(manifest, null, 2), "utf8");
|
|
128
|
-
writeFileSync(join(DIST_DIR, "router.js"), buildRouterRuntime(), "utf8");
|
|
129
|
-
writeFileSync(join(DIST_DIR, "index.html"), buildIndexHtml(existsSync(stylesSrc)), "utf8");
|
|
130
|
-
|
|
131
|
-
console.log(`built FastScript app with ${manifest.routes.length} page route(s) and ${manifest.apiRoutes.length} api route(s)`);
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
function buildIndexHtml(hasStyles) {
|
|
135
|
-
return `<!doctype html>
|
|
136
|
-
<html>
|
|
137
|
-
<head>
|
|
138
|
-
<meta charset="utf-8" />
|
|
139
|
-
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
140
|
-
<title>FastScript</title>
|
|
141
|
-
${hasStyles ? '<link rel="stylesheet" href="/styles.css" />' : ""}
|
|
142
|
-
</head>
|
|
143
|
-
<body>
|
|
144
|
-
<div id="app"></div>
|
|
145
|
-
<script type="module" src="/router.js"></script>
|
|
146
|
-
</body>
|
|
147
|
-
</html>`;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
function buildRouterRuntime() {
|
|
151
|
-
return `
|
|
152
|
-
const app = document.getElementById("app");
|
|
153
|
-
const manifest = await fetch("/fastscript-manifest.json").then((r) => r.json());
|
|
154
|
-
|
|
155
|
-
function match(routePath, pathname) {
|
|
156
|
-
const a = routePath.split("/").filter(Boolean);
|
|
157
|
-
const b = pathname.split("/").filter(Boolean);
|
|
158
|
-
if (a.length !== b.length) return null;
|
|
159
|
-
const params = {};
|
|
160
|
-
for (let i = 0; i < a.length; i += 1) {
|
|
161
|
-
if (a[i].startsWith(":")) params[a[i].slice(1)] = b[i];
|
|
162
|
-
else if (a[i] !== b[i]) return null;
|
|
163
|
-
}
|
|
164
|
-
return params;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
function findRoute(pathname) {
|
|
168
|
-
for (const route of manifest.routes) {
|
|
169
|
-
const params = match(route.path, pathname);
|
|
170
|
-
if (params) return { route, params };
|
|
171
|
-
}
|
|
172
|
-
return null;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
async function hydrate(mod, ctx) {
|
|
176
|
-
if (typeof mod.hydrate === "function") {
|
|
177
|
-
await mod.hydrate({ ...ctx, root: app });
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
async function render(pathname, force = false) {
|
|
182
|
-
const path = pathname || "/";
|
|
183
|
-
const ssr = globalThis.__FASTSCRIPT_SSR;
|
|
184
|
-
const initialHit = ssr && ssr.pathname === path;
|
|
185
|
-
|
|
186
|
-
const matched = findRoute(path);
|
|
187
|
-
let mod = null;
|
|
188
|
-
let params = {};
|
|
189
|
-
let data = {};
|
|
190
|
-
let html = "";
|
|
191
|
-
|
|
192
|
-
if (matched) {
|
|
193
|
-
params = matched.params;
|
|
194
|
-
mod = await import(matched.route.module);
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
if (initialHit && !force) {
|
|
198
|
-
html = app.innerHTML;
|
|
199
|
-
if (ssr?.data) data = ssr.data;
|
|
200
|
-
} else if (!matched && manifest.notFound) {
|
|
201
|
-
const nfMod = await import(manifest.notFound);
|
|
202
|
-
html = (nfMod.default ? nfMod.default({ pathname: path }) : "<h1>404</h1>") || "";
|
|
203
|
-
} else if (matched) {
|
|
204
|
-
if (typeof mod.load === "function") data = (await mod.load({ params, pathname: path })) || {};
|
|
205
|
-
html = (mod.default ? mod.default({ ...data, params, pathname: path }) : "") || "";
|
|
206
|
-
if (manifest.layout) {
|
|
207
|
-
const layout = await import(manifest.layout);
|
|
208
|
-
html = layout.default ? layout.default({ content: html, pathname: path }) : html;
|
|
209
|
-
}
|
|
210
|
-
app.innerHTML = html;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
bindLinks();
|
|
214
|
-
if (mod) await hydrate(mod, { pathname: path, params, data });
|
|
215
|
-
globalThis.__FASTSCRIPT_SSR = null;
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
function bindLinks() {
|
|
219
|
-
for (const a of app.querySelectorAll('a[href^="/"]')) {
|
|
220
|
-
if (a.dataset.fsBound === "1") continue;
|
|
221
|
-
a.dataset.fsBound = "1";
|
|
222
|
-
a.addEventListener("click", (e) => {
|
|
223
|
-
e.preventDefault();
|
|
224
|
-
const href = a.getAttribute("href");
|
|
225
|
-
history.pushState({}, "", href);
|
|
226
|
-
render(location.pathname, true);
|
|
227
|
-
});
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
window.addEventListener("popstate", () => render(location.pathname, true));
|
|
232
|
-
render(location.pathname, false);
|
|
233
|
-
`;
|
|
234
|
-
}
|
|
1
|
+
export * from "@fastscript/core-private/build";
|
package/src/cache.mjs
CHANGED
|
@@ -1,23 +1,80 @@
|
|
|
1
|
-
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { join, resolve } from "node:path";
|
|
3
3
|
|
|
4
|
+
function now() {
|
|
5
|
+
return Date.now();
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function normalizeTags(tags) {
|
|
9
|
+
return [...new Set((Array.isArray(tags) ? tags : []).map((tag) => String(tag).trim()).filter(Boolean))];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function expired(row) {
|
|
13
|
+
return Boolean(row && row.exp && row.exp < now());
|
|
14
|
+
}
|
|
15
|
+
|
|
4
16
|
export function createMemoryCache() {
|
|
5
|
-
const
|
|
17
|
+
const rows = new Map();
|
|
18
|
+
const tagIndex = new Map();
|
|
19
|
+
|
|
20
|
+
function attachTags(key, tags) {
|
|
21
|
+
for (const tag of normalizeTags(tags)) {
|
|
22
|
+
if (!tagIndex.has(tag)) tagIndex.set(tag, new Set());
|
|
23
|
+
tagIndex.get(tag).add(key);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function detachTags(key, tags) {
|
|
28
|
+
for (const tag of normalizeTags(tags)) {
|
|
29
|
+
const set = tagIndex.get(tag);
|
|
30
|
+
if (!set) continue;
|
|
31
|
+
set.delete(key);
|
|
32
|
+
if (set.size === 0) tagIndex.delete(tag);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
6
36
|
return {
|
|
7
37
|
async get(key) {
|
|
8
|
-
const row =
|
|
38
|
+
const row = rows.get(key);
|
|
9
39
|
if (!row) return null;
|
|
10
|
-
if (row
|
|
11
|
-
|
|
40
|
+
if (expired(row)) {
|
|
41
|
+
detachTags(key, row.tags);
|
|
42
|
+
rows.delete(key);
|
|
12
43
|
return null;
|
|
13
44
|
}
|
|
14
45
|
return row.value;
|
|
15
46
|
},
|
|
16
47
|
async set(key, value, ttlMs = 0) {
|
|
17
|
-
|
|
48
|
+
const prev = rows.get(key);
|
|
49
|
+
if (prev) detachTags(key, prev.tags);
|
|
50
|
+
rows.set(key, { value, exp: ttlMs ? now() + ttlMs : 0, tags: [] });
|
|
51
|
+
},
|
|
52
|
+
async setWithTags(key, value, { ttlMs = 0, tags = [] } = {}) {
|
|
53
|
+
const prev = rows.get(key);
|
|
54
|
+
if (prev) detachTags(key, prev.tags);
|
|
55
|
+
const normalizedTags = normalizeTags(tags);
|
|
56
|
+
rows.set(key, { value, exp: ttlMs ? now() + ttlMs : 0, tags: normalizedTags });
|
|
57
|
+
attachTags(key, normalizedTags);
|
|
58
|
+
},
|
|
59
|
+
async del(key) {
|
|
60
|
+
const prev = rows.get(key);
|
|
61
|
+
if (prev) detachTags(key, prev.tags);
|
|
62
|
+
rows.delete(key);
|
|
63
|
+
},
|
|
64
|
+
async invalidateTag(tag) {
|
|
65
|
+
const keys = [...(tagIndex.get(tag) || [])];
|
|
66
|
+
for (const key of keys) {
|
|
67
|
+
const row = rows.get(key);
|
|
68
|
+
if (row) detachTags(key, row.tags);
|
|
69
|
+
rows.delete(key);
|
|
70
|
+
}
|
|
71
|
+
tagIndex.delete(tag);
|
|
72
|
+
return keys.length;
|
|
73
|
+
},
|
|
74
|
+
async clear() {
|
|
75
|
+
rows.clear();
|
|
76
|
+
tagIndex.clear();
|
|
18
77
|
},
|
|
19
|
-
async del(key) { m.delete(key); },
|
|
20
|
-
async clear() { m.clear(); },
|
|
21
78
|
};
|
|
22
79
|
}
|
|
23
80
|
|
|
@@ -25,34 +82,167 @@ export function createFileCache({ dir = ".fastscript/cache" } = {}) {
|
|
|
25
82
|
const root = resolve(dir);
|
|
26
83
|
mkdirSync(root, { recursive: true });
|
|
27
84
|
const p = (key) => join(root, `${encodeURIComponent(key)}.json`);
|
|
85
|
+
const tagsFile = join(root, "_tags.json");
|
|
86
|
+
|
|
87
|
+
function readTags() {
|
|
88
|
+
if (!existsSync(tagsFile)) return {};
|
|
89
|
+
try {
|
|
90
|
+
return JSON.parse(readFileSync(tagsFile, "utf8")) || {};
|
|
91
|
+
} catch {
|
|
92
|
+
return {};
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function writeTags(index) {
|
|
97
|
+
writeFileSync(tagsFile, JSON.stringify(index, null, 2), "utf8");
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function detach(key) {
|
|
101
|
+
const index = readTags();
|
|
102
|
+
let changed = false;
|
|
103
|
+
for (const [tag, keys] of Object.entries(index)) {
|
|
104
|
+
const next = (Array.isArray(keys) ? keys : []).filter((item) => item !== key);
|
|
105
|
+
if (next.length !== keys.length) changed = true;
|
|
106
|
+
if (next.length) index[tag] = next;
|
|
107
|
+
else delete index[tag];
|
|
108
|
+
}
|
|
109
|
+
if (changed) writeTags(index);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function attach(key, tags) {
|
|
113
|
+
const normalized = normalizeTags(tags);
|
|
114
|
+
if (!normalized.length) return;
|
|
115
|
+
const index = readTags();
|
|
116
|
+
for (const tag of normalized) {
|
|
117
|
+
const set = new Set(Array.isArray(index[tag]) ? index[tag] : []);
|
|
118
|
+
set.add(key);
|
|
119
|
+
index[tag] = [...set];
|
|
120
|
+
}
|
|
121
|
+
writeTags(index);
|
|
122
|
+
}
|
|
123
|
+
|
|
28
124
|
return {
|
|
29
125
|
async get(key) {
|
|
30
126
|
const file = p(key);
|
|
31
127
|
if (!existsSync(file)) return null;
|
|
32
128
|
const row = JSON.parse(readFileSync(file, "utf8"));
|
|
33
|
-
if (row
|
|
129
|
+
if (expired(row)) {
|
|
130
|
+
rmSync(file, { force: true });
|
|
131
|
+
detach(key);
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
34
134
|
return row.value;
|
|
35
135
|
},
|
|
36
136
|
async set(key, value, ttlMs = 0) {
|
|
37
|
-
writeFileSync(p(key), JSON.stringify({ value, exp: ttlMs ?
|
|
137
|
+
writeFileSync(p(key), JSON.stringify({ value, exp: ttlMs ? now() + ttlMs : 0, tags: [] }), "utf8");
|
|
138
|
+
detach(key);
|
|
139
|
+
},
|
|
140
|
+
async setWithTags(key, value, { ttlMs = 0, tags = [] } = {}) {
|
|
141
|
+
const normalized = normalizeTags(tags);
|
|
142
|
+
writeFileSync(p(key), JSON.stringify({ value, exp: ttlMs ? now() + ttlMs : 0, tags: normalized }), "utf8");
|
|
143
|
+
detach(key);
|
|
144
|
+
attach(key, normalized);
|
|
145
|
+
},
|
|
146
|
+
async del(key) {
|
|
147
|
+
rmSync(p(key), { force: true });
|
|
148
|
+
detach(key);
|
|
149
|
+
},
|
|
150
|
+
async invalidateTag(tag) {
|
|
151
|
+
const index = readTags();
|
|
152
|
+
const keys = Array.isArray(index[tag]) ? index[tag] : [];
|
|
153
|
+
for (const key of keys) rmSync(p(key), { force: true });
|
|
154
|
+
delete index[tag];
|
|
155
|
+
writeTags(index);
|
|
156
|
+
return keys.length;
|
|
157
|
+
},
|
|
158
|
+
async clear() {
|
|
159
|
+
rmSync(root, { recursive: true, force: true });
|
|
160
|
+
mkdirSync(root, { recursive: true });
|
|
38
161
|
},
|
|
39
|
-
async del(key) { rmSync(p(key), { force: true }); },
|
|
40
|
-
async clear() { rmSync(root, { recursive: true, force: true }); mkdirSync(root, { recursive: true }); },
|
|
41
162
|
};
|
|
42
163
|
}
|
|
43
164
|
|
|
44
|
-
|
|
165
|
+
function stringifyValue(value) {
|
|
166
|
+
if (typeof value === "string") return value;
|
|
167
|
+
return JSON.stringify(value);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function parseValue(value) {
|
|
171
|
+
if (value === null || value === undefined) return null;
|
|
172
|
+
try {
|
|
173
|
+
return JSON.parse(value);
|
|
174
|
+
} catch {
|
|
175
|
+
return value;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export async function createRedisCache({ url = process.env.REDIS_URL, prefix = "fastscript:cache" } = {}) {
|
|
45
180
|
const mod = await import("redis");
|
|
46
181
|
const client = mod.createClient({ url });
|
|
47
182
|
await client.connect();
|
|
183
|
+
|
|
184
|
+
function rowKey(key) {
|
|
185
|
+
return `${prefix}:row:${key}`;
|
|
186
|
+
}
|
|
187
|
+
function tagKey(tag) {
|
|
188
|
+
return `${prefix}:tag:${tag}`;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async function detachFromTags(key) {
|
|
192
|
+
const tags = await client.sMembers(`${prefix}:rowtags:${key}`);
|
|
193
|
+
if (!tags.length) return;
|
|
194
|
+
for (const tag of tags) await client.sRem(tagKey(tag), key);
|
|
195
|
+
await client.del(`${prefix}:rowtags:${key}`);
|
|
196
|
+
}
|
|
197
|
+
|
|
48
198
|
return {
|
|
49
|
-
async get(key) {
|
|
199
|
+
async get(key) {
|
|
200
|
+
const raw = await client.get(rowKey(key));
|
|
201
|
+
if (raw === null) return null;
|
|
202
|
+
const row = parseValue(raw);
|
|
203
|
+
if (!row || typeof row !== "object") return row;
|
|
204
|
+
if (expired(row)) {
|
|
205
|
+
await this.del(key);
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
return row.value;
|
|
209
|
+
},
|
|
50
210
|
async set(key, value, ttlMs = 0) {
|
|
51
|
-
|
|
52
|
-
|
|
211
|
+
await detachFromTags(key);
|
|
212
|
+
const row = stringifyValue({ value, exp: ttlMs ? now() + ttlMs : 0, tags: [] });
|
|
213
|
+
if (ttlMs > 0) await client.set(rowKey(key), row, { PX: ttlMs });
|
|
214
|
+
else await client.set(rowKey(key), row);
|
|
215
|
+
},
|
|
216
|
+
async setWithTags(key, value, { ttlMs = 0, tags = [] } = {}) {
|
|
217
|
+
await detachFromTags(key);
|
|
218
|
+
const normalized = normalizeTags(tags);
|
|
219
|
+
const row = stringifyValue({ value, exp: ttlMs ? now() + ttlMs : 0, tags: normalized });
|
|
220
|
+
if (ttlMs > 0) await client.set(rowKey(key), row, { PX: ttlMs });
|
|
221
|
+
else await client.set(rowKey(key), row);
|
|
222
|
+
for (const tag of normalized) await client.sAdd(tagKey(tag), key);
|
|
223
|
+
if (normalized.length) await client.sAdd(`${prefix}:rowtags:${key}`, ...normalized);
|
|
224
|
+
},
|
|
225
|
+
async del(key) {
|
|
226
|
+
await detachFromTags(key);
|
|
227
|
+
await client.del(rowKey(key));
|
|
228
|
+
},
|
|
229
|
+
async invalidateTag(tag) {
|
|
230
|
+
const keys = await client.sMembers(tagKey(tag));
|
|
231
|
+
for (const key of keys) await this.del(key);
|
|
232
|
+
await client.del(tagKey(tag));
|
|
233
|
+
return keys.length;
|
|
234
|
+
},
|
|
235
|
+
async clear() {
|
|
236
|
+
const cursor = "0";
|
|
237
|
+
let next = cursor;
|
|
238
|
+
do {
|
|
239
|
+
const data = await client.scan(next, { MATCH: `${prefix}:*`, COUNT: 200 });
|
|
240
|
+
next = data.cursor;
|
|
241
|
+
if (data.keys && data.keys.length) await client.del(data.keys);
|
|
242
|
+
} while (next !== "0");
|
|
243
|
+
},
|
|
244
|
+
async close() {
|
|
245
|
+
await client.quit();
|
|
53
246
|
},
|
|
54
|
-
async del(key) { await client.del(key); },
|
|
55
|
-
async clear() { await client.flushDb(); },
|
|
56
|
-
async close() { await client.quit(); },
|
|
57
247
|
};
|
|
58
|
-
}
|
|
248
|
+
}
|
package/src/cli.mjs
CHANGED
|
@@ -8,17 +8,23 @@ import { runBench } from "./bench.mjs";
|
|
|
8
8
|
import { runExport } from "./export.mjs";
|
|
9
9
|
import { runCompat } from "./compat.mjs";
|
|
10
10
|
import { runValidate } from "./validate.mjs";
|
|
11
|
-
import { runDbMigrate, runDbSeed } from "./db-cli.mjs";
|
|
11
|
+
import { runDbMigrate, runDbRollback, runDbSeed } from "./db-cli.mjs";
|
|
12
12
|
import { runStart } from "./start.mjs";
|
|
13
13
|
import { runDeploy } from "./deploy.mjs";
|
|
14
14
|
import { runWorkerCommand } from "./worker.mjs";
|
|
15
|
+
import { runTypeCheck } from "./typecheck.mjs";
|
|
16
|
+
import { runFormat } from "./fs-formatter.mjs";
|
|
17
|
+
import { runLint } from "./fs-linter.mjs";
|
|
18
|
+
import { runMigrationWizard } from "./migration-wizard.mjs";
|
|
15
19
|
|
|
16
20
|
const [, , command, ...args] = process.argv;
|
|
17
21
|
|
|
18
22
|
async function main() {
|
|
19
23
|
switch (command) {
|
|
20
24
|
case "create":
|
|
21
|
-
await createApp(args[0] ?? "app"
|
|
25
|
+
await createApp(args[0] ?? "app", {
|
|
26
|
+
template: args.includes("--template") ? args[args.indexOf("--template") + 1] || "default" : "default",
|
|
27
|
+
});
|
|
22
28
|
break;
|
|
23
29
|
case "dev":
|
|
24
30
|
await runDev();
|
|
@@ -27,7 +33,10 @@ async function main() {
|
|
|
27
33
|
await runStart();
|
|
28
34
|
break;
|
|
29
35
|
case "build":
|
|
30
|
-
await runBuild();
|
|
36
|
+
await runBuild({ mode: args.includes("--mode") ? args[args.indexOf("--mode") + 1] || "build" : "build" });
|
|
37
|
+
break;
|
|
38
|
+
case "ssg":
|
|
39
|
+
await runBuild({ mode: "ssg" });
|
|
31
40
|
break;
|
|
32
41
|
case "check":
|
|
33
42
|
await runCheck();
|
|
@@ -35,6 +44,9 @@ async function main() {
|
|
|
35
44
|
case "migrate":
|
|
36
45
|
await runMigrate(args[0] ?? "app/pages");
|
|
37
46
|
break;
|
|
47
|
+
case "wizard:migrate":
|
|
48
|
+
await runMigrationWizard(args);
|
|
49
|
+
break;
|
|
38
50
|
case "bench":
|
|
39
51
|
await runBench();
|
|
40
52
|
break;
|
|
@@ -47,21 +59,33 @@ async function main() {
|
|
|
47
59
|
case "validate":
|
|
48
60
|
await runValidate();
|
|
49
61
|
break;
|
|
62
|
+
case "typecheck":
|
|
63
|
+
await runTypeCheck(args);
|
|
64
|
+
break;
|
|
65
|
+
case "format":
|
|
66
|
+
await runFormat(args);
|
|
67
|
+
break;
|
|
68
|
+
case "lint":
|
|
69
|
+
await runLint(args);
|
|
70
|
+
break;
|
|
50
71
|
case "db:migrate":
|
|
51
72
|
await runDbMigrate();
|
|
52
73
|
break;
|
|
53
74
|
case "db:seed":
|
|
54
75
|
await runDbSeed();
|
|
55
76
|
break;
|
|
77
|
+
case "db:rollback":
|
|
78
|
+
await runDbRollback(args);
|
|
79
|
+
break;
|
|
56
80
|
case "deploy":
|
|
57
81
|
await runDeploy(args);
|
|
58
82
|
break;
|
|
59
83
|
case "worker":
|
|
60
|
-
await runWorkerCommand();
|
|
84
|
+
await runWorkerCommand(args);
|
|
61
85
|
break;
|
|
62
86
|
default:
|
|
63
87
|
console.log("FastScript CLI");
|
|
64
|
-
console.log("Commands: create, dev, start, build, check, migrate, bench, export, compat, validate, db:migrate, db:seed, deploy, worker");
|
|
88
|
+
console.log("Commands: create, dev, start, build, ssg, check, migrate, wizard:migrate, bench, export, compat, validate, typecheck, format, lint, db:migrate, db:seed, db:rollback, deploy, worker");
|
|
65
89
|
}
|
|
66
90
|
}
|
|
67
91
|
|
package/src/compat.mjs
CHANGED
|
@@ -1,26 +1,24 @@
|
|
|
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 {
|
|
5
|
-
|
|
6
|
-
normalizeFastScriptWithTelemetry,
|
|
7
|
-
} from "./fs-normalize.mjs";
|
|
4
|
+
import { normalizeFastScript } from "./fs-normalize.mjs";
|
|
5
|
+
import { assertFastScript } from "./fs-diagnostics.mjs";
|
|
8
6
|
|
|
9
7
|
const TMP_DIR = resolve(".fastscript-tmp-compat");
|
|
10
8
|
|
|
11
9
|
function fsLoaderPlugin() {
|
|
10
|
+
const compilerMode = (process.env.FASTSCRIPT_COMPILER_MODE || "strict").toLowerCase() === "lenient" ? "lenient" : "strict";
|
|
12
11
|
return {
|
|
13
12
|
name: "fastscript-fs-loader",
|
|
14
13
|
setup(build) {
|
|
15
14
|
build.onLoad({ filter: /\.fs$/ }, async (args) => {
|
|
16
15
|
const { readFile } = await import("node:fs/promises");
|
|
17
16
|
const raw = await readFile(args.path, "utf8");
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
}
|
|
23
|
-
return { contents: result.code, loader: "js" };
|
|
17
|
+
assertFastScript(raw, { file: args.path, mode: compilerMode });
|
|
18
|
+
return {
|
|
19
|
+
contents: normalizeFastScript(raw, { file: args.path, mode: compilerMode, sourceMap: "inline" }),
|
|
20
|
+
loader: "js",
|
|
21
|
+
};
|
|
24
22
|
});
|
|
25
23
|
},
|
|
26
24
|
};
|