@ikas/component-cli 1.4.0-beta.22 → 1.4.0-beta.220

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.
Files changed (204) hide show
  1. package/dist/build-tools.d.ts +37 -0
  2. package/dist/build-tools.d.ts.map +1 -0
  3. package/dist/build-tools.js +36 -0
  4. package/dist/build-tools.js.map +1 -0
  5. package/dist/commands/add-sections-to-page.d.ts +3 -0
  6. package/dist/commands/add-sections-to-page.d.ts.map +1 -0
  7. package/dist/commands/add-sections-to-page.js +39 -0
  8. package/dist/commands/add-sections-to-page.js.map +1 -0
  9. package/dist/commands/add.d.ts.map +1 -1
  10. package/dist/commands/add.js +11 -2
  11. package/dist/commands/add.js.map +1 -1
  12. package/dist/commands/build.d.ts.map +1 -1
  13. package/dist/commands/build.js +15 -167
  14. package/dist/commands/build.js.map +1 -1
  15. package/dist/commands/config-texts.d.ts +38 -0
  16. package/dist/commands/config-texts.d.ts.map +1 -0
  17. package/dist/commands/config-texts.js +186 -0
  18. package/dist/commands/config-texts.js.map +1 -0
  19. package/dist/commands/config.d.ts +45 -0
  20. package/dist/commands/config.d.ts.map +1 -1
  21. package/dist/commands/config.js +1148 -154
  22. package/dist/commands/config.js.map +1 -1
  23. package/dist/commands/create-design-tokens.d.ts +24 -0
  24. package/dist/commands/create-design-tokens.d.ts.map +1 -0
  25. package/dist/commands/create-design-tokens.js +388 -0
  26. package/dist/commands/create-design-tokens.js.map +1 -0
  27. package/dist/commands/create-global-variable.d.ts +3 -0
  28. package/dist/commands/create-global-variable.d.ts.map +1 -0
  29. package/dist/commands/create-global-variable.js +73 -0
  30. package/dist/commands/create-global-variable.js.map +1 -0
  31. package/dist/commands/create-page.d.ts +3 -0
  32. package/dist/commands/create-page.d.ts.map +1 -0
  33. package/dist/commands/create-page.js +31 -0
  34. package/dist/commands/create-page.js.map +1 -0
  35. package/dist/commands/delete-page.d.ts +3 -0
  36. package/dist/commands/delete-page.d.ts.map +1 -0
  37. package/dist/commands/delete-page.js +28 -0
  38. package/dist/commands/delete-page.js.map +1 -0
  39. package/dist/commands/delete-theme-globals.d.ts +4 -0
  40. package/dist/commands/delete-theme-globals.d.ts.map +1 -0
  41. package/dist/commands/delete-theme-globals.js +50 -0
  42. package/dist/commands/delete-theme-globals.js.map +1 -0
  43. package/dist/commands/dev.d.ts.map +1 -1
  44. package/dist/commands/dev.js +308 -39
  45. package/dist/commands/dev.js.map +1 -1
  46. package/dist/commands/enable-builtin-pages.d.ts +3 -0
  47. package/dist/commands/enable-builtin-pages.d.ts.map +1 -0
  48. package/dist/commands/enable-builtin-pages.js +28 -0
  49. package/dist/commands/enable-builtin-pages.js.map +1 -0
  50. package/dist/commands/get-component-props.d.ts +3 -0
  51. package/dist/commands/get-component-props.d.ts.map +1 -0
  52. package/dist/commands/get-component-props.js +32 -0
  53. package/dist/commands/get-component-props.js.map +1 -0
  54. package/dist/commands/get-page-by-type.d.ts +3 -0
  55. package/dist/commands/get-page-by-type.d.ts.map +1 -0
  56. package/dist/commands/get-page-by-type.js +25 -0
  57. package/dist/commands/get-page-by-type.js.map +1 -0
  58. package/dist/commands/get-section-values.d.ts +3 -0
  59. package/dist/commands/get-section-values.d.ts.map +1 -0
  60. package/dist/commands/get-section-values.js +39 -0
  61. package/dist/commands/get-section-values.js.map +1 -0
  62. package/dist/commands/list-builtin-pages.d.ts +3 -0
  63. package/dist/commands/list-builtin-pages.d.ts.map +1 -0
  64. package/dist/commands/list-builtin-pages.js +28 -0
  65. package/dist/commands/list-builtin-pages.js.map +1 -0
  66. package/dist/commands/list-entities.d.ts +3 -0
  67. package/dist/commands/list-entities.d.ts.map +1 -0
  68. package/dist/commands/list-entities.js +32 -0
  69. package/dist/commands/list-entities.js.map +1 -0
  70. package/dist/commands/list-page-sections.d.ts.map +1 -1
  71. package/dist/commands/list-page-sections.js +8 -2
  72. package/dist/commands/list-page-sections.js.map +1 -1
  73. package/dist/commands/list-theme-globals.d.ts +3 -0
  74. package/dist/commands/list-theme-globals.d.ts.map +1 -0
  75. package/dist/commands/list-theme-globals.js +22 -0
  76. package/dist/commands/list-theme-globals.js.map +1 -0
  77. package/dist/commands/list-translations.d.ts +3 -0
  78. package/dist/commands/list-translations.d.ts.map +1 -0
  79. package/dist/commands/list-translations.js +33 -0
  80. package/dist/commands/list-translations.js.map +1 -0
  81. package/dist/commands/move-page-section.d.ts +3 -0
  82. package/dist/commands/move-page-section.d.ts.map +1 -0
  83. package/dist/commands/move-page-section.js +62 -0
  84. package/dist/commands/move-page-section.js.map +1 -0
  85. package/dist/commands/publish-theme.d.ts +3 -0
  86. package/dist/commands/publish-theme.d.ts.map +1 -0
  87. package/dist/commands/publish-theme.js +29 -0
  88. package/dist/commands/publish-theme.js.map +1 -0
  89. package/dist/commands/publish.d.ts.map +1 -1
  90. package/dist/commands/publish.js +65 -3
  91. package/dist/commands/publish.js.map +1 -1
  92. package/dist/commands/remove-page-section.d.ts +3 -0
  93. package/dist/commands/remove-page-section.d.ts.map +1 -0
  94. package/dist/commands/remove-page-section.js +40 -0
  95. package/dist/commands/remove-page-section.js.map +1 -0
  96. package/dist/commands/search-products.d.ts +3 -0
  97. package/dist/commands/search-products.d.ts.map +1 -0
  98. package/dist/commands/search-products.js +40 -0
  99. package/dist/commands/search-products.js.map +1 -0
  100. package/dist/commands/translation-languages.d.ts +3 -0
  101. package/dist/commands/translation-languages.d.ts.map +1 -0
  102. package/dist/commands/translation-languages.js +43 -0
  103. package/dist/commands/translation-languages.js.map +1 -0
  104. package/dist/commands/update-builtin-page-prop.d.ts +3 -0
  105. package/dist/commands/update-builtin-page-prop.d.ts.map +1 -0
  106. package/dist/commands/update-builtin-page-prop.js +49 -0
  107. package/dist/commands/update-builtin-page-prop.js.map +1 -0
  108. package/dist/commands/update-global-variable.d.ts +3 -0
  109. package/dist/commands/update-global-variable.d.ts.map +1 -0
  110. package/dist/commands/update-global-variable.js +70 -0
  111. package/dist/commands/update-global-variable.js.map +1 -0
  112. package/dist/commands/update-page-sections.d.ts +3 -0
  113. package/dist/commands/update-page-sections.d.ts.map +1 -0
  114. package/dist/commands/update-page-sections.js +39 -0
  115. package/dist/commands/update-page-sections.js.map +1 -0
  116. package/dist/commands/update-section-prop.d.ts.map +1 -1
  117. package/dist/commands/update-section-prop.js +25 -8
  118. package/dist/commands/update-section-prop.js.map +1 -1
  119. package/dist/commands/update-translations.d.ts +3 -0
  120. package/dist/commands/update-translations.d.ts.map +1 -0
  121. package/dist/commands/update-translations.js +37 -0
  122. package/dist/commands/update-translations.js.map +1 -0
  123. package/dist/commands/upload-image.d.ts.map +1 -1
  124. package/dist/commands/upload-image.js +5 -46
  125. package/dist/commands/upload-image.js.map +1 -1
  126. package/dist/commands/upload-images.d.ts +3 -0
  127. package/dist/commands/upload-images.d.ts.map +1 -0
  128. package/dist/commands/upload-images.js +48 -0
  129. package/dist/commands/upload-images.js.map +1 -0
  130. package/dist/index.d.ts.map +1 -1
  131. package/dist/index.js +60 -1
  132. package/dist/index.js.map +1 -1
  133. package/dist/types.d.ts +77 -0
  134. package/dist/types.d.ts.map +1 -1
  135. package/dist/utils/archive-project.d.ts +50 -0
  136. package/dist/utils/archive-project.d.ts.map +1 -0
  137. package/dist/utils/archive-project.js +178 -0
  138. package/dist/utils/archive-project.js.map +1 -0
  139. package/dist/utils/compile.d.ts +17 -2
  140. package/dist/utils/compile.d.ts.map +1 -1
  141. package/dist/utils/compile.js +759 -117
  142. package/dist/utils/compile.js.map +1 -1
  143. package/dist/utils/component-helpers.d.ts +222 -4
  144. package/dist/utils/component-helpers.d.ts.map +1 -1
  145. package/dist/utils/component-helpers.js +558 -37
  146. package/dist/utils/component-helpers.js.map +1 -1
  147. package/dist/utils/css-import-resolver.d.ts +4 -1
  148. package/dist/utils/css-import-resolver.d.ts.map +1 -1
  149. package/dist/utils/css-import-resolver.js +9 -1
  150. package/dist/utils/css-import-resolver.js.map +1 -1
  151. package/dist/utils/dev-server-auth.d.ts +19 -0
  152. package/dist/utils/dev-server-auth.d.ts.map +1 -0
  153. package/dist/utils/dev-server-auth.js +60 -0
  154. package/dist/utils/dev-server-auth.js.map +1 -0
  155. package/dist/utils/editor-action-client.d.ts +4 -3
  156. package/dist/utils/editor-action-client.d.ts.map +1 -1
  157. package/dist/utils/editor-action-client.js +56 -19
  158. package/dist/utils/editor-action-client.js.map +1 -1
  159. package/dist/utils/esm-transform.d.ts +30 -0
  160. package/dist/utils/esm-transform.d.ts.map +1 -1
  161. package/dist/utils/esm-transform.js +169 -2
  162. package/dist/utils/esm-transform.js.map +1 -1
  163. package/dist/utils/find-component-for-file.d.ts +12 -0
  164. package/dist/utils/find-component-for-file.d.ts.map +1 -0
  165. package/dist/utils/find-component-for-file.js +24 -0
  166. package/dist/utils/find-component-for-file.js.map +1 -0
  167. package/dist/utils/forbid-deep-imports.d.ts +3 -0
  168. package/dist/utils/forbid-deep-imports.d.ts.map +1 -0
  169. package/dist/utils/forbid-deep-imports.js +54 -0
  170. package/dist/utils/forbid-deep-imports.js.map +1 -0
  171. package/dist/utils/forbid-external-packages.d.ts +26 -0
  172. package/dist/utils/forbid-external-packages.d.ts.map +1 -0
  173. package/dist/utils/forbid-external-packages.js +99 -0
  174. package/dist/utils/forbid-external-packages.js.map +1 -0
  175. package/dist/utils/i18n.d.ts +80 -0
  176. package/dist/utils/i18n.d.ts.map +1 -0
  177. package/dist/utils/i18n.js +265 -0
  178. package/dist/utils/i18n.js.map +1 -0
  179. package/dist/utils/load-image.d.ts +16 -0
  180. package/dist/utils/load-image.d.ts.map +1 -0
  181. package/dist/utils/load-image.js +50 -0
  182. package/dist/utils/load-image.js.map +1 -0
  183. package/dist/utils/storefront-t.d.ts +17 -0
  184. package/dist/utils/storefront-t.d.ts.map +1 -0
  185. package/dist/utils/storefront-t.js +186 -0
  186. package/dist/utils/storefront-t.js.map +1 -0
  187. package/dist/utils/vendor-storefront.d.ts +13 -0
  188. package/dist/utils/vendor-storefront.d.ts.map +1 -0
  189. package/dist/utils/vendor-storefront.js +91 -0
  190. package/dist/utils/vendor-storefront.js.map +1 -0
  191. package/dist/utils/websocket-server.d.ts +161 -7
  192. package/dist/utils/websocket-server.d.ts.map +1 -1
  193. package/dist/utils/websocket-server.js +324 -45
  194. package/dist/utils/websocket-server.js.map +1 -1
  195. package/package.json +19 -2
  196. package/templates/add/section.css +11 -0
  197. package/dist/commands/create.d.ts +0 -4
  198. package/dist/commands/create.d.ts.map +0 -1
  199. package/dist/commands/create.js +0 -228
  200. package/dist/commands/create.js.map +0 -1
  201. package/dist/commands/proxy.d.ts +0 -39
  202. package/dist/commands/proxy.d.ts.map +0 -1
  203. package/dist/commands/proxy.js +0 -210
  204. package/dist/commands/proxy.js.map +0 -1
