@sprlab/wccompiler 0.10.8 → 0.10.10

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.
@@ -1,26 +1,37 @@
1
1
  /**
2
- * Angular adapter for WCC Scoped Slots.
2
+ * Angular adapter for WCC Scoped Slots and Event Binding.
3
3
  *
4
4
  * Exports:
5
5
  * - WccSlotDef: Auxiliary directive for ng-template[slot]
6
6
  * - WccSlotsDirective: Main directive activated via [wccSlots] attribute
7
+ * - WccEvent: Single-event directive (wccEvent="name" + wccEmit output)
8
+ * - WccEvents: Multi-event bridging directive (kebab-case → camelCase)
7
9
  * - SlotContext: Interface for template context typing
8
10
  *
9
11
  * Usage:
10
- * import { WccSlotsDirective, WccSlotDef } from '@sprlab/wccompiler/adapters/angular';
12
+ * import { WccSlotsDirective, WccSlotDef, WccEvent, WccEvents } from '@sprlab/wccompiler/adapters/angular';
11
13
  *
12
14
  * @Component({
13
- * imports: [WccSlotsDirective, WccSlotDef],
15
+ * imports: [WccSlotsDirective, WccSlotDef, WccEvent, WccEvents],
14
16
  * schemas: [CUSTOM_ELEMENTS_SCHEMA],
15
17
  * template: `
16
18
  * <wcc-card wccSlots>
17
19
  * <ng-template slot="header"><strong>Header</strong></ng-template>
18
20
  * <ng-template slot="stats" let-likes>{{ likes }} likes</ng-template>
19
21
  * </wcc-card>
22
+ *
23
+ * <!-- Event binding option 1: single event with unwrapped detail -->
24
+ * <wcc-counter wccEvent="count-changed" (wccEmit)="onCount($event)"></wcc-counter>
25
+ *
26
+ * <!-- Event binding option 2: camelCase event names -->
27
+ * <wcc-counter wccEvents (countChanged)="onCount($event.detail)"></wcc-counter>
28
+ *
29
+ * <!-- Event binding option 3: standard Angular (always works) -->
30
+ * <wcc-counter (count-changed)="onCount($event.detail)"></wcc-counter>
20
31
  * `
21
32
  * })
22
33
  *
23
- * Note: Add the `wccSlots` attribute to any WCC custom element that uses slots.
34
+ * Note: Add the `wccSlots` attribute to any WCC element that uses slots.
24
35
  * This is required because Angular AOT cannot evaluate dynamic selectors.
25
36
  *
26
37
  * @module @sprlab/wccompiler/adapters/angular
@@ -65,6 +76,19 @@ export declare class WccSlotsDirective implements AfterContentInit, OnDestroy {
65
76
  private destroyed;
66
77
  ngAfterContentInit(): void;
67
78
  ngOnDestroy(): void;
79
+ /**
80
+ * Normalizes Angular-style slot attributes to standard HTML slot attributes.
81
+ * Converts: <div slot-header> → <div slot="header">
82
+ *
83
+ * This enables the Angular ng-content select pattern:
84
+ * <wcc-card wccSlots>
85
+ * <nav slot-header>Title</nav>
86
+ * <span slot-footer>Footer</span>
87
+ * </wcc-card>
88
+ *
89
+ * Skips reserved prefixes: slot-props, slot-template-*
90
+ */
91
+ private normalizeSlotAttributes;
68
92
  /** Classifies slots using __scopedSlots from the host element and initializes them */
69
93
  private classifyAndInitSlots;
70
94
  /** Named Slot: immediate static rendering */
