@ripple-ts/vite-plugin 0.3.10 → 0.3.11

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 CHANGED
@@ -1,5 +1,12 @@
1
1
  # @ripple-ts/vite-plugin
2
2
 
3
+ ## 0.3.11
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies []:
8
+ - @ripple-ts/adapter@0.3.11
9
+
3
10
  ## 0.3.10
4
11
 
5
12
  ### Patch Changes
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "description": "Vite plugin for Ripple",
4
4
  "license": "MIT",
5
5
  "author": "Dominic Gannaway",
6
- "version": "0.3.10",
6
+ "version": "0.3.11",
7
7
  "type": "module",
8
8
  "module": "src/index.js",
9
9
  "main": "src/index.js",
@@ -32,13 +32,13 @@
32
32
  "url": "https://github.com/Ripple-TS/ripple/issues"
33
33
  },
34
34
  "dependencies": {
35
- "@ripple-ts/adapter": "0.3.10"
35
+ "@ripple-ts/adapter": "0.3.11"
36
36
  },
37
37
  "devDependencies": {
38
38
  "@types/node": "^24.3.0",
39
39
  "type-fest": "^5.4.4",
40
40
  "vite": "^8.0.0",
41
- "ripple": "0.3.10"
41
+ "ripple": "0.3.11"
42
42
  },