@@ -4,10 +4,14 @@ import { Command } from "commander";
4
4
  import * as crypto from "crypto";
5
5
  import * as fs from "fs";
6
6
  import * as path from "path";
7
- import { toPascalCase, generateTypesFile, generateGlobalTypesFile, collectUsedEnumIds, collectUsedCustomTypeIds, generateComponentFile, generateStylesFile, updateBarrelExport, generateProjectId, generateComponentId, generateUniqueId, buildPropGroupHierarchy, findPropGroup, collectPropGroupIds, movePropGroupInTree, validateFilteredComponentIds, } from "../utils/component-helpers.js";
7
+ import { toPascalCase, generateTypesFile, generateGlobalTypesFile, collectUsedEnumIds, collectUsedCustomTypeIds, shouldWriteGlobalTypes, generateComponentFile, generateStylesFile, updateBarrelExport, generateProjectId, isValidProjectId, findInvalidComponentIds, findIdentityChanges, COMPONENT_ID_HINT, generateComponentId, generateUniqueId, buildPropGroupHierarchy, propIdsByName, resolveCondition, applyTechnicalTexts, translationRecords, visibilityGateError, findPropGroup, collectPropGroupIds, movePropGroupInTree, validateFilteredComponentIds, isBuiltInEnumTypeId, } from "../utils/component-helpers.js";
8
+ import { findComponentForFile } from "../utils/find-component-for-file.js";
9
+ import { archiveProject, removeArchive } from "../utils/archive-project.js";
8
10
  import { compileAllComponents as compileAllComponentsMulti } from "../utils/compile.js";