@@ -1,26 +1,37 @@
1
1
  /**
2
- * Angular adapter for WCC Scoped Slots.
2
+ * Angular adapter for WCC Scoped Slots and Event Binding.
3
3
  *
4
4
  * Exports:
5
5
  * - WccSlotDef: Auxiliary directive for ng-template[slot]
6
6
  * - WccSlotsDirective: Main directive activated via [wccSlots] attribute
7
+ * - WccEvent: Single-event directive (wccEvent="name" + wccEmit output)
8
+ * - WccEvents: Multi-event bridging directive (kebab-case → camelCase)
7
9
  * - SlotContext: Interface for template context typing
8
10
  *
9
11
  * Usage:
10
- * import { WccSlotsDirective, WccSlotDef } from '@sprlab/wccompiler/adapters/angular';
12
+ * import { WccSlotsDirective, WccSlotDef, WccEvent, WccEvents } from '@sprlab/wccompiler/adapters/angular';
11
13
  *
12
14
  * @Component({
13
- * imports: [WccSlotsDirective, WccSlotDef],
15
+ * imports: [WccSlotsDirective, WccSlotDef, WccEvent, WccEvents],
14
16
  * schemas: [CUSTOM_ELEMENTS_SCHEMA],
15
17
  * template: `
16
18
  * <wcc-card wccSlots>
17
19
  * <ng-template slot="header"><strong>Header</strong></ng-template>
18
20
  * <ng-template slot="stats" let-likes>{{ likes }} likes</ng-template>
19
21
  * </wcc-card>
22
+ *
23
+ * <!-- Event binding option 1: single event with unwrapped detail -->
24
+ * <wcc-counter wccEvent="count-changed" (wccEmit)="onCount($event)"></wcc-counter>
25
+ *
26
+ * <!-- Event binding option 2: camelCase event names -->
27
+ * <wcc-counter wccEvents (countChanged)="onCount($event.detail)"></wcc-counter>
28
+ *
29
+ * <!-- Event binding option 3: standard Angular (always works) -->
30
+ * <wcc-counter (count-changed)="onCount($event.detail)"></wcc-counter>
20
31
  * `
21
32
  * })
22
33
  *
23
- * Note: Add the `wccSlots` attribute to any WCC custom element that uses slots.
34
+ * Note: Add the `wccSlots` attribute to any WCC element that uses slots.
24
35
  * This is required because Angular AOT cannot evaluate dynamic selectors.
25
36
  *
26
37
  * @module @sprlab/wccompiler/adapters/angular
@@ -76,12 +87,42 @@ export class WccSlotsDirective {
76
87
  // Runtime guard: only proceed for custom elements (tag name contains hyphen)
77
88
  if (!this.el.nativeElement.tagName.toLowerCase().includes('-'))
78
89
  return;
90
+ // Normalize Angular-style slot attributes: slot-header → slot="header"
91
+ this.normalizeSlotAttributes();
79
92
  this.classifyAndInitSlots();
80
93
  }
81
94
  ngOnDestroy() {
82
95
  this.destroyed = true;
83
96
  this.cleanup();
84
97
  }
98
+ // ─── Slot Attribute Normalization ───────────────────────────────────────
99
+ /**
100
+ * Normalizes Angular-style slot attributes to standard HTML slot attributes.
101
+ * Converts: <div slot-header> → <div slot="header">
102
+ *
103
+ * This enables the Angular ng-content select pattern:
104
+ * <wcc-card wccSlots>
105
+ * <nav slot-header>Title</nav>
106
+ * <span slot-footer>Footer</span>
107
+ * </wcc-card>
108
+ *
109
+ * Skips reserved prefixes: slot-props, slot-template-*
110
+ */
111
+ normalizeSlotAttributes() {
112
+ const hostEl = this.el.nativeElement;
113
+ for (const child of Array.from(hostEl.children)) {
114
+ for (const attr of Array.from(child.attributes)) {
115
+ if (attr.name.startsWith('slot-') &&
116
+ !attr.value &&
117
+ attr.name !== 'slot-props' &&
118
+ !attr.name.startsWith('slot-template-')) {
119
+ const slotName = attr.name.slice(5); // "slot-header" → "header"
120
+ child.removeAttribute(attr.name);
121
+ child.setAttribute('slot', slotName);
122
+ }
123
+ }
124
+ }
125
+ }
85
126
  // ─── Classification ─────────────────────────────────────────────────────
