@stacksjs/stx 0.2.119 → 0.2.121
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/builtins/attrs.d.ts +16 -0
- package/dist/builtins/attrs.js +16 -0
- package/dist/builtins/safe-image.js +2 -9
- package/dist/builtins/stx-image.js +2 -9
- package/dist/builtins/stx-link.js +2 -9
- package/dist/cli.js +1869 -863
- package/dist/client-script-bundler.d.ts +9 -7
- package/dist/client-script-bundler.js +43 -4
- package/dist/client-script.d.ts +24 -0
- package/dist/client-script.js +59 -5
- package/dist/component-library.js +30 -12
- package/dist/component-registry.d.ts +1 -0
- package/dist/component-renderer.js +13 -5
- package/dist/composables/index.d.ts +1 -0
- package/dist/composables/index.js +1 -0
- package/dist/composables/use-scroll-lock.d.ts +13 -0
- package/dist/composables/use-scroll-lock.js +55 -0
- package/dist/composition-api.d.ts +1 -1
- package/dist/composition-api.js +13 -4
- package/dist/craft-bridge.d.ts +80 -1
- package/dist/craft-bridge.js +80 -1
- package/dist/craft-components.d.ts +77 -3
- package/dist/craft-components.js +138 -9
- package/dist/craft.js +12708 -118
- package/dist/dev-server/crosswind.d.ts +15 -1
- package/dist/dev-server/crosswind.js +12 -3
- package/dist/dev-server/serve-app.js +3 -3
- package/dist/document-shell.d.ts +17 -0
- package/dist/document-shell.js +72 -2
- package/dist/expressions.js +14 -7
- package/dist/head.d.ts +8 -0
- package/dist/head.js +11 -8
- package/dist/html-masking.d.ts +12 -0
- package/dist/html-masking.js +50 -15
- package/dist/index.d.ts +9 -1
- package/dist/index.js +9 -1
- package/dist/native-build.d.ts +6 -0
- package/dist/native-build.js +77 -60
- package/dist/process.js +11 -3
- package/dist/pwa.js +1232 -508
- package/dist/render.js +6 -1
- package/dist/runtime-globals.d.ts +7 -4
- package/dist/runtime-globals.js +10 -2
- package/dist/runtime-injection.js +15 -1
- package/dist/signal-processing.js +19 -28
- package/dist/signals-api.d.ts +2 -0
- package/dist/signals-api.js +9 -0
- package/dist/signals.js +785 -62
- package/dist/ssg.js +1176 -452
- package/dist/state-management.d.ts +1 -1
- package/dist/state-management.js +1 -1
- package/dist/strict-lint.d.ts +1 -1
- package/dist/strict-lint.js +1 -1
- package/dist/stx.d.ts +6 -0
- package/dist/style-scoping.js +10 -0
- package/dist/utils.js +5 -3
- package/dist/variable-extractor.js +14 -22
- package/dist/vue-template.js +3 -2
- package/dist/x-element.d.ts +2 -2
- package/dist/x-element.js +9 -8
- package/package.json +5 -5
- package/stx.d.ts +6 -0
|
@@ -10,16 +10,18 @@
|
|
|
10
10
|
*/
|
|
11
11
|
export declare function hasUserImports(code: string): boolean;
|
|
12
12
|
/**
|
|
13
|
-
*
|
|
13
|
+
* Run one client bundle build at a time.
|
|
14
14
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
15
|
+
* Bun.build can return transient "Unseekable reading file" failures when
|
|
16
|
+
* separate builds concurrently traverse the same linked workspace dependency.
|
|
17
|
+
* Dashboard renders commonly compile the layout and page controllers together,
|
|
18
|
+
* so distinct content hashes still share much of their dependency graph.
|
|
17
19
|
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* @returns Bundled code with all imports resolved and inlined
|
|
20
|
+
* Cache reads remain concurrent and identical builds still share one promise.
|
|
21
|
+
* Only cold Bun.build work is queued, and a rejected task cannot stall the next
|
|
22
|
+
* build.
|
|
22
23
|
*/
|
|
24
|
+
export declare function queueClientBundleBuild<T>(task: () => Promise<T>): Promise<T>;
|
|
23
25
|
export declare function bundleClientScript(code: string, filePath: string, options?: {
|
|
24
26
|
projectRoot?: string
|
|
25
27
|
minify?: boolean
|
|
@@ -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 BUNDLE_CACHE_VERSION =
|
|
5
|
+
const BUNDLE_CACHE_VERSION = 5, BUNDLE_CACHE_METADATA_VERSION = 1, EXTERNAL_PATTERNS = [
|
|
6
6
|
/^stx$/,
|
|
7
7
|
/^@stacksjs\/stx$/,
|
|
8
8
|
/^@stores$/,
|
|
@@ -98,9 +98,27 @@ function createBundlePlugin(projectRoot, templateDir, tmpEntry, inputFiles) {
|
|
|
98
98
|
console.warn(`[stx:bundler] could not resolve ${args.path[0]}/ import:`, args.path);
|
|
99
99
|
return { path: resolved };
|
|
100
100
|
});
|
|
101
|
+
build.onLoad({ filter: /\.(?:[cm]?[jt]sx?|json)$/ }, (args) => {
|
|
102
|
+
const extension = path.extname(args.path).toLowerCase(), loader = extension === ".json" ? "json" : extension === ".tsx" ? "tsx" : extension === ".jsx" ? "jsx" : extension === ".ts" || extension === ".mts" || extension === ".cts" ? "ts" : "js";
|
|
103
|
+
inputFiles.add(args.path);
|
|
104
|
+
return {
|
|
105
|
+
contents: fs.readFileSync(args.path, "utf8"),
|
|
106
|
+
loader
|
|
107
|
+
};
|
|
108
|
+
});
|
|
101
109
|
}
|
|
102
110
|
};
|
|
103
111
|
}
|
|
112
|
+
const inFlightBundles = new Map, CLIENT_BUNDLE_QUEUE = Symbol.for("stx.client-bundle-build-queue");
|
|
113
|
+
export function queueClientBundleBuild(task) {
|
|
114
|
+
const shared = globalThis, result = (shared[CLIENT_BUNDLE_QUEUE] ?? Promise.resolve()).then(task, task);
|
|
115
|
+
shared[CLIENT_BUNDLE_QUEUE] = result.then(() => {
|
|
116
|
+
return;
|
|
117
|
+
}, () => {
|
|
118
|
+
return;
|
|
119
|
+
});
|
|
120
|
+
return result;
|
|
121
|
+
}
|
|
104
122
|
export async function bundleClientScript(code, filePath, options = {}) {
|
|
105
123
|
const projectRoot = options.projectRoot || process.cwd(), minify = options.minify ?? !1, cacheDir = options.cacheDir || stateDir(projectRoot, "bundle-cache"), hasher = new Bun.CryptoHasher("md5");
|
|
106
124
|
hasher.update(`${BUNDLE_CACHE_VERSION}\x00${code}\x00${filePath}`);
|
|
@@ -111,7 +129,9 @@ export async function bundleClientScript(code, filePath, options = {}) {
|
|
|
111
129
|
if (await depsFile.exists())
|
|
112
130
|
try {
|
|
113
131
|
const stored = JSON.parse(await depsFile.text());
|
|
114
|
-
|
|
132
|
+
if (stored.metadataVersion !== BUNDLE_CACHE_METADATA_VERSION || !Array.isArray(stored.files))
|
|
133
|
+
depsValid = !1;
|
|
134
|
+
for (const dep of Array.isArray(stored.files) ? stored.files : [])
|
|
115
135
|
try {
|
|
116
136
|
if (fs.statSync(dep.path).mtimeMs !== dep.mtimeMs) {
|
|
117
137
|
depsValid = !1;
|
|
@@ -130,7 +150,19 @@ export async function bundleClientScript(code, filePath, options = {}) {
|
|
|
130
150
|
}
|
|
131
151
|
console.log("[stx:bundler] cache invalidated by dep change:", hash);
|
|
132
152
|
}
|
|
133
|
-
const
|
|
153
|
+
const running = inFlightBundles.get(hash);
|
|
154
|
+
if (running)
|
|
155
|
+
return await running;
|
|
156
|
+
const build = queueClientBundleBuild(() => buildBundle(code, filePath, { projectRoot, minify, cacheDir, hash, cachePath, depsPath }));
|
|
157
|
+
inFlightBundles.set(hash, build);
|
|
158
|
+
try {
|
|
159
|
+
return await build;
|
|
160
|
+
} finally {
|
|
161
|
+
inFlightBundles.delete(hash);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
async function buildBundle(code, filePath, options) {
|
|
165
|
+
const { projectRoot, minify, cacheDir, hash, cachePath, depsPath } = options, tmpDir = stateDir(projectRoot, "bundle-tmp"), tmpEntry = path.join(tmpDir, `${hash}.ts`), tmpOutDir = path.join(tmpDir, "out", hash);
|
|
134
166
|
fs.mkdirSync(tmpDir, { recursive: !0 });
|
|
135
167
|
fs.mkdirSync(tmpOutDir, { recursive: !0 });
|
|
136
168
|
const templateDir = path.dirname(filePath), declNames = [], declRegex = /^(export\s+)?(?:const|let|var)\s+(?:\{([^}]+)\}|(\w+))/gm, funcRegex = /^(export\s+)?(?:async\s+)?function\s+(\w+)/gm;
|
|
@@ -207,6 +239,10 @@ return { ${returnedBindings} };
|
|
|
207
239
|
})();
|
|
208
240
|
${publicAssignments}`.trim();
|
|
209
241
|
console.log("[stx:bundler] bundled:", hash, "output:", bundled.length, "bytes");
|
|
242
|
+
if (declNames.length > 0 && exposedBindings.length === 0) {
|
|
243
|
+
console.warn(`[stx:bundler] dropped every binding for ${path.basename(filePath)} (${hash}); serving the unbundled source and not caching this result`);
|
|
244
|
+
return code;
|
|
245
|
+
}
|
|
210
246
|
fs.mkdirSync(cacheDir, { recursive: !0 });
|
|
211
247
|
await Bun.write(cachePath, bundled);
|
|
212
248
|
const depsSnapshot = Array.from(inputFiles).map((p) => {
|
|
@@ -216,7 +252,10 @@ ${publicAssignments}`.trim();
|
|
|
216
252
|
return null;
|
|
217
253
|
}
|
|
218
254
|
}).filter((d) => d !== null);
|
|
219
|
-
await Bun.write(depsPath, JSON.stringify({
|
|
255
|
+
await Bun.write(depsPath, JSON.stringify({
|
|
256
|
+
metadataVersion: BUNDLE_CACHE_METADATA_VERSION,
|
|
257
|
+
files: depsSnapshot
|
|
258
|
+
}));
|
|
220
259
|
return bundled;
|
|
221
260
|
} catch (error) {
|
|
222
261
|
const msg = error instanceof Error ? error.message : String(error), bunErrors = error?.errors, detail = Array.isArray(bunErrors) && bunErrors.length > 0 ? `
|
package/dist/client-script.d.ts
CHANGED
|
@@ -33,11 +33,31 @@ export declare function extractAndStripCssImports(code: string, options: { fileP
|
|
|
33
33
|
* below un-layered rules, so vendor styles default to lowest precedence.
|
|
34
34
|
*/
|
|
35
35
|
export declare function renderVendorStyleTags(styles: VendorCssImport[]): string;
|
|
36
|
+
/**
|
|
37
|
+
* Turn bare core browser helper usage into a real module import before the
|
|
38
|
+
* client bundler runs.
|
|
39
|
+
*
|
|
40
|
+
* Inline signal scripts execute synchronously while the document is parsed.
|
|
41
|
+
* Loading `window.StacksBrowser` from a later module script is therefore too
|
|
42
|
+
* late. A real import lets Bun inline and tree-shake the selected browser
|
|
43
|
+
* helpers together with the component, exactly like an explicit user import.
|
|
44
|
+
*/
|
|
45
|
+
export declare function injectBrowserCoreAutoImports(code: string): BrowserCoreAutoImportResult;
|
|
36
46
|
/**
|
|
37
47
|
* Generate destructuring statements for auto-imported symbols
|
|
38
48
|
* These access globals set up by the STX runtime and @stacksjs/browser
|
|
39
49
|
*/
|
|
40
50
|
export declare function generateAutoImportDestructuring(stxImports: string[], browserImports: string[]): string;
|
|
51
|
+
/**
|
|
52
|
+
* Generate the browser-runtime bindings needed by a signal setup wrapper.
|
|
53
|
+
*
|
|
54
|
+
* Signal pages and Vue-like signal components use specialized scoped wrappers
|
|
55
|
+
* instead of {@link processClientScript}. They already pull every STX runtime
|
|
56
|
+
* global from `window.stx`, but still need the selected Stacks browser helpers
|
|
57
|
+
* used by their script. Keeping detection here makes classic scripts, signal
|
|
58
|
+
* pages, and signal components share the same browser auto-import contract.
|
|
59
|
+
*/
|
|
60
|
+
export declare function generateBrowserAutoImportDestructuring(code: string, exclude?: readonly string[]): string;
|
|
41
61
|
/**
|
|
42
62
|
* Build the server → client bridge payload from a template context: the
|
|
43
63
|
* top-level, JSON-serializable values (excluding internal `__`/`$` keys and
|
|
@@ -71,6 +91,10 @@ export declare interface VendorCssImport {
|
|
|
71
91
|
resolvedPath: string
|
|
72
92
|
contents: string
|
|
73
93
|
}
|
|
94
|
+
export declare interface BrowserCoreAutoImportResult {
|
|
95
|
+
code: string
|
|
96
|
+
imports: string[]
|
|
97
|
+
}
|
|
74
98
|
// =============================================================================
|
|
75
99
|
// Types
|
|
76
100
|
// =============================================================================
|
package/dist/client-script.js
CHANGED
|
@@ -162,6 +162,7 @@ const STX_AUTO_IMPORTS = [
|
|
|
162
162
|
"useQuery",
|
|
163
163
|
"useMutation",
|
|
164
164
|
"useOptimistic",
|
|
165
|
+
"useId",
|
|
165
166
|
"useReactiveProp",
|
|
166
167
|
"useLocalStorage",
|
|
167
168
|
"useSessionStorage",
|
|
@@ -185,6 +186,7 @@ const STX_AUTO_IMPORTS = [
|
|
|
185
186
|
"useColorMode",
|
|
186
187
|
"useDark",
|
|
187
188
|
"useMediaQuery",
|
|
189
|
+
"useScrollLock",
|
|
188
190
|
"usePreferredDark",
|
|
189
191
|
"usePreferredLight",
|
|
190
192
|
"usePreferredReducedMotion",
|
|
@@ -230,8 +232,53 @@ const STX_AUTO_IMPORTS = [
|
|
|
230
232
|
"getBrowserConfig",
|
|
231
233
|
"createBrowserDb",
|
|
232
234
|
"createBrowserModel",
|
|
233
|
-
"isBrowser"
|
|
234
|
-
|
|
235
|
+
"isBrowser",
|
|
236
|
+
"debounce",
|
|
237
|
+
"throttle",
|
|
238
|
+
"retry",
|
|
239
|
+
"sleep",
|
|
240
|
+
"wait",
|
|
241
|
+
"delay",
|
|
242
|
+
"waitUntil",
|
|
243
|
+
"waitWhile",
|
|
244
|
+
"lazy",
|
|
245
|
+
"clamp",
|
|
246
|
+
"rand",
|
|
247
|
+
"readableSize",
|
|
248
|
+
"useDocumentVisibility",
|
|
249
|
+
"useIntervalFn"
|
|
250
|
+
];
|
|
251
|
+
export function injectBrowserCoreAutoImports(code) {
|
|
252
|
+
const explicitlyImported = new Set;
|
|
253
|
+
for (const match of code.matchAll(/^\s*import\s+(?:type\s+)?\{([^}]*)\}\s+from\s+['"][^'"]+['"]\s*;?\s*$/gm))
|
|
254
|
+
for (const specifier of match[1].split(",")) {
|
|
255
|
+
const localName = specifier.trim().split(/\s+as\s+/).pop()?.replace(/^type\s+/, "").trim();
|
|
256
|
+
if (localName)
|
|
257
|
+
explicitlyImported.add(localName);
|
|
258
|
+
}
|
|
259
|
+
const locallyDeclared = new Set;
|
|
260
|
+
for (const match of code.matchAll(/\b(?:const|let|var|function|class)\s+([A-Za-z_$][\w$]*)/g))
|
|
261
|
+
locallyDeclared.add(match[1]);
|
|
262
|
+
for (const match of code.matchAll(/\b(?:const|let|var)\s*\{([^}]*)\}/g))
|
|
263
|
+
for (const binding of match[1].split(",")) {
|
|
264
|
+
const localName = binding.trim().split(/[:=]/).pop()?.trim();
|
|
265
|
+
if (localName)
|
|
266
|
+
locallyDeclared.add(localName);
|
|
267
|
+
}
|
|
268
|
+
const imports = BROWSER_CORE_IMPORTS.filter((symbol) => {
|
|
269
|
+
if (explicitlyImported.has(symbol) || locallyDeclared.has(symbol))
|
|
270
|
+
return !1;
|
|
271
|
+
return new RegExp(`\\b${symbol}\\b`).test(code);
|
|
272
|
+
});
|
|
273
|
+
if (imports.length === 0)
|
|
274
|
+
return { code, imports };
|
|
275
|
+
return {
|
|
276
|
+
code: `import { ${imports.join(", ")} } from '@stacksjs/browser'
|
|
277
|
+
${code}`,
|
|
278
|
+
imports
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
const JS_BUILTINS = new Set([
|
|
235
282
|
"Promise",
|
|
236
283
|
"Object",
|
|
237
284
|
"Array",
|
|
@@ -350,6 +397,10 @@ ${lines.join(`
|
|
|
350
397
|
${generateAutoImportGuard(stxImports, browserImports)}`;
|
|
351
398
|
return "";
|
|
352
399
|
}
|
|
400
|
+
export function generateBrowserAutoImportDestructuring(code, exclude = []) {
|
|
401
|
+
const { browserImports } = transformAutoImports(code), excluded = new Set(exclude);
|
|
402
|
+
return generateAutoImportDestructuring([], browserImports.filter((name) => !excluded.has(name)));
|
|
403
|
+
}
|
|
353
404
|
function generateAutoImportGuard(stxImports, browserImports) {
|
|
354
405
|
const entries = [
|
|
355
406
|
...stxImports.map((name) => `['${name}', typeof ${name}, 'stx']`),
|
|
@@ -753,13 +804,16 @@ function extractTemplateReferences(templateHtml) {
|
|
|
753
804
|
return refs;
|
|
754
805
|
}
|
|
755
806
|
export async function processClientScript(scriptContent, options = {}) {
|
|
756
|
-
let code = scriptContent, autoImportCode = "", wasBundled = !1;
|
|
807
|
+
let code = scriptContent, autoImportCode = "", wasBundled = !1, bundledBrowserImports = [];
|
|
757
808
|
const cssExtraction = extractAndStripCssImports(code, {
|
|
758
809
|
filePath: options.filePath,
|
|
759
810
|
projectRoot: options.projectRoot
|
|
760
811
|
});
|
|
761
812
|
code = cssExtraction.code;
|
|
762
|
-
const vendorStyleTags = renderVendorStyleTags(cssExtraction.styles),
|
|
813
|
+
const vendorStyleTags = renderVendorStyleTags(cssExtraction.styles), browserCoreImports = injectBrowserCoreAutoImports(code);
|
|
814
|
+
code = browserCoreImports.code;
|
|
815
|
+
bundledBrowserImports = browserCoreImports.imports;
|
|
816
|
+
const { hasUserImports, bundleClientScript } = await import("./client-script-bundler.js");
|
|
763
817
|
if (hasUserImports(code)) {
|
|
764
818
|
console.log("[stx:bundler] user imports detected, bundling...");
|
|
765
819
|
code = await bundleClientScript(code, options.filePath || "", {
|
|
@@ -771,7 +825,7 @@ export async function processClientScript(scriptContent, options = {}) {
|
|
|
771
825
|
if (options.autoImports !== !1) {
|
|
772
826
|
const autoImportResult = transformAutoImports(code);
|
|
773
827
|
code = autoImportResult.code;
|
|
774
|
-
autoImportCode = generateAutoImportDestructuring(autoImportResult.stxImports, autoImportResult.browserImports);
|
|
828
|
+
autoImportCode = generateAutoImportDestructuring(autoImportResult.stxImports, autoImportResult.browserImports.filter((name) => !bundledBrowserImports.includes(name)));
|
|
775
829
|
}
|
|
776
830
|
autoImportCode = `${generateServerDataBridge(code, options.serverData)}${autoImportCode}`;
|
|
777
831
|
code = transformStoreImports(code);
|
|
@@ -58,7 +58,13 @@ function extractClientMethods(source, file) {
|
|
|
58
58
|
for (const script of scripts) {
|
|
59
59
|
if (!/\bclient\b/i.test(script[1]) || /\bcomponent\b|\bstx:component\b/i.test(script[1]))
|
|
60
60
|
continue;
|
|
61
|
-
|
|
61
|
+
let code;
|
|
62
|
+
try {
|
|
63
|
+
code = new Bun.Transpiler({ loader: "ts" }).transformSync(script[2]);
|
|
64
|
+
} catch (error) {
|
|
65
|
+
throw Error(`${file}: failed to transpile <script client> (${error.message})`);
|
|
66
|
+
}
|
|
67
|
+
const functionPattern = /(?:export\s+)?(?:async\s+)?function\s+([A-Za-z_$][\w$]*)\s*\(([^)]*)\)\s*\{/g;
|
|
62
68
|
let match;
|
|
63
69
|
while (match = functionPattern.exec(code)) {
|
|
64
70
|
const name = match[1];
|
|
@@ -673,17 +679,29 @@ ${components.map((component) => `import type { ${component.name}Props } from './
|
|
|
673
679
|
let bundle;
|
|
674
680
|
if (config.bundle !== !1) {
|
|
675
681
|
bundle = path.join(outputDir, "bundle.js");
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
682
|
+
let result;
|
|
683
|
+
try {
|
|
684
|
+
result = await Bun.build({
|
|
685
|
+
entrypoints: [path.join(outputDir, "index.js")],
|
|
686
|
+
outdir: outputDir,
|
|
687
|
+
naming: "bundle.js",
|
|
688
|
+
target: "browser",
|
|
689
|
+
format: "esm",
|
|
690
|
+
minify: config.minify ?? !0,
|
|
691
|
+
sourcemap: config.sourcemap || "none"
|
|
692
|
+
});
|
|
693
|
+
} catch (error) {
|
|
694
|
+
const logs = error instanceof AggregateError ? error.errors : [error], details = logs.map((log) => String(log).trim()).filter(Boolean).join(`
|
|
695
|
+
`);
|
|
696
|
+
throw AggregateError(logs, `Failed to bundle component library${details ? `
|
|
697
|
+
${details}` : ""}`);
|
|
698
|
+
}
|
|
699
|
+
if (!result.success) {
|
|
700
|
+
const details = result.logs.map((log) => String(log).trim()).filter(Boolean).join(`
|
|
701
|
+
`);
|
|
702
|
+
throw AggregateError(result.logs, `Failed to bundle component library${details ? `
|
|
703
|
+
${details}` : ""}`);
|
|
704
|
+
}
|
|
687
705
|
files.push(bundle);
|
|
688
706
|
if (config.sourcemap === "external")
|
|
689
707
|
files.push(`${bundle}.map`);
|
|
@@ -27,6 +27,7 @@ export declare interface RenderContext {
|
|
|
27
27
|
*/
|
|
28
28
|
export declare interface ResolvedProps {
|
|
29
29
|
static: Record<string, string | boolean>
|
|
30
|
+
staticNames: Record<string, string>
|
|
30
31
|
serverDynamic: Record<string, unknown>
|
|
31
32
|
clientReactive: Record<string, string>
|
|
32
33
|
events: Record<string, string>
|
|
@@ -176,6 +176,7 @@ const BOOLEAN_ATTRIBUTE_SENTINEL = "\x00stx-boolean-attribute", htmlTags = new S
|
|
|
176
176
|
function parseComponentProps(rawProps, context, options) {
|
|
177
177
|
const resolved = {
|
|
178
178
|
static: {},
|
|
179
|
+
staticNames: {},
|
|
179
180
|
serverDynamic: {},
|
|
180
181
|
clientReactive: {},
|
|
181
182
|
events: {}
|
|
@@ -199,7 +200,14 @@ function parseComponentProps(rawProps, context, options) {
|
|
|
199
200
|
]), kebabToCamel = (key) => {
|
|
200
201
|
if (!key.includes("-") || key.startsWith("@") || key.startsWith(":") || key.startsWith("x-") || key.startsWith("__"))
|
|
201
202
|
return key;
|
|
203
|
+
if (key.startsWith("aria-") || key.startsWith("data-"))
|
|
204
|
+
return key;
|
|
202
205
|
return key.replace(/-([a-z0-9])/g, (_, c) => c.toUpperCase());
|
|
206
|
+
}, setStatic = (attrName, value) => {
|
|
207
|
+
const propName = kebabToCamel(attrName);
|
|
208
|
+
resolved.static[propName] = value;
|
|
209
|
+
if (propName !== attrName)
|
|
210
|
+
resolved.staticNames[propName] = attrName;
|
|
203
211
|
};
|
|
204
212
|
for (const [rawAttrName, attrValue] of Object.entries(rawProps)) {
|
|
205
213
|
const attrName = rawAttrName.startsWith("x-") && !ALPINE_PASSTHROUGH.has(rawAttrName) && !rawAttrName.startsWith("x-bind:") && !rawAttrName.startsWith("x-on:") ? `:${rawAttrName.slice(2)}` : rawAttrName;
|
|
@@ -242,16 +250,16 @@ function parseComponentProps(rawProps, context, options) {
|
|
|
242
250
|
const expression = singleExprMatch[1];
|
|
243
251
|
if (isExpressionSafe(expression)) {
|
|
244
252
|
const evaluated = createSafeFunction(expression, Object.keys(context))(...Object.values(context));
|
|
245
|
-
|
|
253
|
+
setStatic(attrName, evaluated != null ? String(evaluated) : "");
|
|
246
254
|
} else {
|
|
247
255
|
if (options.debug)
|
|
248
256
|
console.error(`Unsafe expression in ${attrName}: ${expression}`);
|
|
249
|
-
|
|
257
|
+
setStatic(attrName, attrValue);
|
|
250
258
|
}
|
|
251
259
|
} catch (error) {
|
|
252
260
|
if (options.debug)
|
|
253
261
|
console.error(`Error evaluating expression for ${attrName}:`, error);
|
|
254
|
-
|
|
262
|
+
setStatic(attrName, attrValue);
|
|
255
263
|
}
|
|
256
264
|
else {
|
|
257
265
|
let result = attrValue;
|
|
@@ -271,11 +279,11 @@ function parseComponentProps(rawProps, context, options) {
|
|
|
271
279
|
if (options.debug)
|
|
272
280
|
console.error(`Error evaluating expression in ${attrName}:`, error);
|
|
273
281
|
}
|
|
274
|
-
|
|
282
|
+
setStatic(attrName, result);
|
|
275
283
|
}
|
|
276
284
|
continue;
|
|
277
285
|
}
|
|
278
|
-
|
|
286
|
+
setStatic(attrName, attrValue === BOOLEAN_ATTRIBUTE_SENTINEL ? "true" : attrValue);
|
|
279
287
|
}
|
|
280
288
|
return resolved;
|
|
281
289
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Signal } from '../signals-api';
|
|
2
|
+
/**
|
|
3
|
+
* Reactively lock scrolling on an element, defaulting to the document body.
|
|
4
|
+
*
|
|
5
|
+
* Multiple locks on the same element are reference counted, so releasing one
|
|
6
|
+
* overlay cannot restore scrolling while another overlay is still open.
|
|
7
|
+
*/
|
|
8
|
+
export declare function useScrollLock(target?: ScrollLockTarget): Signal<boolean>;
|
|
9
|
+
declare type ScrollLockTarget = | HTMLElement
|
|
10
|
+
| null
|
|
11
|
+
| undefined
|
|
12
|
+
| { current?: HTMLElement | null, value?: HTMLElement | null }
|
|
13
|
+
| (() => HTMLElement | null | undefined);
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { onDestroy, state } from "../signals-api.js";
|
|
2
|
+
const scrollLockStates = new WeakMap;
|
|
3
|
+
function resolveTarget(target) {
|
|
4
|
+
let value = target;
|
|
5
|
+
if (typeof value === "function")
|
|
6
|
+
value = value();
|
|
7
|
+
if (value && typeof value === "object" && "current" in value)
|
|
8
|
+
value = value.current;
|
|
9
|
+
if (value && typeof value === "object" && !("nodeType" in value) && "value" in value)
|
|
10
|
+
value = value.value;
|
|
11
|
+
return value || document.body || document.documentElement;
|
|
12
|
+
}
|
|
13
|
+
function acquire(element) {
|
|
14
|
+
const existing = scrollLockStates.get(element);
|
|
15
|
+
if (existing) {
|
|
16
|
+
existing.count += 1;
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
scrollLockStates.set(element, { count: 1, overflow: element.style.overflow });
|
|
20
|
+
element.style.overflow = "hidden";
|
|
21
|
+
}
|
|
22
|
+
function release(element) {
|
|
23
|
+
const existing = scrollLockStates.get(element);
|
|
24
|
+
if (!existing)
|
|
25
|
+
return;
|
|
26
|
+
existing.count -= 1;
|
|
27
|
+
if (existing.count > 0)
|
|
28
|
+
return;
|
|
29
|
+
element.style.overflow = existing.overflow;
|
|
30
|
+
scrollLockStates.delete(element);
|
|
31
|
+
}
|
|
32
|
+
export function useScrollLock(target) {
|
|
33
|
+
const locked = state(!1);
|
|
34
|
+
let lockedElement = null;
|
|
35
|
+
const unsubscribe = locked.subscribe((value) => {
|
|
36
|
+
if (value) {
|
|
37
|
+
if (lockedElement)
|
|
38
|
+
return;
|
|
39
|
+
lockedElement = resolveTarget(target);
|
|
40
|
+
if (lockedElement)
|
|
41
|
+
acquire(lockedElement);
|
|
42
|
+
} else if (lockedElement) {
|
|
43
|
+
release(lockedElement);
|
|
44
|
+
lockedElement = null;
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
onDestroy(() => {
|
|
48
|
+
unsubscribe();
|
|
49
|
+
if (lockedElement) {
|
|
50
|
+
release(lockedElement);
|
|
51
|
+
lockedElement = null;
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
return locked;
|
|
55
|
+
}
|
|
@@ -74,7 +74,7 @@ export declare function setGlobalProvide<T>(key: string | symbol, value: T): voi
|
|
|
74
74
|
* emit('change', 'newValue')
|
|
75
75
|
* ```
|
|
76
76
|
*/
|
|
77
|
-
export declare function defineEmits<T extends Record<string, any[]> = Record<string, any[]>>(
|
|
77
|
+
export declare function defineEmits<T extends Record<string, any[]> = Record<string, any[]>>(events?: string[] | T): EmitFn<Extract<keyof T, string>>;
|
|
78
78
|
/**
|
|
79
79
|
* Expose public properties from a component instance.
|
|
80
80
|
* These properties become accessible via template refs from the parent.
|
package/dist/composition-api.js
CHANGED
|
@@ -55,16 +55,25 @@ const globalProvides = new Map;
|
|
|
55
55
|
export function setGlobalProvide(key, value) {
|
|
56
56
|
globalProvides.set(key, value);
|
|
57
57
|
}
|
|
58
|
-
export function defineEmits(
|
|
59
|
-
const instance = currentInstance,
|
|
58
|
+
export function defineEmits(events) {
|
|
59
|
+
const instance = currentInstance, element = instance?.el, declaredEvents = Array.isArray(events) ? events : events && typeof events === "object" ? Object.keys(events) : [];
|
|
60
|
+
if (element && declaredEvents.length > 0) {
|
|
61
|
+
const emittedEvents = element.__stx_component_emits ||= Object.create(null);
|
|
62
|
+
for (const event of declaredEvents)
|
|
63
|
+
emittedEvents[event] = !0;
|
|
64
|
+
}
|
|
65
|
+
const emit = (event, ...args) => {
|
|
60
66
|
if (typeof document > "u")
|
|
61
67
|
return;
|
|
62
|
-
if (
|
|
63
|
-
|
|
68
|
+
if (element) {
|
|
69
|
+
const emittedEvents = element.__stx_component_emits ||= Object.create(null);
|
|
70
|
+
emittedEvents[event] = !0;
|
|
71
|
+
element.dispatchEvent(new CustomEvent(event, {
|
|
64
72
|
detail: args.length === 1 ? args[0] : args,
|
|
65
73
|
bubbles: !0,
|
|
66
74
|
cancelable: !0
|
|
67
75
|
}));
|
|
76
|
+
}
|
|
68
77
|
};
|
|
69
78
|
if (instance)
|
|
70
79
|
instance.emit = emit;
|
package/dist/craft-bridge.d.ts
CHANGED
|
@@ -186,6 +186,12 @@ else if (data.type === 'event' && data.method) {
|
|
|
186
186
|
});
|
|
187
187
|
}
|
|
188
188
|
|
|
189
|
+
// Live component handles, keyed by id, so an event coming back from the host
|
|
190
|
+
// can be routed to the callbacks registered on the right one. Without this
|
|
191
|
+
// the onSelect/onSpaceChange callbacks were stored and never invoked.
|
|
192
|
+
const sidebars = new Map();
|
|
193
|
+
const spacesSidebars = new Map();
|
|
194
|
+
|
|
189
195
|
function createNativeUIFacade() {
|
|
190
196
|
class Sidebar {
|
|
191
197
|
constructor(id) {
|
|
@@ -205,10 +211,42 @@ else if (data.type === 'event' && data.method) {
|
|
|
205
211
|
return this;
|
|
206
212
|
}
|
|
207
213
|
destroy() {
|
|
214
|
+
sidebars.delete(this.id);
|
|
208
215
|
sendNativeUI('destroyComponent', { id: this.id, type: 'sidebar' });
|
|
209
216
|
}
|
|
210
217
|
}
|
|
211
218
|
|
|
219
|
+
/**
|
|
220
|
+
* An Arc-style sidebar of swipeable spaces.
|
|
221
|
+
*
|
|
222
|
+
* Separate from Sidebar rather than an option on it: a source list selects
|
|
223
|
+
* one row out of many, while a spaces sidebar selects one whole SCENE, and
|
|
224
|
+
* a host that renders the switcher natively (a segmented control in the
|
|
225
|
+
* toolbar, a Touch Bar strip) needs the space list, not the item list.
|
|
226
|
+
*/
|
|
227
|
+
class SpacesSidebar {
|
|
228
|
+
constructor(id) {
|
|
229
|
+
this.id = id;
|
|
230
|
+
this._spaceCallbacks = [];
|
|
231
|
+
}
|
|
232
|
+
setSpaces(spaces) {
|
|
233
|
+
sendNativeUI('setSidebarSpaces', { sidebarId: this.id, spaces });
|
|
234
|
+
return this;
|
|
235
|
+
}
|
|
236
|
+
setActiveSpace(spaceId) {
|
|
237
|
+
sendNativeUI('setActiveSpace', { sidebarId: this.id, spaceId });
|
|
238
|
+
return this;
|
|
239
|
+
}
|
|
240
|
+
onSpaceChange(callback) {
|
|
241
|
+
this._spaceCallbacks.push(callback);
|
|
242
|
+
return this;
|
|
243
|
+
}
|
|
244
|
+
destroy() {
|
|
245
|
+
spacesSidebars.delete(this.id);
|
|
246
|
+
sendNativeUI('destroyComponent', { id: this.id, type: 'spacesSidebar' });
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
212
250
|
class FileBrowser {
|
|
213
251
|
constructor(id) {
|
|
214
252
|
this.id = id;
|
|
@@ -245,12 +283,33 @@ else if (data.type === 'event' && data.method) {
|
|
|
245
283
|
}
|
|
246
284
|
}
|
|
247
285
|
|
|
286
|
+
// Host -> page. The host names the component in the payload; we look up the
|
|
287
|
+
// handle and fan out to whatever the page registered on it.
|
|
288
|
+
on('sidebar.select', function(params) {
|
|
289
|
+
const sidebar = sidebars.get(params && params.sidebarId);
|
|
290
|
+
if (sidebar) sidebar._selectCallbacks.forEach(fn => fn(params.itemId));
|
|
291
|
+
});
|
|
292
|
+
on('sidebar.spaceChange', function(params) {
|
|
293
|
+
const sidebar = spacesSidebars.get(params && params.sidebarId);
|
|
294
|
+
if (sidebar) sidebar._spaceCallbacks.forEach(fn => fn(params.spaceId));
|
|
295
|
+
});
|
|
296
|
+
|
|
248
297
|
return {
|
|
249
298
|
createSidebar(options) {
|
|
250
299
|
const opts = options || {};
|
|
251
300
|
const id = opts.id || 'sidebar-' + Date.now() + '-' + Math.random().toString(36).slice(2);
|
|
252
301
|
sendNativeUI('createSidebar', Object.assign({}, opts, { id }));
|
|
253
|
-
|
|
302
|
+
const sidebar = new Sidebar(id);
|
|
303
|
+
sidebars.set(id, sidebar);
|
|
304
|
+
return sidebar;
|
|
305
|
+
},
|
|
306
|
+
createSpacesSidebar(options) {
|
|
307
|
+
const opts = options || {};
|
|
308
|
+
const id = opts.id || 'spaces-' + Date.now() + '-' + Math.random().toString(36).slice(2);
|
|
309
|
+
sendNativeUI('createSpacesSidebar', Object.assign({}, opts, { id }));
|
|
310
|
+
const sidebar = new SpacesSidebar(id);
|
|
311
|
+
spacesSidebars.set(id, sidebar);
|
|
312
|
+
return sidebar;
|
|
254
313
|
},
|
|
255
314
|
createFileBrowser(options) {
|
|
256
315
|
const opts = options || {};
|
|
@@ -399,6 +458,26 @@ else if (data.type === 'event' && data.method) {
|
|
|
399
458
|
platform: () => request('process.platform'),
|
|
400
459
|
},
|
|
401
460
|
|
|
461
|
+
// Gesture stream
|
|
462
|
+
//
|
|
463
|
+
// A webview's wheel events carry no phase information, so a page can only
|
|
464
|
+
// guess where a trackpad swipe begins and ends — typically with an idle
|
|
465
|
+
// timeout, which mistimes the release and cannot see momentum at all. The
|
|
466
|
+
// host holds the real NSEvent, so it forwards the phases instead:
|
|
467
|
+
//
|
|
468
|
+
// { axis: 'horizontal' | 'vertical',
|
|
469
|
+
// phase: 'begin' | 'change' | 'end',
|
|
470
|
+
// deltaX, deltaY, // points since the last event
|
|
471
|
+
// velocityX, velocityY, // points per second, at 'end'
|
|
472
|
+
// momentum: boolean } // the OS is coasting, not the finger
|
|
473
|
+
//
|
|
474
|
+
// Consumers must stay correct when nothing ever arrives: a host without
|
|
475
|
+
// gesture support simply never emits, so keep the wheel path bound and let
|
|
476
|
+
// a begin phase take over from it.
|
|
477
|
+
gestures: {
|
|
478
|
+
onSwipe: (callback) => on('gesture.swipe', callback),
|
|
479
|
+
},
|
|
480
|
+
|
|
402
481
|
// Native component helpers
|
|
403
482
|
nativeUI,
|
|
404
483
|
components: componentAPI
|