@webmate-studio/builder 0.2.20 → 0.2.22

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
@@ -62,13 +62,72 @@ async function installDependencies(componentDir, packageJson) {
62
62
  }
63
63
  }
64
64
 
65
+ /**
66
+ * Auto-inject <svelte:options> for Web Component registration
67
+ * Converts filename to kebab-case tag name (e.g., SwiperTest.svelte → swiper-test)
68
+ * Generates props definition from component.json metadata
69
+ */
70
+ function injectSvelteOptions(filename, content, componentMetadata) {
71
+ // Convert filename to kebab-case tag name
72
+ // SwiperTest.svelte → swiper-test
73
+ // MyAwesomeComponent.svelte → my-awesome-component
74
+ const componentName = filename.replace('.svelte', '');
75
+ const tagName = componentName
76
+ .replace(/([A-Z])/g, '-$1') // Insert hyphen before capital letters
77
+ .toLowerCase()
78
+ .replace(/^-/, ''); // Remove leading hyphen
79
+
80
+ // Generate props definition from component.json
81
+ const props = {};
82
+ if (componentMetadata.props) {
83
+ // Props can be either an object or an array
84
+ const propsData = Array.isArray(componentMetadata.props)
85
+ ? componentMetadata.props
86
+ : Object.entries(componentMetadata.props).map(([name, config]) => ({ name, ...config }));
87
+
88
+ for (const prop of propsData) {
89
+ // Map prop types to Svelte types
90
+ const typeMap = {
91
+ 'text': 'String',
92
+ 'string': 'String',
93
+ 'number': 'Number',
94
+ 'boolean': 'Boolean',
95
+ 'array': 'Array',
96
+ 'object': 'Object'
97
+ };
98
+ const svelteType = typeMap[prop.type?.toLowerCase()] || 'String';
99
+ props[prop.name] = { type: svelteType };
100
+ }
101
+ }
102
+
103
+ // Generate <svelte:options> block
104
+ const propsJson = JSON.stringify(props, null, 3).replace(/^/gm, '\t\t\t'); // Indent props
105
+ const svelteOptions = `<svelte:options
106
+ customElement={{
107
+ tag: "${tagName}",
108
+ shadow: "none",
109
+ props: ${propsJson}
110
+ }}
111
+ />\n\n`;
112
+
113
+ // Check if <svelte:options> already exists
114
+ if (content.includes('<svelte:options')) {
115
+ console.log(`[Build Service] ℹ️ ${filename} already has <svelte:options>, skipping auto-inject`);
116
+ return content;
117
+ }
118
+
119
+ // Inject at the top of the file (before <script> or template)
120
+ console.log(`[Build Service] ✓ Auto-injected <svelte:options> for ${tagName}`);
121
+ return svelteOptions + content;
122
+ }
123
+
65
124
  /**
66
125
  * Build a component (islands + CSS)
67
126
  * @param {Object} payload - Build request payload
68
127
  * @returns {Object} Build result
69
128
  */
70
129
  async function buildComponent(payload) {
71
- const { packageJson, islands, html, componentName = 'component' } = payload;
130
+ const { packageJson, islands, html, assets, componentMetadata, componentName = 'component' } = payload;
72
131
 
73
132
  // Create temporary directory
74
133
  const tmpDir = await mkdtemp(join(tmpdir(), 'wm-build-'));
@@ -81,6 +140,28 @@ async function buildComponent(payload) {
81
140
  await installDependencies(tmpDir, packageJson);
82
141
  }
83
142
 
143
+ // Write assets to disk BEFORE building islands (islands may reference them)
144
+ if (assets && assets.length > 0) {
145
+ const assetsDir = join(tmpDir, 'assets');
146
+ await mkdir(assetsDir, { recursive: true });
147
+
148
+ for (const asset of assets) {
149
+ const assetPath = join(assetsDir, asset.file);
150
+
151
+ // Convert base64 back to Buffer for binary files
152
+ // Text files can be written directly as UTF-8
153
+ let content;
154
+ if (asset.encoding === 'base64') {
155
+ content = Buffer.from(asset.content, 'base64');
156
+ } else {
157
+ content = asset.content; // UTF-8 string
158
+ }
159
+
160
+ await writeFile(assetPath, content);
161
+ }
162
+ console.log(`[Build Service] Wrote ${assets.length} asset(s)`);
163
+ }
164
+
84
165
  // Build islands
85
166
  const bundledIslands = [];
86
167
  if (islands && islands.length > 0) {
@@ -91,8 +172,14 @@ async function buildComponent(payload) {
91
172
  const inputPath = join(islandsDir, island.file);
92
173
  const outputPath = join(islandsDir, island.file.replace(/\.(jsx?|tsx?|svelte|vue)$/, '.js'));
93
174
 
175
+ // Auto-inject <svelte:options> for Svelte islands
176
+ let content = island.content;
177
+ if (island.file.endsWith('.svelte') && componentMetadata && componentMetadata.props) {
178
+ content = injectSvelteOptions(island.file, content, componentMetadata);
179
+ }
180
+
94
181
  // Write source file
95
- await writeFile(inputPath, island.content);
182
+ await writeFile(inputPath, content);
96
183
 
97
184
  console.log(`[Build Service] Bundling ${island.file}...`);
98
185
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webmate-studio/builder",
3
- "version": "0.2.20",
3
+ "version": "0.2.22",
4
4
  "type": "module",
5
5
  "description": "Webmate Studio Component Builder",
6
6
  "keywords": [
@@ -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