9
- import { transformEsmToWindowGlobals, transformChunkForCanvas, buildChunkMapping, } from "../utils/esm-transform.js";
11
+ import { transformEsmToWindowGlobals, transformChunkForCanvas, buildChunkMapping, findComputedDynamicImportsInBundles, } from "../utils/esm-transform.js";
10
12
  import { DevServerWebSocket, } from "../utils/websocket-server.js";
13
+ import { i18nDir, loadProjectI18n, writeEditorI18n, } from "../utils/i18n.js";
14
+ import { newDevServerToken, removeDevServerToken, writeDevServerToken } from "../utils/dev-server-auth.js";
11
15
  const DEV_SERVER_PORT = 5201;
12
16
  // Store compiled components, shared modules, and global styles
13
17
  const compiledComponents = new Map();
@@ -20,10 +24,16 @@ let compiledGlobalStyles;
20
24
  const lastBroadcastComponentHashes = new Map();
21
25
  const lastBroadcastSharedHashes = new Map();
22
26
  let lastBroadcastGlobalStyleHash;
27
+ // The project's UI texts (`config.i18n`), as last read, and what the editors were last sent
28
+ let projectI18n;
29
+ let lastBroadcastI18nHash;
23
30
  // Cached types list received from the editor via sync-types
24
31
  let cachedEditorTypes = null;
25
32
  // Cached custom type definitions received from the editor via sync-types (full IType objects)
26
33
  let cachedCustomTypes = null;
34
+ // Cached theme global variables received from the editor via sync-types.
35
+ // Drives the ThemeSettingName union in src/global-types.ts.
36
+ let cachedThemeSettings = null;
27
37
  const LIBRARY_TYPE_PREFIX = "@ikas/";
