@octanejs/vite-plugin 0.1.6 → 0.1.10

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@octanejs/vite-plugin",
3
- "version": "0.1.6",
3
+ "version": "0.1.10",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "engines": {
@@ -47,16 +47,17 @@
47
47
  },
48
48
  "dependencies": {
49
49
  "@ripple-ts/adapter": "^0.3.86",
50
- "@octanejs/app-core": "0.0.2"
50
+ "@octanejs/app-core": "0.0.6"
51
51
  },
52
52
  "peerDependencies": {
53
53
  "vite": "^8.0.16",
54
- "octane": "0.1.6"
54
+ "octane": "0.1.10"
55
55
  },
56
56
  "devDependencies": {
57
57
  "@types/node": "^24.3.0",
58
+ "playwright": "^1.61.0",
58
59
  "type-fest": "^5.6.0",
59
60
  "vite": "^8.0.16",
60
- "octane": "0.1.6"
61
+ "octane": "0.1.10"
61
62
  }
62
63
  }
@@ -0,0 +1,81 @@
1
+ // @ts-check
2
+ import { HYDRATE_QUERY_PARAM } from 'octane/compiler/bundler';
3
+
4
+ /**
5
+ * @typedef {{
6
+ * file: string,
7
+ * src?: string,
8
+ * css?: string[],
9
+ * imports?: string[],
10
+ * dynamicImports?: string[],
11
+ * }} ViteManifestEntry
12
+ */
13
+
14
+ /** @param {string | undefined} id */
15
+ function isDeferredHydrationId(id) {
16
+ if (!id) return false;
17
+ const queryStart = id.indexOf('?');
18
+ if (queryStart === -1) return false;
19
+ const hashStart = id.indexOf('#', queryStart);
20
+ const query = id.slice(queryStart + 1, hashStart === -1 ? undefined : hashStart);
21
+ return new URLSearchParams(query).has(HYDRATE_QUERY_PARAM);
22
+ }
23
+
24
+ /**
25
+ * Build the route asset map consumed by the production server.
26
+ *
27
+ * A normal dynamic import stays lazy in both channels. Compiler-generated
28
+ * `?octane-hydrate=` imports are different: their JavaScript remains deferred,
29
+ * but their CSS must be present while the server-rendered boundary is inert.
30
+ * Once inside one of those branches, collect CSS through the whole async
31
+ * descendant graph so nested Hydrate/lazy components cannot flash unstyled.
32
+ *
33
+ * @param {Record<string, ViteManifestEntry>} manifest
34
+ * @param {string[]} moduleIds
35
+ * @returns {Record<string, { js: string, css: string[] }>}
36
+ */
37
+ export function createClientAssetMap(manifest, moduleIds) {
38
+ /**
39
+ * @param {string} key
40
+ * @param {boolean} deferredHydrationBranch
41
+ * @param {Set<string>} visited
42
+ * @returns {string[]}
43
+ */
44
+ function collectCss(key, deferredHydrationBranch, visited) {
45
+ const visitKey = `${deferredHydrationBranch ? 'deferred' : 'eager'}:${key}`;
46
+ if (visited.has(visitKey)) return [];
47
+ visited.add(visitKey);
48
+ const entry = manifest[key];
49
+ if (!entry) return [];
50
+
51
+ const css = [...(entry.css || [])];
52
+ for (const imported of entry.imports || []) {
53
+ css.push(...collectCss(imported, deferredHydrationBranch, visited));
54
+ }
55
+ for (const imported of entry.dynamicImports || []) {
56
+ const importedEntry = manifest[imported];
57
+ const entersDeferredHydration =
58
+ deferredHydrationBranch ||
59
+ isDeferredHydrationId(imported) ||
60
+ isDeferredHydrationId(importedEntry?.src);
61
+ if (entersDeferredHydration) {
62
+ css.push(...collectCss(imported, true, visited));
63
+ }
64
+ }
65
+ return css;
66
+ }
67
+
68
+ /** @type {Record<string, { js: string, css: string[] }>} */
69
+ const assets = {};
70
+ for (const moduleId of moduleIds) {
71
+ // Vite manifest keys are root-relative without the leading slash.
72
+ const manifestKey = moduleId.startsWith('/') ? moduleId.slice(1) : moduleId;
73
+ const entry = manifest[manifestKey];
74
+ if (!entry) continue;
75
+ assets[moduleId] = {
76
+ js: entry.file,
77
+ css: [...new Set(collectCss(manifestKey, false, new Set()))],
78
+ };
79
+ }
80
+ return assets;
81
+ }
package/src/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // @ts-check
2
- /** @import {Plugin, ResolvedConfig, ViteDevServer, UserConfig} from 'vite' */
3
- /** @import {OctaneConfigOptions, ResolvedOctaneConfig, RenderRoute} from '@octanejs/vite-plugin' */
2
+ /** @import {Plugin, RenderBuiltAssetUrl, ResolvedConfig, ViteDevServer, UserConfig} from 'vite' */
3
+ /** @import {LoadedOctaneConfig, OctaneConfigOptions, ResolvedOctaneConfig, RenderRoute} from '@octanejs/vite-plugin' */
4
4
 
