ruact 0.0.15 → 0.0.17

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 (44) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +42 -1
  3. data/lib/generators/ruact/install/install_generator.rb +2 -1
  4. data/lib/generators/ruact/install/templates/AGENTS.md.tt +2 -2
  5. data/lib/ruact/client_manifest.rb +11 -2
  6. data/lib/ruact/component_attributes.rb +91 -0
  7. data/lib/ruact/configuration.rb +28 -0
  8. data/lib/ruact/controller.rb +3 -1
  9. data/lib/ruact/doctor.rb +64 -4
  10. data/lib/ruact/erb_preprocessor.rb +1 -47
  11. data/lib/ruact/flight/renderer.rb +23 -9
  12. data/lib/ruact/flight/request.rb +31 -2
  13. data/lib/ruact/flight/row_emitter.rb +9 -0
  14. data/lib/ruact/flight/serializer.rb +22 -7
  15. data/lib/ruact/render_pipeline.rb +10 -3
  16. data/lib/ruact/server.rb +11 -7
  17. data/lib/ruact/server_functions/action_assigns.rb +52 -0
  18. data/lib/ruact/server_functions/bucket_two_payload.rb +1 -1
  19. data/lib/ruact/testing/component_query.rb +11 -4
  20. data/lib/ruact/testing/flight_wire_parser.rb +2 -2
  21. data/lib/ruact/version.rb +1 -1
  22. data/lib/ruact.rb +1 -0
  23. data/vendor/javascript/vite-plugin-ruact/bootstrap.test.mjs +44 -2
  24. data/vendor/javascript/vite-plugin-ruact/flight-conformance.test.mjs +286 -0
  25. data/vendor/javascript/vite-plugin-ruact/index.js +89 -14
  26. data/vendor/javascript/vite-plugin-ruact/package-lock.json +804 -3
  27. data/vendor/javascript/vite-plugin-ruact/package.json +5 -1
  28. data/vendor/javascript/vite-plugin-ruact/ruact-router.test.mjs +53 -0
  29. data/vendor/javascript/vite-plugin-ruact/runtime/bootstrap.jsx +46 -17
  30. data/vendor/javascript/vite-plugin-ruact/runtime/flight-modules.js +40 -0
  31. data/vendor/javascript/vite-plugin-ruact/runtime/flight-webpack-globals.js +15 -0
  32. data/vendor/javascript/vite-plugin-ruact/runtime/root-boundary.js +41 -0
  33. data/vendor/javascript/vite-plugin-ruact/runtime/ruact-router.js +75 -88
  34. data/vendor/javascript/vite-plugin-ruact/runtime/suspense-boundary.js +83 -0
  35. data/vendor/javascript/vite-plugin-ruact/runtime/transport.js +41 -0
  36. data/vendor/javascript/vite-plugin-ruact/runtime/vendor/react-server-dom-webpack/LICENSE +21 -0
  37. data/vendor/javascript/vite-plugin-ruact/runtime/vendor/react-server-dom-webpack/VERSION.json +5 -0
  38. data/vendor/javascript/vite-plugin-ruact/runtime/vendor/react-server-dom-webpack/client.browser.development.js +5473 -0
  39. data/vendor/javascript/vite-plugin-ruact/runtime/vendor/react-server-dom-webpack/client.browser.production.js +2126 -0
  40. data/vendor/javascript/vite-plugin-ruact/scripts/vendor-flight-client.mjs +124 -0
  41. data/vendor/javascript/vite-plugin-ruact/vitest.config.mjs +14 -0
  42. metadata +15 -4
  43. data/vendor/javascript/vite-plugin-ruact/flight-client.test.mjs +0 -452
  44. data/vendor/javascript/vite-plugin-ruact/runtime/flight-client.js +0 -408
@@ -1,6 +1,7 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
+ import { createRequire } from "node:module";
4
5
  import { installServerFunctionsHooks } from "./server-functions-codegen.mjs";
5
6
 