86
127
  /** Classifies slots using __scopedSlots from the host element and initializes them */
87
128
  async classifyAndInitSlots() {
@@ -127,6 +127,9 @@ export class WccSlotsDirective implements AfterContentInit, OnDestroy {
127
127
  // Runtime guard: only proceed for custom elements (tag name contains hyphen)
128
128
  if (!this.el.nativeElement.tagName.toLowerCase().includes('-')) return;
129
129
 
130
+ // Normalize Angular-style slot attributes: slot-header → slot="header"
131
+ this.normalizeSlotAttributes();
132
+
130
133
  this.classifyAndInitSlots();
131
134
  }
132
135
 
@@ -135,6 +138,38 @@ export class WccSlotsDirective implements AfterContentInit, OnDestroy {
135
138
  this.cleanup();
136
139
  }
137
140
 
141
+ // ─── Slot Attribute Normalization ───────────────────────────────────────
142
+
143
+ /**
144
+ * Normalizes Angular-style slot attributes to standard HTML slot attributes.
145
+ * Converts: <div slot-header> → <div slot="header">
146
+ *
147
+ * This enables the Angular ng-content select pattern:
148
+ * <wcc-card wccSlots>
149
+ * <nav slot-header>Title</nav>
150
+ * <span slot-footer>Footer</span>
151
+ * </wcc-card>
152
+ *
153
+ * Skips reserved prefixes: slot-props, slot-template-*
154
+ */
155
+ private normalizeSlotAttributes(): void {
156
+ const hostEl = this.el.nativeElement;
157
+ for (const child of Array.from(hostEl.children)) {
158
+ for (const attr of Array.from(child.attributes)) {
159
+ if (
160
+ attr.name.startsWith('slot-') &&
161
+ !attr.value &&
162
+ attr.name !== 'slot-props' &&
163
+ !attr.name.startsWith('slot-template-')
164
+ ) {
165
+ const slotName = attr.name.slice(5); // "slot-header" → "header"
166
+ child.removeAttribute(attr.name);
167
+ child.setAttribute('slot', slotName);
168
+ }
169
+ }
170
+ }
171
+ }
172
+
138
173
  // ─── Classification ─────────────────────────────────────────────────────
139
174
 
140
175
  /** Classifies slots using __scopedSlots from the host element and initializes them */
package/bin/wcc.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { readdirSync, writeFileSync, mkdirSync, existsSync, watch, copyFileSync } from 'node:fs';
3
+ import { readdirSync, writeFileSync, mkdirSync, existsSync, watch, copyFileSync, readFileSync } from 'node:fs';
4
4
  import { resolve, relative, extname, basename, dirname, join } from 'node:path';
5
5
  import { fileURLToPath } from 'node:url';
6
6
  import { loadConfig } from '../lib/config.js';
@@ -61,9 +61,136 @@ async function build(config, cwd) {
61
61
  const runtimeDest = join(outputDir, 'wcc-runtime.js');
62
62
  copyFileSync(runtimeSrc, runtimeDest);
63
63
 
64
+ // Generate framework stubs (React + Vue) from compiled component metadata
65
+ generateFrameworkStubs(outputDir);
66
+
64
67
  return errors;
65
68
  }
66
69
 
70
+ /**
71
+ * Generates framework stub files (React + Vue) from compiled component metadata.
72
+ * Reads static __meta from each compiled .js file and produces:
73
+ * - wcc-react.js + wcc-react.d.ts (importable stubs for React)
74
+ * - wcc-vue.js + wcc-vue.d.ts (importable stubs for Vue)
75
+ */
76
+ function generateFrameworkStubs(outputDir) {
77
+
78
+ const files = readdirSync(outputDir).filter(f => f.endsWith('.js') && !f.startsWith('__') && f !== 'wcc-runtime.js' && f !== 'wcc-react.js' && f !== 'wcc-vue.js');
79
+ const components = [];
80
+
81
+ for (const file of files) {
82
+ const content = readFileSync(join(outputDir, file), 'utf-8');
83
+ // Match static __meta = { ... }; with balanced braces
84
+ const metaStart = content.indexOf('static __meta = {');
85
+ if (metaStart === -1) continue;
86
+
87
+ // Find the balanced closing brace
88
+ let depth = 0;
89
+ let metaEnd = -1;
90
+ for (let i = metaStart + 'static __meta = '.length; i < content.length; i++) {
91
+ if (content[i] === '{') depth++;
92
+ else if (content[i] === '}') {
93
+ depth--;
94
+ if (depth === 0) { metaEnd = i + 1; break; }
95
+ }
96
+ }
97
+ if (metaEnd === -1) continue;
98
+
99
+ const metaStr = content.slice(metaStart + 'static __meta = '.length, metaEnd);
100
+
101
+ try {
102
+ const parsed = metaStr
103
+ .replace(/'/g, '"')
104
+ .replace(/(\w+)\s*:/g, '"$1":')
105
+ .replace(/,\s*}/g, '}')
106
+ .replace(/,\s*]/g, ']');
107
+ const meta = JSON.parse(parsed);
108
+ if (!meta.tag) continue;
109
+
110
+ const pascalName = meta.tag.split('-').map(s => s[0].toUpperCase() + s.slice(1)).join('');
111
+ components.push({ meta, pascalName, file });
112
+ } catch (e) {
113
+ // Skip unparseable
114
+ }
115
+ }
116
+
117
+ if (components.length === 0) return;
118
+
119
+ // ── React stubs ──
120
+ let reactJs = '// Auto-generated by wcc build — React component stubs\n';
121
+ reactJs += '// Import these in your JSX. The wccReactPlugin transforms them at build time.\n\n';
122
+
123
+ let reactDts = '// Auto-generated by wcc build — React component types\n\n';
124
+
125
+ for (const comp of components) {
126
+ const slots = comp.meta.slots || [];
127
+ const slotProps = slots.filter(s => s).map(s => {
128
+ const pascal = s[0].toUpperCase() + s.slice(1);
129
+ return `${pascal}: '${s}'`;
130
+ });
131
+
132
+ // JS stub
133
+ reactJs += `export const ${comp.pascalName} = Object.assign('${comp.meta.tag}', { __tag: '${comp.meta.tag}'`;
134
+ for (const s of slots) {
135
+ if (!s) continue;
136
+ const pascal = s[0].toUpperCase() + s.slice(1);
137
+ reactJs += `, ${pascal}: '${s}'`;
138
+ }
139
+ reactJs += ` });\n`;
140
+
141
+ // TypeScript declaration
142
+ const slotTypes = slots.filter(s => s).map(s => {
143
+ const pascal = s[0].toUpperCase() + s.slice(1);
144
+ return ` ${pascal}: string;`;
145
+ }).join('\n');
146
+
147
+ reactDts += `export declare const ${comp.pascalName}: string & {\n __tag: '${comp.meta.tag}';\n${slotTypes}\n};\n\n`;
148
+ }
149
+
150
+ writeFileSync(join(outputDir, 'wcc-react.js'), reactJs);
151
+ writeFileSync(join(outputDir, 'wcc-react.d.ts'), reactDts);
152
+
153
+ // ── Vue stubs ──
154
+ let vueJs = '// Auto-generated by wcc build — Vue component stubs\n';
155
+ vueJs += '// Import these in your Vue SFC for type safety and IDE support.\n\n';
156
+
157
+ let vueDts = '// Auto-generated by wcc build — Vue component types\n\n';
158
+
159
+ for (const comp of components) {
160
+ const props = comp.meta.props || [];
161
+ const events = comp.meta.events || [];
162
+ const models = comp.meta.models || [];
163
+ const slots = comp.meta.slots || [];
164
+
165
+ // JS stub (just the tag name — Vue uses kebab-case directly)
166
+ vueJs += `export const ${comp.pascalName} = '${comp.meta.tag}';\n`;
167
+
168
+ // TypeScript declaration with props/events/slots info
169
+ const propTypes = props.map(p => {
170
+ const def = String(p.default);
171
+ const type = def === 'true' || def === 'false' ? 'boolean'
172
+ : /^-?\d+(\.\d+)?$/.test(def) ? 'number'
173
+ : 'string';
174
+ return ` ${p.name}?: ${type};`;
175
+ }).join('\n');
176
+
177
+ const eventTypes = events.map(e => ` '${e}': CustomEvent;`).join('\n');
178
+ const modelTypes = models.map(m => ` '${m}': any;`).join('\n');
179
+ const slotTypeEntries = slots.filter(s => s).map(s => ` '${s}': any;`).join('\n');
180
+
181
+ vueDts += `export declare const ${comp.pascalName}: '${comp.meta.tag}';\n`;
182
+ vueDts += `/** Component: ${comp.meta.tag} */\n`;
183
+ if (props.length) vueDts += `export interface ${comp.pascalName}Props {\n${propTypes}\n}\n`;
184
+ if (events.length) vueDts += `export interface ${comp.pascalName}Events {\n${eventTypes}\n}\n`;
185
+ if (models.length) vueDts += `export interface ${comp.pascalName}Models {\n${modelTypes}\n}\n`;
186
+ if (slots.filter(s => s).length) vueDts += `export interface ${comp.pascalName}Slots {\n${slotTypeEntries}\n}\n`;
187
+ vueDts += '\n';
188
+ }
189
+
190
+ writeFileSync(join(outputDir, 'wcc-vue.js'), vueJs);
191
+ writeFileSync(join(outputDir, 'wcc-vue.d.ts'), vueDts);
192
+ }
193
+
67
194
  function discoverFiles(dir) {
68
195
  const results = [];
69
196
  const entries = readdirSync(dir, { withFileTypes: true, recursive: true });
@@ -841,130 +841,19 @@ export function wccReactPlugin(options = {}) {
841
841
 
842
842
 
843
843
  /**
844
- * Vite plugin that generates a virtual module with component stubs for
845
- * PascalCase imports. These stubs satisfy the linter/IDE (component is "defined")
846
- * and the wccReactPlugin transforms them to native custom elements at build time.
844
+ * @deprecated Use the CLI-generated stubs instead (dist/wcc-react.js).
845
+ * The `wcc build` command now auto-generates importable stubs with types.
846
+ * This virtual module plugin is kept for backward compatibility but will be removed.
847
847
  *
848
- * This enables the standard React import pattern:
849
- * import { WccCounter, WccCard } from '@wcc/react'
850
- *
851
- * The stubs are zero-runtime — they're just tag name strings with slot name
852
- * properties. The wccReactPlugin handles the actual JSX transformation.
853
- *
854
- * @param {Object} [options]
855
- * @param {string} [options.moduleId='@wcc/react'] - Virtual module ID for imports
856
- * @param {string} [options.componentsDir='./dist'] - Directory containing compiled WCC .js files
857
- * @param {string} [options.prefix='wcc-'] - Tag prefix filter
858
- * @returns {import('vite').Plugin}
859
- *
860
- * @example vite.config.js
861
- * ```js
862
- * import { wccReactPlugin, wccReactComponents } from '@sprlab/wccompiler/integrations/react'
863
- * export default {
864
- * plugins: [
865
- * wccReactComponents({ componentsDir: './src/wcc' }),
866
- * wccReactPlugin(),
867
- * react()
868
- * ]
869
- * }
870
- * ```
871
- *
872
- * @example Component.jsx
873
- * ```jsx
874
- * import { WccCard, WccList } from '@wcc/react'
875
- *
876
- * <WccCard>
877
- * <WccCard.Header><strong>Title</strong></WccCard.Header>
878
- * <p>Body</p>
879
- * </WccCard>
880
- *
881
- * <WccList>
882
- * <WccList.Item>{(item) => <li>{item}</li>}</WccList.Item>
883
- * </WccList>
884
- * ```
848
+ * Migration:
849
+ * Before: import { WccCard } from '@wcc/react' (virtual module)
850
+ * After: import { WccCard } from './dist/wcc-react' (real file, tree-shakeable)
885
851
  */
886
852
  export function wccReactComponents(options = {}) {
887
- const {
888
- moduleId = '@wcc/react',
889
- componentsDir = './dist',
890
- prefix = 'wcc-'
891
- } = options
892
-
893
- const resolvedId = '\0' + moduleId
894
-
895
853
  return {
896
- name: 'vite-plugin-wcc-react-components',
897
- resolveId(id) {
898
- if (id === moduleId) return resolvedId
899
- return null
900
- },
901
- async load(id) {
902
- if (id !== resolvedId) return null
903
-
904
- // Scan componentsDir for .js files and extract __meta
905
- const fs = await import('fs')
906
- const path = await import('path')
907
-
908
- const dir = path.default.resolve(componentsDir)
909
- if (!fs.default.existsSync(dir)) {
910
- this.warn(`[wcc-react-components] Directory not found: ${dir}`)
911
- return 'export {}'
912
- }
913
-
914
- const files = fs.default.readdirSync(dir).filter(f => f.endsWith('.js'))
915
- const components = []
916
-
917
- for (const file of files) {
918
- const content = fs.default.readFileSync(path.default.join(dir, file), 'utf-8')
919
- const metaMatch = content.match(/static __meta\s*=\s*(\{[^}]+\})/)
920
- if (!metaMatch) continue
921
-
922
- try {
923
- const metaStr = metaMatch[1]
924
- .replace(/'/g, '"')
925
- .replace(/(\w+):/g, '"$1":')
926
- .replace(/,\s*}/g, '}')
927
- .replace(/,\s*]/g, ']')
928
- const meta = JSON.parse(metaStr)
929
-
930
- if (!meta.tag || !meta.tag.startsWith(prefix)) continue
931
-
932
- const pascalName = meta.tag.split('-').map(s => s[0].toUpperCase() + s.slice(1)).join('')
933
- components.push({ meta, pascalName, file })
934
- } catch (e) {
935
- // Skip files with unparseable meta
936
- }
937
- }
938
-
939
- if (components.length === 0) {
940
- return 'export {}'
941
- }
942
-
943
- // Generate lightweight stubs (zero runtime)
944
- // The wccReactPlugin transforms these at build time
945
- let code = '// Auto-generated WCC component stubs (transformed by wccReactPlugin at build time)\n'
946
-
947
- // Import each component file to ensure custom element registration
948
- for (const comp of components) {
949
- code += `import '${path.default.resolve(dir, comp.file)}';\n`
950
- }
951
-
952
- code += '\n'
953
-
954
- // Generate stub exports with compound slot properties
955
- // Use Object.assign to create an object that holds the tag name and slot sub-properties
956
- for (const comp of components) {
957
- const slots = comp.meta.slots || []
958
- code += `export const ${comp.pascalName} = Object.assign(() => '${comp.meta.tag}', { __tag: '${comp.meta.tag}'`
959
- for (const slot of slots) {
960
- if (!slot) continue
961
- const pascalSlot = slot[0].toUpperCase() + slot.slice(1)
962
- code += `, ${pascalSlot}: '${slot}'`
963
- }
964
- code += ` });\n`
965
- }
966
-
967
- return code
854
+ name: 'vite-plugin-wcc-react-components-deprecated',
855
+ buildStart() {
856
+ this.warn('[wcc] wccReactComponents() is deprecated. Use the CLI-generated stubs from dist/wcc-react.js instead.')
968
857
  }
969
858
  }
970
859
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sprlab/wccompiler",
3
- "version": "0.10.8",
3
+ "version": "0.10.10",
4
4
  "description": "Zero-runtime compiler that transforms .wcc single-file components into native web components with signals-based reactivity",
5
5
  "type": "module",
6
6
  "exports": {