@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
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { Preferences, WindowOptions } from '@stacksjs/desktop';
|
|
2
|
+
/**
|
|
3
|
+
* Normalize a route key to `"<METHOD> <path>"`. A key without a method is a
|
|
4
|
+
* `GET`, so `'/api/status'` and `'GET /api/status'` are the same route.
|
|
5
|
+
*/
|
|
6
|
+
export declare function normalizeRouteKey(key: string): string;
|
|
7
|
+
/**
|
|
8
|
+
* Coerce a handler's return value into a `Response`. Handlers that return
|
|
9
|
+
* nothing get `204 No Content`; a handler that built its own `Response` keeps it.
|
|
10
|
+
*/
|
|
11
|
+
export declare function toResponse(result: RouteResult): Response;
|
|
12
|
+
/**
|
|
13
|
+
* Apply an untrusted patch to a preference store, keeping only keys that exist
|
|
14
|
+
* in the defaults so a stray request cannot write arbitrary fields into the
|
|
15
|
+
* preferences file.
|
|
16
|
+
*/
|
|
17
|
+
export declare function applyPreferencePatch<T extends Record<string, unknown>>(preferences: Preferences<T>, patch: Record<string, unknown>): T;
|
|
18
|
+
export declare function createMenuBarApp<T extends Record<string, unknown>>(options: MenuBarAppOptions<T>): MenuBarApp<T>;
|
|
19
|
+
/** A tray menu entry. Actions are plain strings the app interprets itself. */
|
|
20
|
+
export declare interface MenuBarMenuItem {
|
|
21
|
+
label?: string
|
|
22
|
+
type?: 'normal' | 'separator' | 'checkbox' | 'radio' | 'submenu'
|
|
23
|
+
checked?: boolean
|
|
24
|
+
enabled?: boolean
|
|
25
|
+
icon?: string
|
|
26
|
+
shortcut?: string
|
|
27
|
+
action?: string
|
|
28
|
+
submenu?: MenuBarMenuItem[]
|
|
29
|
+
}
|
|
30
|
+
export declare interface MenuBarAppOptions<T extends Record<string, unknown>> {
|
|
31
|
+
name: string
|
|
32
|
+
template: string
|
|
33
|
+
preferences?: T
|
|
34
|
+
context?: (preferences: Preferences<T>) => Record<string, unknown> | Promise<Record<string, unknown>>
|
|
35
|
+
routes?: Routes<T>
|
|
36
|
+
menu?: (preferences: Preferences<T>) => MenuBarMenuItem[]
|
|
37
|
+
launchAtLogin?: keyof T
|
|
38
|
+
window?: WindowOptions
|
|
39
|
+
port?: number
|
|
40
|
+
}
|
|
41
|
+
export declare interface MenuBarApp<T extends Record<string, unknown>> {
|
|
42
|
+
readonly preferences: Preferences<T>
|
|
43
|
+
readonly url: string | null
|
|
44
|
+
start: () => Promise<MenuBarApp<T>>
|
|
45
|
+
stop: () => void
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* What a route may hand back: a `Response` to take over the reply entirely,
|
|
49
|
+
* `undefined` for a `204`, or any JSON-serializable value to send as JSON.
|
|
50
|
+
* Typed as `unknown` so a handler can return a plain interface without the
|
|
51
|
+
* value needing an index signature.
|
|
52
|
+
*/
|
|
53
|
+
export type RouteResult = Response | unknown;
|
|
54
|
+
/** A route handler receives the request and the app's preference store. */
|
|
55
|
+
export type RouteHandler<T extends Record<string, unknown>> = (
|
|
56
|
+
request: Request,
|
|
57
|
+
preferences: Preferences<T>,
|
|
58
|
+
) => RouteResult | Promise<RouteResult>;
|
|
59
|
+
/**
|
|
60
|
+
* Routes keyed by `"<METHOD> <path>"`, e.g. `'POST /api/caffeinate/toggle'`.
|
|
61
|
+
* The method may be omitted, in which case `GET` is assumed.
|
|
62
|
+
*/
|
|
63
|
+
export type Routes<T extends Record<string, unknown>> = Record<string, RouteHandler<T>>;
|
package/dist/menubar.js
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { createPreferences, createWindow, setAutoLaunch } from "@stacksjs/desktop";
|
|
2
|
+
import { renderTemplate } from "./render.js";
|
|
3
|
+
const MENU_BAR_WINDOW = {
|
|
4
|
+
width: 320,
|
|
5
|
+
height: 640,
|
|
6
|
+
systemTray: !0,
|
|
7
|
+
hideDockIcon: !0,
|
|
8
|
+
titlebarHidden: !0,
|
|
9
|
+
resizable: !1,
|
|
10
|
+
alwaysOnTop: !0,
|
|
11
|
+
darkMode: !0
|
|
12
|
+
}, JSON_HEADERS = { "Content-Type": "application/json; charset=utf-8" };
|
|
13
|
+
export function normalizeRouteKey(key) {
|
|
14
|
+
const [first, ...rest] = key.trim().split(/\s+/);
|
|
15
|
+
return rest.length ? `${first.toUpperCase()} ${rest.join(" ")}` : `GET ${first}`;
|
|
16
|
+
}
|
|
17
|
+
export function toResponse(result) {
|
|
18
|
+
if (result instanceof Response)
|
|
19
|
+
return result;
|
|
20
|
+
if (result === void 0)
|
|
21
|
+
return new Response(null, { status: 204 });
|
|
22
|
+
return new Response(JSON.stringify(result), { headers: JSON_HEADERS });
|
|
23
|
+
}
|
|
24
|
+
export function applyPreferencePatch(preferences, patch) {
|
|
25
|
+
const known = preferences.getAll();
|
|
26
|
+
for (const [key, value] of Object.entries(patch))
|
|
27
|
+
if (key in known)
|
|
28
|
+
preferences.set(key, value);
|
|
29
|
+
return preferences.getAll();
|
|
30
|
+
}
|
|
31
|
+
export function createMenuBarApp(options) {
|
|
32
|
+
const preferences = createPreferences({
|
|
33
|
+
name: options.name.toLowerCase(),
|
|
34
|
+
defaults: options.preferences ?? {}
|
|
35
|
+
}), routes = new Map(Object.entries(options.routes ?? {}).map(([key, handler]) => [normalizeRouteKey(key), handler])), builtins = {
|
|
36
|
+
"GET /api/preferences": () => preferences.getAll(),
|
|
37
|
+
"POST /api/preferences": async (request) => applyPreferencePatch(preferences, await request.json()),
|
|
38
|
+
"GET /api/menu": () => options.menu?.(preferences) ?? []
|
|
39
|
+
};
|
|
40
|
+
for (const [key, handler] of Object.entries(builtins))
|
|
41
|
+
if (!routes.has(key))
|
|
42
|
+
routes.set(key, handler);
|
|
43
|
+
let server = null, window = null, url = null;
|
|
44
|
+
async function renderPopup() {
|
|
45
|
+
const context = await options.context?.(preferences) ?? {}, html = await renderTemplate(options.template, {
|
|
46
|
+
context,
|
|
47
|
+
wrapInDocument: !0,
|
|
48
|
+
title: options.name
|
|
49
|
+
});
|
|
50
|
+
return new Response(html, { headers: { "Content-Type": "text/html; charset=utf-8" } });
|
|
51
|
+
}
|
|
52
|
+
function serve() {
|
|
53
|
+
server = Bun.serve({
|
|
54
|
+
port: options.port ?? 0,
|
|
55
|
+
hostname: "127.0.0.1",
|
|
56
|
+
async fetch(request) {
|
|
57
|
+
const { pathname } = new URL(request.url);
|
|
58
|
+
if (request.method === "GET" && (pathname === "/" || pathname === "/index.html"))
|
|
59
|
+
return renderPopup();
|
|
60
|
+
const handler = routes.get(`${request.method} ${pathname}`);
|
|
61
|
+
if (!handler)
|
|
62
|
+
return new Response("Not Found", { status: 404 });
|
|
63
|
+
return toResponse(await handler(request, preferences));
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
url = `http://127.0.0.1:${server.port}`;
|
|
67
|
+
}
|
|
68
|
+
function bindLaunchAtLogin() {
|
|
69
|
+
const key = options.launchAtLogin;
|
|
70
|
+
if (!key)
|
|
71
|
+
return;
|
|
72
|
+
const sync = (enabled) => {
|
|
73
|
+
setAutoLaunch(Boolean(enabled), { appName: options.name, isHidden: !0 }).catch(() => {});
|
|
74
|
+
};
|
|
75
|
+
sync(preferences.get(key));
|
|
76
|
+
preferences.onChange(key, sync);
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
preferences,
|
|
80
|
+
get url() {
|
|
81
|
+
return url;
|
|
82
|
+
},
|
|
83
|
+
async start() {
|
|
84
|
+
if (!server)
|
|
85
|
+
serve();
|
|
86
|
+
bindLaunchAtLogin();
|
|
87
|
+
window = await createWindow(url, {
|
|
88
|
+
...MENU_BAR_WINDOW,
|
|
89
|
+
title: options.name,
|
|
90
|
+
...options.window
|
|
91
|
+
});
|
|
92
|
+
return this;
|
|
93
|
+
},
|
|
94
|
+
stop() {
|
|
95
|
+
window?.close();
|
|
96
|
+
window = null;
|
|
97
|
+
server?.stop();
|
|
98
|
+
server = null;
|
|
99
|
+
url = null;
|
|
100
|
+
preferences.close();
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
}
|
package/dist/plugin.js
CHANGED
|
@@ -5,6 +5,7 @@ import { isDevelopment } from "./env.js";
|
|
|
5
5
|
import { escapeHtml } from "./expressions.js";
|
|
6
6
|
import { performanceMonitor } from "./performance-utils.js";
|
|
7
7
|
import { injectRouterScript } from "./process.js";
|
|
8
|
+
import { findSfcTemplateBlock } from "./sfc-template.js";
|
|
8
9
|
export const plugin = {
|
|
9
10
|
name: "bun-plugin-stx",
|
|
10
11
|
async setup(build) {
|
|
@@ -42,9 +43,9 @@ export const plugin = {
|
|
|
42
43
|
throw new StxFileError(`stx file is empty: ${filePath}`, filePath);
|
|
43
44
|
const { scriptContent, templateContent, allScripts } = performanceMonitor.time("script-extraction", () => {
|
|
44
45
|
let workingContent = content;
|
|
45
|
-
const
|
|
46
|
-
if (
|
|
47
|
-
workingContent =
|
|
46
|
+
const templateBlock = findSfcTemplateBlock(content);
|
|
47
|
+
if (templateBlock)
|
|
48
|
+
workingContent = templateBlock.content.trim();
|
|
48
49
|
const allScriptMatches = content.match(/<script\b[^>]*>[\s\S]*?<\/script>/gi) || [];
|
|
49
50
|
let serverScriptContent = "";
|
|
50
51
|
const clientScripts = [];
|
package/dist/process.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { injectCrosswindCSS } from "./dev-server/crosswind.js";
|
|
3
|
-
import { matchHtmlComment, maskAtElementPosition } from "./html-masking.js";
|
|
3
|
+
import { matchHtmlComment, matchStyleElement, maskAtElementPosition, stashScriptElements } from "./html-masking.js";
|
|
4
4
|
import { processA11yDirectives } from "./a11y.js";
|
|
5
5
|
import { generateLifecycleRuntime } from "./composables.js";
|
|
6
6
|
import { processTemplateBindings } from "./reactive-bindings.js";
|
|
@@ -186,6 +186,58 @@ function findScriptBodyEnd(src, from) {
|
|
|
186
186
|
}
|
|
187
187
|
return -1;
|
|
188
188
|
}
|
|
189
|
+
function findScriptBlockByAttribute(html, attribute) {
|
|
190
|
+
const escapedAttribute = attribute.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), openingTag = new RegExp(`<script\\b(?=[^>]*\\b${escapedAttribute}(?:\\s|=|/?>))[^>]*>`, "i").exec(html);
|
|
191
|
+
if (!openingTag)
|
|
192
|
+
return null;
|
|
193
|
+
const bodyStart = openingTag.index + openingTag[0].length, closeStart = html.indexOf("</script>", bodyStart);
|
|
194
|
+
if (closeStart === -1)
|
|
195
|
+
return null;
|
|
196
|
+
const closeEnd = html.indexOf(">", closeStart);
|
|
197
|
+
if (closeEnd === -1)
|
|
198
|
+
return null;
|
|
199
|
+
return {
|
|
200
|
+
start: openingTag.index,
|
|
201
|
+
end: closeEnd + 1
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
function findFirstScriptTag(html) {
|
|
205
|
+
let searchFrom = 0;
|
|
206
|
+
while (searchFrom < html.length) {
|
|
207
|
+
const tagStart = html.indexOf("<", searchFrom);
|
|
208
|
+
if (tagStart === -1)
|
|
209
|
+
return -1;
|
|
210
|
+
if (html.startsWith("<!--", tagStart)) {
|
|
211
|
+
const commentEnd = html.indexOf("-->", tagStart + 4);
|
|
212
|
+
searchFrom = commentEnd === -1 ? html.length : commentEnd + 3;
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
const remainder = html.slice(tagStart), styleTag = /^<style\b[^>]*>/i.exec(remainder);
|
|
216
|
+
if (styleTag) {
|
|
217
|
+
const styleEnd = html.toLowerCase().indexOf("</style>", tagStart + styleTag[0].length);
|
|
218
|
+
searchFrom = styleEnd === -1 ? html.length : styleEnd + 8;
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
if (/^<script\b[^>]*>/i.test(remainder))
|
|
222
|
+
return tagStart;
|
|
223
|
+
searchFrom = tagStart + 1;
|
|
224
|
+
}
|
|
225
|
+
return -1;
|
|
226
|
+
}
|
|
227
|
+
function placeSignalsRuntimeBeforeScripts(html) {
|
|
228
|
+
const runtime = findScriptBlockByAttribute(html, "data-stx-runtime");
|
|
229
|
+
if (!runtime)
|
|
230
|
+
return html;
|
|
231
|
+
const firstScript = findFirstScriptTag(html);
|
|
232
|
+
if (firstScript === -1 || firstScript === runtime.start)
|
|
233
|
+
return html;
|
|
234
|
+
const runtimeTag = html.slice(runtime.start, runtime.end), withoutRuntime = html.slice(0, runtime.start) + html.slice(runtime.end), insertionPoint = findFirstScriptTag(withoutRuntime);
|
|
235
|
+
if (insertionPoint === -1)
|
|
236
|
+
return runtimeTag + `
|
|
237
|
+
` + withoutRuntime;
|
|
238
|
+
return withoutRuntime.slice(0, insertionPoint) + runtimeTag + `
|
|
239
|
+
` + withoutRuntime.slice(insertionPoint);
|
|
240
|
+
}
|
|
189
241
|
export async function processDirectives(template, context, filePath, options, dependencies) {
|
|
190
242
|
if (options.ssr === !1 && !options.autoShell && options.buildMode === "spa")
|
|
191
243
|
return template;
|
|
@@ -238,21 +290,28 @@ export async function processDirectives(template, context, filePath, options, de
|
|
|
238
290
|
result = result.replace(wrongAttrRegex, "$1");
|
|
239
291
|
}
|
|
240
292
|
}
|
|
241
|
-
if (isTopLevel)
|
|
293
|
+
if (isTopLevel) {
|
|
242
294
|
result = injectCloakStyle(result);
|
|
295
|
+
result = placeSignalsRuntimeBeforeScripts(result);
|
|
296
|
+
}
|
|
243
297
|
if (isTopLevel)
|
|
244
298
|
try {
|
|
245
299
|
const { getStoreScript } = await import("./store-loader.js"), resolvedStoresDir = options.storesDir, storeCode = await getStoreScript(resolvedStoresDir);
|
|
246
300
|
if (storeCode) {
|
|
247
|
-
const storeTag = `<script data-stx-stores>${storeCode}</script>`,
|
|
248
|
-
if (
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
301
|
+
const storeTag = `<script data-stx-stores>${storeCode}</script>`, runtime = findScriptBlockByAttribute(result, "data-stx-runtime");
|
|
302
|
+
if (runtime)
|
|
303
|
+
result = result.slice(0, runtime.end) + `
|
|
304
|
+
` + storeTag + result.slice(runtime.end);
|
|
305
|
+
}
|
|
306
|
+
} catch {}
|
|
307
|
+
if (isTopLevel)
|
|
308
|
+
try {
|
|
309
|
+
const { getComposableScript } = await import("./composable-loader.js"), resolvedComposablesDir = options.composablesDir, composableCode = await getComposableScript(resolvedComposablesDir);
|
|
310
|
+
if (composableCode) {
|
|
311
|
+
const composableTag = `<script data-stx-composables>${composableCode}</script>`, anchor = findScriptBlockByAttribute(result, "data-stx-stores") || findScriptBlockByAttribute(result, "data-stx-runtime");
|
|
312
|
+
if (anchor)
|
|
313
|
+
result = result.slice(0, anchor.end) + `
|
|
314
|
+
` + composableTag + result.slice(anchor.end);
|
|
256
315
|
}
|
|
257
316
|
} catch {}
|
|
258
317
|
if (isTopLevel && context.__stx_inject_css !== !1)
|
|
@@ -370,9 +429,11 @@ ${output.trim()}
|
|
|
370
429
|
});
|
|
371
430
|
output = processStackReplacements(output, stacks);
|
|
372
431
|
if (layoutPath) {
|
|
373
|
-
const orphans = [];
|
|
374
|
-
output = output.replace(
|
|
375
|
-
|
|
432
|
+
const orphans = [], scripts = stashScriptElements(output), styles = maskAtElementPosition(scripts.output, matchStyleElement, (_token, index) => `\x00STX_ORPHAN_STYLE_${index}\x00`);
|
|
433
|
+
output = styles.output.replace(/\x00STX_(SCRIPT|ORPHAN_STYLE)_(\d+)\x00/g, (_match, kind, index) => {
|
|
434
|
+
const token = kind === "SCRIPT" ? scripts.scripts[Number(index)] : styles.tokens[Number(index)];
|
|
435
|
+
if (token)
|
|
436
|
+
orphans.push(token);
|
|
376
437
|
return "";
|
|
377
438
|
});
|
|
378
439
|
if (orphans.length > 0) {
|
|
@@ -670,18 +731,17 @@ ${runtimeScript}`);
|
|
|
670
731
|
` + output;
|
|
671
732
|
}
|
|
672
733
|
{
|
|
673
|
-
const
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
const
|
|
678
|
-
if (
|
|
734
|
+
const { scanScriptTags } = await import("./signal-processing.js"), serverRemoveRanges = [];
|
|
735
|
+
for (const script of scanScriptTags(output)) {
|
|
736
|
+
if (!/\bserver\b/i.test(script.attrs))
|
|
737
|
+
continue;
|
|
738
|
+
const bodyStart = script.start + script.fullMatch.indexOf(">") + 1, closeStart = findScriptBodyEnd(output, bodyStart);
|
|
739
|
+
if (closeStart === -1)
|
|
679
740
|
continue;
|
|
680
|
-
let rangeEnd =
|
|
741
|
+
let rangeEnd = closeStart + 9;
|
|
681
742
|
while (rangeEnd < output.length && /\s/.test(output[rangeEnd]))
|
|
682
743
|
rangeEnd++;
|
|
683
|
-
serverRemoveRanges.push({ start:
|
|
684
|
-
serverScriptRe.lastIndex = rangeEnd;
|
|
744
|
+
serverRemoveRanges.push({ start: script.start, end: rangeEnd });
|
|
685
745
|
}
|
|
686
746
|
for (let ri = serverRemoveRanges.length - 1;ri >= 0; ri--) {
|
|
687
747
|
const { start, end } = serverRemoveRanges[ri];
|