@webmate-studio/builder 0.2.171 → 0.2.172
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 +11 -1
- package/package.json +1 -1
- package/src/html-cleaner.js +169 -0
- 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, getComponentSuffix } from './src/html-cleaner.js';
|
|
11
|
+
import { cleanComponentHTML, getComponentSuffix, extractAndScopeStyles } 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';
|
|
@@ -329,6 +329,16 @@ async function buildComponent(payload) {
|
|
|
329
329
|
console.log(`[Build Service] ✓ CSS → ${(css.length / 1024).toFixed(2)}kb`);
|
|
330
330
|
}
|
|
331
331
|
|
|
332
|
+
// Extract <style> blocks from HTML, scope them, and append to CSS
|
|
333
|
+
if (transformedHtml && componentMetadata?.id) {
|
|
334
|
+
const { html: htmlWithoutStyle, scopedCss } = extractAndScopeStyles(transformedHtml, componentMetadata.id);
|
|
335
|
+
if (scopedCss) {
|
|
336
|
+
transformedHtml = htmlWithoutStyle;
|
|
337
|
+
css = css ? css + '\n\n/* Component Scoped Styles */\n' + scopedCss : scopedCss;
|
|
338
|
+
console.log(`[Build Service] ✓ Appended scoped component styles (${(scopedCss.length / 1024).toFixed(2)}kb)`);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
332
342
|
return {
|
|
333
343
|
success: true,
|
|
334
344
|
bundledIslands,
|
package/package.json
CHANGED
package/src/html-cleaner.js
CHANGED
|
@@ -222,3 +222,172 @@ export function extractStyles(html) {
|
|
|
222
222
|
css: css.trim()
|
|
223
223
|
};
|
|
224
224
|
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Scope CSS rules so they only apply within a specific component.
|
|
228
|
+
* Prepends each selector with [data-wmc-{hash}].
|
|
229
|
+
* Skips @-rules like @keyframes, @font-face, @media (but scopes rules inside @media).
|
|
230
|
+
* Drops element-only selectors like 'body', 'html' that can't be scoped.
|
|
231
|
+
*
|
|
232
|
+
* @param {string} css - Raw CSS from <style> block
|
|
233
|
+
* @param {string} scopeAttr - Scope attribute value, e.g. "a278"
|
|
234
|
+
* @returns {string} Scoped CSS
|
|
235
|
+
*/
|
|
236
|
+
export function scopeCSS(css, scopeAttr) {
|
|
237
|
+
const scope = `[data-wmc-${scopeAttr}]`;
|
|
238
|
+
|
|
239
|
+
// Element-only selectors that should be dropped (they target outside the component)
|
|
240
|
+
const globalElements = new Set(['html', 'body', ':root', ':host']);
|
|
241
|
+
|
|
242
|
+
function scopeSelector(selector) {
|
|
243
|
+
selector = selector.trim();
|
|
244
|
+
if (!selector) return '';
|
|
245
|
+
|
|
246
|
+
// :global(...) — unwrap and don't scope (like Svelte)
|
|
247
|
+
const globalMatch = selector.match(/^:global\((.+)\)$/);
|
|
248
|
+
if (globalMatch) {
|
|
249
|
+
return globalMatch[1];
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Drop pure element selectors that target outside the component
|
|
253
|
+
if (globalElements.has(selector.split(/[\s>+~.#\[:]/)[0].toLowerCase()) &&
|
|
254
|
+
!selector.includes('.') && !selector.includes('#') && !selector.includes('[')) {
|
|
255
|
+
return '';
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Prepend scope to each selector part (comma-separated handled by caller)
|
|
259
|
+
return `${scope} ${selector}`;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function scopeBlock(block) {
|
|
263
|
+
let result = '';
|
|
264
|
+
let i = 0;
|
|
265
|
+
|
|
266
|
+
while (i < block.length) {
|
|
267
|
+
// Skip whitespace
|
|
268
|
+
if (/\s/.test(block[i])) {
|
|
269
|
+
result += block[i];
|
|
270
|
+
i++;
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// @-rules
|
|
275
|
+
if (block[i] === '@') {
|
|
276
|
+
const atRuleMatch = block.substring(i).match(/^@([\w-]+)\s*/);
|
|
277
|
+
if (atRuleMatch) {
|
|
278
|
+
const atName = atRuleMatch[1].toLowerCase();
|
|
279
|
+
|
|
280
|
+
// @media, @supports, @layer — scope contents recursively
|
|
281
|
+
if (['media', 'supports', 'layer', 'container'].includes(atName)) {
|
|
282
|
+
const braceStart = block.indexOf('{', i);
|
|
283
|
+
if (braceStart === -1) break;
|
|
284
|
+
const header = block.substring(i, braceStart + 1);
|
|
285
|
+
|
|
286
|
+
let depth = 1;
|
|
287
|
+
let j = braceStart + 1;
|
|
288
|
+
while (j < block.length && depth > 0) {
|
|
289
|
+
if (block[j] === '{') depth++;
|
|
290
|
+
else if (block[j] === '}') depth--;
|
|
291
|
+
j++;
|
|
292
|
+
}
|
|
293
|
+
const innerContent = block.substring(braceStart + 1, j - 1);
|
|
294
|
+
result += header + scopeBlock(innerContent) + '}';
|
|
295
|
+
i = j;
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// @keyframes, @font-face — pass through without scoping
|
|
300
|
+
if (['keyframes', 'font-face'].includes(atName)) {
|
|
301
|
+
const braceStart = block.indexOf('{', i);
|
|
302
|
+
if (braceStart === -1) break;
|
|
303
|
+
let depth = 1;
|
|
304
|
+
let j = braceStart + 1;
|
|
305
|
+
while (j < block.length && depth > 0) {
|
|
306
|
+
if (block[j] === '{') depth++;
|
|
307
|
+
else if (block[j] === '}') depth--;
|
|
308
|
+
j++;
|
|
309
|
+
}
|
|
310
|
+
result += block.substring(i, j);
|
|
311
|
+
i = j;
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Regular CSS rule: selector { declarations }
|
|
318
|
+
const braceStart = block.indexOf('{', i);
|
|
319
|
+
if (braceStart === -1) {
|
|
320
|
+
result += block.substring(i);
|
|
321
|
+
break;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const selectorText = block.substring(i, braceStart).trim();
|
|
325
|
+
|
|
326
|
+
// Find matching closing brace (handle nested braces for &)
|
|
327
|
+
let depth = 1;
|
|
328
|
+
let j = braceStart + 1;
|
|
329
|
+
while (j < block.length && depth > 0) {
|
|
330
|
+
if (block[j] === '{') depth++;
|
|
331
|
+
else if (block[j] === '}') depth--;
|
|
332
|
+
j++;
|
|
333
|
+
}
|
|
334
|
+
const declarations = block.substring(braceStart + 1, j - 1);
|
|
335
|
+
|
|
336
|
+
// Scope each comma-separated selector
|
|
337
|
+
const scopedSelectors = selectorText
|
|
338
|
+
.split(',')
|
|
339
|
+
.map(scopeSelector)
|
|
340
|
+
.filter(Boolean)
|
|
341
|
+
.join(', ');
|
|
342
|
+
|
|
343
|
+
if (scopedSelectors) {
|
|
344
|
+
result += scopedSelectors + ' {' + declarations + '}';
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
i = j;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
return result;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
return scopeBlock(css);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Extract <style> blocks from component HTML, scope the CSS, and add scope attribute to root element.
|
|
358
|
+
* Returns cleaned HTML (without <style>) and scoped CSS ready to append to component.css.
|
|
359
|
+
*
|
|
360
|
+
* @param {string} html - Component HTML (may contain <style> blocks)
|
|
361
|
+
* @param {string} componentId - Component UUID for generating scope hash
|
|
362
|
+
* @returns {{ html: string, scopedCss: string }} Cleaned HTML + scoped CSS
|
|
363
|
+
*/
|
|
364
|
+
export function extractAndScopeStyles(html, componentId) {
|
|
365
|
+
if (!componentId) {
|
|
366
|
+
return { html, scopedCss: '' };
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const { html: cleanHtml, css } = extractStyles(html);
|
|
370
|
+
|
|
371
|
+
if (!css) {
|
|
372
|
+
return { html, scopedCss: '' };
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const suffix = getComponentSuffix(componentId);
|
|
376
|
+
const scopedCss = scopeCSS(css, suffix);
|
|
377
|
+
|
|
378
|
+
// Add data-wmc-{hash} attribute to the first root element
|
|
379
|
+
const scopeAttr = `data-wmc-${suffix}`;
|
|
380
|
+
const rootTagMatch = cleanHtml.match(/^(\s*<\w+)/);
|
|
381
|
+
let scopedHtml = cleanHtml;
|
|
382
|
+
if (rootTagMatch) {
|
|
383
|
+
const firstTag = rootTagMatch[1];
|
|
384
|
+
scopedHtml = cleanHtml.replace(firstTag, `${firstTag} ${scopeAttr}`);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
console.log(`[HTML Cleaner] Extracted and scoped ${css.split('\n').length} lines of component CSS (scope: ${scopeAttr})`);
|
|
388
|
+
|
|
389
|
+
return {
|
|
390
|
+
html: scopedHtml,
|
|
391
|
+
scopedCss
|
|
392
|
+
};
|
|
393
|
+
}
|
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, getComponentSuffix } from './html-cleaner.js';
|
|
3
|
+
import { cleanComponentHTML, getComponentSuffix, extractAndScopeStyles, scopeCSS } 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, getComponentSuffix, bundleIsland, bundleComponentIslands, deduplicateCSS, markdownToHtml, processMarkdownProps, SafeHtml, getMotionRuntime, TemplateProcessor, templateProcessor, defaultDesignTokens, generateTailwindV4Theme, generateTailwindConfig, generateCSSFromTokens, generateFontImports };
|
|
29
|
+
export { build, generateComponentCSS, generateTailwindCSS, extractTailwindClasses, cleanComponentHTML, getComponentSuffix, extractAndScopeStyles, scopeCSS, 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 };
|