@ailuracode/alpine-child 0.1.0 → 2.0.0

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/README.md CHANGED
@@ -5,24 +5,24 @@ Alpine.js directive for **asChild-style** composition: transfer attributes, clas
5
5
  ## Install
6
6
 
7
7
  ```bash
8
- npm install @ailuracode/alpine-child @alpinejs/morph alpinejs
8
+ pnpm add @ailuracode/alpine-child @ailuracode/alpine-core @alpinejs/morph alpinejs
9
9
  ```
10
10
 
11
- ## Setup
11
+ ## Quick start
12
12
 
13
- ```js
13
+ ```ts
14
14
  import Alpine from "alpinejs";
15
15
  import morph from "@alpinejs/morph";
16
- import child from "@ailuracode/alpine-child";
16
+ import { childPlugin } from "@ailuracode/alpine-child";
17
17
 
18
18
  Alpine.plugin(morph);
19
- Alpine.plugin(child);
19
+ Alpine.plugin(childPlugin());
20
20
  Alpine.start();
21
21
  ```
22
22
 
23
23
  Unwrapping uses [`Alpine.morph()`](https://alpinejs.dev/plugins/morph) — register Morph before `x-child`.
24
24
 
25
- ## Usage
25
+ ## Quick start
26
26
 
27
27
  ```html
28
28
  <span
@@ -89,7 +89,7 @@ Programmatic listeners attached to the wrapper at runtime are **not** transferre
89
89
 
90
90
  ## Blade components
91
91
 
92
- ```blade
92
+ ```html
93
93
  {{-- resources/views/components/button.blade.php --}}
94
94
  <span
95
95
  x-child
@@ -99,7 +99,7 @@ Programmatic listeners attached to the wrapper at runtime are **not** transferre
99
99
  </span>
