@webmate-studio/builder 0.2.19 → 0.2.21

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.19",
3
+ "version": "0.2.21",
4
4
  "type": "module",
5
5
  "description": "Webmate Studio Component Builder",
6
6
  "keywords": [
package/src/bundler.js CHANGED
@@ -3,6 +3,17 @@ import esbuildSvelte from 'esbuild-svelte';
3
3
  import fs from 'fs/promises';
4
4
  import path from 'path';
5
5
  import pc from 'picocolors';
6
+ import crypto from 'crypto';
7
+
8
+ /**
9
+ * Generate content hash for asset file
10
+ * @param {Buffer} content - File content as buffer
11
+ * @param {number} length - Hash length (default: 8)
12
+ * @returns {string} - Content hash (hex)
13
+ */
14
+ function generateAssetHash(content, length = 8) {
15
+ return crypto.createHash('sha256').update(content).digest('hex').substring(0, length);
16
+ }
6
17
 
7
18
  /**
8
19
  * Bundle island JavaScript files with esbuild
@@ -71,39 +82,48 @@ export async function bundleIsland(islandPath, outputPath, options = {}) {
71
82
  '__VUE_PROD_HYDRATION_MISMATCH_DETAILS__': 'false'
72
83
  },
73
84
  plugins: [
74
- // Image to base64 data URL loader
75
- // esbuild's 'dataurl' loader fails for:
76
- // - SVG: generates invalid data:image/svg+xml,<?xml... (unescaped XML)
77
- // - JPG/PNG/etc: generates invalid data:image/jpeg,���� (raw binary instead of base64)
78
- // Solution: Custom loader that properly base64 encodes ALL image formats
85
+ // Asset URL resolver with content hashing
86
+ // Instead of base64 encoding (bad for large images, no caching, huge bundles),
87
+ // we use asset markers with content hashes that get resolved at runtime:
88
+ // - wm dev: /assets/{componentName}/{filename} (no hash in dev)
89
+ // - CMS: /api/components/proxy/organization-components/{uuid}/assets/{filename}.{hash}.{ext}
79
90
  {
80
- name: 'image-base64-dataurl',
91
+ name: 'asset-url-resolver',
81
92
  setup(build) {
82
93
  // Handle all image formats
83
94
  build.onLoad({ filter: /\.(svg|jpg|jpeg|png|gif|webp)$/i }, async (args) => {
84
- const ext = args.path.split('.').pop().toLowerCase();
85
- const mimeTypes = {
86
- svg: 'image/svg+xml',
87
- jpg: 'image/jpeg',
88
- jpeg: 'image/jpeg',
89
- png: 'image/png',
90
- gif: 'image/gif',
91
- webp: 'image/webp'
92
- };
93
-
94
- // Read file as binary buffer
95
+ // Read file to generate content hash
95
96
  const buffer = await fs.readFile(args.path);
96
- // Convert to base64 string (pure ASCII, safe for JavaScript)
97
- const base64 = buffer.toString('base64');
98
- const mimeType = mimeTypes[ext] || 'application/octet-stream';
97
+ const hash = generateAssetHash(buffer);
98
+
99
+ // Get relative path from component directory
100
+ // Example: /full/path/components/Hero/assets/logo.svg
101
+ // → Find "assets/" segment and extract path from there
102
+ const assetPath = args.path;
103
+ const assetsIndex = assetPath.lastIndexOf('/assets/');
104
+
105
+ if (assetsIndex === -1) {
106
+ throw new Error(`Asset must be in "assets/" directory: ${assetPath}`);
107
+ }
108
+
109
+ // Extract relative path from assets/ onwards
110
+ // Example: assets/logo.svg
111
+ const relativePath = assetPath.substring(assetsIndex + 1); // +1 to keep "assets/"
112
+
113
+ // Parse filename and extension
114
+ const filename = path.basename(relativePath);
115
+ const ext = path.extname(filename);
116
+ const nameWithoutExt = path.basename(filename, ext);
117
+ const dir = path.dirname(relativePath);
99
118
 
100
- // Build the complete data URL
101
- const dataUrl = `data:${mimeType};base64,${base64}`;
119
+ // Build hashed filename: logo.abc123.svg
120
+ const hashedFilename = `${nameWithoutExt}.${hash}${ext}`;
121
+ const hashedPath = path.join(dir, hashedFilename);
102
122
 
103
- // Return as a raw JavaScript module with template literal to preserve exact bytes
104
- // Using template literal ensures no escaping or encoding happens
123
+ // Return as asset marker (will be resolved at runtime)
124
+ // Runtime resolver will replace __ASSET__: with correct base path
105
125
  return {
106
- contents: `export default \`${dataUrl}\`;`,
126
+ contents: `export default "__ASSET__:${hashedPath}";`,
107
127
  loader: 'js'
108
128
  };
109
129
  });
@@ -32,8 +32,11 @@ export function cleanComponentHTML(html, islands = []) {
32
32
  }
33
33
 
34
34
  /**
35
- * Transform PascalCase island elements to data-island attributes
36
- * Example: <SwiperTest title={title} /> → <div data-island="swipertest" data-island-props='{"title":"{title}"}'></div>
35
+ * Transform PascalCase island elements to Custom Elements (Web Components)
36
+ * Example: <SwiperTest title={title} /> → <swiper-test title="{title}"></swiper-test>
37
+ *
38
+ * Islands with <svelte:options customElement> self-register as Custom Elements,
39
+ * so we convert to kebab-case tags with inline props.
37
40
  *
38
41
  * @param {string} html - Original HTML
39
42
  * @param {Array<string>} availableIslands - List of available island names
@@ -48,8 +51,16 @@ function transformIslandsToDataAttributes(html, availableIslands = []) {
48
51
  return match; // Not an island, keep as-is
49
52
  }
50
53
 
51
- // Parse attributes to JSON props
52
- const props = {};
54
+ // Convert island name to kebab-case for Custom Element tag
55
+ // SwiperTest swiper-test
56
+ // MyAwesomeComponent → my-awesome-component
57
+ const kebabTag = tagName
58
+ .replace(/([A-Z])/g, '-$1')
59
+ .toLowerCase()
60
+ .replace(/^-/, '');
61
+
62
+ // Parse attributes and convert to HTML attributes
63
+ const attrs = [];
53
64
 
54
65
  // Match prop={value} or prop="value" patterns
55
66
  const attrPattern = /([a-z][a-zA-Z0-9]*)\s*=\s*(?:\{([^}]+)\}|"([^"]*)"|'([^']*)')/g;
@@ -62,17 +73,15 @@ function transformIslandsToDataAttributes(html, availableIslands = []) {
62
73
 
63
74
  // If it was in curly braces, keep the braces for runtime evaluation
64
75
  // Otherwise it's a literal string
65
- props[propName] = attrMatch[2] ? `{${propValue}}` : propValue;
66
- }
76
+ const value = attrMatch[2] ? `{${propValue}}` : propValue;
67
77
 
68
- // Convert island name to lowercase for data-island attribute
69
- const islandName = tagName.toLowerCase();
70
-
71
- // Serialize props to JSON (escape quotes)
72
- const propsJson = JSON.stringify(props).replace(/"/g, '&quot;');
78
+ // Add as HTML attribute
79
+ attrs.push(`${propName}="${value}"`);
80
+ }
73
81
 
74
- // Generate data-island div
75
- return `<div data-island="${islandName}" data-island-props="${propsJson}"></div>`;
82
+ // Generate Custom Element tag
83
+ const attrsString = attrs.length > 0 ? ' ' + attrs.join(' ') : '';
84
+ return `<${kebabTag}${attrsString}></${kebabTag}>`;
76
85
  });
77
86
  }
78
87