@stacksjs/stx 0.2.112 → 0.2.114
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/app-shell.d.ts +1 -1
- package/dist/app-shell.js +7 -1
- package/dist/caching.js +2 -1
- package/dist/cli.js +1437 -956
- package/dist/client-script-bundler.js +33 -16
- package/dist/client-script.js +50 -5
- package/dist/component-renderer.js +56 -13
- package/dist/composable-loader.d.ts +15 -0
- package/dist/composable-loader.js +117 -0
- package/dist/desktop.d.ts +15 -0
- package/dist/desktop.js +1 -0
- package/dist/dev-server/crosswind.js +28 -13
- package/dist/dev-server/serve-app.js +1 -1
- package/dist/expressions.js +9 -2
- package/dist/includes.js +6 -4
- package/dist/menubar.d.ts +63 -0
- package/dist/menubar.js +103 -0
- package/dist/plugin.js +4 -3
- package/dist/process.js +83 -23
- package/dist/pwa.js +7359 -6878
- package/dist/render.js +4 -24
- package/dist/runtime-globals.d.ts +59 -0
- package/dist/runtime-globals.js +65 -0
- package/dist/runtime-injection.js +2 -2
- package/dist/serve.js +4 -21
- package/dist/sfc-template.d.ts +17 -0
- package/dist/sfc-template.js +32 -0
- package/dist/signal-processing.js +27 -12
- package/dist/signals.js +558 -128
- package/dist/slots.js +105 -20
- package/dist/ssg.js +7381 -6900
- package/dist/stx.d.ts +8 -0
- package/dist/testing.js +8 -1
- package/dist/types/config-types.d.ts +2 -0
- package/dist/utils.d.ts +9 -0
- package/dist/utils.js +66 -19
- package/dist/variable-extractor.js +3 -3
- package/package.json +6 -8
- package/stx.d.ts +8 -0
|
@@ -2,7 +2,7 @@ import path from "node:path";
|
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import { getPublicEnvDefine } from "./public-env.js";
|
|
4
4
|
import { stateDir } from "./state-dir.js";
|
|
5
|
-
const EXTERNAL_PATTERNS = [
|
|
5
|
+
const BUNDLE_CACHE_VERSION = 4, EXTERNAL_PATTERNS = [
|
|
6
6
|
/^stx$/,
|
|
7
7
|
/^@stacksjs\/stx$/,
|
|
8
8
|
/^@stores$/,
|
|
@@ -10,10 +10,7 @@ const EXTERNAL_PATTERNS = [
|
|
|
10
10
|
/^@composables$/
|
|
11
11
|
];
|
|
12
12
|
export function hasUserImports(code) {
|
|
13
|
-
const
|
|
14
|
-
if (!code.match(importRegex))
|
|
15
|
-
return !1;
|
|
16
|
-
const fullImportRegex = /^\s*import\s+(?!type\s).*?\s+from\s+['"]([^'"]+)['"]/gm;
|
|
13
|
+
const fullImportRegex = /^[ \t]*import[ \t]+(?!type(?:[ \t]|{))[\s\S]*?\s+from\s+['"]([^'"]+)['"][ \t]*;?/gm;
|
|
17
14
|
let match;
|
|
18
15
|
while ((match = fullImportRegex.exec(code)) !== null) {
|
|
19
16
|
const source = match[1];
|
|
@@ -22,7 +19,7 @@ export function hasUserImports(code) {
|
|
|
22
19
|
return !0;
|
|
23
20
|
}
|
|
24
21
|
}
|
|
25
|
-
const sideEffectRegex =
|
|
22
|
+
const sideEffectRegex = /^[ \t]*import[ \t]+['"]([^'"]+)['"]/gm;
|
|
26
23
|
while ((match = sideEffectRegex.exec(code)) !== null) {
|
|
27
24
|
const source = match[1];
|
|
28
25
|
if (/\.css(?:\?.*)?$/.test(source))
|
|
@@ -66,6 +63,14 @@ function createBundlePlugin(projectRoot, templateDir, tmpEntry, inputFiles) {
|
|
|
66
63
|
path: args.path,
|
|
67
64
|
external: !0
|
|
68
65
|
}));
|
|
66
|
+
build.onResolve({ filter: /^\// }, (args) => {
|
|
67
|
+
if (fs.existsSync(args.path))
|
|
68
|
+
return { path: args.path };
|
|
69
|
+
return {
|
|
70
|
+
path: args.path,
|
|
71
|
+
external: !0
|
|
72
|
+
};
|
|
73
|
+
});
|
|
69
74
|
build.onResolve({ filter: /^\.\.?\// }, (args) => {
|
|
70
75
|
const resolved = resolveRelative(args.importer, args.path);
|
|
71
76
|
inputFiles.add(resolved);
|
|
@@ -98,7 +103,7 @@ function createBundlePlugin(projectRoot, templateDir, tmpEntry, inputFiles) {
|
|
|
98
103
|
}
|
|
99
104
|
export async function bundleClientScript(code, filePath, options = {}) {
|
|
100
105
|
const projectRoot = options.projectRoot || process.cwd(), minify = options.minify ?? !1, cacheDir = options.cacheDir || stateDir(projectRoot, "bundle-cache"), hasher = new Bun.CryptoHasher("md5");
|
|
101
|
-
hasher.update(code
|
|
106
|
+
hasher.update(`${BUNDLE_CACHE_VERSION}\x00${code}\x00${filePath}`);
|
|
102
107
|
const hash = hasher.digest("hex").slice(0, 12), cachePath = path.join(cacheDir, `${hash}.js`), depsPath = path.join(cacheDir, `${hash}.deps.json`), cacheFile = Bun.file(cachePath);
|
|
103
108
|
if (await cacheFile.exists()) {
|
|
104
109
|
const depsFile = Bun.file(depsPath);
|
|
@@ -176,19 +181,31 @@ export { ${declNames.join(", ")} }` : "";
|
|
|
176
181
|
bundled = await output.text();
|
|
177
182
|
break;
|
|
178
183
|
}
|
|
179
|
-
const
|
|
184
|
+
const exposedBindings = [];
|
|
180
185
|
for (const exp of bundled.matchAll(/^\s*export\s*\{([^}]*)\}\s*;?\s*$/gm))
|
|
181
186
|
for (const spec of exp[1].split(",")) {
|
|
182
187
|
const asMatch = spec.trim().match(/^([A-Za-z_$][\w$]*)\s+as\s+([A-Za-z_$][\w$]*)$/);
|
|
183
|
-
if (asMatch
|
|
184
|
-
|
|
188
|
+
if (asMatch) {
|
|
189
|
+
exposedBindings.push([asMatch[2], asMatch[1]]);
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
const name = spec.trim();
|
|
193
|
+
if (/^[A-Za-z_$][\w$]*$/.test(name))
|
|
194
|
+
exposedBindings.push([name, name]);
|
|
185
195
|
}
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
196
|
+
const externalImports = bundled.match(/^\s*import\s+[^;]+;?\s*$/gm) || [];
|
|
197
|
+
bundled = bundled.replace(/^\s*import\s+[^;]+;?\s*$/gm, "").replace(/^\s*export\s*\{[^}]*\}\s*;?\s*$/gm, "").replace(/^\s*export\s+default\s+.*$/gm, "").trim();
|
|
198
|
+
const namespace = `__stxBundle_${hash}`, returnedBindings = exposedBindings.map(([publicName, localName]) => `${JSON.stringify(publicName)}: ${localName}`).join(", "), publicDeclarations = exposedBindings.map(([publicName]) => `var ${publicName} = undefined;`).join(`
|
|
199
|
+
`), publicAssignments = exposedBindings.map(([publicName]) => `${publicName} = ${namespace}[${JSON.stringify(publicName)}];`).join(`
|
|
200
|
+
`);
|
|
201
|
+
bundled = `${externalImports.join(`
|
|
202
|
+
`)}
|
|
203
|
+
${publicDeclarations}
|
|
204
|
+
var ${namespace} = (function() {
|
|
205
|
+
${bundled}
|
|
206
|
+
return { ${returnedBindings} };
|
|
207
|
+
})();
|
|
208
|
+
${publicAssignments}`.trim();
|
|
192
209
|
console.log("[stx:bundler] bundled:", hash, "output:", bundled.length, "bytes");
|
|
193
210
|
fs.mkdirSync(cacheDir, { recursive: !0 });
|
|
194
211
|
await Bun.write(cachePath, bundled);
|
package/dist/client-script.js
CHANGED
|
@@ -184,6 +184,11 @@ const STX_AUTO_IMPORTS = [
|
|
|
184
184
|
"useAsync",
|
|
185
185
|
"useColorMode",
|
|
186
186
|
"useDark",
|
|
187
|
+
"useMediaQuery",
|
|
188
|
+
"usePreferredDark",
|
|
189
|
+
"usePreferredLight",
|
|
190
|
+
"usePreferredReducedMotion",
|
|
191
|
+
"usePreferredContrast",
|
|
187
192
|
"ref",
|
|
188
193
|
"reactive",
|
|
189
194
|
"computed",
|
|
@@ -210,6 +215,7 @@ const STX_AUTO_IMPORTS = [
|
|
|
210
215
|
"useAttrs",
|
|
211
216
|
"createStore",
|
|
212
217
|
"defineStore",
|
|
218
|
+
"registerStoresClient",
|
|
213
219
|
"useStore",
|
|
214
220
|
"action",
|
|
215
221
|
"createSelector",
|
|
@@ -294,7 +300,7 @@ function transformAutoImports(code) {
|
|
|
294
300
|
const usedStxImports = new Set, usedBrowserImports = new Set;
|
|
295
301
|
let transformedCode = code;
|
|
296
302
|
transformedCode = transformedCode.replace(/^\s*import\s+type\s+\{[^}]*\}\s+from\s+['"][^'"]+['"]\s*;?\s*$/gm, "// [type import stripped]");
|
|
297
|
-
const existingImports = new Set, importRegex = /^\s*import\s+(?:type\s+)?{\s*([^}]+)\s*}\s+from\s+['"](@stacksjs\/browser|stx)['"]\s*;?\s*$/gm;
|
|
303
|
+
const existingImports = new Set, importRegex = /^\s*import\s+(?:type\s+)?{\s*([^}]+)\s*}\s+from\s+['"](@stacksjs\/browser|@stacksjs\/stx|stx)['"]\s*;?\s*$/gm;
|
|
298
304
|
let match;
|
|
299
305
|
while ((match = importRegex.exec(code)) !== null) {
|
|
300
306
|
const imports = match[1], source = match[2];
|
|
@@ -302,7 +308,7 @@ function transformAutoImports(code) {
|
|
|
302
308
|
continue;
|
|
303
309
|
imports.split(",").map((s) => s.trim().split(/\s+as\s+/)[0]).filter((s) => s.length > 0).forEach((name) => {
|
|
304
310
|
existingImports.add(name);
|
|
305
|
-
if (source === "stx")
|
|
311
|
+
if (source === "stx" || source === "@stacksjs/stx")
|
|
306
312
|
usedStxImports.add(name);
|
|
307
313
|
else
|
|
308
314
|
usedBrowserImports.add(name);
|
|
@@ -374,6 +380,34 @@ export function extractBridgeData(context) {
|
|
|
374
380
|
}
|
|
375
381
|
return out;
|
|
376
382
|
}
|
|
383
|
+
function extractDestructuredBindings(pattern, objectPattern) {
|
|
384
|
+
const bindings = new Set;
|
|
385
|
+
for (const rawPart of pattern.split(",")) {
|
|
386
|
+
let part = rawPart.trim().replace(/^\.\.\./, "").trim();
|
|
387
|
+
if (!part)
|
|
388
|
+
continue;
|
|
389
|
+
if (objectPattern) {
|
|
390
|
+
const colon = part.lastIndexOf(":");
|
|
391
|
+
if (colon !== -1)
|
|
392
|
+
part = part.slice(colon + 1).trim();
|
|
393
|
+
}
|
|
394
|
+
part = part.split("=")[0].trim();
|
|
395
|
+
if (/^[A-Za-z_$][\w$]*$/.test(part))
|
|
396
|
+
bindings.add(part);
|
|
397
|
+
}
|
|
398
|
+
return bindings;
|
|
399
|
+
}
|
|
400
|
+
function declaresClientIdentifier(code, name) {
|
|
401
|
+
if (new RegExp(`(?:const|let|var|function|class)\\s+${name}\\b`).test(code))
|
|
402
|
+
return !0;
|
|
403
|
+
for (const match of code.matchAll(/(?:const|let|var)\s*\{([^}]*)\}/g))
|
|
404
|
+
if (extractDestructuredBindings(match[1], !0).has(name))
|
|
405
|
+
return !0;
|
|
406
|
+
for (const match of code.matchAll(/(?:const|let|var)\s*\[([^\]]*)\]/g))
|
|
407
|
+
if (extractDestructuredBindings(match[1], !1).has(name))
|
|
408
|
+
return !0;
|
|
409
|
+
return !1;
|
|
410
|
+
}
|
|
377
411
|
export function generateServerDataBridge(code, serverData) {
|
|
378
412
|
if (!serverData)
|
|
379
413
|
return "";
|
|
@@ -383,7 +417,7 @@ export function generateServerDataBridge(code, serverData) {
|
|
|
383
417
|
continue;
|
|
384
418
|
if (!new RegExp(`\\b${name}\\b`).test(code))
|
|
385
419
|
continue;
|
|
386
|
-
if (
|
|
420
|
+
if (declaresClientIdentifier(code, name))
|
|
387
421
|
continue;
|
|
388
422
|
let json;
|
|
389
423
|
try {
|
|
@@ -439,8 +473,13 @@ function generateSingleEventBinding(binding, index) {
|
|
|
439
473
|
let handlerBody = "";
|
|
440
474
|
if (checks)
|
|
441
475
|
handlerBody += `${checks}; `;
|
|
442
|
-
const escapedHandler = handler.replace(/\\/g, "\\\\").replace(/'/g, "\\'")
|
|
476
|
+
const escapedHandler = handler.replace(/\\/g, "\\\\").replace(/'/g, "\\'"), emitHelper = `var $emit = function(name, detail) {
|
|
477
|
+
$el.dispatchEvent(new CustomEvent(name, { detail: detail, bubbles: true, cancelable: true }))
|
|
478
|
+
};`;
|
|
443
479
|
function scopeAwareHandler(h) {
|
|
480
|
+
if (/\$emit\s*\(/.test(h))
|
|
481
|
+
return `${emitHelper}
|
|
482
|
+
${h}`;
|
|
444
483
|
return `var scope = $el.closest('[data-stx-scope]');
|
|
445
484
|
if (scope && scope.__stx_execute) {
|
|
446
485
|
scope.__stx_execute('${escapedHandler}', $event, $el);
|
|
@@ -474,6 +513,12 @@ else {
|
|
|
474
513
|
}
|
|
475
514
|
}${optionsStr})
|
|
476
515
|
})()`;
|
|
516
|
+
if (/\$emit\s*\(/.test(handler))
|
|
517
|
+
return ` var $el = document.getElementById('${elementId}')
|
|
518
|
+
if ($el) $el.addEventListener('${event}', function($event) {
|
|
519
|
+
${handlerBody}${emitHelper}
|
|
520
|
+
${handler}
|
|
521
|
+
}${optionsStr})`;
|
|
477
522
|
return ` var $el = document.getElementById('${elementId}')
|
|
478
523
|
if ($el) $el.addEventListener('${event}', function($event) {
|
|
479
524
|
${handlerBody}var scope = $el.closest('[data-stx-scope]');
|
|
@@ -740,7 +785,7 @@ ${autoImportCode}${code}
|
|
|
740
785
|
${eventCode}
|
|
741
786
|
</script>`;
|
|
742
787
|
}
|
|
743
|
-
const usesSignals = /\b(state|derived|effect|ref|reactive|computed|watch|watchEffect|useLocalStorage|useSessionStorage|useRef|useEventListener|useFetch|useDebounce|useDebouncedValue|useThrottle|useInterval|useTimeout|useToggle|useCounter|useClickOutside|useFocus|useAsync|useColorMode|useDark|useWebSocket|useRoute|useSearchParams|onMount|onDestroy|batch|untrack|peek)\s*(?:<[^>]*>)?\s*\(/.test(scriptContent), isSfcWrapped = /function __stx_setup_/.test(code), alreadyMounts = /\bstx\.mount\s*\(|\bstx\.mountEl\s*\(/.test(scriptContent);
|
|
788
|
+
const usesSignals = /\b(state|derived|effect|ref|reactive|computed|watch|watchEffect|useLocalStorage|useSessionStorage|useRef|useEventListener|useFetch|useDebounce|useDebouncedValue|useThrottle|useInterval|useTimeout|useToggle|useCounter|useClickOutside|useFocus|useAsync|useColorMode|useDark|useWebSocket|useRoute|useSearchParams|onMount|onDestroy|onMounted|onBeforeUnmount|onUnmounted|batch|untrack|peek)\s*(?:<[^>]*>)?\s*\(/.test(scriptContent), isSfcWrapped = /function __stx_setup_/.test(code), alreadyMounts = /\bstx\.mount\s*\(|\bstx\.mountEl\s*\(/.test(scriptContent);
|
|
744
789
|
function wrapInMount(declarations) {
|
|
745
790
|
const returnStmt = declarations.length > 0 ? `
|
|
746
791
|
return { ${declarations.join(", ")} };` : "";
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { registry } from "./component-registry.js";
|
|
2
2
|
import { registerBuiltins } from "./builtins/index.js";
|
|
3
3
|
import { decodeStxProp, findComponentTags, parseMultilineAttributes, pascalToKebab, restoreStashedScripts, stashScriptElements } from "./component-processing.js";
|
|
4
|
+
import { maskAtElementPosition, matchHtmlComment } from "./html-masking.js";
|
|
4
5
|
import { renderComponentWithSlot, userComponentFileExists } from "./utils.js";
|
|
5
6
|
import { createSafeFunction, isExpressionSafe, safeEvaluateObject } from "./safe-evaluator.js";
|
|
6
7
|
import fs from "node:fs";
|
|
7
8
|
import path from "node:path";
|
|
8
9
|
let builtinsRegistered = !1;
|
|
9
|
-
const htmlTags = new Set([
|
|
10
|
+
const BOOLEAN_ATTRIBUTE_SENTINEL = "\x00stx-boolean-attribute", htmlTags = new Set([
|
|
10
11
|
"a",
|
|
11
12
|
"abbr",
|
|
12
13
|
"address",
|
|
@@ -212,7 +213,7 @@ function parseComponentProps(rawProps, context, options) {
|
|
|
212
213
|
continue;
|
|
213
214
|
}
|
|
214
215
|
if (attrName.startsWith(":")) {
|
|
215
|
-
const propName = kebabToCamel(attrName.slice(1)), expression = attrValue ===
|
|
216
|
+
const propName = kebabToCamel(attrName.slice(1)), expression = attrValue === BOOLEAN_ATTRIBUTE_SENTINEL ? propName : attrValue;
|
|
216
217
|
try {
|
|
217
218
|
if (!isExpressionSafe(expression)) {
|
|
218
219
|
if (options.debug)
|
|
@@ -274,7 +275,7 @@ function parseComponentProps(rawProps, context, options) {
|
|
|
274
275
|
}
|
|
275
276
|
continue;
|
|
276
277
|
}
|
|
277
|
-
resolved.static[attrName] = attrValue;
|
|
278
|
+
resolved.static[attrName] = attrValue === BOOLEAN_ATTRIBUTE_SENTINEL ? "true" : attrValue;
|
|
278
279
|
}
|
|
279
280
|
return resolved;
|
|
280
281
|
}
|
|
@@ -326,7 +327,7 @@ function parseAllAttributes(attributesStr) {
|
|
|
326
327
|
}
|
|
327
328
|
props[name] = value;
|
|
328
329
|
} else
|
|
329
|
-
props[name] =
|
|
330
|
+
props[name] = BOOLEAN_ATTRIBUTE_SENTINEL;
|
|
330
331
|
}
|
|
331
332
|
return props;
|
|
332
333
|
}
|
|
@@ -621,10 +622,10 @@ async function processCustomElementTags(template, context, filePath, options, de
|
|
|
621
622
|
if (Object.keys(resolvedProps.clientReactive).length > 0)
|
|
622
623
|
finalContent = emitClientReactiveAttrs(finalContent, resolvedProps.clientReactive);
|
|
623
624
|
if (Object.keys(resolvedProps.events).length > 0) {
|
|
624
|
-
const
|
|
625
|
-
if (
|
|
626
|
-
const eventAttrs = Object.entries(resolvedProps.events).map(([event, handler]) => `${event}="${escapeAttribute(handler)}"`).join(" "),
|
|
627
|
-
finalContent = finalContent.substring(0, insertPos) +
|
|
625
|
+
const root = findRenderedComponentRoot(finalContent);
|
|
626
|
+
if (root) {
|
|
627
|
+
const eventAttrs = Object.entries(resolvedProps.events).map(([event, handler]) => `${event}="${escapeAttribute(handler)}"`).join(" "), parentEvents = Object.keys(resolvedProps.events).map((event) => event.replace(/^@/, "").split(".")[0]).join(" ");
|
|
628
|
+
finalContent = finalContent.substring(0, root.insertPos) + ` data-stx-parent-events="${parentEvents}" ${eventAttrs}` + finalContent.substring(root.insertPos);
|
|
628
629
|
}
|
|
629
630
|
}
|
|
630
631
|
result = result.substring(0, tag.startIndex) + finalContent + result.substring(tag.endIndex);
|
|
@@ -633,11 +634,48 @@ async function processCustomElementTags(template, context, filePath, options, de
|
|
|
633
634
|
}
|
|
634
635
|
}
|
|
635
636
|
function emitClientReactiveAttrs(html, clientReactive) {
|
|
636
|
-
const
|
|
637
|
-
if (!
|
|
637
|
+
const root = findRenderedComponentRoot(html);
|
|
638
|
+
if (!root)
|
|
638
639
|
return html;
|
|
639
|
-
const reactiveAttrs = Object.entries(clientReactive).map(([key, expr]) => `:${key}="${expr.replace(/"/g, """)}"`).join(" "),
|
|
640
|
-
return html.substring(0, insertPos) +
|
|
640
|
+
const structuralBindings = new Set(["if", "for", "show"]), reactiveAttrs = Object.entries(clientReactive).map(([key, expr]) => `:${pascalToKebab(key)}="${expr.replace(/"/g, """)}"`).join(" "), parentBindings = Object.keys(clientReactive).filter((key) => !structuralBindings.has(key)).map((key) => pascalToKebab(key)).join(" "), parentBindingAttr = parentBindings ? ` data-stx-parent-bindings="${parentBindings}"` : "";
|
|
641
|
+
return html.substring(0, root.insertPos) + `${parentBindingAttr} ${reactiveAttrs}` + html.substring(root.insertPos);
|
|
642
|
+
}
|
|
643
|
+
function findRenderedComponentRoot(html) {
|
|
644
|
+
const scopedRoot = /<([a-zA-Z][a-zA-Z0-9-]*)\b[^>]*\bdata-stx-scope(?:\s|=|>)/i.exec(html);
|
|
645
|
+
if (scopedRoot)
|
|
646
|
+
return {
|
|
647
|
+
insertPos: scopedRoot.index + 1 + scopedRoot[1].length
|
|
648
|
+
};
|
|
649
|
+
let cursor = 0;
|
|
650
|
+
while (cursor < html.length) {
|
|
651
|
+
const rest = html.slice(cursor), whitespace = rest.match(/^\s+/);
|
|
652
|
+
if (whitespace) {
|
|
653
|
+
cursor += whitespace[0].length;
|
|
654
|
+
continue;
|
|
655
|
+
}
|
|
656
|
+
if (rest.startsWith("<!--")) {
|
|
657
|
+
const end = html.indexOf("-->", cursor + 4);
|
|
658
|
+
if (end === -1)
|
|
659
|
+
return null;
|
|
660
|
+
cursor = end + 3;
|
|
661
|
+
continue;
|
|
662
|
+
}
|
|
663
|
+
const nonRootTag = rest.match(/^<(script|style)\b[^>]*>/i);
|
|
664
|
+
if (nonRootTag) {
|
|
665
|
+
const closeTag = `</${nonRootTag[1].toLowerCase()}>`, end = html.toLowerCase().indexOf(closeTag, cursor + nonRootTag[0].length);
|
|
666
|
+
if (end === -1)
|
|
667
|
+
return null;
|
|
668
|
+
cursor = end + closeTag.length;
|
|
669
|
+
continue;
|
|
670
|
+
}
|
|
671
|
+
const root = rest.match(/^<([a-zA-Z][a-zA-Z0-9-]*)(?:\s|>|\/)/);
|
|
672
|
+
if (!root)
|
|
673
|
+
return null;
|
|
674
|
+
return {
|
|
675
|
+
insertPos: cursor + 1 + root[1].length
|
|
676
|
+
};
|
|
677
|
+
}
|
|
678
|
+
return null;
|
|
641
679
|
}
|
|
642
680
|
export async function processComponents(template, context, filePath, options, dependencies) {
|
|
643
681
|
if (!builtinsRegistered) {
|
|
@@ -647,12 +685,17 @@ export async function processComponents(template, context, filePath, options, de
|
|
|
647
685
|
let output = await processESImports(template, context, filePath, options, dependencies);
|
|
648
686
|
output = await processImports(output, context, filePath, options, dependencies);
|
|
649
687
|
output = await processComponentDirectives(output, context, filePath, options, dependencies);
|
|
688
|
+
const nestedComments = [];
|
|
650
689
|
for (let pass = 0;pass < 3; pass++) {
|
|
690
|
+
const masked = maskAtElementPosition(output, matchHtmlComment, (_token, index) => `\x00STX_COMPONENT_COMMENT_${nestedComments.length + index}\x00`);
|
|
691
|
+
nestedComments.push(...masked.tokens);
|
|
651
692
|
const before = output;
|
|
652
|
-
output = await processCustomElementTags(output, context, filePath, options, dependencies);
|
|
693
|
+
output = await processCustomElementTags(masked.output, context, filePath, options, dependencies);
|
|
653
694
|
if (output === before)
|
|
654
695
|
break;
|
|
655
696
|
}
|
|
697
|
+
if (nestedComments.length > 0)
|
|
698
|
+
output = output.replace(/\x00STX_COMPONENT_COMMENT_(\d+)\x00/g, (_match, index) => nestedComments[Number(index)] ?? "");
|
|
656
699
|
if (options.debug) {
|
|
657
700
|
const unresolved = findUnresolvedBuiltinTags(output);
|
|
658
701
|
if (unresolved.length > 0)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Discover composable files and bundle them into a single script that
|
|
3
|
+
* populates `window.__composables`.
|
|
4
|
+
*
|
|
5
|
+
* Every file is concatenated into ONE IIFE, so composables can call each other
|
|
6
|
+
* directly without importing — their declarations share a scope once the
|
|
7
|
+
* import statements are stripped.
|
|
8
|
+
*
|
|
9
|
+
* Returns `null` when there is nothing to load, so callers can skip injection.
|
|
10
|
+
*/
|
|
11
|
+
export declare function getComposableScript(composablesDir?: string): Promise<string | null>;
|
|
12
|
+
/**
|
|
13
|
+
* Clear the cached composable script (for dev mode hot reload).
|
|
14
|
+
*/
|
|
15
|
+
export declare function clearComposableCache(): void;
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { loadStxConfig } from "./config.js";
|
|
3
|
+
import { getPublicEnvDefine } from "./public-env.js";
|
|
4
|
+
import { STX_RUNTIME_GLOBALS } from "./runtime-globals.js";
|
|
5
|
+
import { transformStoreImports } from "./store-imports.js";
|
|
6
|
+
const _cachedComposableScripts = new Map, _cachedDirs = new Map, DEFAULT_DIR_CANDIDATES = ["composables", "functions"];
|
|
7
|
+
function isSkippedFile(file) {
|
|
8
|
+
const name = path.basename(file, ".ts");
|
|
9
|
+
return name === "index" || name === "types" || file.endsWith(".d.ts");
|
|
10
|
+
}
|
|
11
|
+
async function resolveComposablesDir(composablesDir) {
|
|
12
|
+
if (composablesDir)
|
|
13
|
+
return path.resolve(composablesDir);
|
|
14
|
+
const cacheKey = `<default>:${process.cwd()}`, cachedDir = _cachedDirs.get(cacheKey);
|
|
15
|
+
if (cachedDir !== void 0)
|
|
16
|
+
return cachedDir;
|
|
17
|
+
const resolved = await probeDefaultDirs();
|
|
18
|
+
_cachedDirs.set(cacheKey, resolved);
|
|
19
|
+
return resolved;
|
|
20
|
+
}
|
|
21
|
+
async function probeDefaultDirs() {
|
|
22
|
+
const config = await loadStxConfig(), root = config.root || process.cwd(), configured = config.composablesDir;
|
|
23
|
+
if (configured)
|
|
24
|
+
return path.resolve(root, configured);
|
|
25
|
+
for (const candidate of DEFAULT_DIR_CANDIDATES) {
|
|
26
|
+
const dir = path.resolve(root, candidate);
|
|
27
|
+
try {
|
|
28
|
+
const glob = new Bun.Glob("**/*.ts");
|
|
29
|
+
for await (const file of glob.scan({ cwd: dir, absolute: !0 }))
|
|
30
|
+
if (!isSkippedFile(file))
|
|
31
|
+
return dir;
|
|
32
|
+
} catch {}
|
|
33
|
+
}
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
export async function getComposableScript(composablesDir) {
|
|
37
|
+
const resolvedDir = await resolveComposablesDir(composablesDir);
|
|
38
|
+
if (!resolvedDir)
|
|
39
|
+
return null;
|
|
40
|
+
const cached = _cachedComposableScripts.get(resolvedDir);
|
|
41
|
+
if (cached !== void 0)
|
|
42
|
+
return cached || null;
|
|
43
|
+
const composableFiles = [];
|
|
44
|
+
try {
|
|
45
|
+
const glob = new Bun.Glob("**/*.ts");
|
|
46
|
+
for await (const file of glob.scan({ cwd: resolvedDir, absolute: !0 })) {
|
|
47
|
+
if (isSkippedFile(file))
|
|
48
|
+
continue;
|
|
49
|
+
composableFiles.push(file);
|
|
50
|
+
}
|
|
51
|
+
} catch {
|
|
52
|
+
_cachedComposableScripts.set(resolvedDir, "");
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
if (composableFiles.length === 0) {
|
|
56
|
+
_cachedComposableScripts.set(resolvedDir, "");
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
composableFiles.sort();
|
|
60
|
+
const transpiler = new Bun.Transpiler({ loader: "ts", target: "browser", define: getPublicEnvDefine() }), chunks = [], exportedNames = new Set, declaredNames = new Set;
|
|
61
|
+
for (const file of composableFiles)
|
|
62
|
+
try {
|
|
63
|
+
let code = await Bun.file(file).text();
|
|
64
|
+
const composableName = path.basename(file, ".ts");
|
|
65
|
+
for (const match of code.matchAll(/\b(?:const|let|var|function|class)\s+([A-Z_a-z$][\w$]*)/g))
|
|
66
|
+
if (match[1])
|
|
67
|
+
declaredNames.add(match[1]);
|
|
68
|
+
try {
|
|
69
|
+
for (const name of transpiler.scan(code).exports)
|
|
70
|
+
if (name !== "default")
|
|
71
|
+
exportedNames.add(name);
|
|
72
|
+
} catch {}
|
|
73
|
+
code = transformStoreImports(code);
|
|
74
|
+
code = code.replace(/^import\s+.*from\s+['"][^'"]+['"]\s*;?\s*$/gm, "");
|
|
75
|
+
code = code.replace(/^export\s+(default\s+)?/gm, "");
|
|
76
|
+
code = transpiler.transformSync(code);
|
|
77
|
+
chunks.push(`// Composable: ${composableName}
|
|
78
|
+
${code.trim()}`);
|
|
79
|
+
} catch (e) {
|
|
80
|
+
console.warn(`[stx] Failed to load composable ${file}:`, e);
|
|
81
|
+
}
|
|
82
|
+
if (chunks.length === 0 || exportedNames.size === 0) {
|
|
83
|
+
_cachedComposableScripts.set(resolvedDir, "");
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
const assignments = [...exportedNames].map((name) => ` try { __c[${JSON.stringify(name)}] = ${name}; } catch (e) {}`).join(`
|
|
87
|
+
`), globalNames = JSON.stringify([...exportedNames]), joinedChunks = chunks.join(`
|
|
88
|
+
|
|
89
|
+
`), code = `;(function(){
|
|
90
|
+
window.__composables = window.__composables || {};
|
|
91
|
+
var __c = window.__composables;
|
|
92
|
+
var __stx = window.stx || {};
|
|
93
|
+
${STX_RUNTIME_GLOBALS.filter((name) => !declaredNames.has(name) && new RegExp(`\\b${name}\\b`).test(joinedChunks)).map((name) => `var ${name} = __stx[${JSON.stringify(name)}];`).join(`
|
|
94
|
+
`)}
|
|
95
|
+
${joinedChunks}
|
|
96
|
+
${assignments}
|
|
97
|
+
var __names = ${globalNames};
|
|
98
|
+
for (var __i = 0; __i < __names.length; __i++) {
|
|
99
|
+
var __n = __names[__i];
|
|
100
|
+
if (!(__n in __c)) continue;
|
|
101
|
+
var __takenByBuiltin = !!(window.stx && __n in window.stx);
|
|
102
|
+
if (__takenByBuiltin || __n in window) {
|
|
103
|
+
console.warn('[stx] composable "' + __n + '" is not available as a bare identifier: '
|
|
104
|
+
+ (__takenByBuiltin ? 'a built-in of that name already exists' : 'window already defines that property')
|
|
105
|
+
+ '. Rename it, or use: import { ' + __n + ' } from "@composables"');
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
window[__n] = __c[__n];
|
|
109
|
+
}
|
|
110
|
+
})();`;
|
|
111
|
+
_cachedComposableScripts.set(resolvedDir, code);
|
|
112
|
+
return code;
|
|
113
|
+
}
|
|
114
|
+
export function clearComposableCache() {
|
|
115
|
+
_cachedComposableScripts.clear();
|
|
116
|
+
_cachedDirs.clear();
|
|
117
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Native desktop APIs.
|
|
3
|
+
*
|
|
4
|
+
* stx bundles `@stacksjs/desktop` so an app needs one dependency rather than
|
|
5
|
+
* three. Import windows, system tray, preferences, power management, global
|
|
6
|
+
* shortcuts and the rest of the native surface from here:
|
|
7
|
+
*
|
|
8
|
+
* ```typescript
|
|
9
|
+
* import { caffeinate, createPreferences, createWindow } from '@stacksjs/stx/desktop'
|
|
10
|
+
* ```
|
|
11
|
+
*
|
|
12
|
+
* For a menu bar app, prefer `@stacksjs/stx/menubar`, which composes these into
|
|
13
|
+
* a single declarative call.
|
|
14
|
+
*/
|
|
15
|
+
export * from '@stacksjs/desktop';
|
package/dist/desktop.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "@stacksjs/desktop";
|
|
@@ -49,8 +49,8 @@ async function tryImportCrosswind(importPath) {
|
|
|
49
49
|
} catch {}
|
|
50
50
|
return null;
|
|
51
51
|
}
|
|
52
|
-
function
|
|
53
|
-
const paths = []
|
|
52
|
+
function findProjectCrosswindPaths() {
|
|
53
|
+
const paths = [];
|
|
54
54
|
let currentDir = process.cwd();
|
|
55
55
|
while (currentDir !== path.dirname(currentDir)) {
|
|
56
56
|
paths.push(path.join(currentDir, "node_modules", "@cwcss", "crosswind", "dist", "index.js"));
|
|
@@ -61,6 +61,10 @@ function findCrosswindPaths() {
|
|
|
61
61
|
paths.push(path.join(currentDir, "pantry", "@stacksjs", "crosswind", "dist", "index.js"));
|
|
62
62
|
currentDir = path.dirname(currentDir);
|
|
63
63
|
}
|
|
64
|
+
return paths;
|
|
65
|
+
}
|
|
66
|
+
function findDevCrosswindPaths() {
|
|
67
|
+
const paths = [], homeDir = process.env.HOME || process.env.USERPROFILE || "";
|
|
64
68
|
if (homeDir) {
|
|
65
69
|
const devPaths = [
|
|
66
70
|
path.join(homeDir, "Code", "Tools", "crosswind", "packages", "crosswind", "dist", "index.js"),
|
|
@@ -81,6 +85,23 @@ export async function loadCrosswind() {
|
|
|
81
85
|
return crosswindModule;
|
|
82
86
|
crosswindLoadAttempted = !0;
|
|
83
87
|
try {
|
|
88
|
+
const loadFrom = async (candidates) => {
|
|
89
|
+
for (const candidate of candidates) {
|
|
90
|
+
if (!await Bun.file(candidate).exists())
|
|
91
|
+
continue;
|
|
92
|
+
const result = await tryImportCrosswind(candidate);
|
|
93
|
+
if (result) {
|
|
94
|
+
if (!process.env.STACKS_DEV_QUIET)
|
|
95
|
+
console.log(`${colors.green}[Crosswind]${colors.reset} CSS engine loaded from ${path.dirname(path.dirname(candidate))}`);
|
|
96
|
+
return result;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return null;
|
|
100
|
+
}, projectModule = await loadFrom(findProjectCrosswindPaths());
|
|
101
|
+
if (projectModule) {
|
|
102
|
+
crosswindModule = projectModule;
|
|
103
|
+
return crosswindModule;
|
|
104
|
+
}
|
|
84
105
|
const importPaths = [
|
|
85
106
|
"@cwcss/crosswind",
|
|
86
107
|
"@cwcss/crosswind/dist/index.js",
|
|
@@ -96,17 +117,11 @@ export async function loadCrosswind() {
|
|
|
96
117
|
return crosswindModule;
|
|
97
118
|
}
|
|
98
119
|
}
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
crosswindModule = result;
|
|
105
|
-
if (!process.env.STACKS_DEV_QUIET)
|
|
106
|
-
console.log(`${colors.green}[Crosswind]${colors.reset} CSS engine loaded from ${path.dirname(path.dirname(localPath))}`);
|
|
107
|
-
return crosswindModule;
|
|
108
|
-
}
|
|
109
|
-
}
|
|
120
|
+
const devModule = await loadFrom(findDevCrosswindPaths());
|
|
121
|
+
if (devModule) {
|
|
122
|
+
crosswindModule = devModule;
|
|
123
|
+
return crosswindModule;
|
|
124
|
+
}
|
|
110
125
|
throw Error("Crosswind CSSGenerator not found in any location");
|
|
111
126
|
} catch {
|
|
112
127
|
console.warn(`${colors.yellow}[Crosswind] CSS engine not available, Tailwind styles will not be generated${colors.reset}`);
|
|
@@ -319,7 +319,7 @@ export async function serveApp(appDir = ".", options = {}) {
|
|
|
319
319
|
actualHmrPort = hmrServer.start(desiredHmrPort);
|
|
320
320
|
}
|
|
321
321
|
const injectRouteParams = (content, params) => {
|
|
322
|
-
const paramsScript = `<script>(function(){var
|
|
322
|
+
const paramsScript = `<script data-stx-route-params>(function(){var p=${JSON.stringify(params).replace(/</g, "\\u003C").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029")};window.__stx_rp=p;if(window.stx){window.stx._rp=p;if(window.stx.setRouteParams)window.stx.setRouteParams(p)}})()</script>`;
|
|
323
323
|
if (content.includes("</head>"))
|
|
324
324
|
return content.replace("</head>", `${paramsScript}</head>`);
|
|
325
325
|
if (content.includes("<body"))
|
package/dist/expressions.js
CHANGED
|
@@ -352,7 +352,7 @@ export function processExpressions(template, context, filePath, options) {
|
|
|
352
352
|
output = maskInto(output, styleMatcher, styleBlocks, (n) => `<!--__STX_STYLE_${n}__-->`);
|
|
353
353
|
const scriptBlocks = [];
|
|
354
354
|
output = maskInto(output, matchScriptElement, scriptBlocks, (n) => `<!--__STX_SCRIPT_EXPR_${n}__-->`);
|
|
355
|
-
const hasSignals = options?.forceSignals === !0 || context?.__stx_force_signals === !0 || usesSignalsInScript(template);
|
|
355
|
+
const hasSignals = options?.forceSignals === !0 || context?.__stx_force_signals === !0 || Array.isArray(context?.__stx_client_signal_names) || usesSignalsInScript(template), clientSignalNames = new Set(Array.isArray(context?.__stx_client_signal_names) ? context.__stx_client_signal_names : []), preservedClientExpressions = new Set(Array.isArray(context?.__stx_preserved_client_expressions) ? context.__stx_preserved_client_expressions : []);
|
|
356
356
|
output = output.replace(/\{\{\{([\s\S]*?)\}\}\}/g, (match, expr, offset) => {
|
|
357
357
|
try {
|
|
358
358
|
const value = evaluateExpression(expr, context);
|
|
@@ -378,17 +378,24 @@ export function processExpressions(template, context, filePath, options) {
|
|
|
378
378
|
output = maskInto(output, matchScriptElement, scriptBlocks, (n) => `<!--__STX_SCRIPT_EXPR_${n}__-->`);
|
|
379
379
|
output = maskInto(output, styleMatcher, styleBlocks, (n) => `<!--__STX_STYLE_${n}__-->`);
|
|
380
380
|
output = output.replace(/\{\{([\s\S]*?)\}\}/g, (match, expr, offset) => {
|
|
381
|
-
const trimmedExpr = expr.trim()
|
|
381
|
+
const trimmedExpr = expr.trim();
|
|
382
|
+
if (preservedClientExpressions.has(trimmedExpr))
|
|
383
|
+
return match;
|
|
384
|
+
const jsBuiltins = ["parseInt", "parseFloat", "String", "Number", "Boolean", "Array", "Object", "JSON", "Math", "Date", "encodeURIComponent", "decodeURIComponent", "encodeURI", "decodeURI", "true", "false", "null", "undefined"], identifierPattern = /^([a-zA-Z_$][a-zA-Z0-9_$]*)/, firstVarName = trimmedExpr.match(identifierPattern)?.[1], firstVarInContext = firstVarName && ((firstVarName in context) || jsBuiltins.includes(firstVarName)), firstVarIsClientSignal = Boolean(firstVarName && clientSignalNames.has(firstVarName)), directFunctionCall = /^([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\(/, directFuncMatch = trimmedExpr.match(directFunctionCall), isLikelySignal = directFuncMatch && !jsBuiltins.includes(directFuncMatch[1]) && !(directFuncMatch[1] in context);
|
|
382
385
|
if (/^__[A-Z_]+__$/.test(trimmedExpr))
|
|
383
386
|
return "";
|
|
384
387
|
try {
|
|
385
388
|
const value = evaluateExpression(expr, context);
|
|
389
|
+
if (firstVarIsClientSignal)
|
|
390
|
+
return match;
|
|
386
391
|
if (hasSignals && !expressionUsesOnlyContextVars(trimmedExpr, context))
|
|
387
392
|
return match;
|
|
388
393
|
if (value === void 0 && context.__stx_buildMode === "compile" && !hasSignals)
|
|
389
394
|
return createPlaceholder("expr", trimmedExpr);
|
|
390
395
|
return value !== void 0 && value !== null ? escapeHtml(String(value)) : "";
|
|
391
396
|
} catch (error) {
|
|
397
|
+
if (firstVarIsClientSignal)
|
|
398
|
+
return match;
|
|
392
399
|
if (hasSignals && !expressionUsesOnlyContextVars(trimmedExpr, context))
|
|
393
400
|
return match;
|
|
394
401
|
if (isLikelySignal)
|
package/dist/includes.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { findSfcTemplateBlock } from "./sfc-template.js";
|
|
1
2
|
import path from "node:path";
|
|
2
3
|
import { processConditionals } from "./conditionals.js";
|
|
3
4
|
import { isProduction, isTest } from "./env.js";
|
|
4
5
|
import { processExpressions, usesSignalsInScript } from "./expressions.js";
|
|
6
|
+
import { buildRuntimeGlobalsDestructure } from "./runtime-globals.js";
|
|
5
7
|
import { LRUCache } from "./performance-utils.js";
|
|
6
8
|
import { createSafeFunction, isExpressionSafe, safeEvaluate, safeEvaluateObject } from "./safe-evaluator.js";
|
|
7
9
|
import { transformStoreImports } from "./store-imports.js";
|
|
@@ -180,7 +182,7 @@ function extractExports(setupContent) {
|
|
|
180
182
|
function wrapClientScript(scriptContent, tail = "") {
|
|
181
183
|
return `
|
|
182
184
|
(function() {
|
|
183
|
-
var
|
|
185
|
+
${buildRuntimeGlobalsDestructure("var")}
|
|
184
186
|
var __destroyHooks = [];
|
|
185
187
|
var __origOnDestroy = onDestroy;
|
|
186
188
|
onDestroy = function(fn) { __origOnDestroy(fn); __destroyHooks.push(fn); };
|
|
@@ -545,9 +547,9 @@ export async function processIncludes(template, context, filePath, options, depe
|
|
|
545
547
|
if (options.debug)
|
|
546
548
|
console.warn(`[stx] partial ES import extraction skipped for ${includeFilePath}:`, err instanceof Error ? err.message : err);
|
|
547
549
|
}
|
|
548
|
-
const
|
|
549
|
-
if (
|
|
550
|
-
workingContent =
|
|
550
|
+
const templateBlock = findSfcTemplateBlock(workingContent);
|
|
551
|
+
if (templateBlock)
|
|
552
|
+
workingContent = templateBlock.content.trim();
|
|
551
553
|
const styleMatch = partialContent.replace(/<script\b[^>]*>[\s\S]*?<\/script>/gi, "").match(/<style\b([^>]*)>([\s\S]*?)<\/style>/i);
|
|
552
554
|
let preservedStyle = "";
|
|
553
555
|
if (styleMatch)
|