100
100
  ```
101
101
 
102
- ```blade
102
+ ```html
103
103
  <x-button>
104
104
  <a href="{{ route('docs') }}">Docs</a>
105
105
  </x-button>
@@ -126,6 +126,16 @@ This package registers a single directive:
126
126
 
127
127
  No stores or magics are added.
128
128
 
129
+ ### Avoiding name collisions
130
+
131
+ If your application already owns an `x-child` directive — or another toolkit plugin registers on that name — rename the integration surface without touching the unwrap pass:
132
+
133
+ ```ts
134
+ Alpine.plugin(childPlugin({ directiveKey: "unwrap" })); // → x-unwrap
135
+ ```
136
+
137
+ The exposed constant `DEFAULT_CHILD_DIRECTIVE_KEY` keeps the rename discoverable from TypeScript.
138
+
129
139
  ## License
130
140
 
131
141
  MIT
package/dist/global.d.ts CHANGED
@@ -1,9 +1,31 @@
1
- /// <reference types="@types/alpinejs" />
1
+ /**
2
+ * Ambient type surface for `@ailuracode/alpine-child`.
3
+ *
4
+ * Re-exports the public type surface so consumers can include this
5
+ * file via the triple-slash directive and typecheck `x-child`
6
+ * references without pulling the runtime entrypoint.
7
+ *
8
+ * Per core's `global.d.ts` convention, this package does NOT augment
9
+ * external modules — the `x-child` directive is identified by its
10
+ * attribute name, not by an `Alpine.*` namespace entry. Consumers that
11
+ * need typed access to the unwrap machinery should import the types
12
+ * from `@ailuracode/alpine-child` directly:
13
+ *
14
+ * ```ts
15
+ * import type {
16
+ * ChildDirectiveConfig,
17
+ * ChildMergeMode,
18
+ * ChildPluginOptions,
19
+ * } from "@ailuracode/alpine-child";
20
+ * ```
21
+ */
2
22
 
3
- export type ChildMergeMode = "default" | "merge" | "replace";
23
+ /// <reference types="@types/alpinejs" />
4
24
 
5
- declare global {
6
- namespace Alpine {
7
- // Directive-only plugin — no magics or stores.
8
- }
9
- }
25
+ export type {
26
+ ChildDirectiveConfig,
27
+ ChildMergeMode,
28
+ ChildMorphOptions,
29
+ ChildPluginCallback,
30
+ ChildPluginOptions,
31
+ } from "./types.js";
package/dist/index.d.ts CHANGED
@@ -1,20 +1,170 @@
1
- import AlpineType from 'alpinejs';
1
+ import { Alpine, PluginCallback } from '@ailuracode/alpine-core';
2
+ import { Alpine as Alpine$1 } from 'alpinejs';
2
3
 
4
+ /**
5
+ * Public type contracts for `@ailuracode/alpine-child`.
6
+ *
7
+ * Per `.cursor/rules/formatting.mdc`, every public type
8
+ * lives in a `types.ts` module so consumers can import them without pulling
9
+ * the implementation. The shape IS the contract — renaming a field or
10
+ * changing a variant is a breaking change.
11
+ *
12
+ * The directive is intentionally minimal: one merge mode per element,
13
+ * no events, no state. The types here document the entire public surface
14
+ * besides the runtime functions in `./controller` and the Alpine glue
15
+ * in `./plugin`.
16
+ */
17
+
18
+ /**
19
+ * How wrapper attributes interact with the first element child during the
20
+ * unwrap pass.
21
+ *
22
+ * - `'default'` — `class` and `style` merge token/property lists with
23
+ * child tokens winning on conflict; other attributes copy only when
24
+ * missing on the child. Existing child `id`, `aria-*`, and `data-*`
25
+ * attributes always win.
26
+ * - `'merge'` — explicit alias for `'default'`. Surfaced via the
27
+ * `.merge` modifier; reserved as a distinct variant so future
28
+ * semantics (e.g. deep attribute merging) can branch on it without
29
+ * breaking the modifier contract.
30
+ * - `'replace'` — wrapper values overwrite child values on conflict.
31
+ * `class` and `style` still merge token/property lists.
32
+ */
3
33
  type ChildMergeMode = "default" | "merge" | "replace";
34
+ /**
35
+ * Parsed `x-child` directive configuration extracted from the wrapper's
36
+ * attributes. Produced by `parseChildDirective` in `./controller`.
37
+ */
38
+ interface ChildDirectiveConfig {
39
+ readonly mode: ChildMergeMode;
40
+ }
41
+ /**
42
+ * Options accepted by {@link childPlugin}. The directive currently has
43
+ * no runtime knobs beyond what the modifiers express, so the options
44
+ * surface is empty by design — reserved as the public seam for future
45
+ * cross-cutting configuration (e.g. warning sinks, scoped mutation
46
+ * observers, debug logging).
47
+ */
48
+ interface ChildPluginOptions {
49
+ readonly id?: string;
50
+ /**
51
+ * `x-child` directive key the Alpine plugin registers under. Defaults
52
+ * to {@link DEFAULT_CHILD_DIRECTIVE_KEY}. Set when the host already
53
+ * owns a `child` directive or another toolkit plugin would collide
54
+ * on that name — the rename avoids the collision without touching
55
+ * the unwrap pass or attribute transfer helpers.
56
+ */
57
+ readonly directiveKey?: string;
58
+ }
59
+ /** Default `x-child` directive key registered by {@link childPlugin}. */
60
+ declare const DEFAULT_CHILD_DIRECTIVE_KEY = "child";
61
+ /**
62
+ * Typed view of `Alpine` the child plugin uses internally.
63
+ *
64
+ * Adds the `morph(el, html, options)` helper from `@alpinejs/morph` on
65
+ * top of the toolkit's {@link Alpine} generic. The morph function is
66
+ * only present when consumers register `Alpine.plugin(morph)` before
67
+ * `childPlugin(...)`; the plugin runtime-checks its presence and warns
68
+ * otherwise. The toolkit's `Alpine<Stores>` only ADDS overloads, so a
69
+ * real `Alpine` runtime is assignable to this view without a cast.
70
+ */
71
+ type ChildAlpine = Alpine<Record<string, never>> & {
72
+ /**
73
+ * Forwarded from `@alpinejs/morph`. The plugin reads this property at
74
+ * runtime and falls back with a warning when it is absent.
75
+ */
76
+ morph?(el: Element, newHtml: string | Element, options?: ChildMorphOptions): Element;
77
+ };
78
+ /** Subset of `Alpine.morph` options the plugin reads. */
79
+ interface ChildMorphOptions {
80
+ readonly added?: (node: Node) => void;
81
+ }
82
+ /**
83
+ * `Alpine.plugin()` callback signature.
84
+ *
85
+ * Typed against the base `Alpine` via the toolkit's `PluginCallback`
86
+ * generic so `childPlugin(...)` drops straight into `Alpine.plugin(...)`
87
+ * without a cast.
88
+ */
89
+ type ChildPluginCallback = PluginCallback<Alpine$1>;
90
+
91
+ /**
92
+ * Internal merge helpers for `@ailuracode/alpine-child`.
93
+ *
94
+ * Pure DOM utilities used by `controller.ts` to apply the wrapper → child
95
+ * attribute transfer. Lives under `internal/` so the public surface stays
96
+ * small — every helper here is consumed by the controller facade.
97
+ *
98
+ * Construction rules (per
99
+ * `.cursor/rules/new-package.mdc`):
100
+ *
101
+ * - No `window` / `document` access — these run on whatever `Element`
102
+ * instances the controller hands them.
103
+ * - No Alpine APIs, no scope reads, no event subscriptions.
104
+ *
105
+ * The merge rules are documented in the package README under
106
+ * "Attribute rules" — keep this file in sync when the public rules
107
+ * change.
108
+ */
109
+
4
110
  /** Returns the first element child, skipping text and comment nodes. */
5
111
  declare function findFirstElementChild(parent: Element): Element | null;
6
112
  /** Counts element children (excluding text/comment nodes). */
7
113
  declare function countElementChildren(parent: Element): number;
8
- /** Parses `x-child` directive modifiers from element attributes. */
9
- declare function parseChildDirective(el: Element): {
10
- mode: ChildMergeMode;
11
- } | null;
12
- /** Transfers wrapper attributes onto the first element child. */
13
- declare function transferAttributes(wrapper: Element, child: Element, mode?: ChildMergeMode): void;
14
- /** Removes transferable attributes from the wrapper after unwrapping. */
114
+ /**
115
+ * Parses `x-child` directive modifiers from element attributes.
116
+ *
117
+ * Returns `null` when the directive is absent, so the plugin can skip
118
+ * elements without `x-child` quickly without extra branching. Returns a
119
+ * config with `mode: 'default'` when the attribute is present without
120
+ * any modifier.
121
+ */
122
+ declare function parseChildDirective(el: Element): ChildDirectiveConfig | null;
123
+ /**
124
+ * Applies the wrapper attributes onto the child following the active
125
+ * {@link ChildMergeMode}.
126
+ *
127
+ * Public from the controller's perspective — `internal/` re-export
128
+ * is internal-by-convention, not strict.
129
+ */
130
+ declare function transferAttributes(wrapper: Element, child: Element, mode: ChildMergeMode): void;
131
+ /**
132
+ * Removes every attribute from the wrapper.
133
+ *
134
+ * After `Alpine.morph()` swaps the wrapper out of the document, this
135
+ * is mostly defensive — the wrapper is already detached. Kept exported
136
+ * so tests can verify the scrub step in isolation, and so consumers
137
+ * that wire `x-child` into a custom Alpine adapter can run the same
138
+ * cleanup themselves.
139
+ */
15
140
  declare function clearTransferredAttributes(wrapper: Element): void;
16
141
 
17
- /** Alpine.js plugin. Registers the `x-child` directive (asChild-style unwrapping). */
18
- declare function childPlugin(Alpine: AlpineType.Alpine): void;
142
+ /**
143
+ * Alpine.js integration for `@ailuracode/alpine-child`.
144
+ *
145
+ * Thin adapter that registers the `x-child` directive. The unwrap pass
146
+ * runs in `Alpine.interceptInit` so the wrapper is hidden before its
147
+ * own descendants init, and the morph swap is deferred to the next
148
+ * tick so sibling components (e.g. `x-show` on a sidebar) init first.
149
+ *
150
+ * Two ways to consume the package:
151
+ *
152
+ * 1. Standalone — import any of the helpers in `./controller` directly
153
+ * for tests, custom directives, or non-Alpine adapters.
154
+ * 2. Alpine — `childPlugin({ id? })` returns the `Alpine.plugin()`
155
+ * callback that registers the directive.
156
+ *
157
+ * The framework-agnostic logic lives in `./controller` and
158
+ * `./internal/transfer`. This file is the only place that imports
159
+ * Alpine APIs.
160
+ */
161
+
162
+ /**
163
+ * Plugin factory — returns the `Alpine.plugin()` callback. Pass
164
+ * {@link ChildPluginOptions} to configure future cross-cutting knobs,
165
+ * or `{}` for the package defaults. See `AGENTS.md` for the
166
+ * integration contract.
167
+ */
168
+ declare function childPlugin(options?: ChildPluginOptions): ChildPluginCallback;
19
169
 
20
- export { type ChildMergeMode, clearTransferredAttributes, countElementChildren, childPlugin as default, findFirstElementChild, parseChildDirective, transferAttributes };
170
+ export { type ChildAlpine, type ChildDirectiveConfig, type ChildMergeMode, type ChildMorphOptions, type ChildPluginCallback, type ChildPluginOptions, DEFAULT_CHILD_DIRECTIVE_KEY, childPlugin, clearTransferredAttributes, countElementChildren, childPlugin as default, findFirstElementChild, parseChildDirective, transferAttributes };
package/dist/index.js CHANGED
@@ -1,232 +1 @@
1
- // src/transfer.ts
2
- var CHILD_DIRECTIVE_PREFIX = "x-child";
3
- var NEVER_TRANSFER = /* @__PURE__ */ new Set([
4
- CHILD_DIRECTIVE_PREFIX,
5
- "x-ignore",
6
- "x-ignore.self",
7
- "x-teleport",
8
- "x-cloak"
9
- ]);
10
- var NEVER_TRANSFER_PREFIXES = ["x-child.", "x-teleport.", "x-transition", "x-effect"];
11
- var SCOPE_ATTRS = /* @__PURE__ */ new Set(["x-data", "x-init", "x-ref"]);
12
- function isNeverTransfer(name) {
13
- if (NEVER_TRANSFER.has(name)) {
14
- return true;
15
- }
16
- return NEVER_TRANSFER_PREFIXES.some((prefix) => name.startsWith(prefix));
17
- }
18
- function findFirstElementChild(parent) {
19
- for (const node of parent.childNodes) {
20
- if (node.nodeType === Node.ELEMENT_NODE) {
21
- return node;
22
- }
23
- }
24
- return null;
25
- }
26
- function countElementChildren(parent) {
27
- let count = 0;
28
- for (const node of parent.childNodes) {
29
- if (node.nodeType === Node.ELEMENT_NODE) {
30
- count += 1;
31
- }
32
- }
33
- return count;
34
- }
35
- function parseChildDirective(el) {
36
- for (const { name } of el.attributes) {
37
- if (name === CHILD_DIRECTIVE_PREFIX) {
38
- return { mode: "default" };
39
- }
40
- if (!name.startsWith(`${CHILD_DIRECTIVE_PREFIX}.`)) {
41
- continue;
42
- }
43
- const modifiers = name.slice(CHILD_DIRECTIVE_PREFIX.length + 1).split(".");
44
- if (modifiers.includes("replace")) {
45
- return { mode: "replace" };
46
- }
47
- if (modifiers.includes("merge")) {
48
- return { mode: "merge" };
49
- }
50
- return { mode: "default" };
51
- }
52
- return null;
53
- }
54
- function mergeClassTokens(...values) {
55
- const tokens = /* @__PURE__ */ new Set();
56
- for (const value of values) {
57
- for (const token of value.split(/\s+/)) {
58
- if (token) {
59
- tokens.add(token);
60
- }
61
- }
62
- }
63
- return [...tokens].join(" ");
64
- }
65
- function parseInlineStyle(style) {
66
- const map = /* @__PURE__ */ new Map();
67
- for (const part of style.split(";")) {
68
- const trimmed = part.trim();
69
- if (!trimmed) {
70
- continue;
71
- }
72
- const colon = trimmed.indexOf(":");
73
- if (colon === -1) {
74
- continue;
75
- }
76
- const property = trimmed.slice(0, colon).trim().toLowerCase();
77
- const value = trimmed.slice(colon + 1).trim();
78
- map.set(property, value);
79
- }
80
- return map;
81
- }
82
- function serializeInlineStyle(map) {
83
- return [...map.entries()].map(([property, value]) => `${property}: ${value}`).join("; ");
84
- }
85
- function mergeInlineStyle(wrapperStyle, childStyle, childWins) {
86
- const wrapperMap = parseInlineStyle(wrapperStyle);
87
- const childMap = parseInlineStyle(childStyle);
88
- const merged = childWins ? new Map([...wrapperMap, ...childMap]) : new Map([...childMap, ...wrapperMap]);
89
- return serializeInlineStyle(merged);
90
- }
91
- function childHasAttribute(el, name) {
92
- return el.hasAttribute(name);
93
- }
94
- function shouldCopyAttribute(name, wrapper, child, mode) {
95
- if (isNeverTransfer(name)) {
96
- return false;
97
- }
98
- if (name === "id" && childHasAttribute(child, "id")) {
99
- return false;
100
- }
101
- if (SCOPE_ATTRS.has(name) && childHasAttribute(child, name)) {
102
- return false;
103
- }
104
- if (mode === "replace") {
105
- return wrapper.hasAttribute(name);
106
- }
107
- if (mode === "default" || mode === "merge") {
108
- if (name === "class" || name === "style") {
109
- return wrapper.hasAttribute(name);
110
- }
111
- return wrapper.hasAttribute(name) && !childHasAttribute(child, name);
112
- }
113
- return false;
114
- }
115
- function applyAttribute(wrapper, child, name, mode) {
116
- const wrapperValue = wrapper.getAttribute(name) ?? "";
117
- const childValue = child.getAttribute(name) ?? "";
118
- if (name === "class") {
119
- if (mode === "replace") {
120
- child.setAttribute("class", mergeClassTokens(wrapperValue, childValue));
121
- return;
122
- }
123
- child.setAttribute("class", mergeClassTokens(childValue, wrapperValue));
124
- return;
125
- }
126
- if (name === "style") {
127
- const merged = mergeInlineStyle(wrapperValue, childValue, mode !== "replace");
128
- if (merged) {
129
- child.setAttribute("style", merged);
130
- }
131
- return;
132
- }
133
- if (mode === "replace" || !childHasAttribute(child, name)) {
134
- child.setAttribute(name, wrapperValue);
135
- }
136
- }
137
- function transferAttributes(wrapper, child, mode = "default") {
138
- const names = /* @__PURE__ */ new Set();
139
- for (const { name } of wrapper.attributes) {
140
- names.add(name);
141
- }
142
- for (const name of names) {
143
- if (!shouldCopyAttribute(name, wrapper, child, mode)) {
144
- continue;
145
- }
146
- applyAttribute(wrapper, child, name, mode);
147
- }
148
- }
149
- function clearTransferredAttributes(wrapper) {
150
- const toRemove = [];
151
- for (const { name } of wrapper.attributes) {
152
- if (isNeverTransfer(name)) {
153
- toRemove.push(name);
154
- continue;
155
- }
156
- toRemove.push(name);
157
- }
158
- for (const name of toRemove) {
159
- wrapper.removeAttribute(name);
160
- }
161
- }
162
-
163
- // src/index.ts
164
- var processedWrappers = /* @__PURE__ */ new WeakSet();
165
- function warnChild(message) {
166
- console.warn(message);
167
- }
168
- function childPlugin(Alpine) {
169
- Alpine.addInitSelector(() => `[${Alpine.prefixed("child")}]`);
170
- Alpine.interceptInit((el, skip) => {
171
- const config = parseChildDirective(el);
172
- if (!config) {
173
- return;
174
- }
175
- if (processedWrappers.has(el)) {
176
- return;
177
- }
178
- const target = findFirstElementChild(el);
179
- if (!target) {
180
- warnChild("[x-child] No element child found; directive ignored.");
181
- return;
182
- }
183
- if (countElementChildren(el) > 1) {
184
- warnChild("[x-child] Multiple element children; only the first is used.");
185
- }
186
- const morph = Alpine.morph;
187
- if (typeof morph !== "function") {
188
- warnChild(
189
- "[x-child] @alpinejs/morph is required \u2014 register Alpine.plugin(morph) before x-child."
190
- );
191
- return;
192
- }
193
- processedWrappers.add(el);
194
- el._x_ignoreSelf = true;
195
- target._x_ignore = true;
196
- skip();
197
- Alpine.nextTick(() => {
198
- if (!(el.isConnected || target.isConnected)) {
199
- return;
200
- }
201
- transferAttributes(el, target, config.mode);
202
- let promoted = null;
203
- Alpine.mutateDom(() => {
204
- morph(el, target.outerHTML, {
205
- added(node) {
206
- if (node.nodeType === Node.ELEMENT_NODE) {
207
- promoted = node;
208
- }
209
- }
210
- });
211
- });
212
- const result = promoted ?? (el.isConnected ? el : null);
213
- if (result) {
214
- processedWrappers.add(result);
215
- result._x_ignore = void 0;
216
- Alpine.initTree(result);
217
- }
218
- clearTransferredAttributes(el);
219
- });
220
- });
221
- Alpine.directive("child", () => {
222
- }).before("ignore");
223
- }
224
- export {
225
- clearTransferredAttributes,
226
- countElementChildren,
227
- childPlugin as default,
228
- findFirstElementChild,
229
- parseChildDirective,
230
- transferAttributes
231
- };
232
- //# sourceMappingURL=index.js.map
1
+ var f="x-child",v=new Set([f,"x-ignore","x-ignore.self","x-teleport","x-cloak"]),D=["x-child.","x-teleport.","x-transition","x-effect"],I=new Set(["x-data","x-init","x-ref"]);function N(t){return v.has(t)?!0:D.some(e=>t.startsWith(e))}function S(t,e,n,r){return N(t)||t==="id"&&n.hasAttribute("id")||I.has(t)&&n.hasAttribute(t)?!1:r==="replace"?e.hasAttribute(t):r==="default"||r==="merge"?t==="class"||t==="style"?e.hasAttribute(t):e.hasAttribute(t)&&!n.hasAttribute(t):!1}function a(t){for(let e of t.childNodes)if(e.nodeType===Node.ELEMENT_NODE)return e;return null}function c(t){let e=0;for(let n of t.childNodes)n.nodeType===Node.ELEMENT_NODE&&(e+=1);return e}function p(t){for(let{name:e}of t.attributes){if(e===f)return{mode:"default"};if(!e.startsWith(`${f}.`))continue;let n=e.slice(f.length+1).split(".");return n.includes("replace")?{mode:"replace"}:n.includes("merge")?{mode:"merge"}:{mode:"default"}}return null}function g(t,e,n){for(let{name:r}of t.attributes)S(r,t,e,n)&&O(t,e,r,n)}function m(t){for(let{name:e}of t.attributes)t.removeAttribute(e)}function x(...t){let e=new Set;for(let n of t)for(let r of n.split(/\s+/))r&&e.add(r);return[...e].join(" ")}function y(t){let e=new Map;for(let n of t.split(";")){let r=n.trim();if(!r)continue;let i=r.indexOf(":");if(i===-1)continue;let l=r.slice(0,i).trim().toLowerCase(),s=r.slice(i+1).trim();e.set(l,s)}return e}function R(t){return[...t.entries()].map(([e,n])=>`${e}: ${n}`).join("; ")}function k(t,e,n){let r=y(t),i=y(e),l=n?new Map([...r,...i]):new Map([...i,...r]);return R(l)}function O(t,e,n,r){let i=t.getAttribute(n)??"",l=e.getAttribute(n)??"";if(n==="class"){if(r==="replace"){e.setAttribute("class",x(i,l));return}e.setAttribute("class",x(l,i));return}if(n==="style"){let s=k(i,l,r!=="replace");s&&e.setAttribute("style",s);return}(r==="replace"||!e.hasAttribute(n))&&e.setAttribute(n,i)}import{guardDirective as P}from"@ailuracode/alpine-core";var E="child";function M(t={}){let e=t.directiveKey??E;return function(r){let i=r,l=new WeakSet;i.addInitSelector(()=>`[${i.prefixed(e)}]`),i.interceptInit((o,_)=>{let h=p(o);if(!h||l.has(o))return;let d=a(o);if(!d)return;c(o)>1;let C=i.morph;typeof C=="function"&&(l.add(o),o._x_ignoreSelf=!0,d._x_ignore=!0,_(),i.nextTick(()=>{if(!(o.isConnected||d.isConnected))return;g(o,d,h.mode);let A=null;i.mutateDom(()=>{let T={added(b){b.nodeType===Node.ELEMENT_NODE&&(A=b)}};C(o,d.outerHTML,T)});let u=A??(o.isConnected?o:null);u&&(l.add(u),u._x_ignore=void 0,i.initTree(u)),m(o)}))});let s=P(i,e,()=>({}),"child");s&&typeof s.before=="function"&&s.before("ignore")}}var L=M;export{E as DEFAULT_CHILD_DIRECTIVE_KEY,M as childPlugin,m as clearTransferredAttributes,c as countElementChildren,L as default,a as findFirstElementChild,p as parseChildDirective,g as transferAttributes};
package/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "@ailuracode/alpine-child",
3
- "version": "0.1.0",
3
+ "version": "2.0.0",
4
4
  "description": "Alpine.js x-child directive — asChild-style attribute transfer to the first child element",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "author": "ailuracode",
8
+ "sideEffects": false,
8
9
  "publishConfig": {
9
10
  "access": "public"
10
11
  },
@@ -32,8 +33,8 @@
32
33
  },
33
34
  "types": "./dist/global.d.ts",
34
35
  "peerDependencies": {
36
+ "@ailuracode/alpine-core": "^0.3.0",
35
37
  "@alpinejs/morph": "^3.0.0",
36
- "@types/alpinejs": "^3.13.11",
37
38
  "alpinejs": "^3.0.0"
38
39
  },
39
40
  "peerDependenciesMeta": {
@@ -42,7 +43,12 @@
42
43
  }
43
44
  },
44
45
  "devDependencies": {
45
- "@alpinejs/morph": "^3.15.12"
46
+ "@alpinejs/morph": "^3.15.12",
47
+ "@types/alpinejs": "^3.13.10",
48
+ "@types/node": "^26.1.0",
49
+ "tsup": "^8.5.0",
50
+ "alpinejs": "^3.15.12",
51
+ "@ailuracode/alpine-core": "0.3.0"
46
52
  },
47
53
  "keywords": [
48
54
  "alpinejs",
@@ -52,8 +58,33 @@
52
58
  "as-child",
53
59
  "headless"
54
60
  ],
61
+ "toolkit": {
62
+ "bundleBudget": {
63
+ "category": "small-feature"
64
+ }
65
+ },
66
+ "size-limit": [
67
+ {
68
+ "name": "full surface",
69
+ "path": "dist/index.js",
70
+ "import": "*",
71
+ "ignore": [
72
+ "alpinejs",
73
+ "@ailuracode/alpine-core",
74
+ "@alpinejs/morph"
75
+ ],
76
+ "gzip": true,
77
+ "brotli": true,
78
+ "limit": "1.5 kB"
79
+ }
80
+ ],
55
81
  "scripts": {
56
- "build": "tsup src/index.ts --format esm --dts --clean --sourcemap --out-dir dist && cp src/global.d.ts dist/global.d.ts",
57
- "test": "vitest run --config ../../vitest.config.ts test"
82
+ "build": "tsup --config tsup.config.ts && cp src/global.d.ts dist/global.d.ts",
83
+ "clean": "rm -rf dist",
84
+ "size": "size-limit",
85
+ "test": "vitest run --config ../../vitest.config.ts packages/child",
86
+ "test:watch": "vitest --config ../../vitest.config.ts",
87
+ "typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.test.json",
88
+ "test:e2e": "playwright test --config playwright.config.ts"
58
89
  }
59
90
  }
package/dist/index.js.map DELETED
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/transfer.ts","../src/index.ts"],"sourcesContent":["export type ChildMergeMode = \"default\" | \"merge\" | \"replace\";\n\nconst CHILD_DIRECTIVE_PREFIX = \"x-child\";\n\nconst NEVER_TRANSFER = new Set([\n CHILD_DIRECTIVE_PREFIX,\n \"x-ignore\",\n \"x-ignore.self\",\n \"x-teleport\",\n \"x-cloak\",\n]);\n\nconst NEVER_TRANSFER_PREFIXES = [\"x-child.\", \"x-teleport.\", \"x-transition\", \"x-effect\"];\n\nconst SCOPE_ATTRS = new Set([\"x-data\", \"x-init\", \"x-ref\"]);\n\nfunction isNeverTransfer(name: string): boolean {\n if (NEVER_TRANSFER.has(name)) {\n return true;\n }\n\n return NEVER_TRANSFER_PREFIXES.some((prefix) => name.startsWith(prefix));\n}\n\n/** Returns the first element child, skipping text and comment nodes. */\nexport function findFirstElementChild(parent: Element): Element | null {\n for (const node of parent.childNodes) {\n if (node.nodeType === Node.ELEMENT_NODE) {\n return node as Element;\n }\n }\n\n return null;\n}\n\n/** Counts element children (excluding text/comment nodes). */\nexport function countElementChildren(parent: Element): number {\n let count = 0;\n\n for (const node of parent.childNodes) {\n if (node.nodeType === Node.ELEMENT_NODE) {\n count += 1;\n }\n }\n\n return count;\n}\n\n/** Parses `x-child` directive modifiers from element attributes. */\nexport function parseChildDirective(el: Element): { mode: ChildMergeMode } | null {\n for (const { name } of el.attributes) {\n if (name === CHILD_DIRECTIVE_PREFIX) {\n return { mode: \"default\" };\n }\n\n if (!name.startsWith(`${CHILD_DIRECTIVE_PREFIX}.`)) {\n continue;\n }\n\n const modifiers = name.slice(CHILD_DIRECTIVE_PREFIX.length + 1).split(\".\");\n\n if (modifiers.includes(\"replace\")) {\n return { mode: \"replace\" };\n }\n\n if (modifiers.includes(\"merge\")) {\n return { mode: \"merge\" };\n }\n\n return { mode: \"default\" };\n }\n\n return null;\n}\n\nfunction mergeClassTokens(...values: string[]): string {\n const tokens = new Set<string>();\n\n for (const value of values) {\n for (const token of value.split(/\\s+/)) {\n if (token) {\n tokens.add(token);\n }\n }\n }\n\n return [...tokens].join(\" \");\n}\n\nfunction parseInlineStyle(style: string): Map<string, string> {\n const map = new Map<string, string>();\n\n for (const part of style.split(\";\")) {\n const trimmed = part.trim();\n if (!trimmed) {\n continue;\n }\n\n const colon = trimmed.indexOf(\":\");\n if (colon === -1) {\n continue;\n }\n\n const property = trimmed.slice(0, colon).trim().toLowerCase();\n const value = trimmed.slice(colon + 1).trim();\n map.set(property, value);\n }\n\n return map;\n}\n\nfunction serializeInlineStyle(map: Map<string, string>): string {\n return [...map.entries()].map(([property, value]) => `${property}: ${value}`).join(\"; \");\n}\n\nfunction mergeInlineStyle(wrapperStyle: string, childStyle: string, childWins: boolean): string {\n const wrapperMap = parseInlineStyle(wrapperStyle);\n const childMap = parseInlineStyle(childStyle);\n const merged = childWins\n ? new Map([...wrapperMap, ...childMap])\n : new Map([...childMap, ...wrapperMap]);\n\n return serializeInlineStyle(merged);\n}\n\nfunction childHasAttribute(el: Element, name: string): boolean {\n return el.hasAttribute(name);\n}\n\nfunction shouldCopyAttribute(\n name: string,\n wrapper: Element,\n child: Element,\n mode: ChildMergeMode\n): boolean {\n if (isNeverTransfer(name)) {\n return false;\n }\n\n if (name === \"id\" && childHasAttribute(child, \"id\")) {\n return false;\n }\n\n if (SCOPE_ATTRS.has(name) && childHasAttribute(child, name)) {\n return false;\n }\n\n if (mode === \"replace\") {\n return wrapper.hasAttribute(name);\n }\n\n if (mode === \"default\" || mode === \"merge\") {\n if (name === \"class\" || name === \"style\") {\n return wrapper.hasAttribute(name);\n }\n\n return wrapper.hasAttribute(name) && !childHasAttribute(child, name);\n }\n\n return false;\n}\n\nfunction applyAttribute(\n wrapper: Element,\n child: Element,\n name: string,\n mode: ChildMergeMode\n): void {\n const wrapperValue = wrapper.getAttribute(name) ?? \"\";\n const childValue = child.getAttribute(name) ?? \"\";\n\n if (name === \"class\") {\n if (mode === \"replace\") {\n child.setAttribute(\"class\", mergeClassTokens(wrapperValue, childValue));\n return;\n }\n\n child.setAttribute(\"class\", mergeClassTokens(childValue, wrapperValue));\n return;\n }\n\n if (name === \"style\") {\n const merged = mergeInlineStyle(wrapperValue, childValue, mode !== \"replace\");\n if (merged) {\n child.setAttribute(\"style\", merged);\n }\n return;\n }\n\n if (mode === \"replace\" || !childHasAttribute(child, name)) {\n child.setAttribute(name, wrapperValue);\n }\n}\n\n/** Transfers wrapper attributes onto the first element child. */\nexport function transferAttributes(\n wrapper: Element,\n child: Element,\n mode: ChildMergeMode = \"default\"\n): void {\n const names = new Set<string>();\n\n for (const { name } of wrapper.attributes) {\n names.add(name);\n }\n\n for (const name of names) {\n if (!shouldCopyAttribute(name, wrapper, child, mode)) {\n continue;\n }\n\n applyAttribute(wrapper, child, name, mode);\n }\n}\n\n/** Removes transferable attributes from the wrapper after unwrapping. */\nexport function clearTransferredAttributes(wrapper: Element): void {\n const toRemove: string[] = [];\n\n for (const { name } of wrapper.attributes) {\n if (isNeverTransfer(name)) {\n toRemove.push(name);\n continue;\n }\n\n toRemove.push(name);\n }\n\n for (const name of toRemove) {\n wrapper.removeAttribute(name);\n }\n}\n","import type AlpineType from \"alpinejs\";\nimport {\n clearTransferredAttributes,\n countElementChildren,\n findFirstElementChild,\n parseChildDirective,\n transferAttributes,\n} from \"./transfer.js\";\n\nexport type { ChildMergeMode } from \"./transfer.js\";\nexport {\n clearTransferredAttributes,\n countElementChildren,\n findFirstElementChild,\n parseChildDirective,\n transferAttributes,\n} from \"./transfer.js\";\n\nconst processedWrappers = new WeakSet<Element>();\n\ntype AlpineElement = Element & {\n _x_ignore?: boolean;\n _x_ignoreSelf?: boolean;\n};\n\ntype MorphOptions = {\n added?: (node: Node) => void;\n};\n\ntype AlpineWithMorph = AlpineType.Alpine & {\n morph?: (el: Element, newHtml: string | Element, options?: MorphOptions) => Element;\n};\n\nfunction warnChild(message: string): void {\n // biome-ignore lint/suspicious/noConsole: intentional developer warnings for invalid markup\n console.warn(message);\n}\n\n/** Alpine.js plugin. Registers the `x-child` directive (asChild-style unwrapping). */\nexport default function childPlugin(Alpine: AlpineType.Alpine): void {\n Alpine.addInitSelector(() => `[${Alpine.prefixed(\"child\")}]`);\n\n Alpine.interceptInit((el: Element, skip: () => void) => {\n const config = parseChildDirective(el);\n if (!config) {\n return;\n }\n\n if (processedWrappers.has(el)) {\n return;\n }\n\n const target = findFirstElementChild(el);\n if (!target) {\n warnChild(\"[x-child] No element child found; directive ignored.\");\n return;\n }\n\n if (countElementChildren(el) > 1) {\n warnChild(\"[x-child] Multiple element children; only the first is used.\");\n }\n\n const morph = (Alpine as AlpineWithMorph).morph;\n if (typeof morph !== \"function\") {\n warnChild(\n \"[x-child] @alpinejs/morph is required — register Alpine.plugin(morph) before x-child.\"\n );\n return;\n }\n\n // Defer unwrap until after the current initTree pass so DOM mutations from\n // morph do not corrupt scope for siblings initialized later (e.g. sidebar).\n processedWrappers.add(el);\n (el as AlpineElement)._x_ignoreSelf = true;\n (target as AlpineElement)._x_ignore = true;\n skip();\n\n Alpine.nextTick(() => {\n if (!(el.isConnected || target.isConnected)) {\n return;\n }\n\n transferAttributes(el, target, config.mode);\n\n let promoted: Element | null = null;\n\n Alpine.mutateDom(() => {\n morph(el, target.outerHTML, {\n added(node) {\n if (node.nodeType === Node.ELEMENT_NODE) {\n promoted = node as Element;\n }\n },\n });\n });\n\n const result = promoted ?? (el.isConnected ? el : null);\n if (result) {\n processedWrappers.add(result);\n (result as AlpineElement)._x_ignore = undefined;\n Alpine.initTree(result as HTMLElement);\n }\n\n clearTransferredAttributes(el);\n });\n });\n\n Alpine.directive(\"child\", () => {\n // Unwrap runs in interceptInit before other directives on the wrapper.\n }).before(\"ignore\");\n}\n"],"mappings":"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","names":[]}