@ikas/component-cli 1.4.0-beta.23 → 1.4.0-beta.230

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 (211) 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-to-page.js +1 -1
  10. package/dist/commands/add-to-page.js.map +1 -1
  11. package/dist/commands/add.d.ts.map +1 -1
  12. package/dist/commands/add.js +11 -2
  13. package/dist/commands/add.js.map +1 -1
  14. package/dist/commands/build.d.ts.map +1 -1
  15. package/dist/commands/build.js +23 -170
  16. package/dist/commands/build.js.map +1 -1
  17. package/dist/commands/config-texts.d.ts +39 -0
  18. package/dist/commands/config-texts.d.ts.map +1 -0
  19. package/dist/commands/config-texts.js +192 -0
  20. package/dist/commands/config-texts.js.map +1 -0
  21. package/dist/commands/config.d.ts +45 -0
  22. package/dist/commands/config.d.ts.map +1 -1
  23. package/dist/commands/config.js +1205 -154
  24. package/dist/commands/config.js.map +1 -1
  25. package/dist/commands/create-design-tokens.d.ts +24 -0
  26. package/dist/commands/create-design-tokens.d.ts.map +1 -0
  27. package/dist/commands/create-design-tokens.js +388 -0
  28. package/dist/commands/create-design-tokens.js.map +1 -0
  29. package/dist/commands/create-global-variable.d.ts +3 -0
  30. package/dist/commands/create-global-variable.d.ts.map +1 -0
  31. package/dist/commands/create-global-variable.js +73 -0
  32. package/dist/commands/create-global-variable.js.map +1 -0
  33. package/dist/commands/create-page.d.ts +3 -0
  34. package/dist/commands/create-page.d.ts.map +1 -0
  35. package/dist/commands/create-page.js +31 -0
  36. package/dist/commands/create-page.js.map +1 -0
  37. package/dist/commands/delete-page.d.ts +3 -0
  38. package/dist/commands/delete-page.d.ts.map +1 -0
  39. package/dist/commands/delete-page.js +28 -0
  40. package/dist/commands/delete-page.js.map +1 -0
  41. package/dist/commands/delete-theme-globals.d.ts +4 -0
  42. package/dist/commands/delete-theme-globals.d.ts.map +1 -0
  43. package/dist/commands/delete-theme-globals.js +50 -0
  44. package/dist/commands/delete-theme-globals.js.map +1 -0
  45. package/dist/commands/dev.d.ts.map +1 -1
  46. package/dist/commands/dev.js +328 -39
  47. package/dist/commands/dev.js.map +1 -1
  48. package/dist/commands/enable-builtin-pages.d.ts +3 -0
  49. package/dist/commands/enable-builtin-pages.d.ts.map +1 -0
  50. package/dist/commands/enable-builtin-pages.js +28 -0
  51. package/dist/commands/enable-builtin-pages.js.map +1 -0
  52. package/dist/commands/get-component-props.d.ts +3 -0
  53. package/dist/commands/get-component-props.d.ts.map +1 -0
  54. package/dist/commands/get-component-props.js +32 -0
  55. package/dist/commands/get-component-props.js.map +1 -0
  56. package/dist/commands/get-page-by-type.d.ts +3 -0
  57. package/dist/commands/get-page-by-type.d.ts.map +1 -0
  58. package/dist/commands/get-page-by-type.js +25 -0
  59. package/dist/commands/get-page-by-type.js.map +1 -0
  60. package/dist/commands/get-section-values.d.ts +3 -0
  61. package/dist/commands/get-section-values.d.ts.map +1 -0
  62. package/dist/commands/get-section-values.js +43 -0
  63. package/dist/commands/get-section-values.js.map +1 -0
  64. package/dist/commands/import.js +1 -1
  65. package/dist/commands/import.js.map +1 -1
  66. package/dist/commands/list-builtin-pages.d.ts +3 -0
  67. package/dist/commands/list-builtin-pages.d.ts.map +1 -0
  68. package/dist/commands/list-builtin-pages.js +28 -0
  69. package/dist/commands/list-builtin-pages.js.map +1 -0
  70. package/dist/commands/list-entities.d.ts +3 -0
  71. package/dist/commands/list-entities.d.ts.map +1 -0
  72. package/dist/commands/list-entities.js +32 -0
  73. package/dist/commands/list-entities.js.map +1 -0
  74. package/dist/commands/list-imported.js +1 -1
  75. package/dist/commands/list-imported.js.map +1 -1
  76. package/dist/commands/list-page-sections.d.ts +3 -0
  77. package/dist/commands/list-page-sections.d.ts.map +1 -0
  78. package/dist/commands/list-page-sections.js +31 -0
  79. package/dist/commands/list-page-sections.js.map +1 -0
  80. package/dist/commands/list-pages.js +1 -1
  81. package/dist/commands/list-pages.js.map +1 -1
  82. package/dist/commands/list-theme-globals.d.ts +3 -0
  83. package/dist/commands/list-theme-globals.d.ts.map +1 -0
  84. package/dist/commands/list-theme-globals.js +22 -0
  85. package/dist/commands/list-theme-globals.js.map +1 -0
  86. package/dist/commands/list-translations.d.ts +3 -0
  87. package/dist/commands/list-translations.d.ts.map +1 -0
  88. package/dist/commands/list-translations.js +33 -0
  89. package/dist/commands/list-translations.js.map +1 -0
  90. package/dist/commands/move-page-section.d.ts +3 -0
  91. package/dist/commands/move-page-section.d.ts.map +1 -0
  92. package/dist/commands/move-page-section.js +62 -0
  93. package/dist/commands/move-page-section.js.map +1 -0
  94. package/dist/commands/publish-theme.d.ts +3 -0
  95. package/dist/commands/publish-theme.d.ts.map +1 -0
  96. package/dist/commands/publish-theme.js +29 -0
  97. package/dist/commands/publish-theme.js.map +1 -0
  98. package/dist/commands/publish.d.ts.map +1 -1
  99. package/dist/commands/publish.js +72 -3
  100. package/dist/commands/publish.js.map +1 -1
  101. package/dist/commands/remove-page-section.d.ts +3 -0
  102. package/dist/commands/remove-page-section.d.ts.map +1 -0
  103. package/dist/commands/remove-page-section.js +40 -0
  104. package/dist/commands/remove-page-section.js.map +1 -0
  105. package/dist/commands/search-products.d.ts +3 -0
  106. package/dist/commands/search-products.d.ts.map +1 -0
  107. package/dist/commands/search-products.js +40 -0
  108. package/dist/commands/search-products.js.map +1 -0
  109. package/dist/commands/translation-languages.d.ts +3 -0
  110. package/dist/commands/translation-languages.d.ts.map +1 -0
  111. package/dist/commands/translation-languages.js +43 -0
  112. package/dist/commands/translation-languages.js.map +1 -0
  113. package/dist/commands/update-builtin-page-prop.d.ts +3 -0
  114. package/dist/commands/update-builtin-page-prop.d.ts.map +1 -0
  115. package/dist/commands/update-builtin-page-prop.js +49 -0
  116. package/dist/commands/update-builtin-page-prop.js.map +1 -0
  117. package/dist/commands/update-global-variable.d.ts +3 -0
  118. package/dist/commands/update-global-variable.d.ts.map +1 -0
  119. package/dist/commands/update-global-variable.js +70 -0
  120. package/dist/commands/update-global-variable.js.map +1 -0
  121. package/dist/commands/update-page-sections.d.ts +3 -0
  122. package/dist/commands/update-page-sections.d.ts.map +1 -0
  123. package/dist/commands/update-page-sections.js +42 -0
  124. package/dist/commands/update-page-sections.js.map +1 -0
  125. package/dist/commands/update-section-prop.d.ts +3 -0
  126. package/dist/commands/update-section-prop.d.ts.map +1 -0
  127. package/dist/commands/update-section-prop.js +81 -0
  128. package/dist/commands/update-section-prop.js.map +1 -0
  129. package/dist/commands/update-translations.d.ts +3 -0
  130. package/dist/commands/update-translations.d.ts.map +1 -0
  131. package/dist/commands/update-translations.js +45 -0
  132. package/dist/commands/update-translations.js.map +1 -0
  133. package/dist/commands/upload-image.d.ts +3 -0
  134. package/dist/commands/upload-image.d.ts.map +1 -0
  135. package/dist/commands/upload-image.js +38 -0
  136. package/dist/commands/upload-image.js.map +1 -0
  137. package/dist/commands/upload-images.d.ts +3 -0
  138. package/dist/commands/upload-images.d.ts.map +1 -0
  139. package/dist/commands/upload-images.js +48 -0
  140. package/dist/commands/upload-images.js.map +1 -0
  141. package/dist/index.d.ts.map +1 -1
  142. package/dist/index.js +66 -1
  143. package/dist/index.js.map +1 -1
  144. package/dist/types.d.ts +77 -0
  145. package/dist/types.d.ts.map +1 -1
  146. package/dist/utils/archive-project.d.ts +50 -0
  147. package/dist/utils/archive-project.d.ts.map +1 -0
  148. package/dist/utils/archive-project.js +178 -0
  149. package/dist/utils/archive-project.js.map +1 -0
  150. package/dist/utils/compile.d.ts +17 -2
  151. package/dist/utils/compile.d.ts.map +1 -1
  152. package/dist/utils/compile.js +742 -109
  153. package/dist/utils/compile.js.map +1 -1
  154. package/dist/utils/component-helpers.d.ts +236 -4
  155. package/dist/utils/component-helpers.d.ts.map +1 -1
  156. package/dist/utils/component-helpers.js +656 -37
  157. package/dist/utils/component-helpers.js.map +1 -1
  158. package/dist/utils/css-import-resolver.d.ts +4 -1
  159. package/dist/utils/css-import-resolver.d.ts.map +1 -1
  160. package/dist/utils/css-import-resolver.js +9 -1
  161. package/dist/utils/css-import-resolver.js.map +1 -1
  162. package/dist/utils/dev-server-auth.d.ts +28 -0
  163. package/dist/utils/dev-server-auth.d.ts.map +1 -0
  164. package/dist/utils/dev-server-auth.js +79 -0
  165. package/dist/utils/dev-server-auth.js.map +1 -0
  166. package/dist/utils/editor-action-client.d.ts +20 -3
  167. package/dist/utils/editor-action-client.d.ts.map +1 -1
  168. package/dist/utils/editor-action-client.js +81 -19
  169. package/dist/utils/editor-action-client.js.map +1 -1
  170. package/dist/utils/esm-transform.d.ts +30 -0
  171. package/dist/utils/esm-transform.d.ts.map +1 -1
  172. package/dist/utils/esm-transform.js +169 -2
  173. package/dist/utils/esm-transform.js.map +1 -1
  174. package/dist/utils/find-component-for-file.d.ts +12 -0
  175. package/dist/utils/find-component-for-file.d.ts.map +1 -0
  176. package/dist/utils/find-component-for-file.js +24 -0
  177. package/dist/utils/find-component-for-file.js.map +1 -0
  178. package/dist/utils/forbid-deep-imports.d.ts +3 -0
  179. package/dist/utils/forbid-deep-imports.d.ts.map +1 -0
  180. package/dist/utils/forbid-deep-imports.js +54 -0
  181. package/dist/utils/forbid-deep-imports.js.map +1 -0
  182. package/dist/utils/forbid-external-packages.d.ts +26 -0
  183. package/dist/utils/forbid-external-packages.d.ts.map +1 -0
  184. package/dist/utils/forbid-external-packages.js +99 -0
  185. package/dist/utils/forbid-external-packages.js.map +1 -0
  186. package/dist/utils/i18n.d.ts +98 -0
  187. package/dist/utils/i18n.d.ts.map +1 -0
  188. package/dist/utils/i18n.js +408 -0
  189. package/dist/utils/i18n.js.map +1 -0
  190. package/dist/utils/load-image.d.ts +16 -0
  191. package/dist/utils/load-image.d.ts.map +1 -0
  192. package/dist/utils/load-image.js +50 -0
  193. package/dist/utils/load-image.js.map +1 -0
  194. package/dist/utils/storefront-t.d.ts +17 -0
  195. package/dist/utils/storefront-t.d.ts.map +1 -0
  196. package/dist/utils/storefront-t.js +187 -0
  197. package/dist/utils/storefront-t.js.map +1 -0
  198. package/dist/utils/vendor-storefront.d.ts +13 -0
  199. package/dist/utils/vendor-storefront.d.ts.map +1 -0
  200. package/dist/utils/vendor-storefront.js +91 -0
  201. package/dist/utils/vendor-storefront.js.map +1 -0
  202. package/dist/utils/websocket-server.d.ts +165 -7
  203. package/dist/utils/websocket-server.d.ts.map +1 -1
  204. package/dist/utils/websocket-server.js +326 -45
  205. package/dist/utils/websocket-server.js.map +1 -1
  206. package/package.json +19 -2
  207. package/templates/add/section.css +11 -0
  208. package/dist/commands/create.d.ts +0 -4
  209. package/dist/commands/create.d.ts.map +0 -1
  210. package/dist/commands/create.js +0 -176
  211. package/dist/commands/create.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, checkConfigTranslations, } 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,18 @@ 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;