5
5
  import fs from 'node:fs';
6
6
  import path from 'node:path';
@@ -33,6 +33,7 @@ import {
33
33
  to_vite_root_import,
34
34
  write_project_generated_file,
35
35
  } from './project-codegen.js';
36
+ import { createClientAssetMap } from './client-assets.js';
36
37
 
37
38
  import { patch_global_fetch, is_rpc_request, handle_rpc_request } from '@ripple-ts/adapter/rpc';
38
39
 
@@ -168,12 +169,37 @@ function has_route_config(config) {
168
169
  return (config?.router.routes.length ?? 0) > 0;
169
170
  }
170
171
 
172
+ /**
173
+ * Every module path the server can name in #__octane_data — page entries,
174
+ * layouts, the preHydrate hook, and root boundaries. The generated hydrate
175
+ * entry maps each as a LITERAL `() => import('/src/…')`. Production needs the
176
+ * map so Rollup chunks and hashes the modules; dev needs it so the imports go
177
+ * through Vite's import analysis and share URL identity with every other
178
+ * importer (see the hydrate-entry load hook).
179
+ *
180
+ * @param {ResolvedOctaneConfig | null} config
181
+ * @returns {string[]}
182
+ */
183
+ function collect_hydrate_module_paths(config) {
184
+ if (!has_route_config(config)) return [];
185
+ const cfg = /** @type {ResolvedOctaneConfig} */ (config);
186
+ const entries = cfg.router.routes
187
+ .filter((r) => r.type === 'render')
188
+ .flatMap((r) => [get_route_entry_path(/** @type {RenderRoute} */ (r).entry), r.layout]);
189
+ if (cfg.router.preHydrate) entries.push(cfg.router.preHydrate);
190
+ entries.push(
191
+ get_route_entry_path(cfg.rootBoundary.pending),
192
+ get_route_entry_path(cfg.rootBoundary.catch),
193
+ );
194
+ return [...new Set(entries.filter((e) => typeof e === 'string'))];
195
+ }
196
+
171
197
  /**
172
198
  * The recommended Octane Vite integration. With no octane.config.ts it behaves
173
199
  * as a compiler plugin inside a normal Vite SPA; configured routes activate the
174
200
  * metaframework layer.
175
201
  *
176
- * Returns an ARRAY: `[octaneCompiler({ hmr }), metaPlugin]`. The first element is
202
+ * Returns an ARRAY: `[octaneCompiler(options), metaPlugin]`. The first element is
177
203
  * octane/compiler's transform plugin — it owns ALL `.tsrx` compilation, picking
178
204
  * client vs server mode per-module from Vite's SSR signal (so the SAME file
179
205
  * compiles to a DOM-clone client body for the browser and to an HTML-building
@@ -188,7 +214,7 @@ function has_route_config(config) {
188
214
  * node builtins external) exporting `handler`/`nodeHandler` and auto-booting
189
215
  * under `node`. See server/virtual-entry.js and server/production.js.
190
216
  *
191
- * @param {{ hmr?: boolean, exclude?: string[] }} [inlineOptions]
217
+ * @param {{ hmr?: boolean, profile?: boolean, exclude?: string[], requireDirective?: boolean, renderers?: import('@octanejs/app-core').ExperimentalRendererConfigOptions }} [inlineOptions]
192
218
  * @returns {Plugin[]}
193
219
  */
