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.
Files changed (83) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +190 -0
  3. package/dist/deploy/ci.js +2 -2
  4. package/dist/deploy/io.js +2 -2
  5. package/dist/dev/harness.d.ts +1 -1
  6. package/dist/dev/harness.js +1 -2
  7. package/dist/external/cache.d.ts +28 -0
  8. package/dist/external/cache.js +35 -0
  9. package/dist/external/envelope.d.ts +35 -0
  10. package/dist/external/envelope.js +48 -0
  11. package/dist/external/graphql.d.ts +25 -0
  12. package/dist/external/graphql.js +78 -0
  13. package/dist/external/handler.d.ts +52 -0
  14. package/dist/external/handler.js +139 -0
  15. package/dist/external/index.d.ts +19 -0
  16. package/dist/external/index.js +38 -0
  17. package/dist/external/lambda.d.ts +49 -0
  18. package/dist/external/lambda.js +86 -0
  19. package/dist/external/rest.d.ts +17 -0
  20. package/dist/external/rest.js +46 -0
  21. package/dist/external/route.d.ts +12 -0
  22. package/dist/external/route.js +33 -0
  23. package/dist/external/service.d.ts +50 -0
  24. package/dist/external/service.js +59 -0
  25. package/dist/external/types.d.ts +107 -0
  26. package/dist/external/types.js +22 -0
  27. package/dist/external/util.d.ts +16 -0
  28. package/dist/external/util.js +58 -0
  29. package/dist/rastack-dev.js +16 -1
  30. package/dist/rastack-wasm-build.d.ts +1 -1
  31. package/dist/rastack-wasm-build.js +6 -6
  32. package/dist/wasm/rastack_wasm_bg.wasm +0 -0
  33. package/external.ts +10 -0
  34. package/hooks/query/delete.ts +12 -11
  35. package/hooks/query/fetch.ts +6 -8
  36. package/hooks/query/interfaces.ts +1 -4
  37. package/hooks/query/list.ts +16 -9
  38. package/hooks/query/update.ts +8 -6
  39. package/package.json +24 -2
  40. package/scripts/copy-templates.js +1 -1
  41. package/scripts/copy-wasm.js +1 -1
  42. package/scripts/dev-install.mjs +63 -0
  43. package/src/deploy/ci.ts +2 -2
  44. package/src/deploy/io.ts +2 -2
  45. package/src/dev/harness.ts +1 -2
  46. package/src/external/cache.ts +53 -0
  47. package/src/external/envelope.ts +78 -0
  48. package/src/external/graphql.ts +98 -0
  49. package/src/external/handler.ts +207 -0
  50. package/src/external/index.ts +25 -0
  51. package/src/external/lambda.ts +108 -0
  52. package/src/external/rest.ts +60 -0
  53. package/src/external/route.ts +33 -0
  54. package/src/external/service.ts +86 -0
  55. package/src/external/types.ts +121 -0
  56. package/src/external/util.ts +57 -0
  57. package/src/rastack-dev.ts +17 -1
  58. package/src/rastack-wasm-build.ts +6 -6
  59. package/jest.config.cjs +0 -12
  60. package/test/auth.spec.ts +0 -207
  61. package/test/cache.spec.ts +0 -263
  62. package/test/cognito.spec.ts +0 -281
  63. package/test/compile.spec.ts +0 -240
  64. package/test/components.spec.ts +0 -524
  65. package/test/csv-schema.spec.ts +0 -143
  66. package/test/deploy.spec.ts +0 -571
  67. package/test/dev.spec.ts +0 -414
  68. package/test/entities.spec.ts +0 -597
  69. package/test/import.spec.ts +0 -241
  70. package/test/plugin.spec.ts +0 -315
  71. package/test/runtime-manifest.spec.ts +0 -309
  72. package/test/schema-entities.spec.ts +0 -688
  73. package/test/tokens.spec.ts +0 -302
  74. package/test/transitions.spec.ts +0 -372
  75. package/test/typed-hooks.spec.ts +0 -412
  76. package/test/update.spec.ts +0 -152
  77. package/test/validate.spec.ts +0 -319
  78. package/tsconfig.json +0 -27
  79. package/wasm/package.json +0 -4
  80. package/wasm/rastack_wasm.d.ts +0 -100
  81. package/wasm/rastack_wasm.js +0 -745
  82. package/wasm/rastack_wasm_bg.wasm +0 -0
  83. package/wasm/rastack_wasm_bg.wasm.d.ts +0 -28
@@ -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
- });
@@ -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
- });