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