@embeddables/cli 0.6.0 → 0.6.2
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/.prompts/embeddables-cli.md +2 -2
- package/dist/cli.js +2 -0
- package/dist/commands/build.d.ts +1 -0
- package/dist/commands/build.d.ts.map +1 -1
- package/dist/commands/build.js +1 -0
- package/dist/commands/dev.d.ts +1 -0
- package/dist/commands/dev.d.ts.map +1 -1
- package/dist/commands/dev.js +3 -0
- package/dist/commands/pull.d.ts.map +1 -1
- package/dist/commands/pull.js +40 -0
- package/dist/compiler/helpers/numericLeadingKeys.d.ts +8 -0
- package/dist/compiler/helpers/numericLeadingKeys.d.ts.map +1 -0
- package/dist/compiler/helpers/numericLeadingKeys.js +17 -0
- package/dist/compiler/index.d.ts +2 -0
- package/dist/compiler/index.d.ts.map +1 -1
- package/dist/compiler/index.js +305 -57
- package/dist/compiler/parsePage.d.ts.map +1 -1
- package/dist/compiler/parsePage.js +68 -26
- package/dist/compiler/reverse.d.ts.map +1 -1
- package/dist/compiler/reverse.js +134 -12
- package/dist/components/primitives/InputBox.d.ts +1 -1
- package/dist/components/primitives/InputBox.d.ts.map +1 -1
- package/dist/proxy/server.d.ts.map +1 -1
- package/dist/proxy/server.js +10 -1
- package/dist/types-builder.d.ts +2 -2
- package/dist/types-builder.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -77,7 +77,7 @@ All component types are derived from `src/types-builder.ts`. The relevant typing
|
|
|
77
77
|
**Key Base Properties** (all components have these):
|
|
78
78
|
|
|
79
79
|
- `id: string` - Unique identifier - this must be unique across the entire Embeddable JSON. Always snake_case.
|
|
80
|
-
- `key: string` - Unique identifier, used in React as key prop - unique in general but duplicate keys can be used in certain cases (e.g. two `email` fields, each hidden by conditions). Always snake_case.
|
|
80
|
+
- `key: string` - Unique identifier, used in React as key prop - unique in general but duplicate keys can be used in certain cases (e.g. two `email` fields, each hidden by conditions). Always snake_case. **Keys must not start with a digit** (invalid in JS/JSON). When deriving a key from text that would start with a number (e.g. "1 to 2 weeks" → "1_to_2_weeks"), use a semantic prefix instead: e.g. `range_1_to_2_weeks`, `option_1_to_2_weeks`, or the component key like `delivery_time_1_to_2_weeks`.
|
|
81
81
|
- `type: ComponentType` - Component type
|
|
82
82
|
- `tags?: string[]` - Used for CSS styling
|
|
83
83
|
- `parent_id?: string` - For nested components
|
|
@@ -98,7 +98,7 @@ All component types are derived from `src/types-builder.ts`. The relevant typing
|
|
|
98
98
|
- `outputs_onchange`
|
|
99
99
|
- etc.
|
|
100
100
|
- `OptionSelector`
|
|
101
|
-
- `buttons`
|
|
101
|
+
- `buttons` - each button has a `key` (and optional `text`, `description`, etc.). **Button keys must not start with a digit.** If the option label would slug to a key starting with a number (e.g. "1 to 2 weeks" → "1_to_2_weeks"), use a prefix such as the component key (e.g. `delivery_range_1_to_2_weeks`) or a short semantic prefix (e.g. `range_1_to_2_weeks`, `option_1_to_2_weeks`).
|
|
102
102
|
- `multiple` - whether to allow multi-select
|
|
103
103
|
- `dropdown`
|
|
104
104
|
- `checkbox` - whether to add a (square/round) visual checkbox in each button (the checkbox will be added for you, no need to add it manually)
|
package/dist/cli.js
CHANGED
|
@@ -33,6 +33,7 @@ program
|
|
|
33
33
|
.option('-i, --id <id>', 'Embeddable ID (will prompt if not provided)')
|
|
34
34
|
.option('-p, --pages <glob>', 'Pages glob')
|
|
35
35
|
.option('-o, --out <path>', 'Output json path')
|
|
36
|
+
.option('--fix', 'Apply lint fixes (duplicate IDs, keys/IDs starting with a number)')
|
|
36
37
|
.option('--pageKeyFrom <mode>', 'filename|export', 'filename')
|
|
37
38
|
.action(async (opts) => {
|
|
38
39
|
await runBuild(opts);
|
|
@@ -42,6 +43,7 @@ program
|
|
|
42
43
|
.option('-i, --id <id>', 'Embeddable ID (will prompt if not provided)')
|
|
43
44
|
.option('-p, --pages <glob>', 'Pages glob')
|
|
44
45
|
.option('-o, --out <path>', 'Output json path')
|
|
46
|
+
.option('--fix', 'Apply lint fixes (duplicate IDs, keys/IDs starting with a number)')
|
|
45
47
|
.option('-L, --local', 'Use local engine (http://localhost:8787)')
|
|
46
48
|
.option('-e, --engine <url>', 'Engine origin', 'https://engine.embeddables.com')
|
|
47
49
|
.option('--port <n>', 'Dev proxy port', '3000')
|
package/dist/commands/build.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../../src/commands/build.ts"],"names":[],"mappings":"AAKA,wBAAsB,QAAQ,CAAC,IAAI,EAAE;IACnC,EAAE,CAAC,EAAE,MAAM,CAAA;IACX,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,WAAW,EAAE,UAAU,GAAG,QAAQ,CAAA;CACnC,
|
|
1
|
+
{"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../../src/commands/build.ts"],"names":[],"mappings":"AAKA,wBAAsB,QAAQ,CAAC,IAAI,EAAE;IACnC,EAAE,CAAC,EAAE,MAAM,CAAA;IACX,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,GAAG,CAAC,EAAE,OAAO,CAAA;IACb,WAAW,EAAE,UAAU,GAAG,QAAQ,CAAA;CACnC,iBA6BA"}
|
package/dist/commands/build.js
CHANGED
package/dist/commands/dev.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dev.d.ts","sourceRoot":"","sources":["../../src/commands/dev.ts"],"names":[],"mappings":"AAmHA,wBAAsB,MAAM,CAAC,IAAI,EAAE;IACjC,EAAE,CAAC,EAAE,MAAM,CAAA;IACX,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,KAAK,CAAC,EAAE,OAAO,CAAA;IACf,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,MAAM,CAAA;IACZ,aAAa,EAAE,MAAM,CAAA;IACrB,WAAW,EAAE,UAAU,GAAG,QAAQ,CAAA;CACnC,
|
|
1
|
+
{"version":3,"file":"dev.d.ts","sourceRoot":"","sources":["../../src/commands/dev.ts"],"names":[],"mappings":"AAmHA,wBAAsB,MAAM,CAAC,IAAI,EAAE;IACjC,EAAE,CAAC,EAAE,MAAM,CAAA;IACX,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,GAAG,CAAC,EAAE,OAAO,CAAA;IACb,KAAK,CAAC,EAAE,OAAO,CAAA;IACf,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,MAAM,CAAA;IACZ,aAAa,EAAE,MAAM,CAAA;IACrB,WAAW,EAAE,UAAU,GAAG,QAAQ,CAAA;CACnC,iBA+GA"}
|
package/dist/commands/dev.js
CHANGED
|
@@ -108,6 +108,7 @@ export async function runDev(opts) {
|
|
|
108
108
|
const globalComponentsGlob = `embeddables/${embeddableId}/global-components/**/*.location.tsx`;
|
|
109
109
|
const computedFieldsGlob = `embeddables/${embeddableId}/computed-fields/**/*.js`;
|
|
110
110
|
const actionsGlob = `embeddables/${embeddableId}/actions/**/*.js`;
|
|
111
|
+
const fixLint = opts.fix ? true : 'prompt';
|
|
111
112
|
// Initial build
|
|
112
113
|
try {
|
|
113
114
|
await compileAllPages({
|
|
@@ -117,6 +118,7 @@ export async function runDev(opts) {
|
|
|
117
118
|
stylesDir,
|
|
118
119
|
embeddableId,
|
|
119
120
|
configPath,
|
|
121
|
+
fixLint,
|
|
120
122
|
});
|
|
121
123
|
}
|
|
122
124
|
catch (e) {
|
|
@@ -158,6 +160,7 @@ export async function runDev(opts) {
|
|
|
158
160
|
stylesDir,
|
|
159
161
|
embeddableId,
|
|
160
162
|
configPath,
|
|
163
|
+
fixLint,
|
|
161
164
|
});
|
|
162
165
|
proxy.broadcastReload();
|
|
163
166
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pull.d.ts","sourceRoot":"","sources":["../../src/commands/pull.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"pull.d.ts","sourceRoot":"","sources":["../../src/commands/pull.ts"],"names":[],"mappings":"AA+GA,MAAM,MAAM,cAAc,GAAG;IAC3B,EAAE,CAAC,EAAE,MAAM,CAAA;IACX,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,GAAG,CAAC,EAAE,OAAO,CAAA;CACd,CAAA;AAED,wBAAsB,OAAO,CAAC,IAAI,EAAE,cAAc,iBAqPjD"}
|
package/dist/commands/pull.js
CHANGED
|
@@ -6,6 +6,44 @@ import { reverseCompile } from '../compiler/reverse.js';
|
|
|
6
6
|
import { getAccessToken, isLoggedIn } from '../auth/index.js';
|
|
7
7
|
import { getProjectId, writeProjectConfig } from '../config/index.js';
|
|
8
8
|
import { promptForProject, promptForEmbeddable, fetchEmbeddableMetadata } from '../prompts/index.js';
|
|
9
|
+
/**
|
|
10
|
+
* Normalize custom_validation_function strings so literal "\\n" (backslash-n) becomes real newline.
|
|
11
|
+
* The API sometimes returns these double-escaped; dataOutputs/computedFields code is written
|
|
12
|
+
* as raw file content so they don't get double-escaped when written to embeddable.json.
|
|
13
|
+
*/
|
|
14
|
+
function normalizeCustomValidationFunctionsInFlow(flow) {
|
|
15
|
+
function walkComponent(comp) {
|
|
16
|
+
const v = comp.custom_validation_function;
|
|
17
|
+
if (typeof v === 'string') {
|
|
18
|
+
comp.custom_validation_function = v.replace(/\\n/g, '\n');
|
|
19
|
+
}
|
|
20
|
+
const children = comp.components ?? comp.buttons;
|
|
21
|
+
if (Array.isArray(children)) {
|
|
22
|
+
for (const c of children) {
|
|
23
|
+
if (c && typeof c === 'object')
|
|
24
|
+
walkComponent(c);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
const pages = flow.pages;
|
|
29
|
+
if (Array.isArray(pages)) {
|
|
30
|
+
for (const page of pages) {
|
|
31
|
+
if (page && typeof page === 'object' && Array.isArray(page.components)) {
|
|
32
|
+
for (const comp of page.components) {
|
|
33
|
+
if (comp && typeof comp === 'object')
|
|
34
|
+
walkComponent(comp);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
const components = flow.components;
|
|
40
|
+
if (Array.isArray(components)) {
|
|
41
|
+
for (const comp of components) {
|
|
42
|
+
if (comp && typeof comp === 'object')
|
|
43
|
+
walkComponent(comp);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
9
47
|
/** Slug for branch name/id for use in filenames (e.g. "my branch" -> "my_branch"). */
|
|
10
48
|
function slugForBranch(nameOrId) {
|
|
11
49
|
return String(nameOrId).replace(/[^a-zA-Z0-9_.-]/g, '_').replace(/_+/g, '_') || 'main';
|
|
@@ -145,6 +183,8 @@ export async function runPull(opts) {
|
|
|
145
183
|
if (!flow) {
|
|
146
184
|
throw new Error("Response does not contain a 'flow' property");
|
|
147
185
|
}
|
|
186
|
+
// Normalize custom_validation_function: literal \n (backslash-n) -> real newline (so embeddable.json is correct)
|
|
187
|
+
normalizeCustomValidationFunctionsInFlow(flow);
|
|
148
188
|
// Save to embeddables/<id>/.generated/embeddable.json
|
|
149
189
|
fs.mkdirSync(path.dirname(outPath), { recursive: true });
|
|
150
190
|
const flowJson = JSON.stringify(flow, null, 2);
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keys and IDs must not start with a digit (invalid in JS/JSON).
|
|
3
|
+
* This helper produces a safe key by prefixing when needed.
|
|
4
|
+
*/
|
|
5
|
+
/** Returns a key safe for use (prefix applied if key starts with a digit). */
|
|
6
|
+
export declare function normalizeKeyIfStartsWithDigit(key: string | null, prefix: string): string | null;
|
|
7
|
+
export declare function keyOrIdStartsWithDigit(value: string | null | undefined): boolean;
|
|
8
|
+
//# sourceMappingURL=numericLeadingKeys.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"numericLeadingKeys.d.ts","sourceRoot":"","sources":["../../../src/compiler/helpers/numericLeadingKeys.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,8EAA8E;AAC9E,wBAAgB,6BAA6B,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAI/F;AAED,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,OAAO,CAIhF"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keys and IDs must not start with a digit (invalid in JS/JSON).
|
|
3
|
+
* This helper produces a safe key by prefixing when needed.
|
|
4
|
+
*/
|
|
5
|
+
/** Returns a key safe for use (prefix applied if key starts with a digit). */
|
|
6
|
+
export function normalizeKeyIfStartsWithDigit(key, prefix) {
|
|
7
|
+
if (key === null || typeof key !== 'string' || key === '')
|
|
8
|
+
return key;
|
|
9
|
+
if (!/^\d/.test(key))
|
|
10
|
+
return key;
|
|
11
|
+
return `${prefix}_${key}`;
|
|
12
|
+
}
|
|
13
|
+
export function keyOrIdStartsWithDigit(value) {
|
|
14
|
+
if (value === null || value === undefined || typeof value !== 'string' || value === '')
|
|
15
|
+
return false;
|
|
16
|
+
return /^\d/.test(value);
|
|
17
|
+
}
|
package/dist/compiler/index.d.ts
CHANGED
|
@@ -12,5 +12,7 @@ export declare function compileAllPages(opts: {
|
|
|
12
12
|
stylesDir?: string;
|
|
13
13
|
embeddableId?: string;
|
|
14
14
|
configPath?: string;
|
|
15
|
+
/** When true, apply lint fixes (duplicate IDs, keys/IDs starting with a digit). When 'prompt', ask user to fix. */
|
|
16
|
+
fixLint?: true | 'prompt';
|
|
15
17
|
}): Promise<void>;
|
|
16
18
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/compiler/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/compiler/index.ts"],"names":[],"mappings":"AAmDA;;;;;GAKG;AACH,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,GAAG,OAAO,CA2DlF;AAED,wBAAsB,eAAe,CAAC,IAAI,EAAE;IAC1C,SAAS,EAAE,MAAM,CAAA;IACjB,OAAO,EAAE,MAAM,CAAA;IACf,WAAW,EAAE,UAAU,GAAG,QAAQ,CAAA;IAClC,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,mHAAmH;IACnH,OAAO,CAAC,EAAE,IAAI,GAAG,QAAQ,CAAA;CAC1B,iBA4cA"}
|
package/dist/compiler/index.js
CHANGED
|
@@ -6,6 +6,7 @@ import { CompileError } from './errors.js';
|
|
|
6
6
|
import CSSJSON from 'cssjson';
|
|
7
7
|
import { sanitizeFileName } from './reverse.js';
|
|
8
8
|
import { generateRandomIdByType } from './helpers/duplicateIds.js';
|
|
9
|
+
import { normalizeKeyIfStartsWithDigit, keyOrIdStartsWithDigit, } from './helpers/numericLeadingKeys.js';
|
|
9
10
|
import { parse } from '@babel/parser';
|
|
10
11
|
import traverseImport from '@babel/traverse';
|
|
11
12
|
import * as generator from '@babel/generator';
|
|
@@ -103,16 +104,13 @@ export async function compileAllPages(opts) {
|
|
|
103
104
|
if (files.length === 0) {
|
|
104
105
|
throw new CompileError(`No pages found for glob: ${opts.pagesGlob}`);
|
|
105
106
|
}
|
|
106
|
-
// First pass: collect
|
|
107
|
-
// Note: We check pages first, then global components are checked separately in loadGlobalComponents
|
|
107
|
+
// First pass: parse and collect lint issues (duplicate IDs, keys/IDs starting with a digit)
|
|
108
108
|
const idOccurrences = new Map();
|
|
109
109
|
const filePages = [];
|
|
110
|
-
// Parse all page files first to collect IDs
|
|
111
110
|
for (const file of files) {
|
|
112
111
|
const code = fs.readFileSync(file, 'utf8');
|
|
113
112
|
const pageKey = derivePageKey(file, opts.pageKeyFrom);
|
|
114
113
|
const page = parsePageFromFile({ code, filePath: file, pageKey });
|
|
115
|
-
// Collect component and button IDs with their locations
|
|
116
114
|
for (let compIndex = 0; compIndex < page.components.length; compIndex++) {
|
|
117
115
|
const component = page.components[compIndex];
|
|
118
116
|
const compId = component.id;
|
|
@@ -126,7 +124,6 @@ export async function compileAllPages(opts) {
|
|
|
126
124
|
componentIndex: compIndex,
|
|
127
125
|
id: compId,
|
|
128
126
|
});
|
|
129
|
-
// Collect button IDs from OptionSelector components
|
|
130
127
|
if (component.type === 'OptionSelector' && Array.isArray(component.buttons)) {
|
|
131
128
|
for (let btnIndex = 0; btnIndex < component.buttons.length; btnIndex++) {
|
|
132
129
|
const button = component.buttons[btnIndex];
|
|
@@ -149,20 +146,16 @@ export async function compileAllPages(opts) {
|
|
|
149
146
|
}
|
|
150
147
|
filePages.push({ file, pageKey, page });
|
|
151
148
|
}
|
|
152
|
-
//
|
|
153
|
-
const idFixes = new Map();
|
|
149
|
+
// Collect duplicate ID fixes (do not apply yet)
|
|
150
|
+
const idFixes = new Map();
|
|
154
151
|
const allIds = new Set();
|
|
155
|
-
// Collect all existing IDs
|
|
156
152
|
for (const occurrences of idOccurrences.values()) {
|
|
157
153
|
for (const occ of occurrences) {
|
|
158
154
|
allIds.add(occ.id);
|
|
159
155
|
}
|
|
160
156
|
}
|
|
161
|
-
// Find duplicates and generate unique replacements
|
|
162
157
|
for (const [id, occurrences] of idOccurrences.entries()) {
|
|
163
158
|
if (occurrences.length > 1) {
|
|
164
|
-
console.warn(`Found duplicate ID "${id}" used ${occurrences.length} times. Fixing...`);
|
|
165
|
-
// Keep first occurrence, fix the rest (type-based: plaintext_xxx, button_xxx, option_xxx)
|
|
166
159
|
for (let i = 1; i < occurrences.length; i++) {
|
|
167
160
|
const occ = occurrences[i];
|
|
168
161
|
const fixKey = occ.buttonIndex !== undefined
|
|
@@ -172,65 +165,154 @@ export async function compileAllPages(opts) {
|
|
|
172
165
|
const newId = generateRandomIdByType(occ.componentType, isOptionButton, allIds, idFixes.values());
|
|
173
166
|
allIds.add(newId);
|
|
174
167
|
idFixes.set(fixKey, newId);
|
|
175
|
-
console.warn(` → Fixing duplicate ID "${id}" to "${newId}" in ${occ.file} (component ${occ.componentIndex}${occ.buttonIndex !== undefined ? `, button ${occ.buttonIndex}` : ''})`);
|
|
176
168
|
}
|
|
177
169
|
}
|
|
178
170
|
}
|
|
179
|
-
//
|
|
180
|
-
|
|
181
|
-
|
|
171
|
+
// Collect numeric-leading key/id fixes (component key, component id, button key, button id)
|
|
172
|
+
const numericLeadingFixes = [];
|
|
173
|
+
for (const { file, pageKey, page } of filePages) {
|
|
174
|
+
for (let compIndex = 0; compIndex < page.components.length; compIndex++) {
|
|
175
|
+
const comp = page.components[compIndex];
|
|
176
|
+
if (keyOrIdStartsWithDigit(comp.key)) {
|
|
177
|
+
const newKey = normalizeKeyIfStartsWithDigit(comp.key, pageKey);
|
|
178
|
+
if (newKey)
|
|
179
|
+
numericLeadingFixes.push({
|
|
180
|
+
file,
|
|
181
|
+
componentIndex: compIndex,
|
|
182
|
+
kind: 'componentKey',
|
|
183
|
+
oldValue: comp.key,
|
|
184
|
+
newValue: newKey,
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
if (keyOrIdStartsWithDigit(comp.id)) {
|
|
188
|
+
const newId = normalizeKeyIfStartsWithDigit(comp.id, 'comp');
|
|
189
|
+
if (newId)
|
|
190
|
+
numericLeadingFixes.push({
|
|
191
|
+
file,
|
|
192
|
+
componentIndex: compIndex,
|
|
193
|
+
kind: 'componentId',
|
|
194
|
+
oldValue: comp.id,
|
|
195
|
+
newValue: newId,
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
if (comp.type === 'OptionSelector' && Array.isArray(comp.buttons)) {
|
|
199
|
+
const prefix = comp.key ?? 'option';
|
|
200
|
+
for (let btnIndex = 0; btnIndex < comp.buttons.length; btnIndex++) {
|
|
201
|
+
const btn = comp.buttons[btnIndex];
|
|
202
|
+
if (btn && keyOrIdStartsWithDigit(btn.key)) {
|
|
203
|
+
const newKey = normalizeKeyIfStartsWithDigit(btn.key, prefix);
|
|
204
|
+
if (newKey)
|
|
205
|
+
numericLeadingFixes.push({
|
|
206
|
+
file,
|
|
207
|
+
componentIndex: compIndex,
|
|
208
|
+
buttonIndex: btnIndex,
|
|
209
|
+
kind: 'buttonKey',
|
|
210
|
+
oldValue: btn.key,
|
|
211
|
+
newValue: newKey,
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
if (btn && keyOrIdStartsWithDigit(btn.id)) {
|
|
215
|
+
const safeKey = normalizeKeyIfStartsWithDigit(btn.key, prefix);
|
|
216
|
+
const newId = btn.key != null && btn.id === `option_${btn.key}`
|
|
217
|
+
? safeKey != null
|
|
218
|
+
? `option_${safeKey}`
|
|
219
|
+
: normalizeKeyIfStartsWithDigit(btn.id, 'option')
|
|
220
|
+
: normalizeKeyIfStartsWithDigit(btn.id, 'option');
|
|
221
|
+
if (newId)
|
|
222
|
+
numericLeadingFixes.push({
|
|
223
|
+
file,
|
|
224
|
+
componentIndex: compIndex,
|
|
225
|
+
buttonIndex: btnIndex,
|
|
226
|
+
kind: 'buttonId',
|
|
227
|
+
oldValue: btn.id,
|
|
228
|
+
newValue: newId,
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
const totalLintIssues = idFixes.size + numericLeadingFixes.length;
|
|
236
|
+
let shouldApplyFixes = false;
|
|
237
|
+
if (totalLintIssues > 0) {
|
|
238
|
+
// Report all issues
|
|
239
|
+
if (idFixes.size > 0) {
|
|
240
|
+
console.warn(`Found ${idFixes.size} duplicate ID(s). Keys and IDs must be unique across the embeddable.`);
|
|
241
|
+
for (const [fixKey, newId] of idFixes) {
|
|
242
|
+
const parts = fixKey.split(':');
|
|
243
|
+
const file = parts[0];
|
|
244
|
+
const compIdx = parts[1];
|
|
245
|
+
const btnIdx = parts[2];
|
|
246
|
+
const loc = btnIdx !== undefined ? `component ${compIdx}, button ${btnIdx}` : `component ${compIdx}`;
|
|
247
|
+
console.warn(` → ${file}: ${loc} → "${newId}"`);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
if (numericLeadingFixes.length > 0) {
|
|
251
|
+
console.warn(`Found ${numericLeadingFixes.length} key(s)/ID(s) that start with a number. Use a prefix (e.g. range_1_to_2_weeks).`);
|
|
252
|
+
for (const f of numericLeadingFixes) {
|
|
253
|
+
const loc = f.buttonIndex !== undefined
|
|
254
|
+
? `component ${f.componentIndex}, button ${f.buttonIndex} (${f.kind})`
|
|
255
|
+
: `component ${f.componentIndex} (${f.kind})`;
|
|
256
|
+
console.warn(` → ${f.file}: "${f.oldValue}" → "${f.newValue}" (${loc})`);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
if (opts.fixLint === true) {
|
|
260
|
+
shouldApplyFixes = true;
|
|
261
|
+
}
|
|
262
|
+
else if (opts.fixLint === 'prompt') {
|
|
263
|
+
const { default: prompts } = await import('prompts');
|
|
264
|
+
const res = await prompts({
|
|
265
|
+
type: 'confirm',
|
|
266
|
+
name: 'apply',
|
|
267
|
+
message: `Apply ${totalLintIssues} fix(es) to source files?`,
|
|
268
|
+
initial: true,
|
|
269
|
+
});
|
|
270
|
+
shouldApplyFixes = res.apply === true;
|
|
271
|
+
}
|
|
272
|
+
if (!shouldApplyFixes) {
|
|
273
|
+
throw new CompileError(`Build failed: ${totalLintIssues} lint issue(s) (duplicate IDs and/or keys/IDs starting with a number). Run with --fix or answer 'y' when prompted to fix.`, { file: files[0] });
|
|
274
|
+
}
|
|
275
|
+
// Apply fixes
|
|
182
276
|
for (const { file } of filePages) {
|
|
183
|
-
const
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
277
|
+
const idFixesForFile = Array.from(idFixes.entries()).filter(([key]) => key.startsWith(file + ':'));
|
|
278
|
+
const numericForFile = numericLeadingFixes.filter((f) => f.file === file);
|
|
279
|
+
if (idFixesForFile.length > 0 || numericForFile.length > 0) {
|
|
280
|
+
let code = fs.readFileSync(file, 'utf8');
|
|
281
|
+
if (idFixesForFile.length > 0) {
|
|
282
|
+
code = fixIdsInSourceFile(code, file, idFixesForFile);
|
|
283
|
+
}
|
|
284
|
+
if (numericForFile.length > 0) {
|
|
285
|
+
code = fixNumericLeadingInSourceFile(code, file, numericForFile);
|
|
286
|
+
}
|
|
287
|
+
fs.writeFileSync(file, code, 'utf8');
|
|
288
|
+
console.log(` ✓ Updated ${file}`);
|
|
189
289
|
}
|
|
190
290
|
}
|
|
291
|
+
// Re-parse after fixes
|
|
292
|
+
filePages.length = 0;
|
|
293
|
+
for (const file of files) {
|
|
294
|
+
const code = fs.readFileSync(file, 'utf8');
|
|
295
|
+
const pageKey = derivePageKey(file, opts.pageKeyFrom);
|
|
296
|
+
const page = parsePageFromFile({ code, filePath: file, pageKey });
|
|
297
|
+
filePages.push({ file, pageKey, page });
|
|
298
|
+
}
|
|
191
299
|
}
|
|
192
|
-
// Second pass: re-parse all files (now with fixed IDs) and compile
|
|
193
|
-
// If no duplicates were found, reuse the already-parsed pages
|
|
194
300
|
const pages = [];
|
|
195
301
|
const seenIds = new Map();
|
|
196
|
-
// Map of pageKey -> PageJson for applying metadata later
|
|
197
302
|
const pageMap = new Map();
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
for (const
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
if (c.type === 'OptionSelector' && Array.isArray(c.buttons)) {
|
|
207
|
-
for (const b of c.buttons) {
|
|
208
|
-
if (b?.id)
|
|
209
|
-
checkUniqueId(seenIds, b.id, { file, pageKey });
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
pages.push(page);
|
|
214
|
-
pageMap.set(pageKey, page);
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
else {
|
|
218
|
-
// No duplicates found, reuse already-parsed pages
|
|
219
|
-
for (const filePage of filePages) {
|
|
220
|
-
const { page, pageKey, file } = filePage;
|
|
221
|
-
// Global ID uniqueness: components + option ids
|
|
222
|
-
for (const c of page.components) {
|
|
223
|
-
checkUniqueId(seenIds, c.id, { file, pageKey });
|
|
224
|
-
if (c.type === 'OptionSelector' && Array.isArray(c.buttons)) {
|
|
225
|
-
for (const b of c.buttons) {
|
|
226
|
-
if (b?.id)
|
|
227
|
-
checkUniqueId(seenIds, b.id, { file, pageKey });
|
|
228
|
-
}
|
|
303
|
+
for (const filePage of filePages) {
|
|
304
|
+
const { page, pageKey, file } = filePage;
|
|
305
|
+
for (const c of page.components) {
|
|
306
|
+
checkUniqueId(seenIds, c.id, { file, pageKey });
|
|
307
|
+
if (c.type === 'OptionSelector' && Array.isArray(c.buttons)) {
|
|
308
|
+
for (const b of c.buttons) {
|
|
309
|
+
if (b?.id)
|
|
310
|
+
checkUniqueId(seenIds, b.id, { file, pageKey });
|
|
229
311
|
}
|
|
230
312
|
}
|
|
231
|
-
pages.push(page);
|
|
232
|
-
pageMap.set(pageKey, page);
|
|
233
313
|
}
|
|
314
|
+
pages.push(page);
|
|
315
|
+
pageMap.set(pageKey, page);
|
|
234
316
|
}
|
|
235
317
|
// Read config.json if it exists
|
|
236
318
|
let config = null;
|
|
@@ -559,6 +641,172 @@ function fixIdsInSourceFile(code, filePath, fixes) {
|
|
|
559
641
|
const result = generate(ast, {}, code);
|
|
560
642
|
return result.code;
|
|
561
643
|
}
|
|
644
|
+
function fixNumericLeadingInSourceFile(code, filePath, fixes) {
|
|
645
|
+
let ast;
|
|
646
|
+
try {
|
|
647
|
+
ast = parse(code, {
|
|
648
|
+
sourceType: 'module',
|
|
649
|
+
plugins: ['typescript', 'jsx'],
|
|
650
|
+
sourceFilename: filePath,
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
catch (error) {
|
|
654
|
+
throw new CompileError(`Failed to parse file for key/id fixing: ${error.message}`, {
|
|
655
|
+
file: filePath,
|
|
656
|
+
});
|
|
657
|
+
}
|
|
658
|
+
const componentKeyMap = new Map();
|
|
659
|
+
const componentIdMap = new Map();
|
|
660
|
+
const buttonKeyMap = new Map();
|
|
661
|
+
const buttonIdMap = new Map();
|
|
662
|
+
for (const f of fixes) {
|
|
663
|
+
if (f.buttonIndex !== undefined) {
|
|
664
|
+
const k = `${f.componentIndex}:${f.buttonIndex}`;
|
|
665
|
+
if (f.kind === 'buttonKey')
|
|
666
|
+
buttonKeyMap.set(k, f.newValue);
|
|
667
|
+
else if (f.kind === 'buttonId')
|
|
668
|
+
buttonIdMap.set(k, f.newValue);
|
|
669
|
+
}
|
|
670
|
+
else {
|
|
671
|
+
if (f.kind === 'componentKey')
|
|
672
|
+
componentKeyMap.set(f.componentIndex, f.newValue);
|
|
673
|
+
else if (f.kind === 'componentId')
|
|
674
|
+
componentIdMap.set(f.componentIndex, f.newValue);
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
// Map from buttons variable name (e.g. "genderButtons") -> OptionSelector key (e.g. "gender") for fixing const arrays
|
|
678
|
+
const buttonsConstNameToPrefix = new Map();
|
|
679
|
+
// Collect (start, end, newText) replacements so we can patch the source without reformatting (preserves line breaks, etc.)
|
|
680
|
+
const replacements = [];
|
|
681
|
+
const pushReplacement = (node, newValue) => {
|
|
682
|
+
if (node.start == null || node.end == null)
|
|
683
|
+
return;
|
|
684
|
+
replacements.push({ start: node.start, end: node.end, newText: JSON.stringify(newValue) });
|
|
685
|
+
};
|
|
686
|
+
let componentIndex = -1;
|
|
687
|
+
traverse(ast, {
|
|
688
|
+
JSXElement(path) {
|
|
689
|
+
const opening = path.node.openingElement;
|
|
690
|
+
const tagName = opening.name.type === 'JSXIdentifier' ? opening.name.name : null;
|
|
691
|
+
if (!tagName || !ALLOWED_PRIMITIVES.has(tagName))
|
|
692
|
+
return;
|
|
693
|
+
const hasIdAttr = opening.attributes.some((a) => a.type === 'JSXAttribute' && a.name.type === 'JSXIdentifier' && a.name.name === 'id');
|
|
694
|
+
const hasKeyAttr = opening.attributes.some((a) => a.type === 'JSXAttribute' && a.name.type === 'JSXIdentifier' && a.name.name === 'key');
|
|
695
|
+
if (hasIdAttr || hasKeyAttr) {
|
|
696
|
+
componentIndex++;
|
|
697
|
+
}
|
|
698
|
+
const recordAttrReplacement = (name, newValue) => {
|
|
699
|
+
for (const attr of opening.attributes) {
|
|
700
|
+
if (attr.type === 'JSXAttribute' &&
|
|
701
|
+
attr.name.type === 'JSXIdentifier' &&
|
|
702
|
+
attr.name.name === name) {
|
|
703
|
+
const valueNode = attr.value?.type === 'JSXExpressionContainer'
|
|
704
|
+
? attr.value.expression
|
|
705
|
+
: attr.value;
|
|
706
|
+
if (valueNode?.type === 'StringLiteral') {
|
|
707
|
+
pushReplacement(valueNode, newValue);
|
|
708
|
+
}
|
|
709
|
+
break;
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
};
|
|
713
|
+
if (componentKeyMap.has(componentIndex)) {
|
|
714
|
+
recordAttrReplacement('key', componentKeyMap.get(componentIndex));
|
|
715
|
+
}
|
|
716
|
+
if (componentIdMap.has(componentIndex)) {
|
|
717
|
+
recordAttrReplacement('id', componentIdMap.get(componentIndex));
|
|
718
|
+
}
|
|
719
|
+
if (tagName === 'OptionSelector') {
|
|
720
|
+
let componentKey;
|
|
721
|
+
for (const attr of opening.attributes) {
|
|
722
|
+
if (attr.type === 'JSXAttribute' &&
|
|
723
|
+
attr.name.type === 'JSXIdentifier' &&
|
|
724
|
+
attr.name.name === 'key' &&
|
|
725
|
+
attr.value?.type === 'StringLiteral') {
|
|
726
|
+
componentKey = attr.value.value;
|
|
727
|
+
break;
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
const prefix = componentKey ?? 'option';
|
|
731
|
+
for (const attr of opening.attributes) {
|
|
732
|
+
if (attr.type === 'JSXAttribute' &&
|
|
733
|
+
attr.name.type === 'JSXIdentifier' &&
|
|
734
|
+
attr.name.name === 'buttons' &&
|
|
735
|
+
attr.value?.type === 'JSXExpressionContainer') {
|
|
736
|
+
const expr = attr.value.expression;
|
|
737
|
+
if (expr.type === 'ArrayExpression') {
|
|
738
|
+
let buttonIndex = 0;
|
|
739
|
+
for (const element of expr.elements) {
|
|
740
|
+
if (element && element.type === 'ObjectExpression') {
|
|
741
|
+
const key = `${componentIndex}:${buttonIndex}`;
|
|
742
|
+
for (const prop of element.properties) {
|
|
743
|
+
if (prop.type === 'ObjectProperty' && prop.key.type === 'Identifier') {
|
|
744
|
+
if (prop.key.name === 'key' && buttonKeyMap.has(key)) {
|
|
745
|
+
pushReplacement(prop.value, buttonKeyMap.get(key));
|
|
746
|
+
}
|
|
747
|
+
else if (prop.key.name === 'id' && buttonIdMap.has(key)) {
|
|
748
|
+
pushReplacement(prop.value, buttonIdMap.get(key));
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
buttonIndex++;
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
else if (expr.type === 'Identifier') {
|
|
757
|
+
if (!buttonsConstNameToPrefix.has(expr.name)) {
|
|
758
|
+
buttonsConstNameToPrefix.set(expr.name, prefix);
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
break;
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
},
|
|
766
|
+
});
|
|
767
|
+
// Fix button key/id in const arrays (e.g. const genderButtons = [{ id: "1_...", key: "..." }])
|
|
768
|
+
traverse(ast, {
|
|
769
|
+
VariableDeclarator(path) {
|
|
770
|
+
const id = path.node.id;
|
|
771
|
+
const init = path.node.init;
|
|
772
|
+
if (id.type !== 'Identifier' || init?.type !== 'ArrayExpression')
|
|
773
|
+
return;
|
|
774
|
+
const constName = id.name;
|
|
775
|
+
const prefix = buttonsConstNameToPrefix.get(constName) ?? 'option';
|
|
776
|
+
for (const element of init.elements) {
|
|
777
|
+
if (!element || element.type !== 'ObjectExpression')
|
|
778
|
+
continue;
|
|
779
|
+
for (const prop of element.properties) {
|
|
780
|
+
if (prop.type !== 'ObjectProperty' || prop.key.type !== 'Identifier')
|
|
781
|
+
continue;
|
|
782
|
+
if (prop.key.name !== 'key' && prop.key.name !== 'id')
|
|
783
|
+
continue;
|
|
784
|
+
const val = prop.value;
|
|
785
|
+
let current = null;
|
|
786
|
+
if (val.type === 'StringLiteral') {
|
|
787
|
+
current = val.value;
|
|
788
|
+
}
|
|
789
|
+
else if (val.type === 'TemplateLiteral' && val.quasis.length === 1 && !val.quasis[0].value.raw.includes('$')) {
|
|
790
|
+
current = val.quasis[0].value.raw;
|
|
791
|
+
}
|
|
792
|
+
if (current === null || !keyOrIdStartsWithDigit(current))
|
|
793
|
+
continue;
|
|
794
|
+
const newValue = normalizeKeyIfStartsWithDigit(current, prefix);
|
|
795
|
+
if (newValue === null)
|
|
796
|
+
continue;
|
|
797
|
+
pushReplacement(val, newValue);
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
},
|
|
801
|
+
});
|
|
802
|
+
// Apply replacements from end to start so indices stay valid; preserves all other formatting
|
|
803
|
+
replacements.sort((a, b) => b.start - a.start);
|
|
804
|
+
let result = code;
|
|
805
|
+
for (const { start, end, newText } of replacements) {
|
|
806
|
+
result = result.slice(0, start) + newText + result.slice(end);
|
|
807
|
+
}
|
|
808
|
+
return result;
|
|
809
|
+
}
|
|
562
810
|
function checkUniqueId(seen, id, loc) {
|
|
563
811
|
const prev = seen.get(id);
|
|
564
812
|
if (prev) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"parsePage.d.ts","sourceRoot":"","sources":["../../src/compiler/parsePage.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAA;AAezD,wBAAgB,iBAAiB,CAAC,IAAI,EAAE;IACtC,IAAI,EAAE,MAAM,CAAA;IACZ,QAAQ,EAAE,MAAM,CAAA;IAChB,OAAO,EAAE,MAAM,CAAA;CAChB,GAAG,QAAQ,
|
|
1
|
+
{"version":3,"file":"parsePage.d.ts","sourceRoot":"","sources":["../../src/compiler/parsePage.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAA;AAezD,wBAAgB,iBAAiB,CAAC,IAAI,EAAE;IACtC,IAAI,EAAE,MAAM,CAAA;IACZ,QAAQ,EAAE,MAAM,CAAA;IAChB,OAAO,EAAE,MAAM,CAAA;CAChB,GAAG,QAAQ,CAuHX;AAED;;;GAGG;AACH,wBAAgB,6BAA6B,CAAC,IAAI,EAAE;IAClD,IAAI,EAAE,MAAM,CAAA;IACZ,QAAQ,EAAE,MAAM,CAAA;CACjB,GAAG,aAAa,EAAE,CAqGlB"}
|