@ikas/component-cli 1.4.0-beta.2 → 1.4.0-beta.200
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/dist/build-tools.d.ts +37 -0
- package/dist/build-tools.d.ts.map +1 -0
- package/dist/build-tools.js +36 -0
- package/dist/build-tools.js.map +1 -0
- package/dist/commands/add-sections-to-page.d.ts +3 -0
- package/dist/commands/add-sections-to-page.d.ts.map +1 -0
- package/dist/commands/add-sections-to-page.js +39 -0
- package/dist/commands/add-sections-to-page.js.map +1 -0
- package/dist/commands/add-to-page.d.ts +3 -0
- package/dist/commands/add-to-page.d.ts.map +1 -0
- package/dist/commands/add-to-page.js +41 -0
- package/dist/commands/add-to-page.js.map +1 -0
- package/dist/commands/add.d.ts.map +1 -1
- package/dist/commands/add.js +11 -2
- package/dist/commands/add.js.map +1 -1
- package/dist/commands/build.d.ts.map +1 -1
- package/dist/commands/build.js +5 -165
- package/dist/commands/build.js.map +1 -1
- package/dist/commands/config.d.ts +35 -0
- package/dist/commands/config.d.ts.map +1 -1
- package/dist/commands/config.js +1235 -228
- package/dist/commands/config.js.map +1 -1
- package/dist/commands/create-design-tokens.d.ts +24 -0
- package/dist/commands/create-design-tokens.d.ts.map +1 -0
- package/dist/commands/create-design-tokens.js +327 -0
- package/dist/commands/create-design-tokens.js.map +1 -0
- package/dist/commands/create-global-variable.d.ts +3 -0
- package/dist/commands/create-global-variable.d.ts.map +1 -0
- package/dist/commands/create-global-variable.js +73 -0
- package/dist/commands/create-global-variable.js.map +1 -0
- package/dist/commands/create-page.d.ts +3 -0
- package/dist/commands/create-page.d.ts.map +1 -0
- package/dist/commands/create-page.js +31 -0
- package/dist/commands/create-page.js.map +1 -0
- package/dist/commands/delete-page.d.ts +3 -0
- package/dist/commands/delete-page.d.ts.map +1 -0
- package/dist/commands/delete-page.js +28 -0
- package/dist/commands/delete-page.js.map +1 -0
- package/dist/commands/delete-theme-globals.d.ts +4 -0
- package/dist/commands/delete-theme-globals.d.ts.map +1 -0
- package/dist/commands/delete-theme-globals.js +48 -0
- package/dist/commands/delete-theme-globals.js.map +1 -0
- package/dist/commands/dev.d.ts.map +1 -1
- package/dist/commands/dev.js +398 -53
- package/dist/commands/dev.js.map +1 -1
- package/dist/commands/enable-builtin-pages.d.ts +3 -0
- package/dist/commands/enable-builtin-pages.d.ts.map +1 -0
- package/dist/commands/enable-builtin-pages.js +27 -0
- package/dist/commands/enable-builtin-pages.js.map +1 -0
- package/dist/commands/get-component-props.d.ts +3 -0
- package/dist/commands/get-component-props.d.ts.map +1 -0
- package/dist/commands/get-component-props.js +32 -0
- package/dist/commands/get-component-props.js.map +1 -0
- package/dist/commands/get-page-by-type.d.ts +3 -0
- package/dist/commands/get-page-by-type.d.ts.map +1 -0
- package/dist/commands/get-page-by-type.js +25 -0
- package/dist/commands/get-page-by-type.js.map +1 -0
- package/dist/commands/get-section-values.d.ts +3 -0
- package/dist/commands/get-section-values.d.ts.map +1 -0
- package/dist/commands/get-section-values.js +39 -0
- package/dist/commands/get-section-values.js.map +1 -0
- package/dist/commands/import.d.ts +3 -0
- package/dist/commands/import.d.ts.map +1 -0
- package/dist/commands/import.js +25 -0
- package/dist/commands/import.js.map +1 -0
- package/dist/commands/list-builtin-pages.d.ts +3 -0
- package/dist/commands/list-builtin-pages.d.ts.map +1 -0
- package/dist/commands/list-builtin-pages.js +28 -0
- package/dist/commands/list-builtin-pages.js.map +1 -0
- package/dist/commands/list-entities.d.ts +3 -0
- package/dist/commands/list-entities.d.ts.map +1 -0
- package/dist/commands/list-entities.js +32 -0
- package/dist/commands/list-entities.js.map +1 -0
- package/dist/commands/list-imported.d.ts +3 -0
- package/dist/commands/list-imported.d.ts.map +1 -0
- package/dist/commands/list-imported.js +25 -0
- package/dist/commands/list-imported.js.map +1 -0
- package/dist/commands/list-page-sections.d.ts +3 -0
- package/dist/commands/list-page-sections.d.ts.map +1 -0
- package/dist/commands/list-page-sections.js +31 -0
- package/dist/commands/list-page-sections.js.map +1 -0
- package/dist/commands/list-pages.d.ts +3 -0
- package/dist/commands/list-pages.d.ts.map +1 -0
- package/dist/commands/list-pages.js +21 -0
- package/dist/commands/list-pages.js.map +1 -0
- package/dist/commands/list-theme-globals.d.ts +3 -0
- package/dist/commands/list-theme-globals.d.ts.map +1 -0
- package/dist/commands/list-theme-globals.js +22 -0
- package/dist/commands/list-theme-globals.js.map +1 -0
- package/dist/commands/move-page-section.d.ts +3 -0
- package/dist/commands/move-page-section.d.ts.map +1 -0
- package/dist/commands/move-page-section.js +62 -0
- package/dist/commands/move-page-section.js.map +1 -0
- package/dist/commands/publish-theme.d.ts +3 -0
- package/dist/commands/publish-theme.d.ts.map +1 -0
- package/dist/commands/publish-theme.js +29 -0
- package/dist/commands/publish-theme.js.map +1 -0
- package/dist/commands/publish.d.ts.map +1 -1
- package/dist/commands/publish.js +54 -3
- package/dist/commands/publish.js.map +1 -1
- package/dist/commands/remove-page-section.d.ts +3 -0
- package/dist/commands/remove-page-section.d.ts.map +1 -0
- package/dist/commands/remove-page-section.js +40 -0
- package/dist/commands/remove-page-section.js.map +1 -0
- package/dist/commands/search-products.d.ts +3 -0
- package/dist/commands/search-products.d.ts.map +1 -0
- package/dist/commands/search-products.js +40 -0
- package/dist/commands/search-products.js.map +1 -0
- package/dist/commands/update-builtin-page-prop.d.ts +3 -0
- package/dist/commands/update-builtin-page-prop.d.ts.map +1 -0
- package/dist/commands/update-builtin-page-prop.js +49 -0
- package/dist/commands/update-builtin-page-prop.js.map +1 -0
- package/dist/commands/update-global-variable.d.ts +3 -0
- package/dist/commands/update-global-variable.d.ts.map +1 -0
- package/dist/commands/update-global-variable.js +70 -0
- package/dist/commands/update-global-variable.js.map +1 -0
- package/dist/commands/update-page-sections.d.ts +3 -0
- package/dist/commands/update-page-sections.d.ts.map +1 -0
- package/dist/commands/update-page-sections.js +39 -0
- package/dist/commands/update-page-sections.js.map +1 -0
- package/dist/commands/update-section-prop.d.ts +3 -0
- package/dist/commands/update-section-prop.d.ts.map +1 -0
- package/dist/commands/update-section-prop.js +76 -0
- package/dist/commands/update-section-prop.js.map +1 -0
- package/dist/commands/upload-image.d.ts +3 -0
- package/dist/commands/upload-image.d.ts.map +1 -0
- package/dist/commands/upload-image.js +38 -0
- package/dist/commands/upload-image.js.map +1 -0
- package/dist/commands/upload-images.d.ts +3 -0
- package/dist/commands/upload-images.d.ts.map +1 -0
- package/dist/commands/upload-images.js +48 -0
- package/dist/commands/upload-images.js.map +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +67 -0
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +52 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/compile.d.ts +17 -2
- package/dist/utils/compile.d.ts.map +1 -1
- package/dist/utils/compile.js +750 -117
- package/dist/utils/compile.js.map +1 -1
- package/dist/utils/component-helpers.d.ts +148 -3
- package/dist/utils/component-helpers.d.ts.map +1 -1
- package/dist/utils/component-helpers.js +376 -29
- package/dist/utils/component-helpers.js.map +1 -1
- package/dist/utils/css-import-resolver.d.ts +4 -1
- package/dist/utils/css-import-resolver.d.ts.map +1 -1
- package/dist/utils/css-import-resolver.js +9 -1
- package/dist/utils/css-import-resolver.js.map +1 -1
- package/dist/utils/editor-action-client.d.ts +29 -0
- package/dist/utils/editor-action-client.d.ts.map +1 -0
- package/dist/utils/editor-action-client.js +137 -0
- package/dist/utils/editor-action-client.js.map +1 -0
- package/dist/utils/esm-transform.d.ts +30 -0
- package/dist/utils/esm-transform.d.ts.map +1 -1
- package/dist/utils/esm-transform.js +169 -2
- package/dist/utils/esm-transform.js.map +1 -1
- package/dist/utils/find-component-for-file.d.ts +12 -0
- package/dist/utils/find-component-for-file.d.ts.map +1 -0
- package/dist/utils/find-component-for-file.js +24 -0
- package/dist/utils/find-component-for-file.js.map +1 -0
- package/dist/utils/forbid-deep-imports.d.ts +3 -0
- package/dist/utils/forbid-deep-imports.d.ts.map +1 -0
- package/dist/utils/forbid-deep-imports.js +54 -0
- package/dist/utils/forbid-deep-imports.js.map +1 -0
- package/dist/utils/forbid-external-packages.d.ts +26 -0
- package/dist/utils/forbid-external-packages.d.ts.map +1 -0
- package/dist/utils/forbid-external-packages.js +99 -0
- package/dist/utils/forbid-external-packages.js.map +1 -0
- package/dist/utils/load-image.d.ts +16 -0
- package/dist/utils/load-image.d.ts.map +1 -0
- package/dist/utils/load-image.js +50 -0
- package/dist/utils/load-image.js.map +1 -0
- package/dist/utils/vendor-storefront.d.ts +13 -0
- package/dist/utils/vendor-storefront.d.ts.map +1 -0
- package/dist/utils/vendor-storefront.js +91 -0
- package/dist/utils/vendor-storefront.js.map +1 -0
- package/dist/utils/websocket-server.d.ts +134 -3
- package/dist/utils/websocket-server.d.ts.map +1 -1
- package/dist/utils/websocket-server.js +279 -24
- package/dist/utils/websocket-server.js.map +1 -1
- package/package.json +17 -2
- package/templates/add/section.css +11 -0
- package/dist/commands/create.d.ts +0 -9
- package/dist/commands/create.d.ts.map +0 -1
- package/dist/commands/create.js +0 -9
- package/dist/commands/create.js.map +0 -1
- package/dist/commands/proxy.d.ts +0 -39
- package/dist/commands/proxy.d.ts.map +0 -1
- package/dist/commands/proxy.js +0 -212
- package/dist/commands/proxy.js.map +0 -1
package/dist/commands/dev.js
CHANGED
|
@@ -1,21 +1,33 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
2
|
import chokidar from "chokidar";
|
|
3
3
|
import { Command } from "commander";
|
|
4
|
+
import * as crypto from "crypto";
|
|
4
5
|
import * as fs from "fs";
|
|
5
6
|
import * as path from "path";
|
|
6
|
-
import { toPascalCase, generateTypesFile, generateGlobalTypesFile, collectUsedEnumIds, collectUsedCustomTypeIds, generateComponentFile, generateStylesFile, updateBarrelExport, generateProjectId, generateComponentId, generateUniqueId, buildPropGroupHierarchy, findPropGroup, collectPropGroupIds, movePropGroupInTree, } from "../utils/component-helpers.js";
|
|
7
|
+
import { toPascalCase, generateTypesFile, generateGlobalTypesFile, collectUsedEnumIds, collectUsedCustomTypeIds, shouldWriteGlobalTypes, generateComponentFile, generateStylesFile, updateBarrelExport, generateProjectId, isValidProjectId, findInvalidComponentIds, generateComponentId, generateUniqueId, buildPropGroupHierarchy, propIdsByName, resolveCondition, visibilityGateError, findPropGroup, collectPropGroupIds, movePropGroupInTree, validateFilteredComponentIds, isBuiltInEnumTypeId, } from "../utils/component-helpers.js";
|
|
8
|
+
import { findComponentForFile } from "../utils/find-component-for-file.js";
|
|
7
9
|
import { compileAllComponents as compileAllComponentsMulti } from "../utils/compile.js";
|
|
8
|
-
import { transformEsmToWindowGlobals, transformChunkForCanvas, buildChunkMapping, } from "../utils/esm-transform.js";
|
|
10
|
+
import { transformEsmToWindowGlobals, transformChunkForCanvas, buildChunkMapping, findComputedDynamicImportsInBundles, } from "../utils/esm-transform.js";
|
|
9
11
|
import { DevServerWebSocket, } from "../utils/websocket-server.js";
|
|
10
12
|
const DEV_SERVER_PORT = 5201;
|
|
11
13
|
// Store compiled components, shared modules, and global styles
|
|
12
14
|
const compiledComponents = new Map();
|
|
13
15
|
const compiledSharedModules = new Map();
|
|
14
16
|
let compiledGlobalStyles;
|
|
17
|
+
// Hashes broadcast on the last cycle, used to compute deltas. Reset only on process
|
|
18
|
+
// startup. Editors compare these hashes (sent on the wire as `contentHash`) against their
|
|
19
|
+
// stored YJS value to decide whether to apply the update — so once we've seen a given hash
|
|
20
|
+
// here, we can safely skip broadcasting it again.
|
|
21
|
+
const lastBroadcastComponentHashes = new Map();
|
|
22
|
+
const lastBroadcastSharedHashes = new Map();
|
|
23
|
+
let lastBroadcastGlobalStyleHash;
|
|
15
24
|
// Cached types list received from the editor via sync-types
|
|
16
25
|
let cachedEditorTypes = null;
|
|
17
26
|
// Cached custom type definitions received from the editor via sync-types (full IType objects)
|
|
18
27
|
let cachedCustomTypes = null;
|
|
28
|
+
// Cached theme global variables received from the editor via sync-types.
|
|
29
|
+
// Drives the ThemeSettingName union in src/global-types.ts.
|
|
30
|
+
let cachedThemeSettings = null;
|
|
19
31
|
const LIBRARY_TYPE_PREFIX = "@ikas/";
|
|
20
32
|
/**
|
|
21
33
|
* Match custom types for a component's props from the cached custom types.
|
|
@@ -69,24 +81,6 @@ function matchCustomTypesForProps(props, allCustomTypes) {
|
|
|
69
81
|
}
|
|
70
82
|
return Array.from(collected.values());
|
|
71
83
|
}
|
|
72
|
-
/**
|
|
73
|
-
* Get the component definition that contains a file path
|
|
74
|
-
*/
|
|
75
|
-
function findComponentForFile(config, filePath) {
|
|
76
|
-
const normalizedPath = path.normalize(filePath);
|
|
77
|
-
for (const component of config.components) {
|
|
78
|
-
const entryPath = path.resolve(process.cwd(), component.entry);
|
|
79
|
-
const stylesPath = path.resolve(process.cwd(), component.styles);
|
|
80
|
-
const componentDir = path.dirname(entryPath);
|
|
81
|
-
// Check if the file is the entry, styles, or in the component directory
|
|
82
|
-
if (normalizedPath === entryPath ||
|
|
83
|
-
normalizedPath === stylesPath ||
|
|
84
|
-
normalizedPath.startsWith(componentDir + path.sep)) {
|
|
85
|
-
return component;
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
return null;
|
|
89
|
-
}
|
|
90
84
|
/**
|
|
91
85
|
* Compile all components using multi-entry code splitting.
|
|
92
86
|
* Updates compiledComponents and compiledSharedModules maps.
|
|
@@ -128,12 +122,35 @@ async function compileAllComponentsForDev(config) {
|
|
|
128
122
|
if (chunkIds.length > 0) {
|
|
129
123
|
console.log(chalk.cyan(` Extracted ${chunkIds.length} shared chunk(s)`));
|
|
130
124
|
}
|
|
131
|
-
//
|
|
125
|
+
// Nothing downstream can resolve a computed specifier, so it 404s on the first lazy path that
|
|
126
|
+
// reaches it. Same pass `publish` runs, on the same two maps, so the two cannot drift apart —
|
|
127
|
+
// and both only warn for now (see the note in publish.ts on why that gate is not yet an error).
|
|
128
|
+
// Warning here would be right regardless: killing the watch loop over it is worse than a canvas
|
|
129
|
+
// that half-works, and the developer is right here to read the line.
|
|
130
|
+
const computedImports = findComputedDynamicImportsInBundles(result.chunks, result.entries, new Map(config.components.map((c) => [c.id, c.name])));
|
|
131
|
+
if (computedImports.length > 0) {
|
|
132
|
+
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):`));
|
|
133
|
+
for (const offender of computedImports) {
|
|
134
|
+
console.warn(chalk.gray(` ${offender}`));
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
// Store global CSS (unscoped). Preserve `updatedAt` across rebuilds when the compiled
|
|
138
|
+
// global CSS is byte-identical so the editor only sees a fresh timestamp when the
|
|
139
|
+
// designer actually changed `global.css` — the publish UI watches this timestamp to
|
|
140
|
+
// decide whether globals need (re)publishing independently of any code component.
|
|
132
141
|
if (result.globalCss && config.projectId) {
|
|
133
|
-
|
|
142
|
+
const previousHash = computeGlobalStyleHash(compiledGlobalStyles);
|
|
143
|
+
const previousUpdatedAt = compiledGlobalStyles?.updatedAt;
|
|
144
|
+
const next = {
|
|
134
145
|
projectId: config.projectId,
|
|
135
146
|
compiledCss: result.globalCss,
|
|
136
147
|
};
|
|
148
|
+
const nextHash = computeGlobalStyleHash(next);
|
|
149
|
+
next.updatedAt =
|
|
150
|
+
nextHash === previousHash && previousUpdatedAt !== undefined
|
|
151
|
+
? previousUpdatedAt
|
|
152
|
+
: Date.now();
|
|
153
|
+
compiledGlobalStyles = next;
|
|
137
154
|
console.log(chalk.cyan(` Compiled global styles`));
|
|
138
155
|
}
|
|
139
156
|
else {
|
|
@@ -157,6 +174,12 @@ async function compileAllComponentsForDev(config) {
|
|
|
157
174
|
const canvasClientJs = transformEsmToWindowGlobals(entry.clientJs, {
|
|
158
175
|
chunkMapping,
|
|
159
176
|
});
|
|
177
|
+
// Config names a prop's visibility gate; the editor addresses props by id, so resolve the one
|
|
178
|
+
// against the other off the same index formula the ids come from.
|
|
179
|
+
const propIdByName = propIdsByName(component.props.map((prop, index) => ({
|
|
180
|
+
id: `${component.id}_prop_${index}`,
|
|
181
|
+
name: prop.name,
|
|
182
|
+
})));
|
|
160
183
|
const propsWithIds = component.props.map((prop, index) => ({
|
|
161
184
|
id: `${component.id}_prop_${index}`,
|
|
162
185
|
name: prop.name,
|
|
@@ -168,10 +191,14 @@ async function compileAllComponentsForDev(config) {
|
|
|
168
191
|
groupId: prop.groupId,
|
|
169
192
|
typeId: prop.typeId,
|
|
170
193
|
enumTypeId: prop.enumTypeId,
|
|
194
|
+
...(prop.numberRangeData
|
|
195
|
+
? { numberRangeData: prop.numberRangeData }
|
|
196
|
+
: {}),
|
|
171
197
|
...(prop.privateVarMap ? { privateVarMap: prop.privateVarMap } : {}),
|
|
172
198
|
...(prop.filteredComponentIds
|
|
173
199
|
? { filteredComponentIds: prop.filteredComponentIds }
|
|
174
200
|
: {}),
|
|
201
|
+
...resolveCondition(prop, propIdByName),
|
|
175
202
|
}));
|
|
176
203
|
// Build prop group hierarchy if groups are defined
|
|
177
204
|
const propGroups = component.propGroups && component.propGroups.length > 0
|
|
@@ -181,7 +208,7 @@ async function compileAllComponentsForDev(config) {
|
|
|
181
208
|
const componentCustomTypes = cachedCustomTypes
|
|
182
209
|
? matchCustomTypesForProps(propsWithIds, cachedCustomTypes)
|
|
183
210
|
: undefined;
|
|
184
|
-
const usedSharedModuleIds = (result.entryChunkIds.get(component.id) || []).map((id) => config.projectId ? `${config.projectId}-${id}` : id);
|
|
211
|
+
const usedSharedModuleIds = (result.entryChunkIds.get(component.id) || []).map((id) => (config.projectId ? `${config.projectId}-${id}` : id));
|
|
185
212
|
const data = {
|
|
186
213
|
id: component.id,
|
|
187
214
|
name: component.name,
|
|
@@ -208,6 +235,7 @@ async function compileAllComponentsForDev(config) {
|
|
|
208
235
|
compiledComponents.set(component.id, data);
|
|
209
236
|
console.log(chalk.green(` Compiled: ${component.name}`));
|
|
210
237
|
}
|
|
238
|
+
populateContentHashes();
|
|
211
239
|
return true;
|
|
212
240
|
}
|
|
213
241
|
/**
|
|
@@ -233,6 +261,10 @@ function rebuildCompiledComponentFromConfig(config, componentId) {
|
|
|
233
261
|
const component = config.components.find((c) => c.id === componentId);
|
|
234
262
|
if (!component)
|
|
235
263
|
return null;
|
|
264
|
+
const propIdByName = propIdsByName(component.props.map((prop, index) => ({
|
|
265
|
+
id: `${component.id}_prop_${index}`,
|
|
266
|
+
name: prop.name,
|
|
267
|
+
})));
|
|
236
268
|
const propsWithIds = component.props.map((prop, index) => ({
|
|
237
269
|
id: `${component.id}_prop_${index}`,
|
|
238
270
|
name: prop.name,
|
|
@@ -244,10 +276,12 @@ function rebuildCompiledComponentFromConfig(config, componentId) {
|
|
|
244
276
|
groupId: prop.groupId,
|
|
245
277
|
typeId: prop.typeId,
|
|
246
278
|
enumTypeId: prop.enumTypeId,
|
|
279
|
+
...(prop.numberRangeData ? { numberRangeData: prop.numberRangeData } : {}),
|
|
247
280
|
...(prop.privateVarMap ? { privateVarMap: prop.privateVarMap } : {}),
|
|
248
281
|
...(prop.filteredComponentIds
|
|
249
282
|
? { filteredComponentIds: prop.filteredComponentIds }
|
|
250
283
|
: {}),
|
|
284
|
+
...resolveCondition(prop, propIdByName),
|
|
251
285
|
}));
|
|
252
286
|
const propGroups = component.propGroups && component.propGroups.length > 0
|
|
253
287
|
? buildPropGroupHierarchy(component.id, component.propGroups, propsWithIds)
|
|
@@ -267,6 +301,7 @@ function rebuildCompiledComponentFromConfig(config, componentId) {
|
|
|
267
301
|
: undefined,
|
|
268
302
|
};
|
|
269
303
|
compiledComponents.set(componentId, updated);
|
|
304
|
+
populateContentHashes();
|
|
270
305
|
return updated;
|
|
271
306
|
}
|
|
272
307
|
/**
|
|
@@ -275,6 +310,158 @@ function rebuildCompiledComponentFromConfig(config, componentId) {
|
|
|
275
310
|
function getAllSharedModules() {
|
|
276
311
|
return Array.from(compiledSharedModules.values());
|
|
277
312
|
}
|
|
313
|
+
function sha256Hex(input) {
|
|
314
|
+
return crypto.createHash("sha256").update(input).digest("hex");
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Compute `contentHash` for every entry in `compiledSharedModules` and `compiledComponents`,
|
|
318
|
+
* mutating each entry in place. A component's hash incorporates the hashes of its referenced
|
|
319
|
+
* shared modules (sorted), so any shared-util change cascades into consumers without
|
|
320
|
+
* requiring a separate "shared module changed" message — the consumer's own hash already
|
|
321
|
+
* differs. Global styles are tracked separately via `updatedAt` on the global style payload;
|
|
322
|
+
* they intentionally do NOT roll into component hashes (designers publish globals as a
|
|
323
|
+
* standalone unit).
|
|
324
|
+
*/
|
|
325
|
+
function populateContentHashes() {
|
|
326
|
+
for (const mod of compiledSharedModules.values()) {
|
|
327
|
+
mod.contentHash = sha256Hex([
|
|
328
|
+
mod.id,
|
|
329
|
+
mod.compiledServerJs,
|
|
330
|
+
mod.compiledClientJs,
|
|
331
|
+
mod.canvasClientJs,
|
|
332
|
+
mod.compiledCss,
|
|
333
|
+
].join("\0"));
|
|
334
|
+
}
|
|
335
|
+
for (const comp of compiledComponents.values()) {
|
|
336
|
+
const referencedHashes = (comp.usedSharedModuleIds || [])
|
|
337
|
+
.slice()
|
|
338
|
+
.sort()
|
|
339
|
+
.map((id) => `${id}:${compiledSharedModules.get(id)?.contentHash || ""}`)
|
|
340
|
+
.join("|");
|
|
341
|
+
comp.contentHash = sha256Hex([
|
|
342
|
+
comp.id,
|
|
343
|
+
comp.name,
|
|
344
|
+
comp.projectId || "",
|
|
345
|
+
comp.type || "component",
|
|
346
|
+
comp.isHeader ? "1" : "0",
|
|
347
|
+
comp.isFooter ? "1" : "0",
|
|
348
|
+
comp.compiledServerJs,
|
|
349
|
+
comp.compiledClientJs,
|
|
350
|
+
comp.compiledCss,
|
|
351
|
+
comp.canvasClientJs,
|
|
352
|
+
JSON.stringify(comp.props),
|
|
353
|
+
JSON.stringify(comp.propGroups || null),
|
|
354
|
+
JSON.stringify(comp.configPropGroups || null),
|
|
355
|
+
JSON.stringify(comp.customTypes || null),
|
|
356
|
+
(comp.usedSharedModuleIds || []).slice().sort().join(","),
|
|
357
|
+
referencedHashes,
|
|
358
|
+
].join("\0"));
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
function computeGlobalStyleHash(g) {
|
|
362
|
+
if (!g)
|
|
363
|
+
return undefined;
|
|
364
|
+
return sha256Hex([g.projectId, g.compiledCss].join("\0"));
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* Build a `component-update` payload's `sharedModules` array for one component: the chunks
|
|
368
|
+
* the editor needs to resolve this component's imports. We always include the full
|
|
369
|
+
* transitive set (cheap) so the editor never sees a missing chunk during a partial update.
|
|
370
|
+
*/
|
|
371
|
+
function sharedModulesForComponent(comp) {
|
|
372
|
+
if (!comp.usedSharedModuleIds || comp.usedSharedModuleIds.length === 0)
|
|
373
|
+
return undefined;
|
|
374
|
+
const out = [];
|
|
375
|
+
for (const id of comp.usedSharedModuleIds) {
|
|
376
|
+
const mod = compiledSharedModules.get(id);
|
|
377
|
+
if (mod)
|
|
378
|
+
out.push(mod);
|
|
379
|
+
}
|
|
380
|
+
return out.length > 0 ? out : undefined;
|
|
381
|
+
}
|
|
382
|
+
/**
|
|
383
|
+
* Diff current compile state against `lastBroadcast*` maps and decide what to send:
|
|
384
|
+
*
|
|
385
|
+
* - Nothing changed → no broadcast at all (this is the whole point — quiet rebuilds).
|
|
386
|
+
* - Exactly one component changed, no deletes, no global change → single
|
|
387
|
+
* `component-update` (cheap, no full-state replay on the editor).
|
|
388
|
+
* - Anything more (multi-component, deletes, global style change) → one bundled
|
|
389
|
+
* `initial-state`. We deliberately collapse multi-component changes into ONE message so
|
|
390
|
+
* the editor doesn't tick through N sequential `markUpdating` / refresh cycles. The
|
|
391
|
+
* editor's `autoSyncToTheme` uses `contentHash` to skip YJS writes for components that
|
|
392
|
+
* didn't actually change, so the full-state payload only mutates what really moved.
|
|
393
|
+
*
|
|
394
|
+
* Newly-connecting editors always receive a full `initial-state` via `onRequestState` in
|
|
395
|
+
* websocket-server.ts and are independent of this path.
|
|
396
|
+
*/
|
|
397
|
+
function broadcastCompileDelta(wsServer) {
|
|
398
|
+
const currentGlobalHash = computeGlobalStyleHash(compiledGlobalStyles);
|
|
399
|
+
const globalStylesChanged = currentGlobalHash !== lastBroadcastGlobalStyleHash;
|
|
400
|
+
const changedComponentIds = [];
|
|
401
|
+
for (const [id, comp] of compiledComponents) {
|
|
402
|
+
const prev = lastBroadcastComponentHashes.get(id);
|
|
403
|
+
if (prev !== comp.contentHash)
|
|
404
|
+
changedComponentIds.push(id);
|
|
405
|
+
}
|
|
406
|
+
const deletedComponentIds = [];
|
|
407
|
+
for (const id of lastBroadcastComponentHashes.keys()) {
|
|
408
|
+
if (!compiledComponents.has(id))
|
|
409
|
+
deletedComponentIds.push(id);
|
|
410
|
+
}
|
|
411
|
+
let changedSharedCount = 0;
|
|
412
|
+
for (const [id, mod] of compiledSharedModules) {
|
|
413
|
+
if (lastBroadcastSharedHashes.get(id) !== mod.contentHash)
|
|
414
|
+
changedSharedCount++;
|
|
415
|
+
}
|
|
416
|
+
const result = {
|
|
417
|
+
changedComponents: changedComponentIds.length,
|
|
418
|
+
deletedComponents: deletedComponentIds.length,
|
|
419
|
+
changedSharedModules: changedSharedCount,
|
|
420
|
+
globalStylesChanged,
|
|
421
|
+
};
|
|
422
|
+
const nothingChanged = changedComponentIds.length === 0 &&
|
|
423
|
+
deletedComponentIds.length === 0 &&
|
|
424
|
+
!globalStylesChanged;
|
|
425
|
+
if (nothingChanged)
|
|
426
|
+
return result;
|
|
427
|
+
// Single-component edit fast path: avoid making the editor clear+refill its devComponents
|
|
428
|
+
// map and run autoSyncToTheme for every component. One targeted update is enough.
|
|
429
|
+
const canUseSingleUpdate = changedComponentIds.length === 1 &&
|
|
430
|
+
deletedComponentIds.length === 0 &&
|
|
431
|
+
!globalStylesChanged;
|
|
432
|
+
if (canUseSingleUpdate) {
|
|
433
|
+
const comp = compiledComponents.get(changedComponentIds[0]);
|
|
434
|
+
wsServer.broadcastComponentUpdate(comp, sharedModulesForComponent(comp));
|
|
435
|
+
lastBroadcastComponentHashes.set(comp.id, comp.contentHash);
|
|
436
|
+
}
|
|
437
|
+
else {
|
|
438
|
+
for (const id of deletedComponentIds) {
|
|
439
|
+
wsServer.broadcastComponentDeleted(id);
|
|
440
|
+
lastBroadcastComponentHashes.delete(id);
|
|
441
|
+
}
|
|
442
|
+
wsServer.broadcast({
|
|
443
|
+
type: "initial-state",
|
|
444
|
+
payload: {
|
|
445
|
+
components: getAllComponents(),
|
|
446
|
+
sharedModules: getAllSharedModules(),
|
|
447
|
+
globalStyles: compiledGlobalStyles,
|
|
448
|
+
timestamp: Date.now(),
|
|
449
|
+
},
|
|
450
|
+
});
|
|
451
|
+
lastBroadcastComponentHashes.clear();
|
|
452
|
+
for (const [id, comp] of compiledComponents) {
|
|
453
|
+
if (comp.contentHash)
|
|
454
|
+
lastBroadcastComponentHashes.set(id, comp.contentHash);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
lastBroadcastSharedHashes.clear();
|
|
458
|
+
for (const [id, mod] of compiledSharedModules) {
|
|
459
|
+
if (mod.contentHash)
|
|
460
|
+
lastBroadcastSharedHashes.set(id, mod.contentHash);
|
|
461
|
+
}
|
|
462
|
+
lastBroadcastGlobalStyleHash = currentGlobalHash;
|
|
463
|
+
return result;
|
|
464
|
+
}
|
|
278
465
|
/**
|
|
279
466
|
* Read and return the current config from disk
|
|
280
467
|
*/
|
|
@@ -294,12 +481,20 @@ function writeConfig(configPath, config) {
|
|
|
294
481
|
function writeGlobalTypesFile(config) {
|
|
295
482
|
const globalTypesPath = path.resolve(process.cwd(), "src/global-types.ts");
|
|
296
483
|
const usedEnumIds = collectUsedEnumIds(config.components);
|
|
297
|
-
const content = generateGlobalTypesFile(cachedEditorTypes, cachedCustomTypes, usedEnumIds);
|
|
298
|
-
//
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
484
|
+
const content = generateGlobalTypesFile(cachedEditorTypes, cachedCustomTypes, usedEnumIds, cachedThemeSettings);
|
|
485
|
+
// Single read instead of existsSync + readFileSync; absent file reads as null.
|
|
486
|
+
let existing = null;
|
|
487
|
+
try {
|
|
488
|
+
existing = fs.readFileSync(globalTypesPath, "utf-8");
|
|
489
|
+
}
|
|
490
|
+
catch {
|
|
491
|
+
existing = null;
|
|
492
|
+
}
|
|
493
|
+
// Skip unnecessary writes (file-watcher loops) and, crucially, don't let the
|
|
494
|
+
// startup write clobber editor-synced theme globals before the editor has
|
|
495
|
+
// connected and re-synced them.
|
|
496
|
+
if (!shouldWriteGlobalTypes(existing, content, cachedThemeSettings != null)) {
|
|
497
|
+
return;
|
|
303
498
|
}
|
|
304
499
|
fs.writeFileSync(globalTypesPath, content);
|
|
305
500
|
}
|
|
@@ -323,7 +518,10 @@ function createEditorHandlers(configPath, getConfig, setConfig, wsServer) {
|
|
|
323
518
|
const broadcastConfigChange = (componentId) => {
|
|
324
519
|
const updated = rebuildCompiledComponentFromConfig(getConfig(), componentId);
|
|
325
520
|
if (updated) {
|
|
326
|
-
wsServer.broadcastComponentUpdate(updated);
|
|
521
|
+
wsServer.broadcastComponentUpdate(updated, sharedModulesForComponent(updated));
|
|
522
|
+
if (updated.contentHash) {
|
|
523
|
+
lastBroadcastComponentHashes.set(updated.id, updated.contentHash);
|
|
524
|
+
}
|
|
327
525
|
}
|
|
328
526
|
};
|
|
329
527
|
return {
|
|
@@ -342,6 +540,12 @@ function createEditorHandlers(configPath, getConfig, setConfig, wsServer) {
|
|
|
342
540
|
if (config.components.some((c) => c.name === pascalName)) {
|
|
343
541
|
throw new Error(`Component "${pascalName}" already exists`);
|
|
344
542
|
}
|
|
543
|
+
// No `!` assertion: the config may have been hot-reloaded since startup, and
|
|
544
|
+
// minting with a missing/malformed projectId writes a corrupt component id
|
|
545
|
+
// into ikas.config.json that later publishes verbatim.
|
|
546
|
+
if (!config.projectId || !isValidProjectId(config.projectId)) {
|
|
547
|
+
throw new Error("Invalid or missing projectId in ikas.config.json — restore the generated value and restart the dev server");
|
|
548
|
+
}
|
|
345
549
|
const componentId = generateComponentId(config.projectId);
|
|
346
550
|
const componentDir = path.resolve(process.cwd(), `src/components/${pascalName}`);
|
|
347
551
|
// Create directory and files
|
|
@@ -383,7 +587,7 @@ function createEditorHandlers(configPath, getConfig, setConfig, wsServer) {
|
|
|
383
587
|
if (!prop.enumTypeId) {
|
|
384
588
|
throw new Error(`ENUM prop "${prop.name}" requires enumTypeId. Create one first with "config add-enum".`);
|
|
385
589
|
}
|
|
386
|
-
if (!prop.enumTypeId
|
|
590
|
+
if (!isBuiltInEnumTypeId(prop.enumTypeId)) {
|
|
387
591
|
const customTypes = config.customTypes || [];
|
|
388
592
|
const enumExists = customTypes.some((t) => t.id === prop.enumTypeId && t.type === "enum");
|
|
389
593
|
if (!enumExists) {
|
|
@@ -395,6 +599,17 @@ function createEditorHandlers(configPath, getConfig, setConfig, wsServer) {
|
|
|
395
599
|
}
|
|
396
600
|
}
|
|
397
601
|
}
|
|
602
|
+
if (prop.filteredComponentIds && prop.filteredComponentIds.length > 0) {
|
|
603
|
+
const { unknown } = validateFilteredComponentIds(prop.filteredComponentIds, config.components);
|
|
604
|
+
if (unknown.length > 0) {
|
|
605
|
+
throw new Error(`Unknown filteredComponentIds: ${unknown.join(", ")}. ` +
|
|
606
|
+
`Component ids are opaque and cannot be derived from names. ` +
|
|
607
|
+
`Known components: ${config.components.map((c) => `${c.name} (${c.id})`).join(", ")}.`);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
const addGateError = visibilityGateError(component, prop, `prop "${prop.name}"`, { selfPropName: prop.name });
|
|
611
|
+
if (addGateError)
|
|
612
|
+
throw new Error(addGateError);
|
|
398
613
|
component.props.push(prop);
|
|
399
614
|
writeConfig(configPath, config);
|
|
400
615
|
setConfig(config);
|
|
@@ -415,6 +630,21 @@ function createEditorHandlers(configPath, getConfig, setConfig, wsServer) {
|
|
|
415
630
|
if (propIndex === -1) {
|
|
416
631
|
throw new Error(`Prop "${propName}" not found on ${component.name}`);
|
|
417
632
|
}
|
|
633
|
+
if (prop.filteredComponentIds && prop.filteredComponentIds.length > 0) {
|
|
634
|
+
const { unknown } = validateFilteredComponentIds(prop.filteredComponentIds, config.components);
|
|
635
|
+
if (unknown.length > 0) {
|
|
636
|
+
throw new Error(`Unknown filteredComponentIds: ${unknown.join(", ")}. ` +
|
|
637
|
+
`Component ids are opaque and cannot be derived from names. ` +
|
|
638
|
+
`Known components: ${config.components.map((c) => `${c.name} (${c.id})`).join(", ")}.`);
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
// Checked against the component as it will be, not as it is: a gate on the incoming prop can
|
|
642
|
+
// name another prop, and a rename in the same save changes what the chain resolves to.
|
|
643
|
+
const nextProps = component.props.slice();
|
|
644
|
+
nextProps[propIndex] = prop;
|
|
645
|
+
const updateGateError = visibilityGateError({ ...component, props: nextProps }, prop, `prop "${prop.name}"`, { selfPropName: prop.name });
|
|
646
|
+
if (updateGateError)
|
|
647
|
+
throw new Error(updateGateError);
|
|
418
648
|
component.props[propIndex] = prop;
|
|
419
649
|
writeConfig(configPath, config);
|
|
420
650
|
setConfig(config);
|
|
@@ -462,7 +692,21 @@ function createEditorHandlers(configPath, getConfig, setConfig, wsServer) {
|
|
|
462
692
|
...(propGroup.description
|
|
463
693
|
? { description: propGroup.description }
|
|
464
694
|
: {}),
|
|
695
|
+
...(propGroup.requiredProp
|
|
696
|
+
? {
|
|
697
|
+
requiredProp: propGroup.requiredProp,
|
|
698
|
+
...(propGroup.requiredEnumOptionValue !== undefined
|
|
699
|
+
? {
|
|
700
|
+
requiredEnumOptionId: propGroup.requiredEnumOptionId,
|
|
701
|
+
requiredEnumOptionValue: propGroup.requiredEnumOptionValue,
|
|
702
|
+
}
|
|
703
|
+
: { requiredPropValue: propGroup.requiredPropValue ?? true }),
|
|
704
|
+
}
|
|
705
|
+
: {}),
|
|
465
706
|
};
|
|
707
|
+
const gateError = visibilityGateError(component, newGroup, `group "${newGroup.id}"`, { group: newGroup });
|
|
708
|
+
if (gateError)
|
|
709
|
+
throw new Error(gateError);
|
|
466
710
|
if (propGroup.parentGroupId) {
|
|
467
711
|
const found = findPropGroup(component.propGroups, propGroup.parentGroupId);
|
|
468
712
|
if (!found)
|
|
@@ -495,6 +739,32 @@ function createEditorHandlers(configPath, getConfig, setConfig, wsServer) {
|
|
|
495
739
|
found.group.name = updates.name;
|
|
496
740
|
if (updates.description !== undefined)
|
|
497
741
|
found.group.description = updates.description || undefined;
|
|
742
|
+
if (updates.requiredProp !== undefined) {
|
|
743
|
+
if (!updates.requiredProp || updates.requiredProp === "none") {
|
|
744
|
+
// Both go together — a state with no prop behind it would read as a rule.
|
|
745
|
+
delete found.group.requiredProp;
|
|
746
|
+
delete found.group.requiredPropValue;
|
|
747
|
+
delete found.group.requiredEnumOptionId;
|
|
748
|
+
delete found.group.requiredEnumOptionValue;
|
|
749
|
+
}
|
|
750
|
+
else {
|
|
751
|
+
found.group.requiredProp = updates.requiredProp;
|
|
752
|
+
if (updates.requiredEnumOptionValue !== undefined) {
|
|
753
|
+
found.group.requiredEnumOptionId = updates.requiredEnumOptionId;
|
|
754
|
+
found.group.requiredEnumOptionValue =
|
|
755
|
+
updates.requiredEnumOptionValue;
|
|
756
|
+
delete found.group.requiredPropValue;
|
|
757
|
+
}
|
|
758
|
+
else {
|
|
759
|
+
found.group.requiredPropValue = updates.requiredPropValue ?? true;
|
|
760
|
+
delete found.group.requiredEnumOptionId;
|
|
761
|
+
delete found.group.requiredEnumOptionValue;
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
const gateError = visibilityGateError(component, found.group, `group "${found.group.id}"`, { group: found.group });
|
|
766
|
+
if (gateError)
|
|
767
|
+
throw new Error(gateError);
|
|
498
768
|
writeConfig(configPath, config);
|
|
499
769
|
setConfig(config);
|
|
500
770
|
broadcastConfigChange(componentId);
|
|
@@ -685,8 +955,10 @@ function createEditorHandlers(configPath, getConfig, setConfig, wsServer) {
|
|
|
685
955
|
regenerateAllComponentTypes(config);
|
|
686
956
|
console.log(chalk.green(` Deleted enum: ${removed.name} (${enumId})`));
|
|
687
957
|
},
|
|
688
|
-
onSyncTypes: (types, customTypes) => {
|
|
958
|
+
onSyncTypes: (types, customTypes, themeSettings) => {
|
|
689
959
|
cachedEditorTypes = types;
|
|
960
|
+
if (themeSettings)
|
|
961
|
+
cachedThemeSettings = themeSettings;
|
|
690
962
|
// Merge incoming custom types with existing ones instead of replacing.
|
|
691
963
|
// Custom types belong to the code component project and should not be
|
|
692
964
|
// lost when a different editor project connects.
|
|
@@ -765,6 +1037,27 @@ async function startDevServer(opts = {}) {
|
|
|
765
1037
|
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
766
1038
|
console.log(chalk.yellow(` Auto-generated projectId: ${config.projectId}`));
|
|
767
1039
|
}
|
|
1040
|
+
else if (!isValidProjectId(config.projectId)) {
|
|
1041
|
+
// A malformed projectId (hand-edited config) breaks the editor's republish
|
|
1042
|
+
// stability: published/local id prefixes stop matching and every publish
|
|
1043
|
+
// INSERTs duplicate design elements. Fail fast instead of corrupting data.
|
|
1044
|
+
console.error(chalk.red(`Error: invalid projectId "${config.projectId}" in ikas.config.json`));
|
|
1045
|
+
console.error(chalk.gray("projectId must be at least 6 alphanumeric characters (no '-' or '_'). " +
|
|
1046
|
+
"Restore the original generated value — do not hand-edit it."));
|
|
1047
|
+
process.exit(1);
|
|
1048
|
+
}
|
|
1049
|
+
// Component ids carry the same prefix/suffix contract as projectId; the dev
|
|
1050
|
+
// server syncs them into the editor (and from there into themes), so a
|
|
1051
|
+
// hand-edited id must be rejected before the server starts.
|
|
1052
|
+
const invalidComponents = findInvalidComponentIds(config.components);
|
|
1053
|
+
if (invalidComponents.length > 0) {
|
|
1054
|
+
console.error(chalk.red("Error: invalid component id(s) in ikas.config.json:"));
|
|
1055
|
+
invalidComponents.forEach((c) => {
|
|
1056
|
+
console.error(chalk.red(` - "${c.id}"${c.name ? ` (${c.name})` : ""}`));
|
|
1057
|
+
});
|
|
1058
|
+
console.error(chalk.gray("Component ids are generated — restore the original values; do not hand-edit them."));
|
|
1059
|
+
process.exit(1);
|
|
1060
|
+
}
|
|
768
1061
|
console.log(chalk.cyan("\n ikas Component Dev Server\n"));
|
|
769
1062
|
console.log(chalk.gray(` Project: ${config.name}`));
|
|
770
1063
|
console.log(chalk.gray(` Components: ${config.components.length}\n`));
|
|
@@ -792,6 +1085,18 @@ async function startDevServer(opts = {}) {
|
|
|
792
1085
|
// Initial compilation of all components (multi-entry with code splitting)
|
|
793
1086
|
console.log(chalk.cyan(" Compiling components..."));
|
|
794
1087
|
await compileAllComponentsForDev(config);
|
|
1088
|
+
// Seed the broadcast-hash maps so the first delta-broadcast after startup doesn't replay
|
|
1089
|
+
// the initial state — newly-connecting editors get a full `initial-state` directly via
|
|
1090
|
+
// `onRequestState`, independent of the delta path.
|
|
1091
|
+
for (const [id, comp] of compiledComponents) {
|
|
1092
|
+
if (comp.contentHash)
|
|
1093
|
+
lastBroadcastComponentHashes.set(id, comp.contentHash);
|
|
1094
|
+
}
|
|
1095
|
+
for (const [id, mod] of compiledSharedModules) {
|
|
1096
|
+
if (mod.contentHash)
|
|
1097
|
+
lastBroadcastSharedHashes.set(id, mod.contentHash);
|
|
1098
|
+
}
|
|
1099
|
+
lastBroadcastGlobalStyleHash = computeGlobalStyleHash(compiledGlobalStyles);
|
|
795
1100
|
console.log("");
|
|
796
1101
|
// Start WebSocket server
|
|
797
1102
|
const wsServer = new DevServerWebSocket(DEV_SERVER_PORT);
|
|
@@ -805,6 +1110,16 @@ async function startDevServer(opts = {}) {
|
|
|
805
1110
|
console.error(chalk.red(` Failed to start WebSocket server: ${error.message}`));
|
|
806
1111
|
process.exit(1);
|
|
807
1112
|
}
|
|
1113
|
+
console.log(chalk.green(` Dev server listening on 127.0.0.1:${DEV_SERVER_PORT}`));
|
|
1114
|
+
// Studio reaches this server only through the tunnel + websocket routing
|
|
1115
|
+
// that `ikas theme dev` registers; a directly-started server is invisible
|
|
1116
|
+
// to the editor. Hint only on a TTY — under `ikas theme dev` stdio is
|
|
1117
|
+
// piped and the wrapper already handles the connection.
|
|
1118
|
+
if (process.stdout.isTTY) {
|
|
1119
|
+
console.log(chalk.yellow("\n Note: ikas Studio cannot connect to this server directly."));
|
|
1120
|
+
console.log(chalk.yellow(" To develop with the editor, run `ikas theme dev` in this folder —"));
|
|
1121
|
+
console.log(chalk.yellow(" it starts this server, opens a tunnel and registers it with your store."));
|
|
1122
|
+
}
|
|
808
1123
|
// Watch for file changes
|
|
809
1124
|
const watchPaths = [
|
|
810
1125
|
"src/**/*.tsx",
|
|
@@ -832,16 +1147,15 @@ async function startDevServer(opts = {}) {
|
|
|
832
1147
|
console.log(chalk.yellow(` Recompiling all (triggered by: ${triggeredBy})...`));
|
|
833
1148
|
const success = await compileAllComponentsForDev(config);
|
|
834
1149
|
if (success) {
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
});
|
|
1150
|
+
const delta = broadcastCompileDelta(wsServer);
|
|
1151
|
+
if (delta.changedComponents === 0 &&
|
|
1152
|
+
delta.deletedComponents === 0 &&
|
|
1153
|
+
!delta.globalStylesChanged) {
|
|
1154
|
+
console.log(chalk.gray(" No editor-visible changes (skipped broadcast)"));
|
|
1155
|
+
}
|
|
1156
|
+
else {
|
|
1157
|
+
console.log(chalk.cyan(` Broadcast delta: ${delta.changedComponents} changed, ${delta.deletedComponents} deleted, ${delta.changedSharedModules} shared chunks changed${delta.globalStylesChanged ? ", globals updated" : ""}`));
|
|
1158
|
+
}
|
|
845
1159
|
}
|
|
846
1160
|
else {
|
|
847
1161
|
// Broadcast error for the triggering component if identifiable
|
|
@@ -858,20 +1172,34 @@ async function startDevServer(opts = {}) {
|
|
|
858
1172
|
if (normalizedPath === configPath) {
|
|
859
1173
|
console.log(chalk.yellow("\n Config changed, reloading..."));
|
|
860
1174
|
try {
|
|
861
|
-
|
|
1175
|
+
const reloaded = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
|
1176
|
+
// The startup guards validated projectId/component ids once; a hot-reload
|
|
1177
|
+
// must not smuggle a hand-edited value past them — a malformed id reaching
|
|
1178
|
+
// `generateComponentId` or chunk namespacing corrupts republish matching.
|
|
1179
|
+
// Keep the session's validated projectId if the key was removed; reject the
|
|
1180
|
+
// reload outright (previous config stays active) on invalid values.
|
|
1181
|
+
if (!reloaded.projectId) {
|
|
1182
|
+
reloaded.projectId = config.projectId;
|
|
1183
|
+
}
|
|
1184
|
+
else if (!isValidProjectId(reloaded.projectId)) {
|
|
1185
|
+
console.error(chalk.red(` Ignoring config reload: invalid projectId "${reloaded.projectId}". `) +
|
|
1186
|
+
chalk.gray("Restore the original generated value — previous config stays active."));
|
|
1187
|
+
return;
|
|
1188
|
+
}
|
|
1189
|
+
const reloadedInvalidComponents = findInvalidComponentIds(reloaded.components ?? []);
|
|
1190
|
+
if (reloadedInvalidComponents.length > 0) {
|
|
1191
|
+
console.error(chalk.red(` Ignoring config reload: invalid component id(s): ${reloadedInvalidComponents
|
|
1192
|
+
.map((c) => `"${c.id}"`)
|
|
1193
|
+
.join(", ")}. `) +
|
|
1194
|
+
chalk.gray("Restore the original generated values — previous config stays active."));
|
|
1195
|
+
return;
|
|
1196
|
+
}
|
|
1197
|
+
config = reloaded;
|
|
862
1198
|
cachedCustomTypes = config.customTypes || [];
|
|
863
1199
|
writeGlobalTypesFile(config);
|
|
864
1200
|
await compileAllComponentsForDev(config);
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
type: "initial-state",
|
|
868
|
-
payload: {
|
|
869
|
-
components: getAllComponents(),
|
|
870
|
-
sharedModules: getAllSharedModules(),
|
|
871
|
-
globalStyles: compiledGlobalStyles,
|
|
872
|
-
timestamp: Date.now(),
|
|
873
|
-
},
|
|
874
|
-
});
|
|
1201
|
+
const delta = broadcastCompileDelta(wsServer);
|
|
1202
|
+
console.log(chalk.cyan(` Config reload delta: ${delta.changedComponents} changed, ${delta.deletedComponents} deleted, ${delta.changedSharedModules} shared chunks changed${delta.globalStylesChanged ? ", globals updated" : ""}`));
|
|
875
1203
|
}
|
|
876
1204
|
catch (e) {
|
|
877
1205
|
console.error(chalk.red(" Failed to reload config"));
|
|
@@ -898,6 +1226,23 @@ async function startDevServer(opts = {}) {
|
|
|
898
1226
|
const entryPath = path.resolve(process.cwd(), component.entry);
|
|
899
1227
|
if (!fs.existsSync(entryPath)) {
|
|
900
1228
|
compiledComponents.delete(component.id);
|
|
1229
|
+
// Sync ikas.config.json: remove the component entry and strip any orphaned
|
|
1230
|
+
// filteredComponentIds references in remaining components' props. Write config
|
|
1231
|
+
// before broadcasting so an editor requesting state sees the post-delete view.
|
|
1232
|
+
const idx = config.components.findIndex((c) => c.id === component.id);
|
|
1233
|
+
if (idx !== -1) {
|
|
1234
|
+
config.components.splice(idx, 1);
|
|
1235
|
+
for (const remaining of config.components) {
|
|
1236
|
+
for (const prop of remaining.props || []) {
|
|
1237
|
+
if (!prop.filteredComponentIds)
|
|
1238
|
+
continue;
|
|
1239
|
+
prop.filteredComponentIds = prop.filteredComponentIds.filter((id) => id !== component.id);
|
|
1240
|
+
if (prop.filteredComponentIds.length === 0)
|
|
1241
|
+
delete prop.filteredComponentIds;
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
writeConfig(configPath, config);
|
|
1245
|
+
}
|
|
901
1246
|
wsServer.broadcastComponentDeleted(component.id);
|
|
902
1247
|
console.log(chalk.red(` Component removed: ${component.name}`));
|
|
903
1248
|
}
|