6
7
  /**
@@ -56,6 +57,11 @@ const RESOLVED_REGISTRY_ID = "\0" + REGISTRY_VIRTUAL_ID;
56
57
  // `/@id/__x00__virtual:ruact/bootstrap`; prod Vite-manifest key
57
58
  // `virtual:ruact/bootstrap`). Mirrors REGISTRY_VIRTUAL_ID exactly.
58
59
  export const BOOTSTRAP_VIRTUAL_ID = "virtual:ruact/bootstrap";
60
+
61
+ // Story 18-1 — React's Flight client, as the runtime imports it. The plugin
62
+ // decides which copy answers (see `flightClientSource`).
63
+ export const FLIGHT_CLIENT_VIRTUAL_ID = "virtual:ruact/flight-client";
64
+ const RESOLVED_FLIGHT_CLIENT_ID = "\0" + FLIGHT_CLIENT_VIRTUAL_ID;
59
65
  const RESOLVED_BOOTSTRAP_ID = "\0" + BOOTSTRAP_VIRTUAL_ID;
60
66
 
61
67
  // The gem-shipped runtime sources the virtual bootstrap pulls in. Resolved
@@ -71,18 +77,30 @@ export default function ruact(options = {}) {
71
77
 
72
78
  let root;
73
79
  let manifest = {};
80
+ let flightClient; // { source: "app" | "vendored", version, mode }
74
81
 
75
82
  return installServerFunctionsHooks({
76
83
  name: "vite-plugin-ruact",
77
84
 
85
+ // Story 18-1 — the runtime (and React's Flight client vendored with it) is
86
+ // served from the gem's directory, not the app's. Its `react` and
87
+ // `react-dom` imports must still be the app's: one React instance.
88
+ config() {
89
+ return { resolve: { dedupe: ["react", "react-dom"] } };
90
+ },
91
+
78
92
  configResolved(config) {
79
93
  root = config.root;
94
+ flightClient = resolveFlightClient(root, config.isProduction ? "production" : "development");
95
+ // Dev server and build alike, so a build log shows it too.
96
+ config.logger?.info?.(`[ruact] Flight client: ${describeFlightClient(flightClient)}`);
80
97
  },
81
98
 
82
99
  // Story 10.1b / 14.2 — resolve ruact's virtual module ids.
83
100
  resolveId(id) {
84
101
  if (id === REGISTRY_VIRTUAL_ID) return RESOLVED_REGISTRY_ID;
85
102
  if (id === BOOTSTRAP_VIRTUAL_ID) return RESOLVED_BOOTSTRAP_ID;
103
+ if (id === FLIGHT_CLIENT_VIRTUAL_ID) return RESOLVED_FLIGHT_CLIENT_ID;
86
104
  return null;
87
105
  },
88
106
 
@@ -96,6 +114,7 @@ export default function ruact(options = {}) {
96
114
  // Story 14.2 — serve the gem-shipped bootstrap source, with its relative
97
115
  // runtime imports rewritten to absolute fs specifiers (see below).
98
116
  if (id === RESOLVED_BOOTSTRAP_ID) return generateBootstrapSource();
117
+ if (id === RESOLVED_FLIGHT_CLIENT_ID) return flightClientSource(flightClient);
99
118
  return null;
100
119
  },
101
120
 
@@ -303,28 +322,84 @@ export function generateRegistrySource(manifest) {
303
322
  // Story 14.2 (FR104) — render the virtual bootstrap source from the gem-shipped
304
323
  // `runtime/bootstrap.jsx`. A `load` hook returns module TEXT whose relative
305
324
  // imports resolve against the resolved id (`\0virtual:ruact/bootstrap`), which
306
- // is NOT a filesystem path — so `./flight-client.js` / `./ruact-router.js`
307
- // would fail. We rewrite those two specifiers to ABSOLUTE fs specifiers into the
325
+ // is NOT a filesystem path — so `./flight-modules.js` / `./ruact-router.js`
326
+ // would fail. We rewrite those specifiers to ABSOLUTE fs specifiers into the
308
327
  // gem `runtime/` dir (the same technique `generateRegistrySource` uses via
309
328
  // `toImportSpecifier`). The bare `react` / `react-dom/client` specifiers and the
310
329
  // `virtual:ruact/registry` id are left untouched — Vite resolves them from the
311
330
  // app root (so React comes from the USER's node_modules: one React instance).
312
331
  export function generateBootstrapSource(runtimeDir = RUNTIME_DIR) {
313
332
  const src = fs.readFileSync(path.join(runtimeDir, "bootstrap.jsx"), "utf8");
314
- const abs = (name) => toImportSpecifier(path.join(runtimeDir, name));
315
- // Rewrite EVERY `from './<runtime>.js'` import specifier to its absolute fs
316
- // path. Matching the quoted `from '...'` form (not the bare filename) avoids
317
- // hitting prose mentions of the file in comments, and the global regex
318
- // tolerates either quote style. The replacement is supplied as a function so a
319
- // `$` in the absolute path is never treated as a replacement pattern.
320
- const rewrite = (code, name) => {
321
- const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
322
- return code.replace(
323
- new RegExp(`from\\s+(['"])\\./${escaped}\\1`, "g"),
324
- () => `from '${abs(name)}'`,
333
+ // Rewrite EVERY relative `from './<runtime>.js'` and side-effect
334
+ // `import './<runtime>.js'` specifier to its absolute fs path. Matching the
335
+ // quoted specifier after `from`/`import` (not the bare filename) leaves prose
336
+ // mentions of a file in comments alone, and either quote style is accepted.
337
+ // The replacement is a function so a `$` in the absolute path is never read
338
+ // as a replacement pattern.
339
+ return src.replace(
340
+ /\b(from|import)(\s+)(['"])\.\/([\w.-]+\.js)\3/g,
341
+ (_match, keyword, space, _quote, name) =>
342
+ `${keyword}${space}'${toImportSpecifier(path.join(runtimeDir, name))}'`,
343
+ );
344
+ }
345
+
346
+ // Story 18-1 (packaging option C, docs/internal/decisions/flight-client-compatibility.md)
347
+ // — which copy of React's Flight client the runtime uses. The app's own
348
+ // `react-server-dom-webpack` when the app declares it in its package.json (an
349
+ // app that needs a different React version installs it); otherwise the copy
350
+ // vendored in the gem, which brings no `webpack` into the app's install. A copy
351
+ // that only resolves — another package's dependency, hoisted — is not chosen:
352
+ // it was not picked for this app's React.
353
+ export function resolveFlightClient(appRoot, mode, runtimeDir = RUNTIME_DIR) {
354
+ const vendored = () => {
355
+ const { version } = JSON.parse(
356
+ fs.readFileSync(path.join(runtimeDir, "vendor", "react-server-dom-webpack", "VERSION.json"), "utf8"),
325
357
  );
358
+ return { source: "vendored", version, mode };
326
359
  };
327
- return rewrite(rewrite(src, "flight-client.js"), "ruact-router.js");
360
+ let declared = false;
361
+ try {
362
+ const app = JSON.parse(fs.readFileSync(path.join(appRoot, "package.json"), "utf8"));
363
+ declared = Boolean(app.dependencies?.["react-server-dom-webpack"] ?? app.devDependencies?.["react-server-dom-webpack"]);
364
+ } catch {
365
+ return vendored();
366
+ }
367
+ if (!declared) return vendored();
368
+ try {
369
+ const require = createRequire(path.join(appRoot, "package.json"));
370
+ const pkg = require("react-server-dom-webpack/package.json");
371
+ return { source: "app", version: pkg.version, mode };
372
+ } catch {
373
+ // Declared but not installed: npm install has not run. The vendored copy
374
+ // keeps the page working meanwhile.
375
+ return vendored();
376
+ }
377
+ }
378
+
379
+ export function describeFlightClient({ source, version, mode }) {
380
+ return source === "app"
381
+ ? `react-server-dom-webpack ${version} from the app (${mode})`
382
+ : `react-server-dom-webpack ${version} vendored in ruact (${mode})`;
383
+ }
384
+
385
+ // The `virtual:ruact/flight-client` module. The vendored copy is an ES module
386
+ // whose webpack hooks are module-local. The app's copy is CommonJS reading
387
+ // webpack's globals, which must exist before it evaluates — hence the
388
+ // side-effect import first.
389
+ export function flightClientSource({ source, mode }, runtimeDir = RUNTIME_DIR) {
390
+ const names = "createFromReadableStream, createFromFetch";
391
+ if (source === "app") {
392
+ const globals = toImportSpecifier(path.join(runtimeDir, "flight-webpack-globals.js"));
393
+ return [
394
+ `import '${globals}';`,
395
+ `export { ${names} } from 'react-server-dom-webpack/client.browser';`,
396
+ "",
397
+ ].join("\n");
398
+ }
399
+ const vendored = toImportSpecifier(
400
+ path.join(runtimeDir, "vendor", "react-server-dom-webpack", `client.browser.${mode}.js`),
401
+ );
402
+ return `export { ${names} } from '${vendored}';\n`;
328
403
  }
329
404
 
330
405
  // A bundler import specifier for an absolute fs path. Vite/Rollup resolve