@webmate-studio/builder 0.2.126 → 0.2.129

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webmate-studio/builder",
3
- "version": "0.2.126",
3
+ "version": "0.2.129",
4
4
  "type": "module",
5
5
  "description": "Webmate Studio Component Builder",
6
6
  "keywords": [
package/src/bundler.js CHANGED
@@ -41,12 +41,65 @@ export async function bundleIsland(islandPath, outputPath, options = {}) {
41
41
  // NEW: Component-specific node_modules (highest priority)
42
42
  const componentNodeModules = componentDir ? path.join(componentDir, 'node_modules') : null;
43
43
 
44
+ // Check if island source already registers as Custom Element
45
+ // If not (plain class export), create a wrapper entry that auto-registers it
46
+ let entryPoint = islandPath;
47
+ let wrapperPath = null;
48
+
49
+ const isSvelte = islandPath.endsWith('.svelte');
50
+ if (!isSvelte) {
51
+ const sourceCode = await fs.readFile(islandPath, 'utf-8');
52
+ if (!sourceCode.includes('customElements.define')) {
53
+ // Derive tag name from filename: NewsSlider.js → news-slider
54
+ const baseName = path.basename(islandPath).replace(/\.(js|jsx|ts|tsx|vue)$/, '');
55
+ const tagName = baseName
56
+ .replace(/([a-z0-9])([A-Z])/g, '$1-$2')
57
+ .toLowerCase();
58
+
59
+ // Create temporary wrapper entry file
60
+ wrapperPath = islandPath.replace(/\.(js|jsx|ts|tsx|vue)$/, '.__wm-wrapper.js');
61
+ const wrapperCode = `
62
+ import IslandClass from './${path.basename(islandPath)}';
63
+
64
+ class WmIsland extends HTMLElement {
65
+ connectedCallback() {
66
+ let props = {};
67
+ const propsJson = this.getAttribute('data-island-props');
68
+ if (propsJson) {
69
+ try { props = JSON.parse(propsJson); } catch {}
70
+ } else {
71
+ for (const attr of this.attributes) {
72
+ if (attr.name === 'data-island-props') continue;
73
+ try { props[attr.name] = JSON.parse(attr.value); }
74
+ catch { props[attr.name] = attr.value; }
75
+ }
76
+ }
77
+ try {
78
+ this._instance = new IslandClass(this, props);
79
+ } catch (err) {
80
+ console.error('[Island] Failed to initialize <${tagName}>:', err);
81
+ }
82
+ }
83
+ disconnectedCallback() {
84
+ if (this._instance && typeof this._instance.destroy === 'function') {
85
+ this._instance.destroy();
86
+ }
87
+ }
88
+ }
89
+ customElements.define('${tagName}', WmIsland);
90
+ `;
91
+ await fs.writeFile(wrapperPath, wrapperCode, 'utf-8');
92
+ entryPoint = wrapperPath;
93
+ console.log(pc.dim(` ↳ Wrapping ${baseName} as Custom Element <${tagName}>`));
94
+ }
95
+ }
96
+
44
97
  // Determine if this file should use JSX loader
45
98
  // Only use JSX for .jsx files (React/Preact), not for .js files (Lit/Alpine/Vue/Vanilla)
46
99
  const useJsxLoader = islandPath.endsWith('.jsx');
47
100
 
48
101
  const result = await esbuild.build({
49
- entryPoints: [islandPath],
102
+ entryPoints: [entryPoint],
50
103
  bundle: true,
51
104
  minify,
52
105
  sourcemap,
@@ -175,6 +228,11 @@ export async function bundleIsland(islandPath, outputPath, options = {}) {
175
228
  logLevel: 'warning'
176
229
  });
177
230
 
231
+ // Clean up temporary wrapper file
232
+ if (wrapperPath) {
233
+ await fs.unlink(wrapperPath).catch(() => {});
234
+ }
235
+
178
236
  return {
179
237
  success: true,
180
238
  outputPath,
@@ -182,6 +240,10 @@ export async function bundleIsland(islandPath, outputPath, options = {}) {
182
240
  warnings: result.warnings
183
241
  };
184
242
  } catch (error) {
243
+ // Clean up temporary wrapper file on error
244
+ if (wrapperPath) {
245
+ await fs.unlink(wrapperPath).catch(() => {});
246
+ }
185
247
  return {
186
248
  success: false,
187
249
  error: error.message,
@@ -47,53 +47,53 @@ export function cleanComponentHTML(html, islands = []) {
47
47
  * @returns {string} HTML with transformed islands
48
48
  */
49
49
  function transformIslandsToDataAttributes(html, availableIslands = []) {
50
- // Match self-closing PascalCase tags with attributes
51
- // Pattern: <ComponentName attrs... />
52
- return html.replace(/<([A-Z][a-zA-Z0-9]*)\s+([^>]*)\/>/g, (match, tagName, attrsString) => {
53
- // Check if this is a registered island
54
- if (!availableIslands.includes(tagName)) {
55
- return match; // Not an island, keep as-is
56
- }
57
-
58
- // Convert island name to kebab-case for Custom Element tag
59
- // SwiperTest → swiper-test
60
- // MyAwesomeComponent → my-awesome-component
50
+ // Convert PascalCase island name to kebab-case Custom Element tag
51
+ function toKebabTag(tagName) {
61
52
  let kebabTag = tagName
62
53
  .replace(/([A-Z])/g, '-$1')
63
54
  .toLowerCase()
64
55
  .replace(/^-/, '');
65
-
66
- // Svelte requires custom element names to be hyphenated
67
- // If there's no hyphen, add a prefix to make it valid
68
- // IMPORTANT: Must match the prefix logic in injectSvelteOptions()
56
+ // Custom elements must contain a hyphen
69
57
  if (!kebabTag.includes('-')) {
70
- kebabTag = 'wm-' + kebabTag; // e.g., "sidebar" → "wm-sidebar"
58
+ kebabTag = 'wm-' + kebabTag;
71
59
  }
60
+ return kebabTag;
61
+ }
72
62
 
73
- // Parse attributes and convert to HTML attributes
63
+ // Parse attributes string into HTML attributes
64
+ function parseAttrs(attrsString) {
74
65
  const attrs = [];
75
-
76
- // Match prop={value} or prop="value" patterns
77
66
  const attrPattern = /([a-z][a-zA-Z0-9]*)\s*=\s*(?:\{([^}]+)\}|"([^"]*)"|'([^']*)')/g;
78
67
  let attrMatch;
79
-
80
68
  while ((attrMatch = attrPattern.exec(attrsString)) !== null) {
81
69
  const propName = attrMatch[1];
82
- // Get value from whichever group matched (curly braces, double quotes, or single quotes)
83
70
  const propValue = attrMatch[2] || attrMatch[3] || attrMatch[4];
84
-
85
- // If it was in curly braces, keep the braces for runtime evaluation
86
- // Otherwise it's a literal string
87
71
  const value = attrMatch[2] ? `{${propValue}}` : propValue;
88
-
89
- // Add as HTML attribute
90
72
  attrs.push(`${propName}="${value}"`);
91
73
  }
74
+ return attrs.length > 0 ? ' ' + attrs.join(' ') : '';
75
+ }
92
76
 
93
- // Generate Custom Element tag
94
- const attrsHtml = attrs.length > 0 ? ' ' + attrs.join(' ') : '';
95
- return `<${kebabTag}${attrsHtml}></${kebabTag}>`;
77
+ // 1. Self-closing: <ComponentName attrs... />
78
+ html = html.replace(/<([A-Z][a-zA-Z0-9]*)\s+([^>]*)\/>/g, (match, tagName, attrsString) => {
79
+ if (!availableIslands.includes(tagName)) return match;
80
+ const kebabTag = toKebabTag(tagName);
81
+ return `<${kebabTag}${parseAttrs(attrsString)}></${kebabTag}>`;
96
82
  });
83
+
84
+ // 2. Opening + closing with children: <ComponentName attrs>...children...</ComponentName>
85
+ for (const islandName of availableIslands) {
86
+ const openPattern = new RegExp(`<${islandName}(\\s[^>]*)?>`, 'g');
87
+ const closePattern = new RegExp(`</${islandName}>`, 'g');
88
+ const kebabTag = toKebabTag(islandName);
89
+
90
+ html = html.replace(openPattern, (match, attrsString) => {
91
+ return `<${kebabTag}${parseAttrs(attrsString || '')}>`;
92
+ });
93
+ html = html.replace(closePattern, `</${kebabTag}>`);
94
+ }
95
+
96
+ return html;
97
97
  }
98
98
 
99
99
  /**