@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/src/virtual.js
ADDED
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
export function sanitizeConfig(config) {
|
|
2
|
+
const seen = new WeakSet();
|
|
3
|
+
return JSON.parse(
|
|
4
|
+
JSON.stringify(config, (key, value) => {
|
|
5
|
+
if (typeof value === "object" && value !== null) {
|
|
6
|
+
if (seen.has(value)) {
|
|
7
|
+
return;
|
|
8
|
+
}
|
|
9
|
+
seen.add(value);
|
|
10
|
+
}
|
|
11
|
+
if (typeof value === "function") {
|
|
12
|
+
return undefined;
|
|
13
|
+
}
|
|
14
|
+
return value;
|
|
15
|
+
}),
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Generate a virtual module that wires the Standard OS runtime store.
|
|
21
|
+
*
|
|
22
|
+
* Module manifests declare `store:` entries as key → import path mappings:
|
|
23
|
+
*
|
|
24
|
+
* store: {
|
|
25
|
+
* user: "./models/Visitor.js#visitorStore",
|
|
26
|
+
* search: "./search.js#searchNotes",
|
|
27
|
+
* locale: "./i18n.js", // default export
|
|
28
|
+
* }
|
|
29
|
+
*
|
|
30
|
+
* The loader resolves the file paths and parses the `#exportName` suffix.
|
|
31
|
+
* This generator produces a module that imports each value and calls
|
|
32
|
+
* `provide(key, value)` from the Standard OS runtime store.
|
|
33
|
+
*
|
|
34
|
+
* @param {{ key: string, path: string, export: string|null }[]} storeEntries
|
|
35
|
+
* @returns {string} module source
|
|
36
|
+
*/
|
|
37
|
+
export function generateStoreModule(storeEntries = []) {
|
|
38
|
+
if (!storeEntries || storeEntries.length === 0) {
|
|
39
|
+
return "// No store entries registered.\n";
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
let importStatements = 'import { provide } from "@stnd/store";\n';
|
|
43
|
+
let provideStatements = "";
|
|
44
|
+
|
|
45
|
+
storeEntries.forEach((entry, i) => {
|
|
46
|
+
const alias = `__store_${i}`;
|
|
47
|
+
if (entry.export) {
|
|
48
|
+
// Named export: import { visitorStore as __store_0 } from "...";
|
|
49
|
+
importStatements += `import { ${entry.export} as ${alias} } from ${JSON.stringify(entry.path)};\n`;
|
|
50
|
+
} else {
|
|
51
|
+
// Default export: import __store_0 from "...";
|
|
52
|
+
importStatements += `import ${alias} from ${JSON.stringify(entry.path)};\n`;
|
|
53
|
+
}
|
|
54
|
+
provideStatements += `provide(${JSON.stringify(entry.key)}, ${alias});\n`;
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
return importStatements + "\n" + provideStatements;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function generateConfigModule(finalConfig) {
|
|
61
|
+
const cleanConfig = sanitizeConfig(finalConfig);
|
|
62
|
+
return `export default ${JSON.stringify(cleanConfig)}`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function generateModulesModule(runtimeModules) {
|
|
66
|
+
// Build a sanitized client payload with functions removed (safe for JSON serialization)
|
|
67
|
+
const sanitizedModules = runtimeModules.map((m) => {
|
|
68
|
+
const copy = JSON.parse(JSON.stringify(m));
|
|
69
|
+
// Remove internal import path from the public payload
|
|
70
|
+
delete copy.__importPath;
|
|
71
|
+
return copy;
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// Generate import statements for the actual module modules so their functions
|
|
75
|
+
// are available at runtime (useful for SSR route handlers, etc.)
|
|
76
|
+
let importStatements = "";
|
|
77
|
+
const moduleNames = [];
|
|
78
|
+
runtimeModules.forEach((m, i) => {
|
|
79
|
+
if (m.__importPath) {
|
|
80
|
+
const name = `__module_${i}`;
|
|
81
|
+
importStatements += `import ${name} from ${JSON.stringify(m.__importPath)};\n`;
|
|
82
|
+
moduleNames.push(name);
|
|
83
|
+
} else {
|
|
84
|
+
moduleNames.push("undefined");
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// Compose and return the virtual module source:
|
|
89
|
+
// - Import module modules (preserves functions)
|
|
90
|
+
// - Export `modules` (sanitized metadata) for client-side usage
|
|
91
|
+
// - Export `instances` as the actual imported modules for server/runtime use
|
|
92
|
+
return (
|
|
93
|
+
importStatements +
|
|
94
|
+
`\nexport const modules = ${JSON.stringify(sanitizedModules)};\n` +
|
|
95
|
+
`export const instances = [${moduleNames.join(", ")}];`
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function generateModulesMetadataModule(runtimeModules) {
|
|
100
|
+
// Build a sanitized client payload with functions removed (safe for JSON serialization)
|
|
101
|
+
const sanitizedModules = runtimeModules.map((m) => {
|
|
102
|
+
const copy = JSON.parse(JSON.stringify(m));
|
|
103
|
+
// Remove internal import path from the public payload
|
|
104
|
+
delete copy.__importPath;
|
|
105
|
+
return copy;
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
return `export const modules = ${JSON.stringify(sanitizedModules)};`;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Generate a virtual module that statically imports all module styles.
|
|
113
|
+
* This guarantees styles are included at build time (works for both
|
|
114
|
+
* `output: "server"` and `output: "static"`) and avoids relying on
|
|
115
|
+
* injectScript runtime quirks.
|
|
116
|
+
*
|
|
117
|
+
* @param {string[]} styleExtensions
|
|
118
|
+
* @returns {string} module source
|
|
119
|
+
*/
|
|
120
|
+
export function generateStylesModule(styleExtensions = []) {
|
|
121
|
+
const seen = new Set();
|
|
122
|
+
const imports = [];
|
|
123
|
+
for (const s of styleExtensions || []) {
|
|
124
|
+
if (!s) continue;
|
|
125
|
+
// Skip external URLs — handled separately via config._externalScripts/Base.astro
|
|
126
|
+
if (/^https?:\/\//.test(s) || /^\/\//.test(s)) continue;
|
|
127
|
+
if (seen.has(s)) continue;
|
|
128
|
+
seen.add(s);
|
|
129
|
+
imports.push(`import ${JSON.stringify(s)};`);
|
|
130
|
+
}
|
|
131
|
+
return imports.join("\n") + (imports.length ? "\n" : "");
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Generate a unified virtual module for all module scripts.
|
|
136
|
+
*
|
|
137
|
+
* Manifests declare scripts as a mixed array of strings and descriptor objects:
|
|
138
|
+
*
|
|
139
|
+
* scripts: [
|
|
140
|
+
* "boot.js", // string → fire-and-forget, runs once
|
|
141
|
+
* "https://cdn.../lib.min.js", // string → external CDN, skipped here
|
|
142
|
+
* { src: "@stnd/client/copy-buttons", // object → lifecycle-managed
|
|
143
|
+
* init: "initCopyButtons", // called on first load
|
|
144
|
+
* refresh: "refreshCopyButtons" }, // called on astro:after-swap
|
|
145
|
+
* { src: "@stnd/client/image-zoom",
|
|
146
|
+
* init: "initImageZoom" }, // no refresh → init reruns on swap too
|
|
147
|
+
* ]
|
|
148
|
+
*
|
|
149
|
+
* Fire-and-forget strings are imported statically (side-effect only, once).
|
|
150
|
+
* Object entries with `init` are registered in a lifecycle runner that calls
|
|
151
|
+
* init on load and refresh (or init again) on every astro:after-swap.
|
|
152
|
+
*
|
|
153
|
+
* External CDN URLs in string form are excluded — handled via
|
|
154
|
+
* config._externalScripts and rendered as <script src="..."> by Base.astro.
|
|
155
|
+
*
|
|
156
|
+
* @param {(string | { src: string, init?: string, refresh?: string })[]} scriptEntries
|
|
157
|
+
* @returns {string} module source
|
|
158
|
+
*/
|
|
159
|
+
export function generateScriptsModule(scriptEntries = []) {
|
|
160
|
+
const seenPaths = new Set();
|
|
161
|
+
const fireAndForget = [];
|
|
162
|
+
const lifecycle = [];
|
|
163
|
+
|
|
164
|
+
for (const entry of scriptEntries || []) {
|
|
165
|
+
if (!entry) continue;
|
|
166
|
+
|
|
167
|
+
if (typeof entry === "string") {
|
|
168
|
+
if (/^https?:\/\//.test(entry) || /^\/\//.test(entry)) continue;
|
|
169
|
+
if (seenPaths.has(entry)) continue;
|
|
170
|
+
seenPaths.add(entry);
|
|
171
|
+
fireAndForget.push(entry);
|
|
172
|
+
} else if (typeof entry === "object" && entry.src) {
|
|
173
|
+
if (seenPaths.has(entry.src)) continue;
|
|
174
|
+
seenPaths.add(entry.src);
|
|
175
|
+
lifecycle.push(entry);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (fireAndForget.length === 0 && lifecycle.length === 0) {
|
|
180
|
+
return "// No scripts registered.\n";
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
let out = "";
|
|
184
|
+
|
|
185
|
+
// Fire-and-forget: static imports, side-effect only, run once
|
|
186
|
+
for (const p of fireAndForget) {
|
|
187
|
+
out += `import ${JSON.stringify(p)};\n`;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (lifecycle.length === 0) return out;
|
|
191
|
+
|
|
192
|
+
// Lifecycle-managed: import as namespace, register in runner
|
|
193
|
+
out += "\n";
|
|
194
|
+
const registrations = [];
|
|
195
|
+
lifecycle.forEach((entry, i) => {
|
|
196
|
+
const alias = `__script_${i}`;
|
|
197
|
+
out += `import * as ${alias} from ${JSON.stringify(entry.src)};\n`;
|
|
198
|
+
registrations.push(
|
|
199
|
+
`{ src: ${JSON.stringify(entry.src)}, mod: ${alias}` +
|
|
200
|
+
(entry.init ? `, init: ${JSON.stringify(entry.init)}` : "") +
|
|
201
|
+
(entry.refresh ? `, refresh: ${JSON.stringify(entry.refresh)}` : "") +
|
|
202
|
+
` }`,
|
|
203
|
+
);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
out += `
|
|
207
|
+
var __scripts = [${registrations.join(", ")}];
|
|
208
|
+
|
|
209
|
+
function __runScripts(isSwap) {
|
|
210
|
+
__scripts.forEach(function(entry) {
|
|
211
|
+
try {
|
|
212
|
+
var fn = isSwap
|
|
213
|
+
? (entry.refresh ? entry.mod[entry.refresh] : (entry.init ? entry.mod[entry.init] : null))
|
|
214
|
+
: (entry.init ? entry.mod[entry.init] : null);
|
|
215
|
+
if (typeof fn === "function") fn();
|
|
216
|
+
} catch (err) {
|
|
217
|
+
console.error("[stnd] script init failed: " + entry.src, err);
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (typeof window !== "undefined") {
|
|
223
|
+
__runScripts(false);
|
|
224
|
+
document.addEventListener("astro:after-swap", function() { __runScripts(true); });
|
|
225
|
+
}
|
|
226
|
+
`;
|
|
227
|
+
|
|
228
|
+
return out;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export function generateComponentsModule(componentExtensions) {
|
|
232
|
+
let importStatements = "";
|
|
233
|
+
let body = "export const extensions = {};\n";
|
|
234
|
+
let counter = 0;
|
|
235
|
+
Object.entries(componentExtensions || {}).forEach(([zone, entries]) => {
|
|
236
|
+
entries.forEach((entry) => {
|
|
237
|
+
const hasComponent = entry.component && entry.component.trim().length > 0;
|
|
238
|
+
const hasAction = entry.action && entry.action.trim().length > 0;
|
|
239
|
+
|
|
240
|
+
const componentName = hasComponent
|
|
241
|
+
? `__ui_component_${counter}`
|
|
242
|
+
: "undefined";
|
|
243
|
+
const actionName = hasAction ? `__ui_action_${counter}` : "undefined";
|
|
244
|
+
|
|
245
|
+
if (hasComponent) {
|
|
246
|
+
importStatements += `import ${componentName} from ${JSON.stringify(entry.component)};\n`;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (hasAction) {
|
|
250
|
+
importStatements += `import ${actionName} from ${JSON.stringify(entry.action)};\n`;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
body += `extensions[${JSON.stringify(zone)}] = extensions[${JSON.stringify(zone)}] || [];\n`;
|
|
254
|
+
body += `extensions[${JSON.stringify(zone)}].push({ moduleId: ${JSON.stringify(entry.moduleId)}, trigger: ${entry.trigger ? JSON.stringify(entry.trigger) : "undefined"}, meta: ${JSON.stringify(entry.meta || {})}, component: ${componentName}, action: ${actionName} });\n`;
|
|
255
|
+
|
|
256
|
+
counter += 1;
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
return importStatements + "\n" + body;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export function generateMiddlewareModule(middlewareExtensions) {
|
|
263
|
+
let importStatements =
|
|
264
|
+
'import { sequence, defineMiddleware } from "astro/middleware";\n';
|
|
265
|
+
let middlewareItems = [];
|
|
266
|
+
const sorted = [...(middlewareExtensions || [])].sort(
|
|
267
|
+
(a, b) => a.order - b.order,
|
|
268
|
+
);
|
|
269
|
+
sorted.forEach((entry, index) => {
|
|
270
|
+
const importName = `__mw_${index}`;
|
|
271
|
+
importStatements += `import * as ${importName} from ${JSON.stringify(entry.entrypoint)};\n`;
|
|
272
|
+
const handler = `${importName}.onRequest || ${importName}.default || ${importName}`;
|
|
273
|
+
middlewareItems.push(`defineMiddleware(async (context, next) => {
|
|
274
|
+
const h = ${handler};
|
|
275
|
+
return typeof h === "function" ? h(context, next) : next();
|
|
276
|
+
})`);
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
return (
|
|
280
|
+
importStatements +
|
|
281
|
+
`\nexport const onRequest = sequence(\n ${middlewareItems.join(",\n ")}\n);\n`
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export function generateActionsModule(actionsExtensions) {
|
|
286
|
+
let importStatements = "";
|
|
287
|
+
let body = "export const actions = {};\n";
|
|
288
|
+
// Aggregate all actions from modules
|
|
289
|
+
(actionsExtensions || []).forEach((entry, index) => {
|
|
290
|
+
if (entry.entrypoint && entry.entrypoint.trim().length > 0) {
|
|
291
|
+
const importName = `__act_${index}`;
|
|
292
|
+
importStatements += `import * as ${importName} from ${JSON.stringify(entry.entrypoint)};\n`;
|
|
293
|
+
body += `Object.assign(actions, ${importName}.actions || ${importName}.default || ${importName});\n`;
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
return importStatements + "\n" + body;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
export function generateHooksModule(customHooks = [], options = { ssr: true }) {
|
|
300
|
+
// Runtime Hooks Generator
|
|
301
|
+
// Filter hooks based on environment (SSR vs Client)
|
|
302
|
+
// If we are NOT in SSR, skip hooks marked as server-only
|
|
303
|
+
const filteredHooks = customHooks.filter((ext) => {
|
|
304
|
+
if (!options.ssr && ext.server) return false;
|
|
305
|
+
return true;
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
// Group custom hooks by hook name
|
|
309
|
+
const hooksMap = {};
|
|
310
|
+
filteredHooks.forEach((ext) => {
|
|
311
|
+
hooksMap[ext.hook] = hooksMap[ext.hook] || [];
|
|
312
|
+
hooksMap[ext.hook].push(ext);
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
let importStatements = "";
|
|
316
|
+
let body = "const hooks = {};\n";
|
|
317
|
+
|
|
318
|
+
let hookIndex = 0;
|
|
319
|
+
Object.entries(hooksMap).forEach(([hookName, listeners]) => {
|
|
320
|
+
body += `hooks[${JSON.stringify(hookName)}] = [];\n`;
|
|
321
|
+
listeners.forEach((listener) => {
|
|
322
|
+
if (listener.entrypoint && listener.entrypoint.trim().length > 0) {
|
|
323
|
+
const importName = `__hook_handler_${hookIndex++}`;
|
|
324
|
+
importStatements += `import ${importName} from ${JSON.stringify(listener.entrypoint)};\n`;
|
|
325
|
+
body += `hooks[${JSON.stringify(hookName)}].push({ moduleId: ${JSON.stringify(listener.moduleId)}, mod: ${importName} });\n`;
|
|
326
|
+
}
|
|
327
|
+
});
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
// Both runners isolate handler failures: one module's throwing handler
|
|
331
|
+
// must not blank out every other module's contribution to the same
|
|
332
|
+
// hook name, and (for runPipeline specifically) must not 500 a page
|
|
333
|
+
// that handler wasn't even trying to act on. A failure is logged and
|
|
334
|
+
// treated as "this handler contributed nothing" — the fan-out/pipeline
|
|
335
|
+
// continues with the remaining handlers.
|
|
336
|
+
const runnerHelper = `
|
|
337
|
+
export async function runHook(name, ...args) {
|
|
338
|
+
const entries = hooks[name] || [];
|
|
339
|
+
const results = [];
|
|
340
|
+
for (const entry of entries) {
|
|
341
|
+
const fn = typeof entry.mod === 'function' ? entry.mod : entry.mod?.default;
|
|
342
|
+
if (typeof fn !== 'function') continue;
|
|
343
|
+
try {
|
|
344
|
+
results.push(await fn(...args));
|
|
345
|
+
} catch (err) {
|
|
346
|
+
console.error('[stnd] hook "' + name + '" handler from module "' + entry.moduleId + '" threw:', err);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
return results;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Sequential transform: threads a single value through every handler
|
|
353
|
+
// registered for \`name\`, in order. Each handler receives the current value
|
|
354
|
+
// (plus any extra args) and must return the next value — there is no
|
|
355
|
+
// implicit "return nothing to pass through unchanged"; a handler that
|
|
356
|
+
// wants to leave the value untouched returns it explicitly. This is
|
|
357
|
+
// deliberate: a handler that forgets to return should fail loudly
|
|
358
|
+
// (the next handler/consumer chokes on undefined) rather than silently
|
|
359
|
+
// becoming a no-op. Unlike runHook (fan-out + collect), this is for "let
|
|
360
|
+
// each interested module inspect and optionally transform the same
|
|
361
|
+
// thing," e.g. a note-render descriptor that a gate/paywall module can
|
|
362
|
+
// override without the caller knowing which module (if any) acted.
|
|
363
|
+
export async function runPipeline(name, initialValue, ...args) {
|
|
364
|
+
const entries = hooks[name] || [];
|
|
365
|
+
let value = initialValue;
|
|
366
|
+
for (const entry of entries) {
|
|
367
|
+
const fn = typeof entry.mod === 'function' ? entry.mod : entry.mod?.default;
|
|
368
|
+
if (typeof fn !== 'function') continue;
|
|
369
|
+
try {
|
|
370
|
+
value = await fn(value, ...args);
|
|
371
|
+
} catch (err) {
|
|
372
|
+
console.error('[stnd] pipeline "' + name + '" handler from module "' + entry.moduleId + '" threw — value unchanged:', err);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
return value;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
export default hooks;
|
|
379
|
+
`;
|
|
380
|
+
return importStatements + "\n" + body + "\n" + runnerHelper;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
export function generateContentModule(contentExtensions) {
|
|
384
|
+
let importStatements = "";
|
|
385
|
+
let body = "export const collections = {};\n";
|
|
386
|
+
|
|
387
|
+
(contentExtensions || []).forEach((entry, index) => {
|
|
388
|
+
const importName = `__content_${index}`;
|
|
389
|
+
// pathToFileURL is not needed here as JSON.stringify(path) works for import if it's absolute
|
|
390
|
+
// but 'entrypoint' is absolute path.
|
|
391
|
+
// On windows it needs file://, on mac/linux absolute path needs special handling in import?
|
|
392
|
+
// Vite handles absolute paths if they are in the allowed scope.
|
|
393
|
+
// Better use JSON.stringify(entry.entrypoint).
|
|
394
|
+
|
|
395
|
+
importStatements += `import * as ${importName} from ${JSON.stringify(entry.entrypoint)};\n`;
|
|
396
|
+
|
|
397
|
+
if (entry.type === "inline") {
|
|
398
|
+
// Inline: content is defined in module.default.content
|
|
399
|
+
body += `
|
|
400
|
+
if (${importName}.default && ${importName}.default.content && typeof ${importName}.default.content === 'object') {
|
|
401
|
+
Object.assign(collections, ${importName}.default.content);
|
|
402
|
+
}
|
|
403
|
+
`;
|
|
404
|
+
} else {
|
|
405
|
+
// External: file exports collections directly or as named exports
|
|
406
|
+
body += `
|
|
407
|
+
// Merge named exports as collections. Exclude default unless it's explicitly 'collections'
|
|
408
|
+
Object.keys(${importName}).forEach(key => {
|
|
409
|
+
if (key !== 'default') {
|
|
410
|
+
collections[key] = ${importName}[key];
|
|
411
|
+
}
|
|
412
|
+
});
|
|
413
|
+
// If default export exists and looks like a collection map (and is not the module def itself, which it shouldn't be for external content file)
|
|
414
|
+
// We can optionally support export default { home: ... }
|
|
415
|
+
if (${importName}.default && typeof ${importName}.default === 'object') {
|
|
416
|
+
// Verify it's not a module manifest by checking for 'id' + 'name'?
|
|
417
|
+
// Or just assume content file is pure content.
|
|
418
|
+
Object.assign(collections, ${importName}.default);
|
|
419
|
+
}
|
|
420
|
+
`;
|
|
421
|
+
}
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
return importStatements + "\n" + body;
|
|
425
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { generateStylesModule } from "./virtual.js";
|
|
3
|
+
|
|
4
|
+
describe("generateStylesModule", () => {
|
|
5
|
+
it("returns an empty string when no styles provided", () => {
|
|
6
|
+
expect(generateStylesModule([])).toBe("");
|
|
7
|
+
expect(generateStylesModule()).toBe("");
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it("generates import statements, dedupes and preserves order", () => {
|
|
11
|
+
const styles = [
|
|
12
|
+
"/abs/path/a.scss",
|
|
13
|
+
"./local/b.scss",
|
|
14
|
+
"/abs/path/a.scss", // duplicate
|
|
15
|
+
"@pkg/styles.css",
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
const src = generateStylesModule(styles);
|
|
19
|
+
|
|
20
|
+
// Contains expected import lines
|
|
21
|
+
expect(src).toContain('import "/abs/path/a.scss";');
|
|
22
|
+
expect(src).toContain('import "./local/b.scss";');
|
|
23
|
+
expect(src).toContain('import "@pkg/styles.css";');
|
|
24
|
+
|
|
25
|
+
// Deduplication: only 3 unique imports
|
|
26
|
+
const importCount = (src.match(/^import\s/gm) || []).length;
|
|
27
|
+
expect(importCount).toBe(3);
|
|
28
|
+
|
|
29
|
+
// Order preserved as first-appearance order
|
|
30
|
+
const idxA = src.indexOf('"/abs/path/a.scss"');
|
|
31
|
+
const idxB = src.indexOf('"./local/b.scss"');
|
|
32
|
+
const idxC = src.indexOf('"@pkg/styles.css"');
|
|
33
|
+
|
|
34
|
+
expect(idxA).toBeLessThan(idxB);
|
|
35
|
+
expect(idxB).toBeLessThan(idxC);
|
|
36
|
+
});
|
|
37
|
+
});
|