@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 +48 -12
- package/package.json +1 -1
- package/src/bundler.js +8 -2
- package/src/html-cleaner.js +24 -3
- package/src/index.js +2 -2
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
|
-
//
|
|
176
|
-
|
|
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 =
|
|
180
|
-
|
|
181
|
-
|
|
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
|
-
|
|
267
|
-
|
|
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
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
|
-
|
|
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');
|
package/src/html-cleaner.js
CHANGED
|
@@ -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 };
|