@webmate-studio/builder 0.2.170 → 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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webmate-studio/builder",
3
- "version": "0.2.170",
3
+ "version": "0.2.172",
4
4
  "type": "module",
5
5
  "description": "Webmate Studio Component Builder",
6
6
  "keywords": [
@@ -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 };