@lotics/cli 0.73.0 → 0.74.0
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/app_commands.d.ts +9 -0
- package/dist/app_commands.js +1 -1
- package/dist/client.d.ts +6 -0
- package/dist/client.js +4 -0
- package/dist/dev/rpc_handler.d.ts +1 -1
- package/dist/dev/rpc_handler.js +3 -0
- package/dist/generate_app_fields.d.ts +2 -0
- package/dist/generate_app_fields.js +1 -1
- package/dist/generate_package_fields.d.ts +48 -0
- package/dist/generate_package_fields.js +104 -0
- package/dist/generate_package_fields.test.d.ts +1 -0
- package/dist/generate_package_fields.test.js +51 -0
- package/dist/package_commands.js +111 -3
- package/dist/src/cli.js +158 -6
- package/dist/starter_template.d.ts +1 -1
- package/dist/starter_template.js +10 -4
- package/package.json +1 -1
package/dist/app_commands.d.ts
CHANGED
|
@@ -1,4 +1,13 @@
|
|
|
1
1
|
import { LoticsClient } from "./client.js";
|
|
2
|
+
/**
|
|
3
|
+
* Resolve the latest published version of a package from the npm registry.
|
|
4
|
+
* Returns null on any failure (network error, 404, malformed payload) so
|
|
5
|
+
* callers can fall back to a static pin rather than crashing `app create`.
|
|
6
|
+
*
|
|
7
|
+
* 1.5s timeout — npm registry is fast when reachable; the fallback is fine
|
|
8
|
+
* the rare times it isn't, and we don't want to block scaffold on a hang.
|
|
9
|
+
*/
|
|
10
|
+
export declare function fetchLatestNpmVersion(packageName: string): Promise<string | null>;
|
|
2
11
|
/**
|
|
3
12
|
* Manifest declaration for one workflow alias:
|
|
4
13
|
* `"alias": { workflow_id, inputs?: { key: { type, … } }, outputs?: { key: { type, … } } }`
|
package/dist/app_commands.js
CHANGED
|
@@ -32,7 +32,7 @@ import { loadProjectTypescript, checkWorkflowBodies, } from "./app_workflow_chec
|
|
|
32
32
|
* 1.5s timeout — npm registry is fast when reachable; the fallback is fine
|
|
33
33
|
* the rare times it isn't, and we don't want to block scaffold on a hang.
|
|
34
34
|
*/
|
|
35
|
-
async function fetchLatestNpmVersion(packageName) {
|
|
35
|
+
export async function fetchLatestNpmVersion(packageName) {
|
|
36
36
|
try {
|
|
37
37
|
const controller = new AbortController();
|
|
38
38
|
const timeout = setTimeout(() => controller.abort(), 1500);
|
package/dist/client.d.ts
CHANGED
|
@@ -559,6 +559,12 @@ export declare class LoticsClient {
|
|
|
559
559
|
image: string | null;
|
|
560
560
|
}>;
|
|
561
561
|
}>;
|
|
562
|
+
/** A package installation's alias→id maps (fields/options/roles) — the app's runtime F/OPT resolution. */
|
|
563
|
+
appBinding(app_id: string): Promise<{
|
|
564
|
+
fields: Record<string, string>;
|
|
565
|
+
options: Record<string, string>;
|
|
566
|
+
roles: Record<string, string>;
|
|
567
|
+
}>;
|
|
562
568
|
/**
|
|
563
569
|
* Resolve the full option set (key, label, color) of a named query's select
|
|
564
570
|
* columns — the picker companion to `appQuery`. Mirrors
|
package/dist/client.js
CHANGED
|
@@ -433,6 +433,10 @@ export class LoticsClient {
|
|
|
433
433
|
const qs = group_id ? `?group_id=${encodeURIComponent(group_id)}` : "";
|
|
434
434
|
return this.request("GET", `/v1/apps/${encodeURIComponent(app_id)}/members${qs}`);
|
|
435
435
|
}
|
|
436
|
+
/** A package installation's alias→id maps (fields/options/roles) — the app's runtime F/OPT resolution. */
|
|
437
|
+
async appBinding(app_id) {
|
|
438
|
+
return this.request("GET", `/v1/apps/${encodeURIComponent(app_id)}/binding`);
|
|
439
|
+
}
|
|
436
440
|
/**
|
|
437
441
|
* Resolve the full option set (key, label, color) of a named query's select
|
|
438
442
|
* columns — the picker companion to `appQuery`. Mirrors
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* { message }. Same shape as the production iframe-host's error path.
|
|
7
7
|
*/
|
|
8
8
|
import { LoticsClient } from "../client.js";
|
|
9
|
-
export type RpcOp = "query" | "field_options" | "workflow" | "members" | "context" | "upload_url" | "upload_complete" | "comments.list" | "comments.create" | "comments.update" | "comments.delete" | "comments.counts";
|
|
9
|
+
export type RpcOp = "query" | "field_options" | "workflow" | "members" | "context" | "binding" | "upload_url" | "upload_complete" | "comments.list" | "comments.create" | "comments.update" | "comments.delete" | "comments.counts";
|
|
10
10
|
export interface RpcRequest {
|
|
11
11
|
app_id: string;
|
|
12
12
|
op: RpcOp;
|
package/dist/dev/rpc_handler.js
CHANGED
|
@@ -11,6 +11,7 @@ const SUPPORTED_OPS = new Set([
|
|
|
11
11
|
"workflow",
|
|
12
12
|
"members",
|
|
13
13
|
"context",
|
|
14
|
+
"binding",
|
|
14
15
|
"upload_url",
|
|
15
16
|
"upload_complete",
|
|
16
17
|
"comments.list",
|
|
@@ -76,6 +77,8 @@ export async function dispatchRpc(client, body, opts) {
|
|
|
76
77
|
const p = body.payload;
|
|
77
78
|
return client.appMembers(body.app_id, p?.group);
|
|
78
79
|
}
|
|
80
|
+
case "binding":
|
|
81
|
+
return client.appBinding(body.app_id);
|
|
79
82
|
case "upload_url": {
|
|
80
83
|
const p = body.payload;
|
|
81
84
|
if (!p ||
|
|
@@ -46,6 +46,8 @@ export interface TableSchema {
|
|
|
46
46
|
* `upper` uppercases the result (TABLE aliases read as constants).
|
|
47
47
|
*/
|
|
48
48
|
export declare function slugifyAlias(name: string, upper: boolean): string;
|
|
49
|
+
/** A property key for an object literal — bare when a valid identifier, else quoted. */
|
|
50
|
+
export declare function propKey(alias: string): string;
|
|
49
51
|
/**
|
|
50
52
|
* Generate the full `.lotics/app_fields.ts` source. `tables` is the resolved
|
|
51
53
|
* workspace schema (the subset the app touches). An empty list yields valid,
|
|
@@ -68,7 +68,7 @@ function dedupeAliases(names, upper) {
|
|
|
68
68
|
});
|
|
69
69
|
}
|
|
70
70
|
/** A property key for an object literal — bare when a valid identifier, else quoted. */
|
|
71
|
-
function propKey(alias) {
|
|
71
|
+
export function propKey(alias) {
|
|
72
72
|
return isValidIdentifier(alias) ? alias : JSON.stringify(alias);
|
|
73
73
|
}
|
|
74
74
|
/** Resolve table + field aliases once, so the `F`/`OPT` maps and the union types agree. */
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codegen: emit a RUNTIME `.lotics/app_fields.ts` for a PACKAGE project from
|
|
3
|
+
* its `contract.json`.
|
|
4
|
+
*
|
|
5
|
+
* The bespoke twin (`generate_app_fields.ts`) bakes this workspace's concrete
|
|
6
|
+
* `fld_…`/`opt_…` ids at authoring time — correct when source and workspace
|
|
7
|
+
* co-live, fundamentally wrong for a package (one bundle, many workspaces,
|
|
8
|
+
* different ids in each). A package project instead resolves the SAME `F` /
|
|
9
|
+
* `OPT` surface at MODULE LOAD from the installation's binding via the SDK's
|
|
10
|
+
* `getAppBinding()` (the `binding` RPC op), using top-level await — the ESM
|
|
11
|
+
* graph waits for the binding before any importer evaluates, so
|
|
12
|
+
* `F.TASKS.title` is a plain string everywhere, including module-top-level
|
|
13
|
+
* constants. Extracted bespoke sources therefore compile unchanged: same
|
|
14
|
+
* import path, same shapes, same alias naming (contract aliases derive from
|
|
15
|
+
* the same display names the bespoke codegen slugifies).
|
|
16
|
+
*
|
|
17
|
+
* A missing alias fails LOUD at boot (the binding is adopt/install-verified
|
|
18
|
+
* complete, so a miss means the generated file is stale relative to the
|
|
19
|
+
* installed contract version) — never a silent undefined key in a write.
|
|
20
|
+
*
|
|
21
|
+
* Requires the app build to target es2022+ (the starter's `build.target`) —
|
|
22
|
+
* top-level await does not exist below it.
|
|
23
|
+
*/
|
|
24
|
+
/** The subset of a contract this generator reads (structural, not zod-validated —
|
|
25
|
+
* publish is the validating boundary; codegen must work on drafts too). */
|
|
26
|
+
export interface PackageContractShape {
|
|
27
|
+
entities?: Array<{
|
|
28
|
+
alias: string;
|
|
29
|
+
fields?: Array<{
|
|
30
|
+
alias: string;
|
|
31
|
+
options?: Array<{
|
|
32
|
+
alias: string;
|
|
33
|
+
}>;
|
|
34
|
+
}>;
|
|
35
|
+
}>;
|
|
36
|
+
roles?: Array<{
|
|
37
|
+
alias: string;
|
|
38
|
+
}>;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Generate the full package `.lotics/app_fields.ts` source. Valid contract
|
|
42
|
+
* aliases are slug-grammar (`^[a-z][a-z0-9_]*$`) and emit as bare identifiers
|
|
43
|
+
* — but this also runs on unvalidated DRAFTS (dev/sync pre-publish), so every
|
|
44
|
+
* key goes through `propKey`, which quotes anything that isn't a valid
|
|
45
|
+
* identifier instead of emitting broken TypeScript into a "DO NOT EDIT" file.
|
|
46
|
+
* Pure; same contract → same bytes.
|
|
47
|
+
*/
|
|
48
|
+
export declare function generatePackageAppFields(contract: PackageContractShape): string;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codegen: emit a RUNTIME `.lotics/app_fields.ts` for a PACKAGE project from
|
|
3
|
+
* its `contract.json`.
|
|
4
|
+
*
|
|
5
|
+
* The bespoke twin (`generate_app_fields.ts`) bakes this workspace's concrete
|
|
6
|
+
* `fld_…`/`opt_…` ids at authoring time — correct when source and workspace
|
|
7
|
+
* co-live, fundamentally wrong for a package (one bundle, many workspaces,
|
|
8
|
+
* different ids in each). A package project instead resolves the SAME `F` /
|
|
9
|
+
* `OPT` surface at MODULE LOAD from the installation's binding via the SDK's
|
|
10
|
+
* `getAppBinding()` (the `binding` RPC op), using top-level await — the ESM
|
|
11
|
+
* graph waits for the binding before any importer evaluates, so
|
|
12
|
+
* `F.TASKS.title` is a plain string everywhere, including module-top-level
|
|
13
|
+
* constants. Extracted bespoke sources therefore compile unchanged: same
|
|
14
|
+
* import path, same shapes, same alias naming (contract aliases derive from
|
|
15
|
+
* the same display names the bespoke codegen slugifies).
|
|
16
|
+
*
|
|
17
|
+
* A missing alias fails LOUD at boot (the binding is adopt/install-verified
|
|
18
|
+
* complete, so a miss means the generated file is stale relative to the
|
|
19
|
+
* installed contract version) — never a silent undefined key in a write.
|
|
20
|
+
*
|
|
21
|
+
* Requires the app build to target es2022+ (the starter's `build.target`) —
|
|
22
|
+
* top-level await does not exist below it.
|
|
23
|
+
*/
|
|
24
|
+
import { propKey } from "./generate_app_fields.js";
|
|
25
|
+
const HEADER = `// Auto-generated by 'lotics package new/extract/dev/sync' from contract.json.
|
|
26
|
+
// DO NOT EDIT — regenerated whenever the contract changes.
|
|
27
|
+
//
|
|
28
|
+
// Package apps resolve F/OPT/ROLE at MODULE LOAD from the installation's
|
|
29
|
+
// binding (contract alias → THIS workspace's concrete id) via the SDK's
|
|
30
|
+
// \`binding\` RPC. Top-level await: the module graph waits for the binding
|
|
31
|
+
// before any importer evaluates, so every entry is a plain string.
|
|
32
|
+
import { getAppBinding } from "@lotics/app-sdk";
|
|
33
|
+
|
|
34
|
+
const binding = await getAppBinding();
|
|
35
|
+
|
|
36
|
+
function bound(map: Record<string, string>, key: string, kind: string): string {
|
|
37
|
+
const id = map[key];
|
|
38
|
+
if (id === undefined) {
|
|
39
|
+
throw new Error(
|
|
40
|
+
\`app_fields: \${kind} "\${key}" is not in this installation's binding — \` +
|
|
41
|
+
\`the generated app_fields.ts is stale relative to the installed contract version.\`,
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
return id;
|
|
45
|
+
}
|
|
46
|
+
`;
|
|
47
|
+
/**
|
|
48
|
+
* Generate the full package `.lotics/app_fields.ts` source. Valid contract
|
|
49
|
+
* aliases are slug-grammar (`^[a-z][a-z0-9_]*$`) and emit as bare identifiers
|
|
50
|
+
* — but this also runs on unvalidated DRAFTS (dev/sync pre-publish), so every
|
|
51
|
+
* key goes through `propKey`, which quotes anything that isn't a valid
|
|
52
|
+
* identifier instead of emitting broken TypeScript into a "DO NOT EDIT" file.
|
|
53
|
+
* Pure; same contract → same bytes.
|
|
54
|
+
*/
|
|
55
|
+
export function generatePackageAppFields(contract) {
|
|
56
|
+
const entities = contract.entities ?? [];
|
|
57
|
+
const roles = contract.roles ?? [];
|
|
58
|
+
const fieldBlocks = [];
|
|
59
|
+
for (const entity of entities) {
|
|
60
|
+
const lines = (entity.fields ?? []).map((field) => ` ${propKey(field.alias)}: bound(binding.fields, ${JSON.stringify(`${entity.alias}.${field.alias}`)}, "field"),`);
|
|
61
|
+
if (lines.length === 0)
|
|
62
|
+
continue;
|
|
63
|
+
fieldBlocks.push(` ${propKey(entity.alias.toUpperCase())}: {\n${lines.join("\n")}\n },`);
|
|
64
|
+
}
|
|
65
|
+
const optionBlocks = [];
|
|
66
|
+
for (const entity of entities) {
|
|
67
|
+
const perField = [];
|
|
68
|
+
for (const field of entity.fields ?? []) {
|
|
69
|
+
const options = field.options ?? [];
|
|
70
|
+
if (options.length === 0)
|
|
71
|
+
continue;
|
|
72
|
+
const lines = options.map((option) => ` ${propKey(option.alias)}: bound(binding.options, ${JSON.stringify(`${entity.alias}.${field.alias}:${option.alias}`)}, "option"),`);
|
|
73
|
+
perField.push(` ${propKey(field.alias)}: {\n${lines.join("\n")}\n },`);
|
|
74
|
+
}
|
|
75
|
+
if (perField.length > 0) {
|
|
76
|
+
optionBlocks.push(` ${propKey(entity.alias.toUpperCase())}: {\n${perField.join("\n")}\n },`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
const fMap = fieldBlocks.length > 0
|
|
80
|
+
? `export const F = {\n${fieldBlocks.join("\n")}\n} as const;`
|
|
81
|
+
: `export const F = {} as const;`;
|
|
82
|
+
const optMap = optionBlocks.length > 0
|
|
83
|
+
? `export const OPT = {\n${optionBlocks.join("\n")}\n} as const;`
|
|
84
|
+
: `export const OPT = {} as const;`;
|
|
85
|
+
const roleMap = roles.length > 0
|
|
86
|
+
? `export const ROLE = {\n${roles
|
|
87
|
+
.map((role) => ` ${propKey(role.alias)}: bound(binding.roles, ${JSON.stringify(role.alias)}, "role"),`)
|
|
88
|
+
.join("\n")}\n} as const;`
|
|
89
|
+
: `export const ROLE = {} as const;`;
|
|
90
|
+
return `${HEADER}
|
|
91
|
+
${fMap}
|
|
92
|
+
|
|
93
|
+
${optMap}
|
|
94
|
+
|
|
95
|
+
${roleMap}
|
|
96
|
+
|
|
97
|
+
/** Field-id alias map: \`F[<ENTITY>][<field>]\` is this installation's \`fld_…\` id. */
|
|
98
|
+
export type AppFields = typeof F;
|
|
99
|
+
/** Select-option alias map: \`OPT[<ENTITY>][<field>][<option>]\` is this installation's \`opt_…\` id. */
|
|
100
|
+
export type AppOptions = typeof OPT;
|
|
101
|
+
/** Role alias map: \`ROLE[<role>]\` is this installation's \`grp_…\` id. */
|
|
102
|
+
export type AppRoles = typeof ROLE;
|
|
103
|
+
`;
|
|
104
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { generatePackageAppFields } from "./generate_package_fields.js";
|
|
3
|
+
const contract = {
|
|
4
|
+
entities: [
|
|
5
|
+
{
|
|
6
|
+
alias: "tasks",
|
|
7
|
+
fields: [
|
|
8
|
+
{ alias: "title" },
|
|
9
|
+
{
|
|
10
|
+
alias: "status",
|
|
11
|
+
options: [{ alias: "to_do" }, { alias: "doing" }, { alias: "done" }],
|
|
12
|
+
},
|
|
13
|
+
],
|
|
14
|
+
},
|
|
15
|
+
{ alias: "projects", fields: [{ alias: "name" }] },
|
|
16
|
+
],
|
|
17
|
+
roles: [{ alias: "approver" }],
|
|
18
|
+
};
|
|
19
|
+
describe("generatePackageAppFields", () => {
|
|
20
|
+
it("emits runtime-resolved F/OPT/ROLE keyed by contract aliases", () => {
|
|
21
|
+
const src = generatePackageAppFields(contract);
|
|
22
|
+
// Resolves through the SDK binding at module load (top-level await).
|
|
23
|
+
expect(src).toContain('import { getAppBinding } from "@lotics/app-sdk";');
|
|
24
|
+
expect(src).toContain("const binding = await getAppBinding();");
|
|
25
|
+
// F: uppercased entity alias, fully-qualified binding keys.
|
|
26
|
+
expect(src).toContain('title: bound(binding.fields, "tasks.title", "field")');
|
|
27
|
+
expect(src).toContain('name: bound(binding.fields, "projects.name", "field")');
|
|
28
|
+
expect(src).toMatch(/TASKS: \{/);
|
|
29
|
+
expect(src).toMatch(/PROJECTS: \{/);
|
|
30
|
+
// OPT: qualified entity.field:opt keys; option-less fields absent.
|
|
31
|
+
expect(src).toContain('to_do: bound(binding.options, "tasks.status:to_do", "option")');
|
|
32
|
+
expect(src).toContain('done: bound(binding.options, "tasks.status:done", "option")');
|
|
33
|
+
expect(src).not.toContain('"tasks.title:');
|
|
34
|
+
// ROLE map from contract roles.
|
|
35
|
+
expect(src).toContain('approver: bound(binding.roles, "approver", "role")');
|
|
36
|
+
// No concrete id in any VALUE position — the module is workspace-agnostic.
|
|
37
|
+
// (Doc comments legitimately name the id prefixes, so match quoted ids.)
|
|
38
|
+
expect(src).not.toMatch(/"(fld|opt|tbl|grp)_/);
|
|
39
|
+
});
|
|
40
|
+
it("emits valid empty maps for an empty contract", () => {
|
|
41
|
+
const src = generatePackageAppFields({});
|
|
42
|
+
expect(src).toContain("export const F = {} as const;");
|
|
43
|
+
expect(src).toContain("export const OPT = {} as const;");
|
|
44
|
+
expect(src).toContain("export const ROLE = {} as const;");
|
|
45
|
+
// Still imports the SDK so the module shape is uniform.
|
|
46
|
+
expect(src).toContain("const binding = await getAppBinding();");
|
|
47
|
+
});
|
|
48
|
+
it("is deterministic — same contract, same bytes", () => {
|
|
49
|
+
expect(generatePackageAppFields(contract)).toBe(generatePackageAppFields(contract));
|
|
50
|
+
});
|
|
51
|
+
});
|
package/dist/package_commands.js
CHANGED
|
@@ -25,8 +25,9 @@ import fs from "node:fs";
|
|
|
25
25
|
import path from "node:path";
|
|
26
26
|
import { tmpdir } from "node:os";
|
|
27
27
|
import "./client.js";
|
|
28
|
-
import { buildStarterTemplate } from "./starter_template.js";
|
|
29
|
-
import {
|
|
28
|
+
import { buildStarterTemplate, STARTER_FALLBACK_SDK_VERSION } from "./starter_template.js";
|
|
29
|
+
import { generatePackageAppFields, } from "./generate_package_fields.js";
|
|
30
|
+
import { appDirName, fetchLatestNpmVersion, runNpm, runTar } from "./app_commands.js";
|
|
30
31
|
import { writeFileAtomic } from "./file_command_io.js";
|
|
31
32
|
import { startDevServer, openBrowser } from "./dev/server.js";
|
|
32
33
|
const CONTRACT_FILE = "contract.json";
|
|
@@ -160,6 +161,36 @@ export function formatExtractReport(report) {
|
|
|
160
161
|
.map((f) => ` [${f.severity}] ${f.area}: ${f.message}`));
|
|
161
162
|
return { lines, hasError: report.some((f) => f.severity === "error") };
|
|
162
163
|
}
|
|
164
|
+
function dotLoticsDirEnsured(projectDir) {
|
|
165
|
+
const dir = path.join(projectDir, ".lotics");
|
|
166
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
167
|
+
return dir;
|
|
168
|
+
}
|
|
169
|
+
/** Numeric "x.y.z" compare — no semver dep (the CLI has zero runtime deps). */
|
|
170
|
+
function cmpVersions(a, b) {
|
|
171
|
+
const pa = a.split(".").map(Number);
|
|
172
|
+
const pb = b.split(".").map(Number);
|
|
173
|
+
for (let i = 0; i < 3; i++) {
|
|
174
|
+
const d = (pa[i] || 0) - (pb[i] || 0);
|
|
175
|
+
if (d !== 0)
|
|
176
|
+
return d;
|
|
177
|
+
}
|
|
178
|
+
return 0;
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* The `@lotics/app-sdk` range a package project needs: the live npm latest,
|
|
182
|
+
* clamped to never fall below `STARTER_FALLBACK_SDK_VERSION` — the release
|
|
183
|
+
* that ships `getAppBinding`, which the generated `.lotics/app_fields.ts`
|
|
184
|
+
* imports. A `^0.x` caret range never crosses a minor, so pinning below the
|
|
185
|
+
* floor (offline, or npm not yet carrying the release) would permanently
|
|
186
|
+
* scaffold projects that cannot compile their own generated code.
|
|
187
|
+
*/
|
|
188
|
+
function packageSdkRange(sdkLatest) {
|
|
189
|
+
const version = sdkLatest !== null && cmpVersions(sdkLatest, STARTER_FALLBACK_SDK_VERSION) > 0
|
|
190
|
+
? sdkLatest
|
|
191
|
+
: STARTER_FALLBACK_SDK_VERSION;
|
|
192
|
+
return `^${version}`;
|
|
193
|
+
}
|
|
163
194
|
/** Read + JSON-parse the project's contract.json (the alias-keyed declaration). */
|
|
164
195
|
function readContract(projectDir) {
|
|
165
196
|
const contractPath = path.join(projectDir, CONTRACT_FILE);
|
|
@@ -168,6 +199,21 @@ function readContract(projectDir) {
|
|
|
168
199
|
}
|
|
169
200
|
return JSON.parse(fs.readFileSync(contractPath, "utf-8"));
|
|
170
201
|
}
|
|
202
|
+
/**
|
|
203
|
+
* (Re)generate the package project's `.lotics/app_fields.ts` from its
|
|
204
|
+
* contract.json — the runtime-resolving F/OPT/ROLE surface (see
|
|
205
|
+
* `generate_package_fields.ts`). Runs on new/extract and on every dev/sync so
|
|
206
|
+
* contract edits keep the aliases addressable. `.lotics` is in
|
|
207
|
+
* SOURCE_STAGE_EXCLUDES: the compiled module ships inside `dist/`, and a
|
|
208
|
+
* post-eject `lotics app pull` regenerates the bespoke (baked) variant.
|
|
209
|
+
*/
|
|
210
|
+
function writePackageAppFields(projectDir) {
|
|
211
|
+
const contract = readContract(projectDir);
|
|
212
|
+
const dotLotics = path.join(projectDir, ".lotics");
|
|
213
|
+
fs.mkdirSync(dotLotics, { recursive: true });
|
|
214
|
+
fs.writeFileSync(path.join(dotLotics, "app_fields.ts"), generatePackageAppFields(contract));
|
|
215
|
+
console.error("Wrote .lotics/app_fields.ts (contract-derived, runtime-resolved)");
|
|
216
|
+
}
|
|
171
217
|
/** Top-level project entries that never ship in the source archive. */
|
|
172
218
|
const SOURCE_STAGE_EXCLUDES = new Set([
|
|
173
219
|
"node_modules",
|
|
@@ -299,10 +345,21 @@ export async function packageNew(args) {
|
|
|
299
345
|
// a package is workspace-agnostic, so those placeholders are replaced with the
|
|
300
346
|
// package manifest below. The rest of the starter (config, src, tests) is the
|
|
301
347
|
// package's code surface verbatim.
|
|
348
|
+
// Resolve the live @lotics/ui + @lotics/app-sdk versions like `app create`
|
|
349
|
+
// does — the generated .lotics/app_fields.ts imports `getAppBinding`, and a
|
|
350
|
+
// ^0.x caret range never crosses a minor, so a stale starter fallback would
|
|
351
|
+
// permanently pin the project below the API it needs (offline still works
|
|
352
|
+
// off STARTER_FALLBACK_*, kept ≥ that floor).
|
|
353
|
+
const [uiLatest, sdkLatest] = await Promise.all([
|
|
354
|
+
fetchLatestNpmVersion("@lotics/ui"),
|
|
355
|
+
fetchLatestNpmVersion("@lotics/app-sdk"),
|
|
356
|
+
]);
|
|
302
357
|
const files = buildStarterTemplate({
|
|
303
358
|
app_name: args.name,
|
|
304
359
|
app_id: "",
|
|
305
360
|
workspace_id: "",
|
|
361
|
+
ui_version: uiLatest ? `^${uiLatest}` : undefined,
|
|
362
|
+
sdk_version: packageSdkRange(sdkLatest),
|
|
306
363
|
});
|
|
307
364
|
for (const file of files) {
|
|
308
365
|
const fullPath = path.join(targetPath, file.path);
|
|
@@ -324,6 +381,7 @@ export async function packageNew(args) {
|
|
|
324
381
|
fs.writeFileSync(fullPath, file.content);
|
|
325
382
|
}
|
|
326
383
|
fs.writeFileSync(path.join(targetPath, CONTRACT_FILE), JSON.stringify(starterContract(args.name), null, 2) + "\n");
|
|
384
|
+
writePackageAppFields(targetPath);
|
|
327
385
|
console.error(`Scaffolded ${files.length + 1} files into ${targetPath}`);
|
|
328
386
|
console.error("Installing npm dependencies...");
|
|
329
387
|
await runNpm(["install"], targetPath);
|
|
@@ -426,6 +484,9 @@ async function syncToDevWorkspace(client, projectDir) {
|
|
|
426
484
|
throw new Error(`Workspace ${devWorkspaceId} is not accessible with these credentials. Pass --workspace <dev_ws> (a workspace created with --dev).`);
|
|
427
485
|
}
|
|
428
486
|
assertDevWorkspace(workspace);
|
|
487
|
+
// Contract may have been edited since the last sync — regenerate the
|
|
488
|
+
// runtime F/OPT/ROLE surface before the build that publishVersion runs.
|
|
489
|
+
writePackageAppFields(projectDir);
|
|
429
490
|
const { package_id, version } = await publishVersion(client, projectDir, {
|
|
430
491
|
changelog: "dev sync",
|
|
431
492
|
});
|
|
@@ -741,10 +802,57 @@ export async function packageExtract(client, args) {
|
|
|
741
802
|
// Swap the app manifest (lotics.app_id/workspace_id) for a fresh, unpublished
|
|
742
803
|
// package manifest — atomic write via the existing manifest writer.
|
|
743
804
|
const appPkgJson = JSON.parse(fs.readFileSync(packageJsonPath(targetPath), "utf-8"));
|
|
744
|
-
|
|
805
|
+
const draft = draftPackageProjectFromApp(appPkgJson, { name: app.name, description: null });
|
|
806
|
+
// The origin app's package.json rides in the source archive with ITS OWN
|
|
807
|
+
// @lotics/app-sdk range — raise it to the getAppBinding floor the generated
|
|
808
|
+
// .lotics/app_fields.ts needs (never lower an already-newer range).
|
|
809
|
+
const deps = isPlainObject(draft.pkgJson.dependencies) ? draft.pkgJson.dependencies : {};
|
|
810
|
+
const originRange = typeof deps["@lotics/app-sdk"] === "string" ? deps["@lotics/app-sdk"] : null;
|
|
811
|
+
const originFloor = originRange?.replace(/^[\^~]/, "") ?? null;
|
|
812
|
+
if (originFloor === null || cmpVersions(originFloor, STARTER_FALLBACK_SDK_VERSION) < 0) {
|
|
813
|
+
const raised = packageSdkRange(await fetchLatestNpmVersion("@lotics/app-sdk"));
|
|
814
|
+
draft.pkgJson.dependencies = { ...deps, "@lotics/app-sdk": raised };
|
|
815
|
+
console.error(`Raised @lotics/app-sdk to ${raised} (generated app_fields needs getAppBinding)`);
|
|
816
|
+
}
|
|
817
|
+
writePackageManifest(targetPath, draft);
|
|
745
818
|
// The alias-keyed draft contract — the reviewable master. Pretty + trailing newline.
|
|
746
819
|
fs.writeFileSync(path.join(targetPath, CONTRACT_FILE), JSON.stringify(extracted.contract, null, 2) + "\n");
|
|
747
820
|
console.error(`Wrote ${CONTRACT_FILE}`);
|
|
821
|
+
// The origin's baked `.lotics/app_fields.ts` (concrete ids) never travels —
|
|
822
|
+
// regenerate it contract-derived so `F`/`OPT` resolve per-install at runtime
|
|
823
|
+
// and the extracted source compiles unchanged.
|
|
824
|
+
writePackageAppFields(targetPath);
|
|
825
|
+
// vite.config.ts is starter-owned machinery — refresh it wholesale so the
|
|
826
|
+
// package project carries the current starter config (notably
|
|
827
|
+
// `build.target: "es2022"`, which the generated app_fields' top-level await
|
|
828
|
+
// requires). Origin-side dev customizations (e.g. `lotics ui link` aliases)
|
|
829
|
+
// don't belong in a package.
|
|
830
|
+
const starterViteConfig = buildStarterTemplate({
|
|
831
|
+
app_name: app.name,
|
|
832
|
+
app_id: "",
|
|
833
|
+
workspace_id: "",
|
|
834
|
+
}).find((f) => f.path === "vite.config.ts");
|
|
835
|
+
if (starterViteConfig === undefined) {
|
|
836
|
+
throw new Error("starter template is missing vite.config.ts — cannot refresh the package project");
|
|
837
|
+
}
|
|
838
|
+
const viteConfigPath = path.join(targetPath, "vite.config.ts");
|
|
839
|
+
const originViteConfig = fs.existsSync(viteConfigPath)
|
|
840
|
+
? fs.readFileSync(viteConfigPath, "utf-8")
|
|
841
|
+
: null;
|
|
842
|
+
fs.writeFileSync(viteConfigPath, starterViteConfig.content);
|
|
843
|
+
if (originViteConfig !== null && originViteConfig !== starterViteConfig.content) {
|
|
844
|
+
// The origin may carry legitimate customizations (optimizeDeps entries,
|
|
845
|
+
// plugins) beyond the dev-links that must not ship — never destroy them
|
|
846
|
+
// silently: stash the original for manual re-application.
|
|
847
|
+
const stash = path.join(dotLoticsDirEnsured(targetPath), "vite.config.origin.ts");
|
|
848
|
+
fs.writeFileSync(stash, originViteConfig);
|
|
849
|
+
console.error("Refreshed vite.config.ts from the starter (build.target es2022). The origin app's config " +
|
|
850
|
+
"differed — its original was saved to .lotics/vite.config.origin.ts; re-apply any " +
|
|
851
|
+
"custom optimizeDeps/plugins entries you still need (never dev-link aliases).");
|
|
852
|
+
}
|
|
853
|
+
else {
|
|
854
|
+
console.error("Refreshed vite.config.ts from the starter (build.target es2022)");
|
|
855
|
+
}
|
|
748
856
|
// Stage each file-backed template at the `bytes_ref` the contract references.
|
|
749
857
|
// The download lands in the target dir (fresh, so no name collision) under the
|
|
750
858
|
// file's own name; rename it to the exact `bytes_ref` basename the contract
|
package/dist/src/cli.js
CHANGED
|
@@ -30012,6 +30012,10 @@ var LoticsClient = class {
|
|
|
30012
30012
|
const qs = group_id ? `?group_id=${encodeURIComponent(group_id)}` : "";
|
|
30013
30013
|
return this.request("GET", `/v1/apps/${encodeURIComponent(app_id)}/members${qs}`);
|
|
30014
30014
|
}
|
|
30015
|
+
/** A package installation's alias→id maps (fields/options/roles) — the app's runtime F/OPT resolution. */
|
|
30016
|
+
async appBinding(app_id) {
|
|
30017
|
+
return this.request("GET", `/v1/apps/${encodeURIComponent(app_id)}/binding`);
|
|
30018
|
+
}
|
|
30015
30019
|
/**
|
|
30016
30020
|
* Resolve the full option set (key, label, color) of a named query's select
|
|
30017
30021
|
* columns — the picker companion to `appQuery`. Mirrors
|
|
@@ -30554,7 +30558,7 @@ import { tmpdir } from "node:os";
|
|
|
30554
30558
|
|
|
30555
30559
|
// src/starter_template.ts
|
|
30556
30560
|
var STARTER_FALLBACK_UI_VERSION = "6.1.0";
|
|
30557
|
-
var STARTER_FALLBACK_SDK_VERSION = "0.
|
|
30561
|
+
var STARTER_FALLBACK_SDK_VERSION = "0.51.0";
|
|
30558
30562
|
var STARTER_REACT_NATIVE_VERSION = "0.85.3";
|
|
30559
30563
|
function buildStarterTemplate(args) {
|
|
30560
30564
|
const uiVersion = args.ui_version ?? `^${STARTER_FALLBACK_UI_VERSION}`;
|
|
@@ -30774,6 +30778,11 @@ export default defineConfig({
|
|
|
30774
30778
|
build: {
|
|
30775
30779
|
outDir: "dist",
|
|
30776
30780
|
sourcemap: true,
|
|
30781
|
+
// Top-level await (package projects' generated .lotics/app_fields.ts
|
|
30782
|
+
// resolves the installation binding at module load) needs es2022 \u2014 Vite's
|
|
30783
|
+
// default 'modules' baseline is es2020 and esbuild hard-fails TLA there.
|
|
30784
|
+
// Dev already transforms at esnext, so this only aligns the prod build.
|
|
30785
|
+
target: "es2022",
|
|
30777
30786
|
},
|
|
30778
30787
|
server: {
|
|
30779
30788
|
// Allow the sandboxed null-origin iframe used by \`lotics app dev\` to
|
|
@@ -31277,6 +31286,7 @@ var SUPPORTED_OPS = /* @__PURE__ */ new Set([
|
|
|
31277
31286
|
"workflow",
|
|
31278
31287
|
"members",
|
|
31279
31288
|
"context",
|
|
31289
|
+
"binding",
|
|
31280
31290
|
"upload_url",
|
|
31281
31291
|
"upload_complete",
|
|
31282
31292
|
"comments.list",
|
|
@@ -31334,6 +31344,8 @@ async function dispatchRpc(client, body, opts) {
|
|
|
31334
31344
|
const p = body.payload;
|
|
31335
31345
|
return client.appMembers(body.app_id, p?.group);
|
|
31336
31346
|
}
|
|
31347
|
+
case "binding":
|
|
31348
|
+
return client.appBinding(body.app_id);
|
|
31337
31349
|
case "upload_url": {
|
|
31338
31350
|
const p = body.payload;
|
|
31339
31351
|
if (!p || typeof p.filename !== "string" || typeof p.mime_type !== "string" || typeof p.file_size !== "number") {
|
|
@@ -33351,6 +33363,86 @@ import fs6 from "node:fs";
|
|
|
33351
33363
|
import path7 from "node:path";
|
|
33352
33364
|
import { tmpdir as tmpdir2 } from "node:os";
|
|
33353
33365
|
|
|
33366
|
+
// src/generate_package_fields.ts
|
|
33367
|
+
var HEADER5 = `// Auto-generated by 'lotics package new/extract/dev/sync' from contract.json.
|
|
33368
|
+
// DO NOT EDIT \u2014 regenerated whenever the contract changes.
|
|
33369
|
+
//
|
|
33370
|
+
// Package apps resolve F/OPT/ROLE at MODULE LOAD from the installation's
|
|
33371
|
+
// binding (contract alias \u2192 THIS workspace's concrete id) via the SDK's
|
|
33372
|
+
// \`binding\` RPC. Top-level await: the module graph waits for the binding
|
|
33373
|
+
// before any importer evaluates, so every entry is a plain string.
|
|
33374
|
+
import { getAppBinding } from "@lotics/app-sdk";
|
|
33375
|
+
|
|
33376
|
+
const binding = await getAppBinding();
|
|
33377
|
+
|
|
33378
|
+
function bound(map: Record<string, string>, key: string, kind: string): string {
|
|
33379
|
+
const id = map[key];
|
|
33380
|
+
if (id === undefined) {
|
|
33381
|
+
throw new Error(
|
|
33382
|
+
\`app_fields: \${kind} "\${key}" is not in this installation's binding \u2014 \` +
|
|
33383
|
+
\`the generated app_fields.ts is stale relative to the installed contract version.\`,
|
|
33384
|
+
);
|
|
33385
|
+
}
|
|
33386
|
+
return id;
|
|
33387
|
+
}
|
|
33388
|
+
`;
|
|
33389
|
+
function generatePackageAppFields(contract) {
|
|
33390
|
+
const entities = contract.entities ?? [];
|
|
33391
|
+
const roles = contract.roles ?? [];
|
|
33392
|
+
const fieldBlocks = [];
|
|
33393
|
+
for (const entity of entities) {
|
|
33394
|
+
const lines = (entity.fields ?? []).map(
|
|
33395
|
+
(field) => ` ${propKey(field.alias)}: bound(binding.fields, ${JSON.stringify(`${entity.alias}.${field.alias}`)}, "field"),`
|
|
33396
|
+
);
|
|
33397
|
+
if (lines.length === 0) continue;
|
|
33398
|
+
fieldBlocks.push(` ${propKey(entity.alias.toUpperCase())}: {
|
|
33399
|
+
${lines.join("\n")}
|
|
33400
|
+
},`);
|
|
33401
|
+
}
|
|
33402
|
+
const optionBlocks = [];
|
|
33403
|
+
for (const entity of entities) {
|
|
33404
|
+
const perField = [];
|
|
33405
|
+
for (const field of entity.fields ?? []) {
|
|
33406
|
+
const options = field.options ?? [];
|
|
33407
|
+
if (options.length === 0) continue;
|
|
33408
|
+
const lines = options.map(
|
|
33409
|
+
(option) => ` ${propKey(option.alias)}: bound(binding.options, ${JSON.stringify(`${entity.alias}.${field.alias}:${option.alias}`)}, "option"),`
|
|
33410
|
+
);
|
|
33411
|
+
perField.push(` ${propKey(field.alias)}: {
|
|
33412
|
+
${lines.join("\n")}
|
|
33413
|
+
},`);
|
|
33414
|
+
}
|
|
33415
|
+
if (perField.length > 0) {
|
|
33416
|
+
optionBlocks.push(` ${propKey(entity.alias.toUpperCase())}: {
|
|
33417
|
+
${perField.join("\n")}
|
|
33418
|
+
},`);
|
|
33419
|
+
}
|
|
33420
|
+
}
|
|
33421
|
+
const fMap = fieldBlocks.length > 0 ? `export const F = {
|
|
33422
|
+
${fieldBlocks.join("\n")}
|
|
33423
|
+
} as const;` : `export const F = {} as const;`;
|
|
33424
|
+
const optMap = optionBlocks.length > 0 ? `export const OPT = {
|
|
33425
|
+
${optionBlocks.join("\n")}
|
|
33426
|
+
} as const;` : `export const OPT = {} as const;`;
|
|
33427
|
+
const roleMap = roles.length > 0 ? `export const ROLE = {
|
|
33428
|
+
${roles.map((role) => ` ${propKey(role.alias)}: bound(binding.roles, ${JSON.stringify(role.alias)}, "role"),`).join("\n")}
|
|
33429
|
+
} as const;` : `export const ROLE = {} as const;`;
|
|
33430
|
+
return `${HEADER5}
|
|
33431
|
+
${fMap}
|
|
33432
|
+
|
|
33433
|
+
${optMap}
|
|
33434
|
+
|
|
33435
|
+
${roleMap}
|
|
33436
|
+
|
|
33437
|
+
/** Field-id alias map: \`F[<ENTITY>][<field>]\` is this installation's \`fld_\u2026\` id. */
|
|
33438
|
+
export type AppFields = typeof F;
|
|
33439
|
+
/** Select-option alias map: \`OPT[<ENTITY>][<field>][<option>]\` is this installation's \`opt_\u2026\` id. */
|
|
33440
|
+
export type AppOptions = typeof OPT;
|
|
33441
|
+
/** Role alias map: \`ROLE[<role>]\` is this installation's \`grp_\u2026\` id. */
|
|
33442
|
+
export type AppRoles = typeof ROLE;
|
|
33443
|
+
`;
|
|
33444
|
+
}
|
|
33445
|
+
|
|
33354
33446
|
// src/file_command_io.ts
|
|
33355
33447
|
import fs5 from "node:fs";
|
|
33356
33448
|
import path6 from "node:path";
|
|
@@ -33474,6 +33566,24 @@ function formatExtractReport(report) {
|
|
|
33474
33566
|
);
|
|
33475
33567
|
return { lines, hasError: report.some((f) => f.severity === "error") };
|
|
33476
33568
|
}
|
|
33569
|
+
function dotLoticsDirEnsured(projectDir) {
|
|
33570
|
+
const dir = path7.join(projectDir, ".lotics");
|
|
33571
|
+
fs6.mkdirSync(dir, { recursive: true });
|
|
33572
|
+
return dir;
|
|
33573
|
+
}
|
|
33574
|
+
function cmpVersions(a, b) {
|
|
33575
|
+
const pa = a.split(".").map(Number);
|
|
33576
|
+
const pb = b.split(".").map(Number);
|
|
33577
|
+
for (let i2 = 0; i2 < 3; i2++) {
|
|
33578
|
+
const d = (pa[i2] || 0) - (pb[i2] || 0);
|
|
33579
|
+
if (d !== 0) return d;
|
|
33580
|
+
}
|
|
33581
|
+
return 0;
|
|
33582
|
+
}
|
|
33583
|
+
function packageSdkRange(sdkLatest) {
|
|
33584
|
+
const version = sdkLatest !== null && cmpVersions(sdkLatest, STARTER_FALLBACK_SDK_VERSION) > 0 ? sdkLatest : STARTER_FALLBACK_SDK_VERSION;
|
|
33585
|
+
return `^${version}`;
|
|
33586
|
+
}
|
|
33477
33587
|
function readContract(projectDir) {
|
|
33478
33588
|
const contractPath = path7.join(projectDir, CONTRACT_FILE);
|
|
33479
33589
|
if (!fs6.existsSync(contractPath)) {
|
|
@@ -33483,6 +33593,13 @@ function readContract(projectDir) {
|
|
|
33483
33593
|
}
|
|
33484
33594
|
return JSON.parse(fs6.readFileSync(contractPath, "utf-8"));
|
|
33485
33595
|
}
|
|
33596
|
+
function writePackageAppFields(projectDir) {
|
|
33597
|
+
const contract = readContract(projectDir);
|
|
33598
|
+
const dotLotics = path7.join(projectDir, ".lotics");
|
|
33599
|
+
fs6.mkdirSync(dotLotics, { recursive: true });
|
|
33600
|
+
fs6.writeFileSync(path7.join(dotLotics, "app_fields.ts"), generatePackageAppFields(contract));
|
|
33601
|
+
console.error("Wrote .lotics/app_fields.ts (contract-derived, runtime-resolved)");
|
|
33602
|
+
}
|
|
33486
33603
|
var SOURCE_STAGE_EXCLUDES = /* @__PURE__ */ new Set([
|
|
33487
33604
|
"node_modules",
|
|
33488
33605
|
"dist",
|
|
@@ -33576,10 +33693,16 @@ async function packageNew(args) {
|
|
|
33576
33693
|
throw new Error(`Target directory ${targetPath} is not empty.`);
|
|
33577
33694
|
}
|
|
33578
33695
|
fs6.mkdirSync(targetPath, { recursive: true });
|
|
33696
|
+
const [uiLatest, sdkLatest] = await Promise.all([
|
|
33697
|
+
fetchLatestNpmVersion("@lotics/ui"),
|
|
33698
|
+
fetchLatestNpmVersion("@lotics/app-sdk")
|
|
33699
|
+
]);
|
|
33579
33700
|
const files = buildStarterTemplate({
|
|
33580
33701
|
app_name: args.name,
|
|
33581
33702
|
app_id: "",
|
|
33582
|
-
workspace_id: ""
|
|
33703
|
+
workspace_id: "",
|
|
33704
|
+
ui_version: uiLatest ? `^${uiLatest}` : void 0,
|
|
33705
|
+
sdk_version: packageSdkRange(sdkLatest)
|
|
33583
33706
|
});
|
|
33584
33707
|
for (const file of files) {
|
|
33585
33708
|
const fullPath = path7.join(targetPath, file.path);
|
|
@@ -33604,6 +33727,7 @@ async function packageNew(args) {
|
|
|
33604
33727
|
path7.join(targetPath, CONTRACT_FILE),
|
|
33605
33728
|
JSON.stringify(starterContract(args.name), null, 2) + "\n"
|
|
33606
33729
|
);
|
|
33730
|
+
writePackageAppFields(targetPath);
|
|
33607
33731
|
console.error(`Scaffolded ${files.length + 1} files into ${targetPath}`);
|
|
33608
33732
|
console.error("Installing npm dependencies...");
|
|
33609
33733
|
await runNpm(["install"], targetPath);
|
|
@@ -33675,6 +33799,7 @@ async function syncToDevWorkspace(client, projectDir) {
|
|
|
33675
33799
|
);
|
|
33676
33800
|
}
|
|
33677
33801
|
assertDevWorkspace(workspace);
|
|
33802
|
+
writePackageAppFields(projectDir);
|
|
33678
33803
|
const { package_id, version } = await publishVersion(client, projectDir, {
|
|
33679
33804
|
changelog: "dev sync"
|
|
33680
33805
|
});
|
|
@@ -33950,15 +34075,42 @@ async function packageExtract(client, args) {
|
|
|
33950
34075
|
const appPkgJson = JSON.parse(
|
|
33951
34076
|
fs6.readFileSync(packageJsonPath(targetPath), "utf-8")
|
|
33952
34077
|
);
|
|
33953
|
-
|
|
33954
|
-
|
|
33955
|
-
|
|
33956
|
-
);
|
|
34078
|
+
const draft = draftPackageProjectFromApp(appPkgJson, { name: app.name, description: null });
|
|
34079
|
+
const deps = isPlainObject(draft.pkgJson.dependencies) ? draft.pkgJson.dependencies : {};
|
|
34080
|
+
const originRange = typeof deps["@lotics/app-sdk"] === "string" ? deps["@lotics/app-sdk"] : null;
|
|
34081
|
+
const originFloor = originRange?.replace(/^[\^~]/, "") ?? null;
|
|
34082
|
+
if (originFloor === null || cmpVersions(originFloor, STARTER_FALLBACK_SDK_VERSION) < 0) {
|
|
34083
|
+
const raised = packageSdkRange(await fetchLatestNpmVersion("@lotics/app-sdk"));
|
|
34084
|
+
draft.pkgJson.dependencies = { ...deps, "@lotics/app-sdk": raised };
|
|
34085
|
+
console.error(`Raised @lotics/app-sdk to ${raised} (generated app_fields needs getAppBinding)`);
|
|
34086
|
+
}
|
|
34087
|
+
writePackageManifest(targetPath, draft);
|
|
33957
34088
|
fs6.writeFileSync(
|
|
33958
34089
|
path7.join(targetPath, CONTRACT_FILE),
|
|
33959
34090
|
JSON.stringify(extracted.contract, null, 2) + "\n"
|
|
33960
34091
|
);
|
|
33961
34092
|
console.error(`Wrote ${CONTRACT_FILE}`);
|
|
34093
|
+
writePackageAppFields(targetPath);
|
|
34094
|
+
const starterViteConfig = buildStarterTemplate({
|
|
34095
|
+
app_name: app.name,
|
|
34096
|
+
app_id: "",
|
|
34097
|
+
workspace_id: ""
|
|
34098
|
+
}).find((f) => f.path === "vite.config.ts");
|
|
34099
|
+
if (starterViteConfig === void 0) {
|
|
34100
|
+
throw new Error("starter template is missing vite.config.ts \u2014 cannot refresh the package project");
|
|
34101
|
+
}
|
|
34102
|
+
const viteConfigPath = path7.join(targetPath, "vite.config.ts");
|
|
34103
|
+
const originViteConfig = fs6.existsSync(viteConfigPath) ? fs6.readFileSync(viteConfigPath, "utf-8") : null;
|
|
34104
|
+
fs6.writeFileSync(viteConfigPath, starterViteConfig.content);
|
|
34105
|
+
if (originViteConfig !== null && originViteConfig !== starterViteConfig.content) {
|
|
34106
|
+
const stash = path7.join(dotLoticsDirEnsured(targetPath), "vite.config.origin.ts");
|
|
34107
|
+
fs6.writeFileSync(stash, originViteConfig);
|
|
34108
|
+
console.error(
|
|
34109
|
+
"Refreshed vite.config.ts from the starter (build.target es2022). The origin app's config differed \u2014 its original was saved to .lotics/vite.config.origin.ts; re-apply any custom optimizeDeps/plugins entries you still need (never dev-link aliases)."
|
|
34110
|
+
);
|
|
34111
|
+
} else {
|
|
34112
|
+
console.error("Refreshed vite.config.ts from the starter (build.target es2022)");
|
|
34113
|
+
}
|
|
33962
34114
|
for (const tf of extracted.template_files) {
|
|
33963
34115
|
const dest = path7.join(targetPath, tf.bytes_ref);
|
|
33964
34116
|
fs6.mkdirSync(path7.dirname(dest), { recursive: true });
|
|
@@ -42,7 +42,7 @@ export interface StarterFile {
|
|
|
42
42
|
* fall back here when the lookup fails.
|
|
43
43
|
*/
|
|
44
44
|
export declare const STARTER_FALLBACK_UI_VERSION = "6.1.0";
|
|
45
|
-
export declare const STARTER_FALLBACK_SDK_VERSION = "0.
|
|
45
|
+
export declare const STARTER_FALLBACK_SDK_VERSION = "0.51.0";
|
|
46
46
|
/**
|
|
47
47
|
* react-native pin for scaffolded apps. Matches the monorepo frontend's pin so
|
|
48
48
|
* an app deep-typechecks `@lotics/ui`'s `.tsx` source against the SAME RN types
|
package/dist/starter_template.js
CHANGED
|
@@ -38,10 +38,11 @@
|
|
|
38
38
|
* fall back here when the lookup fails.
|
|
39
39
|
*/
|
|
40
40
|
export const STARTER_FALLBACK_UI_VERSION = "6.1.0";
|
|
41
|
-
// Must be ≥ the release that added
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
|
|
41
|
+
// Must be ≥ the release that added `getAppBinding` (0.51.0, used by package
|
|
42
|
+
// projects' generated .lotics/app_fields.ts) — and note ^0.x caret ranges never
|
|
43
|
+
// cross a minor, so a stale floor here permanently pins scaffolds below the
|
|
44
|
+
// APIs the generated code imports.
|
|
45
|
+
export const STARTER_FALLBACK_SDK_VERSION = "0.51.0";
|
|
45
46
|
/**
|
|
46
47
|
* react-native pin for scaffolded apps. Matches the monorepo frontend's pin so
|
|
47
48
|
* an app deep-typechecks `@lotics/ui`'s `.tsx` source against the SAME RN types
|
|
@@ -267,6 +268,11 @@ export default defineConfig({
|
|
|
267
268
|
build: {
|
|
268
269
|
outDir: "dist",
|
|
269
270
|
sourcemap: true,
|
|
271
|
+
// Top-level await (package projects' generated .lotics/app_fields.ts
|
|
272
|
+
// resolves the installation binding at module load) needs es2022 — Vite's
|
|
273
|
+
// default 'modules' baseline is es2020 and esbuild hard-fails TLA there.
|
|
274
|
+
// Dev already transforms at esnext, so this only aligns the prod build.
|
|
275
|
+
target: "es2022",
|
|
270
276
|
},
|
|
271
277
|
server: {
|
|
272
278
|
// Allow the sandboxed null-origin iframe used by \`lotics app dev\` to
|