rastack 0.0.57 → 0.0.59
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 +11 -0
- package/README.md +190 -0
- package/admin/index.tsx +19 -13
- package/admin/ui/BootScreen.tsx +201 -0
- package/dist/admin.js +156 -18
- 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 +21 -5
- 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 +200 -4
- 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 +25 -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 +21 -5
- 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 +233 -6
- 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/import.spec.ts
DELETED
|
@@ -1,241 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The external-data import pipeline: format parsers (CSV via `parseCsv`, the
|
|
3
|
-
* dependency-free XLSX and PDF readers) and the pure validation core that
|
|
4
|
-
* runs a whole tabular source through a resource's constraint state machine.
|
|
5
|
-
*
|
|
6
|
-
* The XLSX fixture is built with the repo's own `zipStore` (deploy/zip.ts) —
|
|
7
|
-
* the reader handles STORE entries, which is exactly what the writer emits.
|
|
8
|
-
* The PDF fixtures are hand-written minimal documents (one raw content
|
|
9
|
-
* stream, one FlateDecode) exercising the text operators the extractor
|
|
10
|
-
* replays.
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import { deflateSync } from "zlib";
|
|
14
|
-
import { zipStore } from "../src/deploy/zip";
|
|
15
|
-
import {
|
|
16
|
-
formatOf,
|
|
17
|
-
idsFromTabular,
|
|
18
|
-
mapHeaders,
|
|
19
|
-
parsePdfTable,
|
|
20
|
-
parseTabular,
|
|
21
|
-
parseXlsx,
|
|
22
|
-
validateDataset,
|
|
23
|
-
} from "../src/import";
|
|
24
|
-
import { specsFromResource } from "../src/validate";
|
|
25
|
-
import type { FieldModel } from "../src/compile/model";
|
|
26
|
-
|
|
27
|
-
// The airline fixtures, as `rastack compile` writes them to the manifest.
|
|
28
|
-
const AIRPORT_FIELDS: FieldModel[] = [
|
|
29
|
-
{ name: "code", type: "string", maxLength: 3, unique: true },
|
|
30
|
-
{ name: "name", type: "string", maxLength: 120 },
|
|
31
|
-
];
|
|
32
|
-
const TERMINAL_FIELDS: FieldModel[] = [
|
|
33
|
-
{
|
|
34
|
-
name: "airport",
|
|
35
|
-
type: "fk",
|
|
36
|
-
relation: { app: "airports", model: "airport" },
|
|
37
|
-
},
|
|
38
|
-
{ name: "label", type: "string", maxLength: 20 },
|
|
39
|
-
];
|
|
40
|
-
|
|
41
|
-
describe("header mapping", () => {
|
|
42
|
-
it("matches case-, space- and separator-insensitively", () => {
|
|
43
|
-
const specs = specsFromResource({
|
|
44
|
-
fields: [
|
|
45
|
-
{ name: "owner_id", type: "int" },
|
|
46
|
-
{ name: "code", type: "string" },
|
|
47
|
-
] as FieldModel[],
|
|
48
|
-
});
|
|
49
|
-
const { columns, missingFields } = mapHeaders(
|
|
50
|
-
["Owner ID", "CODE", "junk"],
|
|
51
|
-
specs,
|
|
52
|
-
);
|
|
53
|
-
expect(columns).toEqual([
|
|
54
|
-
{ header: "Owner ID", field: "owner_id" },
|
|
55
|
-
{ header: "CODE", field: "code" },
|
|
56
|
-
{ header: "junk", field: null },
|
|
57
|
-
]);
|
|
58
|
-
expect(missingFields).toEqual([]);
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
it("reports fields no column covers", () => {
|
|
62
|
-
const specs = specsFromResource({ fields: AIRPORT_FIELDS });
|
|
63
|
-
const { missingFields } = mapHeaders(["code"], specs);
|
|
64
|
-
expect(missingFields).toEqual(["name"]);
|
|
65
|
-
});
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
describe("dataset validation through the state machine", () => {
|
|
69
|
-
const airportSpecs = specsFromResource({ fields: AIRPORT_FIELDS });
|
|
70
|
-
|
|
71
|
-
it("validates a CSV row-by-row, locating each failed gate", () => {
|
|
72
|
-
const csv = Buffer.from(
|
|
73
|
-
"code,name\n" +
|
|
74
|
-
"LHR,Heathrow\n" +
|
|
75
|
-
"LHRX,Too Long A Code\n" +
|
|
76
|
-
"LHR,Duplicate Of Row One\n" +
|
|
77
|
-
",Missing Code\n",
|
|
78
|
-
);
|
|
79
|
-
const report = validateDataset(airportSpecs, parseTabular("csv", csv));
|
|
80
|
-
|
|
81
|
-
expect(report.total).toBe(4);
|
|
82
|
-
expect(report.valid).toBe(1);
|
|
83
|
-
expect(report.invalid).toBe(3);
|
|
84
|
-
expect(report.records).toEqual([{ code: "LHR", name: "Heathrow" }]);
|
|
85
|
-
expect(report.issues).toMatchObject([
|
|
86
|
-
{ row: 1, field: "code", gate: "bounded" },
|
|
87
|
-
{ row: 2, field: "code", gate: "unique" },
|
|
88
|
-
{ row: 3, field: "code", gate: "present" },
|
|
89
|
-
]);
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
it("resolves foreign keys against a related file's ids", () => {
|
|
93
|
-
const airports = parseTabular(
|
|
94
|
-
"csv",
|
|
95
|
-
Buffer.from("id,code,name\n1,LHR,Heathrow\n2,JFK,Kennedy\n"),
|
|
96
|
-
);
|
|
97
|
-
const terminals = parseTabular(
|
|
98
|
-
"csv",
|
|
99
|
-
Buffer.from("airport,label\n1,Terminal 5\n9,Ghost Terminal\n"),
|
|
100
|
-
);
|
|
101
|
-
|
|
102
|
-
const report = validateDataset(
|
|
103
|
-
specsFromResource({ fields: TERMINAL_FIELDS }),
|
|
104
|
-
terminals,
|
|
105
|
-
{ relatedIds: { "airports.airport": idsFromTabular(airports) } },
|
|
106
|
-
);
|
|
107
|
-
|
|
108
|
-
expect(report.valid).toBe(1);
|
|
109
|
-
// FK ids coerce to numbers on the way through the `typed` gate.
|
|
110
|
-
expect(report.records).toEqual([{ airport: 1, label: "Terminal 5" }]);
|
|
111
|
-
expect(report.issues).toMatchObject([
|
|
112
|
-
{ row: 1, field: "airport", gate: "resolved" },
|
|
113
|
-
]);
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
it("infers the source format from the file name", () => {
|
|
117
|
-
expect(formatOf("airports.csv")).toBe("csv");
|
|
118
|
-
expect(formatOf("Airports.XLSX")).toBe("xlsx");
|
|
119
|
-
expect(formatOf("report.pdf")).toBe("pdf");
|
|
120
|
-
expect(formatOf("notes.txt")).toBeNull();
|
|
121
|
-
});
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
describe("XLSX reader", () => {
|
|
125
|
-
const SHARED_STRINGS =
|
|
126
|
-
`<?xml version="1.0" encoding="UTF-8" standalone="yes"?>` +
|
|
127
|
-
`<sst xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" count="4" uniqueCount="4">` +
|
|
128
|
-
`<si><t>code</t></si><si><t>name</t></si><si><t>LHR</t></si>` +
|
|
129
|
-
`<si><r><t>Heath</t></r><r><t>row & Co</t></r></si>` +
|
|
130
|
-
`</sst>`;
|
|
131
|
-
|
|
132
|
-
const SHEET =
|
|
133
|
-
`<?xml version="1.0" encoding="UTF-8" standalone="yes"?>` +
|
|
134
|
-
`<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"><sheetData>` +
|
|
135
|
-
`<row r="1"><c r="A1" t="s"><v>0</v></c><c r="B1" t="s"><v>1</v></c></row>` +
|
|
136
|
-
`<row r="2"><c r="A2" t="s"><v>2</v></c><c r="B2" t="s"><v>3</v></c></row>` +
|
|
137
|
-
`<row r="3"><c r="A3" t="inlineStr"><is><t>JFK</t></is></c><c r="B3"><v>42</v></c></row>` +
|
|
138
|
-
// A row with only column B populated — column A must come back as "".
|
|
139
|
-
`<row r="4"><c r="B4" t="inlineStr"><is><t>Orphan</t></is></c></row>` +
|
|
140
|
-
`</sheetData></worksheet>`;
|
|
141
|
-
|
|
142
|
-
const workbook = () =>
|
|
143
|
-
zipStore([
|
|
144
|
-
{ name: "xl/sharedStrings.xml", data: Buffer.from(SHARED_STRINGS) },
|
|
145
|
-
{ name: "xl/worksheets/sheet1.xml", data: Buffer.from(SHEET) },
|
|
146
|
-
]);
|
|
147
|
-
|
|
148
|
-
it("reads shared strings (incl. rich-text runs), inline strings and numbers", () => {
|
|
149
|
-
const { headers, rows } = parseXlsx(workbook());
|
|
150
|
-
expect(headers).toEqual(["code", "name"]);
|
|
151
|
-
expect(rows).toEqual([
|
|
152
|
-
["LHR", "Heathrow & Co"],
|
|
153
|
-
["JFK", "42"],
|
|
154
|
-
["", "Orphan"],
|
|
155
|
-
]);
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
it("feeds straight into the state machine like any other source", () => {
|
|
159
|
-
const report = validateDataset(
|
|
160
|
-
specsFromResource({ fields: AIRPORT_FIELDS }),
|
|
161
|
-
parseXlsx(workbook()),
|
|
162
|
-
);
|
|
163
|
-
expect(report.valid).toBe(2);
|
|
164
|
-
// Row 3 has no `code` — required, so it halts at `present`.
|
|
165
|
-
expect(report.issues).toMatchObject([
|
|
166
|
-
{ row: 2, field: "code", gate: "present" },
|
|
167
|
-
]);
|
|
168
|
-
});
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
describe("PDF table extractor", () => {
|
|
172
|
-
const CONTENT =
|
|
173
|
-
"BT\n" +
|
|
174
|
-
"/F1 12 Tf\n" +
|
|
175
|
-
"72 720 Td\n" +
|
|
176
|
-
"(code) Tj\n" +
|
|
177
|
-
"150 0 Td\n" +
|
|
178
|
-
"(name) Tj\n" +
|
|
179
|
-
"ET\n" +
|
|
180
|
-
"BT\n" +
|
|
181
|
-
"72 700 Td\n" +
|
|
182
|
-
"(LHR) Tj\n" +
|
|
183
|
-
"150 0 Td\n" +
|
|
184
|
-
"(Heathrow \\(west\\)) Tj\n" +
|
|
185
|
-
"ET\n" +
|
|
186
|
-
"BT\n" +
|
|
187
|
-
"72 680 Td\n" +
|
|
188
|
-
"[(J) 20 (FK)] TJ\n" +
|
|
189
|
-
"150 0 Td\n" +
|
|
190
|
-
"(Kennedy) Tj\n" +
|
|
191
|
-
"ET\n";
|
|
192
|
-
|
|
193
|
-
function rawPdf(content: string): Buffer {
|
|
194
|
-
return Buffer.from(
|
|
195
|
-
`%PDF-1.4\n1 0 obj\n<< /Length ${content.length} >>\nstream\n` +
|
|
196
|
-
content +
|
|
197
|
-
`\nendstream\nendobj\n%%EOF\n`,
|
|
198
|
-
"latin1",
|
|
199
|
-
);
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
function flatePdf(content: string): Buffer {
|
|
203
|
-
const compressed = deflateSync(Buffer.from(content, "latin1"));
|
|
204
|
-
return Buffer.concat([
|
|
205
|
-
Buffer.from(
|
|
206
|
-
`%PDF-1.4\n1 0 obj\n<< /Length ${compressed.length} /Filter /FlateDecode >>\nstream\n`,
|
|
207
|
-
"latin1",
|
|
208
|
-
),
|
|
209
|
-
compressed,
|
|
210
|
-
Buffer.from(`\nendstream\nendobj\n%%EOF\n`, "latin1"),
|
|
211
|
-
]);
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
const expected = {
|
|
215
|
-
headers: ["code", "name"],
|
|
216
|
-
rows: [
|
|
217
|
-
["LHR", "Heathrow (west)"],
|
|
218
|
-
["JFK", "Kennedy"],
|
|
219
|
-
],
|
|
220
|
-
};
|
|
221
|
-
|
|
222
|
-
it("clusters positioned text into rows (raw stream)", () => {
|
|
223
|
-
expect(parsePdfTable(rawPdf(CONTENT))).toEqual(expected);
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
it("decodes FlateDecode content streams", () => {
|
|
227
|
-
expect(parsePdfTable(flatePdf(CONTENT))).toEqual(expected);
|
|
228
|
-
});
|
|
229
|
-
|
|
230
|
-
it("feeds straight into the state machine like any other source", () => {
|
|
231
|
-
const report = validateDataset(
|
|
232
|
-
specsFromResource({ fields: AIRPORT_FIELDS }),
|
|
233
|
-
parsePdfTable(rawPdf(CONTENT)),
|
|
234
|
-
);
|
|
235
|
-
expect(report.valid).toBe(2);
|
|
236
|
-
expect(report.records).toEqual([
|
|
237
|
-
{ code: "LHR", name: "Heathrow (west)" },
|
|
238
|
-
{ code: "JFK", name: "Kennedy" },
|
|
239
|
-
]);
|
|
240
|
-
});
|
|
241
|
-
});
|
package/test/plugin.spec.ts
DELETED
|
@@ -1,315 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The dev-server schema compiler (`rastack/plugin`) — the way around
|
|
3
|
-
* "classes need a compile step". The plugin runs the same
|
|
4
|
-
* `analyzeProject`/`compile` pass as the CLI *inside* the bundler: entities
|
|
5
|
-
* authored as plain classes/interfaces become the `virtual:rastack-manifest`
|
|
6
|
-
* module, recompiled on save, with `rastack-env.d.ts` kept fresh and the
|
|
7
|
-
* last good manifest served through broken in-progress edits.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
|
|
11
|
-
import { join } from "path";
|
|
12
|
-
import {
|
|
13
|
-
GLOBAL_MANIFEST_KEY,
|
|
14
|
-
isEntitySource,
|
|
15
|
-
manifestModuleCode,
|
|
16
|
-
rastackPlugin,
|
|
17
|
-
registerModuleCode,
|
|
18
|
-
resolveOptions,
|
|
19
|
-
RESOLVED_MANIFEST_ID,
|
|
20
|
-
startRastackWatcher,
|
|
21
|
-
VIRTUAL_MANIFEST_ID,
|
|
22
|
-
VIRTUAL_REGISTER_ID,
|
|
23
|
-
virtualModuleDts,
|
|
24
|
-
} from "../src/plugin";
|
|
25
|
-
import { getGlobalManifest } from "../hooks/manifest";
|
|
26
|
-
import { resolveEntityRef } from "../hooks/entity";
|
|
27
|
-
|
|
28
|
-
const workRoot = join(__dirname, "plugin-fixtures");
|
|
29
|
-
const resourcesDir = join(workRoot, "entities");
|
|
30
|
-
const outDir = join(workRoot, ".rastack");
|
|
31
|
-
|
|
32
|
-
// Plain classes + a hook-rooted interface — no resource() DSL anywhere.
|
|
33
|
-
const GOOD_SOURCE = `
|
|
34
|
-
export class Airport {
|
|
35
|
-
code!: string;
|
|
36
|
-
name!: string;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export class Terminal {
|
|
40
|
-
airport!: Airport; // FK — inferred from the type
|
|
41
|
-
label!: string;
|
|
42
|
-
}
|
|
43
|
-
`;
|
|
44
|
-
|
|
45
|
-
// An all-required FK cycle — a compile *error* (the mid-edit breakage case).
|
|
46
|
-
const BROKEN_SOURCE = `
|
|
47
|
-
export class Airport {
|
|
48
|
-
code!: string;
|
|
49
|
-
home!: Terminal;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export class Terminal {
|
|
53
|
-
airport!: Airport;
|
|
54
|
-
label!: string;
|
|
55
|
-
}
|
|
56
|
-
`;
|
|
57
|
-
|
|
58
|
-
function writeEntities(source: string): void {
|
|
59
|
-
writeFileSync(join(resourcesDir, "airports.ts"), source);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
beforeEach(() => {
|
|
63
|
-
rmSync(workRoot, { recursive: true, force: true });
|
|
64
|
-
mkdirSync(resourcesDir, { recursive: true });
|
|
65
|
-
writeEntities(GOOD_SOURCE);
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
afterAll(() => {
|
|
69
|
-
rmSync(workRoot, { recursive: true, force: true });
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
function makePlugin(overrides: Record<string, unknown> = {}) {
|
|
73
|
-
return rastackPlugin({
|
|
74
|
-
resourcesDir,
|
|
75
|
-
outDir,
|
|
76
|
-
onError: () => {},
|
|
77
|
-
...overrides,
|
|
78
|
-
});
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
describe("rastackPlugin — the bundler hooks", () => {
|
|
82
|
-
it("serves class entities as the virtual manifest module", () => {
|
|
83
|
-
const plugin = makePlugin();
|
|
84
|
-
plugin.buildStart();
|
|
85
|
-
|
|
86
|
-
expect(plugin.resolveId(VIRTUAL_MANIFEST_ID)).toBe(RESOLVED_MANIFEST_ID);
|
|
87
|
-
expect(plugin.resolveId("some/other/module")).toBeUndefined();
|
|
88
|
-
|
|
89
|
-
const code = plugin.load(RESOLVED_MANIFEST_ID)!;
|
|
90
|
-
expect(code).toContain("export default manifest");
|
|
91
|
-
const manifest = plugin.api.manifest();
|
|
92
|
-
expect(code).toContain(JSON.stringify(manifest));
|
|
93
|
-
|
|
94
|
-
// The classes were compiled: fields read off the type system, the
|
|
95
|
-
// Airport-typed property inferred as a foreign key.
|
|
96
|
-
// The app name derives from the defining file (airports.ts → "airports").
|
|
97
|
-
const terminal = manifest.resources.find((r) => r.model === "terminal")!;
|
|
98
|
-
expect(terminal.relations).toEqual([
|
|
99
|
-
{ field: "airport", target: "airports.airport", kind: "fk" },
|
|
100
|
-
]);
|
|
101
|
-
expect(manifestModuleCode(manifest)).toBe(code);
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
it("writes the artifacts — schema, openapi, env.d.ts, virtual-module types", () => {
|
|
105
|
-
makePlugin().buildStart();
|
|
106
|
-
expect(existsSync(join(outDir, "schema.rastack.json"))).toBe(true);
|
|
107
|
-
expect(existsSync(join(outDir, "openapi.json"))).toBe(true);
|
|
108
|
-
|
|
109
|
-
const env = readFileSync(join(outDir, "rastack-env.d.ts"), "utf8");
|
|
110
|
-
expect(env).toContain('"airports.airport": import("../entities/airports").Airport;');
|
|
111
|
-
|
|
112
|
-
const dts = readFileSync(join(outDir, "rastack-plugin.d.ts"), "utf8");
|
|
113
|
-
expect(dts).toBe(virtualModuleDts());
|
|
114
|
-
expect(dts).toContain(`declare module "${VIRTUAL_MANIFEST_ID}"`);
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
it("recompiles when a watched entity changes", () => {
|
|
118
|
-
const plugin = makePlugin();
|
|
119
|
-
plugin.buildStart();
|
|
120
|
-
expect(
|
|
121
|
-
plugin.api.manifest().resources.map((r) => r.model),
|
|
122
|
-
).toEqual(["airport", "terminal"]);
|
|
123
|
-
|
|
124
|
-
writeEntities(GOOD_SOURCE + "\nexport class Gate { number!: string }\n");
|
|
125
|
-
plugin.watchChange(join(resourcesDir, "airports.ts"));
|
|
126
|
-
|
|
127
|
-
expect(plugin.api.manifest().resources.map((r) => r.model)).toEqual([
|
|
128
|
-
"airport",
|
|
129
|
-
"gate",
|
|
130
|
-
"terminal",
|
|
131
|
-
]);
|
|
132
|
-
expect(plugin.load(RESOLVED_MANIFEST_ID)).toContain('"gate"');
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
it("keeps serving the last good manifest through a broken edit", () => {
|
|
136
|
-
const errors: unknown[] = [];
|
|
137
|
-
const plugin = makePlugin({ onError: (e: unknown) => errors.push(e) });
|
|
138
|
-
plugin.buildStart();
|
|
139
|
-
const goodCode = plugin.load(RESOLVED_MANIFEST_ID);
|
|
140
|
-
|
|
141
|
-
writeEntities(BROKEN_SOURCE);
|
|
142
|
-
plugin.watchChange(join(resourcesDir, "airports.ts"));
|
|
143
|
-
|
|
144
|
-
// The compile failed (reported), but the app keeps its manifest.
|
|
145
|
-
expect(plugin.load(RESOLVED_MANIFEST_ID)).toBe(goodCode);
|
|
146
|
-
expect(errors).toHaveLength(1);
|
|
147
|
-
expect(String((errors[0] as Error).message)).toMatch(/circular/i);
|
|
148
|
-
expect(plugin.api.error()).toBe(errors[0]);
|
|
149
|
-
|
|
150
|
-
// Fixing the file recovers on the next change (load() recompiles).
|
|
151
|
-
writeEntities(GOOD_SOURCE);
|
|
152
|
-
plugin.watchChange(join(resourcesDir, "airports.ts"));
|
|
153
|
-
expect(plugin.load(RESOLVED_MANIFEST_ID)).toBe(goodCode);
|
|
154
|
-
expect(plugin.api.error()).toBeUndefined();
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
it("throws at startup when there is no good manifest to fall back to", () => {
|
|
158
|
-
writeEntities(BROKEN_SOURCE);
|
|
159
|
-
const plugin = makePlugin();
|
|
160
|
-
expect(() => plugin.buildStart()).toThrow(/circular/i);
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
it("hot-invalidates the virtual module in a Vite-shaped server", () => {
|
|
164
|
-
const plugin = makePlugin();
|
|
165
|
-
plugin.buildStart();
|
|
166
|
-
|
|
167
|
-
const virtualModule = { id: RESOLVED_MANIFEST_ID };
|
|
168
|
-
const invalidated: unknown[] = [];
|
|
169
|
-
const server = {
|
|
170
|
-
moduleGraph: {
|
|
171
|
-
getModuleById: (id: string) =>
|
|
172
|
-
id === RESOLVED_MANIFEST_ID ? virtualModule : undefined,
|
|
173
|
-
invalidateModule: (mod: unknown) => invalidated.push(mod),
|
|
174
|
-
},
|
|
175
|
-
};
|
|
176
|
-
|
|
177
|
-
const changed = { id: "changed" };
|
|
178
|
-
const result = plugin.handleHotUpdate({
|
|
179
|
-
file: join(resourcesDir, "airports.ts"),
|
|
180
|
-
server,
|
|
181
|
-
modules: [changed],
|
|
182
|
-
});
|
|
183
|
-
expect(invalidated).toEqual([virtualModule]);
|
|
184
|
-
expect(result).toEqual([changed, virtualModule]);
|
|
185
|
-
|
|
186
|
-
// Unrelated files don't touch the schema.
|
|
187
|
-
expect(
|
|
188
|
-
plugin.handleHotUpdate({
|
|
189
|
-
file: join(workRoot, "App.tsx"),
|
|
190
|
-
server,
|
|
191
|
-
modules: [changed],
|
|
192
|
-
}),
|
|
193
|
-
).toBeUndefined();
|
|
194
|
-
});
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
describe("auto-registration — no manifest prop anywhere", () => {
|
|
198
|
-
afterEach(() => {
|
|
199
|
-
delete (globalThis as Record<string, unknown>)[GLOBAL_MANIFEST_KEY];
|
|
200
|
-
});
|
|
201
|
-
|
|
202
|
-
it("serves a register module that publishes the manifest globally", () => {
|
|
203
|
-
const plugin = makePlugin();
|
|
204
|
-
plugin.buildStart();
|
|
205
|
-
|
|
206
|
-
expect(plugin.resolveId(VIRTUAL_REGISTER_ID)).toBe(VIRTUAL_REGISTER_ID);
|
|
207
|
-
const code = plugin.load(VIRTUAL_REGISTER_ID)!;
|
|
208
|
-
expect(code).toBe(registerModuleCode());
|
|
209
|
-
expect(code).toContain(`import manifest from "${VIRTUAL_MANIFEST_ID}"`);
|
|
210
|
-
expect(code).toContain(`globalThis.${GLOBAL_MANIFEST_KEY} = manifest`);
|
|
211
|
-
});
|
|
212
|
-
|
|
213
|
-
it("injects the register script into index.html (dev src, build inline)", () => {
|
|
214
|
-
const plugin = makePlugin();
|
|
215
|
-
expect(plugin.transformIndexHtml.order).toBe("pre");
|
|
216
|
-
|
|
217
|
-
const dev = plugin.transformIndexHtml.handler("<html/>", { server: {} });
|
|
218
|
-
expect(dev).toEqual([
|
|
219
|
-
{
|
|
220
|
-
tag: "script",
|
|
221
|
-
attrs: { type: "module", src: `/@id/${VIRTUAL_REGISTER_ID}` },
|
|
222
|
-
injectTo: "head-prepend",
|
|
223
|
-
},
|
|
224
|
-
]);
|
|
225
|
-
|
|
226
|
-
const build = plugin.transformIndexHtml.handler("<html/>", {});
|
|
227
|
-
expect(build).toEqual([
|
|
228
|
-
{
|
|
229
|
-
tag: "script",
|
|
230
|
-
attrs: { type: "module" },
|
|
231
|
-
children: `import "${VIRTUAL_REGISTER_ID}";`,
|
|
232
|
-
injectTo: "head-prepend",
|
|
233
|
-
},
|
|
234
|
-
]);
|
|
235
|
-
});
|
|
236
|
-
|
|
237
|
-
it("the runtime falls back to the registered global manifest", () => {
|
|
238
|
-
class Airport {
|
|
239
|
-
code!: string;
|
|
240
|
-
}
|
|
241
|
-
// Nothing registered → the class cannot resolve without a manifest.
|
|
242
|
-
expect(getGlobalManifest()).toBeUndefined();
|
|
243
|
-
expect(() => resolveEntityRef(Airport, getGlobalManifest())).toThrow(
|
|
244
|
-
/manifest/,
|
|
245
|
-
);
|
|
246
|
-
|
|
247
|
-
// What the injected register module does at boot:
|
|
248
|
-
(globalThis as Record<string, unknown>)[GLOBAL_MANIFEST_KEY] = {
|
|
249
|
-
version: 1,
|
|
250
|
-
resources: [
|
|
251
|
-
{ app: "airports", model: "airport", fields: [], relations: [] },
|
|
252
|
-
],
|
|
253
|
-
};
|
|
254
|
-
|
|
255
|
-
const manifest = getGlobalManifest()!;
|
|
256
|
-
expect(manifest.resources).toHaveLength(1);
|
|
257
|
-
expect(getGlobalManifest()).toBe(manifest); // cached per assignment
|
|
258
|
-
// useRastackManifest (which useData/useForm read outside any provider)
|
|
259
|
-
// falls back to this global, so a bare useData(Airport) resolves with
|
|
260
|
-
// zero schema wiring in app code.
|
|
261
|
-
expect(resolveEntityRef(Airport, getGlobalManifest()).listUrl).toBe(
|
|
262
|
-
"/api/airports/v1/airport/",
|
|
263
|
-
);
|
|
264
|
-
});
|
|
265
|
-
});
|
|
266
|
-
|
|
267
|
-
describe("isEntitySource — what triggers a recompile", () => {
|
|
268
|
-
const options = resolveOptions({ resourcesDir, outDir }, workRoot);
|
|
269
|
-
|
|
270
|
-
it("accepts .ts/.tsx under the resources dir and nothing else", () => {
|
|
271
|
-
expect(isEntitySource(join(resourcesDir, "a.ts"), options)).toBe(true);
|
|
272
|
-
expect(isEntitySource(join(resourcesDir, "deep", "b.tsx"), options)).toBe(true);
|
|
273
|
-
expect(isEntitySource(join(resourcesDir, "a.d.ts"), options)).toBe(false);
|
|
274
|
-
expect(isEntitySource(join(resourcesDir, "notes.md"), options)).toBe(false);
|
|
275
|
-
expect(isEntitySource(join(workRoot, "elsewhere.ts"), options)).toBe(false);
|
|
276
|
-
// The emitted artifacts must never re-trigger the compiler.
|
|
277
|
-
expect(isEntitySource(join(outDir, "rastack-env.d.ts"), options)).toBe(false);
|
|
278
|
-
});
|
|
279
|
-
});
|
|
280
|
-
|
|
281
|
-
describe("startRastackWatcher — the non-Rollup dev servers", () => {
|
|
282
|
-
it("compiles at start and rewrites artifacts on save", async () => {
|
|
283
|
-
const watcher = startRastackWatcher({
|
|
284
|
-
resourcesDir,
|
|
285
|
-
outDir,
|
|
286
|
-
onError: () => {},
|
|
287
|
-
});
|
|
288
|
-
try {
|
|
289
|
-
expect(existsSync(join(outDir, "schema.rastack.json"))).toBe(true);
|
|
290
|
-
expect(
|
|
291
|
-
watcher.state.manifest().resources.map((r) => r.model),
|
|
292
|
-
).toEqual(["airport", "terminal"]);
|
|
293
|
-
|
|
294
|
-
writeEntities(GOOD_SOURCE + "\nexport class Gate { number!: string }\n");
|
|
295
|
-
await waitFor(() =>
|
|
296
|
-
readFileSync(join(outDir, "schema.rastack.json"), "utf8").includes(
|
|
297
|
-
'"gate"',
|
|
298
|
-
),
|
|
299
|
-
);
|
|
300
|
-
} finally {
|
|
301
|
-
watcher.close();
|
|
302
|
-
}
|
|
303
|
-
});
|
|
304
|
-
});
|
|
305
|
-
|
|
306
|
-
async function waitFor(check: () => boolean, timeoutMs = 5000): Promise<void> {
|
|
307
|
-
const start = Date.now();
|
|
308
|
-
for (;;) {
|
|
309
|
-
if (check()) return;
|
|
310
|
-
if (Date.now() - start > timeoutMs) {
|
|
311
|
-
throw new Error("waitFor: condition not met in time");
|
|
312
|
-
}
|
|
313
|
-
await new Promise((r) => setTimeout(r, 50));
|
|
314
|
-
}
|
|
315
|
-
}
|