194
220
  export function octane(inlineOptions = {}) {
@@ -204,6 +230,15 @@ export function octane(inlineOptions = {}) {
204
230
  let isBuild = false;
205
231
  /** @type {boolean} Is this the SSR sub-build closeBundle launches? */
206
232
  let isSSRBuild = false;
233
+ /**
234
+ * Config dependencies that select compiler renderers. A change requires a
235
+ * server restart because the neutral compiler snapshots normalized renderer
236
+ * metadata before the first module transform.
237
+ * @type {Set<string>}
238
+ */
239
+ const rendererConfigWatchFiles = new Set();
240
+ /** @type {Map<string, Promise<LoadedOctaneConfig | null>>} */
241
+ const startupConfigLoads = new Map();
207
242
  /** @type {ResolvedOctaneConfig | null} Config loaded for the build (config hook, reused in closeBundle) */
208
243
  let buildOctaneConfig = null;
209
244
  /** @type {string[]} Module paths the generated client entry maps statically (build only) */
@@ -211,6 +246,25 @@ export function octane(inlineOptions = {}) {
211
246
  /** @type {Set<string>} Vite-root paths of modules containing `module server` */
212
247
  const serverModuleModules = new Set();
213
248
 
249
+ /**
250
+ * Load declarative app config early enough for the compiler plugin's own
251
+ * `config` hook. Cache per project root for the paired compiler/meta hooks;
252
+ * a dev-server restart constructs a fresh plugin instance and fresh snapshot.
253
+ *
254
+ * @param {string} projectRoot
255
+ * @returns {Promise<LoadedOctaneConfig | null>}
256
+ */
257
+ function loadStartupConfig(projectRoot) {
258
+ const resolvedRoot = path.resolve(projectRoot);
259
+ let load = startupConfigLoads.get(resolvedRoot);
260
+ if (load !== undefined) return load;
261
+ load = octaneConfigExists(resolvedRoot)
262
+ ? loadOctaneConfigWithMetadata(resolvedRoot)
263
+ : Promise.resolve(null);
264
+ startupConfigLoads.set(resolvedRoot, load);
265
+ return load;
266
+ }
267
+
214
268
  /** @type {Plugin} */
215
269
  const metaPlugin = {
216
270
  name: '@octanejs/vite-plugin',
@@ -274,7 +328,28 @@ export function octane(inlineOptions = {}) {
274
328
  if (buildOctaneConfig.build.target !== undefined) {
275
329
  buildConfig.target = buildOctaneConfig.build.target;
276
330
  }
277
- return { ...base, build: buildConfig };
331
+ const userRenderBuiltUrl = userConfig.experimental?.renderBuiltUrl;
332
+ /** @type {RenderBuiltAssetUrl} */
333
+ const renderBuiltUrl = (filename, context) => {
334
+ const userResult = userRenderBuiltUrl?.(filename, context);
335
+ if (userResult !== undefined) return userResult;
336
+
337
+ // Vite's production module-preload helper otherwise resolves its
338
+ // root-relative dependency URLs through document.baseURI. Generate
339
+ // module-relative JS asset URLs so an authored <base> cannot redirect
340
+ // route, layout, or pre-hydrate chunk preloads off the app origin.
341
+ if (!context.ssr && context.type === 'asset' && context.hostType === 'js') {
342
+ return { relative: true };
343
+ }
344
+ };
345
+ return {
346
+ ...base,
347
+ build: buildConfig,
348
+ experimental: {
349
+ ...userConfig.experimental,
350
+ renderBuiltUrl,
351
+ },
352
+ };
278
353
  }
279
354
  }
280
355
  }
@@ -291,16 +366,7 @@ export function octane(inlineOptions = {}) {
291
366
  buildStart() {
292
367
  if (!isBuild || isSSRBuild || !has_route_config(buildOctaneConfig)) return;
293
368
  serverModuleModules.clear();
294
- const cfg = /** @type {ResolvedOctaneConfig} */ (buildOctaneConfig);
295
- const entries = cfg.router.routes
296
- .filter((r) => r.type === 'render')
297
- .flatMap((r) => [get_route_entry_path(/** @type {RenderRoute} */ (r).entry), r.layout]);
298
- if (cfg.router.preHydrate) entries.push(cfg.router.preHydrate);
299
- entries.push(
300
- get_route_entry_path(cfg.rootBoundary.pending),
301
- get_route_entry_path(cfg.rootBoundary.catch),
302
- );
303
- staticEntries = [...new Set(entries.filter((e) => typeof e === 'string'))];
369
+ staticEntries = collect_hydrate_module_paths(buildOctaneConfig);
304
370
  },
305
371
 
306
372
  async configResolved(resolvedConfig) {
@@ -324,16 +390,29 @@ export function octane(inlineOptions = {}) {
324
390
  return create_adapter_browser_stub_source();
325
391
  }
326
392
  if (id === RESOLVED_VIRTUAL_HYDRATE_ID) {
327
- // Dev: dynamic import() of the route entry works through Vite, so the
328
- // static import map stays empty (the codegen falls back to a dynamic
329
- // import per entry). Production builds pass the routes' module paths
330
- // (collected in buildStart) so Rollup bundles them.
393
+ // Production builds pass the routes' module paths (collected in
394
+ // buildStart) so Rollup bundles them. Dev ALSO needs the literal map
395
+ // not for chunking (dev serves any module by URL) but for MODULE
396
+ // IDENTITY on a hot server: the codegen's fallback `dynamicImport(path)`
397
+ // is hidden from Vite's import analysis, so it fetches the BARE url
398
+ // while the page's own import chain fetches the analyzed url (`?import`
399
+ // for non-JS extensions, `?t=` stamps after an HMR invalidation). Two
400
+ // urls = two browser module instances — e.g. two app-router singletons,
401
+ // where preHydrate commits matches on one and the page renders the
402
+ // empty other, breaking hydration on every reload until the dev server
403
+ // restarts. Literal `import('/src/…')` entries go through import
404
+ // analysis and share url identity with every other importer.
405
+ let entries = staticEntries;
406
+ if (!isBuild) {
407
+ const loaded = octaneConfig ?? (await loadStartupConfig(root))?.config ?? null;
408
+ entries = collect_hydrate_module_paths(loaded);
409
+ }
331
410
  const file = write_project_generated_file(
332
411
  config,
333
412
  'client-entry.js',
334
413
  create_client_entry_source({
335
414
  configPath: to_vite_root_import(getOctaneConfigPath(root), root),
336
- staticEntries,
415
+ staticEntries: entries,
337
416
  }),
338
417
  );
339
418
  return fs.readFileSync(file, 'utf-8');
@@ -365,6 +444,9 @@ export function octane(inlineOptions = {}) {
365
444
  * @param {ViteDevServer} vite
366
445
  */
367
446
  configureServer(vite) {
447
+ if (rendererConfigWatchFiles.size > 0) {
448
+ vite.watcher.add([...rendererConfigWatchFiles]);
449
+ }
368
450
  /** @type {Promise<void> | null} */
369
451
  let initPromise = null;
370
452
  /** @type {number} */
@@ -537,6 +619,13 @@ export function octane(inlineOptions = {}) {
537
619
  order: 'pre',
538
620
  async handler({ file, modules, server }) {
539
621
  if (this.environment.name !== 'client') return;
622
+ if (rendererConfigWatchFiles.has(path.resolve(file))) {
623
+ // Renderer rules and boundary metadata are immutable inputs to every
624
+ // compiler environment. Rebuild the plugin/compiler snapshot instead
625
+ // of letting later transforms observe a mixture of old and new config.
626
+ await server.restart();
627
+ return [];
628
+ }
540
629
  if (modules.length > 0 && modules.every((m) => m.isSelfAccepting)) return;
541
630
  if (!is_octane_module_path(file)) return;
542
631
 
@@ -593,7 +682,7 @@ export function octane(inlineOptions = {}) {
593
682
  // tags the production server emits for the matched route).
594
683
  // ------------------------------------------------------------------
595
684
  const manifestPath = path.join(clientOutDir, '.vite', 'manifest.json');
596
- /** @type {Record<string, { file: string, css?: string[], imports?: string[] }>} */
685
+ /** @type {Record<string, { file: string, src?: string, css?: string[], imports?: string[], dynamicImports?: string[] }>} */
597
686
  let clientManifest = {};
598
687
  if (fs.existsSync(manifestPath)) {
599
688
  clientManifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
@@ -603,35 +692,7 @@ export function octane(inlineOptions = {}) {
603
692
  );
604
693
  }
605
694
 
606
- /**
607
- * All CSS a manifest entry needs, transitively (cycle-safe).
608
- * @param {string} key
609
- * @param {Set<string>} [visited]
610
- * @returns {string[]}
611
- */
612
- const collectCss = (key, visited = new Set()) => {
613
- if (visited.has(key)) return [];
614
- visited.add(key);
615
- const entry = clientManifest[key];
616
- if (!entry) return [];
617
- const css = [...(entry.css || [])];
618
- for (const imp of entry.imports || []) css.push(...collectCss(imp, visited));
619
- return css;
620
- };
621
-
622
- /** @type {Record<string, { js: string, css: string[] }>} */
623
- const clientAssetMap = {};
624
- for (const moduleId of staticEntries) {
625
- // Manifest keys are root-relative without the leading slash.
626
- const manifestKey = moduleId.startsWith('/') ? moduleId.slice(1) : moduleId;
627
- const manifestEntry = clientManifest[manifestKey];
628
- if (manifestEntry) {
629
- clientAssetMap[moduleId] = {
630
- js: manifestEntry.file,
631
- css: [...new Set(collectCss(manifestKey))],
632
- };
633
- }
634
- }
695
+ const clientAssetMap = createClientAssetMap(clientManifest, staticEntries);
635
696
 
636
697
  // The manifest was only needed here; leaving .vite/ in dist/client would
637
698
  // publish source file paths through the static server.
@@ -752,11 +813,58 @@ export function octane(inlineOptions = {}) {
752
813
  // `"octane": { "hookSlots": { "manual": ["src"] } }` in their own package.json and the
753
814
  // compiler plugin skips those directories via a nearest-manifest lookup.
754
815
  // Other installed raw-source Octane packages are transformed automatically.
816
+ /**
817
+ * @type {{
818
+ * hmr?: boolean,
819
+ * profile?: boolean,
820
+ * exclude?: string[],
821
+ * requireDirective?: boolean,
822
+ * renderers?: import('@octanejs/app-core').ExperimentalRendererConfigOptions,
823
+ * }}
824
+ */
755
825
  const compilerOptions = {};
756
826
  if (inlineOptions.hmr !== undefined) compilerOptions.hmr = inlineOptions.hmr;
827
+ if (inlineOptions.profile !== undefined) compilerOptions.profile = inlineOptions.profile;
757
828
  if (inlineOptions.exclude !== undefined) compilerOptions.exclude = inlineOptions.exclude;
829
+ if (inlineOptions.requireDirective !== undefined) {
830
+ compilerOptions.requireDirective = inlineOptions.requireDirective;
831
+ }
832
+ if (inlineOptions.renderers !== undefined) compilerOptions.renderers = inlineOptions.renderers;
833
+ const compilerPlugin = /** @type {Plugin} */ (octaneCompiler(compilerOptions));
834
+ const compilerConfigHook = compilerPlugin.config;
835
+ if (typeof compilerConfigHook === 'function') {
836
+ compilerPlugin.config = function compilerConfigWithAppRenderers(userConfig, env) {
837
+ const projectRoot = userConfig.root ? path.resolve(userConfig.root) : process.cwd();
838
+ // Inline renderer metadata is an explicit full override. Preserve the
839
+ // synchronous no-config/inline path used by compiler-only SPA projects.
840
+ if (inlineOptions.renderers !== undefined) {
841
+ rendererConfigWatchFiles.clear();
842
+ return compilerConfigHook.call(this, userConfig, env);
843
+ }
844
+
845
+ const configPath = getOctaneConfigPath(projectRoot);
846
+ if (!octaneConfigExists(projectRoot)) {
847
+ delete compilerOptions.renderers;
848
+ rendererConfigWatchFiles.clear();
849
+ // A newly-created octane.config.ts can introduce renderer rules. Watch
850
+ // the missing path so dev restarts into the configured compiler.
851
+ rendererConfigWatchFiles.add(path.resolve(configPath));
852
+ return compilerConfigHook.call(this, userConfig, env);
853
+ }
854
+
855
+ return loadStartupConfig(projectRoot).then((loaded) => {
856
+ const config = /** @type {LoadedOctaneConfig} */ (loaded);
857
+ compilerOptions.renderers = config.config.compiler.renderers;
858
+ rendererConfigWatchFiles.clear();
859
+ for (const file of [...config.dependencies, ...config.missingDependencies]) {
860
+ rendererConfigWatchFiles.add(path.resolve(file));
861
+ }
862
+ return compilerConfigHook.call(this, userConfig, env);
863
+ });
864
+ };
865
+ }
758
866
  // The compiler plugin is untyped JS (its `enforce` infers as `string`).
759
- return [/** @type {Plugin} */ (octaneCompiler(compilerOptions)), metaPlugin];
867
+ return [compilerPlugin, metaPlugin];
760
868
  }
761
869
 
762
870
  // Mainly to enforce types / DX.
@@ -1,5 +1,7 @@
1
1
  // @ts-check
2
2
 
3
+ import fs from 'node:fs';
4
+
3
5
  import {
4
6
  getOctaneConfigPath,
5
7
  loadOctaneConfig as loadCoreOctaneConfig,
@@ -78,9 +80,7 @@ async function withDefaultViteRunner(projectRoot, options, loader) {
78
80
  try {
79
81
  return await loader(projectRoot, {
80
82
  ...options,
81
- moduleRunner: {
82
- loadModule: (/** @type {string} */ id) => tempVite.ssrLoadModule(id),
83
- },
83
+ moduleRunner: viteConfigModuleRunner(tempVite),
84
84
  });
85
85
  } finally {
86
86
  await tempVite.close();
@@ -93,8 +93,51 @@ function withViteModuleRunner(options) {
93
93
  const { vite, ...rest } = options;
94
94
  return {
95
95
  ...rest,
96
- moduleRunner: {
97
- loadModule: (/** @type {string} */ id) => vite.ssrLoadModule(id),
96
+ moduleRunner: viteConfigModuleRunner(vite),
97
+ };
98
+ }
99
+
100
+ /**
101
+ * Adapt Vite's SSR runner and module graph to app-core's config-loader
102
+ * contract. Config evaluation itself is not enough: integrations also need
103
+ * the transitive file set so edits to imported renderer rules/boundary tables
104
+ * invalidate the compiler snapshot.
105
+ *
106
+ * @param {import('vite').ViteDevServer} vite
107
+ * @returns {import('@octanejs/app-core').ConfigModuleRunner}
108
+ */
109
+ function viteConfigModuleRunner(vite) {
110
+ return {
111
+ loadModule: (/** @type {string} */ id) => vite.ssrLoadModule(id),
112
+ getDependencies(id) {
113
+ const graph = vite.environments.ssr.moduleGraph;
114
+ const roots = new Set();
115
+ const candidates = new Set([id]);
116
+ try {
117
+ // Vite canonicalizes graph IDs through realpath. Preserve the
118
+ // config loader's lexical path in its own metadata, but use both
119
+ // forms to find the root (notably /var -> /private/var on macOS).
120
+ candidates.add(fs.realpathSync(id));
121
+ } catch {
122
+ // The config loader reports the useful missing-file error.
123
+ }
124
+ for (const candidate of candidates) {
125
+ const byId = graph.getModuleById(candidate);
126
+ if (byId) roots.add(byId);
127
+ for (const module of graph.getModulesByFile(candidate) ?? []) roots.add(module);
128
+ }
129
+
130
+ const seen = new Set();
131
+ const dependencies = new Set();
132
+ /** @param {import('vite').EnvironmentModuleNode} module */
133
+ function visit(module) {
134
+ if (seen.has(module)) return;
135
+ seen.add(module);
136
+ if (module.file) dependencies.add(module.file);
137
+ for (const imported of module.importedModules) visit(imported);
138
+ }
139
+ for (const root of roots) visit(root);
140
+ return [...dependencies];
98
141
  },
99
142
  };
100
143
  }
package/types/index.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import type { Plugin, ViteDevServer } from 'vite';
2
2
  import type {
3
3
  ConfigModuleRunner,
4
+ ExperimentalRendererConfigOptions,
4
5
  LoadedOctaneConfig,
5
6
  OctaneConfigOptions,
6
7
  ResolvedOctaneConfig,
@@ -11,11 +12,32 @@ export * from '@octanejs/app-core';
11
12
  export interface OctanePluginOptions {
12
13
  /** Override the client HMR default (on in serve mode, off for SSR). */
13
14
  hmr?: boolean;
15
+ /** Enable component profiling in client transforms. */
16
+ profile?: boolean;
14
17
  /**
15
18
  * Path fragments the compiler's plain `.ts`/`.js` hook-slotting pass must
16
19
  * skip. Prefer package manifest `octane.hookSlots.manual` declarations.
20
+ * With `requireDirective`, excluded paths are exempt from Octane ownership
21
+ * entirely — including `.tsrx`/`.tsx` — for projects routing those paths
22
+ * through a different tsrx compiler (e.g. `@tsrx/react`).
17
23
  */
18
24
  exclude?: string[];
25
+ /**
26
+ * Mixed-toolchain ownership gate: when `true`, Octane compiles only
27
+ * project modules that declare `'use octane'` in their directive prologue.
28
+ * Undirected project `.tsx`/`.ts`/`.js` pass through to the host
29
+ * framework's own pipeline (e.g. React's JSX transform); an undirected
30
+ * project `.tsrx` is a build error. Installed and linked packages keep
31
+ * their Octane package-manifest decision. The directive is always
32
+ * tolerated and stripped from compiled output, even when this is off.
33
+ * @default false
34
+ */
35
+ requireDirective?: boolean;
36
+ /**
37
+ * @experimental Full renderer-config override. When omitted, the compiler
38
+ * reads `compiler.renderers` from `octane.config.ts` before transforming modules.
39
+ */
40
+ renderers?: ExperimentalRendererConfigOptions;
19
41
  }
20
42
 
21
43
  /** The Octane compiler plugin plus Vite app/metaframework integration. */