30
+ // What the editor would otherwise not hear of: UI texts left out, translations the config holds wrong
31
+ let projectWarnings = [];
23
32
  // Cached types list received from the editor via sync-types
24
33
  let cachedEditorTypes = null;
25
34
  // Cached custom type definitions received from the editor via sync-types (full IType objects)
26
35
  let cachedCustomTypes = null;
36
+ // Cached theme global variables received from the editor via sync-types.
37
+ // Drives the ThemeSettingName union in src/global-types.ts.
38
+ let cachedThemeSettings = null;
27
39
  const LIBRARY_TYPE_PREFIX = "@ikas/";
28
40
  /**
29
41
  * Match custom types for a component's props from the cached custom types.
@@ -77,24 +89,6 @@ function matchCustomTypesForProps(props, allCustomTypes) {
77
89
  }
78
90
  return Array.from(collected.values());
79
91
  }
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
92
  /**
99
93
  * Compile all components using multi-entry code splitting.
100
94
  * Updates compiledComponents and compiledSharedModules maps.
@@ -136,6 +130,18 @@ async function compileAllComponentsForDev(config) {
136
130
  if (chunkIds.length > 0) {
137
131
  console.log(chalk.cyan(` Extracted ${chunkIds.length} shared chunk(s)`));
138
132
  }
133
+ // Nothing downstream can resolve a computed specifier, so it 404s on the first lazy path that
134
+ // reaches it. Same pass `publish` runs, on the same two maps, so the two cannot drift apart —
135
+ // and both only warn for now (see the note in publish.ts on why that gate is not yet an error).
136
+ // Warning here would be right regardless: killing the watch loop over it is worse than a canvas
137
+ // that half-works, and the developer is right here to read the line.
138
+ const computedImports = findComputedDynamicImportsInBundles(result.chunks, result.entries, new Map(config.components.map((c) => [c.id, c.name])));
139
+ if (computedImports.length > 0) {
140
+ 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):`));
141
+ for (const offender of computedImports) {
142
+ console.warn(chalk.gray(` ${offender}`));
143
+ }
144
+ }
139
145
  // Store global CSS (unscoped). Preserve `updatedAt` across rebuilds when the compiled
140
146
  // global CSS is byte-identical so the editor only sees a fresh timestamp when the
141
147
  // designer actually changed `global.css` — the publish UI watches this timestamp to
@@ -176,6 +182,12 @@ async function compileAllComponentsForDev(config) {
176
182
  const canvasClientJs = transformEsmToWindowGlobals(entry.clientJs, {
177
183
  chunkMapping,
178
184
  });
185
+ // Config names a prop's visibility gate; the editor addresses props by id, so resolve the one
186
+ // against the other off the same index formula the ids come from.
187
+ const propIdByName = propIdsByName(component.props.map((prop, index) => ({
188
+ id: `${component.id}_prop_${index}`,
189
+ name: prop.name,
190
+ })));
179
191
  const propsWithIds = component.props.map((prop, index) => ({
180
192
  id: `${component.id}_prop_${index}`,
181
193
  name: prop.name,
@@ -187,10 +199,15 @@ async function compileAllComponentsForDev(config) {
187
199
  groupId: prop.groupId,
188
200
  typeId: prop.typeId,
189
201
  enumTypeId: prop.enumTypeId,
202
+ ...(prop.numberRangeData
203
+ ? { numberRangeData: prop.numberRangeData }
204
+ : {}),
190
205
  ...(prop.privateVarMap ? { privateVarMap: prop.privateVarMap } : {}),
191
206
  ...(prop.filteredComponentIds
192
207
  ? { filteredComponentIds: prop.filteredComponentIds }
193
208
  : {}),
209
+ ...resolveCondition(prop, propIdByName),
210
+ ...translationRecords(prop.translations),
194
211
  }));
195
212
  // Build prop group hierarchy if groups are defined
196
213
  const propGroups = component.propGroups && component.propGroups.length > 0
@@ -200,7 +217,7 @@ async function compileAllComponentsForDev(config) {
200
217
  const componentCustomTypes = cachedCustomTypes
201
218
  ? matchCustomTypesForProps(propsWithIds, cachedCustomTypes)
202
219
  : undefined;
203
- const usedSharedModuleIds = (result.entryChunkIds.get(component.id) || []).map((id) => config.projectId ? `${config.projectId}-${id}` : id);
220
+ const usedSharedModuleIds = (result.entryChunkIds.get(component.id) || []).map((id) => (config.projectId ? `${config.projectId}-${id}` : id));
204
221
  const data = {
205
222
  id: component.id,
206
223
  name: component.name,
@@ -215,6 +232,7 @@ async function compileAllComponentsForDev(config) {
215
232
  type: component.type || "component",
216
233
  ...(component.isHeader ? { isHeader: true } : {}),
217
234
  ...(component.isFooter ? { isFooter: true } : {}),
235
+ ...translationRecords(component.translations),
218
236
  propGroups,
219
237
  configPropGroups: component.propGroups && component.propGroups.length > 0
220
238
  ? component.propGroups
@@ -253,6 +271,10 @@ function rebuildCompiledComponentFromConfig(config, componentId) {
253
271
  const component = config.components.find((c) => c.id === componentId);
254
272
  if (!component)
255
273
  return null;
274
+ const propIdByName = propIdsByName(component.props.map((prop, index) => ({
275
+ id: `${component.id}_prop_${index}`,
276
+ name: prop.name,
277
+ })));
256
278
  const propsWithIds = component.props.map((prop, index) => ({
257
279
  id: `${component.id}_prop_${index}`,
258
280
  name: prop.name,
@@ -264,10 +286,13 @@ function rebuildCompiledComponentFromConfig(config, componentId) {
264
286
  groupId: prop.groupId,
265
287
  typeId: prop.typeId,
266
288
  enumTypeId: prop.enumTypeId,
289
+ ...(prop.numberRangeData ? { numberRangeData: prop.numberRangeData } : {}),
267
290
  ...(prop.privateVarMap ? { privateVarMap: prop.privateVarMap } : {}),
268
291
  ...(prop.filteredComponentIds
269
292
  ? { filteredComponentIds: prop.filteredComponentIds }
270
293
  : {}),
294
+ ...resolveCondition(prop, propIdByName),
295
+ ...translationRecords(prop.translations),
271
296
  }));
272
297
  const propGroups = component.propGroups && component.propGroups.length > 0
273
298
  ? buildPropGroupHierarchy(component.id, component.propGroups, propsWithIds)
@@ -277,6 +302,7 @@ function rebuildCompiledComponentFromConfig(config, componentId) {
277
302
  : undefined;
278
303
  const updated = {
279
304
  ...existing,
305
+ translations: translationRecords(component.translations).translations,
280
306
  props: propsWithIds,
281
307
  propGroups,
282
308
  configPropGroups: component.propGroups && component.propGroups.length > 0
@@ -310,7 +336,13 @@ function sha256Hex(input) {
310
336
  */
