rastack 0.0.58 → 0.0.60
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/CHANGELOG.md +16 -0
- package/README.md +190 -0
- package/dist/deploy/ci.js +2 -2
- package/dist/deploy/io.js +2 -2
- package/dist/dev/harness.d.ts +1 -1
- package/dist/dev/harness.js +1 -2
- package/dist/external/cache.d.ts +28 -0
- package/dist/external/cache.js +35 -0
- package/dist/external/envelope.d.ts +35 -0
- package/dist/external/envelope.js +48 -0
- package/dist/external/graphql.d.ts +25 -0
- package/dist/external/graphql.js +78 -0
- package/dist/external/handler.d.ts +52 -0
- package/dist/external/handler.js +139 -0
- package/dist/external/index.d.ts +19 -0
- package/dist/external/index.js +38 -0
- package/dist/external/lambda.d.ts +49 -0
- package/dist/external/lambda.js +86 -0
- package/dist/external/rest.d.ts +17 -0
- package/dist/external/rest.js +46 -0
- package/dist/external/route.d.ts +12 -0
- package/dist/external/route.js +33 -0
- package/dist/external/service.d.ts +50 -0
- package/dist/external/service.js +59 -0
- package/dist/external/types.d.ts +107 -0
- package/dist/external/types.js +22 -0
- package/dist/external/util.d.ts +16 -0
- package/dist/external/util.js +58 -0
- package/dist/rastack-dev.js +16 -1
- package/dist/rastack-wasm-build.d.ts +1 -1
- package/dist/rastack-wasm-build.js +6 -6
- package/dist/wasm/rastack_wasm_bg.wasm +0 -0
- package/external.ts +10 -0
- package/hooks/query/delete.ts +12 -11
- package/hooks/query/fetch.ts +6 -8
- package/hooks/query/interfaces.ts +1 -4
- package/hooks/query/list.ts +16 -9
- package/hooks/query/update.ts +8 -6
- package/package.json +24 -2
- package/scripts/copy-templates.js +1 -1
- package/scripts/copy-wasm.js +1 -1
- package/scripts/dev-install.mjs +63 -0
- package/src/deploy/ci.ts +2 -2
- package/src/deploy/io.ts +2 -2
- package/src/dev/harness.ts +1 -2
- package/src/external/cache.ts +53 -0
- package/src/external/envelope.ts +78 -0
- package/src/external/graphql.ts +98 -0
- package/src/external/handler.ts +207 -0
- package/src/external/index.ts +25 -0
- package/src/external/lambda.ts +108 -0
- package/src/external/rest.ts +60 -0
- package/src/external/route.ts +33 -0
- package/src/external/service.ts +86 -0
- package/src/external/types.ts +121 -0
- package/src/external/util.ts +57 -0
- package/src/rastack-dev.ts +17 -1
- package/src/rastack-wasm-build.ts +6 -6
- package/jest.config.cjs +0 -12
- package/test/auth.spec.ts +0 -207
- package/test/cache.spec.ts +0 -263
- package/test/cognito.spec.ts +0 -281
- package/test/compile.spec.ts +0 -240
- package/test/components.spec.ts +0 -524
- package/test/csv-schema.spec.ts +0 -143
- package/test/deploy.spec.ts +0 -571
- package/test/dev.spec.ts +0 -414
- package/test/entities.spec.ts +0 -597
- package/test/import.spec.ts +0 -241
- package/test/plugin.spec.ts +0 -315
- package/test/runtime-manifest.spec.ts +0 -309
- package/test/schema-entities.spec.ts +0 -688
- package/test/tokens.spec.ts +0 -302
- package/test/transitions.spec.ts +0 -372
- package/test/typed-hooks.spec.ts +0 -412
- package/test/update.spec.ts +0 -152
- package/test/validate.spec.ts +0 -319
- package/tsconfig.json +0 -27
- package/wasm/package.json +0 -4
- package/wasm/rastack_wasm.d.ts +0 -100
- package/wasm/rastack_wasm.js +0 -745
- package/wasm/rastack_wasm_bg.wasm +0 -0
- package/wasm/rastack_wasm_bg.wasm.d.ts +0 -28
package/test/typed-hooks.spec.ts
DELETED
|
@@ -1,412 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The pure cores behind `useData(Entity)` / `useForm(Entity)`: resolving a
|
|
3
|
-
* plain TypeScript type to its manifest resource, deriving the write schema
|
|
4
|
-
* (and its form field structures) from the manifest at runtime, and the
|
|
5
|
-
* type-level transition names. The React shells around these are thin; the
|
|
6
|
-
* decisions all live here.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { mkdirSync, readFileSync, rmSync, writeFileSync, existsSync } from "fs";
|
|
10
|
-
import { join } from "path";
|
|
11
|
-
import {
|
|
12
|
-
entityModelName,
|
|
13
|
-
humanizeName,
|
|
14
|
-
isEntityRef,
|
|
15
|
-
resolveEntityRef,
|
|
16
|
-
} from "../hooks/entity";
|
|
17
|
-
import { useData } from "../hooks/data";
|
|
18
|
-
import type { RowForKey } from "../hooks/registry";
|
|
19
|
-
import { compile } from "../src/compile";
|
|
20
|
-
import { initialValuesFromResource } from "../hooks/form/entity-form";
|
|
21
|
-
import { getFieldMachine, getFieldStructure } from "../hooks/form/structure";
|
|
22
|
-
import { writeSchema } from "../src/compile/openapi";
|
|
23
|
-
import type { Manifest, ResourceModel } from "../src/compile/model";
|
|
24
|
-
import { resource, s, type TransitionNamesOf } from "../src/define";
|
|
25
|
-
import { runFieldMachine } from "../src/validate";
|
|
26
|
-
|
|
27
|
-
const airport: ResourceModel = {
|
|
28
|
-
app: "airports",
|
|
29
|
-
model: "airport",
|
|
30
|
-
fields: [
|
|
31
|
-
{ name: "code", type: "string", maxLength: 3, unique: true },
|
|
32
|
-
{ name: "name", type: "string", maxLength: 120 },
|
|
33
|
-
],
|
|
34
|
-
relations: [],
|
|
35
|
-
admin: { listDisplay: ["code", "name"] },
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
const flight: ResourceModel = {
|
|
39
|
-
app: "airports",
|
|
40
|
-
model: "flight",
|
|
41
|
-
fields: [
|
|
42
|
-
{ name: "number", type: "string", maxLength: 8 },
|
|
43
|
-
{
|
|
44
|
-
name: "status",
|
|
45
|
-
type: "string",
|
|
46
|
-
options: ["scheduled", "boarding", "departed"],
|
|
47
|
-
default: "scheduled",
|
|
48
|
-
},
|
|
49
|
-
{ name: "active", type: "bool" },
|
|
50
|
-
{ name: "airport", type: "fk", relation: { app: "airports", model: "airport" } },
|
|
51
|
-
],
|
|
52
|
-
relations: [{ field: "airport", target: "airports.airport", kind: "fk" }],
|
|
53
|
-
transitions: {
|
|
54
|
-
field: "status",
|
|
55
|
-
states: ["scheduled", "boarding", "departed"],
|
|
56
|
-
initial: "scheduled",
|
|
57
|
-
on: {
|
|
58
|
-
board: { from: ["scheduled"], to: "boarding" },
|
|
59
|
-
depart: { from: ["boarding"], to: "departed" },
|
|
60
|
-
},
|
|
61
|
-
},
|
|
62
|
-
};
|
|
63
|
-
|
|
64
|
-
const manifest: Manifest = { version: 1, resources: [airport, flight] };
|
|
65
|
-
|
|
66
|
-
describe("resolveEntityRef — a type names its resource", () => {
|
|
67
|
-
it("resolves a class by its (compiler-normalised) name", () => {
|
|
68
|
-
class Airport {
|
|
69
|
-
code!: string;
|
|
70
|
-
}
|
|
71
|
-
const resolved = resolveEntityRef(Airport, manifest);
|
|
72
|
-
expect(resolved).toMatchObject({
|
|
73
|
-
app: "airports",
|
|
74
|
-
model: "airport",
|
|
75
|
-
key: "airports.airport",
|
|
76
|
-
listUrl: "/api/airports/v1/airport/",
|
|
77
|
-
});
|
|
78
|
-
expect(resolved.detailUrl(7)).toBe("/api/airports/v1/airport/7/");
|
|
79
|
-
expect(resolved.resource).toBe(airport);
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
it("strips the generated `I` prefix, like the compiler does", () => {
|
|
83
|
-
expect(entityModelName("IFlight")).toBe("flight");
|
|
84
|
-
expect(entityModelName("Flight")).toBe("flight");
|
|
85
|
-
expect(entityModelName("Item")).toBe("item"); // `It` is not an I-prefix… but `Item` is I+tem
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
it("resolves a `resource()` value directly — manifest optional", () => {
|
|
89
|
-
const Flight = resource("airports", "flight", {
|
|
90
|
-
number: s.string({ maxLength: 8 }),
|
|
91
|
-
});
|
|
92
|
-
expect(resolveEntityRef(Flight, manifest).resource).toBe(flight);
|
|
93
|
-
// Without a manifest the value is *its own* manifest: URLs resolve and
|
|
94
|
-
// the resource model is synthesised from the definition's runtime shape.
|
|
95
|
-
const bare = resolveEntityRef(Flight);
|
|
96
|
-
expect(bare.listUrl).toBe("/api/airports/v1/flight/");
|
|
97
|
-
expect(bare.resource).toMatchObject({
|
|
98
|
-
app: "airports",
|
|
99
|
-
model: "flight",
|
|
100
|
-
fields: [{ name: "number", type: "string", maxLength: 8 }],
|
|
101
|
-
});
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
it("carries a resource()'s transitions even without a manifest", () => {
|
|
105
|
-
const Flight = resource(
|
|
106
|
-
"airports",
|
|
107
|
-
"flight",
|
|
108
|
-
{ status: s.string({ maxLength: 20 }) },
|
|
109
|
-
{
|
|
110
|
-
transitions: {
|
|
111
|
-
field: "status",
|
|
112
|
-
states: ["scheduled", "boarding"],
|
|
113
|
-
on: { board: { from: "scheduled", to: "boarding" } },
|
|
114
|
-
},
|
|
115
|
-
},
|
|
116
|
-
);
|
|
117
|
-
const resolved = resolveEntityRef(Flight);
|
|
118
|
-
expect(resolved.transitions).toMatchObject({
|
|
119
|
-
on: { board: { from: ["scheduled"], to: "boarding" } },
|
|
120
|
-
});
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
it('resolves "app.model" and bare "model" strings', () => {
|
|
124
|
-
expect(resolveEntityRef("airports.flight", manifest).resource).toBe(flight);
|
|
125
|
-
expect(resolveEntityRef("flight", manifest).model).toBe("flight");
|
|
126
|
-
expect(() => resolveEntityRef("airports.spaceship", manifest)).toThrow(
|
|
127
|
-
/no resource "airports.spaceship"/,
|
|
128
|
-
);
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
it("fails loudly on ambiguity and missing manifests", () => {
|
|
132
|
-
const two: Manifest = {
|
|
133
|
-
version: 1,
|
|
134
|
-
resources: [flight, { ...flight, app: "cargo" }],
|
|
135
|
-
};
|
|
136
|
-
expect(() => resolveEntityRef("flight", two)).toThrow(/"app.model"/);
|
|
137
|
-
class Flight {
|
|
138
|
-
number!: string;
|
|
139
|
-
}
|
|
140
|
-
expect(() => resolveEntityRef(Flight, two)).toThrow(/airports\.flight, cargo\.flight/);
|
|
141
|
-
expect(() => resolveEntityRef(Flight)).toThrow(/manifest/);
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
it("classifies entity refs vs. legacy form props", () => {
|
|
145
|
-
expect(isEntityRef("airports.flight")).toBe(true);
|
|
146
|
-
expect(isEntityRef(class A {})).toBe(true);
|
|
147
|
-
expect(isEntityRef({ app: "a", model: "b" })).toBe(true);
|
|
148
|
-
expect(isEntityRef({ formValues: {}, mutations: {} })).toBe(false);
|
|
149
|
-
});
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
describe("useForm(Entity) — the manifest-derived form core", () => {
|
|
153
|
-
it("derives form-friendly initial values from the resource", () => {
|
|
154
|
-
expect(initialValuesFromResource(flight)).toEqual({
|
|
155
|
-
number: "",
|
|
156
|
-
status: "scheduled", // the machine's initial state, via the field default
|
|
157
|
-
active: false,
|
|
158
|
-
airport: "",
|
|
159
|
-
});
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
it("derives the same write schema the OpenAPI contract publishes", () => {
|
|
163
|
-
const schema = writeSchema(flight, false);
|
|
164
|
-
expect(schema.properties.status.enum).toEqual([
|
|
165
|
-
"scheduled",
|
|
166
|
-
"boarding",
|
|
167
|
-
"departed",
|
|
168
|
-
]);
|
|
169
|
-
expect(schema.properties.airport["x-rastack-relation"]).toEqual({
|
|
170
|
-
app: "airports",
|
|
171
|
-
model: "airport",
|
|
172
|
-
});
|
|
173
|
-
expect(schema["x-rastack-transitions"]).toBe(flight.transitions);
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
it("renders the state field as a select and runs its member gate", () => {
|
|
177
|
-
const schema = writeSchema(flight, false);
|
|
178
|
-
|
|
179
|
-
const structure = getFieldStructure(schema, "status");
|
|
180
|
-
expect(structure.type).toBe("select");
|
|
181
|
-
expect(structure.options).toEqual([
|
|
182
|
-
{ label: "scheduled", value: "scheduled" },
|
|
183
|
-
{ label: "boarding", value: "boarding" },
|
|
184
|
-
{ label: "departed", value: "departed" },
|
|
185
|
-
]);
|
|
186
|
-
|
|
187
|
-
const machine = getFieldMachine(schema, "status")!;
|
|
188
|
-
expect(machine.states).toContain("member");
|
|
189
|
-
expect(runFieldMachine(machine, "boarding").state).toBe("valid");
|
|
190
|
-
expect(runFieldMachine(machine, "teleported").state).toBe("invalid");
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
it("humanises transition names into button labels", () => {
|
|
194
|
-
expect(humanizeName("board")).toBe("Board");
|
|
195
|
-
expect(humanizeName("close_out")).toBe("Close out");
|
|
196
|
-
expect(humanizeName("markShipped")).toBe("Mark shipped");
|
|
197
|
-
});
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
describe("TransitionNamesOf — transition names are literal types", () => {
|
|
201
|
-
it("reads names from a resource()'s options and a class's static block", () => {
|
|
202
|
-
const Flight = resource(
|
|
203
|
-
"airports",
|
|
204
|
-
"flight",
|
|
205
|
-
{ status: s.string({ maxLength: 20 }) },
|
|
206
|
-
{
|
|
207
|
-
transitions: {
|
|
208
|
-
field: "status",
|
|
209
|
-
states: ["scheduled", "boarding", "departed"],
|
|
210
|
-
initial: "scheduled",
|
|
211
|
-
on: {
|
|
212
|
-
board: { from: "scheduled", to: "boarding" },
|
|
213
|
-
depart: { from: "boarding", to: "departed" },
|
|
214
|
-
},
|
|
215
|
-
},
|
|
216
|
-
},
|
|
217
|
-
);
|
|
218
|
-
type FlightNames = TransitionNamesOf<typeof Flight>;
|
|
219
|
-
const boardName: FlightNames = "board";
|
|
220
|
-
const departName: FlightNames = "depart";
|
|
221
|
-
// @ts-expect-error — "explode" is not a declared transition
|
|
222
|
-
const bad: FlightNames = "explode";
|
|
223
|
-
expect([boardName, departName, bad]).toBeDefined();
|
|
224
|
-
|
|
225
|
-
class Order {
|
|
226
|
-
status = "draft";
|
|
227
|
-
static transitions = {
|
|
228
|
-
field: "status",
|
|
229
|
-
states: ["draft", "placed"],
|
|
230
|
-
initial: "draft",
|
|
231
|
-
on: { place: { from: "draft", to: "placed" } },
|
|
232
|
-
};
|
|
233
|
-
}
|
|
234
|
-
type OrderNames = TransitionNamesOf<typeof Order>;
|
|
235
|
-
const placeName: OrderNames = "place";
|
|
236
|
-
expect(placeName).toBe("place");
|
|
237
|
-
|
|
238
|
-
class Plain {
|
|
239
|
-
name!: string;
|
|
240
|
-
}
|
|
241
|
-
type None = TransitionNamesOf<typeof Plain>; // resolves to never
|
|
242
|
-
const nothing: None[] = [];
|
|
243
|
-
expect(nothing).toEqual([]);
|
|
244
|
-
});
|
|
245
|
-
});
|
|
246
|
-
|
|
247
|
-
// ---------------------------------------------------------------------------
|
|
248
|
-
// Row inference — no type argument needed
|
|
249
|
-
// ---------------------------------------------------------------------------
|
|
250
|
-
|
|
251
|
-
// What `rastack compile` emits as rastack-env.d.ts (declaration merging into
|
|
252
|
-
// the registry) — hand-rolled here so the inference is exercised in-test.
|
|
253
|
-
declare module "../hooks/registry" {
|
|
254
|
-
interface EntityTypes {
|
|
255
|
-
"airports.terminal": { id: number; label: string; airport: number };
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
describe("row inference — the type argument is not needed", () => {
|
|
260
|
-
it("a registered \"app.model\" string resolves its row type", () => {
|
|
261
|
-
type TerminalRow = RowForKey<"airports.terminal">;
|
|
262
|
-
const row: TerminalRow = { id: 1, label: "T1", airport: 2 };
|
|
263
|
-
// @ts-expect-error — `label` is required on the registered row type
|
|
264
|
-
const missing: TerminalRow = { id: 1, airport: 2 };
|
|
265
|
-
expect([row, missing]).toBeDefined();
|
|
266
|
-
});
|
|
267
|
-
|
|
268
|
-
it("a resource() value derives its row type from its fields", () => {
|
|
269
|
-
const Flight = resource("airports", "flight", {
|
|
270
|
-
number: s.string({ maxLength: 8 }),
|
|
271
|
-
seats: s.int(),
|
|
272
|
-
gate: s.string({ maxLength: 5, null: true }),
|
|
273
|
-
active: s.bool(),
|
|
274
|
-
});
|
|
275
|
-
type FlightRow = import("../src/define").RowOf<typeof Flight>;
|
|
276
|
-
const row: FlightRow = {
|
|
277
|
-
id: 1,
|
|
278
|
-
number: "BA1",
|
|
279
|
-
seats: 180,
|
|
280
|
-
gate: null, // `null: true` → `string | null`
|
|
281
|
-
active: true,
|
|
282
|
-
};
|
|
283
|
-
// @ts-expect-error — `seats` is a number
|
|
284
|
-
const wrong: FlightRow = { ...row, seats: "lots" };
|
|
285
|
-
// @ts-expect-error — `number` is not nullable
|
|
286
|
-
const wrongNull: FlightRow = { ...row, number: null };
|
|
287
|
-
expect([row, wrong, wrongNull]).toBeDefined();
|
|
288
|
-
});
|
|
289
|
-
|
|
290
|
-
// Type-level only — never executed (useData is a React hook).
|
|
291
|
-
function _typeOnly() {
|
|
292
|
-
const terminals = useData("airports.terminal");
|
|
293
|
-
const rows: Array<{ id: number; label: string; airport: number }> | undefined =
|
|
294
|
-
terminals.data;
|
|
295
|
-
// @ts-expect-error — rows are Terminal-shaped, not Airport-shaped
|
|
296
|
-
const wrong: Array<{ code: string }> | undefined = terminals.data;
|
|
297
|
-
return { rows, wrong };
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
it("keeps the explicit generic and unregistered strings working", () => {
|
|
301
|
-
expect(typeof _typeOnly).toBe("function");
|
|
302
|
-
function _fallbacks() {
|
|
303
|
-
interface IGate {
|
|
304
|
-
id: number;
|
|
305
|
-
number: string;
|
|
306
|
-
}
|
|
307
|
-
const explicit = useData<IGate>("airports.gate"); // unregistered → explicit
|
|
308
|
-
const gates: IGate[] | undefined = explicit.data;
|
|
309
|
-
const loose = useData("airports.gate"); // unregistered, no generic → any
|
|
310
|
-
const anything: undefined | Array<{ whatever: boolean }> = loose.data;
|
|
311
|
-
return { gates, anything };
|
|
312
|
-
}
|
|
313
|
-
expect(typeof _fallbacks).toBe("function");
|
|
314
|
-
});
|
|
315
|
-
});
|
|
316
|
-
|
|
317
|
-
describe("rastack compile — the emitted rastack-env.d.ts registry", () => {
|
|
318
|
-
const workRoot = join(__dirname, "registry-fixtures");
|
|
319
|
-
const resourcesDir = join(workRoot, "resources");
|
|
320
|
-
const outDir = join(workRoot, ".rastack");
|
|
321
|
-
|
|
322
|
-
beforeAll(() => {
|
|
323
|
-
mkdirSync(resourcesDir, { recursive: true });
|
|
324
|
-
writeFileSync(
|
|
325
|
-
join(resourcesDir, "entities.ts"),
|
|
326
|
-
`
|
|
327
|
-
export class Airport {
|
|
328
|
-
code!: string;
|
|
329
|
-
name!: string;
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
// Not exported — cannot be imported by the registry, so it is skipped.
|
|
333
|
-
class Secret {
|
|
334
|
-
token!: string;
|
|
335
|
-
}
|
|
336
|
-
export const _root = [Airport, Secret];
|
|
337
|
-
|
|
338
|
-
export interface Terminal {
|
|
339
|
-
id: number;
|
|
340
|
-
label: string;
|
|
341
|
-
}
|
|
342
|
-
`,
|
|
343
|
-
);
|
|
344
|
-
writeFileSync(
|
|
345
|
-
join(resourcesDir, "flights.ts"),
|
|
346
|
-
`
|
|
347
|
-
import { resource, s } from "rastack/define";
|
|
348
|
-
|
|
349
|
-
export const Flight = resource("airports", "flight", {
|
|
350
|
-
number: s.string({ maxLength: 8 }),
|
|
351
|
-
});
|
|
352
|
-
`,
|
|
353
|
-
);
|
|
354
|
-
// The dotted string argument roots the Terminal interface — no type
|
|
355
|
-
// argument, no JSDoc tag.
|
|
356
|
-
writeFileSync(
|
|
357
|
-
join(workRoot, "screen.tsx"),
|
|
358
|
-
`
|
|
359
|
-
export function Screen() {
|
|
360
|
-
const terminals = useData("entities.terminal");
|
|
361
|
-
return terminals;
|
|
362
|
-
}
|
|
363
|
-
declare function useData(key: string): unknown;
|
|
364
|
-
`,
|
|
365
|
-
);
|
|
366
|
-
});
|
|
367
|
-
|
|
368
|
-
afterAll(() => {
|
|
369
|
-
rmSync(workRoot, { recursive: true, force: true });
|
|
370
|
-
});
|
|
371
|
-
|
|
372
|
-
it("roots interfaces from dotted string hook arguments and registers every exported entity", () => {
|
|
373
|
-
const result = compile(workRoot, outDir);
|
|
374
|
-
|
|
375
|
-
// The string argument rooted the interface into the manifest…
|
|
376
|
-
expect(
|
|
377
|
-
result.manifest.resources.some(
|
|
378
|
-
(r) => r.app === "entities" && r.model === "terminal",
|
|
379
|
-
),
|
|
380
|
-
).toBe(true);
|
|
381
|
-
|
|
382
|
-
// …and the registry maps keys to the caller's own types.
|
|
383
|
-
const envPath = join(outDir, "rastack-env.d.ts");
|
|
384
|
-
expect(result.files).toContain(envPath);
|
|
385
|
-
const dts = readFileSync(envPath, "utf8");
|
|
386
|
-
expect(dts).toContain('declare module "rastack/hooks/registry"');
|
|
387
|
-
expect(dts).toContain(
|
|
388
|
-
'"entities.airport": import("../resources/entities").Airport;',
|
|
389
|
-
);
|
|
390
|
-
expect(dts).toContain(
|
|
391
|
-
'"entities.terminal": import("../resources/entities").Terminal;',
|
|
392
|
-
);
|
|
393
|
-
// DSL consts register through RowOf so string refs infer for them too.
|
|
394
|
-
expect(dts).toContain(
|
|
395
|
-
'"airports.flight": import("rastack/define").RowOf<typeof import("../resources/flights").Flight>;',
|
|
396
|
-
);
|
|
397
|
-
// Unexported declarations are skipped.
|
|
398
|
-
expect(dts).not.toContain("Secret");
|
|
399
|
-
});
|
|
400
|
-
|
|
401
|
-
it("emits no registry when nothing is exported", () => {
|
|
402
|
-
const emptyDir = join(workRoot, "empty");
|
|
403
|
-
mkdirSync(join(emptyDir, "src"), { recursive: true });
|
|
404
|
-
writeFileSync(
|
|
405
|
-
join(emptyDir, "src", "none.ts"),
|
|
406
|
-
`export const nothing = 1;\n`,
|
|
407
|
-
);
|
|
408
|
-
const out = join(emptyDir, "out");
|
|
409
|
-
compile(emptyDir, out);
|
|
410
|
-
expect(existsSync(join(out, "rastack-env.d.ts"))).toBe(false);
|
|
411
|
-
});
|
|
412
|
-
});
|
package/test/update.spec.ts
DELETED
|
@@ -1,152 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
compareSemver,
|
|
3
|
-
globalPrefixFromPackageDir,
|
|
4
|
-
installSpec,
|
|
5
|
-
npmBinary,
|
|
6
|
-
npmInstallArgs,
|
|
7
|
-
npmShellCommand,
|
|
8
|
-
npmSpawnShell,
|
|
9
|
-
parseUpdateFlags,
|
|
10
|
-
sanitizeVersion,
|
|
11
|
-
updateAvailable,
|
|
12
|
-
} from "../src/update";
|
|
13
|
-
|
|
14
|
-
describe("update — flag parsing", () => {
|
|
15
|
-
it("defaults to a latest, non-check, non-dry-run update", () => {
|
|
16
|
-
expect(parseUpdateFlags([])).toEqual({
|
|
17
|
-
version: "latest",
|
|
18
|
-
check: false,
|
|
19
|
-
dryRun: false,
|
|
20
|
-
});
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
it("takes a bare positional as the target version", () => {
|
|
24
|
-
expect(parseUpdateFlags(["0.0.40"]).version).toBe("0.0.40");
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
it("accepts --version / --to and the check + dry-run flags", () => {
|
|
28
|
-
expect(parseUpdateFlags(["--version", "1.2.3"]).version).toBe("1.2.3");
|
|
29
|
-
expect(parseUpdateFlags(["--to", "1.2.3"]).version).toBe("1.2.3");
|
|
30
|
-
expect(parseUpdateFlags(["--check"]).check).toBe(true);
|
|
31
|
-
expect(parseUpdateFlags(["--dry-run"]).dryRun).toBe(true);
|
|
32
|
-
expect(parseUpdateFlags(["--print"]).dryRun).toBe(true);
|
|
33
|
-
});
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
describe("update — npm spec + args", () => {
|
|
37
|
-
it("installs bare `rastack` for latest, pinned otherwise", () => {
|
|
38
|
-
expect(installSpec("latest")).toBe("rastack");
|
|
39
|
-
expect(installSpec("")).toBe("rastack");
|
|
40
|
-
expect(installSpec("0.0.40")).toBe("rastack@0.0.40");
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
it("builds a quiet global install argv", () => {
|
|
44
|
-
expect(npmInstallArgs("latest")).toEqual([
|
|
45
|
-
"install",
|
|
46
|
-
"-g",
|
|
47
|
-
"rastack",
|
|
48
|
-
"--no-fund",
|
|
49
|
-
"--no-audit",
|
|
50
|
-
"--loglevel=error",
|
|
51
|
-
]);
|
|
52
|
-
expect(npmInstallArgs("0.0.40")[2]).toBe("rastack@0.0.40");
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
it("spawns npm.cmd on Windows and plain npm elsewhere", () => {
|
|
56
|
-
// npm is a `.cmd` shim on Windows; execFile (no shell) can't launch a bare
|
|
57
|
-
// `npm` there, so the wrapper must target `npm.cmd`. POSIX keeps `npm`.
|
|
58
|
-
expect(npmBinary("win32")).toBe("npm.cmd");
|
|
59
|
-
expect(npmBinary("linux")).toBe("npm");
|
|
60
|
-
expect(npmBinary("darwin")).toBe("npm");
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
it("runs npm through a shell only on Windows", () => {
|
|
64
|
-
// A `.cmd` shim can't be spawned directly since the Node CVE-2024-27980 fix
|
|
65
|
-
// (spawnSync npm.cmd EINVAL) — it must go through cmd.exe. POSIX spawns npm
|
|
66
|
-
// directly so there is no shell and no injection surface.
|
|
67
|
-
expect(npmSpawnShell("win32")).toBe(true);
|
|
68
|
-
expect(npmSpawnShell("linux")).toBe(false);
|
|
69
|
-
expect(npmSpawnShell("darwin")).toBe(false);
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
it("joins the shell command as one string (avoids the Node DEP0190 warning)", () => {
|
|
73
|
-
// The Windows path runs npm via `execSync(<string>)` rather than
|
|
74
|
-
// `execFileSync(npm, args, { shell: true })`, which Node 22+ deprecates
|
|
75
|
-
// (args concatenated, not escaped). The sanitized version keeps it safe.
|
|
76
|
-
expect(npmShellCommand("npm.cmd", npmInstallArgs("latest"))).toBe(
|
|
77
|
-
"npm.cmd install -g rastack --no-fund --no-audit --loglevel=error",
|
|
78
|
-
);
|
|
79
|
-
expect(npmShellCommand("npm.cmd", npmInstallArgs("0.0.42"))).toContain(
|
|
80
|
-
"rastack@0.0.42",
|
|
81
|
-
);
|
|
82
|
-
});
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
describe("update — version sanitization", () => {
|
|
86
|
-
it("keeps valid semver, dist-tags, and pinned versions", () => {
|
|
87
|
-
expect(sanitizeVersion("latest")).toBe("latest");
|
|
88
|
-
expect(sanitizeVersion("next")).toBe("next");
|
|
89
|
-
expect(sanitizeVersion("0.0.40")).toBe("0.0.40");
|
|
90
|
-
expect(sanitizeVersion("1.2.3-beta.1")).toBe("1.2.3-beta.1");
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
it("falls back to latest for empty or shell-unsafe input", () => {
|
|
94
|
-
expect(sanitizeVersion(undefined)).toBe("latest");
|
|
95
|
-
expect(sanitizeVersion("")).toBe("latest");
|
|
96
|
-
// Shell metacharacters must never reach the Windows shell-spawned npm.
|
|
97
|
-
expect(sanitizeVersion("0.0.40; rm -rf /")).toBe("latest");
|
|
98
|
-
expect(sanitizeVersion("$(whoami)")).toBe("latest");
|
|
99
|
-
expect(sanitizeVersion("a b")).toBe("latest");
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
it("is applied when parsing the bare positional and --version", () => {
|
|
103
|
-
expect(parseUpdateFlags(["0.0.40; evil"]).version).toBe("latest");
|
|
104
|
-
expect(parseUpdateFlags(["--version", "&& evil"]).version).toBe("latest");
|
|
105
|
-
expect(parseUpdateFlags(["0.0.40"]).version).toBe("0.0.40");
|
|
106
|
-
});
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
describe("update — global prefix derivation", () => {
|
|
110
|
-
it("derives the per-user prefix from a POSIX global install", () => {
|
|
111
|
-
expect(
|
|
112
|
-
globalPrefixFromPackageDir("/home/me/.rastack/lib/node_modules/rastack"),
|
|
113
|
-
).toBe("/home/me/.rastack");
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
it("derives the prefix from a Windows global install", () => {
|
|
117
|
-
expect(
|
|
118
|
-
globalPrefixFromPackageDir(
|
|
119
|
-
"C:\\Users\\me\\.rastack\\node_modules\\rastack",
|
|
120
|
-
),
|
|
121
|
-
).toBe("C:\\Users\\me\\.rastack");
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
it("prefers the lib/node_modules layout when both could match", () => {
|
|
125
|
-
// POSIX path also contains `/node_modules/` — the lib form must win so the
|
|
126
|
-
// prefix is the install root, not `<root>/lib`.
|
|
127
|
-
expect(
|
|
128
|
-
globalPrefixFromPackageDir("/opt/tools/lib/node_modules/rastack"),
|
|
129
|
-
).toBe("/opt/tools");
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
it("returns null for a source checkout (not under node_modules)", () => {
|
|
133
|
-
expect(
|
|
134
|
-
globalPrefixFromPackageDir("/home/me/src/reactapistack/tools"),
|
|
135
|
-
).toBeNull();
|
|
136
|
-
});
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
describe("update — semver comparison", () => {
|
|
140
|
-
it("orders versions numerically, ignoring a `v` prefix and pre-release", () => {
|
|
141
|
-
expect(compareSemver("0.0.9", "0.0.10")).toBe(-1);
|
|
142
|
-
expect(compareSemver("v1.2.0", "1.2.0")).toBe(0);
|
|
143
|
-
expect(compareSemver("2.0.0", "1.9.9")).toBe(1);
|
|
144
|
-
expect(compareSemver("1.0.0-beta", "1.0.0")).toBe(0);
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
it("reports whether an update is available", () => {
|
|
148
|
-
expect(updateAvailable("0.0.33", "0.0.40")).toBe(true);
|
|
149
|
-
expect(updateAvailable("0.0.40", "0.0.40")).toBe(false);
|
|
150
|
-
expect(updateAvailable("0.1.0", "0.0.40")).toBe(false);
|
|
151
|
-
});
|
|
152
|
-
});
|