@embeddables/cli 0.5.1 → 0.6.1
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 +3 -1
- package/dist/commands/branch.d.ts.map +1 -1
- package/dist/commands/branch.js +5 -1
- package/dist/commands/build.d.ts +2 -1
- package/dist/commands/build.d.ts.map +1 -1
- package/dist/commands/build.js +12 -1
- 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 +4 -2
- package/dist/commands/pull.d.ts.map +1 -1
- package/dist/commands/pull.js +116 -29
- package/dist/commands/save.d.ts.map +1 -1
- package/dist/commands/save.js +121 -9
- 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 +309 -57
- package/dist/compiler/reverse.d.ts +5 -0
- package/dist/compiler/reverse.d.ts.map +1 -1
- package/dist/compiler/reverse.js +129 -2
- package/dist/types-builder.d.ts +1 -1
- 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
|
@@ -30,9 +30,10 @@ program
|
|
|
30
30
|
});
|
|
31
31
|
program
|
|
32
32
|
.command('build')
|
|
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')
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"branch.d.ts","sourceRoot":"","sources":["../../src/commands/branch.ts"],"names":[],"mappings":"AAKA,wBAAsB,SAAS,CAAC,IAAI,EAAE;IAAE,EAAE,CAAC,EAAE,MAAM,CAAA;CAAE,
|
|
1
|
+
{"version":3,"file":"branch.d.ts","sourceRoot":"","sources":["../../src/commands/branch.ts"],"names":[],"mappings":"AAKA,wBAAsB,SAAS,CAAC,IAAI,EAAE;IAAE,EAAE,CAAC,EAAE,MAAM,CAAA;CAAE,iBAoDpD"}
|
package/dist/commands/branch.js
CHANGED
|
@@ -41,6 +41,10 @@ export async function runBranch(opts) {
|
|
|
41
41
|
else {
|
|
42
42
|
console.log(pc.cyan(`Switching to branch: ${selectedBranch.name}...`));
|
|
43
43
|
console.log('');
|
|
44
|
-
await runPull({
|
|
44
|
+
await runPull({
|
|
45
|
+
id: embeddableId,
|
|
46
|
+
branch: selectedBranch.id,
|
|
47
|
+
branchName: selectedBranch.name,
|
|
48
|
+
});
|
|
45
49
|
}
|
|
46
50
|
}
|
package/dist/commands/build.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../../src/commands/build.ts"],"names":[],"mappings":"
|
|
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
|
@@ -1,8 +1,18 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { compileAllPages } from '../compiler/index.js';
|
|
3
3
|
import { formatError } from '../compiler/errors.js';
|
|
4
|
+
import { promptForLocalEmbeddable } from '../prompts/index.js';
|
|
4
5
|
export async function runBuild(opts) {
|
|
5
|
-
|
|
6
|
+
let embeddableId = opts.id;
|
|
7
|
+
if (!embeddableId) {
|
|
8
|
+
const selected = await promptForLocalEmbeddable({
|
|
9
|
+
message: 'Select an embeddable to build:',
|
|
10
|
+
});
|
|
11
|
+
if (!selected) {
|
|
12
|
+
process.exit(1);
|
|
13
|
+
}
|
|
14
|
+
embeddableId = selected;
|
|
15
|
+
}
|
|
6
16
|
const pagesGlob = opts.pages || `embeddables/${embeddableId}/pages/**/*.page.tsx`;
|
|
7
17
|
const outPath = opts.out || path.join('embeddables', embeddableId, '.generated', 'embeddable.json');
|
|
8
18
|
const stylesDir = path.join('embeddables', embeddableId, 'styles');
|
|
@@ -13,6 +23,7 @@ export async function runBuild(opts) {
|
|
|
13
23
|
pageKeyFrom: opts.pageKeyFrom,
|
|
14
24
|
stylesDir,
|
|
15
25
|
embeddableId,
|
|
26
|
+
fixLint: opts.fix ? true : 'prompt',
|
|
16
27
|
});
|
|
17
28
|
}
|
|
18
29
|
catch (e) {
|
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
|
}
|
package/dist/commands/pull.d.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
export
|
|
1
|
+
export type RunPullOptions = {
|
|
2
2
|
id?: string;
|
|
3
3
|
out?: string;
|
|
4
4
|
branch?: string;
|
|
5
|
+
branchName?: string;
|
|
5
6
|
fix?: boolean;
|
|
6
|
-
}
|
|
7
|
+
};
|
|
8
|
+
export declare function runPull(opts: RunPullOptions): Promise<void>;
|
|
7
9
|
//# sourceMappingURL=pull.d.ts.map
|
|
@@ -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":"AAyEA,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,iBAkPjD"}
|
package/dist/commands/pull.js
CHANGED
|
@@ -6,6 +6,69 @@ 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
|
+
/** Slug for branch name/id for use in filenames (e.g. "my branch" -> "my_branch"). */
|
|
10
|
+
function slugForBranch(nameOrId) {
|
|
11
|
+
return String(nameOrId).replace(/[^a-zA-Z0-9_.-]/g, '_').replace(/_+/g, '_') || 'main';
|
|
12
|
+
}
|
|
13
|
+
/** Versioned embeddable filename: embeddable-{branchSlug}@{version}.json */
|
|
14
|
+
function getVersionedBasename(version, branchSlug) {
|
|
15
|
+
const v = typeof version === 'number' ? version : String(version);
|
|
16
|
+
return `embeddable-${branchSlug}@${v}.json`;
|
|
17
|
+
}
|
|
18
|
+
/** Read current _branch_id and _branch_name from config (set when on a branch via `embeddables branch`). */
|
|
19
|
+
function getCurrentBranchFromConfig(embeddableId) {
|
|
20
|
+
const configPath = path.join('embeddables', embeddableId, 'config.json');
|
|
21
|
+
if (!fs.existsSync(configPath))
|
|
22
|
+
return null;
|
|
23
|
+
try {
|
|
24
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
25
|
+
const branchId = config._branch_id;
|
|
26
|
+
if (typeof branchId !== 'string' || !branchId)
|
|
27
|
+
return null;
|
|
28
|
+
const branchName = config._branch_name;
|
|
29
|
+
return { branchId, branchName: typeof branchName === 'string' ? branchName : undefined };
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/** Write _version, _branch_id, _branch_name to config.json. Called early and in finally so it always runs. */
|
|
36
|
+
function writePullMetadataToConfig(embeddableId, version, branch, branchName) {
|
|
37
|
+
const configPath = path.join('embeddables', embeddableId, 'config.json');
|
|
38
|
+
let config = {};
|
|
39
|
+
try {
|
|
40
|
+
if (fs.existsSync(configPath)) {
|
|
41
|
+
try {
|
|
42
|
+
const content = fs.readFileSync(configPath, 'utf8');
|
|
43
|
+
config = JSON.parse(content);
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
/* use empty config */
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (version != null) {
|
|
50
|
+
const n = typeof version === 'number' ? version : parseInt(String(version), 10);
|
|
51
|
+
if (!isNaN(n))
|
|
52
|
+
config._version = n;
|
|
53
|
+
}
|
|
54
|
+
if (branch) {
|
|
55
|
+
config._branch_id = branch;
|
|
56
|
+
if (branchName)
|
|
57
|
+
config._branch_name = branchName;
|
|
58
|
+
else
|
|
59
|
+
delete config._branch_name;
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
delete config._branch_id;
|
|
63
|
+
delete config._branch_name;
|
|
64
|
+
}
|
|
65
|
+
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
66
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n', 'utf8');
|
|
67
|
+
}
|
|
68
|
+
catch (err) {
|
|
69
|
+
console.warn(pc.yellow(`Could not write config.json: ${err instanceof Error ? err.message : err}`));
|
|
70
|
+
}
|
|
71
|
+
}
|
|
9
72
|
export async function runPull(opts) {
|
|
10
73
|
let embeddableId = opts.id;
|
|
11
74
|
// If no ID provided, try to get it interactively
|
|
@@ -34,7 +97,7 @@ export async function runPull(opts) {
|
|
|
34
97
|
project_id: projectId,
|
|
35
98
|
project_name: selectedProject.title || undefined,
|
|
36
99
|
});
|
|
37
|
-
console.log(pc.green(`✓
|
|
100
|
+
console.log(pc.green(`✓ Wrote project config to embeddables.json`));
|
|
38
101
|
console.log('');
|
|
39
102
|
}
|
|
40
103
|
console.log(pc.cyan('Fetching embeddables from project...'));
|
|
@@ -47,12 +110,23 @@ export async function runPull(opts) {
|
|
|
47
110
|
embeddableId = selected;
|
|
48
111
|
console.log('');
|
|
49
112
|
}
|
|
113
|
+
// Stay on current branch when no --branch: use config's _branch_id if set
|
|
114
|
+
const currentFromConfig = opts.branch == null ? getCurrentBranchFromConfig(embeddableId) : null;
|
|
115
|
+
const effectiveBranch = opts.branch ?? currentFromConfig?.branchId;
|
|
116
|
+
const effectiveBranchName = opts.branchName ?? currentFromConfig?.branchName;
|
|
50
117
|
let url = `https://engine.embeddables.com/${embeddableId}?version=latest`;
|
|
51
|
-
if (
|
|
52
|
-
url += `&embeddable_branch=${
|
|
118
|
+
if (effectiveBranch) {
|
|
119
|
+
url += `&embeddable_branch=${effectiveBranch}`;
|
|
53
120
|
}
|
|
54
121
|
const outPath = opts.out || path.join('embeddables', embeddableId, '.generated', 'embeddable.json');
|
|
122
|
+
const branchLabel = effectiveBranch
|
|
123
|
+
? effectiveBranchName
|
|
124
|
+
? `${effectiveBranchName} (${effectiveBranch})`
|
|
125
|
+
: effectiveBranch
|
|
126
|
+
: 'main';
|
|
127
|
+
console.log(pc.cyan(`Pulling branch: ${pc.bold(branchLabel)}`));
|
|
55
128
|
console.log(`Fetching embeddable from ${url}...`);
|
|
129
|
+
let pullVersion;
|
|
56
130
|
try {
|
|
57
131
|
// Add authentication header if available
|
|
58
132
|
const accessToken = getAccessToken();
|
|
@@ -75,24 +149,28 @@ export async function runPull(opts) {
|
|
|
75
149
|
fs.mkdirSync(path.dirname(outPath), { recursive: true });
|
|
76
150
|
const flowJson = JSON.stringify(flow, null, 2);
|
|
77
151
|
fs.writeFileSync(outPath, flowJson, 'utf8');
|
|
78
|
-
console.log(pc.cyan(`✓
|
|
79
|
-
// Also save version-specific file (e.g. embeddable-
|
|
80
|
-
|
|
152
|
+
console.log(pc.cyan(`✓ Wrote embeddable JSON to ${outPath}`));
|
|
153
|
+
// Also save version-specific file (e.g. embeddable-main@135.json or embeddable-my_branch@135.json)
|
|
154
|
+
pullVersion =
|
|
155
|
+
data.version ?? data.embeddable_version ?? flow.version;
|
|
156
|
+
const version = pullVersion;
|
|
157
|
+
const branchSlug = slugForBranch(effectiveBranchName ?? effectiveBranch ?? 'main');
|
|
81
158
|
if (version != null) {
|
|
82
159
|
const versionStr = typeof version === 'string' ? version : String(version);
|
|
83
|
-
const
|
|
84
|
-
const versionedBasename = `embeddable-${versionLabel}.json`;
|
|
160
|
+
const versionedBasename = getVersionedBasename(versionStr, branchSlug);
|
|
85
161
|
const versionedPath = path.join(path.dirname(outPath), versionedBasename);
|
|
86
162
|
fs.writeFileSync(versionedPath, flowJson, 'utf8');
|
|
87
|
-
console.log(pc.cyan(`✓
|
|
163
|
+
console.log(pc.cyan(`✓ Wrote versioned embeddable JSON to ${versionedPath}`));
|
|
88
164
|
}
|
|
165
|
+
// Persist _version and _branch_id in config.json immediately so they survive reverseCompile
|
|
166
|
+
writePullMetadataToConfig(embeddableId, version, effectiveBranch, effectiveBranchName);
|
|
89
167
|
// Fetch and save flow metadata from DB (title, archived, created_at)
|
|
90
168
|
const metadata = await fetchEmbeddableMetadata(embeddableId);
|
|
91
169
|
if (metadata) {
|
|
92
170
|
const metadataPath = path.join('embeddables', embeddableId, 'metadata.json');
|
|
93
171
|
fs.mkdirSync(path.dirname(metadataPath), { recursive: true });
|
|
94
172
|
fs.writeFileSync(metadataPath, JSON.stringify(metadata, null, 2) + '\n', 'utf8');
|
|
95
|
-
console.log(pc.cyan(`✓
|
|
173
|
+
console.log(pc.cyan(`✓ Wrote flow metadata to ${metadataPath}`));
|
|
96
174
|
}
|
|
97
175
|
// Clear existing pages, styles, computed-fields, actions, and global-components before generating new ones
|
|
98
176
|
const pagesDir = path.join('embeddables', embeddableId, 'pages');
|
|
@@ -137,9 +215,16 @@ export async function runPull(opts) {
|
|
|
137
215
|
}
|
|
138
216
|
console.log(`${pc.gray(`Cleared ${existingGlobalComponents.length} existing global component(s)`)}`);
|
|
139
217
|
}
|
|
140
|
-
// Run reverse compiler
|
|
218
|
+
// Run reverse compiler (pass pullMetadata so config.json gets _version and _branch_id even on fix retry)
|
|
219
|
+
const versionNum = version != null && !isNaN(Number(version)) ? Number(version) : undefined;
|
|
220
|
+
const pullMetadata = {
|
|
221
|
+
version: versionNum,
|
|
222
|
+
branchId: effectiveBranch,
|
|
223
|
+
branchName: effectiveBranchName,
|
|
224
|
+
};
|
|
225
|
+
let usedFix = opts.fix;
|
|
141
226
|
try {
|
|
142
|
-
await reverseCompile(flow, embeddableId, { fix: opts.fix });
|
|
227
|
+
await reverseCompile(flow, embeddableId, { fix: opts.fix, pullMetadata });
|
|
143
228
|
}
|
|
144
229
|
catch (compileError) {
|
|
145
230
|
// If fix mode wasn't already enabled, offer to retry with fix mode
|
|
@@ -159,10 +244,11 @@ export async function runPull(opts) {
|
|
|
159
244
|
},
|
|
160
245
|
});
|
|
161
246
|
if (response.fix) {
|
|
247
|
+
usedFix = true;
|
|
162
248
|
console.log('');
|
|
163
249
|
console.log(pc.cyan('Retrying with auto-fix enabled...'));
|
|
164
250
|
console.log('');
|
|
165
|
-
await reverseCompile(flow, embeddableId, { fix: true });
|
|
251
|
+
await reverseCompile(flow, embeddableId, { fix: true, pullMetadata });
|
|
166
252
|
}
|
|
167
253
|
else {
|
|
168
254
|
process.exit(1);
|
|
@@ -172,22 +258,17 @@ export async function runPull(opts) {
|
|
|
172
258
|
throw compileError;
|
|
173
259
|
}
|
|
174
260
|
}
|
|
175
|
-
//
|
|
176
|
-
if (
|
|
177
|
-
const
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
}
|
|
187
|
-
catch {
|
|
188
|
-
// Ignore errors updating config.json - versioned files are a fallback
|
|
189
|
-
}
|
|
190
|
-
}
|
|
261
|
+
// Persist mutated flow when fixes were applied (e.g. parent_key -> parent_id)
|
|
262
|
+
if (usedFix) {
|
|
263
|
+
const flowJson = JSON.stringify(flow, null, 2);
|
|
264
|
+
fs.writeFileSync(outPath, flowJson, 'utf8');
|
|
265
|
+
console.log(pc.cyan(`✓ Wrote embeddable JSON to ${outPath} (with fixes applied)`));
|
|
266
|
+
if (version != null) {
|
|
267
|
+
const versionStr = typeof version === 'string' ? version : String(version);
|
|
268
|
+
const versionedBasename = getVersionedBasename(versionStr, branchSlug);
|
|
269
|
+
const versionedPath = path.join(path.dirname(outPath), versionedBasename);
|
|
270
|
+
fs.writeFileSync(versionedPath, flowJson, 'utf8');
|
|
271
|
+
console.log(pc.cyan(`✓ Wrote versioned embeddable JSON to ${versionedPath} (with fixes applied)`));
|
|
191
272
|
}
|
|
192
273
|
}
|
|
193
274
|
}
|
|
@@ -195,4 +276,10 @@ export async function runPull(opts) {
|
|
|
195
276
|
console.error('Error pulling embeddable:', error);
|
|
196
277
|
process.exit(1);
|
|
197
278
|
}
|
|
279
|
+
finally {
|
|
280
|
+
// Always persist _version and _branch_id once we've fetched (so config is correct even if reverseCompile failed or was retried with fix)
|
|
281
|
+
if (embeddableId != null && pullVersion !== undefined) {
|
|
282
|
+
writePullMetadataToConfig(embeddableId, pullVersion, effectiveBranch, effectiveBranchName);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
198
285
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"save.d.ts","sourceRoot":"","sources":["../../src/commands/save.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"save.d.ts","sourceRoot":"","sources":["../../src/commands/save.ts"],"names":[],"mappings":"AA+OA,wBAAsB,OAAO,CAAC,IAAI,EAAE;IAClC,EAAE,CAAC,EAAE,MAAM,CAAA;IACX,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB,iBAoBA"}
|
package/dist/commands/save.js
CHANGED
|
@@ -2,7 +2,7 @@ import fs from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import pc from 'picocolors';
|
|
4
4
|
import prompts from 'prompts';
|
|
5
|
-
import { getAccessToken, isLoggedIn } from '../auth/index.js';
|
|
5
|
+
import { getAccessToken, getAuthenticatedSupabaseClient, isLoggedIn } from '../auth/index.js';
|
|
6
6
|
import { getProjectId, writeProjectConfig } from '../config/index.js';
|
|
7
7
|
import { compileAllPages } from '../compiler/index.js';
|
|
8
8
|
import { formatError } from '../compiler/errors.js';
|
|
@@ -69,6 +69,45 @@ function getVersionFromConfig(embeddableId) {
|
|
|
69
69
|
}
|
|
70
70
|
return null;
|
|
71
71
|
}
|
|
72
|
+
/**
|
|
73
|
+
* Read `_branch_id` from config.json for the given embeddable (set when on a branch via `embeddables branch`).
|
|
74
|
+
*/
|
|
75
|
+
function getBranchFromConfig(embeddableId) {
|
|
76
|
+
const configPath = path.join('embeddables', embeddableId, 'config.json');
|
|
77
|
+
if (!fs.existsSync(configPath)) {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
try {
|
|
81
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
82
|
+
if (typeof config._branch_id === 'string' && config._branch_id) {
|
|
83
|
+
return config._branch_id;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
// Ignore parse errors
|
|
88
|
+
}
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
/** Slug for branch name/id for versioned filenames (e.g. "my branch" -> "my_branch"). */
|
|
92
|
+
function slugForBranch(nameOrId) {
|
|
93
|
+
return String(nameOrId).replace(/[^a-zA-Z0-9_.-]/g, '_').replace(/_+/g, '_') || 'main';
|
|
94
|
+
}
|
|
95
|
+
/** Get branch slug from config (_branch_name preferred, else _branch_id, else main). */
|
|
96
|
+
function getBranchSlugFromConfig(embeddableId) {
|
|
97
|
+
const configPath = path.join('embeddables', embeddableId, 'config.json');
|
|
98
|
+
if (!fs.existsSync(configPath))
|
|
99
|
+
return 'main';
|
|
100
|
+
try {
|
|
101
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
102
|
+
const name = config._branch_name ?? config._branch_id;
|
|
103
|
+
if (typeof name === 'string' && name)
|
|
104
|
+
return slugForBranch(name);
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
/* ignore */
|
|
108
|
+
}
|
|
109
|
+
return 'main';
|
|
110
|
+
}
|
|
72
111
|
/**
|
|
73
112
|
* Update `_version` in config.json for the given embeddable.
|
|
74
113
|
*/
|
|
@@ -87,7 +126,7 @@ function setVersionInConfig(embeddableId, version) {
|
|
|
87
126
|
}
|
|
88
127
|
}
|
|
89
128
|
/**
|
|
90
|
-
* Scan the .generated/ directory for versioned files (embeddable-v*.json)
|
|
129
|
+
* Scan the .generated/ directory for versioned files (embeddable-v*.json or embeddable-*@*.json)
|
|
91
130
|
* and return the highest version number found.
|
|
92
131
|
*/
|
|
93
132
|
function getLatestVersionFromFiles(generatedDir) {
|
|
@@ -95,10 +134,11 @@ function getLatestVersionFromFiles(generatedDir) {
|
|
|
95
134
|
return null;
|
|
96
135
|
}
|
|
97
136
|
const files = fs.readdirSync(generatedDir);
|
|
98
|
-
const
|
|
137
|
+
const legacyPattern = /^embeddable-v(\d+)\.json$/;
|
|
138
|
+
const branchPattern = /^embeddable-[^@]+@(\d+)\.json$/;
|
|
99
139
|
let maxVersion = null;
|
|
100
140
|
for (const file of files) {
|
|
101
|
-
const match = file.match(
|
|
141
|
+
const match = file.match(legacyPattern) ?? file.match(branchPattern);
|
|
102
142
|
if (match) {
|
|
103
143
|
const version = parseInt(match[1], 10);
|
|
104
144
|
if (maxVersion === null || version > maxVersion) {
|
|
@@ -108,6 +148,41 @@ function getLatestVersionFromFiles(generatedDir) {
|
|
|
108
148
|
}
|
|
109
149
|
return maxVersion;
|
|
110
150
|
}
|
|
151
|
+
/**
|
|
152
|
+
* Fetch active drafts from other users on the same version (status=DRAFT, not saved/discarded).
|
|
153
|
+
* Used to warn before saving that others may have unsaved edits.
|
|
154
|
+
*/
|
|
155
|
+
async function fetchOtherUsersDrafts(supabase, flowId, versionNumber, branchId, currentUserId) {
|
|
156
|
+
try {
|
|
157
|
+
let query = supabase
|
|
158
|
+
.from('flow_versions')
|
|
159
|
+
.select('id, author_id, author_name')
|
|
160
|
+
.eq('flow_id', flowId)
|
|
161
|
+
.eq('status', 'DRAFT')
|
|
162
|
+
.eq('version_number', versionNumber)
|
|
163
|
+
.not('draft_saved', 'is', true)
|
|
164
|
+
.not('draft_discarded', 'is', true)
|
|
165
|
+
.neq('author_id', currentUserId);
|
|
166
|
+
if (branchId === null) {
|
|
167
|
+
query = query.is('branch_id', null);
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
query = query.eq('branch_id', branchId);
|
|
171
|
+
}
|
|
172
|
+
const { data, error } = await query;
|
|
173
|
+
if (error) {
|
|
174
|
+
return [];
|
|
175
|
+
}
|
|
176
|
+
return (data || []).map((row) => ({
|
|
177
|
+
id: row.id,
|
|
178
|
+
author_id: row.author_id,
|
|
179
|
+
author_name: row.author_name ?? null,
|
|
180
|
+
}));
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
return [];
|
|
184
|
+
}
|
|
185
|
+
}
|
|
111
186
|
export async function runSave(opts) {
|
|
112
187
|
try {
|
|
113
188
|
await runSaveInner(opts);
|
|
@@ -158,6 +233,8 @@ async function runSaveInner(opts) {
|
|
|
158
233
|
embeddableId = selected;
|
|
159
234
|
console.log('');
|
|
160
235
|
}
|
|
236
|
+
// Resolve branch: explicit -b flag wins, otherwise use current branch from config (set by `embeddables branch`)
|
|
237
|
+
const effectiveBranch = opts.branch ?? getBranchFromConfig(embeddableId) ?? undefined;
|
|
161
238
|
// 4. Get project ID (from config or interactive prompt)
|
|
162
239
|
let projectId = getProjectId();
|
|
163
240
|
if (!projectId) {
|
|
@@ -242,6 +319,39 @@ async function runSaveInner(opts) {
|
|
|
242
319
|
}
|
|
243
320
|
fromVersionNumber = detectedVersion;
|
|
244
321
|
}
|
|
322
|
+
// 7b. Check for other users' drafts on this version; warn and optionally abort
|
|
323
|
+
const supabase = await getAuthenticatedSupabaseClient();
|
|
324
|
+
if (supabase) {
|
|
325
|
+
const { data: { user }, } = await supabase.auth.getUser();
|
|
326
|
+
const currentUserId = user?.id;
|
|
327
|
+
if (currentUserId) {
|
|
328
|
+
const branchIdForDrafts = effectiveBranch ?? null;
|
|
329
|
+
const otherDrafts = await fetchOtherUsersDrafts(supabase, embeddableId, fromVersionNumber, branchIdForDrafts, currentUserId);
|
|
330
|
+
if (otherDrafts.length > 0) {
|
|
331
|
+
const names = otherDrafts
|
|
332
|
+
.map((d) => d.author_name?.trim() || d.author_id || 'Someone')
|
|
333
|
+
.filter((n, i, a) => a.indexOf(n) === i);
|
|
334
|
+
const namesText = names.length === 1 ? names[0] : `${names.slice(0, -1).join(', ')} and ${names[names.length - 1]}`;
|
|
335
|
+
console.log('');
|
|
336
|
+
console.warn(pc.yellow(`⚠ ${namesText} ${names.length === 1 ? 'has' : 'have'} unsaved edits on version ${fromVersionNumber}. Saving may cause conflicts.`));
|
|
337
|
+
const { proceed } = await prompts({
|
|
338
|
+
type: 'confirm',
|
|
339
|
+
name: 'proceed',
|
|
340
|
+
message: 'Save anyway?',
|
|
341
|
+
initial: false,
|
|
342
|
+
}, {
|
|
343
|
+
onCancel: () => {
|
|
344
|
+
process.exit(1);
|
|
345
|
+
},
|
|
346
|
+
});
|
|
347
|
+
if (!proceed) {
|
|
348
|
+
console.log(pc.gray('Save cancelled.'));
|
|
349
|
+
process.exit(0);
|
|
350
|
+
}
|
|
351
|
+
console.log('');
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
245
355
|
// 8. POST to save-version API
|
|
246
356
|
console.log(pc.cyan(`Saving embeddable (based on v${fromVersionNumber})...`));
|
|
247
357
|
const body = {
|
|
@@ -253,8 +363,8 @@ async function runSaveInner(opts) {
|
|
|
253
363
|
if (opts.label) {
|
|
254
364
|
body.label = opts.label;
|
|
255
365
|
}
|
|
256
|
-
if (
|
|
257
|
-
body.branchId =
|
|
366
|
+
if (effectiveBranch) {
|
|
367
|
+
body.branchId = effectiveBranch;
|
|
258
368
|
}
|
|
259
369
|
const url = `${WEB_APP_BASE_URL}/api/embeddables/save-version`;
|
|
260
370
|
const headers = {
|
|
@@ -340,7 +450,8 @@ async function runSaveInner(opts) {
|
|
|
340
450
|
const { newVersionNumber } = forceResult.data;
|
|
341
451
|
console.log(pc.green(`✓ Saved as version ${newVersionNumber}`));
|
|
342
452
|
setVersionInConfig(embeddableId, newVersionNumber);
|
|
343
|
-
const
|
|
453
|
+
const branchSlug = getBranchSlugFromConfig(embeddableId);
|
|
454
|
+
const versionedPath = path.join(generatedDir, `embeddable-${branchSlug}@${newVersionNumber}.json`);
|
|
344
455
|
fs.mkdirSync(generatedDir, { recursive: true });
|
|
345
456
|
fs.writeFileSync(versionedPath, jsonContent, 'utf8');
|
|
346
457
|
console.log(pc.cyan(`✓ Saved version file to ${versionedPath}`));
|
|
@@ -358,8 +469,9 @@ async function runSaveInner(opts) {
|
|
|
358
469
|
console.log(pc.green(`✓ Saved as version ${newVersionNumber}`));
|
|
359
470
|
// Update _version in config.json so future saves know the base version
|
|
360
471
|
setVersionInConfig(embeddableId, newVersionNumber);
|
|
361
|
-
// Also save the versioned file to .generated/ as a snapshot
|
|
362
|
-
const
|
|
472
|
+
// Also save the versioned file to .generated/ as a snapshot (embeddable-{branch}@{version}.json)
|
|
473
|
+
const branchSlug = getBranchSlugFromConfig(embeddableId);
|
|
474
|
+
const versionedPath = path.join(generatedDir, `embeddable-${branchSlug}@${newVersionNumber}.json`);
|
|
363
475
|
fs.mkdirSync(generatedDir, { recursive: true });
|
|
364
476
|
fs.writeFileSync(versionedPath, jsonContent, 'utf8');
|
|
365
477
|
console.log(pc.cyan(`✓ Saved version file to ${versionedPath}`));
|
|
@@ -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"}
|