@stacksjs/stx 0.2.169 → 0.2.171

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,81 @@
1
+ /**
2
+ * The one piece of state that decides what is reactive to what.
3
+ *
4
+ * stx shipped two independent reactive systems in the same package entry, and
5
+ * the only thing keeping them apart was a variable name: `signals-api.ts` had
6
+ * `activeEffect`, `reactivity.ts` had `currentEffect`, and each recorded its
7
+ * dependencies against its own. Both are the same shape — the nullary function
8
+ * to re-run — so which system a value belonged to was decided entirely by
9
+ * which module happened to declare the variable that was set when it was read.
10
+ *
11
+ * Measured before this module existed, on `import { … } from 'stx'`:
12
+ *
13
+ * reactive() + effect() INERT reactive() + watchEffect() REACTIVE
14
+ * ref() + effect() INERT ref() + watchEffect() REACTIVE
15
+ * state() + effect() REACTIVE state() + watchEffect() INERT
16
+ *
17
+ * Four of six silently dead. Not an error, not a warning — the effect ran once
18
+ * and never again, which reads as "the value never changed". All eight names
19
+ * come from the same entry point with nothing to tell an author which half of
20
+ * the matrix they are standing in (stacksjs/stx#1885).
21
+ *
22
+ * Owning the state here rather than in either module is what makes the two
23
+ * interoperate without one importing the other, which would be a cycle. It is
24
+ * also why this file is small and has no opinions: everything else about a
25
+ * signal — how it stores its value, when it considers itself changed, what it
26
+ * exposes — stays where it was.
27
+ *
28
+ * ## What this does NOT bridge
29
+ *
30
+ * The client runtime in `signals.ts` is generated as a template literal and
31
+ * owns a third copy of this state inside that string. No module can import it,
32
+ * so the runtime is unaffected by this file and stays a separate system. See
33
+ * CLAUDE.md item 40; the parity suites are what hold those two in step.
34
+ *
35
+ * @module reactive-tracking
36
+ */
37
+ /**
38
+ * The effect currently collecting dependencies.
39
+ *
40
+ * A reactive read calls this and, when it is not null, records the result as a
41
+ * subscriber. That is the whole of dependency tracking.
42
+ */
43
+ export declare function getActiveSubscriber(): (() => void) | null;
44
+ /**
45
+ * Make `subscriber` the one collecting dependencies, and return the previous.
46
+ *
47
+ * Returning the previous value rather than requiring the caller to read it
48
+ * first is deliberate: the restore is the step that gets forgotten, and a
49
+ * forgotten restore leaks the inner effect's identity into the outer scope, so
50
+ * every subsequent read subscribes the wrong function. Pair with
51
+ * {@link restoreActiveSubscriber} in a `finally`.
52
+ */
53
+ export declare function setActiveSubscriber(subscriber: () => void): (() => void) | null;
54
+ /** Undo a {@link setActiveSubscriber}, restoring what was listening before. */
55
+ export declare function restoreActiveSubscriber(previous: (() => void) | null): void;
56
+ /** How deeply effects are currently nested. */
57
+ export declare function subscriberDepth(): number;
58
+ /** Whether updates are currently being collected rather than applied. */
59
+ export declare function isBatching(): boolean;
60
+ /**
61
+ * Defer an effect to the end of the current batch.
62
+ *
63
+ * A `Set` rather than an array, so a signal written five times in one batch
64
+ * still runs its subscribers once.
65
+ */
66
+ export declare function enqueueEffect(effect: () => void): void;
67
+ /**
68
+ * Collect updates rather than applying them, and run each affected effect once
69
+ * when the outermost batch ends.
70
+ *
71
+ * Nested calls do not flush early — a `batch` inside a `batch` would otherwise
72
+ * run the outer batch's pending effects against half-applied state.
73
+ */
74
+ export declare function runBatched(fn: () => void): void;
75
+ /**
76
+ * Run `fn` with dependency tracking suspended.
77
+ *
78
+ * Reads inside are not recorded against the enclosing effect, which is what
79
+ * `untrack`/`peek` are for in both modules.
80
+ */
81
+ export declare function withoutTracking<T>(fn: () => T): T;
@@ -0,0 +1,49 @@
1
+ let activeSubscriber = null;
2
+ const subscriberStack = [];
3
+ let batchDepth = 0;
4
+ const pendingEffects = new Set;
5
+ export function getActiveSubscriber() {
6
+ return activeSubscriber;
7
+ }
8
+ export function setActiveSubscriber(subscriber) {
9
+ const previous = activeSubscriber;
10
+ subscriberStack.push(subscriber);
11
+ activeSubscriber = subscriber;
12
+ return previous;
13
+ }
14
+ export function restoreActiveSubscriber(previous) {
15
+ subscriberStack.pop();
16
+ activeSubscriber = previous;
17
+ }
18
+ export function subscriberDepth() {
19
+ return subscriberStack.length;
20
+ }
21
+ export function isBatching() {
22
+ return batchDepth > 0;
23
+ }
24
+ export function enqueueEffect(effect) {
25
+ pendingEffects.add(effect);
26
+ }
27
+ export function runBatched(fn) {
28
+ batchDepth++;
29
+ try {
30
+ fn();
31
+ } finally {
32
+ batchDepth--;
33
+ if (batchDepth === 0) {
34
+ const queued = [...pendingEffects];
35
+ pendingEffects.clear();
36
+ for (const effect of queued)
37
+ effect();
38
+ }
39
+ }
40
+ }
41
+ export function withoutTracking(fn) {
42
+ const previous = activeSubscriber;
43
+ activeSubscriber = null;
44
+ try {
45
+ return fn();
46
+ } finally {
47
+ activeSubscriber = previous;
48
+ }
49
+ }
@@ -1,4 +1,5 @@
1
- let currentInstance = null, currentEffect = null;
1
+ import { enqueueEffect, getActiveSubscriber, isBatching, restoreActiveSubscriber, setActiveSubscriber } from "./reactive-tracking.js";
2
+ let currentInstance = null;
2
3
  function structuralEqual(a, b, seen = new WeakSet) {
3
4
  if (Object.is(a, b))
4
5
  return !0;
@@ -86,8 +87,9 @@ export function ref(initialValue) {
86
87
  let value = initialValue;
87
88
  const id = Symbol("ref"), refObj = {
88
89
  get value() {
89
- if (currentEffect)
90
- effectSubscribers.add(currentEffect);
90
+ const subscriber = getActiveSubscriber();
91
+ if (subscriber)
92
+ effectSubscribers.add(subscriber);
91
93
  return value;
92
94
  },
93
95
  set value(newValue) {
@@ -97,7 +99,10 @@ export function ref(initialValue) {
97
99
  for (const callback of subscribers)
98
100
  callback(newValue, oldValue);
99
101
  for (const effect of effectSubscribers)
100
- effect();
102
+ if (isBatching())
103
+ enqueueEffect(effect);
104
+ else
105
+ effect();
101
106
  }
102
107
  },
103
108
  subscribe(callback) {
@@ -118,8 +123,12 @@ export function reactive(target) {
118
123
  return existing;
119
124
  const subscribers = new Map, notify = (key) => {
120
125
  const subs = subscribers.get(key);
121
- if (subs)
122
- for (const callback of [...subs])
126
+ if (!subs)
127
+ return;
128
+ for (const callback of [...subs])
129
+ if (isBatching())
130
+ enqueueEffect(callback);
131
+ else
123
132
  callback();
124
133
  }, subscribe = (key, callback) => {
125
134
  if (!subscribers.has(key))
@@ -130,9 +139,9 @@ export function reactive(target) {
130
139
  };
131
140
  }, proxy = new Proxy(target, {
132
141
  get(obj, prop, receiver) {
133
- const value = Reflect.get(obj, prop, receiver);
134
- if (currentEffect)
135
- subscribe(prop, currentEffect);
142
+ const value = Reflect.get(obj, prop, receiver), subscriber = getActiveSubscriber();
143
+ if (subscriber)
144
+ subscribe(prop, subscriber);
136
145
  if (value !== null && typeof value === "object" && !Array.isArray(value)) {
137
146
  const proto = Object.getPrototypeOf(value);
138
147
  if (proto === Object.prototype || proto === null)
@@ -162,10 +171,9 @@ export function computed(getter) {
162
171
  update();
163
172
  }, update = () => {
164
173
  if (dirty) {
165
- const oldValue = cachedValue, prevEffect = currentEffect;
166
- currentEffect = markDirty;
174
+ const oldValue = cachedValue, prevEffect = setActiveSubscriber(markDirty);
167
175
  cachedValue = getter();
168
- currentEffect = prevEffect;
176
+ restoreActiveSubscriber(prevEffect);
169
177
  dirty = !1;
170
178
  if (!Object.is(cachedValue, oldValue)) {
171
179
  for (const callback of subscribers)
@@ -174,15 +182,15 @@ export function computed(getter) {
174
182
  effect();
175
183
  }
176
184
  }
177
- }, prevEffect = currentEffect;
178
- currentEffect = markDirty;
185
+ }, prevEffect = setActiveSubscriber(markDirty);
179
186
  cachedValue = getter();
180
187
  dirty = !1;
181
- currentEffect = prevEffect;
188
+ restoreActiveSubscriber(prevEffect);
182
189
  return {
183
190
  get value() {
184
- if (currentEffect)
185
- effectSubscribers.add(currentEffect);
191
+ const subscriber = getActiveSubscriber();
192
+ if (subscriber)
193
+ effectSubscribers.add(subscriber);
186
194
  update();
187
195
  return cachedValue;
188
196
  },
@@ -243,12 +251,11 @@ export function watch(source, callback, options = {}) {
243
251
  });
244
252
  }, getValueTracking = () => {
245
253
  if (typeof source === "function") {
246
- const prevEffect = currentEffect;
247
- currentEffect = onChange;
254
+ const prevEffect = setActiveSubscriber(onChange);
248
255
  try {
249
256
  return source();
250
257
  } finally {
251
- currentEffect = prevEffect;
258
+ restoreActiveSubscriber(prevEffect);
252
259
  }
253
260
  }
254
261
  return source.value;
@@ -299,10 +306,9 @@ export function watchEffect(effect) {
299
306
  return;
300
307
  if (cleanup)
301
308
  cleanup();
302
- const prevEffect = currentEffect;
303
- currentEffect = run;
309
+ const prevEffect = setActiveSubscriber(run);
304
310
  cleanup = effect();
305
- currentEffect = prevEffect;
311
+ restoreActiveSubscriber(prevEffect);
306
312
  };
307
313
  run();
308
314
  if (currentInstance)
@@ -76,51 +76,37 @@ export function outputNeedsSignalsRuntime(html) {
76
76
  }
77
77
  return !1;
78
78
  }
79
- export async function injectSignalsRuntime(template, options) {
80
- if (template.includes("data-stx-runtime") || template.includes("window.stx.state") || template.includes("STX Signals Runtime"))
81
- return template;
82
- if (options.buildMode === "serve") {
83
- const runtimeScript = '<script data-stx-runtime src="/_stx/runtime.js"></script>', firstScriptInDoc = template.indexOf("<script");
84
- if (firstScriptInDoc !== -1)
85
- return template.slice(0, firstScriptInDoc) + runtimeScript + `
86
- ` + template.slice(firstScriptInDoc);
87
- if (template.includes("</head>")) {
88
- const idx = template.indexOf("</head>");
89
- return template.slice(0, idx) + runtimeScript + `
90
- ` + template.slice(idx);
91
- }
92
- return runtimeScript + `
93
- ` + template;
94
- }
95
- if (options.buildMode === "compile") {
96
- const runtimeScript = '<script data-stx-runtime src="/__stx/runtime.__STX_HASH__.js"></script>', firstScriptInDoc = template.indexOf("<script");
97
- if (firstScriptInDoc !== -1)
98
- return template.slice(0, firstScriptInDoc) + runtimeScript + `
99
- ` + template.slice(firstScriptInDoc);
100
- if (template.includes("</head>")) {
101
- const idx = template.indexOf("</head>");
102
- return template.slice(0, idx) + runtimeScript + `
103
- ` + template.slice(idx);
104
- }
105
- return runtimeScript + `
106
- ` + template;
79
+ function placeRuntimeTag(template, runtimeScript) {
80
+ const firstScript = template.indexOf("<script"), doctype = template.search(/<!doctype\b/i);
81
+ if (firstScript !== -1 && (doctype === -1 || firstScript > doctype))
82
+ return template.slice(0, firstScript) + runtimeScript + `
83
+ ` + template.slice(firstScript);
84
+ const headOpen = /<head\b[^>]*>/i.exec(template);
85
+ if (headOpen) {
86
+ const at = headOpen.index + headOpen[0].length;
87
+ return `${template.slice(0, at)}
88
+ ${runtimeScript}${template.slice(at)}`;
107
89
  }
108
- const { getCachedSignalsRuntime } = await import("./caching.js"), runtimeScript = `<script data-stx-scoped data-stx-runtime>${await getCachedSignalsRuntime(options.debug)}</script>`, firstScriptInDoc = template.indexOf("<script");
109
- if (firstScriptInDoc !== -1)
110
- return template.slice(0, firstScriptInDoc) + runtimeScript + `
111
- ` + template.slice(firstScriptInDoc);
112
90
  if (template.includes("</head>")) {
113
91
  const idx = template.indexOf("</head>");
114
92
  return template.slice(0, idx) + runtimeScript + `
115
93
  ` + template.slice(idx);
116
94
  }
117
- if (template.includes("<body")) {
118
- const idx = template.indexOf("<body"), closeIdx = template.indexOf(">", idx);
119
- return template.slice(0, closeIdx + 1) + `
120
- ` + runtimeScript + template.slice(closeIdx + 1);
121
- }
122
- return runtimeScript + `
123
- ` + template;
95
+ if (findBodyOpenTag(template))
96
+ return replaceBodyOpenTag(template, (tag) => `${tag}
97
+ ${runtimeScript}`);
98
+ return `${runtimeScript}
99
+ ${template}`;
100
+ }
101
+ export async function injectSignalsRuntime(template, options) {
102
+ if (template.includes("data-stx-runtime") || template.includes("window.stx.state") || template.includes("STX Signals Runtime"))
103
+ return template;
104
+ if (options.buildMode === "serve")
105
+ return placeRuntimeTag(template, '<script data-stx-runtime src="/_stx/runtime.js"></script>');
106
+ if (options.buildMode === "compile")
107
+ return placeRuntimeTag(template, '<script data-stx-runtime src="/__stx/runtime.__STX_HASH__.js"></script>');
108
+ const { getCachedSignalsRuntime } = await import("./caching.js"), runtime = await getCachedSignalsRuntime(options.debug);
109
+ return placeRuntimeTag(template, `<script data-stx-scoped data-stx-runtime>${runtime}</script>`);
124
110
  }
125
111
  export async function injectRouterScript(template, options) {
126
112
  if (!template.includes("</body>"))
package/dist/serve.js CHANGED
@@ -5,6 +5,7 @@ import { findSfcTemplateBlock } from "./sfc-template.js";
5
5
  import { readMarkdownFile } from "./assets.js";
6
6
  import { processDirectives } from "./process.js";
7
7
  import { extractVariables } from "./utils.js";
8
+ import { compressResponse } from "./compression.js";
8
9
  export function htmlResponse(html, status = 200, headers = {}) {
9
10
  return new Response(html, {
10
11
  status,
@@ -157,7 +158,7 @@ ${error instanceof Error ? error.stack : ""}</pre>` : "<h1>Internal Server Error
157
158
  }
158
159
  const server = bunServe({
159
160
  port,
160
- fetch: handleRequest
161
+ fetch: async (request) => compressResponse(request, await handleRequest(request))
161
162
  });
162
163
  let watcher = null;
163
164
  if (watch)
@@ -43,6 +43,24 @@
43
43
  *
44
44
  * @module signals-api
45
45
  */
46
+ /**
47
+ * ## The canonical set (stacksjs/stx#1885 asks 3 and 4)
48
+ *
49
+ * `state` / `derived` / `effect`, exported here, are the primitives to reach
50
+ * for. They are what the client runtime puts on `window.stx`, what a
51
+ * `<script client>` block sees as bare identifiers, and what stx's own
52
+ * composables and stores are written against — the only set that reads the
53
+ * same in a module, in a component, and in a template.
54
+ *
55
+ * `reactivity.ts` exports a Vue-shaped family beside them — `ref`, `reactive`,
56
+ * `computed`, `watch`, `watchEffect` — from the same package entry. Both are
57
+ * backed by one dependency tracker now (`reactive-tracking.ts`), so mixing
58
+ * them works; four of the six pairings used to be silently inert. They are
59
+ * kept for code ported from Vue, not recommended for new code.
60
+ *
61
+ * Note `ref()` returns a `.value` object while `state()` returns a callable
62
+ * signal, so the two are not interchangeable at the call site.
63
+ */
46
64
  /**
47
65
  * A reactive state signal.
48
66
  *
@@ -1,12 +1,19 @@
1
- let activeEffect = null;
2
- const effectStack = [];
3
- let isBatching = !1;
4
- const pendingEffects = new Set, mountCallbacks = [], destroyCallbacks = [];
1
+ import {
2
+ enqueueEffect,
3
+ getActiveSubscriber,
4
+ isBatching,
5
+ restoreActiveSubscriber,
6
+ runBatched,
7
+ setActiveSubscriber,
8
+ withoutTracking
9
+ } from "./reactive-tracking.js";
10
+ const mountCallbacks = [], destroyCallbacks = [];
5
11
  export function state(initialValue) {
6
12
  let value = initialValue;
7
13
  const subscribers = new Set, effects = new Set, signal = () => {
8
- if (activeEffect)
9
- effects.add(activeEffect);
14
+ const subscriber = getActiveSubscriber();
15
+ if (subscriber)
16
+ effects.add(subscriber);
10
17
  return value;
11
18
  };
12
19
  signal.set = (newValue) => {
@@ -14,8 +21,8 @@ export function state(initialValue) {
14
21
  const prev = value;
15
22
  value = newValue;
16
23
  subscribers.forEach((cb) => cb(value, prev));
17
- if (isBatching)
18
- effects.forEach((effect) => pendingEffects.add(effect));
24
+ if (isBatching())
25
+ effects.forEach((effect) => enqueueEffect(effect));
19
26
  else
20
27
  effects.forEach((effect) => effect());
21
28
  }
@@ -40,23 +47,21 @@ export function derived(compute) {
40
47
  const effects = new Set, markDirty = () => {
41
48
  if (!isDirty) {
42
49
  isDirty = !0;
43
- if (isBatching)
44
- effects.forEach((effect) => pendingEffects.add(effect));
50
+ if (isBatching())
51
+ effects.forEach((effect) => enqueueEffect(effect));
45
52
  else
46
53
  effects.forEach((effect) => effect());
47
54
  }
48
55
  }, signal = () => {
49
- if (activeEffect)
50
- effects.add(activeEffect);
56
+ const subscriber = getActiveSubscriber();
57
+ if (subscriber)
58
+ effects.add(subscriber);
51
59
  if (isDirty) {
52
- const prevEffect = activeEffect;
53
- activeEffect = markDirty;
54
- effectStack.push(markDirty);
60
+ const prevEffect = setActiveSubscriber(markDirty);
55
61
  try {
56
62
  cachedValue = compute();
57
63
  } finally {
58
- effectStack.pop();
59
- activeEffect = prevEffect;
64
+ restoreActiveSubscriber(prevEffect);
60
65
  }
61
66
  isDirty = !1;
62
67
  }
@@ -78,15 +83,12 @@ export function effect(fn, options = {}) {
78
83
  cleanup();
79
84
  cleanup = void 0;
80
85
  }
81
- const prevEffect = activeEffect;
82
- activeEffect = runEffect;
83
- effectStack.push(runEffect);
86
+ const prevEffect = setActiveSubscriber(runEffect);
84
87
  try {
85
88
  const result = fn();
86
89
  cleanup = typeof result === "function" ? result : void 0;
87
90
  } finally {
88
- effectStack.pop();
89
- activeEffect = prevEffect;
91
+ restoreActiveSubscriber(prevEffect);
90
92
  }
91
93
  };
92
94
  if (options.immediate !== !1)
@@ -98,18 +100,7 @@ export function effect(fn, options = {}) {
98
100
  };
99
101
  }
100
102
  export function batch(fn) {
101
- if (isBatching) {
102
- fn();
103
- return;
104
- }
105
- isBatching = !0;
106
- try {
107
- fn();
108
- } finally {
109
- isBatching = !1;
110
- pendingEffects.forEach((effect) => effect());
111
- pendingEffects.clear();
112
- }
103
+ runBatched(fn);
113
104
  }
114
105
  export function onMount(callback) {
115
106
  mountCallbacks.push(callback);
@@ -149,13 +140,7 @@ export function untrack(value) {
149
140
  return value;
150
141
  }
151
142
  export function peek(fn) {
152
- const prevEffect = activeEffect;
153
- activeEffect = null;
154
- try {
155
- return fn();
156
- } finally {
157
- activeEffect = prevEffect;
158
- }
143
+ return withoutTracking(fn);
159
144
  }
160
145
  export function useOptimistic(base, reducer) {
161
146
  const readBase = typeof base === "function" ? base : () => base, pending = state([]), optimistic = derived(() => pending().reduce((acc, entry) => reducer(acc, entry.action), readBase()));
package/dist/ssg.d.ts CHANGED
@@ -257,6 +257,14 @@ export interface CachedPage {
257
257
  etag: string;
258
258
  headers?: Record<string, string>;
259
259
  }
260
+ /**
261
+ * Whether a page declares an `action` — the export that receives its own POST.
262
+ *
263
+ * Exported so the rule is testable without running a build. A page action
264
+ * cannot run in a static build: `action` receives a POST, and a static build
265
+ * produces files, so there is no server left to receive one (stacksjs/stx#1847).
266
+ */
267
+ export declare function declaresPageAction(source: string): boolean;
260
268
  /**
261
269
  * Generate sitemap.xml
262
270
  */