28
38
  /**
29
39
  * Match custom types for a component's props from the cached custom types.
@@ -77,24 +87,6 @@ function matchCustomTypesForProps(props, allCustomTypes) {
77
87
  }
78
88
  return Array.from(collected.values());
79
89
  }
80
- /**
81
- * Get the component definition that contains a file path
82
- */
83
- function findComponentForFile(config, filePath) {
84
- const normalizedPath = path.normalize(filePath);
85
- for (const component of config.components) {
86
- const entryPath = path.resolve(process.cwd(), component.entry);
87
- const stylesPath = path.resolve(process.cwd(), component.styles);
88
- const componentDir = path.dirname(entryPath);
89
- // Check if the file is the entry, styles, or in the component directory
90
- if (normalizedPath === entryPath ||
91
- normalizedPath === stylesPath ||
92
- normalizedPath.startsWith(componentDir + path.sep)) {
93
- return component;
94
- }
95
- }
96
- return null;
97
- }
98
90
  /**
99
91
  * Compile all components using multi-entry code splitting.
100
92
  * Updates compiledComponents and compiledSharedModules maps.
@@ -136,6 +128,18 @@ async function compileAllComponentsForDev(config) {
136
128
  if (chunkIds.length > 0) {
137
129
  console.log(chalk.cyan(` Extracted ${chunkIds.length} shared chunk(s)`));
138
130
  }
131
+ // Nothing downstream can resolve a computed specifier, so it 404s on the first lazy path that
132
+ // reaches it. Same pass `publish` runs, on the same two maps, so the two cannot drift apart —
133
+ // and both only warn for now (see the note in publish.ts on why that gate is not yet an error).
134
+ // Warning here would be right regardless: killing the watch loop over it is worse than a canvas
135
+ // that half-works, and the developer is right here to read the line.
136
+ const computedImports = findComputedDynamicImportsInBundles(result.chunks, result.entries, new Map(config.components.map((c) => [c.id, c.name])));
137
+ if (computedImports.length > 0) {
138
+ console.warn(chalk.yellow(` ${computedImports.length} dynamic import(s) with a computed specifier — these will 404 in the canvas. Use a literal, e.g. import("./thing.js") not import(name):`));
139
+ for (const offender of computedImports) {
140
+ console.warn(chalk.gray(` ${offender}`));
141
+ }
142
+ }
139
143
  // Store global CSS (unscoped). Preserve `updatedAt` across rebuilds when the compiled
140
144
  // global CSS is byte-identical so the editor only sees a fresh timestamp when the
141
145
  // designer actually changed `global.css` — the publish UI watches this timestamp to
@@ -176,6 +180,12 @@ async function compileAllComponentsForDev(config) {
176
180
  const canvasClientJs = transformEsmToWindowGlobals(entry.clientJs, {
177
181
  chunkMapping,
178
182
  });
183
+ // Config names a prop's visibility gate; the editor addresses props by id, so resolve the one
184
+ // against the other off the same index formula the ids come from.
185
+ const propIdByName = propIdsByName(component.props.map((prop, index) => ({
186
+ id: `${component.id}_prop_${index}`,
187
+ name: prop.name,
188
+ })));
179
189
  const propsWithIds = component.props.map((prop, index) => ({
180
190
  id: `${component.id}_prop_${index}`,
181
191
  name: prop.name,
@@ -187,10 +197,15 @@ async function compileAllComponentsForDev(config) {
187
197
  groupId: prop.groupId,
188
198
  typeId: prop.typeId,
189
199
  enumTypeId: prop.enumTypeId,
200
+ ...(prop.numberRangeData
201
+ ? { numberRangeData: prop.numberRangeData }
202
+ : {}),
190
203
  ...(prop.privateVarMap ? { privateVarMap: prop.privateVarMap } : {}),
191
204
  ...(prop.filteredComponentIds
192
205
  ? { filteredComponentIds: prop.filteredComponentIds }
193
206
  : {}),
207
+ ...resolveCondition(prop, propIdByName),
208
+ ...translationRecords(prop.translations),
194
209
  }));
195
210
  // Build prop group hierarchy if groups are defined
196
211
  const propGroups = component.propGroups && component.propGroups.length > 0
@@ -200,7 +215,7 @@ async function compileAllComponentsForDev(config) {
200
215
  const componentCustomTypes = cachedCustomTypes
201
216
  ? matchCustomTypesForProps(propsWithIds, cachedCustomTypes)
202
217
  : undefined;
203
- const usedSharedModuleIds = (result.entryChunkIds.get(component.id) || []).map((id) => config.projectId ? `${config.projectId}-${id}` : id);
218
+ const usedSharedModuleIds = (result.entryChunkIds.get(component.id) || []).map((id) => (config.projectId ? `${config.projectId}-${id}` : id));
204
219
  const data = {
205
220
  id: component.id,
206
221
  name: component.name,
@@ -215,6 +230,7 @@ async function compileAllComponentsForDev(config) {
215
230
  type: component.type || "component",
216
231
  ...(component.isHeader ? { isHeader: true } : {}),
217
232
  ...(component.isFooter ? { isFooter: true } : {}),
233
+ ...translationRecords(component.translations),
218
234
  propGroups,
219
235
  configPropGroups: component.propGroups && component.propGroups.length > 0
220
236
  ? component.propGroups
@@ -253,6 +269,10 @@ function rebuildCompiledComponentFromConfig(config, componentId) {
253
269
  const component = config.components.find((c) => c.id === componentId);
254
270
  if (!component)
255
271
  return null;
272
+ const propIdByName = propIdsByName(component.props.map((prop, index) => ({
273
+ id: `${component.id}_prop_${index}`,
274
+ name: prop.name,
275
+ })));
256
276
  const propsWithIds = component.props.map((prop, index) => ({
257
277
  id: `${component.id}_prop_${index}`,
258
278
  name: prop.name,
@@ -264,10 +284,13 @@ function rebuildCompiledComponentFromConfig(config, componentId) {
264
284
  groupId: prop.groupId,
265
285
  typeId: prop.typeId,
266
286
  enumTypeId: prop.enumTypeId,
287
+ ...(prop.numberRangeData ? { numberRangeData: prop.numberRangeData } : {}),
267
288
  ...(prop.privateVarMap ? { privateVarMap: prop.privateVarMap } : {}),
268
289
  ...(prop.filteredComponentIds
269
290
  ? { filteredComponentIds: prop.filteredComponentIds }
270
291
  : {}),
292
+ ...resolveCondition(prop, propIdByName),
293
+ ...translationRecords(prop.translations),
271
294
  }));
272
295
  const propGroups = component.propGroups && component.propGroups.length > 0
273
296
  ? buildPropGroupHierarchy(component.id, component.propGroups, propsWithIds)
@@ -277,6 +300,7 @@ function rebuildCompiledComponentFromConfig(config, componentId) {
277
300
  : undefined;
278
301
  const updated = {
279
302
  ...existing,
303
+ translations: translationRecords(component.translations).translations,
280
304
  props: propsWithIds,
281
305
  propGroups,
282
306
  configPropGroups: component.propGroups && component.propGroups.length > 0
@@ -310,7 +334,13 @@ function sha256Hex(input) {
310
334
  */
