@stacksjs/stx 0.2.97 → 0.2.99

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.
@@ -197,6 +197,17 @@ const serialize = (value, type) => {
197
197
  return String(value);
198
198
  };
199
199
 
200
+ const cloneDefault = (value, type) => {
201
+ if (value == null || (type !== 'object' && type !== 'array')) return value;
202
+ if (typeof structuredClone === 'function') return structuredClone(value);
203
+ return JSON.parse(JSON.stringify(value));
204
+ };
205
+
206
+ const defaultFor = property => cloneDefault(
207
+ property.default ?? ({ string: '', number: 0, boolean: false, object: {}, array: [] })[property.type],
208
+ property.type,
209
+ );
210
+
200
211
  export class StxElement extends HTMLElementBase {
201
212
  static definition = {};
202
213
  static get observedAttributes() { return this.definition.observedAttributes || []; }
@@ -206,11 +217,13 @@ export class StxElement extends HTMLElementBase {
206
217
  this._values = Object.create(null);
207
218
  this._reflecting = false;
208
219
  this._connected = false;
220
+ this._hasConnected = false;
221
+ this._initializing = false;
209
222
  this._updatePending = false;
210
223
  this._listeners = new Map();
211
224
  const definition = this.constructor.definition;
212
225
  for (const [name, property] of Object.entries(definition.properties || {})) {
213
- this._values[name] = property.default ?? ({ string: '', number: 0, boolean: false, object: {}, array: [] })[property.type];
226
+ this._values[name] = defaultFor(property);
214
227
  }
215
228
  if (definition.shadowMode && this.attachShadow && !this.shadowRoot) {
216
229
  this._closedRoot = this.attachShadow({ mode: definition.shadowMode });
@@ -223,19 +236,28 @@ export class StxElement extends HTMLElementBase {
223
236
  connectedCallback() {
224
237
  if (this._connected) return;
225
238
  this._connected = true;
239
+ this._initializing = true;
226
240
  const definition = this.constructor.definition;
227
- for (const name of Object.keys(definition.properties || {})) this._upgradeProperty(name);
228
241
  for (const [name, property] of Object.entries(definition.properties || {})) {
229
242
  const attribute = property.attribute === false ? null : (property.attribute || name.replace(/[A-Z]/g, letter => '-' + letter.toLowerCase()));
230
- if (attribute && this.hasAttribute?.(attribute)) this._values[name] = deserialize(this.getAttribute(attribute), property.type, this._values[name]);
243
+ if (attribute && this.hasAttribute?.(attribute)) this._values[name] = deserialize(this.getAttribute(attribute), property.type, defaultFor(property));
231
244
  }
245
+ // Properties assigned before customElements.define() become own properties.
246
+ // Upgrade them after attributes so imperative state wins, matching the
247
+ // platform's property-first custom-element initialization convention.
248
+ for (const name of Object.keys(definition.properties || {})) this._upgradeProperty(name);
249
+ this._initializing = false;
232
250
  const root = this.renderRoot;
233
251
  const hasMarkup = [...(root.childNodes || [])].some(node => node.nodeType !== 3 || node.textContent.trim());
234
- if (!(definition.progressive && hasMarkup)) this._render();
252
+ const firstConnection = !this._hasConnected;
253
+ if (!(firstConnection && definition.progressive && hasMarkup)) this._render();
235
254
  else this._upgradeDeclarativeEvents();
236
255
  this._bindEvents();
237
- this.setAttribute?.('hydrated', '');
238
- this.dispatchEvent?.(new CustomEvent('stx:hydrated', { bubbles: true, composed: true }));
256
+ if (firstConnection) {
257
+ this._hasConnected = true;
258
+ this.setAttribute?.('hydrated', '');
259
+ this.dispatchEvent?.(new CustomEvent('stx:hydrated', { bubbles: true, composed: true }));
260
+ }
239
261
  }
240
262
 
241
263
  disconnectedCallback() {
@@ -250,7 +272,7 @@ export class StxElement extends HTMLElementBase {
250
272
  const name = definition.attributeToProperty?.[attribute];
251
273
  if (!name) return;
252
274
  const property = definition.properties[name];
253
- const value = deserialize(newValue, property.type, property.default);
275
+ const value = deserialize(newValue, property.type, defaultFor(property));
254
276
  if (!Object.is(this._values[name], value)) {
255
277
  this._values[name] = value;
256
278
  this.requestUpdate();
@@ -258,7 +280,7 @@ export class StxElement extends HTMLElementBase {
258
280
  }
259
281
 
260
282
  requestUpdate() {
261
- if (!this._connected || this._updatePending) return this.updateComplete;
283
+ if (!this._connected || this._initializing || this._updatePending) return this.updateComplete;
262
284
  this._updatePending = true;
263
285
  this._updateComplete = new Promise((resolve) => {
264
286
  queueMicrotask(() => {
@@ -15,8 +15,8 @@ export declare function resetCrosswindCache(): void;
15
15
  * `stx.config.ts` and other stacks configs — a single source of truth for
16
16
  * config loading across the stack.
17
17
  *
18
- * Pre-check: we only hand off to bunfig once we've confirmed a
19
- * `crosswind.config.*` file actually exists in the target directory. When
18
+ * Pre-check: bunfig's lightweight discovery entry point confirms a matching
19
+ * project-local config exists before we load its full configuration pipeline. When
20
20
  * stx is run from a parent repo root (e.g. running an example from the
21
21
  * monorepo root, or a page rendered before the app dir is known) bunfig's
22
22
  * built-in search would span 250+ paths up the tree and, on miss, log a
@@ -25,9 +25,9 @@ export declare function resetCrosswindCache(): void;
25
25
  */
26
26
  /**
27
27
  * Load the user's crosswind config via bunfig — our shared config loader.
28
- * bunfig's name-based search already covers `config/crosswind.ts`,
29
- * `.config/crosswind.ts`, and root-level `crosswind.config.{ts,js,mjs,cjs}`,
30
- * so we don't need to maintain a parallel list of paths here. Returns `null`
28
+ * `hasLocalConfig` uses the same filename, extension, alias, custom-directory,
29
+ * and resolution-priority rules as bunfig's loader, so this integration never
30
+ * maintains a parallel list of paths. Returns `null`
31
31
  * when no config file is present, so callers can fall through to defaults
32
32
  * without surfacing a "not found" warning to the user.
33
33
  */
@@ -1,5 +1,5 @@
1
- import fs from "node:fs";
2
1
  import path from "node:path";
2
+ import { hasLocalConfig } from "bunfig/discovery";
3
3
  import { colors } from "./terminal-colors.js";
4
4
  let crosswindModule = null, crosswindLoadAttempted = !1, cachedConfig = null, cachedCSS = "", isBuilding = !1;
5
5
  const MAX_CACHE = 256, cssByClassSet = new Map;
@@ -126,8 +126,7 @@ export function resetCrosswindCache() {
126
126
  }
127
127
  export async function loadCrosswindConfig(cwd) {
128
128
  try {
129
- const configDirectories = ["", "config", ".config"], configNames = ["crosswind.config", "crosswind", ".crosswind.config", ".crosswind"], configExtensions = [".ts", ".js", ".mjs", ".cjs", ".json"];
130
- if (!configDirectories.flatMap((directory) => configNames.flatMap((name) => configExtensions.map((extension) => path.join(directory, `${name}${extension}`)))).some((candidate) => fs.existsSync(path.join(cwd, candidate))))
129
+ if (!hasLocalConfig({ name: "crosswind", cwd }))
131
130
  return null;
132
131
  const { loadConfigWithResult } = await import("bunfig"), result = await loadConfigWithResult({
133
132
  name: "crosswind",