43
43
  "publishConfig": {
44
44
  "access": "public"
package/src/index.js CHANGED
@@ -27,9 +27,19 @@ import { patch_global_fetch, is_rpc_request, handle_rpc_request } from '@ripple-
27
27
 
28
28
  // Re-export route classes
29
29
  export { RenderRoute, ServerRoute } from './routes.js';
30
+ export {
31
+ getRippleConfigPath,
32
+ loadRippleConfig,
33
+ resolveRippleConfig,
34
+ rippleConfigExists,
35
+ } from './load-config.js';
30
36
 
31
37
  const VITE_FS_PREFIX = '/@fs/';
32
38
  const IS_WINDOWS = process.platform === 'win32';
39
+ const VIRTUAL_HYDRATE_ID = 'virtual:ripple-hydrate';
40
+ const RESOLVED_VIRTUAL_HYDRATE_ID = '\0virtual:ripple-hydrate';
41
+ const VIRTUAL_COMPAT_ID = 'virtual:ripple-compat';
42
+ const RESOLVED_VIRTUAL_COMPAT_ID = '\0virtual:ripple-compat';
33
43
 
34
44
  // Dev server always runs in Node — use node:async_hooks as default runtime
35
45
  // If the user provides adapter.runtime in their config, that will be used instead.
@@ -67,6 +77,60 @@ function getDevAsyncContext(config) {
67
77
  return devAsyncContext;
68
78
  }
69
79
 
80
+ /**
81
+ * @param {ResolvedRippleConfig | null} config
82
+ * @returns {string}
83
+ */
84
+ function create_compat_virtual_module(config) {
85
+ const compat_entries = Object.entries(config?.compat ?? {});
86
+
87
+ if (compat_entries.length === 0) {
88
+ return `const compat = undefined;
89
+ globalThis.__RIPPLE_COMPAT__ = compat;
90
+ export { compat };
91
+ export default compat;
92
+ `;
93
+ }
94
+
95
+ const imports = [];
96
+ const properties = [];
97
+
98
+ for (let i = 0; i < compat_entries.length; i++) {
99
+ const [kind, entry] = compat_entries[i];
100
+ const local_name = `__ripple_compat_factory_${i}`;
101
+
102
+ if (entry.factory) {
103
+ imports.push(
104
+ `import { ${entry.factory} as ${local_name} } from ${JSON.stringify(entry.from)};`,
105
+ );
106
+ } else {
107
+ imports.push(`import ${local_name} from ${JSON.stringify(entry.from)};`);
108
+ }
109
+
110
+ properties.push(` ${JSON.stringify(kind)}: ${local_name}(),`);
111
+ }
112
+
113
+ return `${imports.join('\n')}
114
+
115
+ const compat = {
116
+ ${properties.join('\n')}
117
+ };
118
+
119
+ globalThis.__RIPPLE_COMPAT__ = compat;
120
+
121
+ export { compat };
122
+ export default compat;
123
+ `;
124
+ }
125
+
126
+ /**
127
+ * @param {ResolvedRippleConfig | null} config
128
+ * @returns {boolean}
129
+ */
130
+ function has_route_config(config) {
131
+ return (config?.router.routes.length ?? 0) > 0;
132
+ }
133
+
70
134
  /**
71
135
  * @param {string} filename
72
136
  * @param {ResolvedConfig['root']} root
@@ -327,6 +391,18 @@ export function ripple(inlineOptions = {}) {
327
391
  /** @type {Set<string>} File paths (relative to root) of .ripple modules with #server blocks */
328
392
  const serverBlockModules = new Set();
329
393
 
394
+ /**
395
+ * @returns {Promise<ResolvedRippleConfig | null>}
396
+ */
397
+ async function get_current_ripple_config() {
398
+ if (loadedRippleConfig) return loadedRippleConfig;
399
+ if (rippleConfig) return rippleConfig;
400
+ if (!root || !rippleConfigExists(root)) return null;
401
+
402
+ loadedRippleConfig = await loadRippleConfig(root);
403
+ return loadedRippleConfig;
404
+ }
405
+
330
406
  /** @type {Plugin[]} */
331
407
  const plugins = [
332
408
  {
@@ -344,6 +420,12 @@ export function ripple(inlineOptions = {}) {
344
420
  const projectRoot = userConfig.root || process.cwd();
345
421
 
346
422
  if (rippleConfigExists(projectRoot)) {
423
+ loadedRippleConfig = await loadRippleConfig(projectRoot);
424
+
425
+ if (!has_route_config(loadedRippleConfig)) {
426
+ return null;
427
+ }
428
+
347
429
  const htmlInput = path.join(projectRoot, 'index.html');
348
430
  if (!fs.existsSync(htmlInput)) {
349
431
  throw new Error(
@@ -356,11 +438,9 @@ export function ripple(inlineOptions = {}) {
356
438
  '[@ripple-ts/vite-plugin] Detected ripple.config.ts — configuring client build',
357
439
  );
358
440
 
359
- // Load ripple.config.ts early so build options (e.g. minify) can
441
+ // The config was loaded above so build options (e.g. minify) can
360
442
  // influence the client build config returned from this hook.
361
- // The loaded config is cached and reused by
362
- // buildStart and closeBundle.
363
- loadedRippleConfig = await loadRippleConfig(projectRoot);
443
+ // The loaded config is cached and reused by buildStart/closeBundle.
364
444
 
365
445
  const outDir = loadedRippleConfig.build.outDir;
366
446
 
@@ -408,9 +488,11 @@ export function ripple(inlineOptions = {}) {
408
488
  }
409
489
 
410
490
  if (excludeRippleExternalModules) {
491
+ /** @type {string[]} */
492
+ const excluded = userConfig.optimizeDeps?.exclude || [];
411
493
  return {
412
494
  optimizeDeps: {
413
- exclude: userConfig.optimizeDeps?.exclude || [],
495
+ exclude: excluded,
414
496
  },
415
497
  };
416
498
  }
@@ -421,6 +503,7 @@ export function ripple(inlineOptions = {}) {
421
503
  detectedPackages.forEach((pkg) => {
422
504
  ripplePackages.add(pkg);
423
505
  });
506
+ /** @type {string[]} */
424
507
  const existingExclude = userConfig.optimizeDeps?.exclude || [];
425
508
  console.log('[@ripple-ts/vite-plugin] Scan complete. Found:', detectedPackages);
426
509
  console.log(
@@ -428,7 +511,9 @@ export function ripple(inlineOptions = {}) {
428
511
  existingExclude,
429
512
  );
430
513
  // Merge with existing exclude list
431
- const allExclude = [...new Set([...existingExclude, ...ripplePackages])];
514
+ const ripple_package_list = /** @type {string[]} */ (Array.from(ripplePackages));
515
+ /** @type {string[]} */
516
+ const allExclude = [...new Set([...existingExclude, ...ripple_package_list])];
432
517
 
433
518
  console.log(`[@ripple-ts/vite-plugin] Merged 'optimizeDeps.exclude':`, allExclude);
434
519
  console.log(
@@ -464,6 +549,8 @@ export function ripple(inlineOptions = {}) {
464
549
  loadedRippleConfig = await loadRippleConfig(root);
465
550
  }
466
551
 
552
+ if (!has_route_config(loadedRippleConfig)) return;
553
+
467
554
  renderRouteEntries = loadedRippleConfig.router.routes
468
555
  .filter((/** @type {Route} */ r) => r.type === 'render')
469
556
  .map((/** @type {RenderRoute} */ r) => r.entry);
@@ -488,6 +575,10 @@ export function ripple(inlineOptions = {}) {
488
575
  try {
489
576
  rippleConfig = await loadRippleConfig(root, { vite });
490
577
 
578
+ if (!has_route_config(rippleConfig)) {
579
+ return;
580
+ }
581
+
491
582
  // Create router from config
492
583
  router = createRouter(rippleConfig.router.routes);
493
584
  console.log(
@@ -687,7 +778,7 @@ export function ripple(inlineOptions = {}) {
687
778
  transformIndexHtml: {
688
779
  order: 'pre',
689
780
  handler(html) {
690
- if (!isBuild || isSSRBuild) return html;
781
+ if (!isBuild || isSSRBuild || !has_route_config(loadedRippleConfig)) return html;
691
782
 
692
783
  // Inject the hydration client entry script before </body>
693
784
  const hydrationScript = `<script type="module" src="virtual:ripple-hydrate"></script>`;
@@ -708,6 +799,8 @@ export function ripple(inlineOptions = {}) {
708
799
  loadedRippleConfig = await loadRippleConfig(root);
709
800
  }
710
801
 
802
+ if (!has_route_config(loadedRippleConfig)) return;
803
+
711
804
  console.log('[@ripple-ts/vite-plugin] Client build done. Starting server build...');
712
805
 
713
806
  // Re-resolve with adapter validation for production builds.
@@ -897,8 +990,12 @@ export function ripple(inlineOptions = {}) {
897
990
 
898
991
  async resolveId(id, importer, options) {
899
992
  // Handle virtual hydrate module
900
- if (id === 'virtual:ripple-hydrate') {
901
- return '\0virtual:ripple-hydrate';
993
+ if (id === VIRTUAL_HYDRATE_ID) {
994
+ return RESOLVED_VIRTUAL_HYDRATE_ID;
995
+ }
996
+
997
+ if (id === VIRTUAL_COMPAT_ID) {
998
+ return RESOLVED_VIRTUAL_COMPAT_ID;
902
999
  }
903
1000
 
904
1001
  // Skip non-package imports (relative/absolute paths)
@@ -941,8 +1038,13 @@ export function ripple(inlineOptions = {}) {
941
1038
  },
942
1039
 
943
1040
  async load(id, opts) {
1041
+ if (id === RESOLVED_VIRTUAL_COMPAT_ID) {
1042
+ const compat_config = await get_current_ripple_config();
1043
+ return create_compat_virtual_module(compat_config);
1044
+ }
1045
+
944
1046
  // Handle virtual hydrate module
945
- if (id === '\0virtual:ripple-hydrate') {
1047
+ if (id === RESOLVED_VIRTUAL_HYDRATE_ID) {
946
1048
  if (isBuild && renderRouteEntries.length > 0) {
947
1049
  // Production: generate static import map so Vite bundles page components
948
1050
  const importMapLines = renderRouteEntries
@@ -955,6 +1057,7 @@ export function ripple(inlineOptions = {}) {
955
1057
  // main bundle awaits page module import → page module awaits main bundle's
956
1058
  // TLA to complete → circular wait.
957
1059
  return `
1060
+ import ${JSON.stringify(VIRTUAL_COMPAT_ID)};
958
1061
  import { hydrate, mount } from 'ripple';
959
1062
 
960
1063
  const routeModules = {
@@ -1004,6 +1107,7 @@ ${importMapLines}
1004
1107
  // Dev mode: use async IIFE to avoid top-level await deadlock
1005
1108
  // (same reason as production — page modules import from the main bundle)
1006
1109
  return `
1110
+ import ${JSON.stringify(VIRTUAL_COMPAT_ID)};
1007
1111
  import { hydrate, mount } from 'ripple';
1008
1112
 
1009
1113
  (async () => {
@@ -1052,11 +1156,16 @@ import { hydrate, mount } from 'ripple';
1052
1156
  const ssr = opts?.ssr === true || this.environment.config.consumer === 'server';
1053
1157
 
1054
1158
  const is_dev = config?.command === 'serve';
1159
+ const current_ripple_config = await get_current_ripple_config();
1055
1160
 
1056
1161
  const { js, css } = await compile(code, filename, {
1057
1162
  mode: ssr ? 'server' : 'client',
1058
1163
  dev: is_dev,
1059
1164
  hmr: is_dev && !ssr,
1165
+ compat_kinds:
1166
+ current_ripple_config === null
1167
+ ? undefined
1168
+ : Object.keys(current_ripple_config.compat),
1060
1169
  });
1061
1170
 
1062
1171
  // Track modules with #server blocks for RPC (client build only)
@@ -15,12 +15,67 @@
15
15
  * and the generated production server entry.
16
16
  */
17
17
 
18
- /** @import { RippleConfigOptions, ResolvedRippleConfig } from '@ripple-ts/vite-plugin' */
18
+ /** @import { CompatFactoryConfig, RippleConfigOptions, ResolvedRippleConfig } from '@ripple-ts/vite-plugin' */
19
19
 
20
20
  import path from 'node:path';
21
21
  import fs from 'node:fs';
22
22
  import { DEFAULT_OUTDIR } from './constants.js';
23
23
 
24
+ /**
25
+ * @param {unknown} entry
26
+ * @returns {entry is CompatFactoryConfig}
27
+ */
28
+ function is_compat_descriptor(entry) {
29
+ return !!entry && typeof entry === 'object' && 'from' in entry;
30
+ }
31
+
32
+ /**
33
+ * @param {unknown} entry
34
+ * @returns {entry is { __ripple_compat__: CompatFactoryConfig }}
35
+ */
36
+ function is_compat_branded_entry(entry) {
37
+ return (
38
+ !!entry &&
39
+ (typeof entry === 'function' || typeof entry === 'object') &&
40
+ '__ripple_compat__' in entry &&
41
+ is_compat_descriptor(entry.__ripple_compat__)
42
+ );
43
+ }
44
+
45
+ /**
46
+ * @param {string} kind
47
+ * @param {unknown} entry
48
+ * @returns {CompatFactoryConfig}
49
+ */
50
+ function normalize_compat_entry(kind, entry) {
51
+ if (is_compat_branded_entry(entry)) {
52
+ entry = entry.__ripple_compat__;
53
+ }
54
+
55
+ if (!is_compat_descriptor(entry)) {
56
+ throw new Error(
57
+ `[@ripple-ts/vite-plugin] ripple.config.ts compat.${kind} must be either a compat descriptor, a compat factory, or an invoked compat entry.`,
58
+ );
59
+ }
60
+
61
+ if (typeof entry.from !== 'string' || entry.from.length === 0) {
62
+ throw new Error(
63
+ `[@ripple-ts/vite-plugin] ripple.config.ts compat.${kind}.from must be a non-empty string.`,
64
+ );
65
+ }
66
+
67
+ if (entry.factory !== undefined && typeof entry.factory !== 'string') {
68
+ throw new Error(
69
+ `[@ripple-ts/vite-plugin] ripple.config.ts compat.${kind}.factory must be a string when provided.`,
70
+ );
71
+ }
72
+
73
+ return {
74
+ from: entry.from,
75
+ ...(entry.factory ? { factory: entry.factory } : {}),
76
+ };
77
+ }
78
+
24
79
  /**
25
80
  * Validate a raw ripple config and apply all defaults.
26
81
  *
@@ -46,10 +101,6 @@ export function resolveRippleConfig(raw, options = {}) {
46
101
  );
47
102
  }
48
103
 
49
- if (!raw.router?.routes) {
50
- throw new Error('[@ripple-ts/vite-plugin] ripple.config.ts must define `router.routes`.');
51
- }
52
-
53
104
  if (requireAdapter) {
54
105
  if (!raw.adapter) {
55
106
  throw new Error(
@@ -77,9 +128,15 @@ export function resolveRippleConfig(raw, options = {}) {
77
128
  },
78
129
  adapter: raw.adapter,
79
130
  router: {
80
- routes: raw.router.routes,
131
+ routes: raw.router?.routes ?? [],
81
132
  },
82
133
  middlewares: raw.middlewares ?? [],
134
+ compat: Object.fromEntries(
135
+ Object.entries(raw.compat ?? {}).map(([kind, entry]) => [
136
+ kind,
137
+ normalize_compat_entry(kind, entry),
138
+ ]),
139
+ ),
83
140
  platform: {
84
141
  env: raw.platform?.env ?? {},
85
142
  },
package/types/index.d.ts CHANGED
@@ -99,6 +99,26 @@ declare module '@ripple-ts/vite-plugin' {
99
99
  excludeRippleExternalModules?: boolean;
100
100
  }
101
101
 
102
+ export interface CompatFactoryConfig {
103
+ /** Module specifier that exports the compat factory */
104
+ from: string;
105
+ /** Named export to call. Omit to use the module's default export. */
106
+ factory?: string;
107
+ }
108
+
109
+ export interface CompatFactory<T = unknown> {
110
+ (): T;
111
+ __ripple_compat__: CompatFactoryConfig;
112
+ }
113
+
114
+ export interface CompatEntryValue {
115
+ __ripple_compat__: CompatFactoryConfig;
116
+ }
117
+
118
+ export type CompatConfigEntry = CompatFactoryConfig | CompatFactory | CompatEntryValue;
119
+
120
+ export type CompatConfig = Record<string, CompatConfigEntry>;
121
+
102
122
  export interface RippleConfigOptions {
103
123
  build?: {
104
124
  /** Output directory for the production build. @default 'dist' */
@@ -119,11 +139,21 @@ declare module '@ripple-ts/vite-plugin' {
119
139
  */
120
140
  runtime: RuntimePrimitives;
121
141
  };
122
- router: {
142
+ router?: {
123
143
  routes: Route[];
124
144
  };
125
145
  /** Global middlewares applied to all routes */
126
146
  middlewares?: Middleware[];
147
+ /**
148
+ * Client-side TSX compat integrations keyed by kind, e.g. `react` for `<tsx:react>`.
149
+ *
150
+ * You can either pass a descriptor object or import a compat factory directly,
151
+ * as long as that factory export carries Ripple compat metadata.
152
+ *
153
+ * These are compiled into a browser-side compat registry by the Vite plugin,
154
+ * allowing `mount()` / `hydrate()` to pick them up automatically.
155
+ */
156
+ compat?: CompatConfig;
127
157
  platform?: {
128
158
  env: Record<string, string>;
129
159
  };
@@ -163,6 +193,8 @@ declare module '@ripple-ts/vite-plugin' {
163
193
  };
164
194
  /** @default [] */
165
195
  middlewares: Middleware[];
196
+ /** @default {} */
197
+ compat: Record<string, CompatFactoryConfig>;
166
198
  platform: {
167
199
  /** @default {} */
168
200
  env: Record<string, string>;