@stnd/core 0.1.0 → 0.22.2

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/standard.js ADDED
@@ -0,0 +1,545 @@
1
+ /**
2
+ * @stnd/core Astro Integration
3
+ *
4
+ * The core orchestration layer that bootstraps the Standard module system,
5
+ * manages assets, virtual modules, and proxies lifecycle hooks.
6
+ */
7
+ import { fileURLToPath, pathToFileURL } from "url";
8
+ import * as path from "path";
9
+ import * as fs from "fs";
10
+ import { createRequire } from "module";
11
+ import svelte from "@astrojs/svelte";
12
+ import Log from "@stnd/log";
13
+ import { deepMerge, mergeModuleConfigs } from "@stnd/utils";
14
+ import { loadModules } from "./src/loader.js";
15
+ import {
16
+ generateConfigModule,
17
+ generateModulesModule,
18
+ generateModulesMetadataModule,
19
+ generateModuleDomainsModule,
20
+ generateStylesModule,
21
+ generateScriptsModule,
22
+ generateComponentsModule,
23
+ generateMiddlewareModule,
24
+ generateActionsModule,
25
+ generateHooksModule,
26
+ generateContentModule,
27
+ generateStoreModule,
28
+ } from "./src/virtual.js";
29
+
30
+ const __filename = fileURLToPath(import.meta.url);
31
+ const __dirname = path.dirname(__filename);
32
+
33
+ export default function standard(options = {}) {
34
+ const startTime = performance.now();
35
+ const log = Log({
36
+ scope: "Core",
37
+ level: options?.debug ? "DEBUG" : undefined,
38
+ });
39
+ let allLifecycleExtensions = [];
40
+
41
+ /** Helper to filter registered hooks by name */
42
+ const activeHooks = (name) => {
43
+ return allLifecycleExtensions.filter((e) => e.hook === name);
44
+ };
45
+
46
+ /** Helper to import and execute a hook from a module */
47
+ const proxyHook = async (hookExt, args) => {
48
+ try {
49
+ const runner = await import(pathToFileURL(hookExt.entrypoint).href);
50
+ const fn = runner.onRequest || runner.default || runner.handler || runner;
51
+ if (typeof fn === "function") {
52
+ await fn(args);
53
+ }
54
+ } catch (e) {
55
+ log.error(
56
+ `Failed to execute module hook [${hookExt.hook}] from [${hookExt.moduleId}]:`,
57
+ e,
58
+ );
59
+ }
60
+ };
61
+
62
+ const integration = {
63
+ name: "@stnd/core",
64
+ hooks: {
65
+ "astro:config:setup": async ({
66
+ command,
67
+ config,
68
+ updateConfig,
69
+ injectScript,
70
+ injectRoute,
71
+ addMiddleware,
72
+ addWatchFile,
73
+ addDevToolbarApp,
74
+ }) => {
75
+ log.debug("Phase: [config:setup] starting...");
76
+
77
+ // 1. Prepare Base Configuration
78
+ const safeConfig = {};
79
+ const unsafeKeys = [
80
+ "vite",
81
+ "integrations",
82
+ "server",
83
+ "adapter",
84
+ "markdown",
85
+ "build",
86
+ "_cloudflare",
87
+ ];
88
+ for (const key in config) {
89
+ if (!unsafeKeys.includes(key)) safeConfig[key] = config[key];
90
+ }
91
+
92
+ // Gold Standard defaults — every @stnd site ships with these.
93
+ // Users extend via `moduleLoad`, opt-out via `moduleExclude`.
94
+ const GOLD_STANDARD_MODULES = [
95
+ // System UI — every page gets these
96
+ "@stnd/modules/toast",
97
+ "@stnd/modules/confetti",
98
+ "@stnd/modules/lab",
99
+ "@stnd/modules/launcher",
100
+
101
+ // Styles & fonts
102
+ "@stnd/modules/styles",
103
+ "@stnd/fonts/inter",
104
+ "@stnd/fonts/source-serif-4",
105
+ "@stnd/fonts/ibm-plex",
106
+
107
+ // Client enhancements
108
+ "@stnd/modules/copy-buttons",
109
+ "@stnd/modules/image-zoom",
110
+ "@stnd/modules/scroll-wrappers",
111
+
112
+ // Content enhancements
113
+ "@stnd/modules/mermaid",
114
+ "@stnd/modules/math",
115
+ "@stnd/modules/prism",
116
+
117
+ // SEO & web standards
118
+ "@stnd/modules/robots",
119
+ "@stnd/modules/headers",
120
+ "@stnd/modules/manifest",
121
+ "@stnd/modules/sitemap",
122
+
123
+ // Icon & Lottie asset delivery
124
+ "@stnd/icon/module",
125
+ ];
126
+
127
+ const baseOptions = {
128
+ moduleFolder: "modules",
129
+ };
130
+
131
+ // Merge everything except moduleLoad (we handle that ourselves)
132
+ const {
133
+ moduleLoad: userModuleLoad,
134
+ moduleExclude: userModuleExclude,
135
+ ...restOptions
136
+ } = options || {};
137
+
138
+ // Create excludeSet early to conditionally register toolbar apps
139
+ const excludeSet = new Set(userModuleExclude || []);
140
+
141
+ // Register Standard Debug Toggle in Astro Dev Bar
142
+ addDevToolbarApp({
143
+ id: "stnd-debug",
144
+ name: "Standard Debug",
145
+ icon: "puzzle",
146
+ entrypoint: fileURLToPath(
147
+ new URL("./src/dev-toolbar/debug-toggle.js", import.meta.url),
148
+ ),
149
+ });
150
+ // Only register Lab toggle if lab module is loaded
151
+ if (!excludeSet.has("@stnd/modules/lab")) {
152
+ addDevToolbarApp({
153
+ id: "stnd-lab",
154
+ name: "Standard Lab",
155
+ icon: "robot",
156
+ entrypoint: fileURLToPath(
157
+ new URL("./src/dev-toolbar/lab-toggle.js", import.meta.url),
158
+ ),
159
+ });
160
+ }
161
+ const finalConfig = deepMerge({}, baseOptions, safeConfig, restOptions);
162
+
163
+ // Build the final module list: gold standard + user additions - exclusions
164
+ const goldModules = GOLD_STANDARD_MODULES.filter(
165
+ (m) => !excludeSet.has(m),
166
+ );
167
+ const userModules = (userModuleLoad || []).filter(
168
+ (m) => !GOLD_STANDARD_MODULES.includes(m),
169
+ );
170
+ finalConfig.moduleLoad = [...goldModules, ...userModules];
171
+ globalThis.__STANDARD_CONFIG__ = finalConfig;
172
+
173
+ // Keep user-level config separate so it can override module contributions
174
+ const userConfig = { ...restOptions };
175
+
176
+ const { modules, loadedPaths } = await loadModules({
177
+ command,
178
+ config,
179
+ finalConfig,
180
+ injectRoute,
181
+ injectScript,
182
+ });
183
+
184
+ // Merge module config contributions (in load order), then user config on top.
185
+ // deepMerge concatenates arrays (e.g. nav items) and recurses objects.
186
+ if (modules.config.length > 0) {
187
+ const merged = mergeModuleConfigs(
188
+ finalConfig,
189
+ modules.config,
190
+ userConfig,
191
+ );
192
+ Object.assign(finalConfig, merged);
193
+ globalThis.__STANDARD_CONFIG__ = finalConfig;
194
+ log.debug(
195
+ `Config: merged contributions from ${modules.config.length} module(s)`,
196
+ );
197
+ }
198
+
199
+ // Merge module icon token contributions.
200
+ // Module tokens are the base layer; user-level tokens (from standard() config) win on top.
201
+ // Resolution priority: user tokens > module tokens > built-in tokens.js defaults
202
+ if (
203
+ modules.icon &&
204
+ (Object.keys(modules.icon.tokens).length > 0 || modules.icon.library)
205
+ ) {
206
+ finalConfig.icon = finalConfig.icon ?? {};
207
+ finalConfig.icon.tokens = {
208
+ ...modules.icon.tokens, // module contributions (base)
209
+ ...(finalConfig.icon.tokens ?? {}), // user-level overrides win
210
+ };
211
+ if (modules.icon.library && !finalConfig.icon.library) {
212
+ finalConfig.icon.library = modules.icon.library;
213
+ }
214
+ globalThis.__STANDARD_CONFIG__ = finalConfig;
215
+ }
216
+
217
+ // Actions Check: If modules provide actions, ensure the project has the bridge file
218
+ if (modules.actions.length > 0) {
219
+ const rootPath = fileURLToPath(config.root);
220
+ // Check for index.ts, index.js or index.mjs
221
+ const hasActionsFile = [".ts", ".js", ".mjs"].some((ext) =>
222
+ fs.existsSync(path.resolve(rootPath, `src/actions/index${ext}`)),
223
+ );
224
+
225
+ if (!hasActionsFile) {
226
+ log.warn(
227
+ `Modules define actions but 'src/actions/index.ts' is missing.\n` +
228
+ ` Actions will trigger 404 errors without this bridge file.\n` +
229
+ ` See fix: https://stnd.build/manual/diagnostic/actions-404`,
230
+ );
231
+ }
232
+ }
233
+
234
+ // Watch System: Trigger reload when manifest modify
235
+ loadedPaths.forEach((p) => {
236
+ addWatchFile(p);
237
+ });
238
+
239
+ // Watch System: Watch the module folder for new additions
240
+ if (finalConfig.moduleFolder) {
241
+ const moduleRoot = path.resolve(
242
+ fileURLToPath(config.root),
243
+ finalConfig.moduleFolder,
244
+ );
245
+ addWatchFile(moduleRoot);
246
+ }
247
+
248
+ allLifecycleExtensions = modules.hooks;
249
+
250
+ // Vault support is owned by @stnd/loader-vault (the content-collection
251
+ // loader apps actually use). Core's old `vault:` option built a second,
252
+ // parallel index — removed. One vault owner. See ADR-0001 context.
253
+
254
+ // 3. Register Middlewares
255
+ if (addMiddleware && modules.middleware.length > 0) {
256
+ const sortedMiddleware = [...modules.middleware].sort(
257
+ (a, b) => (a.order || 0) - (b.order || 0),
258
+ );
259
+ sortedMiddleware.forEach((mw) => {
260
+ addMiddleware({
261
+ entrypoint: mw.entrypoint,
262
+ order: (mw.order || 0) < 0 ? "pre" : "post",
263
+ });
264
+ });
265
+ }
266
+
267
+ // 4. Vite & Workspace Aliases (Universal Resolution)
268
+ //
269
+ // @stnd/core is the sanctioned "meta-package": its own code imports only
270
+ // @stnd/log + @stnd/utils, but it *declares* the whole framework as
271
+ // dependencies so a single `install @stnd/core` brings everything. The
272
+ // alias list is derived from those declared @stnd/* deps, so it can never
273
+ // drift from what the package actually ships. (See packages/README.)
274
+ const require = createRequire(import.meta.url);
275
+ const corePkg = JSON.parse(
276
+ fs.readFileSync(path.join(__dirname, "package.json"), "utf-8"),
277
+ );
278
+ const packages = Object.keys(corePkg.dependencies || {})
279
+ .filter((dep) => dep.startsWith("@stnd/"))
280
+ .map((dep) => dep.slice("@stnd/".length));
281
+
282
+ const aliases = packages.reduce((acc, pkg) => {
283
+ const pkgName = `@stnd/${pkg}`;
284
+ try {
285
+ const pkgMain = require.resolve(`${pkgName}/package.json`);
286
+ const pkgDir = path.dirname(pkgMain);
287
+ acc[pkgName] = pkgDir;
288
+ acc[`${pkgName}/`] = path.join(pkgDir, "/");
289
+ } catch (e) {
290
+ // Fallback for monorepo development: assume packages/pkg exists
291
+ const fallbackPath = path.resolve(__dirname, `../${pkg}`);
292
+ log.debug(
293
+ `Package ${pkgName} resolved via monorepo fallback: ${fallbackPath}`,
294
+ );
295
+ acc[pkgName] = fallbackPath;
296
+ acc[`${pkgName}/`] = path.join(fallbackPath, "/");
297
+ }
298
+ return acc;
299
+ }, {});
300
+
301
+ Object.assign(aliases, modules.aliases || {});
302
+
303
+ // @modules — canonical alias for the app's vertical slice folder
304
+ // Every @stnd/core app gets this for free. No configuration needed.
305
+ if (finalConfig.moduleFolder) {
306
+ const moduleRoot = path.resolve(
307
+ fileURLToPath(config.root),
308
+ finalConfig.moduleFolder,
309
+ );
310
+ aliases["@modules"] = moduleRoot;
311
+ aliases["@modules/"] = moduleRoot + "/";
312
+ }
313
+
314
+ // 5. Virtual Modules — must be registered via updateConfig so the
315
+ // module lands in the actual merged Vite config, not a stale reference.
316
+ const virtualModuleStnd = {
317
+ name: "vite-module-stnd",
318
+ resolveId(id, importer, options) {
319
+ if (!id.startsWith("virtual:stnd/")) return;
320
+ // virtual:stnd/scripts and virtual:stnd/store are imported from
321
+ // <script> tags (client-side), so the browser fetches the URL
322
+ // directly in dev mode. The \0 prefix gets encoded as __x00__
323
+ // in the URL and the dev server doesn't decode it back → 404.
324
+ // All other virtual modules are imported from the frontmatter
325
+ // (SSR/Vite context) where \0 works fine.
326
+ if (id === "virtual:stnd/scripts") return "virtual-stnd/scripts";
327
+ if (id === "virtual:stnd/store") return "virtual-stnd/store";
328
+
329
+ // Hooks need environment-specific IDs so we can filter server-only hooks
330
+ if (id === "virtual:stnd/hooks") {
331
+ return options?.ssr
332
+ ? "\0virtual-stnd/hooks-server"
333
+ : "\0virtual-stnd/hooks-client";
334
+ }
335
+
336
+ return "\0" + id.replace("virtual:", "virtual-");
337
+ },
338
+ load(id) {
339
+ const appHooks = modules.hooks.filter(
340
+ (e) => !e.hook.startsWith("astro:"),
341
+ );
342
+ if (id === "\0virtual-stnd/config")
343
+ return generateConfigModule(finalConfig);
344
+ if (id === "\0virtual-stnd/modules")
345
+ return generateModulesModule(modules.runtime);
346
+ if (id === "\0virtual-stnd/modules-metadata")
347
+ return generateModulesMetadataModule(modules.runtime);
348
+ if (id === "\0virtual-stnd/module-domains")
349
+ return generateModuleDomainsModule(modules.domainFilters);
350
+ if (id === "\0virtual-stnd/components")
351
+ return generateComponentsModule(modules.ui);
352
+ if (id === "\0virtual-stnd/middleware")
353
+ return generateMiddlewareModule(modules.middleware);
354
+ if (id === "\0virtual-stnd/actions")
355
+ return generateActionsModule(modules.actions);
356
+ if (id === "\0virtual-stnd/hooks-server")
357
+ return generateHooksModule(appHooks, { ssr: true });
358
+ if (id === "\0virtual-stnd/hooks-client")
359
+ return generateHooksModule(appHooks, { ssr: false });
360
+ if (id === "\0virtual-stnd/styles")
361
+ return generateStylesModule(modules.styles);
362
+ if (id === "virtual-stnd/scripts")
363
+ return generateScriptsModule(modules.scripts);
364
+ if (id === "virtual-stnd/store")
365
+ return generateStoreModule(modules.store);
366
+ if (id === "\0virtual-stnd/content")
367
+ return generateContentModule(modules.content);
368
+ },
369
+ };
370
+
371
+ // Resolve module integrations: factory functions receive the fully merged
372
+ // finalConfig (including all icon token contributions); plain integration
373
+ // objects are used as-is.
374
+ const moduleIntegrations = [
375
+ svelte(),
376
+ ...modules.integrations
377
+ .flatMap((entry) =>
378
+ typeof entry === "function" ? entry(finalConfig) : [entry],
379
+ )
380
+ .filter(Boolean),
381
+ ];
382
+
383
+ updateConfig({
384
+ // Speed is a respect (Standard value #2): every Standard app prefetches
385
+ // links in the viewport and speculatively prerenders them. These are
386
+ // root-level Astro config options — setting them on the integration
387
+ // object (as before) was a silent no-op; Astro only reads `name` +
388
+ // `hooks` off an integration. `clientPrerender` is an Astro experimental
389
+ // flag, valid in the pinned Astro 7.x; it degrades gracefully in
390
+ // browsers without the Speculation Rules API.
391
+ prefetch: {
392
+ prefetchAll: true,
393
+ defaultStrategy: "viewport",
394
+ },
395
+ experimental: {
396
+ clientPrerender: true,
397
+ },
398
+ integrations: moduleIntegrations,
399
+ vite: {
400
+ plugins: [virtualModuleStnd],
401
+ resolve: {
402
+ alias: aliases,
403
+ dedupe: ["svelte"],
404
+ // Indispensable en Monorepo pour que @stnd/* reste @stnd/* et soit bundlé
405
+ // preserveSymlinks: true, -> dont apply it or common-ancestor-path will scream
406
+ extensions: [
407
+ ".astro",
408
+ ".svelte.js",
409
+ ".svelte.ts",
410
+ ".mjs",
411
+ ".js",
412
+ ".ts",
413
+ ".jsx",
414
+ ".tsx",
415
+ ".json",
416
+ ],
417
+ },
418
+ optimizeDeps: {
419
+ entries: (modules.runtime || [])
420
+ .map((m) =>
421
+ m.__importPath ? fileURLToPath(m.__importPath) : null,
422
+ )
423
+ .filter(Boolean),
424
+ include: modules.optimizeDeps || [],
425
+ exclude: [...packages.map((p) => `@stnd/${p}`), "cloudflare:workers"],
426
+ },
427
+ ssr: {
428
+ noExternal: ["@stnd/*"],
429
+ },
430
+ },
431
+ });
432
+
433
+ // 6. Asset Injection (Virtual Style Module + Individual Scripts)
434
+ // Collect external CDN script URLs into finalConfig so the virtual
435
+ // config module exposes them. Base.astro renders them as <script> / <link>
436
+ // tags directly in <head>, which is the only reliable path in
437
+ // Astro 6 beta + Cloudflare adapter dev mode (where the injectScript
438
+ // API's pipeline is bypassed entirely by the NonRunnablePipeline).
439
+ // Separate external CDN string URLs from local/object entries.
440
+ // External strings become <script src> tags via Base.astro.
441
+ // Everything else (local strings + objects) goes into virtual:stnd/scripts.
442
+ const externalScripts = [
443
+ ...new Set(
444
+ (modules.scripts || []).filter(
445
+ (s) => typeof s === "string" && /^https?:\/\//.test(s),
446
+ ),
447
+ ),
448
+ ];
449
+ const externalStyles = [
450
+ ...new Set(
451
+ (modules.styles || []).filter((s) => /^https?:\/\//.test(s)),
452
+ ),
453
+ ];
454
+ // Keep only local styles for static imports — external styles via <link> tags in head.
455
+ const localStyles = (modules.styles || []).filter(
456
+ (s) => !/^https?:\/\//.test(s),
457
+ );
458
+ modules.styles = localStyles;
459
+
460
+ if (externalScripts.length > 0) {
461
+ finalConfig._externalScripts = externalScripts;
462
+ }
463
+ if (externalStyles.length > 0) {
464
+ finalConfig._externalStyles = externalStyles;
465
+ }
466
+ if (externalScripts.length > 0 || externalStyles.length > 0) {
467
+ globalThis.__STANDARD_CONFIG__ = finalConfig;
468
+ }
469
+
470
+ log.debug(
471
+ `Assets: ${modules.styles.length} styles, ${modules.scripts.length} scripts, ${modules.head.length} head tags`,
472
+ );
473
+
474
+ // Inject the virtual styles module automatically (Clean & SSR-Safe)
475
+ if (modules.styles.length > 0) {
476
+ injectScript("page-ssr", 'import "virtual:stnd/styles";');
477
+ injectScript("page", 'import "virtual:stnd/styles";');
478
+ }
479
+
480
+ // Inject the Standard OS runtime store (provide/inject)
481
+ // Must run before components mount so inject() calls find their values.
482
+ if (modules.store.length > 0) {
483
+ injectScript("page", 'import "virtual:stnd/store";');
484
+ }
485
+
486
+ // Inject virtual:stnd/scripts once — it handles both fire-and-forget
487
+ // string imports and lifecycle-managed object entries in one bundle.
488
+ if (modules.scripts.length > 0) {
489
+ injectScript("page", 'import "virtual:stnd/scripts";');
490
+ }
491
+
492
+ // External CDN scripts can't go through Vite's bundler.
493
+ // Inject via `head-inline` so they render for ALL routes.
494
+ externalScripts.forEach((s) => {
495
+ injectScript(
496
+ "head-inline",
497
+ `{const _s=document.createElement("script");_s.src=${JSON.stringify(s)};document.head.appendChild(_s);}`,
498
+ );
499
+ });
500
+
501
+ modules.head.forEach((h) => {
502
+ if (h.type === "script") {
503
+ injectScript("page-ssr", `import ${JSON.stringify(h.content)};`);
504
+ } else if (h.type === "inline") {
505
+ injectScript("head-inline", h.content);
506
+ }
507
+ });
508
+
509
+ log.debug("Phase: [config:setup] success");
510
+ },
511
+
512
+ // --- Hook Proxying ---
513
+ "astro:config:done": async (args) => {
514
+ for (const h of activeHooks("astro:config:done"))
515
+ await proxyHook(h, args);
516
+ },
517
+ "astro:server:setup": async (args) => {
518
+ for (const h of activeHooks("astro:server:setup"))
519
+ await proxyHook(h, args);
520
+ },
521
+ "astro:server:start": async (args) => {
522
+ const { address } = args;
523
+ const elapsed = Math.round(performance.now() - startTime);
524
+ const url = `http://${address.address === "0.0.0.0" || address.address === "::" ? "localhost" : address.address}:${address.port}`;
525
+ log.bannerSuccess(`Kernel Ready: ${url} (${elapsed}ms)`);
526
+ for (const h of activeHooks("astro:server:start"))
527
+ await proxyHook(h, args);
528
+ },
529
+ "astro:build:start": async (args) => {
530
+ for (const h of activeHooks("astro:build:start"))
531
+ await proxyHook(h, args);
532
+ },
533
+ "astro:build:setup": async (args) => {
534
+ for (const h of activeHooks("astro:build:setup"))
535
+ await proxyHook(h, args);
536
+ },
537
+ "astro:build:done": async (args) => {
538
+ for (const h of activeHooks("astro:build:done"))
539
+ await proxyHook(h, args);
540
+ },
541
+ },
542
+ };
543
+
544
+ return integration;
545
+ }
package/CHANGELOG.md DELETED
@@ -1,13 +0,0 @@
1
- # @stnd/core
2
-
3
- ## 0.1.0
4
-
5
- ### Minor Changes
6
-
7
- - 6d8888a: First release
8
-
9
- ### Patch Changes
10
-
11
- - Updated dependencies [6d8888a]
12
- - @stnd/log@0.2.0
13
- - @stnd/utils@0.2.0
@@ -1,55 +0,0 @@
1
-
2
- import { StandardRoot } from "./StandardRoot.js";
3
-
4
- export abstract class StandardModel<TData, TRoot extends StandardRoot = StandardRoot> {
5
- public raw: TData;
6
- protected root: TRoot;
7
-
8
- constructor(root: TRoot, data: TData) {
9
- this.root = root;
10
- this.raw = data;
11
- }
12
-
13
- /**
14
- * SERIALIZATION
15
- * Strips the circular 'root' dependency for Svelte/React
16
- */
17
- toJSON(): TData {
18
- return this.raw;
19
- }
20
-
21
- /**
22
- * AUTOMATIC CACHING (The "Standard" Way)
23
- * Replaces manual KV logic
24
- */
25
- protected static async withCache<T>(
26
- root: StandardRoot,
27
- key: string,
28
- fetcher: () => Promise<T | null>,
29
- ttl: number = 60 // Default 60s
30
- ): Promise<T | null> {
31
- const { env } = root;
32
-
33
- // 1. Try KV (Fast Path)
34
- if (env.GARDEN_KV) {
35
- try {
36
- const hit = await env.GARDEN_KV.get(key, { type: "json" });
37
- if (hit) return hit as T;
38
- } catch (e) {
39
- console.warn("KV Get Error", e);
40
- }
41
- }
42
-
43
- // 2. Fetch Source (Slow Path)
44
- const data = await fetcher();
45
-
46
- // 3. Update KV (Background Path)
47
- if (data && env.GARDEN_KV) {
48
- root.waitUntil(
49
- env.GARDEN_KV.put(key, JSON.stringify(data), { expirationTtl: ttl })
50
- );
51
- }
52
-
53
- return data;
54
- }
55
- }
@@ -1,73 +0,0 @@
1
- import Visitor from "./Visitor.js";
2
- import Log from "@stnd/log";
3
-
4
- const log = Log({ scope: "Root" });
5
-
6
- export interface RootServices {
7
- db: any;
8
- bucket?: any;
9
- ai?: any;
10
- vectorize?: any;
11
- env?: any;
12
- ctx?: any;
13
- }
14
-
15
- // Rename to StandardRoot to indicate it's the base
16
- export class StandardRoot {
17
- public db: any;
18
- public env: any;
19
- public ctx?: any;
20
- public url: URL;
21
-
22
- // Visitor is universal (everyone needs auth)
23
- public visitor!: Visitor;
24
-
25
- constructor(services: RootServices, url: URL) {
26
- this.db = services.db;
27
- this.env = services.env;
28
- this.ctx = services.ctx;
29
- this.url = url;
30
- }
31
-
32
- // The Factory
33
- static async enter(context: any): Promise<StandardRoot> {
34
- const { locals, request } = context;
35
- let env: any;
36
- let ctx: any;
37
-
38
- if ("cfContext" in locals) {
39
- ctx = (locals as any).cfContext;
40
- }
41
- if ("env" in locals) {
42
- env = (locals as any).env;
43
- }
44
-
45
- // Fallback logic
46
- if (!env) {
47
- try {
48
- // @ts-ignore
49
- const cf = await import("cloudflare:workers");
50
- env = cf.env;
51
- } catch (e) {
52
- // Env might be missing in some contexts
53
- }
54
- }
55
-
56
- const services = { db: env?.DB, env, ctx };
57
- const root = new this(services, new URL(request.url));
58
-
59
- // Initialize Visitor (Universal)
60
- root.visitor = await Visitor.fromUrl(root, context);
61
-
62
- return root;
63
- }
64
-
65
- // Helper for background tasks
66
- public waitUntil(promise: Promise<any>): void {
67
- if (this.ctx && typeof this.ctx.waitUntil === "function") {
68
- this.ctx.waitUntil(promise);
69
- } else {
70
- promise.catch(err => console.error(err));
71
- }
72
- }
73
- }