ruact 0.0.5 → 0.0.6
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.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +54 -2
- data/.rubocop_todo.yml +3 -115
- data/CHANGELOG.md +67 -18
- data/bench/server_functions_dispatch_bench.rb +109 -142
- data/bench/server_functions_dispatch_bench.results.md +29 -0
- data/docs/internal/decisions/server-functions-api.md +402 -0
- data/lib/generators/ruact/install/install_generator.rb +310 -25
- data/lib/generators/ruact/install/templates/Procfile.dev.tt +2 -0
- data/lib/generators/ruact/install/templates/dev.tt +16 -0
- data/lib/generators/ruact/install/templates/package.json.tt +17 -0
- data/lib/generators/ruact/install/templates/vite.config.js.tt +3 -1
- data/lib/generators/ruact/scaffold/scaffold_attribute.rb +114 -0
- data/lib/generators/ruact/scaffold/scaffold_form_helpers.rb +114 -0
- data/lib/generators/ruact/scaffold/scaffold_generator.rb +491 -0
- data/lib/generators/ruact/scaffold/scaffold_shadcn_preflight.rb +229 -0
- data/lib/generators/ruact/scaffold/templates/components/DeleteDialog.tsx.tt +104 -0
- data/lib/generators/ruact/scaffold/templates/components/Form.tsx.tt +212 -0
- data/lib/generators/ruact/scaffold/templates/components/List.tsx.tt +338 -0
- data/lib/generators/ruact/scaffold/templates/components/agnostic/DeleteDialog.tsx.tt +92 -0
- data/lib/generators/ruact/scaffold/templates/components/agnostic/Form.tsx.tt +174 -0
- data/lib/generators/ruact/scaffold/templates/components/agnostic/List.tsx.tt +253 -0
- data/lib/generators/ruact/scaffold/templates/controller.rb.tt +130 -0
- data/lib/generators/ruact/scaffold/templates/queries/application_query.rb.tt +13 -0
- data/lib/generators/ruact/scaffold/templates/queries/query.rb.tt +59 -0
- data/lib/generators/ruact/scaffold/templates/request_spec.rb.tt +77 -0
- data/lib/generators/ruact/scaffold/templates/views/edit.html.erb.tt +7 -0
- data/lib/generators/ruact/scaffold/templates/views/index.html.erb.tt +6 -0
- data/lib/generators/ruact/scaffold/templates/views/new.html.erb.tt +5 -0
- data/lib/generators/ruact/scaffold/templates/views/show.html.erb.tt +16 -0
- data/lib/ruact/client_manifest.rb +37 -36
- data/lib/ruact/component_contract.rb +115 -0
- data/lib/ruact/configuration.rb +80 -28
- data/lib/ruact/controller.rb +69 -421
- data/lib/ruact/doctor.rb +133 -4
- data/lib/ruact/erb_preprocessor.rb +65 -12
- data/lib/ruact/erb_preprocessor_hook.rb +4 -1
- data/lib/ruact/errors.rb +30 -44
- data/lib/ruact/html_converter.rb +22 -1
- data/lib/ruact/railtie.rb +56 -200
- data/lib/ruact/server.rb +28 -6
- data/lib/ruact/server_functions/codegen.rb +49 -188
- data/lib/ruact/server_functions/codegen_v2.rb +23 -6
- data/lib/ruact/server_functions/codegen_v2_query_params.rb +140 -0
- data/lib/ruact/server_functions/error_payload.rb +1 -1
- data/lib/ruact/server_functions/error_rendering.rb +22 -29
- data/lib/ruact/server_functions/name_bridge.rb +14 -16
- data/lib/ruact/server_functions/query_source.rb +35 -8
- data/lib/ruact/server_functions/route_source.rb +3 -4
- data/lib/ruact/server_functions/snapshot.rb +12 -139
- data/lib/ruact/server_functions/validation_errors.rb +70 -0
- data/lib/ruact/server_functions.rb +21 -25
- data/lib/ruact/signed_references.rb +162 -0
- data/lib/ruact/string_distance.rb +72 -0
- data/lib/ruact/validation_errors_collector.rb +139 -0
- data/lib/ruact/version.rb +1 -1
- data/lib/ruact/view_helper.rb +102 -0
- data/lib/ruact.rb +19 -19
- data/lib/tasks/ruact.rake +10 -53
- data/spec/fixtures/story_7_9_views/controller_request_spec_support/errors_demo/new.html.erb +3 -0
- data/spec/ruact/client_manifest_spec.rb +36 -0
- data/spec/ruact/component_contract_spec.rb +119 -0
- data/spec/ruact/configuration_spec.rb +51 -34
- data/spec/ruact/controller_request_spec.rb +264 -0
- data/spec/ruact/controller_spec.rb +63 -326
- data/spec/ruact/doctor_spec.rb +201 -0
- data/spec/ruact/erb_preprocessor_hook_spec.rb +4 -1
- data/spec/ruact/erb_preprocessor_spec.rb +127 -0
- data/spec/ruact/errors_spec.rb +0 -45
- data/spec/ruact/html_converter_spec.rb +50 -0
- data/spec/ruact/install_generator_spec.rb +591 -4
- data/spec/ruact/query_request_spec.rb +109 -1
- data/spec/ruact/scaffold_generator_spec.rb +1835 -0
- data/spec/ruact/server_bucket_request_spec.rb +142 -0
- data/spec/ruact/server_function_name_spec.rb +1 -1
- data/spec/ruact/server_functions/codegen_spec.rb +158 -269
- data/spec/ruact/server_functions/name_bridge_spec.rb +9 -9
- data/spec/ruact/server_functions/query_source_spec.rb +51 -0
- data/spec/ruact/server_functions/railtie_integration_spec.rb +71 -268
- data/spec/ruact/server_functions/rake_spec.rb +29 -29
- data/spec/ruact/server_functions/snapshot_spec.rb +53 -213
- data/spec/ruact/server_rescue_request_spec.rb +6 -6
- data/spec/ruact/server_spec.rb +8 -9
- data/spec/ruact/signed_references_spec.rb +164 -0
- data/spec/ruact/string_distance_spec.rb +38 -0
- data/spec/ruact/validation_errors_spec.rb +116 -0
- data/spec/ruact/view_helper_spec.rb +79 -0
- data/spec/spec_helper.rb +0 -5
- data/vendor/javascript/ruact-server-functions-runtime/index.d.ts +40 -45
- data/vendor/javascript/ruact-server-functions-runtime/index.js +42 -98
- data/vendor/javascript/ruact-server-functions-runtime/index.test.mjs +72 -75
- data/vendor/javascript/ruact-server-functions-runtime/package.json +1 -1
- data/vendor/javascript/vite-plugin-ruact/bootstrap.test.mjs +96 -0
- data/vendor/javascript/vite-plugin-ruact/index.js +353 -7
- data/vendor/javascript/vite-plugin-ruact/manifest-contract.test.mjs +182 -0
- data/vendor/javascript/vite-plugin-ruact/package-lock.json +16 -0
- data/vendor/javascript/vite-plugin-ruact/package.json +3 -1
- data/vendor/javascript/vite-plugin-ruact/registry.test.mjs +199 -0
- data/vendor/javascript/vite-plugin-ruact/runtime/bootstrap.jsx +68 -0
- data/vendor/javascript/vite-plugin-ruact/runtime/flight-client.js +235 -0
- data/vendor/javascript/vite-plugin-ruact/runtime/ruact-router.js +473 -0
- data/vendor/javascript/vite-plugin-ruact/server-functions-codegen.mjs +114 -139
- data/vendor/javascript/vite-plugin-ruact/server-functions-codegen.test.mjs +199 -262
- data/vendor/javascript/vite-plugin-ruact/tsconfig.json +18 -0
- data/vendor/javascript/vite-plugin-ruact/tsconfig.scaffold-agnostic.json +18 -0
- data/vendor/javascript/vite-plugin-ruact/tsconfig.scaffold.json +20 -0
- data/vendor/javascript/vite-plugin-ruact/type-tests/emitted-module.test-d.ts +23 -0
- data/vendor/javascript/vite-plugin-ruact/type-tests/scaffold/PostDeleteDialog.tsx +90 -0
- data/vendor/javascript/vite-plugin-ruact/type-tests/scaffold/PostForm.tsx +238 -0
- data/vendor/javascript/vite-plugin-ruact/type-tests/scaffold/PostList.tsx +339 -0
- data/vendor/javascript/vite-plugin-ruact/type-tests/scaffold/agnostic/PostDeleteDialog.tsx +85 -0
- data/vendor/javascript/vite-plugin-ruact/type-tests/scaffold/agnostic/PostForm.tsx +216 -0
- data/vendor/javascript/vite-plugin-ruact/type-tests/scaffold/agnostic/PostList.tsx +269 -0
- data/vendor/javascript/vite-plugin-ruact/type-tests/scaffold/agnostic/ambient.d.ts +78 -0
- data/vendor/javascript/vite-plugin-ruact/type-tests/scaffold/ambient.d.ts +166 -0
- data/vendor/javascript/vite-plugin-ruact/type-tests/typed-query.test-d.ts +48 -0
- data/vendor/javascript/vite-plugin-ruact/type-tests/usequery.test-d.ts +26 -0
- metadata +55 -15
- data/lib/generators/ruact/install/templates/application.jsx.tt +0 -51
- data/lib/ruact/server_action.rb +0 -131
- data/lib/ruact/server_functions/endpoint_controller.rb +0 -237
- data/lib/ruact/server_functions/registry.rb +0 -148
- data/lib/ruact/server_functions/registry_entry.rb +0 -26
- data/lib/ruact/server_functions/standalone_context.rb +0 -103
- data/lib/ruact/server_functions/standalone_dispatcher.rb +0 -178
- data/spec/ruact/server_functions/csrf_request_spec.rb +0 -380
- data/spec/ruact/server_functions/dispatch_request_spec.rb +0 -819
- data/spec/ruact/server_functions/registry_spec.rb +0 -199
- data/spec/ruact/server_functions/standalone_action_spec.rb +0 -224
- data/spec/ruact/server_functions/standalone_context_spec.rb +0 -142
- data/spec/ruact/server_functions/standalone_dispatcher_spec.rb +0 -273
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
3
4
|
import { installServerFunctionsHooks } from "./server-functions-codegen.mjs";
|
|
4
5
|
|
|
5
6
|
/**
|
|
@@ -17,7 +18,51 @@ import { installServerFunctionsHooks } from "./server-functions-codegen.mjs";
|
|
|
17
18
|
* "chunks": ["/assets/LikeButton-abc123.js"]
|
|
18
19
|
* }
|
|
19
20
|
* }
|
|
21
|
+
*
|
|
22
|
+
* Story 13.5 (FR100) — compile-time component contract. A component opts into a
|
|
23
|
+
* call-site contract by exporting `__ruactContract` from its own module
|
|
24
|
+
* (HEEx-style `attr`/`slot`, declared next to the component):
|
|
25
|
+
*
|
|
26
|
+
* export const __ruactContract = {
|
|
27
|
+
* props: { title: "required", subtitle: "optional" },
|
|
28
|
+
* slots: { header: "optional" }, // optional; { name: required|optional } or ["name", ...]
|
|
29
|
+
* passthrough: false, // optional; true allows undeclared props
|
|
30
|
+
* };
|
|
31
|
+
*
|
|
32
|
+
* The scanner extracts this NAMES-ONLY (no TS-AST, no value types) into an
|
|
33
|
+
* optional `contract` field on the manifest entry. A component without the
|
|
34
|
+
* export emits no `contract` field (byte-additive, back-compatible). A
|
|
35
|
+
* malformed/partial declaration is warned + skipped (the Ruby side then sees
|
|
36
|
+
* "no contract" and validates nothing — fail open). The Ruby preprocess-time
|
|
37
|
+
* validator (`Ruact::ComponentContract`) reads this and checks `<Component .../>`
|
|
38
|
+
* ERB call sites for missing-required / unknown-prop / slot-misuse before render.
|
|
20
39
|
*/
|
|
40
|
+
// Story 10.1b — the auto-registry virtual module. The install template (and the
|
|
41
|
+
// playgrounds) import this instead of hand-maintaining a `MODULE_REGISTRY`. Its
|
|
42
|
+
// value is `{ [manifest id]: moduleExports }`, derived from the SAME scan that
|
|
43
|
+
// writes react-client-manifest.json, so registry membership == manifest
|
|
44
|
+
// membership and the keys equal the manifest `id` the gem serializes as the
|
|
45
|
+
// Flight `moduleId` (client_manifest.rb:79) — by construction, in dev AND prod.
|
|
46
|
+
export const REGISTRY_VIRTUAL_ID = "virtual:ruact/registry";
|
|
47
|
+
const RESOLVED_REGISTRY_ID = "\0" + REGISTRY_VIRTUAL_ID;
|
|
48
|
+
|
|
49
|
+
// Story 14.2 (FR104) — the bootstrap virtual module. The React entry that boots
|
|
50
|
+
// the app used to be written into every app as `app/javascript/application.jsx`
|
|
51
|
+
// (ruact plumbing interleaved with the user's components). It is now served as a
|
|
52
|
+
// virtual module from gem-shipped source (`runtime/bootstrap.jsx`), so a fresh
|
|
53
|
+
// install leaves `app/javascript/` with ONLY the user's `components/`. The
|
|
54
|
+
// generated `vite.config` input is `virtual:ruact/bootstrap`; the gem's
|
|
55
|
+
// `ruact_js_assets` view helper targets the same id (dev `<script src>` →
|
|
56
|
+
// `/@id/__x00__virtual:ruact/bootstrap`; prod Vite-manifest key
|
|
57
|
+
// `virtual:ruact/bootstrap`). Mirrors REGISTRY_VIRTUAL_ID exactly.
|
|
58
|
+
export const BOOTSTRAP_VIRTUAL_ID = "virtual:ruact/bootstrap";
|
|
59
|
+
const RESOLVED_BOOTSTRAP_ID = "\0" + BOOTSTRAP_VIRTUAL_ID;
|
|
60
|
+
|
|
61
|
+
// The gem-shipped runtime sources the virtual bootstrap pulls in. Resolved
|
|
62
|
+
// against THIS file so the absolute specifiers the bootstrap imports always
|
|
63
|
+
// point at the gem copy, regardless of the app's cwd.
|
|
64
|
+
const RUNTIME_DIR = path.join(path.dirname(fileURLToPath(import.meta.url)), "runtime");
|
|
65
|
+
|
|
21
66
|
export default function ruact(options = {}) {
|
|
22
67
|
const {
|
|
23
68
|
componentsDir = "app/javascript/components",
|
|
@@ -34,6 +79,26 @@ export default function ruact(options = {}) {
|
|
|
34
79
|
root = config.root;
|
|
35
80
|
},
|
|
36
81
|
|
|
82
|
+
// Story 10.1b / 14.2 — resolve ruact's virtual module ids.
|
|
83
|
+
resolveId(id) {
|
|
84
|
+
if (id === REGISTRY_VIRTUAL_ID) return RESOLVED_REGISTRY_ID;
|
|
85
|
+
if (id === BOOTSTRAP_VIRTUAL_ID) return RESOLVED_BOOTSTRAP_ID;
|
|
86
|
+
return null;
|
|
87
|
+
},
|
|
88
|
+
|
|
89
|
+
// Story 10.1b — emit the registry source from the in-memory manifest. The
|
|
90
|
+
// manifest is populated in `buildStart` (and rebuilt by the dev watcher
|
|
91
|
+
// below), which Rollup/Vite run before module loading, so it is ready here
|
|
92
|
+
// in both `build` and `serve`. Keys are the manifest `id` values verbatim
|
|
93
|
+
// (no re-normalization → zero drift with what the gem resolves).
|
|
94
|
+
load(id) {
|
|
95
|
+
if (id === RESOLVED_REGISTRY_ID) return generateRegistrySource(manifest);
|
|
96
|
+
// Story 14.2 — serve the gem-shipped bootstrap source, with its relative
|
|
97
|
+
// runtime imports rewritten to absolute fs specifiers (see below).
|
|
98
|
+
if (id === RESOLVED_BOOTSTRAP_ID) return generateBootstrapSource();
|
|
99
|
+
return null;
|
|
100
|
+
},
|
|
101
|
+
|
|
37
102
|
// During dev: build the manifest from source files
|
|
38
103
|
buildStart() {
|
|
39
104
|
manifest = buildManifest(path.resolve(root, componentsDir));
|
|
@@ -59,10 +124,36 @@ export default function ruact(options = {}) {
|
|
|
59
124
|
name,
|
|
60
125
|
chunks: [url],
|
|
61
126
|
};
|
|
127
|
+
// Story 13.5 — preserve the opt-in contract across the dev→build
|
|
128
|
+
// rewrite (the hashed-URL pass must not drop it).
|
|
129
|
+
if (entry.contract) updated[name].contract = entry.contract;
|
|
62
130
|
}
|
|
63
131
|
}
|
|
64
132
|
}
|
|
65
133
|
|
|
134
|
+
// Story 10.1b — id-match guard (NFR16 dev/prod parity). The auto-registry
|
|
135
|
+
// (virtual:ruact/registry) is keyed on the source-relative manifest id and
|
|
136
|
+
// is loaded BEFORE this rewrite. If a "use client" component became a
|
|
137
|
+
// standalone facade chunk (e.g. it was added to build.rollupOptions.input,
|
|
138
|
+
// or split via manualChunks), its id is rewritten to a hashed URL here
|
|
139
|
+
// while the registry still resolves it by source path — a SILENT prod
|
|
140
|
+
// hydration miss. Fail the build loudly instead. In the shipped model
|
|
141
|
+
// components are imported by the registry and inlined into the app bundle,
|
|
142
|
+
// so they never become facade chunks and this never fires.
|
|
143
|
+
const rewritten = Object.keys(updated).filter(
|
|
144
|
+
(name) => manifest[name] && updated[name].id !== manifest[name].id
|
|
145
|
+
);
|
|
146
|
+
if (rewritten.length > 0) {
|
|
147
|
+
throw new Error(
|
|
148
|
+
`[vite-plugin-ruact] component(s) ${rewritten.join(", ")} were emitted as ` +
|
|
149
|
+
"standalone (code-split) chunks, so the Flight manifest names them by a " +
|
|
150
|
+
"hashed URL while virtual:ruact/registry resolves them by source path — a " +
|
|
151
|
+
"silent hydration miss in production. Keep \"use client\" components OUT of " +
|
|
152
|
+
"build.rollupOptions.input and avoid manualChunks for them: they are meant " +
|
|
153
|
+
"to be imported by the auto-registry and inlined into the app bundle."
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
66
157
|
// Merge: keep entries that didn't get a hashed URL (dev mode)
|
|
67
158
|
const final = { ...manifest, ...updated };
|
|
68
159
|
// Strip internal _sourceFile field
|
|
@@ -73,21 +164,31 @@ export default function ruact(options = {}) {
|
|
|
73
164
|
writeManifest(path.resolve(root, manifestOutput), final);
|
|
74
165
|
},
|
|
75
166
|
|
|
76
|
-
// Dev server: watch components dir and rebuild manifest on change
|
|
167
|
+
// Dev server: watch components dir and rebuild manifest on change. Story
|
|
168
|
+
// 10.1b — also react to add/unlink and invalidate the auto-registry virtual
|
|
169
|
+
// module so a newly added (or removed) "use client" component is registered
|
|
170
|
+
// with ZERO app-code edits (AC1, dev side).
|
|
77
171
|
configureServer(server) {
|
|
78
172
|
const dir = path.resolve(root, componentsDir);
|
|
79
173
|
server.watcher.add(dir);
|
|
80
|
-
|
|
81
|
-
if (file.startsWith(dir))
|
|
82
|
-
|
|
83
|
-
|
|
174
|
+
const rebuild = (file) => {
|
|
175
|
+
if (!file.startsWith(dir)) return;
|
|
176
|
+
manifest = buildManifest(dir);
|
|
177
|
+
writeManifest(path.resolve(root, manifestOutput), manifest);
|
|
178
|
+
const mod = server.moduleGraph.getModuleById(RESOLVED_REGISTRY_ID);
|
|
179
|
+
if (mod) {
|
|
180
|
+
server.moduleGraph.invalidateModule(mod);
|
|
181
|
+
server.ws.send({ type: "full-reload" });
|
|
84
182
|
}
|
|
85
|
-
}
|
|
183
|
+
};
|
|
184
|
+
server.watcher.on("change", rebuild);
|
|
185
|
+
server.watcher.on("add", rebuild);
|
|
186
|
+
server.watcher.on("unlink", rebuild);
|
|
86
187
|
},
|
|
87
188
|
}, options);
|
|
88
189
|
}
|
|
89
190
|
|
|
90
|
-
function buildManifest(componentsDir) {
|
|
191
|
+
export function buildManifest(componentsDir) {
|
|
91
192
|
const manifest = {};
|
|
92
193
|
|
|
93
194
|
if (!fs.existsSync(componentsDir)) return manifest;
|
|
@@ -102,6 +203,22 @@ function buildManifest(componentsDir) {
|
|
|
102
203
|
|
|
103
204
|
const exports = extractExportNames(content);
|
|
104
205
|
const relUrl = "/" + path.relative(componentsDir, file);
|
|
206
|
+
let contract = extractContract(content, file);
|
|
207
|
+
|
|
208
|
+
// Story 13.5 — a single `__ruactContract` cannot say WHICH component it
|
|
209
|
+
// describes, so it only applies when the file has exactly one component
|
|
210
|
+
// export (the documented one-component-per-file convention). A multi-export
|
|
211
|
+
// file would otherwise validate every export against the same contract —
|
|
212
|
+
// warn + skip rather than guess.
|
|
213
|
+
if (contract && exports.length > 1) {
|
|
214
|
+
// eslint-disable-next-line no-console
|
|
215
|
+
console.warn(
|
|
216
|
+
`[vite-plugin-ruact] ignoring __ruactContract in ${file}: a contract ` +
|
|
217
|
+
`applies to a single component, but this file exports ${exports.length} ` +
|
|
218
|
+
`(${exports.join(", ")}). Split them into one component per file.`
|
|
219
|
+
);
|
|
220
|
+
contract = null;
|
|
221
|
+
}
|
|
105
222
|
|
|
106
223
|
for (const name of exports) {
|
|
107
224
|
manifest[name] = {
|
|
@@ -110,12 +227,93 @@ function buildManifest(componentsDir) {
|
|
|
110
227
|
chunks: [relUrl],
|
|
111
228
|
_sourceFile: file, // used during build to match hashed chunks
|
|
112
229
|
};
|
|
230
|
+
// Story 13.5 — opt-in, byte-additive: only present when declared.
|
|
231
|
+
if (contract) manifest[name].contract = contract;
|
|
113
232
|
}
|
|
114
233
|
}
|
|
115
234
|
|
|
116
235
|
return manifest;
|
|
117
236
|
}
|
|
118
237
|
|
|
238
|
+
// Story 10.1b (AC1, AC2) — render the auto-registry virtual module source from a
|
|
239
|
+
// manifest (the same object `buildManifest` produces). The result is an ESM
|
|
240
|
+
// module whose default export is `{ [manifest id]: moduleNamespace }`:
|
|
241
|
+
//
|
|
242
|
+
// import * as __ruact_m0 from "/abs/app/javascript/components/PostList.jsx";
|
|
243
|
+
// const MODULE_REGISTRY = { "/PostList.jsx": __ruact_m0 };
|
|
244
|
+
// export default MODULE_REGISTRY;
|
|
245
|
+
//
|
|
246
|
+
// Why keyed on the manifest `id` (not a re-derived path): the gem serializes
|
|
247
|
+
// `entry["id"]` as the Flight `moduleId`, and the client resolves
|
|
248
|
+
// `moduleRegistry[row.moduleId]` — so keying the registry on the very same
|
|
249
|
+
// `id` makes the id-match invariant hold BY CONSTRUCTION. The shipped runtime
|
|
250
|
+
// loads components from this eager registry (the Flight client ignores the
|
|
251
|
+
// Import row's `chunks`), so each component is statically imported and inlined
|
|
252
|
+
// into the app bundle; it never becomes a standalone facade chunk, and
|
|
253
|
+
// `generateBundle`'s hashed-URL rewrite (which only fires for facade chunks)
|
|
254
|
+
// leaves the component `id` as its source-relative path in prod exactly as in
|
|
255
|
+
// dev. NFR16 dev/prod parity therefore holds with one source-relative key set.
|
|
256
|
+
//
|
|
257
|
+
// One import per source file (a file may export several components sharing an
|
|
258
|
+
// `id`/`_sourceFile`); membership equals manifest membership because we iterate
|
|
259
|
+
// the manifest the scan built ("use client"-only, `.jsx`/`.tsx`, same dir).
|
|
260
|
+
export function generateRegistrySource(manifest) {
|
|
261
|
+
const byId = new Map(); // manifest id -> absolute source file
|
|
262
|
+
for (const entry of Object.values(manifest || {})) {
|
|
263
|
+
if (!entry || !entry._sourceFile || !entry.id) continue;
|
|
264
|
+
if (!byId.has(entry.id)) byId.set(entry.id, entry._sourceFile);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const lines = [
|
|
268
|
+
"// AUTO-GENERATED by vite-plugin-ruact — component auto-registry (Story 10.1b).",
|
|
269
|
+
"// Maps each react-client-manifest `id` to its module exports. Do not edit.",
|
|
270
|
+
];
|
|
271
|
+
const props = [];
|
|
272
|
+
let i = 0;
|
|
273
|
+
for (const [id, sourceFile] of byId) {
|
|
274
|
+
const local = `__ruact_m${i++}`;
|
|
275
|
+
lines.push(`import * as ${local} from ${JSON.stringify(toImportSpecifier(sourceFile))};`);
|
|
276
|
+
props.push(` ${JSON.stringify(id)}: ${local},`);
|
|
277
|
+
}
|
|
278
|
+
lines.push("const MODULE_REGISTRY = {", ...props, "};", "export default MODULE_REGISTRY;", "");
|
|
279
|
+
|
|
280
|
+
return lines.join("\n");
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Story 14.2 (FR104) — render the virtual bootstrap source from the gem-shipped
|
|
284
|
+
// `runtime/bootstrap.jsx`. A `load` hook returns module TEXT whose relative
|
|
285
|
+
// imports resolve against the resolved id (`\0virtual:ruact/bootstrap`), which
|
|
286
|
+
// is NOT a filesystem path — so `./flight-client.js` / `./ruact-router.js`
|
|
287
|
+
// would fail. We rewrite those two specifiers to ABSOLUTE fs specifiers into the
|
|
288
|
+
// gem `runtime/` dir (the same technique `generateRegistrySource` uses via
|
|
289
|
+
// `toImportSpecifier`). The bare `react` / `react-dom/client` specifiers and the
|
|
290
|
+
// `virtual:ruact/registry` id are left untouched — Vite resolves them from the
|
|
291
|
+
// app root (so React comes from the USER's node_modules: one React instance).
|
|
292
|
+
export function generateBootstrapSource(runtimeDir = RUNTIME_DIR) {
|
|
293
|
+
const src = fs.readFileSync(path.join(runtimeDir, "bootstrap.jsx"), "utf8");
|
|
294
|
+
const abs = (name) => toImportSpecifier(path.join(runtimeDir, name));
|
|
295
|
+
// Rewrite EVERY `from './<runtime>.js'` import specifier to its absolute fs
|
|
296
|
+
// path. Matching the quoted `from '...'` form (not the bare filename) avoids
|
|
297
|
+
// hitting prose mentions of the file in comments, and the global regex
|
|
298
|
+
// tolerates either quote style. The replacement is supplied as a function so a
|
|
299
|
+
// `$` in the absolute path is never treated as a replacement pattern.
|
|
300
|
+
const rewrite = (code, name) => {
|
|
301
|
+
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
302
|
+
return code.replace(
|
|
303
|
+
new RegExp(`from\\s+(['"])\\./${escaped}\\1`, "g"),
|
|
304
|
+
() => `from '${abs(name)}'`,
|
|
305
|
+
);
|
|
306
|
+
};
|
|
307
|
+
return rewrite(rewrite(src, "flight-client.js"), "ruact-router.js");
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// A bundler import specifier for an absolute fs path. Vite/Rollup resolve
|
|
311
|
+
// absolute paths directly; normalize Windows backslashes to forward slashes so
|
|
312
|
+
// the emitted specifier is a valid module string on every platform.
|
|
313
|
+
function toImportSpecifier(absPath) {
|
|
314
|
+
return absPath.replace(/\\/g, "/");
|
|
315
|
+
}
|
|
316
|
+
|
|
119
317
|
function hasUseClient(content) {
|
|
120
318
|
// "use client" must appear as a directive at the top of the file
|
|
121
319
|
return /^\s*["']use client["']/m.test(content);
|
|
@@ -145,6 +343,154 @@ function extractExportNames(content) {
|
|
|
145
343
|
return Array.from(names);
|
|
146
344
|
}
|
|
147
345
|
|
|
346
|
+
// Story 13.5 (FR100) — extract the opt-in `__ruactContract` declaration.
|
|
347
|
+
//
|
|
348
|
+
// NAMES-ONLY by design (no TS-AST, no value types): the Ruby preprocess-time
|
|
349
|
+
// validator only needs prop/slot names + required/optional. We locate the
|
|
350
|
+
// exported object literal, balance its braces, then pull names out of the
|
|
351
|
+
// `props` / `slots` sub-blocks with targeted regexes (robust to formatting,
|
|
352
|
+
// and immune to eval). Returns:
|
|
353
|
+
// - null when the component declares no contract (byte-additive opt-out)
|
|
354
|
+
// - null when the declaration is malformed/empty (warn + skip → Ruby fails open)
|
|
355
|
+
// - { props?, slots?, passthrough? } otherwise (only non-empty members present)
|
|
356
|
+
export function extractContract(content, file) {
|
|
357
|
+
const marker = /export\s+(?:default\s+)?(?:const|let|var)\s+__ruactContract\s*=\s*/m;
|
|
358
|
+
const m = marker.exec(content);
|
|
359
|
+
if (!m) return null;
|
|
360
|
+
|
|
361
|
+
const braceStart = content.indexOf("{", m.index + m[0].length);
|
|
362
|
+
if (braceStart === -1) return warnSkip(file);
|
|
363
|
+
|
|
364
|
+
const objText = extractBalanced(content, braceStart, "{", "}");
|
|
365
|
+
if (objText === null) return warnSkip(file);
|
|
366
|
+
|
|
367
|
+
const contract = {};
|
|
368
|
+
|
|
369
|
+
const props = extractRequiredness(extractBlock(objText, "props", "{", "}"));
|
|
370
|
+
if (props && Object.keys(props).length) contract.props = props;
|
|
371
|
+
|
|
372
|
+
const slots = extractRequiredness(extractBlock(objText, "slots", "{", "}"));
|
|
373
|
+
if (slots && Object.keys(slots).length) {
|
|
374
|
+
contract.slots = slots;
|
|
375
|
+
} else {
|
|
376
|
+
// Array form: slots: ["header", "footer"] → all optional.
|
|
377
|
+
const arr = extractBlock(objText, "slots", "[", "]");
|
|
378
|
+
if (arr !== null) {
|
|
379
|
+
const names = {};
|
|
380
|
+
const nameRe = /["'`]([^"'`]+)["'`]/g;
|
|
381
|
+
let s;
|
|
382
|
+
while ((s = nameRe.exec(arr)) !== null) names[s[1]] = "optional";
|
|
383
|
+
if (Object.keys(names).length) contract.slots = names;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
if (/\bpassthrough\s*:\s*true\b/.test(objText)) contract.passthrough = true;
|
|
388
|
+
|
|
389
|
+
// A contract with no props, no slots, and no passthrough carries no
|
|
390
|
+
// information — treat as malformed/empty and fail open.
|
|
391
|
+
if (!contract.props && !contract.slots && !contract.passthrough) {
|
|
392
|
+
return warnSkip(file);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
return contract;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// Parse a `{ name: "required", other: "optional" }` block into a
|
|
399
|
+
// { name: "required" | "optional" } map. Accepts quoted or bare values, and an
|
|
400
|
+
// object form `name: { required: true }`. Returns {} for an empty block.
|
|
401
|
+
function extractRequiredness(block) {
|
|
402
|
+
if (block === null) return null;
|
|
403
|
+
const map = {};
|
|
404
|
+
// name: "required" | 'optional' | required (string or bare identifier)
|
|
405
|
+
const strRe = /([A-Za-z_$][\w$]*)\s*:\s*["'`]?(required|optional)["'`]?/g;
|
|
406
|
+
let m;
|
|
407
|
+
while ((m = strRe.exec(block)) !== null) map[m[1]] = m[2];
|
|
408
|
+
// name: { required: true } → required; { required: false } → optional
|
|
409
|
+
const objRe = /([A-Za-z_$][\w$]*)\s*:\s*\{[^}]*?\brequired\s*:\s*(true|false)[^}]*?\}/g;
|
|
410
|
+
while ((m = objRe.exec(block)) !== null) {
|
|
411
|
+
map[m[1]] = m[2] === "true" ? "required" : "optional";
|
|
412
|
+
}
|
|
413
|
+
return map;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// Extract the balanced inner text of `key: <open> ... <close>` from `objText`.
|
|
417
|
+
// Returns the inner text (without the delimiters) or null when the key is
|
|
418
|
+
// absent or the delimiters are unbalanced.
|
|
419
|
+
function extractBlock(objText, key, open, close) {
|
|
420
|
+
const keyRe = new RegExp(`\\b${key}\\s*:\\s*\\${open}`, "m");
|
|
421
|
+
const km = keyRe.exec(objText);
|
|
422
|
+
if (!km) return null;
|
|
423
|
+
const start = km.index + km[0].length - 1; // position of `open`
|
|
424
|
+
const inner = extractBalanced(objText, start, open, close);
|
|
425
|
+
return inner;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// Given a string and the index of an opening delimiter, return the inner text
|
|
429
|
+
// up to (excluding) the matching close delimiter, or null when unbalanced.
|
|
430
|
+
// String literals ('...', "...", `...`) and comments (// and / * ... * /) are
|
|
431
|
+
// skipped so a brace inside a comment or string never throws off the balance.
|
|
432
|
+
function extractBalanced(str, openIndex, open, close) {
|
|
433
|
+
let depth = 0;
|
|
434
|
+
let i = openIndex;
|
|
435
|
+
while (i < str.length) {
|
|
436
|
+
const ch = str[i];
|
|
437
|
+
const next = str[i + 1];
|
|
438
|
+
|
|
439
|
+
if (ch === "/" && next === "/") {
|
|
440
|
+
const nl = str.indexOf("\n", i);
|
|
441
|
+
if (nl === -1) return null;
|
|
442
|
+
i = nl + 1;
|
|
443
|
+
continue;
|
|
444
|
+
}
|
|
445
|
+
if (ch === "/" && next === "*") {
|
|
446
|
+
const end = str.indexOf("*/", i + 2);
|
|
447
|
+
if (end === -1) return null;
|
|
448
|
+
i = end + 2;
|
|
449
|
+
continue;
|
|
450
|
+
}
|
|
451
|
+
if (ch === '"' || ch === "'" || ch === "`") {
|
|
452
|
+
i = skipString(str, i);
|
|
453
|
+
if (i === -1) return null;
|
|
454
|
+
continue;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
if (ch === open) {
|
|
458
|
+
depth++;
|
|
459
|
+
} else if (ch === close) {
|
|
460
|
+
depth--;
|
|
461
|
+
if (depth === 0) return str.slice(openIndex + 1, i);
|
|
462
|
+
}
|
|
463
|
+
i++;
|
|
464
|
+
}
|
|
465
|
+
return null;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// Skip a JS string/template literal starting at the opening quote at +i+.
|
|
469
|
+
// Returns the index just past the closing quote, or -1 if unterminated.
|
|
470
|
+
function skipString(str, i) {
|
|
471
|
+
const quote = str[i];
|
|
472
|
+
i++;
|
|
473
|
+
while (i < str.length) {
|
|
474
|
+
const ch = str[i];
|
|
475
|
+
if (ch === "\\") {
|
|
476
|
+
i += 2;
|
|
477
|
+
continue;
|
|
478
|
+
}
|
|
479
|
+
if (ch === quote) return i + 1;
|
|
480
|
+
i++;
|
|
481
|
+
}
|
|
482
|
+
return -1;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function warnSkip(file) {
|
|
486
|
+
// eslint-disable-next-line no-console
|
|
487
|
+
console.warn(
|
|
488
|
+
`[vite-plugin-ruact] ignoring malformed __ruactContract in ${file} ` +
|
|
489
|
+
`(could not extract prop/slot names — the component will not be contract-validated)`
|
|
490
|
+
);
|
|
491
|
+
return null;
|
|
492
|
+
}
|
|
493
|
+
|
|
148
494
|
function writeManifest(outputPath, manifest) {
|
|
149
495
|
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
|
|
150
496
|
fs.writeFileSync(outputPath, JSON.stringify(manifest, null, 2));
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
// Story 13.5 (FR100) — buildManifest contract extraction.
|
|
2
|
+
//
|
|
3
|
+
// A component opts into a call-site contract by exporting `__ruactContract`.
|
|
4
|
+
// The scanner extracts it NAMES-ONLY into an optional `contract` field on the
|
|
5
|
+
// manifest entry. A component without the export emits NO `contract` field
|
|
6
|
+
// (byte-additive). A malformed declaration is warned + skipped (fail open).
|
|
7
|
+
|
|
8
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
|
9
|
+
import fs from "node:fs";
|
|
10
|
+
import os from "node:os";
|
|
11
|
+
import path from "node:path";
|
|
12
|
+
import { buildManifest, extractContract } from "./index.js";
|
|
13
|
+
|
|
14
|
+
let dir;
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
dir = fs.mkdtempSync(path.join(os.tmpdir(), "ruact-13-5-"));
|
|
17
|
+
});
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
function writeComponent(name, body) {
|
|
23
|
+
fs.writeFileSync(path.join(dir, name), body);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
describe("buildManifest — contract extraction (Story 13.5)", () => {
|
|
27
|
+
it("attaches a `contract` field when the component exports __ruactContract", () => {
|
|
28
|
+
writeComponent(
|
|
29
|
+
"LikeButton.tsx",
|
|
30
|
+
[
|
|
31
|
+
'"use client";',
|
|
32
|
+
"export const __ruactContract = {",
|
|
33
|
+
' props: { postId: "required", initialCount: "optional" },',
|
|
34
|
+
"};",
|
|
35
|
+
"export function LikeButton() { return null; }",
|
|
36
|
+
].join("\n")
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
const manifest = buildManifest(dir);
|
|
40
|
+
|
|
41
|
+
expect(manifest.LikeButton.contract).toEqual({
|
|
42
|
+
props: { postId: "required", initialCount: "optional" },
|
|
43
|
+
});
|
|
44
|
+
// The base entry shape is untouched.
|
|
45
|
+
expect(manifest.LikeButton.id).toBe("/LikeButton.tsx");
|
|
46
|
+
expect(manifest.LikeButton.name).toBe("LikeButton");
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("emits NO `contract` field when the component declares none (byte-additive)", () => {
|
|
50
|
+
writeComponent(
|
|
51
|
+
"Plain.tsx",
|
|
52
|
+
['"use client";', "export function Plain() { return null; }"].join("\n")
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
const manifest = buildManifest(dir);
|
|
56
|
+
|
|
57
|
+
expect(manifest.Plain).toBeDefined();
|
|
58
|
+
expect("contract" in manifest.Plain).toBe(false);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("extracts slots (object form) and passthrough", () => {
|
|
62
|
+
writeComponent(
|
|
63
|
+
"Card.tsx",
|
|
64
|
+
[
|
|
65
|
+
'"use client";',
|
|
66
|
+
"export const __ruactContract = {",
|
|
67
|
+
' props: { title: "required" },',
|
|
68
|
+
' slots: { header: "optional", footer: "required" },',
|
|
69
|
+
" passthrough: true,",
|
|
70
|
+
"};",
|
|
71
|
+
"export const Card = () => null;",
|
|
72
|
+
].join("\n")
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
const manifest = buildManifest(dir);
|
|
76
|
+
|
|
77
|
+
expect(manifest.Card.contract).toEqual({
|
|
78
|
+
props: { title: "required" },
|
|
79
|
+
slots: { header: "optional", footer: "required" },
|
|
80
|
+
passthrough: true,
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("extracts slots in array form (all optional)", () => {
|
|
85
|
+
const c = extractContract(
|
|
86
|
+
[
|
|
87
|
+
"export const __ruactContract = {",
|
|
88
|
+
' props: { title: "required" },',
|
|
89
|
+
' slots: ["header", "footer"],',
|
|
90
|
+
"};",
|
|
91
|
+
].join("\n"),
|
|
92
|
+
"Card.tsx"
|
|
93
|
+
);
|
|
94
|
+
expect(c).toEqual({
|
|
95
|
+
props: { title: "required" },
|
|
96
|
+
slots: { header: "optional", footer: "optional" },
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("handles nested braces in prop values without breaking balance", () => {
|
|
101
|
+
const c = extractContract(
|
|
102
|
+
[
|
|
103
|
+
"export const __ruactContract = {",
|
|
104
|
+
" props: { a: { required: true }, b: { required: false } },",
|
|
105
|
+
"};",
|
|
106
|
+
].join("\n"),
|
|
107
|
+
"Nested.tsx"
|
|
108
|
+
);
|
|
109
|
+
expect(c).toEqual({ props: { a: "required", b: "optional" } });
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("ignores braces inside comments and strings when balancing", () => {
|
|
113
|
+
const c = extractContract(
|
|
114
|
+
[
|
|
115
|
+
"export const __ruactContract = {",
|
|
116
|
+
" // a stray brace in a comment { should not break balance",
|
|
117
|
+
' props: { label: "required" }, // trailing }',
|
|
118
|
+
' slots: ["a-}-slot"],',
|
|
119
|
+
"};",
|
|
120
|
+
"export const X = () => null;",
|
|
121
|
+
].join("\n"),
|
|
122
|
+
"Commented.tsx"
|
|
123
|
+
);
|
|
124
|
+
expect(c).toEqual({
|
|
125
|
+
props: { label: "required" },
|
|
126
|
+
slots: { "a-}-slot": "optional" },
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("skips the contract for a multi-component file (warn) — cannot attribute it", () => {
|
|
131
|
+
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
132
|
+
writeComponent(
|
|
133
|
+
"Pair.tsx",
|
|
134
|
+
[
|
|
135
|
+
'"use client";',
|
|
136
|
+
'export const __ruactContract = { props: { x: "required" } };',
|
|
137
|
+
"export function Primary() { return null; }",
|
|
138
|
+
"export function Secondary() { return null; }",
|
|
139
|
+
].join("\n")
|
|
140
|
+
);
|
|
141
|
+
const manifest = buildManifest(dir);
|
|
142
|
+
expect("contract" in manifest.Primary).toBe(false);
|
|
143
|
+
expect("contract" in manifest.Secondary).toBe(false);
|
|
144
|
+
expect(warn).toHaveBeenCalledOnce();
|
|
145
|
+
warn.mockRestore();
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("warns and skips a malformed declaration (unbalanced braces) → null", () => {
|
|
149
|
+
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
150
|
+
const c = extractContract(
|
|
151
|
+
"export const __ruactContract = { props: { a: 'required' ",
|
|
152
|
+
"Broken.tsx"
|
|
153
|
+
);
|
|
154
|
+
expect(c).toBeNull();
|
|
155
|
+
expect(warn).toHaveBeenCalledOnce();
|
|
156
|
+
warn.mockRestore();
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("warns and skips an information-less declaration → null", () => {
|
|
160
|
+
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
161
|
+
const c = extractContract("export const __ruactContract = {};", "Empty.tsx");
|
|
162
|
+
expect(c).toBeNull();
|
|
163
|
+
warn.mockRestore();
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("returns null when there is no __ruactContract export", () => {
|
|
167
|
+
expect(extractContract("export const Foo = 1;", "None.tsx")).toBeNull();
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it("does not treat __ruactContract itself as a component export", () => {
|
|
171
|
+
writeComponent(
|
|
172
|
+
"Widget.tsx",
|
|
173
|
+
[
|
|
174
|
+
'"use client";',
|
|
175
|
+
'export const __ruactContract = { props: { x: "required" } };',
|
|
176
|
+
"export function Widget() { return null; }",
|
|
177
|
+
].join("\n")
|
|
178
|
+
);
|
|
179
|
+
const manifest = buildManifest(dir);
|
|
180
|
+
expect(Object.keys(manifest)).toEqual(["Widget"]);
|
|
181
|
+
});
|
|
182
|
+
});
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
"version": "0.0.0-bundled",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"devDependencies": {
|
|
12
|
+
"typescript": "^5.5.0",
|
|
12
13
|
"vitest": "^2.0.0"
|
|
13
14
|
}
|
|
14
15
|
},
|
|
@@ -1259,12 +1260,27 @@
|
|
|
1259
1260
|
"node": ">=14.0.0"
|
|
1260
1261
|
}
|
|
1261
1262
|
},
|
|
1263
|
+
"node_modules/typescript": {
|
|
1264
|
+
"version": "5.9.3",
|
|
1265
|
+
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
|
1266
|
+
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
|
1267
|
+
"dev": true,
|
|
1268
|
+
"license": "Apache-2.0",
|
|
1269
|
+
"bin": {
|
|
1270
|
+
"tsc": "bin/tsc",
|
|
1271
|
+
"tsserver": "bin/tsserver"
|
|
1272
|
+
},
|
|
1273
|
+
"engines": {
|
|
1274
|
+
"node": ">=14.17"
|
|
1275
|
+
}
|
|
1276
|
+
},
|
|
1262
1277
|
"node_modules/vite": {
|
|
1263
1278
|
"version": "5.4.21",
|
|
1264
1279
|
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz",
|
|
1265
1280
|
"integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
|
|
1266
1281
|
"dev": true,
|
|
1267
1282
|
"license": "MIT",
|
|
1283
|
+
"peer": true,
|
|
1268
1284
|
"dependencies": {
|
|
1269
1285
|
"esbuild": "^0.21.3",
|
|
1270
1286
|
"postcss": "^8.4.43",
|
|
@@ -5,9 +5,11 @@
|
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
7
7
|
"scripts": {
|
|
8
|
-
"test": "vitest run"
|
|
8
|
+
"test": "vitest run",
|
|
9
|
+
"typecheck": "tsc --noEmit -p tsconfig.json && tsc --noEmit -p tsconfig.scaffold.json && tsc --noEmit -p tsconfig.scaffold-agnostic.json"
|
|
9
10
|
},
|
|
10
11
|
"devDependencies": {
|
|
12
|
+
"typescript": "^5.5.0",
|
|
11
13
|
"vitest": "^2.0.0"
|
|
12
14
|
},
|
|
13
15
|
"license": "MIT",
|