@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.
@@ -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>>;
@@ -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 templateTagMatch = content.match(/<template\b(?![^>]*(?:\b(?:id|x-for|x-if|@for|@if|:for|:if)\s*=|\s#[\w-]|\bv-slot|\bslot\s*=))[^>]*>([\s\S]*?)<\/template>/i);
46
- if (templateTagMatch)
47
- workingContent = templateTagMatch[1].trim();
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>`, runtimeStart = result.indexOf("<script data-stx-scoped>");
248
- if (runtimeStart !== -1) {
249
- const runtimeClose = result.indexOf("</script>", runtimeStart + 24);
250
- if (runtimeClose !== -1) {
251
- const insertPos = runtimeClose + 9;
252
- result = result.slice(0, insertPos) + `
253
- ` + storeTag + result.slice(insertPos);
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(/<(script|style)\b[\s\S]*?<\/\1>/gi, (m) => {
375
- orphans.push(m);
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 serverScriptRe = /<script\s+server\b[^>]*>/gi;
674
- let serverMatch;
675
- const serverRemoveRanges = [];
676
- while ((serverMatch = serverScriptRe.exec(output)) !== null) {
677
- const tagEnd = serverMatch.index + serverMatch[0].length, closeIdx = findScriptBodyEnd(output, tagEnd);
678
- if (closeIdx === -1)
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 = closeIdx + 9;
741
+ let rangeEnd = closeStart + 9;
681
742
  while (rangeEnd < output.length && /\s/.test(output[rangeEnd]))
682
743
  rangeEnd++;
683
- serverRemoveRanges.push({ start: serverMatch.index, end: rangeEnd });
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];