311
337
  function populateContentHashes() {
312
338
  for (const mod of compiledSharedModules.values()) {
313
- mod.contentHash = sha256Hex([mod.id, mod.compiledServerJs, mod.compiledClientJs, mod.canvasClientJs, mod.compiledCss].join("\0"));
339
+ mod.contentHash = sha256Hex([
340
+ mod.id,
341
+ mod.compiledServerJs,
342
+ mod.compiledClientJs,
343
+ mod.canvasClientJs,
344
+ mod.compiledCss,
345
+ ].join("\0"));
314
346
  }
315
347
  for (const comp of compiledComponents.values()) {
316
348
  const referencedHashes = (comp.usedSharedModuleIds || [])
@@ -330,6 +362,7 @@ function populateContentHashes() {
330
362
  comp.compiledCss,
331
363
  comp.canvasClientJs,
332
364
  JSON.stringify(comp.props),
365
+ JSON.stringify(comp.translations || null),
333
366
  JSON.stringify(comp.propGroups || null),
334
367
  JSON.stringify(comp.configPropGroups || null),
335
368
  JSON.stringify(comp.customTypes || null),
@@ -425,9 +458,12 @@ function broadcastCompileDelta(wsServer) {
425
458
  components: getAllComponents(),
426
459
  sharedModules: getAllSharedModules(),
427
460
  globalStyles: compiledGlobalStyles,
461
+ i18n: projectI18n ?? null,
462
+ warnings: projectWarnings,
428
463
  timestamp: Date.now(),
429
464
  },
430
465
  });
466
+ lastBroadcastI18nHash = i18nHash();
431
467
  lastBroadcastComponentHashes.clear();
432
468
  for (const [id, comp] of compiledComponents) {
433
469
  if (comp.contentHash)
@@ -442,6 +478,38 @@ function broadcastCompileDelta(wsServer) {
442
478
  lastBroadcastGlobalStyleHash = currentGlobalHash;
443
479
  return result;
444
480
  }
481
+ const i18nHash = () => sha256Hex(JSON.stringify([projectI18n ?? null, projectWarnings]));
482
+ /**
483
+ * Reads the project's UI texts again (and writes their `index.d.ts`) and checks the config's translations; a problem
484
+ * is shown here and in the editor, the rest is kept.
485
+ */
486
+ function reloadI18n(config) {
487
+ const { data, errors } = loadProjectI18n(config, projectI18n);
488
+ projectWarnings = [
489
+ ...errors.map((error) => `i18n: ${error}`),
490
+ ...Object.values(checkConfigTranslations(config.components))
491
+ .flat()
492
+ .map((problem) => `ikas.config.json: ${problem}`),
493
+ ];
494
+ projectWarnings.forEach((warning) => console.warn(chalk.yellow(` ${warning}`)));
495
+ projectI18n = data;
496
+ }
497
+ /** Sends the UI texts to the editors when they changed since they were last sent. */
498
+ function broadcastI18n(wsServer) {
499
+ const hash = i18nHash();
500
+ if (hash === lastBroadcastI18nHash)
501
+ return;
502
+ wsServer.broadcast({
503
+ type: "i18n-update",
504
+ payload: {
505
+ i18n: projectI18n ?? null,
506
+ warnings: projectWarnings,
507
+ timestamp: Date.now(),
508
+ },
509
+ });
510
+ lastBroadcastI18nHash = hash;
511
+ console.log(chalk.cyan(" Broadcast UI texts"));
512
+ }
445
513
  /**
446
514
  * Read and return the current config from disk
447
515
  */
@@ -461,12 +529,20 @@ function writeConfig(configPath, config) {
461
529
  function writeGlobalTypesFile(config) {
462
530
  const globalTypesPath = path.resolve(process.cwd(), "src/global-types.ts");
463
531
  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;
532
+ const content = generateGlobalTypesFile(cachedEditorTypes, cachedCustomTypes, usedEnumIds, cachedThemeSettings);
533
+ // Single read instead of existsSync + readFileSync; absent file reads as null.
534
+ let existing = null;
535
+ try {
536
+ existing = fs.readFileSync(globalTypesPath, "utf-8");
537
+ }
538
+ catch {
539
+ existing = null;
540
+ }
541
+ // Skip unnecessary writes (file-watcher loops) and, crucially, don't let the
542
+ // startup write clobber editor-synced theme globals before the editor has
543
+ // connected and re-synced them.
544
+ if (!shouldWriteGlobalTypes(existing, content, cachedThemeSettings != null)) {
545
+ return;
470
546
  }
471
547
  fs.writeFileSync(globalTypesPath, content);
472
548
  }
@@ -475,10 +551,19 @@ function writeGlobalTypesFile(config) {
475
551
  * Extracted from onSyncTypes to avoid duplication.
476
552
  */
477
553
  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
- }
554
+ config.components.forEach(regenerateComponentTypes);
555
+ }
556
+ /**
557
+ * Regenerate one component's types.ts (over its props). An unchanged file
558
+ * is not rewritten: the watcher recompiles every component on a write under `src`.
559
+ */
560
+ function regenerateComponentTypes(component) {
561
+ const typesPath = path.join(path.resolve(process.cwd(), path.dirname(component.entry)), "types.ts");
562
+ const content = generateTypesFile(component.name, component.props, component.type, cachedEditorTypes, cachedCustomTypes);
563
+ if (fs.existsSync(typesPath) &&
564
+ fs.readFileSync(typesPath, "utf8") === content)
565
+ return;
566
+ fs.writeFileSync(typesPath, content);
482
567
  }
483
568
  /**
484
569
  * Create handler functions for editor commands
@@ -501,7 +586,36 @@ function createEditorHandlers(configPath, getConfig, setConfig, wsServer) {
501
586
  components: getAllComponents(),
502
587
  sharedModules: getAllSharedModules(),
503
588
  globalStyles: compiledGlobalStyles,
589
+ i18n: projectI18n ?? null,
590
+ warnings: projectWarnings,
504
591
  }),
592
+ // The editor sends this after a partner publishes a design-asset update:
593
+ // zip this folder without node_modules and send the zip back; the editor
594
+ // hands it to the ikas backend. The working tree is untouched.
595
+ onArchiveProject: async (payload) => {
596
+ const rootDir = process.cwd();
597
+ console.log(chalk.cyan(` Archiving theme source (${rootDir}) without node_modules...`));
598
+ const archive = await archiveProject({
599
+ rootDir,
600
+ exclude: payload.exclude,
601
+ rewriteIds: payload.rewriteIds,
602
+ });
603
+ try {
604
+ const contentBase64 = fs
605
+ .readFileSync(archive.zipPath)
606
+ .toString("base64");
607
+ console.log(chalk.green(` Theme source archived: ${archive.fileCount} files, ${(archive.size / 1024).toFixed(0)} KB`));
608
+ return {
609
+ rootDir,
610
+ size: archive.size,
611
+ fileCount: archive.fileCount,
612
+ contentBase64,
613
+ };
614
+ }
615
+ finally {
616
+ removeArchive(archive.zipPath);
617
+ }
618
+ },
505
619
  onCreateComponent: async (name, type) => {
506
620
  const pascalName = toPascalCase(name);
507
621
  // Validate
@@ -512,6 +626,12 @@ function createEditorHandlers(configPath, getConfig, setConfig, wsServer) {
512
626
  if (config.components.some((c) => c.name === pascalName)) {
513
627
  throw new Error(`Component "${pascalName}" already exists`);
514
628
  }
629
+ // No `!` assertion: the config may have been hot-reloaded since startup, and
630
+ // minting with a missing/malformed projectId writes a corrupt component id
631
+ // into ikas.config.json that later publishes verbatim.
632
+ if (!config.projectId || !isValidProjectId(config.projectId)) {
633
+ throw new Error("Invalid or missing projectId in ikas.config.json — restore the generated value and restart the dev server");
634
+ }
515
635
  const componentId = generateComponentId(config.projectId);
516
636
  const componentDir = path.resolve(process.cwd(), `src/components/${pascalName}`);
517
637
  // Create directory and files
@@ -532,8 +652,7 @@ function createEditorHandlers(configPath, getConfig, setConfig, wsServer) {
532
652
  writeConfig(configPath, config);
533
653
  setConfig(config);
534
654
  // Update barrel export
535
- const allNames = config.components.map((c) => c.name);
536
- updateBarrelExport(process.cwd(), allNames);
655
+ updateBarrelExport(process.cwd(), config.components);
537
656
  // Regenerate global types
538
657
  writeGlobalTypesFile(config);
539
658
  console.log(chalk.green(` Created component: ${pascalName}`));
@@ -553,7 +672,7 @@ function createEditorHandlers(configPath, getConfig, setConfig, wsServer) {
553
672
  if (!prop.enumTypeId) {
554
673
  throw new Error(`ENUM prop "${prop.name}" requires enumTypeId. Create one first with "config add-enum".`);
555
674
  }
556
- if (!prop.enumTypeId.startsWith("@ikas/")) {
675
+ if (!isBuiltInEnumTypeId(prop.enumTypeId)) {
557
676
  const customTypes = config.customTypes || [];
558
677
  const enumExists = customTypes.some((t) => t.id === prop.enumTypeId && t.type === "enum");
559
678
  if (!enumExists) {
@@ -573,6 +692,9 @@ function createEditorHandlers(configPath, getConfig, setConfig, wsServer) {
573
692
  `Known components: ${config.components.map((c) => `${c.name} (${c.id})`).join(", ")}.`);
574
693
  }
575
694
  }
695
+ const addGateError = visibilityGateError(component, prop, `prop "${prop.name}"`, { selfPropName: prop.name });
696
+ if (addGateError)
697
+ throw new Error(addGateError);
576
698
  component.props.push(prop);
577
699
  writeConfig(configPath, config);
578
700
  setConfig(config);
@@ -601,6 +723,13 @@ function createEditorHandlers(configPath, getConfig, setConfig, wsServer) {
601
723
  `Known components: ${config.components.map((c) => `${c.name} (${c.id})`).join(", ")}.`);
602
724
  }
603
725
  }
726
+ // Checked against the component as it will be, not as it is: a gate on the incoming prop can
727
+ // name another prop, and a rename in the same save changes what the chain resolves to.
728
+ const nextProps = component.props.slice();
729
+ nextProps[propIndex] = prop;
730
+ const updateGateError = visibilityGateError({ ...component, props: nextProps }, prop, `prop "${prop.name}"`, { selfPropName: prop.name });
731
+ if (updateGateError)
732
+ throw new Error(updateGateError);
604
733
  component.props[propIndex] = prop;
605
734
  writeConfig(configPath, config);
606
735
  setConfig(config);
@@ -648,7 +777,21 @@ function createEditorHandlers(configPath, getConfig, setConfig, wsServer) {
648
777
  ...(propGroup.description
649
778
  ? { description: propGroup.description }
650
779
  : {}),
780
+ ...(propGroup.requiredProp
781
+ ? {
782
+ requiredProp: propGroup.requiredProp,
783
+ ...(propGroup.requiredEnumOptionValue !== undefined
784
+ ? {
785
+ requiredEnumOptionId: propGroup.requiredEnumOptionId,
786
+ requiredEnumOptionValue: propGroup.requiredEnumOptionValue,
787
+ }
788
+ : { requiredPropValue: propGroup.requiredPropValue ?? true }),
789
+ }
790
+ : {}),
651
791
  };
792
+ const gateError = visibilityGateError(component, newGroup, `group "${newGroup.id}"`, { group: newGroup });
793
+ if (gateError)
794
+ throw new Error(gateError);
652
795
  if (propGroup.parentGroupId) {
653
796
  const found = findPropGroup(component.propGroups, propGroup.parentGroupId);
654
797
  if (!found)
@@ -681,11 +824,61 @@ function createEditorHandlers(configPath, getConfig, setConfig, wsServer) {
681
824
  found.group.name = updates.name;
682
825
  if (updates.description !== undefined)
683
826
  found.group.description = updates.description || undefined;
827
+ if (updates.requiredProp !== undefined) {
828
+ if (!updates.requiredProp || updates.requiredProp === "none") {
829
+ // Both go together — a state with no prop behind it would read as a rule.
830
+ delete found.group.requiredProp;
831
+ delete found.group.requiredPropValue;
832
+ delete found.group.requiredEnumOptionId;
833
+ delete found.group.requiredEnumOptionValue;
834
+ }
835
+ else {
836
+ found.group.requiredProp = updates.requiredProp;
837
+ if (updates.requiredEnumOptionValue !== undefined) {
838
+ found.group.requiredEnumOptionId = updates.requiredEnumOptionId;
839
+ found.group.requiredEnumOptionValue =
840
+ updates.requiredEnumOptionValue;
841
+ delete found.group.requiredPropValue;
842
+ }
843
+ else {
844
+ found.group.requiredPropValue = updates.requiredPropValue ?? true;
845
+ delete found.group.requiredEnumOptionId;
846
+ delete found.group.requiredEnumOptionValue;
847
+ }
848
+ }
849
+ }
850
+ const gateError = visibilityGateError(component, found.group, `group "${found.group.id}"`, { group: found.group });
851
+ if (gateError)
852
+ throw new Error(gateError);
684
853
  writeConfig(configPath, config);
685
854
  setConfig(config);
686
855
  broadcastConfigChange(componentId);
687
856
  console.log(chalk.green(` Updated prop group "${propGroupId}" on ${component.name}`));
688
857
  },
858
+ onUpdateTranslations: async (componentId, texts) => {
859
+ const config = getConfig();
860
+ const component = config.components.find((c) => c.id === componentId);
861
+ if (!component)
862
+ throw new Error(`Component not found: ${componentId}`);
863
+ // The editor sends every component's texts after an undo: an unchanged one is not rewritten.
864
+ const before = JSON.stringify(component);
865
+ applyTechnicalTexts(component, texts);
866
+ if (JSON.stringify(component) === before)
867
+ return;
868
+ writeConfig(configPath, config);
869
+ setConfig(config);
870
+ broadcastConfigChange(componentId);
871
+ console.log(chalk.green(` Updated editor texts of ${component.name}`));
872
+ },
873
+ onUpdateI18n: async (projectId, texts) => {
874
+ const config = getConfig();
875
+ if (projectId !== config.projectId)
876
+ throw new Error(`Not this project's UI texts: ${projectId}`);
877
+ // The watcher then sends the files' texts back (the same as the editor's: no change there)
878
+ const written = writeEditorI18n(config, texts);
879
+ if (written.length)
880
+ console.log(chalk.green(` Updated UI texts: ${written.join(", ")}`));
881
+ },
689
882
  onDeletePropGroup: async (componentId, propGroupId) => {
690
883
  const config = getConfig();
691
884
  const component = config.components.find((c) => c.id === componentId);
@@ -871,8 +1064,10 @@ function createEditorHandlers(configPath, getConfig, setConfig, wsServer) {
871
1064
  regenerateAllComponentTypes(config);
872
1065
  console.log(chalk.green(` Deleted enum: ${removed.name} (${enumId})`));
873
1066
  },
874
- onSyncTypes: (types, customTypes) => {
1067
+ onSyncTypes: (types, customTypes, themeSettings) => {
875
1068
  cachedEditorTypes = types;
1069
+ if (themeSettings)
1070
+ cachedThemeSettings = themeSettings;
876
1071
  // Merge incoming custom types with existing ones instead of replacing.
877
1072
  // Custom types belong to the code component project and should not be
878
1073
  // lost when a different editor project connects.
@@ -951,6 +1146,27 @@ async function startDevServer(opts = {}) {
951
1146
  fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
952
1147
  console.log(chalk.yellow(` Auto-generated projectId: ${config.projectId}`));
953
1148
  }
1149
+ else if (!isValidProjectId(config.projectId)) {
1150
+ // A malformed projectId (hand-edited config) breaks the editor's republish
1151
+ // stability: published/local id prefixes stop matching and every publish
1152
+ // INSERTs duplicate design elements. Fail fast instead of corrupting data.
1153
+ console.error(chalk.red(`Error: invalid projectId "${config.projectId}" in ikas.config.json`));
1154
+ console.error(chalk.gray("projectId must be at least 6 alphanumeric characters (no '-' or '_'). " +
1155
+ "Restore the original generated value — do not hand-edit it."));
1156
+ process.exit(1);
1157
+ }
1158
+ // Component ids carry the same prefix/suffix contract as projectId; the dev
1159
+ // server syncs them into the editor (and from there into themes), so a
1160
+ // hand-edited id must be rejected before the server starts.
1161
+ const invalidComponents = findInvalidComponentIds(config.components, config.projectId);
1162
+ if (invalidComponents.length > 0) {
1163
+ console.error(chalk.red("Error: invalid component id(s) in ikas.config.json:"));
1164
+ invalidComponents.forEach((c) => {
1165
+ console.error(chalk.red(` - "${c.id}"${c.name ? ` (${c.name})` : ""} — ${c.reason}`));
1166
+ });
1167
+ console.error(chalk.gray(COMPONENT_ID_HINT));
1168
+ process.exit(1);
1169
+ }
954
1170
  console.log(chalk.cyan("\n ikas Component Dev Server\n"));
955
1171
  console.log(chalk.gray(` Project: ${config.name}`));
956
1172
  console.log(chalk.gray(` Components: ${config.components.length}\n`));
@@ -975,6 +1191,7 @@ async function startDevServer(opts = {}) {
975
1191
  }
976
1192
  // Generate global types file at startup
977
1193
  writeGlobalTypesFile(config);
1194
+ reloadI18n(config);
978
1195
  // Initial compilation of all components (multi-entry with code splitting)
979
1196
  console.log(chalk.cyan(" Compiling components..."));
980
1197
  await compileAllComponentsForDev(config);
@@ -990,25 +1207,42 @@ async function startDevServer(opts = {}) {
990
1207
  lastBroadcastSharedHashes.set(id, mod.contentHash);
991
1208
  }
992
1209
  lastBroadcastGlobalStyleHash = computeGlobalStyleHash(compiledGlobalStyles);
1210
+ lastBroadcastI18nHash = i18nHash();
993
1211
  console.log("");
994
1212
  // Start WebSocket server
995
- const wsServer = new DevServerWebSocket(DEV_SERVER_PORT);
1213
+ // Control clients (the CLI's commands, the MCP) prove they run as this user with the token.
1214
+ const devServerToken = newDevServerToken();
1215
+ const wsServer = new DevServerWebSocket(DEV_SERVER_PORT, devServerToken);
996
1216
  const handlers = createEditorHandlers(configPath, () => config, (newConfig) => {
997
1217
  config = newConfig;
998
1218
  }, wsServer);
999
1219
  try {
1000
1220
  await wsServer.start(handlers);
1221
+ // Written once this server owns the port: another dev server's token is not replaced.
1222
+ writeDevServerToken(DEV_SERVER_PORT, devServerToken);
1001
1223
  }
1002
1224
  catch (error) {
1003
1225
  console.error(chalk.red(` Failed to start WebSocket server: ${error.message}`));
1004
1226
  process.exit(1);
1005
1227
  }
1228
+ console.log(chalk.green(` Dev server listening on 127.0.0.1:${DEV_SERVER_PORT}`));
1229
+ // Studio reaches this server only through the tunnel + websocket routing
1230
+ // that `ikas theme dev` registers; a directly-started server is invisible
1231
+ // to the editor. Hint only on a TTY — under `ikas theme dev` stdio is
1232
+ // piped and the wrapper already handles the connection.
1233
+ if (process.stdout.isTTY) {
1234
+ console.log(chalk.yellow("\n Note: ikas Studio cannot connect to this server directly."));
1235
+ console.log(chalk.yellow(" To develop with the editor, run `ikas theme dev` in this folder —"));
1236
+ console.log(chalk.yellow(" it starts this server, opens a tunnel and registers it with your store."));
1237
+ }
1006
1238
  // Watch for file changes
1007
1239
  const watchPaths = [
1008
1240
  "src/**/*.tsx",
1009
1241
  "src/**/*.ts",
1010
1242
  "src/**/*.css",
1011
1243
  "ikas.config.json",
1244
+ // The UI texts' files (`default.json`, `<locale>.json`)
1245
+ ...(config.i18n ? [path.posix.join(config.i18n, "*.json")] : []),
1012
1246
  ];
1013
1247
  const watcher = chokidar.watch(watchPaths, {
1014
1248
  ignored: ["**/global-types.ts"],
@@ -1049,15 +1283,67 @@ async function startDevServer(opts = {}) {
1049
1283
  }
1050
1284
  }, DEBOUNCE_MS);
1051
1285
  };
