fastscript 1.0.0 → 3.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 +38 -7
- package/LICENSE +33 -21
- package/README.md +605 -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 +121 -0
- package/node_modules/@fastscript/core-private/src/fs-parser.mjs +1120 -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 +1466 -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 +108 -14
- 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/benchmark-discipline.mjs +39 -0
- package/src/build.mjs +1 -234
- package/src/cache.mjs +210 -20
- package/src/cli.mjs +65 -6
- package/src/compat.mjs +8 -10
- package/src/conversion-manifest.mjs +101 -0
- 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/diagnostics.mjs +100 -0
- 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 +52 -239
- package/src/fs-parser.mjs +1 -0
- package/src/generated/docs-search-index.mjs +3591 -0
- package/src/i18n.mjs +25 -0
- package/src/jobs.mjs +283 -32
- package/src/metrics.mjs +45 -0
- package/src/migrate-rollback.mjs +144 -0
- package/src/migrate.mjs +1275 -47
- package/src/migration-wizard.mjs +42 -0
- package/src/module-loader.mjs +22 -11
- package/src/oauth-providers.mjs +103 -0
- package/src/permissions-cli.mjs +112 -0
- package/src/plugins.mjs +194 -0
- package/src/profile.mjs +95 -0
- package/src/regression-guard.mjs +245 -0
- package/src/retention.mjs +57 -0
- package/src/routes.mjs +178 -0
- package/src/runtime-permissions.mjs +299 -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/trace.mjs +95 -0
- package/src/typecheck.mjs +1 -0
- package/src/validate.mjs +13 -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,670 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { dirname, extname, join, relative, resolve } from "node:path";
|
|
4
|
+
import { pathToFileURL } from "node:url";
|
|
5
|
+
import esbuild from "esbuild";
|
|
6
|
+
import { normalizeFastScript } from "./fs-normalize.mjs";
|
|
7
|
+
import { createPluginRuntime } from "./plugins.mjs";
|
|
8
|
+
import { ensureDesignSystem, validateAppStyles } from "./style-system.mjs";
|
|
9
|
+
import { buildStylePrimitiveRuntimeSource, transformStylePrimitives } from "./style-primitives.mjs";
|
|
10
|
+
import { assertFastScript } from "./fs-diagnostics.mjs";
|
|
11
|
+
import { inferRouteLayouts, inferRouteMeta, isLayoutFile, isNotFoundFile, sortRoutesByPriority } from "./routes.mjs";
|
|
12
|
+
import { optimizeFontAssets, optimizeImageAssets } from "./asset-optimizer.mjs";
|
|
13
|
+
import { getI18nConfig } from "./i18n.mjs";
|
|
14
|
+
|
|
15
|
+
const APP_DIR = resolve("app");
|
|
16
|
+
const PAGES_DIR = join(APP_DIR, "pages");
|
|
17
|
+
const API_DIR = join(APP_DIR, "api");
|
|
18
|
+
const DIST_DIR = resolve("dist");
|
|
19
|
+
const DETERMINISTIC_BUILD_STAMP = "1970-01-01T00:00:00.000Z";
|
|
20
|
+
|
|
21
|
+
function resolveBuildStamp() {
|
|
22
|
+
const explicit = process.env.FASTSCRIPT_BUILD_TIMESTAMP || process.env.SOURCE_DATE_EPOCH;
|
|
23
|
+
if (!explicit) return DETERMINISTIC_BUILD_STAMP;
|
|
24
|
+
if (/^\d+$/.test(String(explicit))) {
|
|
25
|
+
const millis = Number(explicit) * (String(explicit).length <= 10 ? 1000 : 1);
|
|
26
|
+
return new Date(millis).toISOString();
|
|
27
|
+
}
|
|
28
|
+
const parsed = new Date(String(explicit));
|
|
29
|
+
return Number.isNaN(parsed.getTime()) ? DETERMINISTIC_BUILD_STAMP : parsed.toISOString();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function walk(dir) {
|
|
33
|
+
const out = [];
|
|
34
|
+
if (!existsSync(dir)) return out;
|
|
35
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
36
|
+
const full = join(dir, entry.name);
|
|
37
|
+
if (entry.isDirectory()) out.push(...walk(full));
|
|
38
|
+
else out.push(full);
|
|
39
|
+
}
|
|
40
|
+
return out;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function sleep(ms) {
|
|
44
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function ensureEmptyDir(dir, { retries = 6, delayMs = 40 } = {}) {
|
|
48
|
+
mkdirSync(dir, { recursive: true });
|
|
49
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
50
|
+
for (const entry of entries) {
|
|
51
|
+
const target = join(dir, entry.name);
|
|
52
|
+
let lastError = null;
|
|
53
|
+
for (let attempt = 0; attempt <= retries; attempt += 1) {
|
|
54
|
+
try {
|
|
55
|
+
rmSync(target, { recursive: true, force: true });
|
|
56
|
+
lastError = null;
|
|
57
|
+
break;
|
|
58
|
+
} catch (error) {
|
|
59
|
+
lastError = error;
|
|
60
|
+
if (attempt === retries) break;
|
|
61
|
+
await sleep(delayMs * (attempt + 1));
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (lastError) throw lastError;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function fsLoaderPlugin() {
|
|
69
|
+
const compilerMode = (process.env.FASTSCRIPT_COMPILER_MODE || "strict").toLowerCase() === "lenient" ? "lenient" : "strict";
|
|
70
|
+
return {
|
|
71
|
+
name: "fastscript-fs-loader",
|
|
72
|
+
setup(build) {
|
|
73
|
+
build.onLoad({ filter: /\.fs$/ }, async (args) => {
|
|
74
|
+
const { readFile } = await import("node:fs/promises");
|
|
75
|
+
const raw = await readFile(args.path, "utf8");
|
|
76
|
+
assertFastScript(raw, { file: args.path, mode: compilerMode });
|
|
77
|
+
return {
|
|
78
|
+
contents: normalizeFastScript(raw, { file: args.path, mode: compilerMode, sourceMap: "inline" }),
|
|
79
|
+
loader: "js",
|
|
80
|
+
};
|
|
81
|
+
});
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function routeFromApiFile(file) {
|
|
87
|
+
const rel = relative(API_DIR, file).replace(/\\/g, "/").replace(/\.(js|fs)$/, "");
|
|
88
|
+
const segs = rel.split("/").filter(Boolean).map((segment) => {
|
|
89
|
+
const optionalCatchAll = /^\[\[\.\.\.(.+)\]\]$/.exec(segment);
|
|
90
|
+
if (optionalCatchAll) return `:${optionalCatchAll[1]}*?`;
|
|
91
|
+
const catchAll = /^\[\.\.\.(.+)\]$/.exec(segment);
|
|
92
|
+
if (catchAll) return `:${catchAll[1]}*`;
|
|
93
|
+
const dynamic = /^\[(.+)\]$/.exec(segment);
|
|
94
|
+
if (dynamic) return `:${dynamic[1]}`;
|
|
95
|
+
return segment;
|
|
96
|
+
});
|
|
97
|
+
if (segs.at(-1) === "index") segs.pop();
|
|
98
|
+
return "/api/" + segs.join("/");
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function compileFile(file, out, platform) {
|
|
102
|
+
mkdirSync(dirname(out), { recursive: true });
|
|
103
|
+
await esbuild.build({
|
|
104
|
+
entryPoints: [file],
|
|
105
|
+
outfile: out,
|
|
106
|
+
bundle: true,
|
|
107
|
+
format: "esm",
|
|
108
|
+
platform,
|
|
109
|
+
sourcemap: true,
|
|
110
|
+
minify: platform === "browser",
|
|
111
|
+
logLevel: "silent",
|
|
112
|
+
resolveExtensions: [".fs", ".js", ".mjs", ".cjs", ".json"],
|
|
113
|
+
plugins: [fsLoaderPlugin()],
|
|
114
|
+
loader: { ".fs": "js" },
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function hashContent(buf) {
|
|
119
|
+
return createHash("sha1").update(buf).digest("hex").slice(0, 8);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function relDist(path) {
|
|
123
|
+
return relative(DIST_DIR, path).replace(/\\/g, "/");
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function mapAssetPath(path, mapping) {
|
|
127
|
+
if (!path) return path;
|
|
128
|
+
const rel = path.replace(/^\.\//, "");
|
|
129
|
+
return mapping[rel] ? `./${mapping[rel]}` : path;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function rewriteManifestAssetPaths(manifest, mapping) {
|
|
133
|
+
manifest.layout = mapAssetPath(manifest.layout, mapping);
|
|
134
|
+
manifest.notFound = mapAssetPath(manifest.notFound, mapping);
|
|
135
|
+
manifest.middleware = mapAssetPath(manifest.middleware, mapping);
|
|
136
|
+
manifest.routes = manifest.routes.map((route) => ({
|
|
137
|
+
...route,
|
|
138
|
+
module: mapAssetPath(route.module, mapping),
|
|
139
|
+
layouts: (route.layouts || []).map((layout) => mapAssetPath(layout, mapping)),
|
|
140
|
+
}));
|
|
141
|
+
manifest.parallelRoutes = (manifest.parallelRoutes || []).map((route) => ({
|
|
142
|
+
...route,
|
|
143
|
+
module: mapAssetPath(route.module, mapping),
|
|
144
|
+
}));
|
|
145
|
+
manifest.apiRoutes = manifest.apiRoutes.map((route) => ({
|
|
146
|
+
...route,
|
|
147
|
+
module: mapAssetPath(route.module, mapping),
|
|
148
|
+
}));
|
|
149
|
+
return manifest;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function applyAssetFingerprinting(manifest) {
|
|
153
|
+
const assets = walk(DIST_DIR)
|
|
154
|
+
.filter((file) => /\.(js|css)$/.test(file))
|
|
155
|
+
.filter((file) => !file.endsWith(".map"));
|
|
156
|
+
const mapping = {};
|
|
157
|
+
const originalBodies = new Map();
|
|
158
|
+
const originalMaps = new Map();
|
|
159
|
+
|
|
160
|
+
for (const file of assets) {
|
|
161
|
+
const rel = relDist(file);
|
|
162
|
+
const body = readFileSync(file);
|
|
163
|
+
originalBodies.set(rel, body);
|
|
164
|
+
const mapFrom = `${file}.map`;
|
|
165
|
+
if (existsSync(mapFrom)) {
|
|
166
|
+
originalMaps.set(rel, readFileSync(mapFrom));
|
|
167
|
+
}
|
|
168
|
+
const hash = hashContent(body);
|
|
169
|
+
const ext = extname(rel);
|
|
170
|
+
const stem = rel.slice(0, -ext.length);
|
|
171
|
+
const nextRel = `${stem}.${hash}${ext}`;
|
|
172
|
+
if (nextRel === rel) continue;
|
|
173
|
+
renameSync(file, join(DIST_DIR, nextRel));
|
|
174
|
+
const mapTo = join(DIST_DIR, `${nextRel}.map`);
|
|
175
|
+
if (existsSync(mapFrom)) renameSync(mapFrom, mapTo);
|
|
176
|
+
mapping[rel] = nextRel;
|
|
177
|
+
const logicalRel = rel.replace(/\.[a-f0-9]{8}(\.[mc]?js|\.css)$/i, "$1");
|
|
178
|
+
if (logicalRel !== rel && !mapping[logicalRel]) {
|
|
179
|
+
mapping[logicalRel] = nextRel;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
rewriteManifestAssetPaths(manifest, mapping);
|
|
184
|
+
const routerAsset = mapping["router.js"] || "router.js";
|
|
185
|
+
const stylesAsset = mapping["styles.css"] || "styles.css";
|
|
186
|
+
const middlewareAsset = mapping["middleware.js"] || "middleware.js";
|
|
187
|
+
|
|
188
|
+
if (mapping["router.js"] && originalBodies.has("router.js")) {
|
|
189
|
+
writeFileSync(join(DIST_DIR, "router.js"), originalBodies.get("router.js"));
|
|
190
|
+
if (originalMaps.has("router.js")) {
|
|
191
|
+
writeFileSync(join(DIST_DIR, "router.js.map"), originalMaps.get("router.js"));
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
if (mapping["styles.css"] && originalBodies.has("styles.css")) {
|
|
195
|
+
writeFileSync(join(DIST_DIR, "styles.css"), originalBodies.get("styles.css"));
|
|
196
|
+
}
|
|
197
|
+
if (mapping["middleware.js"] && originalBodies.has("middleware.js")) {
|
|
198
|
+
writeFileSync(join(DIST_DIR, "middleware.js"), originalBodies.get("middleware.js"));
|
|
199
|
+
if (originalMaps.has("middleware.js")) {
|
|
200
|
+
writeFileSync(join(DIST_DIR, "middleware.js.map"), originalMaps.get("middleware.js"));
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
writeFileSync(join(DIST_DIR, "asset-manifest.json"), JSON.stringify({ mapping }, null, 2), "utf8");
|
|
205
|
+
return { routerAsset, stylesAsset, middlewareAsset, mapping };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async function runSsg(manifest, { stylesAsset, routerAsset }) {
|
|
209
|
+
const staticDir = join(DIST_DIR, "static");
|
|
210
|
+
mkdirSync(staticDir, { recursive: true });
|
|
211
|
+
const routes = manifest.routes.filter((route) => !route.path.includes(":"));
|
|
212
|
+
for (const route of routes) {
|
|
213
|
+
const mod = await import(`${pathToFileURL(join(DIST_DIR, route.module.replace(/^\.\//, ""))).href}?t=${Date.now()}`);
|
|
214
|
+
const html = typeof mod.default === "function" ? transformStylePrimitives(mod.default({ pathname: route.path, params: {} })) : "";
|
|
215
|
+
const target = route.path === "/" ? join(staticDir, "index.html") : join(staticDir, route.path.slice(1), "index.html");
|
|
216
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
217
|
+
const wrapped = buildIndexHtml(Boolean(stylesAsset), {
|
|
218
|
+
content: html,
|
|
219
|
+
stylesAsset,
|
|
220
|
+
routerAsset,
|
|
221
|
+
devMode: false,
|
|
222
|
+
});
|
|
223
|
+
writeFileSync(target, wrapped, "utf8");
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function generatePwaArtifacts({ routerAsset, stylesAsset, cacheVersion = "v1" }) {
|
|
228
|
+
const assets = walk(DIST_DIR)
|
|
229
|
+
.map((file) => relDist(file))
|
|
230
|
+
.filter((rel) => /\.(js|css|json|html)$/.test(rel))
|
|
231
|
+
.map((rel) => `/${rel}`);
|
|
232
|
+
const cacheName = `fastscript-${cacheVersion}`;
|
|
233
|
+
const webmanifest = {
|
|
234
|
+
name: "FastScript App",
|
|
235
|
+
short_name: "FastScript",
|
|
236
|
+
start_url: "/",
|
|
237
|
+
display: "standalone",
|
|
238
|
+
background_color: "#0a0a0a",
|
|
239
|
+
theme_color: "#0a0a0a",
|
|
240
|
+
icons: [],
|
|
241
|
+
};
|
|
242
|
+
writeFileSync(join(DIST_DIR, "manifest.webmanifest"), JSON.stringify(webmanifest, null, 2), "utf8");
|
|
243
|
+
const sw = `const CACHE_NAME = "${cacheName}";\nconst ASSETS = ${JSON.stringify([...new Set(["/", `/${routerAsset}`, `/${stylesAsset}`, ...assets])], null, 2)};\nconst FASTSCRIPT_CACHE_PREFIX = "fastscript-";\nself.addEventListener("install", (event) => {\n event.waitUntil(\n caches.open(CACHE_NAME)\n .then((c) => c.addAll(ASSETS))\n .then(() => self.skipWaiting())\n );\n});\nself.addEventListener("activate", (event) => {\n event.waitUntil(\n caches.keys()\n .then((keys) => Promise.all(keys.filter((k) => k.startsWith(FASTSCRIPT_CACHE_PREFIX) && k !== CACHE_NAME).map((k) => caches.delete(k))))\n .then(() => self.clients.claim())\n );\n});\nself.addEventListener("fetch", (event) => {\n if (event.request.method !== "GET") return;\n const req = event.request;\n const accept = req.headers.get("accept") || "";\n const isHtml = req.mode === "navigate" || accept.includes("text/html");\n\n if (isHtml) {\n event.respondWith(\n fetch(req)\n .then((res) => {\n if (res && res.ok) {\n const copy = res.clone();\n caches.open(CACHE_NAME).then((cache) => cache.put(req, copy)).catch(() => {});\n }\n return res;\n })\n .catch(() => caches.match(req).then((hit) => hit || caches.match("/")))\n );\n return;\n }\n\n event.respondWith(\n caches.match(req).then((hit) => {\n if (hit) return hit;\n return fetch(req).then((res) => {\n const copy = res.clone();\n caches.open(CACHE_NAME).then((cache) => cache.put(req, copy)).catch(() => {});\n return res;\n });\n })\n );\n});\n`;
|
|
244
|
+
writeFileSync(join(DIST_DIR, "service-worker.js"), sw, "utf8");
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export async function runBuild(options = {}) {
|
|
248
|
+
if (!existsSync(PAGES_DIR)) throw new Error("Missing app/pages directory. Run: fastscript create app");
|
|
249
|
+
ensureDesignSystem({ root: process.cwd() });
|
|
250
|
+
validateAppStyles({ root: process.cwd() });
|
|
251
|
+
|
|
252
|
+
const pluginLogger = {
|
|
253
|
+
info(msg, extra = {}) { console.log(`[plugin] ${msg}`, extra); },
|
|
254
|
+
warn(msg, extra = {}) { console.warn(`[plugin] ${msg}`, extra); },
|
|
255
|
+
error(msg, extra = {}) { console.error(`[plugin] ${msg}`, extra); },
|
|
256
|
+
};
|
|
257
|
+
const plugins = await createPluginRuntime({ logger: pluginLogger });
|
|
258
|
+
await plugins.onBuildStart({ appDir: APP_DIR, distDir: DIST_DIR });
|
|
259
|
+
|
|
260
|
+
await ensureEmptyDir(DIST_DIR);
|
|
261
|
+
|
|
262
|
+
const manifest = {
|
|
263
|
+
routes: [],
|
|
264
|
+
parallelRoutes: [],
|
|
265
|
+
apiRoutes: [],
|
|
266
|
+
layout: null,
|
|
267
|
+
notFound: null,
|
|
268
|
+
middleware: null,
|
|
269
|
+
mode: options.mode || "build",
|
|
270
|
+
compilerMode: (process.env.FASTSCRIPT_COMPILER_MODE || "strict").toLowerCase() === "lenient" ? "lenient" : "strict",
|
|
271
|
+
generatedAt: resolveBuildStamp(),
|
|
272
|
+
devMode: (process.env.NODE_ENV || "development") !== "production",
|
|
273
|
+
i18n: getI18nConfig(process.env),
|
|
274
|
+
};
|
|
275
|
+
const pageFiles = walk(PAGES_DIR).filter((f) => [".js", ".fs"].includes(extname(f)));
|
|
276
|
+
const layoutFiles = new Set(
|
|
277
|
+
pageFiles
|
|
278
|
+
.filter((file) => isLayoutFile(file, PAGES_DIR))
|
|
279
|
+
.map((file) => relative(PAGES_DIR, file).replace(/\\/g, "/")),
|
|
280
|
+
);
|
|
281
|
+
|
|
282
|
+
for (const file of pageFiles) {
|
|
283
|
+
const rel = relative(APP_DIR, file).replace(/\\/g, "/");
|
|
284
|
+
const relModule = rel.replace(/\.fs$/, ".js");
|
|
285
|
+
const out = join(DIST_DIR, relModule);
|
|
286
|
+
await compileFile(file, out, "browser");
|
|
287
|
+
|
|
288
|
+
if (isLayoutFile(file, PAGES_DIR)) {
|
|
289
|
+
if (!manifest.layout) manifest.layout = `./${relModule}`;
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (isNotFoundFile(file, PAGES_DIR)) {
|
|
294
|
+
if (!manifest.notFound) manifest.notFound = `./${relModule}`;
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const routeMeta = inferRouteMeta(file, PAGES_DIR);
|
|
299
|
+
const layouts = inferRouteLayouts(file, PAGES_DIR, layoutFiles)
|
|
300
|
+
.map((layoutRel) => `./pages/${layoutRel}`.replace(/\.fs$/, ".js"));
|
|
301
|
+
const record = {
|
|
302
|
+
path: routeMeta.routePath,
|
|
303
|
+
module: `./${relModule}`,
|
|
304
|
+
slot: routeMeta.slot || null,
|
|
305
|
+
params: routeMeta.params,
|
|
306
|
+
paramTypes: routeMeta.paramTypes || {},
|
|
307
|
+
layouts,
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
if (routeMeta.slot) manifest.parallelRoutes.push(record);
|
|
311
|
+
else manifest.routes.push(record);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (existsSync(API_DIR)) {
|
|
315
|
+
const apiFiles = walk(API_DIR).filter((f) => [".js", ".fs"].includes(extname(f)));
|
|
316
|
+
for (const file of apiFiles) {
|
|
317
|
+
const rel = relative(APP_DIR, file).replace(/\\/g, "/");
|
|
318
|
+
const relModule = rel.replace(/\.fs$/, ".js");
|
|
319
|
+
const out = join(DIST_DIR, relModule);
|
|
320
|
+
await compileFile(file, out, "node");
|
|
321
|
+
manifest.apiRoutes.push({ path: routeFromApiFile(file), module: `./${relModule}` });
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
manifest.routes = sortRoutesByPriority(manifest.routes);
|
|
326
|
+
manifest.parallelRoutes = sortRoutesByPriority(manifest.parallelRoutes);
|
|
327
|
+
manifest.apiRoutes = sortRoutesByPriority(manifest.apiRoutes);
|
|
328
|
+
|
|
329
|
+
const middlewareSource = [join(APP_DIR, "middleware.fs"), join(APP_DIR, "middleware.js")].find((p) => existsSync(p));
|
|
330
|
+
if (middlewareSource) {
|
|
331
|
+
const rel = relative(APP_DIR, middlewareSource).replace(/\\/g, "/").replace(/\.fs$/, ".js");
|
|
332
|
+
const out = join(DIST_DIR, rel);
|
|
333
|
+
await compileFile(middlewareSource, out, "node");
|
|
334
|
+
manifest.middleware = `./${rel}`;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
await optimizeImageAssets({ appDir: APP_DIR, distDir: DIST_DIR });
|
|
338
|
+
await optimizeFontAssets({ appDir: APP_DIR, distDir: DIST_DIR });
|
|
339
|
+
|
|
340
|
+
const stylesSrc = join(APP_DIR, "styles.css");
|
|
341
|
+
const generatedStylesSrc = join(APP_DIR, "styles.generated.css");
|
|
342
|
+
const generatedFontsCss = join(DIST_DIR, "fonts.generated.css");
|
|
343
|
+
const styleChunks = [];
|
|
344
|
+
if (existsSync(generatedFontsCss)) styleChunks.push(readFileSync(generatedFontsCss, "utf8"));
|
|
345
|
+
if (existsSync(generatedStylesSrc)) styleChunks.push(readFileSync(generatedStylesSrc, "utf8"));
|
|
346
|
+
if (existsSync(stylesSrc)) styleChunks.push(readFileSync(stylesSrc, "utf8"));
|
|
347
|
+
const hasStyles = styleChunks.length > 0;
|
|
348
|
+
if (hasStyles) {
|
|
349
|
+
writeFileSync(join(DIST_DIR, "styles.css"), styleChunks.join("\n\n"), "utf8");
|
|
350
|
+
}
|
|
351
|
+
writeFileSync(join(DIST_DIR, "router.js"), buildRouterRuntime(), "utf8");
|
|
352
|
+
const { routerAsset, stylesAsset } = applyAssetFingerprinting(manifest);
|
|
353
|
+
generatePwaArtifacts({
|
|
354
|
+
routerAsset,
|
|
355
|
+
stylesAsset,
|
|
356
|
+
cacheVersion: `${routerAsset}-${stylesAsset}`.replace(/[^a-zA-Z0-9.-]/g, "-"),
|
|
357
|
+
});
|
|
358
|
+
writeFileSync(join(DIST_DIR, "fastscript-manifest.json"), JSON.stringify(manifest, null, 2), "utf8");
|
|
359
|
+
writeFileSync(
|
|
360
|
+
join(DIST_DIR, "index.html"),
|
|
361
|
+
buildIndexHtml(hasStyles, { stylesAsset, routerAsset, devMode: manifest.devMode }),
|
|
362
|
+
"utf8",
|
|
363
|
+
);
|
|
364
|
+
|
|
365
|
+
if (options.mode === "ssg") {
|
|
366
|
+
await runSsg(manifest, { stylesAsset, routerAsset });
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
await plugins.onBuildEnd({ appDir: APP_DIR, distDir: DIST_DIR, manifest });
|
|
370
|
+
|
|
371
|
+
console.log(`built FastScript app with ${manifest.routes.length} page route(s) and ${manifest.apiRoutes.length} api route(s)`);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function buildIndexHtml(hasStyles, { stylesAsset = "styles.css", routerAsset = "router.js", content = "", devMode = false } = {}) {
|
|
375
|
+
const faviconSvg = `<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><rect width='32' height='32' rx='8' fill='%23000'/><text x='50%25' y='54%25' dominant-baseline='middle' text-anchor='middle' font-family='system-ui,sans-serif' font-weight='800' font-size='16' fill='%23fff'>FS</text></svg>`;
|
|
376
|
+
const faviconUrl = `data:image/svg+xml,${faviconSvg}`;
|
|
377
|
+
return `<!doctype html>
|
|
378
|
+
<html lang="en">
|
|
379
|
+
<head>
|
|
380
|
+
<meta charset="utf-8" />
|
|
381
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
382
|
+
<title>FastScript — Full-stack .fs language runtime</title>
|
|
383
|
+
<meta name="description" content="Write product code in .fs, compile to optimized JavaScript, and ship to Node, Vercel, or Cloudflare with one command pipeline." />
|
|
384
|
+
<meta name="theme-color" content="#000000" />
|
|
385
|
+
<link rel="icon" type="image/svg+xml" href="${faviconUrl}" />
|
|
386
|
+
<link rel="manifest" href="/manifest.webmanifest" />
|
|
387
|
+
<meta property="og:type" content="website" />
|
|
388
|
+
<meta property="og:title" content="FastScript — Full-stack .fs language runtime" />
|
|
389
|
+
<meta property="og:description" content="Write .fs, compile to JS, ship anywhere. 1.8KB runtime. <1s builds. 3 deploy targets." />
|
|
390
|
+
<meta property="og:image" content="/og-image.png" />
|
|
391
|
+
<meta name="twitter:card" content="summary_large_image" />
|
|
392
|
+
<meta name="twitter:title" content="FastScript" />
|
|
393
|
+
<meta name="twitter:description" content="Full-stack .fs language runtime. Write once, ship to Node, Vercel, or Cloudflare." />
|
|
394
|
+
${hasStyles ? `<link rel="stylesheet" href="/${stylesAsset}" />` : ""}
|
|
395
|
+
</head>
|
|
396
|
+
<body>
|
|
397
|
+
<div id="app">${content}</div>
|
|
398
|
+
<script type="module" src="/${routerAsset}"></script>
|
|
399
|
+
<script>
|
|
400
|
+
(function () {
|
|
401
|
+
if (!("serviceWorker" in navigator)) return;
|
|
402
|
+
const isDevMode = ${devMode ? "true" : "false"};
|
|
403
|
+
const host = (location && location.hostname) || "";
|
|
404
|
+
const isLocalhost = host === "localhost" || host === "127.0.0.1" || host === "::1";
|
|
405
|
+
if (isDevMode || isLocalhost) {
|
|
406
|
+
navigator.serviceWorker.getRegistrations()
|
|
407
|
+
.then((regs) => Promise.all(regs.map((r) => r.unregister())))
|
|
408
|
+
.catch(() => {});
|
|
409
|
+
if (window.caches && typeof window.caches.keys === "function") {
|
|
410
|
+
window.caches.keys()
|
|
411
|
+
.then((keys) => Promise.all(keys.filter((k) => String(k).indexOf("fastscript-") === 0).map((k) => window.caches.delete(k))))
|
|
412
|
+
.catch(() => {});
|
|
413
|
+
}
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
navigator.serviceWorker.register("/service-worker.js").catch(() => {});
|
|
417
|
+
})();
|
|
418
|
+
</script>
|
|
419
|
+
</body>
|
|
420
|
+
</html>`;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function buildRouterRuntime() {
|
|
424
|
+
return `
|
|
425
|
+
const app = document.getElementById("app");
|
|
426
|
+
const manifest = await fetch("/fastscript-manifest.json").then((r) => r.json());
|
|
427
|
+
let overlayEl = null;
|
|
428
|
+
${buildStylePrimitiveRuntimeSource()}
|
|
429
|
+
|
|
430
|
+
function stackMap(stack) {
|
|
431
|
+
if (!stack) return "";
|
|
432
|
+
return String(stack).replace(/\\b(dist\\\\|dist\\/)/g, "app/");
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function showOverlay(error) {
|
|
436
|
+
if (!manifest.devMode) return;
|
|
437
|
+
if (!overlayEl) {
|
|
438
|
+
overlayEl = document.createElement("div");
|
|
439
|
+
overlayEl.id = "__fs_error_overlay";
|
|
440
|
+
overlayEl.style.cssText = "position:fixed;inset:0;z-index:999999;background:rgba(10,10,10,.94);color:#fff;padding:24px;font:14px/1.45 ui-monospace,Menlo,monospace;overflow:auto;white-space:pre-wrap";
|
|
441
|
+
document.body.appendChild(overlayEl);
|
|
442
|
+
}
|
|
443
|
+
const message = error?.message || String(error);
|
|
444
|
+
const stack = stackMap(error?.stack || "");
|
|
445
|
+
overlayEl.textContent = "FastScript Runtime Error\\n\\n" + message + (stack ? "\\n\\n" + stack : "");
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function clearOverlay() {
|
|
449
|
+
if (!overlayEl) return;
|
|
450
|
+
overlayEl.remove();
|
|
451
|
+
overlayEl = null;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function parseRouteToken(token) {
|
|
455
|
+
const m = /^:([A-Za-z_$][\\w$]*)(\\*)?(\\?)?$/.exec(token || "");
|
|
456
|
+
if (!m) return null;
|
|
457
|
+
return { name: m[1], catchAll: Boolean(m[2]), optional: Boolean(m[3]) };
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function match(routePath, pathname) {
|
|
461
|
+
const routeParts = routePath.split("/").filter(Boolean);
|
|
462
|
+
const pathParts = pathname.split("/").filter(Boolean);
|
|
463
|
+
const params = {};
|
|
464
|
+
let ri = 0;
|
|
465
|
+
let pi = 0;
|
|
466
|
+
|
|
467
|
+
while (ri < routeParts.length) {
|
|
468
|
+
const token = routeParts[ri];
|
|
469
|
+
const dyn = parseRouteToken(token);
|
|
470
|
+
|
|
471
|
+
if (dyn?.catchAll) {
|
|
472
|
+
const rest = pathParts.slice(pi);
|
|
473
|
+
if (!rest.length && !dyn.optional) return null;
|
|
474
|
+
params[dyn.name] = rest;
|
|
475
|
+
pi = pathParts.length;
|
|
476
|
+
ri = routeParts.length;
|
|
477
|
+
break;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
if (dyn) {
|
|
481
|
+
const value = pathParts[pi];
|
|
482
|
+
if (value === undefined) {
|
|
483
|
+
if (dyn.optional) {
|
|
484
|
+
params[dyn.name] = undefined;
|
|
485
|
+
ri += 1;
|
|
486
|
+
continue;
|
|
487
|
+
}
|
|
488
|
+
return null;
|
|
489
|
+
}
|
|
490
|
+
params[dyn.name] = value;
|
|
491
|
+
ri += 1;
|
|
492
|
+
pi += 1;
|
|
493
|
+
continue;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
if (pathParts[pi] !== token) return null;
|
|
497
|
+
ri += 1;
|
|
498
|
+
pi += 1;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
if (pi !== pathParts.length) return null;
|
|
502
|
+
return params;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function routePriorityScore(routePath) {
|
|
506
|
+
const parts = String(routePath || "/").split("/").filter(Boolean);
|
|
507
|
+
if (!parts.length) return 1000;
|
|
508
|
+
let score = parts.length;
|
|
509
|
+
for (const part of parts) {
|
|
510
|
+
const dyn = parseRouteToken(part);
|
|
511
|
+
if (!dyn) score += 40;
|
|
512
|
+
else if (dyn.catchAll && dyn.optional) score += 5;
|
|
513
|
+
else if (dyn.catchAll) score += 10;
|
|
514
|
+
else if (dyn.optional) score += 20;
|
|
515
|
+
else score += 30;
|
|
516
|
+
}
|
|
517
|
+
return score;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
function findRoute(pathname) {
|
|
521
|
+
let best = null;
|
|
522
|
+
for (const route of manifest.routes) {
|
|
523
|
+
const params = match(route.path, pathname);
|
|
524
|
+
if (!params) continue;
|
|
525
|
+
if (!best) {
|
|
526
|
+
best = { route, params, score: routePriorityScore(route.path) };
|
|
527
|
+
continue;
|
|
528
|
+
}
|
|
529
|
+
const score = routePriorityScore(route.path);
|
|
530
|
+
if (score > best.score) best = { route, params, score };
|
|
531
|
+
}
|
|
532
|
+
return best ? { route: best.route, params: best.params } : null;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function resolveLocale(pathname) {
|
|
536
|
+
const cfg = manifest.i18n || { locales: ["en"], defaultLocale: "en" };
|
|
537
|
+
const parts = String(pathname || "/").split("/").filter(Boolean);
|
|
538
|
+
const head = parts[0];
|
|
539
|
+
if (head && cfg.locales.includes(head)) {
|
|
540
|
+
const normalized = "/" + parts.slice(1).join("/");
|
|
541
|
+
return { locale: head, pathname: normalized === "/" ? "/" : normalized || "/" };
|
|
542
|
+
}
|
|
543
|
+
return { locale: cfg.defaultLocale || "en", pathname: pathname || "/" };
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
async function hydrate(mod, ctx) {
|
|
547
|
+
if (typeof mod.hydrate === "function") {
|
|
548
|
+
await mod.hydrate({ ...ctx, root: app });
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
async function hydrateLayouts(route, ctx) {
|
|
553
|
+
const layouts = route?.layouts && route.layouts.length ? route.layouts : (manifest.layout ? [manifest.layout] : []);
|
|
554
|
+
for (const layoutPath of layouts) {
|
|
555
|
+
const layout = await import(layoutPath);
|
|
556
|
+
if (typeof layout.hydrate === "function") {
|
|
557
|
+
await layout.hydrate({ ...ctx, root: app });
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
async function applyLayouts(html, route, ctx, slots = {}) {
|
|
563
|
+
const layouts = route?.layouts && route.layouts.length ? route.layouts : (manifest.layout ? [manifest.layout] : []);
|
|
564
|
+
if (!layouts.length) return html;
|
|
565
|
+
let out = html;
|
|
566
|
+
for (const layoutPath of layouts) {
|
|
567
|
+
const layout = await import(layoutPath);
|
|
568
|
+
out = layout.default ? layout.default({ ...ctx, slots, content: out }) : out;
|
|
569
|
+
}
|
|
570
|
+
return out;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
async function resolveParallel(pathname, params, data) {
|
|
574
|
+
const out = {};
|
|
575
|
+
for (const route of manifest.parallelRoutes || []) {
|
|
576
|
+
const hit = match(route.path, pathname);
|
|
577
|
+
if (!hit) continue;
|
|
578
|
+
const mod = await import(route.module);
|
|
579
|
+
let payload = {};
|
|
580
|
+
if (typeof mod.load === "function") payload = (await mod.load({ pathname, params, slot: route.slot })) || {};
|
|
581
|
+
const html = mod.default ? mod.default({ ...payload, params, pathname, slot: route.slot }) : "";
|
|
582
|
+
out[route.slot || "default"] = html || "";
|
|
583
|
+
}
|
|
584
|
+
return out;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
async function render(pathname, force = false) {
|
|
588
|
+
try {
|
|
589
|
+
clearOverlay();
|
|
590
|
+
const path = pathname || "/";
|
|
591
|
+
const localeInfo = resolveLocale(path);
|
|
592
|
+
const routePath = localeInfo.pathname;
|
|
593
|
+
const ssr = globalThis.__FASTSCRIPT_SSR;
|
|
594
|
+
const initialHit = ssr && ssr.pathname === routePath;
|
|
595
|
+
|
|
596
|
+
const matched = findRoute(routePath);
|
|
597
|
+
let mod = null;
|
|
598
|
+
let params = {};
|
|
599
|
+
let data = {};
|
|
600
|
+
let html = "";
|
|
601
|
+
let slots = {};
|
|
602
|
+
|
|
603
|
+
if (matched) {
|
|
604
|
+
params = matched.params;
|
|
605
|
+
mod = await import(matched.route.module);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
if (initialHit && !force) {
|
|
609
|
+
html = app.innerHTML;
|
|
610
|
+
if (ssr?.data) data = ssr.data;
|
|
611
|
+
} else if (!matched && manifest.notFound) {
|
|
612
|
+
const nfMod = await import(manifest.notFound);
|
|
613
|
+
html = (nfMod.default ? nfMod.default({ pathname: routePath, locale: localeInfo.locale }) : "<h1>404</h1>") || "";
|
|
614
|
+
app.innerHTML = html;
|
|
615
|
+
} else if (matched) {
|
|
616
|
+
if (typeof mod.load === "function") data = (await mod.load({ params, pathname: routePath, locale: localeInfo.locale })) || {};
|
|
617
|
+
html = (mod.default ? mod.default({ ...data, params, pathname: routePath, locale: localeInfo.locale }) : "") || "";
|
|
618
|
+
slots = await resolveParallel(routePath, params, data);
|
|
619
|
+
html = await applyLayouts(html, matched.route, { pathname: routePath, locale: localeInfo.locale, params, data }, slots);
|
|
620
|
+
app.innerHTML = html;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
upgradeStylePrimitivesInDom(app);
|
|
624
|
+
|
|
625
|
+
bindLinks();
|
|
626
|
+
if (matched) {
|
|
627
|
+
await hydrateLayouts(matched.route, { pathname: routePath, locale: localeInfo.locale, params, data, slots });
|
|
628
|
+
} else if (!matched && manifest.notFound) {
|
|
629
|
+
await hydrateLayouts({ layouts: manifest.layout ? [manifest.layout] : [] }, { pathname: routePath, locale: localeInfo.locale, params, data, slots });
|
|
630
|
+
}
|
|
631
|
+
if (mod) await hydrate(mod, { pathname: routePath, locale: localeInfo.locale, params, data, slots });
|
|
632
|
+
globalThis.__FASTSCRIPT_SSR = null;
|
|
633
|
+
} catch (error) {
|
|
634
|
+
showOverlay(error);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
function bindLinks() {
|
|
639
|
+
for (const a of app.querySelectorAll('a[href^="/"]')) {
|
|
640
|
+
if (a.dataset.fsBound === "1") continue;
|
|
641
|
+
a.dataset.fsBound = "1";
|
|
642
|
+
a.addEventListener("click", (e) => {
|
|
643
|
+
e.preventDefault();
|
|
644
|
+
let href = a.getAttribute("href");
|
|
645
|
+
const localeInfo = resolveLocale(location.pathname);
|
|
646
|
+
const cfg = manifest.i18n || { locales: ["en"], defaultLocale: "en" };
|
|
647
|
+
if (localeInfo.locale && localeInfo.locale !== cfg.defaultLocale && !href.startsWith("/" + localeInfo.locale + "/") && href !== "/" + localeInfo.locale) {
|
|
648
|
+
href = href === "/" ? "/" + localeInfo.locale : "/" + localeInfo.locale + href;
|
|
649
|
+
}
|
|
650
|
+
history.pushState({}, "", href);
|
|
651
|
+
window.scrollTo(0, 0);
|
|
652
|
+
render(location.pathname, true);
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
if (history.scrollRestoration) history.scrollRestoration = "manual";
|
|
658
|
+
|
|
659
|
+
function connectHmr() {
|
|
660
|
+
if (!manifest.devMode) return;
|
|
661
|
+
const es = new EventSource("/__hmr");
|
|
662
|
+
es.onmessage = () => render(location.pathname, true);
|
|
663
|
+
es.onerror = () => {};
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
window.addEventListener("popstate", () => render(location.pathname, true));
|
|
667
|
+
connectHmr();
|
|
668
|
+
render(location.pathname, false);
|
|
669
|
+
`;
|
|
670
|
+
}
|