@stacksjs/stx 0.2.146 → 0.2.148
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/dist/cli.js +617 -560
- package/dist/client-script.d.ts +5 -0
- package/dist/client-script.js +49 -5
- package/dist/color-mode-boot.d.ts +88 -0
- package/dist/color-mode-boot.js +35 -0
- package/dist/composables/use-color-mode.js +5 -5
- package/dist/composables/use-storage.js +6 -1
- package/dist/craft.js +330 -273
- package/dist/dev-server/crosswind.js +1 -1
- package/dist/document-shell.d.ts +8 -0
- package/dist/document-shell.js +2 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/process.js +7 -1
- package/dist/production-builder.js +6 -2
- package/dist/pwa.js +322 -265
- package/dist/script-classifier.d.ts +14 -8
- package/dist/script-classifier.js +33 -14
- package/dist/signals.js +66 -15
- package/dist/ssg.js +299 -242
- package/dist/stx.d.ts +32 -4
- package/dist/types/config-types.d.ts +2 -0
- package/package.json +6 -6
- package/stx.d.ts +32 -4
package/dist/client-script.d.ts
CHANGED
|
@@ -43,6 +43,11 @@ export declare function renderVendorStyleTags(styles: VendorCssImport[]): string
|
|
|
43
43
|
* helpers together with the component, exactly like an explicit user import.
|
|
44
44
|
*/
|
|
45
45
|
export declare function injectBrowserCoreAutoImports(code: string): BrowserCoreAutoImportResult;
|
|
46
|
+
/**
|
|
47
|
+
* Blank out comments and string literals, preserving length and line structure
|
|
48
|
+
* so offsets still line up, so that identifier detection only ever sees code.
|
|
49
|
+
*/
|
|
50
|
+
export declare function stripCommentsAndLiterals(code: string): string;
|
|
46
51
|
/**
|
|
47
52
|
* Generate destructuring statements for auto-imported symbols
|
|
48
53
|
* These access globals set up by the STX runtime and @stacksjs/browser
|
package/dist/client-script.js
CHANGED
|
@@ -250,7 +250,7 @@ const STX_AUTO_IMPORTS = [
|
|
|
250
250
|
"useIntervalFn"
|
|
251
251
|
];
|
|
252
252
|
export function injectBrowserCoreAutoImports(code) {
|
|
253
|
-
const explicitlyImported = new Set;
|
|
253
|
+
const searchable = stripCommentsAndLiterals(code), explicitlyImported = new Set;
|
|
254
254
|
for (const match of code.matchAll(/^\s*import\s+(?:type\s+)?\{([^}]*)\}\s+from\s+['"][^'"]+['"]\s*;?\s*$/gm))
|
|
255
255
|
for (const specifier of match[1].split(",")) {
|
|
256
256
|
const localName = specifier.trim().split(/\s+as\s+/).pop()?.replace(/^type\s+/, "").trim();
|
|
@@ -258,9 +258,9 @@ export function injectBrowserCoreAutoImports(code) {
|
|
|
258
258
|
explicitlyImported.add(localName);
|
|
259
259
|
}
|
|
260
260
|
const locallyDeclared = new Set;
|
|
261
|
-
for (const match of
|
|
261
|
+
for (const match of searchable.matchAll(/\b(?:const|let|var|function|class)\s+([A-Za-z_$][\w$]*)/g))
|
|
262
262
|
locallyDeclared.add(match[1]);
|
|
263
|
-
for (const match of
|
|
263
|
+
for (const match of searchable.matchAll(/\b(?:const|let|var)\s*\{([^}]*)\}/g))
|
|
264
264
|
for (const binding of match[1].split(",")) {
|
|
265
265
|
const localName = binding.trim().split(/[:=]/).pop()?.trim();
|
|
266
266
|
if (localName)
|
|
@@ -269,8 +269,8 @@ export function injectBrowserCoreAutoImports(code) {
|
|
|
269
269
|
const imports = BROWSER_CORE_IMPORTS.filter((symbol) => {
|
|
270
270
|
if (explicitlyImported.has(symbol) || locallyDeclared.has(symbol))
|
|
271
271
|
return !1;
|
|
272
|
-
return new RegExp(`\\b${symbol}\\
|
|
273
|
-
}), models = detectModelUsage(
|
|
272
|
+
return new RegExp(`\\b${symbol}\\s*\\(`).test(searchable);
|
|
273
|
+
}), models = detectModelUsage(searchable).filter((model) => !explicitlyImported.has(model) && !locallyDeclared.has(model));
|
|
274
274
|
if (imports.length === 0 && models.length === 0)
|
|
275
275
|
return { code, imports, models };
|
|
276
276
|
return {
|
|
@@ -342,6 +342,50 @@ const JS_BUILTINS = new Set([
|
|
|
342
342
|
"Modal",
|
|
343
343
|
"Intl"
|
|
344
344
|
]);
|
|
345
|
+
export function stripCommentsAndLiterals(code) {
|
|
346
|
+
let out = "", i = 0;
|
|
347
|
+
while (i < code.length) {
|
|
348
|
+
const two = code.slice(i, i + 2);
|
|
349
|
+
if (two === "//") {
|
|
350
|
+
while (i < code.length && code[i] !== `
|
|
351
|
+
`)
|
|
352
|
+
out += " ", i++;
|
|
353
|
+
continue;
|
|
354
|
+
}
|
|
355
|
+
if (two === "/*") {
|
|
356
|
+
while (i < code.length && code.slice(i, i + 2) !== "*/")
|
|
357
|
+
out += code[i] === `
|
|
358
|
+
` ? `
|
|
359
|
+
` : " ", i++;
|
|
360
|
+
out += " ";
|
|
361
|
+
i += 2;
|
|
362
|
+
continue;
|
|
363
|
+
}
|
|
364
|
+
const ch = code[i];
|
|
365
|
+
if (ch === '"' || ch === "'" || ch === "`") {
|
|
366
|
+
const quote = ch;
|
|
367
|
+
out += " ";
|
|
368
|
+
i++;
|
|
369
|
+
while (i < code.length && code[i] !== quote) {
|
|
370
|
+
if (code[i] === "\\") {
|
|
371
|
+
out += " ";
|
|
372
|
+
i += 2;
|
|
373
|
+
continue;
|
|
374
|
+
}
|
|
375
|
+
out += code[i] === `
|
|
376
|
+
` ? `
|
|
377
|
+
` : " ";
|
|
378
|
+
i++;
|
|
379
|
+
}
|
|
380
|
+
out += " ";
|
|
381
|
+
i++;
|
|
382
|
+
continue;
|
|
383
|
+
}
|
|
384
|
+
out += ch;
|
|
385
|
+
i++;
|
|
386
|
+
}
|
|
387
|
+
return out;
|
|
388
|
+
}
|
|
345
389
|
function detectModelUsage(code) {
|
|
346
390
|
const models = new Set, modelPattern = /\b([A-Z][a-zA-Z0-9]*)\s*\.\s*(all|find|first|get|where|orderBy|orderByDesc|limit|select|pluck|create|update|delete)\s*\(/g;
|
|
347
391
|
let match;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build the synchronous pre-paint `<script>`.
|
|
3
|
+
*
|
|
4
|
+
* Every storage access is wrapped: a sandboxed origin or disabled storage must
|
|
5
|
+
* degrade to the default mode, never throw. A throw here is unusually costly
|
|
6
|
+
* because the script sits above the application shell — an uncaught error would
|
|
7
|
+
* block the rest of the document.
|
|
8
|
+
*/
|
|
9
|
+
export declare function generateColorModeBootScript(options?: ColorModeBootConfig): string;
|
|
10
|
+
/**
|
|
11
|
+
* Insert the boot script as the first thing in `<head>`.
|
|
12
|
+
*
|
|
13
|
+
* Placement matters twice over: the script must run before the browser paints,
|
|
14
|
+
* and a stylesheet `<link>` above it would block its execution (CSS blocks JS),
|
|
15
|
+
* delaying the very thing it exists to make early.
|
|
16
|
+
*
|
|
17
|
+
* `<meta charset>` is stepped over when it leads the head, so the encoding
|
|
18
|
+
* declaration keeps its place at the top of the document.
|
|
19
|
+
*
|
|
20
|
+
* Idempotent — a document that already carries the marker is returned
|
|
21
|
+
* unchanged, so this is safe on paths that run it more than once.
|
|
22
|
+
*/
|
|
23
|
+
export declare function injectColorModeBootScript(html: string, options?: ColorModeBootConfig): string;
|
|
24
|
+
/**
|
|
25
|
+
* Pre-paint color-mode bootstrap (stacksjs/stx#1794).
|
|
26
|
+
*
|
|
27
|
+
* `useColorMode` is part of the client signals runtime: it reads localStorage
|
|
28
|
+
* at call time, after the runtime bundle has loaded and hydration has begun.
|
|
29
|
+
* That is too late. Applying the theme post-hydration guarantees a flash of the
|
|
30
|
+
* wrong theme on every cold load for every user whose stored preference differs
|
|
31
|
+
* from the default — which is why apps hand-write a render-blocking snippet in
|
|
32
|
+
* `<head>` and why `useColorMode` could not replace it.
|
|
33
|
+
*
|
|
34
|
+
* This module emits that snippet from the *same* option surface `useColorMode`
|
|
35
|
+
* takes, so the duplication collapses into one declaration in `stx.config.ts`:
|
|
36
|
+
*
|
|
37
|
+
* ```ts
|
|
38
|
+
* // stx.config.ts
|
|
39
|
+
* export default {
|
|
40
|
+
* app: {
|
|
41
|
+
* colorMode: { storageKey: 'theme', attribute: 'data-theme' },
|
|
42
|
+
* },
|
|
43
|
+
* }
|
|
44
|
+
* ```
|
|
45
|
+
*
|
|
46
|
+
* The generated script is deliberately render-blocking, is injected as the
|
|
47
|
+
* first thing inside `<head>` (above any stylesheet), and touches the root
|
|
48
|
+
* element synchronously.
|
|
49
|
+
*
|
|
50
|
+
* ## Staying in lockstep with useColorMode
|
|
51
|
+
*
|
|
52
|
+
* The boot script must apply *exactly* what `useColorMode` will apply on
|
|
53
|
+
* hydration. If they disagree the flash comes back — the boot script would set
|
|
54
|
+
* one thing and hydration would immediately undo it, which is worse than not
|
|
55
|
+
* having a boot script at all. So this mirrors the runtime's `applyDOM` and
|
|
56
|
+
* `readPersisted` precisely, including two limitations that are NOT fixed here:
|
|
57
|
+
*
|
|
58
|
+
* - `attribute` and `darkClass` are either/or, never both (stacksjs/stx#1788)
|
|
59
|
+
* - the persisted vocabulary is exactly `light | dark | auto` — `'system'`
|
|
60
|
+
* is not accepted (also #1788)
|
|
61
|
+
*
|
|
62
|
+
* Mirroring the limitation is intentional. When #1788 lands, both sides change
|
|
63
|
+
* together; `test/directives/color-mode-boot.test.ts` pins them to the same
|
|
64
|
+
* expectations so drift fails the suite rather than shipping a flash.
|
|
65
|
+
*
|
|
66
|
+
* The script publishes its resolved configuration on `window.__STX_COLOR_MODE__`
|
|
67
|
+
* so a bare `useColorMode()` call picks up the app's configured storage key and
|
|
68
|
+
* attribute without the options having to be repeated at the call site.
|
|
69
|
+
*
|
|
70
|
+
* @module color-mode-boot
|
|
71
|
+
*/
|
|
72
|
+
/** Marker attribute — makes injection idempotent and findable by tests. */
|
|
73
|
+
export declare const COLOR_MODE_BOOT_MARKER: 'data-stx-color-mode-boot';
|
|
74
|
+
/** The global the boot script publishes its resolved config on. */
|
|
75
|
+
export declare const COLOR_MODE_BOOT_GLOBAL: '__STX_COLOR_MODE__';
|
|
76
|
+
/**
|
|
77
|
+
* Pre-paint options. A strict subset of `ColorModeOptions` — only the fields
|
|
78
|
+
* that affect what lands on the root element before paint. `disableTransitions`
|
|
79
|
+
* is deliberately absent: nothing has painted yet, so there is no transition to
|
|
80
|
+
* suppress, and forcing a reflow in the critical path would only cost time.
|
|
81
|
+
*/
|
|
82
|
+
export declare interface ColorModeBootConfig {
|
|
83
|
+
storageKey?: string
|
|
84
|
+
initialMode?: ColorModeBootMode
|
|
85
|
+
darkClass?: string
|
|
86
|
+
attribute?: string
|
|
87
|
+
}
|
|
88
|
+
export type ColorModeBootMode = 'light' | 'dark' | 'auto';
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export const COLOR_MODE_BOOT_MARKER = "data-stx-color-mode-boot", COLOR_MODE_BOOT_GLOBAL = "__STX_COLOR_MODE__";
|
|
2
|
+
const MODES = new Set(["light", "dark", "auto"]), ATTRIBUTE_PATTERN = /^[a-z][\w:-]*$/i, CLASS_PATTERN = /^[\w-]+$/;
|
|
3
|
+
function resolveConfig(options) {
|
|
4
|
+
const storageKey = options.storageKey ?? "stx-color-mode", initialMode = options.initialMode ?? "auto", darkClass = options.darkClass ?? "dark", attribute = options.attribute ?? null;
|
|
5
|
+
if (typeof storageKey !== "string" || storageKey.length === 0)
|
|
6
|
+
throw Error("colorMode.storageKey must be a non-empty string");
|
|
7
|
+
if (!MODES.has(initialMode))
|
|
8
|
+
throw Error(`colorMode.initialMode must be one of light, dark, auto \u2014 got ${JSON.stringify(initialMode)}`);
|
|
9
|
+
if (typeof darkClass !== "string" || !CLASS_PATTERN.test(darkClass))
|
|
10
|
+
throw Error(`colorMode.darkClass must be a single CSS class name \u2014 got ${JSON.stringify(darkClass)}`);
|
|
11
|
+
if (attribute !== null && (typeof attribute !== "string" || !ATTRIBUTE_PATTERN.test(attribute)))
|
|
12
|
+
throw Error(`colorMode.attribute must be a valid HTML attribute name \u2014 got ${JSON.stringify(attribute)}`);
|
|
13
|
+
return { storageKey, initialMode, darkClass, attribute };
|
|
14
|
+
}
|
|
15
|
+
function serializeForScript(value) {
|
|
16
|
+
return JSON.stringify(value).replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/&/g, "\\u0026").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
|
|
17
|
+
}
|
|
18
|
+
export function generateColorModeBootScript(options = {}) {
|
|
19
|
+
const config = resolveConfig(options), body = `(function(){"use strict";var c=${serializeForScript(config)};var p=c.initialMode;try{var v=localStorage.getItem(c.storageKey);if(v==="light"||v==="dark"||v==="auto")p=v}catch(e){}var m=p;if(p==="auto"){try{m=matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light"}catch(e){m="light"}}var r=document.documentElement;if(c.attribute){r.setAttribute(c.attribute,m)}else if(m==="dark"){r.classList.add(c.darkClass)}else{r.classList.remove(c.darkClass)}try{window.${COLOR_MODE_BOOT_GLOBAL}={storageKey:c.storageKey,initialMode:c.initialMode,darkClass:c.darkClass,attribute:c.attribute,preference:p,mode:m}}catch(e){}}());`;
|
|
20
|
+
return `<script ${COLOR_MODE_BOOT_MARKER}>${body}</script>`;
|
|
21
|
+
}
|
|
22
|
+
export function injectColorModeBootScript(html, options = {}) {
|
|
23
|
+
if (html.includes(COLOR_MODE_BOOT_MARKER))
|
|
24
|
+
return html;
|
|
25
|
+
const headOpen = /<head\b[^>]*>/i.exec(html);
|
|
26
|
+
if (!headOpen)
|
|
27
|
+
return html;
|
|
28
|
+
let insertAt = headOpen.index + headOpen[0].length;
|
|
29
|
+
const afterHead = html.slice(insertAt), charset = /^\s*<meta\b[^>]*\bcharset\b[^>]*>/i.exec(afterHead);
|
|
30
|
+
if (charset)
|
|
31
|
+
insertAt += charset[0].length;
|
|
32
|
+
const script = generateColorModeBootScript(options);
|
|
33
|
+
return `${html.slice(0, insertAt)}
|
|
34
|
+
${script}${html.slice(insertAt)}`;
|
|
35
|
+
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
export function useColorMode(options = {}) {
|
|
2
|
-
const {
|
|
3
|
-
storageKey = "stx-color-mode",
|
|
4
|
-
initialMode = "auto",
|
|
5
|
-
darkClass = "dark",
|
|
6
|
-
attribute,
|
|
2
|
+
const boot = typeof window < "u" ? window.__STX_COLOR_MODE__ : null, {
|
|
3
|
+
storageKey = boot?.storageKey ?? "stx-color-mode",
|
|
4
|
+
initialMode = boot?.initialMode ?? "auto",
|
|
5
|
+
darkClass = boot?.darkClass ?? "dark",
|
|
6
|
+
attribute = boot?.attribute,
|
|
7
7
|
disableTransitions = !0
|
|
8
8
|
} = options;
|
|
9
9
|
let preference = initialMode, resolved = "light";
|
|
@@ -61,7 +61,12 @@ export function useStorage(key, defaultValue, options = {}) {
|
|
|
61
61
|
const onStorage = (event) => {
|
|
62
62
|
if (event.key === key && event.storageArea === getStorage()) {
|
|
63
63
|
const prevValue = currentValue;
|
|
64
|
-
|
|
64
|
+
try {
|
|
65
|
+
currentValue = event.newValue ? serializer.read(event.newValue) : defaultValue;
|
|
66
|
+
} catch (e) {
|
|
67
|
+
console.warn(`[useStorage] Failed to read key "${key}" from a cross-tab update:`, e);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
65
70
|
notify(currentValue, prevValue);
|
|
66
71
|
}
|
|
67
72
|
};
|