311
335
  function populateContentHashes() {
312
336
  for (const mod of compiledSharedModules.values()) {
313
- mod.contentHash = sha256Hex([mod.id, mod.compiledServerJs, mod.compiledClientJs, mod.canvasClientJs, mod.compiledCss].join("\0"));
337
+ mod.contentHash = sha256Hex([
338
+ mod.id,
339
+ mod.compiledServerJs,
340
+ mod.compiledClientJs,
341
+ mod.canvasClientJs,
342
+ mod.compiledCss,
343
+ ].join("\0"));
314
344
  }
315
345
  for (const comp of compiledComponents.values()) {
316
346
  const referencedHashes = (comp.usedSharedModuleIds || [])
@@ -330,6 +360,7 @@ function populateContentHashes() {
330
360
  comp.compiledCss,
331
361
  comp.canvasClientJs,
332
362
  JSON.stringify(comp.props),
363
+ JSON.stringify(comp.translations || null),
333
364
  JSON.stringify(comp.propGroups || null),
334
365
  JSON.stringify(comp.configPropGroups || null),
335
366
  JSON.stringify(comp.customTypes || null),
@@ -425,9 +456,11 @@ function broadcastCompileDelta(wsServer) {
425
456
  components: getAllComponents(),
426
457
  sharedModules: getAllSharedModules(),
427
458
  globalStyles: compiledGlobalStyles,
459
+ i18n: projectI18n ?? null,
428
460
  timestamp: Date.now(),
429
461
  },
430
462
  });
463
+ lastBroadcastI18nHash = i18nHash();
431
464
  lastBroadcastComponentHashes.clear();
432
465
  for (const [id, comp] of compiledComponents) {
433
466
  if (comp.contentHash)
@@ -442,6 +475,25 @@ function broadcastCompileDelta(wsServer) {
442
475
  lastBroadcastGlobalStyleHash = currentGlobalHash;
443
476
  return result;
444
477
  }
478
+ const i18nHash = () => sha256Hex(JSON.stringify(projectI18n ?? null));
479
+ /** Reads the project's UI texts again (and writes their `index.d.ts`); a problem is shown, the rest is kept. */
480
+ function reloadI18n(config) {
481
+ const { data, errors } = loadProjectI18n(config, projectI18n);
482
+ errors.forEach((error) => console.warn(chalk.yellow(` i18n: ${error}`)));
483
+ projectI18n = data;
484
+ }
485
+ /** Sends the UI texts to the editors when they changed since they were last sent. */
486
+ function broadcastI18n(wsServer) {
487
+ const hash = i18nHash();
488
+ if (hash === lastBroadcastI18nHash)
489
+ return;
490
+ wsServer.broadcast({
491
+ type: "i18n-update",
492
+ payload: { i18n: projectI18n ?? null, timestamp: Date.now() },
493
+ });
494
+ lastBroadcastI18nHash = hash;
495
+ console.log(chalk.cyan(" Broadcast UI texts"));
496
+ }
445
497
  /**
446
498
  * Read and return the current config from disk
447
499
  */
@@ -461,12 +513,20 @@ function writeConfig(configPath, config) {
461
513
  function writeGlobalTypesFile(config) {
462
514
  const globalTypesPath = path.resolve(process.cwd(), "src/global-types.ts");
463
515
  const usedEnumIds = collectUsedEnumIds(config.components);
464
- const content = generateGlobalTypesFile(cachedEditorTypes, cachedCustomTypes, usedEnumIds);
465
- // Avoid unnecessary writes (prevents file watcher loops)
466
- if (fs.existsSync(globalTypesPath)) {
467
- const existing = fs.readFileSync(globalTypesPath, "utf-8");
468
- if (existing === content)
469
- return;
516
+ const content = generateGlobalTypesFile(cachedEditorTypes, cachedCustomTypes, usedEnumIds, cachedThemeSettings);
517
+ // Single read instead of existsSync + readFileSync; absent file reads as null.
518
+ let existing = null;
519
+ try {
520
+ existing = fs.readFileSync(globalTypesPath, "utf-8");
521
+ }
522
+ catch {
523
+ existing = null;
524
+ }
525
+ // Skip unnecessary writes (file-watcher loops) and, crucially, don't let the
526
+ // startup write clobber editor-synced theme globals before the editor has
527
+ // connected and re-synced them.
528
+ if (!shouldWriteGlobalTypes(existing, content, cachedThemeSettings != null)) {
529
+ return;
470
530
  }
471
531
  fs.writeFileSync(globalTypesPath, content);
472
532
  }
@@ -475,10 +535,19 @@ function writeGlobalTypesFile(config) {
475
535
  * Extracted from onSyncTypes to avoid duplication.
476
536
  */
477
537
  function regenerateAllComponentTypes(config) {
478
- for (const component of config.components) {
479
- const componentDir = path.resolve(process.cwd(), path.dirname(component.entry));
480
- fs.writeFileSync(path.join(componentDir, "types.ts"), generateTypesFile(component.name, component.props, component.type, cachedEditorTypes, cachedCustomTypes));
481
- }
538
+ config.components.forEach(regenerateComponentTypes);
539
+ }
540
+ /**
541
+ * Regenerate one component's types.ts (over its props). An unchanged file
542
+ * is not rewritten: the watcher recompiles every component on a write under `src`.
543
+ */
544
+ function regenerateComponentTypes(component) {
545
+ const typesPath = path.join(path.resolve(process.cwd(), path.dirname(component.entry)), "types.ts");
546
+ const content = generateTypesFile(component.name, component.props, component.type, cachedEditorTypes, cachedCustomTypes);
547
+ if (fs.existsSync(typesPath) &&
548
+ fs.readFileSync(typesPath, "utf8") === content)
549
+ return;
550
+ fs.writeFileSync(typesPath, content);
482
551
  }
483
552
  /**
484
553
  * Create handler functions for editor commands
@@ -501,7 +570,35 @@ function createEditorHandlers(configPath, getConfig, setConfig, wsServer) {
501
570
  components: getAllComponents(),
502
571
  sharedModules: getAllSharedModules(),
503
572
  globalStyles: compiledGlobalStyles,
573
+ i18n: projectI18n ?? null,
504
574
  }),
575
+ // The editor sends this after a partner publishes a design-asset update:
576
+ // zip this folder without node_modules and send the zip back; the editor
577
+ // hands it to the ikas backend. The working tree is untouched.
578
+ onArchiveProject: async (payload) => {
579
+ const rootDir = process.cwd();
580
+ console.log(chalk.cyan(` Archiving theme source (${rootDir}) without node_modules...`));
581
+ const archive = await archiveProject({
582
+ rootDir,
583
+ exclude: payload.exclude,
584
+ rewriteIds: payload.rewriteIds,
585
+ });
586
+ try {
587
+ const contentBase64 = fs
588
+ .readFileSync(archive.zipPath)
589
+ .toString("base64");
590
+ console.log(chalk.green(` Theme source archived: ${archive.fileCount} files, ${(archive.size / 1024).toFixed(0)} KB`));
591
+ return {
592
+ rootDir,
593
+ size: archive.size,
594
+ fileCount: archive.fileCount,
595
+ contentBase64,
596
+ };
597
+ }
598
+ finally {
599
+ removeArchive(archive.zipPath);
600
+ }
601
+ },
505
602
  onCreateComponent: async (name, type) => {
506
603
  const pascalName = toPascalCase(name);
507
604
  // Validate
@@ -512,6 +609,12 @@ function createEditorHandlers(configPath, getConfig, setConfig, wsServer) {
512
609
  if (config.components.some((c) => c.name === pascalName)) {
513
610
  throw new Error(`Component "${pascalName}" already exists`);
514
611
  }
612
+ // No `!` assertion: the config may have been hot-reloaded since startup, and
613
+ // minting with a missing/malformed projectId writes a corrupt component id
614
+ // into ikas.config.json that later publishes verbatim.
615
+ if (!config.projectId || !isValidProjectId(config.projectId)) {
616
+ throw new Error("Invalid or missing projectId in ikas.config.json — restore the generated value and restart the dev server");
617
+ }
515
618
  const componentId = generateComponentId(config.projectId);
516
619
  const componentDir = path.resolve(process.cwd(), `src/components/${pascalName}`);
517
620
  // Create directory and files
@@ -532,8 +635,7 @@ function createEditorHandlers(configPath, getConfig, setConfig, wsServer) {
532
635
  writeConfig(configPath, config);
533
636
  setConfig(config);
534
637
  // Update barrel export
535
- const allNames = config.components.map((c) => c.name);
536
- updateBarrelExport(process.cwd(), allNames);
638
+ updateBarrelExport(process.cwd(), config.components);
537
639
  // Regenerate global types
538
640
  writeGlobalTypesFile(config);
539
641
  console.log(chalk.green(` Created component: ${pascalName}`));
@@ -553,7 +655,7 @@ function createEditorHandlers(configPath, getConfig, setConfig, wsServer) {
553
655
  if (!prop.enumTypeId) {
554
656
  throw new Error(`ENUM prop "${prop.name}" requires enumTypeId. Create one first with "config add-enum".`);
555
657
  }
556
- if (!prop.enumTypeId.startsWith("@ikas/")) {
658
+ if (!isBuiltInEnumTypeId(prop.enumTypeId)) {
557
659
  const customTypes = config.customTypes || [];
558
660
  const enumExists = customTypes.some((t) => t.id === prop.enumTypeId && t.type === "enum");
559
661
  if (!enumExists) {
@@ -573,6 +675,9 @@ function createEditorHandlers(configPath, getConfig, setConfig, wsServer) {
573
675
  `Known components: ${config.components.map((c) => `${c.name} (${c.id})`).join(", ")}.`);
574
676
  }
575
677
  }
678
+ const addGateError = visibilityGateError(component, prop, `prop "${prop.name}"`, { selfPropName: prop.name });
679
+ if (addGateError)
680
+ throw new Error(addGateError);
576
681
  component.props.push(prop);
577
682
  writeConfig(configPath, config);
578
683
  setConfig(config);
@@ -601,6 +706,13 @@ function createEditorHandlers(configPath, getConfig, setConfig, wsServer) {
601
706
  `Known components: ${config.components.map((c) => `${c.name} (${c.id})`).join(", ")}.`);
602
707
  }
603
708
  }
709
+ // Checked against the component as it will be, not as it is: a gate on the incoming prop can
710
+ // name another prop, and a rename in the same save changes what the chain resolves to.
711
+ const nextProps = component.props.slice();
712
+ nextProps[propIndex] = prop;
713
+ const updateGateError = visibilityGateError({ ...component, props: nextProps }, prop, `prop "${prop.name}"`, { selfPropName: prop.name });
714
+ if (updateGateError)
715
+ throw new Error(updateGateError);
604
716
  component.props[propIndex] = prop;
605
717
  writeConfig(configPath, config);
606
718
  setConfig(config);
@@ -648,7 +760,21 @@ function createEditorHandlers(configPath, getConfig, setConfig, wsServer) {
648
760
  ...(propGroup.description
649
761
  ? { description: propGroup.description }
650
762
  : {}),
763
+ ...(propGroup.requiredProp
764
+ ? {
765
+ requiredProp: propGroup.requiredProp,
766
+ ...(propGroup.requiredEnumOptionValue !== undefined
767
+ ? {
768
+ requiredEnumOptionId: propGroup.requiredEnumOptionId,
769
+ requiredEnumOptionValue: propGroup.requiredEnumOptionValue,
770
+ }
771
+ : { requiredPropValue: propGroup.requiredPropValue ?? true }),
772
+ }
773
+ : {}),
651
774
  };
775
+ const gateError = visibilityGateError(component, newGroup, `group "${newGroup.id}"`, { group: newGroup });
776
+ if (gateError)
777
+ throw new Error(gateError);
652
778
  if (propGroup.parentGroupId) {
653
779
  const found = findPropGroup(component.propGroups, propGroup.parentGroupId);
654
780
  if (!found)
@@ -681,11 +807,61 @@ function createEditorHandlers(configPath, getConfig, setConfig, wsServer) {
681
807
  found.group.name = updates.name;
682
808
  if (updates.description !== undefined)
683
809
  found.group.description = updates.description || undefined;
810
+ if (updates.requiredProp !== undefined) {
811
+ if (!updates.requiredProp || updates.requiredProp === "none") {
812
+ // Both go together — a state with no prop behind it would read as a rule.
813
+ delete found.group.requiredProp;
814
+ delete found.group.requiredPropValue;
815
+ delete found.group.requiredEnumOptionId;
816
+ delete found.group.requiredEnumOptionValue;
817
+ }
818
+ else {
819
+ found.group.requiredProp = updates.requiredProp;
820
+ if (updates.requiredEnumOptionValue !== undefined) {
821
+ found.group.requiredEnumOptionId = updates.requiredEnumOptionId;
822
+ found.group.requiredEnumOptionValue =
823
+ updates.requiredEnumOptionValue;
824
+ delete found.group.requiredPropValue;
825
+ }
826
+ else {
827
+ found.group.requiredPropValue = updates.requiredPropValue ?? true;
828
+ delete found.group.requiredEnumOptionId;
829
+ delete found.group.requiredEnumOptionValue;
830
+ }
831
+ }
832
+ }
833
+ const gateError = visibilityGateError(component, found.group, `group "${found.group.id}"`, { group: found.group });
834
+ if (gateError)
835
+ throw new Error(gateError);
684
836
  writeConfig(configPath, config);
685
837
  setConfig(config);
686
838
  broadcastConfigChange(componentId);
687
839
  console.log(chalk.green(` Updated prop group "${propGroupId}" on ${component.name}`));
688
840
  },
841
+ onUpdateTranslations: async (componentId, texts) => {
842
+ const config = getConfig();
843
+ const component = config.components.find((c) => c.id === componentId);
844
+ if (!component)
845
+ throw new Error(`Component not found: ${componentId}`);
846
+ // The editor sends every component's texts after an undo: an unchanged one is not rewritten.
847
+ const before = JSON.stringify(component);
848
+ applyTechnicalTexts(component, texts);
849
+ if (JSON.stringify(component) === before)
850
+ return;
851
+ writeConfig(configPath, config);
852
+ setConfig(config);
853
+ broadcastConfigChange(componentId);
854
+ console.log(chalk.green(` Updated editor texts of ${component.name}`));
855
+ },
856
+ onUpdateI18n: async (projectId, texts) => {
857
+ const config = getConfig();
858
+ if (projectId !== config.projectId)
859
+ throw new Error(`Not this project's UI texts: ${projectId}`);
860
+ // The watcher then sends the files' texts back (the same as the editor's: no change there)
861
+ const written = writeEditorI18n(config, texts);
862
+ if (written.length)
863
+ console.log(chalk.green(` Updated UI texts: ${written.join(", ")}`));
864
+ },
689
865
  onDeletePropGroup: async (componentId, propGroupId) => {
690
866
  const config = getConfig();
691
867
  const component = config.components.find((c) => c.id === componentId);
@@ -871,8 +1047,10 @@ function createEditorHandlers(configPath, getConfig, setConfig, wsServer) {
871
1047
  regenerateAllComponentTypes(config);
872
1048
  console.log(chalk.green(` Deleted enum: ${removed.name} (${enumId})`));
873
1049
  },
874
- onSyncTypes: (types, customTypes) => {
1050
+ onSyncTypes: (types, customTypes, themeSettings) => {
875
1051
  cachedEditorTypes = types;
1052
+ if (themeSettings)
1053
+ cachedThemeSettings = themeSettings;
876
1054
  // Merge incoming custom types with existing ones instead of replacing.
877
1055
  // Custom types belong to the code component project and should not be
878
1056
  // lost when a different editor project connects.
@@ -951,6 +1129,27 @@ async function startDevServer(opts = {}) {
951
1129
  fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
952
1130
  console.log(chalk.yellow(` Auto-generated projectId: ${config.projectId}`));
953
1131
  }
1132
+ else if (!isValidProjectId(config.projectId)) {
1133
+ // A malformed projectId (hand-edited config) breaks the editor's republish
1134
+ // stability: published/local id prefixes stop matching and every publish
1135
+ // INSERTs duplicate design elements. Fail fast instead of corrupting data.
1136
+ console.error(chalk.red(`Error: invalid projectId "${config.projectId}" in ikas.config.json`));
1137
+ console.error(chalk.gray("projectId must be at least 6 alphanumeric characters (no '-' or '_'). " +
1138
+ "Restore the original generated value — do not hand-edit it."));
1139
+ process.exit(1);
1140
+ }
1141
+ // Component ids carry the same prefix/suffix contract as projectId; the dev
1142
+ // server syncs them into the editor (and from there into themes), so a
1143
+ // hand-edited id must be rejected before the server starts.
1144
+ const invalidComponents = findInvalidComponentIds(config.components, config.projectId);
1145
+ if (invalidComponents.length > 0) {
1146
+ console.error(chalk.red("Error: invalid component id(s) in ikas.config.json:"));
1147
+ invalidComponents.forEach((c) => {
1148
+ console.error(chalk.red(` - "${c.id}"${c.name ? ` (${c.name})` : ""} — ${c.reason}`));
1149
+ });
1150
+ console.error(chalk.gray(COMPONENT_ID_HINT));
1151
+ process.exit(1);
1152
+ }
954
1153
  console.log(chalk.cyan("\n ikas Component Dev Server\n"));
955
1154
  console.log(chalk.gray(` Project: ${config.name}`));
956
1155
  console.log(chalk.gray(` Components: ${config.components.length}\n`));
@@ -975,6 +1174,7 @@ async function startDevServer(opts = {}) {
975
1174
  }
976
1175
  // Generate global types file at startup
977
1176
  writeGlobalTypesFile(config);
1177
+ reloadI18n(config);
978
1178
  // Initial compilation of all components (multi-entry with code splitting)
979
1179
  console.log(chalk.cyan(" Compiling components..."));
980
1180
  await compileAllComponentsForDev(config);
@@ -990,25 +1190,42 @@ async function startDevServer(opts = {}) {
990
1190
  lastBroadcastSharedHashes.set(id, mod.contentHash);
991
1191
  }
992
1192
  lastBroadcastGlobalStyleHash = computeGlobalStyleHash(compiledGlobalStyles);
1193
+ lastBroadcastI18nHash = i18nHash();
993
1194
  console.log("");
994
1195
  // Start WebSocket server
995
- const wsServer = new DevServerWebSocket(DEV_SERVER_PORT);
1196
+ // Control clients (the CLI's commands, the MCP) prove they run as this user with the token.
1197
+ const devServerToken = newDevServerToken();
1198
+ const wsServer = new DevServerWebSocket(DEV_SERVER_PORT, devServerToken);
996
1199
  const handlers = createEditorHandlers(configPath, () => config, (newConfig) => {
997
1200
  config = newConfig;
998
1201
  }, wsServer);
999
1202
  try {
1000
1203
  await wsServer.start(handlers);
1204
+ // Written once this server owns the port: another dev server's token is not replaced.
1205
+ writeDevServerToken(DEV_SERVER_PORT, devServerToken);
1001
1206
  }
1002
1207
  catch (error) {
1003
1208
  console.error(chalk.red(` Failed to start WebSocket server: ${error.message}`));
1004
1209
  process.exit(1);
1005
1210
  }
1211
+ console.log(chalk.green(` Dev server listening on 127.0.0.1:${DEV_SERVER_PORT}`));
1212
+ // Studio reaches this server only through the tunnel + websocket routing
1213
+ // that `ikas theme dev` registers; a directly-started server is invisible
1214
+ // to the editor. Hint only on a TTY — under `ikas theme dev` stdio is
1215
+ // piped and the wrapper already handles the connection.
1216
+ if (process.stdout.isTTY) {
1217
+ console.log(chalk.yellow("\n Note: ikas Studio cannot connect to this server directly."));
1218
+ console.log(chalk.yellow(" To develop with the editor, run `ikas theme dev` in this folder —"));
1219
+ console.log(chalk.yellow(" it starts this server, opens a tunnel and registers it with your store."));
1220
+ }
1006
1221
  // Watch for file changes
1007
1222
  const watchPaths = [
1008
1223
  "src/**/*.tsx",
1009
1224
  "src/**/*.ts",
1010
1225
  "src/**/*.css",
1011
1226
  "ikas.config.json",
1227
+ // The UI texts' files (`default.json`, `<locale>.json`)
1228
+ ...(config.i18n ? [path.posix.join(config.i18n, "*.json")] : []),
1012
1229
  ];
1013
1230
  const watcher = chokidar.watch(watchPaths, {
1014
1231
  ignored: ["**/global-types.ts"],
@@ -1049,15 +1266,64 @@ async function startDevServer(opts = {}) {
1049
1266
  }
1050
1267
  }, DEBOUNCE_MS);
1051
1268
  };
1269
+ // A UI texts file changed, came or went: the texts go to the editors (a changed base rewrites `index.d.ts`, whose
1270
+ // change recompiles)
1271
+ const onI18nFile = (filePath) => {
1272
+ const dir = i18nDir(config);
1273
+ const normalizedPath = path.resolve(filePath);
1274
+ if (!dir ||
1275
+ path.dirname(normalizedPath) !== dir ||
1276
+ !normalizedPath.endsWith(".json"))
1277
+ return false;
1278
+ console.log(chalk.yellow(` UI texts changed: ${path.basename(filePath)}`));
1279
+ reloadI18n(config);
1280
+ broadcastI18n(wsServer);
1281
+ return true;
1282
+ };
1052
1283
  watcher.on("change", async (filePath) => {
1053
1284
  const normalizedPath = path.resolve(filePath);
1285
+ if (onI18nFile(filePath))
1286
+ return;
1054
1287
  // Handle config changes
1055
1288
  if (normalizedPath === configPath) {
1056
1289
  console.log(chalk.yellow("\n Config changed, reloading..."));
1057
1290
  try {
1058
- config = JSON.parse(fs.readFileSync(configPath, "utf-8"));
1291
+ const reloaded = JSON.parse(fs.readFileSync(configPath, "utf-8"));
1292
+ // The startup guards validated projectId/component ids once; a hot-reload
1293
+ // must not smuggle a hand-edited value past them — a malformed id reaching
1294
+ // `generateComponentId` or chunk namespacing corrupts republish matching.
1295
+ // Keep the session's validated projectId if the key was removed; reject the
1296
+ // reload outright (previous config stays active) on invalid values.
1297
+ if (!reloaded.projectId) {
1298
+ reloaded.projectId = config.projectId;
1299
+ }
1300
+ else if (!isValidProjectId(reloaded.projectId)) {
1301
+ console.error(chalk.red(` Ignoring config reload: invalid projectId "${reloaded.projectId}". `) +
1302
+ chalk.gray("Restore the original generated value — previous config stays active."));
1303
+ return;
1304
+ }
1305
+ const reloadedInvalidComponents = findInvalidComponentIds(reloaded.components ?? [], reloaded.projectId);
1306
+ if (reloadedInvalidComponents.length > 0) {
1307
+ console.error(chalk.red(` Ignoring config reload: invalid component id(s): ${reloadedInvalidComponents
1308
+ .map((c) => `"${c.id}" (${c.reason})`)
1309
+ .join(", ")}. `) +
1310
+ chalk.gray("Restore the original generated values — previous config stays active."));
1311
+ return;
1312
+ }
1313
+ // Well-formed is not enough: an id that changes while the server runs is
1314
+ // broadcast as `component-deleted` for the old one, and the editor then
1315
+ // removes the imported component and its page placements from the theme.
1316
+ const identityChanges = findIdentityChanges(config, reloaded);
1317
+ if (identityChanges.length > 0) {
1318
+ console.error(chalk.red(` Ignoring config reload: generated ids were changed (${identityChanges.join(", ")}). `) +
1319
+ chalk.gray("Restore the original generated values — previous config stays active."));
1320
+ return;
1321
+ }
1322
+ config = reloaded;
1059
1323
  cachedCustomTypes = config.customTypes || [];
1060
1324
  writeGlobalTypesFile(config);
1325
+ reloadI18n(config);
1326
+ broadcastI18n(wsServer);
1061
1327
  await compileAllComponentsForDev(config);
1062
1328
  const delta = broadcastCompileDelta(wsServer);
1063
1329
  console.log(chalk.cyan(` Config reload delta: ${delta.changedComponents} changed, ${delta.deletedComponents} deleted, ${delta.changedSharedModules} shared chunks changed${delta.globalStylesChanged ? ", globals updated" : ""}`));
@@ -1073,12 +1339,14 @@ async function startDevServer(opts = {}) {
1073
1339
  });
1074
1340
  watcher.on("add", async (filePath) => {
1075
1341
  const normalizedPath = path.resolve(filePath);
1076
- if (normalizedPath === configPath)
1342
+ if (normalizedPath === configPath || onI18nFile(filePath))
1077
1343
  return;
1078
1344
  console.log(chalk.cyan(` New file: ${path.basename(filePath)}`));
1079
1345
  scheduleFullRecompile(normalizedPath);
1080
1346
  });
1081
1347
  watcher.on("unlink", (filePath) => {
1348
+ if (onI18nFile(filePath))
1349
+ return;
1082
1350
  const component = findComponentForFile(config, filePath);
1083
1351
  if (component) {
1084
1352
  console.log(chalk.yellow(` File removed: ${path.basename(filePath)}`));
@@ -1118,6 +1386,7 @@ async function startDevServer(opts = {}) {
1118
1386
  console.log(chalk.yellow("\n Shutting down..."));
1119
1387
  watcher.close();
1120
1388
  await wsServer.stop();
1389
+ removeDevServerToken(DEV_SERVER_PORT);
1121
1390
  process.exit(0);
1122
1391
  };
1123
1392
  process.on("SIGINT", shutdown);