@lotics/cli 0.43.0 → 0.44.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/dist/app_commands.d.ts +6 -3
- package/dist/generate_app_workflows_dts.d.ts +7 -0
- package/dist/generate_app_workflows_dts.js +79 -7
- package/dist/generate_app_workflows_dts.test.d.ts +1 -0
- package/dist/generate_app_workflows_dts.test.js +47 -0
- package/dist/src/cli.js +88 -12
- package/dist/starter_template.d.ts +1 -1
- package/dist/starter_template.js +28 -8
- package/dist/starter_template.test.js +27 -6
- package/package.json +1 -1
package/dist/app_commands.d.ts
CHANGED
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
import { LoticsClient } from "./client.js";
|
|
2
2
|
/**
|
|
3
3
|
* Manifest declaration for one workflow alias:
|
|
4
|
-
* `"alias": { workflow_id, inputs?: { key: { type, … } } }`
|
|
4
|
+
* `"alias": { workflow_id, inputs?: { key: { type, … } }, outputs?: { key: { type, … } } }`
|
|
5
5
|
*
|
|
6
6
|
* When `inputs` is declared, the CLI codegen emits a typed `AppWorkflows[alias]`
|
|
7
|
-
* entry and the server validates payloads against it at execute time.
|
|
8
|
-
* `
|
|
7
|
+
* entry and the server validates payloads against it at execute time. When
|
|
8
|
+
* `outputs` is declared, codegen emits a typed `AppWorkflowResults[alias]` so the
|
|
9
|
+
* app reads a typed `result.data`. Omit either for workflows that take no typed
|
|
10
|
+
* inputs / return no structured data.
|
|
9
11
|
*/
|
|
10
12
|
export type AppWorkflowDeclaration = {
|
|
11
13
|
workflow_id: string;
|
|
12
14
|
inputs?: Record<string, unknown>;
|
|
15
|
+
outputs?: Record<string, unknown>;
|
|
13
16
|
};
|
|
14
17
|
/**
|
|
15
18
|
* Manifest declaration for one named query:
|
|
@@ -27,4 +27,11 @@ export declare function generateAppWorkflowsDts(workflows: Record<string, AppWor
|
|
|
27
27
|
* catches structurally-invalid declarations before they reach us.
|
|
28
28
|
*/
|
|
29
29
|
export declare function inputsToType(inputs: Record<string, unknown>): string;
|
|
30
|
+
/**
|
|
31
|
+
* Map an output `fields` map (also the top-level `outputs`) to a TS object type.
|
|
32
|
+
* Mirrors the backend `outputObjectToTsType`; recursive for nested object/array.
|
|
33
|
+
* Zero-dep like `inputsToType` — pattern-matches on `type`, falls through to
|
|
34
|
+
* `unknown` for shapes the server's schema parse rejects before we see them.
|
|
35
|
+
*/
|
|
36
|
+
export declare function objectFieldsToType(fields: Record<string, unknown>): string;
|
|
30
37
|
export declare function isValidIdentifier(name: string): boolean;
|
|
@@ -34,22 +34,32 @@ declare module "@lotics/app-sdk" {
|
|
|
34
34
|
}
|
|
35
35
|
// Sort alphabetically for deterministic output across regenerations.
|
|
36
36
|
entries.sort(([a], [b]) => a.localeCompare(b));
|
|
37
|
-
const
|
|
37
|
+
const inputLines = [];
|
|
38
|
+
const resultLines = [];
|
|
38
39
|
for (const [alias, declaration] of entries) {
|
|
39
|
-
// declaration is
|
|
40
|
-
//
|
|
41
|
-
// `
|
|
40
|
+
// declaration is `{ workflow_id, inputs?, outputs? }` per manifest schema.
|
|
41
|
+
// No typed inputs ⇒ untyped `Record<string, unknown>` callable. An alias with
|
|
42
|
+
// declared `outputs` also gets an `AppWorkflowResults` entry → typed result.data.
|
|
42
43
|
const valueType = declaration.inputs
|
|
43
44
|
? inputsToType(declaration.inputs)
|
|
44
45
|
: "Record<string, unknown>";
|
|
45
46
|
const aliasKey = isValidIdentifier(alias) ? alias : JSON.stringify(alias);
|
|
46
|
-
|
|
47
|
+
inputLines.push(` ${aliasKey}: ${valueType};`);
|
|
48
|
+
if (declaration.outputs) {
|
|
49
|
+
resultLines.push(` ${aliasKey}: ${objectFieldsToType(declaration.outputs)};`);
|
|
50
|
+
}
|
|
47
51
|
}
|
|
52
|
+
const resultsBlock = resultLines.length > 0
|
|
53
|
+
? `
|
|
54
|
+
interface AppWorkflowResults {
|
|
55
|
+
${resultLines.join("\n")}
|
|
56
|
+
}`
|
|
57
|
+
: "";
|
|
48
58
|
return `${HEADER}
|
|
49
59
|
declare module "@lotics/app-sdk" {
|
|
50
60
|
interface AppWorkflows {
|
|
51
|
-
${
|
|
52
|
-
}
|
|
61
|
+
${inputLines.join("\n")}
|
|
62
|
+
}${resultsBlock}
|
|
53
63
|
}
|
|
54
64
|
`;
|
|
55
65
|
}
|
|
@@ -119,6 +129,68 @@ function inputDeclToTsType(decl) {
|
|
|
119
129
|
return "unknown";
|
|
120
130
|
}
|
|
121
131
|
}
|
|
132
|
+
/**
|
|
133
|
+
* Map an output `fields` map (also the top-level `outputs`) to a TS object type.
|
|
134
|
+
* Mirrors the backend `outputObjectToTsType`; recursive for nested object/array.
|
|
135
|
+
* Zero-dep like `inputsToType` — pattern-matches on `type`, falls through to
|
|
136
|
+
* `unknown` for shapes the server's schema parse rejects before we see them.
|
|
137
|
+
*/
|
|
138
|
+
export function objectFieldsToType(fields) {
|
|
139
|
+
const parts = [];
|
|
140
|
+
for (const [key, decl] of Object.entries(fields)) {
|
|
141
|
+
if (decl === null || typeof decl !== "object")
|
|
142
|
+
continue;
|
|
143
|
+
const d = decl;
|
|
144
|
+
const optional = d.required === false ? "?" : "";
|
|
145
|
+
const fieldKey = isValidIdentifier(key) ? key : JSON.stringify(key);
|
|
146
|
+
parts.push(`${fieldKey}${optional}: ${outputDeclToTsType(d)}`);
|
|
147
|
+
}
|
|
148
|
+
if (parts.length === 0)
|
|
149
|
+
return "Record<string, never>";
|
|
150
|
+
return `{ ${parts.join("; ")} }`;
|
|
151
|
+
}
|
|
152
|
+
function outputDeclToTsType(decl) {
|
|
153
|
+
const type = decl.type;
|
|
154
|
+
switch (type) {
|
|
155
|
+
case "text":
|
|
156
|
+
case "email":
|
|
157
|
+
case "date":
|
|
158
|
+
case "datetime":
|
|
159
|
+
return "string";
|
|
160
|
+
case "number":
|
|
161
|
+
return "number";
|
|
162
|
+
case "boolean":
|
|
163
|
+
return "boolean";
|
|
164
|
+
case "record_link":
|
|
165
|
+
return decl.multi === true ? "ReadonlyArray<string>" : "string";
|
|
166
|
+
case "select": {
|
|
167
|
+
const options = Array.isArray(decl.options) ? decl.options : [];
|
|
168
|
+
const literals = options
|
|
169
|
+
.map((o) => o !== null && typeof o === "object" && "value" in o && typeof o.value === "string"
|
|
170
|
+
? JSON.stringify(o.value)
|
|
171
|
+
: null)
|
|
172
|
+
.filter((v) => v !== null);
|
|
173
|
+
const inner = literals.length > 0 ? literals.join(" | ") : "string";
|
|
174
|
+
return decl.multi === true ? `ReadonlyArray<${inner}>` : inner;
|
|
175
|
+
}
|
|
176
|
+
case "object": {
|
|
177
|
+
const fields = decl.fields !== null && typeof decl.fields === "object"
|
|
178
|
+
? decl.fields
|
|
179
|
+
: {};
|
|
180
|
+
return objectFieldsToType(fields);
|
|
181
|
+
}
|
|
182
|
+
case "array": {
|
|
183
|
+
const items = decl.items !== null && typeof decl.items === "object"
|
|
184
|
+
? decl.items
|
|
185
|
+
: null;
|
|
186
|
+
return items ? `ReadonlyArray<${outputDeclToTsType(items)}>` : "ReadonlyArray<unknown>";
|
|
187
|
+
}
|
|
188
|
+
case "json":
|
|
189
|
+
return "unknown";
|
|
190
|
+
default:
|
|
191
|
+
return "unknown";
|
|
192
|
+
}
|
|
193
|
+
}
|
|
122
194
|
const IDENTIFIER_REGEX = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/;
|
|
123
195
|
export function isValidIdentifier(name) {
|
|
124
196
|
return IDENTIFIER_REGEX.test(name);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { generateAppWorkflowsDts } from "./generate_app_workflows_dts.js";
|
|
3
|
+
describe("generateAppWorkflowsDts", () => {
|
|
4
|
+
it("emits no AppWorkflowResults block when no alias declares outputs", () => {
|
|
5
|
+
const dts = generateAppWorkflowsDts({
|
|
6
|
+
issue: { workflow_id: "wfl_1", inputs: { record_id: { type: "text" } } },
|
|
7
|
+
});
|
|
8
|
+
expect(dts).toContain("interface AppWorkflows {");
|
|
9
|
+
expect(dts).toContain("issue: {\n record_id: string;\n };");
|
|
10
|
+
expect(dts).not.toContain("AppWorkflowResults");
|
|
11
|
+
});
|
|
12
|
+
it("emits a typed AppWorkflowResults entry for an alias with a nested outputs schema", () => {
|
|
13
|
+
const dts = generateAppWorkflowsDts({
|
|
14
|
+
computeQuote: {
|
|
15
|
+
workflow_id: "wfl_2",
|
|
16
|
+
outputs: {
|
|
17
|
+
total: { type: "number" },
|
|
18
|
+
note: { type: "text", required: false },
|
|
19
|
+
lines: {
|
|
20
|
+
type: "array",
|
|
21
|
+
items: {
|
|
22
|
+
type: "object",
|
|
23
|
+
fields: { name: { type: "text" }, amount: { type: "number" } },
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
expect(dts).toContain("interface AppWorkflowResults {");
|
|
30
|
+
// Nested array-of-object + optional field render correctly.
|
|
31
|
+
expect(dts).toContain("computeQuote: { total: number; note?: string; lines: ReadonlyArray<{ name: string; amount: number }> };");
|
|
32
|
+
// No declared inputs → untyped callable in AppWorkflows.
|
|
33
|
+
expect(dts).toContain("computeQuote: Record<string, unknown>;");
|
|
34
|
+
});
|
|
35
|
+
it("maps record_link/select outputs the same way as inputs", () => {
|
|
36
|
+
const dts = generateAppWorkflowsDts({
|
|
37
|
+
lookup: {
|
|
38
|
+
workflow_id: "wfl_3",
|
|
39
|
+
outputs: {
|
|
40
|
+
owner: { type: "record_link", table_id: "tbl_x" },
|
|
41
|
+
tags: { type: "select", options: [{ label: "A", value: "a" }, { label: "B", value: "b" }], multi: true },
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
expect(dts).toContain('owner: string; tags: ReadonlyArray<"a" | "b">');
|
|
46
|
+
});
|
|
47
|
+
});
|
package/dist/src/cli.js
CHANGED
|
@@ -30179,7 +30179,7 @@ import { spawn as spawn2 } from "node:child_process";
|
|
|
30179
30179
|
import { tmpdir } from "node:os";
|
|
30180
30180
|
|
|
30181
30181
|
// src/starter_template.ts
|
|
30182
|
-
var STARTER_FALLBACK_UI_VERSION = "
|
|
30182
|
+
var STARTER_FALLBACK_UI_VERSION = "2.0.0";
|
|
30183
30183
|
var STARTER_FALLBACK_SDK_VERSION = "0.11.0";
|
|
30184
30184
|
var STARTER_REACT_NATIVE_VERSION = "0.85.3";
|
|
30185
30185
|
function buildStarterTemplate(args) {
|
|
@@ -30322,13 +30322,14 @@ export default defineConfig({
|
|
|
30322
30322
|
dedupe: ["react", "react-dom", "react-native-web"],
|
|
30323
30323
|
},
|
|
30324
30324
|
optimizeDeps: {
|
|
30325
|
-
//
|
|
30326
|
-
//
|
|
30327
|
-
// server
|
|
30328
|
-
//
|
|
30329
|
-
//
|
|
30330
|
-
//
|
|
30331
|
-
|
|
30325
|
+
// Force-prebundle react-native-web: it does \`import normalizeColor from
|
|
30326
|
+
// "@react-native/normalize-colors"\` (a nested CJS module). Un-prebundled,
|
|
30327
|
+
// Vite's dev server serves that as ESM with no default export \u2192 "does not
|
|
30328
|
+
// provide an export named 'default'" and a blank iframe the moment a
|
|
30329
|
+
// colour-touching RN-Web component (DatePicker, Picker) mounts under
|
|
30330
|
+
// \`lotics app dev\`. Prebundling folds its CJS deps into one interop'd chunk.
|
|
30331
|
+
// The rollup build handles the interop on its own, so this is dev-only.
|
|
30332
|
+
include: ["react-native-web"],
|
|
30332
30333
|
// The dep optimizer pre-bundles deps with a SEPARATE esbuild pass that
|
|
30333
30334
|
// top-level \`define\` doesn't always reach, so a pre-bundled RN dep can
|
|
30334
30335
|
// still hit \`__DEV__ is not defined\` under \`lotics app dev\`. Define it
|
|
@@ -30359,6 +30360,25 @@ export default defineConfig({
|
|
|
30359
30360
|
},
|
|
30360
30361
|
test: {
|
|
30361
30362
|
environment: "jsdom",
|
|
30363
|
+
// RN packages ship Flow (\`import typeof\`) in their native source, reached
|
|
30364
|
+
// transitively by RN-Web components (pickers, calendars, anything touching
|
|
30365
|
+
// Animated). Vitest's web optimizer is OFF by default and ignores the
|
|
30366
|
+
// \`optimizeDeps.resolveExtensions\` above, so it pre-bundles the native
|
|
30367
|
+
// \`.js\` / \`src\` Flow files and esbuild can't parse them. Enable it with the
|
|
30368
|
+
// same \`.web.js\`-first resolution so it bundles the compiled web variants
|
|
30369
|
+
// (Picker.web.js, RN-Web dist) \u2014 without this, any test that renders a tree
|
|
30370
|
+
// pulling a Picker / DatePicker fails with "Unexpected token 'typeof'".
|
|
30371
|
+
deps: {
|
|
30372
|
+
optimizer: {
|
|
30373
|
+
web: {
|
|
30374
|
+
enabled: true,
|
|
30375
|
+
include: ["react-native", "react-native-web", "@react-native-picker/picker"],
|
|
30376
|
+
esbuildOptions: {
|
|
30377
|
+
resolveExtensions: [".web.tsx", ".web.ts", ".web.js", ".tsx", ".ts", ".jsx", ".js", ".json"],
|
|
30378
|
+
},
|
|
30379
|
+
},
|
|
30380
|
+
},
|
|
30381
|
+
},
|
|
30362
30382
|
},
|
|
30363
30383
|
});
|
|
30364
30384
|
`
|
|
@@ -31085,17 +31105,25 @@ declare module "@lotics/app-sdk" {
|
|
|
31085
31105
|
`;
|
|
31086
31106
|
}
|
|
31087
31107
|
entries.sort(([a], [b]) => a.localeCompare(b));
|
|
31088
|
-
const
|
|
31108
|
+
const inputLines = [];
|
|
31109
|
+
const resultLines = [];
|
|
31089
31110
|
for (const [alias, declaration] of entries) {
|
|
31090
31111
|
const valueType = declaration.inputs ? inputsToType(declaration.inputs) : "Record<string, unknown>";
|
|
31091
31112
|
const aliasKey = isValidIdentifier(alias) ? alias : JSON.stringify(alias);
|
|
31092
|
-
|
|
31113
|
+
inputLines.push(` ${aliasKey}: ${valueType};`);
|
|
31114
|
+
if (declaration.outputs) {
|
|
31115
|
+
resultLines.push(` ${aliasKey}: ${objectFieldsToType(declaration.outputs)};`);
|
|
31116
|
+
}
|
|
31093
31117
|
}
|
|
31118
|
+
const resultsBlock = resultLines.length > 0 ? `
|
|
31119
|
+
interface AppWorkflowResults {
|
|
31120
|
+
${resultLines.join("\n")}
|
|
31121
|
+
}` : "";
|
|
31094
31122
|
return `${HEADER}
|
|
31095
31123
|
declare module "@lotics/app-sdk" {
|
|
31096
31124
|
interface AppWorkflows {
|
|
31097
|
-
${
|
|
31098
|
-
}
|
|
31125
|
+
${inputLines.join("\n")}
|
|
31126
|
+
}${resultsBlock}
|
|
31099
31127
|
}
|
|
31100
31128
|
`;
|
|
31101
31129
|
}
|
|
@@ -31152,6 +31180,54 @@ function inputDeclToTsType(decl) {
|
|
|
31152
31180
|
return "unknown";
|
|
31153
31181
|
}
|
|
31154
31182
|
}
|
|
31183
|
+
function objectFieldsToType(fields) {
|
|
31184
|
+
const parts = [];
|
|
31185
|
+
for (const [key, decl] of Object.entries(fields)) {
|
|
31186
|
+
if (decl === null || typeof decl !== "object") continue;
|
|
31187
|
+
const d = decl;
|
|
31188
|
+
const optional = d.required === false ? "?" : "";
|
|
31189
|
+
const fieldKey = isValidIdentifier(key) ? key : JSON.stringify(key);
|
|
31190
|
+
parts.push(`${fieldKey}${optional}: ${outputDeclToTsType(d)}`);
|
|
31191
|
+
}
|
|
31192
|
+
if (parts.length === 0) return "Record<string, never>";
|
|
31193
|
+
return `{ ${parts.join("; ")} }`;
|
|
31194
|
+
}
|
|
31195
|
+
function outputDeclToTsType(decl) {
|
|
31196
|
+
const type = decl.type;
|
|
31197
|
+
switch (type) {
|
|
31198
|
+
case "text":
|
|
31199
|
+
case "email":
|
|
31200
|
+
case "date":
|
|
31201
|
+
case "datetime":
|
|
31202
|
+
return "string";
|
|
31203
|
+
case "number":
|
|
31204
|
+
return "number";
|
|
31205
|
+
case "boolean":
|
|
31206
|
+
return "boolean";
|
|
31207
|
+
case "record_link":
|
|
31208
|
+
return decl.multi === true ? "ReadonlyArray<string>" : "string";
|
|
31209
|
+
case "select": {
|
|
31210
|
+
const options = Array.isArray(decl.options) ? decl.options : [];
|
|
31211
|
+
const literals = options.map(
|
|
31212
|
+
(o) => o !== null && typeof o === "object" && "value" in o && typeof o.value === "string" ? JSON.stringify(o.value) : null
|
|
31213
|
+
).filter((v) => v !== null);
|
|
31214
|
+
const inner = literals.length > 0 ? literals.join(" | ") : "string";
|
|
31215
|
+
return decl.multi === true ? `ReadonlyArray<${inner}>` : inner;
|
|
31216
|
+
}
|
|
31217
|
+
case "object": {
|
|
31218
|
+
const fields = decl.fields !== null && typeof decl.fields === "object" ? decl.fields : {};
|
|
31219
|
+
return objectFieldsToType(fields);
|
|
31220
|
+
}
|
|
31221
|
+
case "array": {
|
|
31222
|
+
const items = decl.items !== null && typeof decl.items === "object" ? decl.items : null;
|
|
31223
|
+
return items ? `ReadonlyArray<${outputDeclToTsType(items)}>` : "ReadonlyArray<unknown>";
|
|
31224
|
+
}
|
|
31225
|
+
case "json":
|
|
31226
|
+
return "unknown";
|
|
31227
|
+
default:
|
|
31228
|
+
return "unknown";
|
|
31229
|
+
}
|
|
31230
|
+
}
|
|
31155
31231
|
var IDENTIFIER_REGEX = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/;
|
|
31156
31232
|
function isValidIdentifier(name) {
|
|
31157
31233
|
return IDENTIFIER_REGEX.test(name);
|
|
@@ -39,7 +39,7 @@ export interface StarterFile {
|
|
|
39
39
|
* scaffolds resolve the live version via `fetchLatestNpmVersion` and only
|
|
40
40
|
* fall back here when the lookup fails.
|
|
41
41
|
*/
|
|
42
|
-
export declare const STARTER_FALLBACK_UI_VERSION = "
|
|
42
|
+
export declare const STARTER_FALLBACK_UI_VERSION = "2.0.0";
|
|
43
43
|
export declare const STARTER_FALLBACK_SDK_VERSION = "0.11.0";
|
|
44
44
|
/**
|
|
45
45
|
* react-native pin for scaffolded apps. Matches the monorepo frontend's pin so
|
package/dist/starter_template.js
CHANGED
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
* scaffolds resolve the live version via `fetchLatestNpmVersion` and only
|
|
36
36
|
* fall back here when the lookup fails.
|
|
37
37
|
*/
|
|
38
|
-
export const STARTER_FALLBACK_UI_VERSION = "
|
|
38
|
+
export const STARTER_FALLBACK_UI_VERSION = "2.0.0";
|
|
39
39
|
export const STARTER_FALLBACK_SDK_VERSION = "0.11.0";
|
|
40
40
|
/**
|
|
41
41
|
* react-native pin for scaffolded apps. Matches the monorepo frontend's pin so
|
|
@@ -184,13 +184,14 @@ export default defineConfig({
|
|
|
184
184
|
dedupe: ["react", "react-dom", "react-native-web"],
|
|
185
185
|
},
|
|
186
186
|
optimizeDeps: {
|
|
187
|
-
//
|
|
188
|
-
//
|
|
189
|
-
// server
|
|
190
|
-
//
|
|
191
|
-
//
|
|
192
|
-
//
|
|
193
|
-
|
|
187
|
+
// Force-prebundle react-native-web: it does \`import normalizeColor from
|
|
188
|
+
// "@react-native/normalize-colors"\` (a nested CJS module). Un-prebundled,
|
|
189
|
+
// Vite's dev server serves that as ESM with no default export → "does not
|
|
190
|
+
// provide an export named 'default'" and a blank iframe the moment a
|
|
191
|
+
// colour-touching RN-Web component (DatePicker, Picker) mounts under
|
|
192
|
+
// \`lotics app dev\`. Prebundling folds its CJS deps into one interop'd chunk.
|
|
193
|
+
// The rollup build handles the interop on its own, so this is dev-only.
|
|
194
|
+
include: ["react-native-web"],
|
|
194
195
|
// The dep optimizer pre-bundles deps with a SEPARATE esbuild pass that
|
|
195
196
|
// top-level \`define\` doesn't always reach, so a pre-bundled RN dep can
|
|
196
197
|
// still hit \`__DEV__ is not defined\` under \`lotics app dev\`. Define it
|
|
@@ -221,6 +222,25 @@ export default defineConfig({
|
|
|
221
222
|
},
|
|
222
223
|
test: {
|
|
223
224
|
environment: "jsdom",
|
|
225
|
+
// RN packages ship Flow (\`import typeof\`) in their native source, reached
|
|
226
|
+
// transitively by RN-Web components (pickers, calendars, anything touching
|
|
227
|
+
// Animated). Vitest's web optimizer is OFF by default and ignores the
|
|
228
|
+
// \`optimizeDeps.resolveExtensions\` above, so it pre-bundles the native
|
|
229
|
+
// \`.js\` / \`src\` Flow files and esbuild can't parse them. Enable it with the
|
|
230
|
+
// same \`.web.js\`-first resolution so it bundles the compiled web variants
|
|
231
|
+
// (Picker.web.js, RN-Web dist) — without this, any test that renders a tree
|
|
232
|
+
// pulling a Picker / DatePicker fails with "Unexpected token 'typeof'".
|
|
233
|
+
deps: {
|
|
234
|
+
optimizer: {
|
|
235
|
+
web: {
|
|
236
|
+
enabled: true,
|
|
237
|
+
include: ["react-native", "react-native-web", "@react-native-picker/picker"],
|
|
238
|
+
esbuildOptions: {
|
|
239
|
+
resolveExtensions: [".web.tsx", ".web.ts", ".web.js", ".tsx", ".ts", ".jsx", ".js", ".json"],
|
|
240
|
+
},
|
|
241
|
+
},
|
|
242
|
+
},
|
|
243
|
+
},
|
|
224
244
|
},
|
|
225
245
|
});
|
|
226
246
|
`,
|
|
@@ -8,14 +8,23 @@ function fileNamed(files, path) {
|
|
|
8
8
|
}
|
|
9
9
|
describe("buildStarterTemplate", () => {
|
|
10
10
|
const baseArgs = { app_name: "Demo", app_id: "app_test", workspace_id: "wsp_test" };
|
|
11
|
-
test("vite.config.ts pre-bundles
|
|
11
|
+
test("vite.config.ts pre-bundles RN deps with __DEV__ + .web.js for the dev server", () => {
|
|
12
12
|
const config = fileNamed(buildStarterTemplate(baseArgs), "vite.config.ts");
|
|
13
|
-
//
|
|
14
|
-
//
|
|
13
|
+
// The optimizer runs a SEPARATE esbuild pass that ignores `resolve.extensions`
|
|
14
|
+
// and the top-level `define`, so `__DEV__` + `.web.js` must be repeated here —
|
|
15
|
+
// else a pre-bundled RN dep (e.g. react-native-svg, which @lotics/ui charts
|
|
16
|
+
// use) resolves to its native build and `__DEV__` is undefined.
|
|
15
17
|
expect(config).toMatch(/optimizeDeps:\s*\{/);
|
|
16
|
-
expect(config).toContain('"
|
|
17
|
-
expect(config).toContain('"
|
|
18
|
-
|
|
18
|
+
expect(config).toContain('__DEV__: "false"');
|
|
19
|
+
expect(config).toContain('".web.js"');
|
|
20
|
+
});
|
|
21
|
+
test("vite.config.ts force-prebundles react-native-web (else a DatePicker/Picker blanks the dev iframe)", () => {
|
|
22
|
+
// RN-Web does `import normalizeColor from "@react-native/normalize-colors"`
|
|
23
|
+
// (nested CJS). Un-prebundled, the dev server serves it as ESM with no
|
|
24
|
+
// default → "does not provide an export named 'default'" + blank iframe the
|
|
25
|
+
// moment a colour-touching component mounts. Prebundling interops it.
|
|
26
|
+
const config = fileNamed(buildStarterTemplate(baseArgs), "vite.config.ts");
|
|
27
|
+
expect(config).toMatch(/include:\s*\[[^\]]*"react-native-web"/);
|
|
19
28
|
});
|
|
20
29
|
test("vite.config.ts defines the RN __DEV__ global (else the bundle throws at load → blank iframe)", () => {
|
|
21
30
|
// `__DEV__` is a Metro-injected global that RN-ecosystem deps reference at
|
|
@@ -30,6 +39,18 @@ describe("buildStarterTemplate", () => {
|
|
|
30
39
|
// deps under `lotics app dev`).
|
|
31
40
|
expect(config).toMatch(/esbuildOptions:\s*\{\s*define:\s*\{\s*__DEV__:\s*"false"/);
|
|
32
41
|
});
|
|
42
|
+
test("vite.config.ts enables the vitest web optimizer with .web.js resolution (else tests rendering a Picker/DatePicker fail on Flow)", () => {
|
|
43
|
+
// Vitest pre-bundles deps with its OWN esbuild optimizer, which is OFF by
|
|
44
|
+
// default and ignores the top-level `optimizeDeps.resolveExtensions`. Without
|
|
45
|
+
// it enabled + given `.web.js` first, it grabs react-native's native `.js` /
|
|
46
|
+
// src Flow files (`import typeof`) that esbuild can't parse — so any test that
|
|
47
|
+
// renders a tree pulling a Picker/DatePicker dies with "Unexpected token 'typeof'".
|
|
48
|
+
const config = fileNamed(buildStarterTemplate(baseArgs), "vite.config.ts");
|
|
49
|
+
expect(config).toMatch(/test:\s*\{[\s\S]*environment:\s*"jsdom"/);
|
|
50
|
+
expect(config).toMatch(/optimizer:\s*\{\s*web:\s*\{\s*enabled:\s*true/);
|
|
51
|
+
// The web optimizer carries the same `.web.js`-first resolution as the build.
|
|
52
|
+
expect(config).toMatch(/web:\s*\{[\s\S]*resolveExtensions:[\s\S]*"\.web\.js"/);
|
|
53
|
+
});
|
|
33
54
|
test("vite.config.ts maps the RN `global` to globalThis (else rn-web Animated throws when a spring is torn down)", () => {
|
|
34
55
|
// react-native-web's Animated reads `global.cancelAnimationFrame` (free var)
|
|
35
56
|
// when a spring animation is interrupted — e.g. toggling @lotics/ui's Switch.
|