@webmate-studio/builder 0.2.167 → 0.2.170

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/build-service.js CHANGED
@@ -8,7 +8,7 @@
8
8
  import http from 'http';
9
9
  import { generateComponentCSS } from './src/tailwind-generator.js';
10
10
  import { bundleIsland } from './src/bundler.js';
11
- import { cleanComponentHTML } from './src/html-cleaner.js';
11
+ import { cleanComponentHTML, getComponentSuffix } from './src/html-cleaner.js';
12
12
  import { mkdtemp, writeFile, rm, mkdir } from 'fs/promises';
13
13
  import { existsSync, readFileSync, writeFileSync } from 'fs';
14
14
  import { join } from 'path';
@@ -68,7 +68,7 @@ async function installDependencies(componentDir, packageJson) {
68
68
  * Converts filename to kebab-case tag name (e.g., SwiperTest.svelte → swiper-test)
69
69
  * Generates props definition from component.json metadata
70
70
  */
71
- function injectSvelteOptions(filename, content, componentMetadata) {
71
+ function injectSvelteOptions(filename, content, componentMetadata, componentId) {
72
72
  // Convert filename to kebab-case tag name
73
73
  // SwiperTest.svelte → swiper-test
74
74
  // MyAwesomeComponent.svelte → my-awesome-component
@@ -84,6 +84,11 @@ function injectSvelteOptions(filename, content, componentMetadata) {
84
84
  tagName = 'wm-' + tagName; // e.g., "button" → "wm-button"
85
85
  }
86
86
 
87
+ // Scope tag name with component suffix to avoid collisions
88
+ if (componentId) {
89
+ tagName = tagName + '-' + getComponentSuffix(componentId);
90
+ }
91
+
87
92
  // Generate props definition from component.json
88
93
  const props = {};
89
94
  if (componentMetadata.props) {
@@ -172,13 +177,31 @@ async function buildComponent(payload) {
172
177
  // These become Custom Elements, others are just utilities/sub-components
173
178
  const usedIslands = new Set();
174
179
  if (html) {
175
- // Find all custom elements in HTML (e.g. <MenuMobile />, <SearchModal data={...} />)
176
- // Match: <ComponentName ...> or <ComponentName .../> (with or without attributes)
177
- const componentTagRegex = /<([A-Z][a-zA-Z0-9]*)[^>]*?\/?>/g;
180
+ // Match PascalCase tags: <SliderMitText ...> or <SliderMitText .../>
181
+ const pascalCaseRegex = /<([A-Z][a-zA-Z0-9]*)[^>]*?\/?>/g;
178
182
  let match;
179
- while ((match = componentTagRegex.exec(html)) !== null) {
180
- const componentName = match[1]; // e.g. "MenuMobile"
181
- usedIslands.add(componentName);
183
+ while ((match = pascalCaseRegex.exec(html)) !== null) {
184
+ usedIslands.add(match[1]); // e.g. "SliderMitText"
185
+ }
186
+
187
+ // Also match kebab-case custom elements (HTML may already be transformed by Workbench)
188
+ // e.g. <slider-mit-text ...> or <wm-counter ...>
189
+ if (usedIslands.size === 0 && islands && islands.length > 0) {
190
+ const kebabRegex = /<([a-z][a-z0-9]*(?:-[a-z0-9]+)+)[^>]*?\/?>/g;
191
+ const kebabTags = new Set();
192
+ while ((match = kebabRegex.exec(html)) !== null) {
193
+ kebabTags.add(match[1]); // e.g. "slider-mit-text"
194
+ }
195
+
196
+ // Map island filenames to their kebab-case equivalents and check
197
+ for (const island of islands) {
198
+ const name = island.file.replace(/\.(jsx?|tsx?|svelte|vue)$/, '');
199
+ const kebab = name.replace(/([A-Z])/g, '-$1').toLowerCase().replace(/^-/, '');
200
+ const prefixed = 'wm-' + kebab;
201
+ if (kebabTags.has(kebab) || kebabTags.has(prefixed)) {
202
+ usedIslands.add(name); // Add PascalCase name for later matching
203
+ }
204
+ }
182
205
  }
183
206
  }
184
207
 
@@ -195,8 +218,9 @@ async function buildComponent(payload) {
195
218
 
196
219
  // Auto-inject <svelte:options> for Svelte islands
197
220
  let content = island.content;
221
+ const componentId = componentMetadata?.id;
198
222
  if (island.file.endsWith('.svelte') && componentMetadata && componentMetadata.props) {
199
- content = injectSvelteOptions(island.file, content, componentMetadata);
223
+ content = injectSvelteOptions(island.file, content, componentMetadata, componentId);
200
224
  }
201
225
 
202
226
  // Write source file
@@ -227,11 +251,22 @@ async function buildComponent(payload) {
227
251
 
228
252
  console.log(`[Build Service] Bundling ${island.file} (top-level island)...`);
229
253
 
254
+ // Compute scoped tag name for non-Svelte islands
255
+ const cId = componentMetadata?.id;
256
+ let scopedTagName;
257
+ if (cId && !island.file.endsWith('.svelte')) {
258
+ const baseName = island.file.replace(/\.(jsx?|tsx?|vue)$/, '');
259
+ let tag = baseName.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase();
260
+ if (!tag.includes('-')) tag = 'wm-' + tag;
261
+ scopedTagName = tag + '-' + getComponentSuffix(cId);
262
+ }
263
+
230
264
  // Bundle with esbuild (pass tmpDir for node_modules resolution)
231
265
  const result = await bundleIsland(inputPath, outputPath, {
232
266
  componentDir: tmpDir,
233
267
  minify: true,
234
- sourcemap: false
268
+ sourcemap: false,
269
+ scopedTagName
235
270
  });
236
271
 
237
272
  if (!result.success) {
@@ -263,8 +298,9 @@ async function buildComponent(payload) {
263
298
  let transformedHtml = html;
264
299
  if (html && islands.length > 0) {
265
300
  const islandNames = islands.map(i => i.file.replace(/\.(jsx?|tsx?|svelte|vue)$/, ''));
266
- transformedHtml = cleanComponentHTML(html, islandNames);
267
- console.log(`[Build Service] Transformed HTML: ${islandNames.length} island(s) → Custom Elements`);
301
+ const componentId = componentMetadata?.id;
302
+ transformedHtml = cleanComponentHTML(html, islandNames, componentId);
303
+ console.log(`[Build Service] ✓ Transformed HTML: ${islandNames.length} island(s) → Custom Elements${componentId ? ` (scoped: -${getComponentSuffix(componentId)})` : ''}`);
268
304
  }
269
305
 
270
306
  // Generate CSS from HTML + Islands
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webmate-studio/builder",
3
- "version": "0.2.167",
3
+ "version": "0.2.170",
4
4
  "type": "module",
5
5
  "description": "Webmate Studio Component Builder",
6
6
  "keywords": [
package/src/bundler.js CHANGED
@@ -26,7 +26,8 @@ export async function bundleIsland(islandPath, outputPath, options = {}) {
26
26
  target = 'es2020',
27
27
  format = 'esm',
28
28
  componentDir = null, // Component directory for component-specific node_modules
29
- svelteCustomElement = true // Set false for mount()-based islands (preview mode)
29
+ svelteCustomElement = true, // Set false for mount()-based islands (preview mode)
30
+ scopedTagName = null // Scoped custom element tag name (e.g. "wm-slide-550e")
30
31
  } = options;
31
32
 
32
33
  let entryPoint = islandPath;
@@ -53,9 +54,14 @@ export async function bundleIsland(islandPath, outputPath, options = {}) {
53
54
  if (!sourceCode.includes('customElements.define')) {
54
55
  // Derive tag name from filename: NewsSlider.js → news-slider
55
56
  const baseName = path.basename(islandPath).replace(/\.(js|jsx|ts|tsx|vue)$/, '');
56
- const tagName = baseName
57
+ let tagName = scopedTagName || baseName
57
58
  .replace(/([a-z0-9])([A-Z])/g, '$1-$2')
58
59
  .toLowerCase();
60
+ // Note: scopedTagName already includes the hyphen requirement,
61
+ // so we only need the wm- prefix check for the fallback path
62
+ if (!scopedTagName && !tagName.includes('-')) {
63
+ tagName = 'wm-' + tagName;
64
+ }
59
65
 
60
66
  // Create temporary wrapper entry file
61
67
  wrapperPath = islandPath.replace(/\.(js|jsx|ts|tsx|vue)$/, '.__wm-wrapper.js');
@@ -2,18 +2,32 @@ import { parseDocument } from 'htmlparser2';
2
2
  import { DomUtils } from 'htmlparser2';
3
3
  import render from 'dom-serializer';
4
4
 
5
+ /**
6
+ * Get a 4-char hex suffix from a component ID (UUID/CUID).
7
+ * Uses the first 4 hex characters after removing hyphens.
8
+ * Example: "550e8400-e29b-41d4-a716-446655440000" → "550e"
9
+ *
10
+ * @param {string} componentId - UUID or CUID string
11
+ * @returns {string} 4-char hex suffix
12
+ */
13
+ export function getComponentSuffix(componentId) {
14
+ const hex = componentId.replace(/-/g, '');
15
+ return hex.substring(0, 4).toLowerCase();
16
+ }
17
+
5
18
  /**
6
19
  * Clean component HTML by removing all wm: attributes
7
20
  * Keeps the HTML structure intact, only removes markers
8
21
  *
9
22
  * @param {string} html - Original component HTML
10
23
  * @param {Array<string>} islands - List of available island names (optional)
24
+ * @param {string} [componentId] - Component UUID for scoped island tag names
11
25
  * @returns {string} Cleaned HTML without wm: attributes
12
26
  */
13
- export function cleanComponentHTML(html, islands = []) {
27
+ export function cleanComponentHTML(html, islands = [], componentId) {
14
28
  // Transform PascalCase island elements to data-island FIRST
15
29
  if (islands && islands.length > 0) {
16
- html = transformIslandsToDataAttributes(html, islands);
30
+ html = transformIslandsToDataAttributes(html, islands, componentId);
17
31
  }
18
32
 
19
33
  // Remove wm: attributes using regex (avoids htmlparser2 breaking class: directives)
@@ -46,7 +60,10 @@ export function cleanComponentHTML(html, islands = []) {
46
60
  * @param {Array<string>} availableIslands - List of available island names
47
61
  * @returns {string} HTML with transformed islands
48
62
  */
49
- function transformIslandsToDataAttributes(html, availableIslands = []) {
63
+ function transformIslandsToDataAttributes(html, availableIslands = [], componentId) {
64
+ // Compute suffix once if componentId is provided
65
+ const suffix = componentId ? getComponentSuffix(componentId) : null;
66
+
50
67
  // Convert PascalCase island name to kebab-case Custom Element tag
51
68
  function toKebabTag(tagName) {
52
69
  let kebabTag = tagName
@@ -57,6 +74,10 @@ function transformIslandsToDataAttributes(html, availableIslands = []) {
57
74
  if (!kebabTag.includes('-')) {
58
75
  kebabTag = 'wm-' + kebabTag;
59
76
  }
77
+ // Scope tag name with component suffix to avoid collisions
78
+ if (suffix) {
79
+ kebabTag = kebabTag + '-' + suffix;
80
+ }
60
81
  return kebabTag;
61
82
  }
62
83
 
package/src/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { build } from './build.js';
2
2
  import { generateComponentCSS, generateTailwindCSS, extractTailwindClasses } from './tailwind-generator.js';
3
- import { cleanComponentHTML } from './html-cleaner.js';
3
+ import { cleanComponentHTML, getComponentSuffix } from './html-cleaner.js';
4
4
  import { bundleIsland, bundleComponentIslands } from './bundler.js';
5
5
  import { deduplicateCSS } from './css-deduplicator.js';
6
6
  import { markdownToHtml, processMarkdownProps } from './markdown.js';
@@ -26,7 +26,7 @@ function getMotionRuntime() {
26
26
  }
27
27
 
28
28
  // V1 exports (backward compatible)
29
- export { build, generateComponentCSS, generateTailwindCSS, extractTailwindClasses, cleanComponentHTML, bundleIsland, bundleComponentIslands, deduplicateCSS, markdownToHtml, processMarkdownProps, SafeHtml, getMotionRuntime, TemplateProcessor, templateProcessor, defaultDesignTokens, generateTailwindV4Theme, generateTailwindConfig, generateCSSFromTokens, generateFontImports };
29
+ export { build, generateComponentCSS, generateTailwindCSS, extractTailwindClasses, cleanComponentHTML, getComponentSuffix, bundleIsland, bundleComponentIslands, deduplicateCSS, markdownToHtml, processMarkdownProps, SafeHtml, getMotionRuntime, TemplateProcessor, templateProcessor, defaultDesignTokens, generateTailwindV4Theme, generateTailwindConfig, generateCSSFromTokens, generateFontImports };
30
30
 
31
31
  // V2 exports
32
32
  export { defaultDesignTokensV2, generateTailwindV4ThemeV2, generateCSSFromTokensV2, generateFontImportsV2, migrateDesignTokensV1toV2, validateDesignTokensV2, migrateResponsiveToFluid, generateColorScale, generateDarkColorScale, calculateOnColor, isV1Format, isV2Format, COLOR_WORLDS, SEMANTIC_COLOR_WORLDS, TEXT_VOICES, TEXT_LEVELS, BUTTON_VARIANTS, BUTTON_SIZES, DEFAULT_SEMANTIC_MAPPINGS, DEFAULT_STATUS_SEMANTIC_MAPPINGS, getDefaultMappingsForWorld, DEFAULT_TEXT_ALIASES };