1286
+ // A UI texts file changed, came or went: the texts go to the editors (a changed base rewrites `index.d.ts`, whose
1287
+ // change recompiles)
1288
+ const onI18nFile = (filePath) => {
1289
+ const dir = i18nDir(config);
1290
+ const normalizedPath = path.resolve(filePath);
1291
+ if (!dir ||
1292
+ path.dirname(normalizedPath) !== dir ||
1293
+ !normalizedPath.endsWith(".json"))
1294
+ return false;
1295
+ console.log(chalk.yellow(` UI texts changed: ${path.basename(filePath)}`));
1296
+ reloadI18n(config);
1297
+ broadcastI18n(wsServer);
1298
+ return true;
1299
+ };
1052
1300
  watcher.on("change", async (filePath) => {
1053
1301
  const normalizedPath = path.resolve(filePath);
1302
+ if (onI18nFile(filePath))
1303
+ return;
1054
1304
  // Handle config changes
1055
1305
  if (normalizedPath === configPath) {
1056
1306
  console.log(chalk.yellow("\n Config changed, reloading..."));
1057
1307
  try {
1058
- config = JSON.parse(fs.readFileSync(configPath, "utf-8"));
1308
+ const reloaded = JSON.parse(fs.readFileSync(configPath, "utf-8"));
1309
+ // The startup guards validated projectId/component ids once; a hot-reload
1310
+ // must not smuggle a hand-edited value past them — a malformed id reaching
1311
+ // `generateComponentId` or chunk namespacing corrupts republish matching.
1312
+ // Keep the session's validated projectId if the key was removed; reject the
1313
+ // reload outright (previous config stays active) on invalid values.
1314
+ if (!reloaded.projectId) {
1315
+ reloaded.projectId = config.projectId;
1316
+ }
1317
+ else if (!isValidProjectId(reloaded.projectId)) {
1318
+ console.error(chalk.red(` Ignoring config reload: invalid projectId "${reloaded.projectId}". `) +
1319
+ chalk.gray("Restore the original generated value — previous config stays active."));
1320
+ return;
1321
+ }
1322
+ const reloadedInvalidComponents = findInvalidComponentIds(reloaded.components ?? [], reloaded.projectId);
1323
+ if (reloadedInvalidComponents.length > 0) {
1324
+ console.error(chalk.red(` Ignoring config reload: invalid component id(s): ${reloadedInvalidComponents
1325
+ .map((c) => `"${c.id}" (${c.reason})`)
1326
+ .join(", ")}. `) +
1327
+ chalk.gray("Restore the original generated values — previous config stays active."));
1328
+ return;
1329
+ }
1330
+ // Well-formed is not enough: an id that changes while the server runs is
1331
+ // broadcast as `component-deleted` for the old one, and the editor then
1332
+ // removes the imported component and its page placements from the theme.
1333
+ const identityChanges = findIdentityChanges(config, reloaded);
1334
+ if (identityChanges.length > 0) {
1335
+ console.error(chalk.red(` Ignoring config reload: generated ids were changed (${identityChanges.join(", ")}). `) +
1336
+ chalk.gray("Restore the original generated values — previous config stays active."));
1337
+ return;
1338
+ }
1339
+ // The first `set-text` names the UI texts' folder: watch it from then on
1340
+ if (reloaded.i18n && reloaded.i18n !== config.i18n)
1341
+ watcher.add(path.posix.join(reloaded.i18n, "*.json"));
1342
+ config = reloaded;
1059
1343
  cachedCustomTypes = config.customTypes || [];
1060
1344
  writeGlobalTypesFile(config);
1345
+ reloadI18n(config);
1346
+ broadcastI18n(wsServer);
1061
1347
  await compileAllComponentsForDev(config);
1062
1348
  const delta = broadcastCompileDelta(wsServer);
1063
1349
  console.log(chalk.cyan(` Config reload delta: ${delta.changedComponents} changed, ${delta.deletedComponents} deleted, ${delta.changedSharedModules} shared chunks changed${delta.globalStylesChanged ? ", globals updated" : ""}`));
@@ -1073,12 +1359,14 @@ async function startDevServer(opts = {}) {
1073
1359
  });
1074
1360
  watcher.on("add", async (filePath) => {
1075
1361
  const normalizedPath = path.resolve(filePath);
1076
- if (normalizedPath === configPath)
1362
+ if (normalizedPath === configPath || onI18nFile(filePath))
1077
1363
  return;
1078
1364
  console.log(chalk.cyan(` New file: ${path.basename(filePath)}`));
1079
1365
  scheduleFullRecompile(normalizedPath);
1080
1366
  });
1081
1367
  watcher.on("unlink", (filePath) => {
1368
+ if (onI18nFile(filePath))
1369
+ return;
1082
1370
  const component = findComponentForFile(config, filePath);
1083
1371
  if (component) {
1084
1372
  console.log(chalk.yellow(` File removed: ${path.basename(filePath)}`));
@@ -1118,6 +1406,7 @@ async function startDevServer(opts = {}) {
1118
1406
  console.log(chalk.yellow("\n Shutting down..."));
1119
1407
  watcher.close();
1120
1408
  await wsServer.stop();
1409
+ removeDevServerToken(DEV_SERVER_PORT);
1121
1410
  process.exit(0);
1122
1411
  };
1123
1412
  process.on("SIGINT", shutdown);