@bleedingdev/modern-js-renderer-octane 0.0.0-stage → 3.9.3-ultramodern.1

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 (138) hide show
  1. package/LICENSE +21 -0
  2. package/bin/octane-tsc.mjs +4 -0
  3. package/dist/cjs/bootstrap.js +68 -0
  4. package/dist/cjs/client.js +371 -0
  5. package/dist/cjs/entry-application.js +83 -0
  6. package/dist/cjs/entry-client.js +92 -0
  7. package/dist/cjs/entry-server.js +127 -0
  8. package/dist/cjs/federation-context.js +72 -0
  9. package/dist/cjs/federation.js +103 -0
  10. package/dist/cjs/i18n/I18nProvider.js +67 -0
  11. package/dist/cjs/i18n/LocalizedLink.js +89 -0
  12. package/dist/cjs/i18n/context.js +41 -0
  13. package/dist/cjs/i18n/index.js +86 -0
  14. package/dist/cjs/i18n/languageSync.js +99 -0
  15. package/dist/cjs/i18n/types.js +18 -0
  16. package/dist/cjs/i18n/useI18n.js +110 -0
  17. package/dist/cjs/index.js +46 -0
  18. package/dist/cjs/manifest.js +167 -0
  19. package/dist/cjs/plugin/compiler.js +125 -0
  20. package/dist/cjs/plugin/create.js +265 -0
  21. package/dist/cjs/plugin/index.js +151 -0
  22. package/dist/cjs/plugin/profile.js +89 -0
  23. package/dist/cjs/plugin/source.js +56 -0
  24. package/dist/cjs/router-client.js +139 -0
  25. package/dist/cjs/router-handler.js +168 -0
  26. package/dist/cjs/router-injection.js +139 -0
  27. package/dist/cjs/router-server-cleanup.js +46 -0
  28. package/dist/cjs/router-server-snapshot.js +405 -0
  29. package/dist/cjs/router-server.js +108 -0
  30. package/dist/cjs/router-snapshot.js +270 -0
  31. package/dist/cjs/router.js +218 -0
  32. package/dist/cjs/routes.js +187 -0
  33. package/dist/cjs/server.js +224 -0
  34. package/dist/cjs/typecheck.js +615 -0
  35. package/dist/esm/bootstrap.mjs +24 -0
  36. package/dist/esm/client.mjs +327 -0
  37. package/dist/esm/entry-application.mjs +39 -0
  38. package/dist/esm/entry-client.mjs +54 -0
  39. package/dist/esm/entry-server.mjs +86 -0
  40. package/dist/esm/federation-context.mjs +25 -0
  41. package/dist/esm/federation.mjs +65 -0
  42. package/dist/esm/i18n/I18nProvider.mjs +29 -0
  43. package/dist/esm/i18n/LocalizedLink.mjs +48 -0
  44. package/dist/esm/i18n/context.mjs +3 -0
  45. package/dist/esm/i18n/index.mjs +8 -0
  46. package/dist/esm/i18n/languageSync.mjs +61 -0
  47. package/dist/esm/i18n/types.mjs +0 -0
  48. package/dist/esm/i18n/useI18n.mjs +72 -0
  49. package/dist/esm/index.mjs +8 -0
  50. package/dist/esm/manifest.mjs +117 -0
  51. package/dist/esm/router-client.mjs +98 -0
  52. package/dist/esm/router-handler.mjs +130 -0
  53. package/dist/esm/router-injection.mjs +101 -0
  54. package/dist/esm/router-server-cleanup.mjs +8 -0
  55. package/dist/esm/router-server-snapshot.mjs +367 -0
  56. package/dist/esm/router-server.mjs +12 -0
  57. package/dist/esm/router-snapshot.mjs +199 -0
  58. package/dist/esm/router.mjs +94 -0
  59. package/dist/esm/routes.mjs +134 -0
  60. package/dist/esm/server.mjs +183 -0
  61. package/dist/esm-node/bootstrap.mjs +24 -0
  62. package/dist/esm-node/manifest.mjs +117 -0
  63. package/dist/esm-node/plugin/compiler.mjs +70 -0
  64. package/dist/esm-node/plugin/create.mjs +227 -0
  65. package/dist/esm-node/plugin/index.mjs +103 -0
  66. package/dist/esm-node/plugin/profile.mjs +51 -0
  67. package/dist/esm-node/plugin/source.mjs +7 -0
  68. package/dist/esm-node/typecheck.mjs +560 -0
  69. package/dist/types/bootstrap.d.ts +10 -0
  70. package/dist/types/client.d.ts +36 -0
  71. package/dist/types/entry-application.d.ts +19 -0
  72. package/dist/types/entry-client.d.ts +8 -0
  73. package/dist/types/entry-server.d.ts +11 -0
  74. package/dist/types/federation-context.d.ts +28 -0
  75. package/dist/types/federation.d.ts +18 -0
  76. package/dist/types/i18n/I18nProvider.d.ts +8 -0
  77. package/dist/types/i18n/LocalizedLink.d.ts +41 -0
  78. package/dist/types/i18n/context.d.ts +9 -0
  79. package/dist/types/i18n/index.d.ts +9 -0
  80. package/dist/types/i18n/languageSync.d.ts +26 -0
  81. package/dist/types/i18n/types.d.ts +34 -0
  82. package/dist/types/i18n/useI18n.d.ts +21 -0
  83. package/dist/types/index.d.ts +9 -0
  84. package/dist/types/manifest.d.ts +34 -0
  85. package/dist/types/plugin/compiler-manifest.d.cts +10 -0
  86. package/dist/types/plugin/compiler.d.ts +5 -0
  87. package/dist/types/plugin/create.d.ts +23 -0
  88. package/dist/types/plugin/index.d.ts +3 -0
  89. package/dist/types/plugin/profile.d.ts +2 -0
  90. package/dist/types/plugin/source.d.ts +1 -0
  91. package/dist/types/plugin/svg-component-template.d.cts +6 -0
  92. package/dist/types/router-client.d.ts +12 -0
  93. package/dist/types/router-handler.d.ts +13 -0
  94. package/dist/types/router-injection.d.ts +8 -0
  95. package/dist/types/router-server-cleanup.d.ts +3 -0
  96. package/dist/types/router-server-snapshot.d.ts +15 -0
  97. package/dist/types/router-server.d.ts +13 -0
  98. package/dist/types/router-snapshot.d.ts +14 -0
  99. package/dist/types/router.d.ts +30 -0
  100. package/dist/types/routes.d.ts +18 -0
  101. package/dist/types/server.d.ts +41 -0
  102. package/dist/types/typecheck.d.ts +31 -0
  103. package/package.json +216 -4
  104. package/src/bootstrap.ts +60 -0
  105. package/src/client.ts +485 -0
  106. package/src/entry-application.ts +72 -0
  107. package/src/entry-client.ts +80 -0
  108. package/src/entry-server.ts +142 -0
  109. package/src/federation-context.ts +53 -0
  110. package/src/federation.ts +158 -0
  111. package/src/i18n/I18nProvider.ts +56 -0
  112. package/src/i18n/LocalizedLink.ts +112 -0
  113. package/src/i18n/context.ts +12 -0
  114. package/src/i18n/index.ts +35 -0
  115. package/src/i18n/languageSync.ts +132 -0
  116. package/src/i18n/types.ts +36 -0
  117. package/src/i18n/useI18n.ts +119 -0
  118. package/src/index.ts +10 -0
  119. package/src/manifest.ts +253 -0
  120. package/src/plugin/compiler-manifest.cjs +178 -0
  121. package/src/plugin/compiler.ts +112 -0
  122. package/src/plugin/create.ts +272 -0
  123. package/src/plugin/index.ts +105 -0
  124. package/src/plugin/profile.ts +46 -0
  125. package/src/plugin/source.ts +16 -0
  126. package/src/plugin/svg-component-template.cjs +17 -0
  127. package/src/router-client.ts +144 -0
  128. package/src/router-handler.ts +182 -0
  129. package/src/router-injection.ts +145 -0
  130. package/src/router-server-cleanup.ts +11 -0
  131. package/src/router-server-snapshot.ts +564 -0
  132. package/src/router-server.ts +23 -0
  133. package/src/router-snapshot.ts +251 -0
  134. package/src/router.ts +192 -0
  135. package/src/routes.ts +263 -0
  136. package/src/server.ts +300 -0
  137. package/src/typecheck.ts +723 -0
  138. package/README.md +0 -3
@@ -0,0 +1,178 @@
1
+ const { createHash } = require('node:crypto');
2
+ const path = require('node:path');
3
+ const { getOctaneRspackBuildInfo } = require('@octanejs/rspack-plugin');
4
+
5
+ const name = 'ultramodern:octane:compiler-manifest';
6
+ const digest = bytes => createHash('sha256').update(bytes).digest('hex');
7
+
8
+ /** The released native compiler remains responsible for every transformation. */
9
+ class OctaneCompilerManifestPlugin {
10
+ constructor(options) {
11
+ this.options = options;
12
+ }
13
+
14
+ apply(compiler) {
15
+ compiler.hooks.normalModuleFactory.tap(name, factory => {
16
+ factory.hooks.beforeResolve.tap(name, result => {
17
+ if (/\.svg\?(?:[^#]*&)?react(?:[=&]|$)/u.test(result.request)) {
18
+ throw new Error(
19
+ 'unsupported-renderer-capability: `?react` SVG imports are React components; import Octane SVG components with `?component`.',
20
+ );
21
+ }
22
+ });
23
+ factory.hooks.afterResolve.tap(name, result => {
24
+ const resource = result.createData?.resource?.split('?')[0];
25
+ if (resource && path.extname(resource).toLowerCase() === '.jsx') {
26
+ throw new Error(
27
+ `unsupported-renderer-capability: Octane does not support .jsx source: ${resource}. Supported source extensions: .tsx, .tsrx, .ts, .js.`,
28
+ );
29
+ }
30
+ });
31
+ });
32
+ if (!this.options.emitClientManifest) return;
33
+ compiler.hooks.thisCompilation.tap(name, compilation => {
34
+ compilation.hooks.processAssets.tap(
35
+ {
36
+ name,
37
+ stage: compiler.webpack.Compilation.PROCESS_ASSETS_STAGE_REPORT + 1,
38
+ },
39
+ () => {
40
+ // Preserve the owning resolver/compiler diagnostic on an invalid graph.
41
+ if (compilation.errors.length) return;
42
+ const buildAsset = compilation.getAsset('octane-client-build.json');
43
+ if (!buildAsset)
44
+ throw new Error(
45
+ 'The native Octane client build manifest is missing.',
46
+ );
47
+ const nativeBuild = JSON.parse(buildAsset.source.source().toString());
48
+ if (
49
+ nativeBuild.version !== 1 ||
50
+ nativeBuild.buildId !== compilation.hash
51
+ ) {
52
+ throw new Error(
53
+ 'The native Octane client manifest does not match the completed compilation.',
54
+ );
55
+ }
56
+ const sourcesByModule = new Map();
57
+ const moduleAssets = new Map();
58
+ const visit = (module, inheritedAssets = []) => {
59
+ const chunks = [
60
+ ...compilation.chunkGraph.getModuleChunksIterable(module),
61
+ ];
62
+ const previousAssets = moduleAssets.get(module);
63
+ const assets = [
64
+ ...new Set([
65
+ ...(previousAssets ?? []),
66
+ ...inheritedAssets,
67
+ ...chunks.flatMap(chunk => [...chunk.files]),
68
+ ]),
69
+ ]
70
+ .filter(file => file.endsWith('.js'))
71
+ .sort();
72
+ // Rspack also lists concatenated children as standalone modules.
73
+ // A later parent visit can supply their first emitted chunk.
74
+ if (
75
+ previousAssets &&
76
+ assets.every(file => previousAssets.has(file))
77
+ )
78
+ return;
79
+ moduleAssets.set(module, new Set(assets));
80
+ const info = getOctaneRspackBuildInfo(module);
81
+ if (info && assets.length) {
82
+ const resource = module.resource?.split('?')[0];
83
+ const emitted = module.originalSource();
84
+ if (!resource || !emitted) {
85
+ throw new Error(
86
+ `Octane compiled module has no source: ${module.identifier()}.`,
87
+ );
88
+ }
89
+ const relative = path.relative(this.options.root, resource);
90
+ sourcesByModule.set(module, {
91
+ resource:
92
+ relative.split(path.sep).join('/') +
93
+ (info.resourceQuery ?? ''),
94
+ canonicalId: info.canonicalId,
95
+ moduleId:
96
+ compilation.chunkGraph.getModuleId(module) ??
97
+ module.identifier(),
98
+ transformKind: info.transformKind,
99
+ emittedSourceSha256: digest(emitted.buffer()),
100
+ assets,
101
+ });
102
+ }
103
+ for (const nested of module.modules ?? []) visit(nested, assets);
104
+ };
105
+ for (const module of compilation.modules) visit(module);
106
+ const sources = [...sourcesByModule.values()];
107
+ sources.sort((left, right) =>
108
+ left.resource.localeCompare(right.resource),
109
+ );
110
+ const identities = this.options.rendererIdentities();
111
+ if (!identities || !Object.keys(identities).length) {
112
+ throw new Error(
113
+ 'Octane compiler requires immutable application entry identities.',
114
+ );
115
+ }
116
+ for (const [entryName, rendererIdentity] of Object.entries(
117
+ identities,
118
+ )) {
119
+ if (
120
+ rendererIdentity?.renderer !== 'octane' ||
121
+ rendererIdentity.entryName !== entryName
122
+ ) {
123
+ throw new Error(
124
+ `Octane compiler identity conflicts with entry ${entryName}.`,
125
+ );
126
+ }
127
+ const entrypoint = compilation.entrypoints.get(entryName);
128
+ if (!entrypoint)
129
+ throw new Error(
130
+ `Octane compiler has no client entry for ${entryName}.`,
131
+ );
132
+ const entryChunks = new Set(entrypoint.chunks);
133
+ for (const chunk of entrypoint.chunks) {
134
+ for (const referenced of chunk.getAllReferencedChunks())
135
+ entryChunks.add(referenced);
136
+ }
137
+ const files = [
138
+ ...new Set(
139
+ [...entryChunks]
140
+ .flatMap(chunk => [...chunk.files])
141
+ .filter(file => file.endsWith('.js')),
142
+ ),
143
+ ].sort();
144
+ const fileSet = new Set(files);
145
+ const manifest = {
146
+ schemaVersion: 1,
147
+ renderer: 'octane',
148
+ runtimeVersion: this.options.runtimeVersion,
149
+ compilerVersion: this.options.compilerVersion,
150
+ rendererIdentity,
151
+ nativeHydrationBuildId: nativeBuild.buildId,
152
+ sourceModules: sources.flatMap(source => {
153
+ const assets = source.assets.filter(file => fileSet.has(file));
154
+ return assets.length ? [{ ...source, assets }] : [];
155
+ }),
156
+ assets: files.map(file => ({
157
+ file,
158
+ sha256: digest(compilation.getAsset(file).source.buffer()),
159
+ })),
160
+ };
161
+ this.options.validateManifest(
162
+ manifest,
163
+ rendererIdentity,
164
+ compilation.hash,
165
+ );
166
+ compilation.emitAsset(
167
+ this.options.manifestFilename(entryName),
168
+ new compiler.webpack.sources.RawSource(
169
+ `${JSON.stringify(manifest, null, 2)}\n`,
170
+ ),
171
+ );
172
+ }
173
+ },
174
+ );
175
+ });
176
+ }
177
+ }
178
+ module.exports = { OctaneCompilerManifestPlugin };
@@ -0,0 +1,112 @@
1
+ import fs from 'node:fs';
2
+ import { createRequire } from 'node:module';
3
+ import path from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import type { NativeRendererCompilerOptions } from '@modern-js/renderer-core/adapter';
6
+ import type { RsbuildPlugin } from '@rsbuild/core';
7
+ import {
8
+ OCTANE_COMPILER_VERSION,
9
+ OCTANE_RUNTIME_VERSION,
10
+ octaneModuleManifestFileName,
11
+ validateOctaneModuleManifest,
12
+ } from '../manifest';
13
+
14
+ export function pluginSourceDirectory(): string {
15
+ let directory = path.dirname(fileURLToPath(import.meta.url));
16
+ while (true) {
17
+ const compiler = path.join(directory, 'src/plugin');
18
+ if (
19
+ fs.existsSync(path.join(directory, 'package.json')) &&
20
+ fs.existsSync(path.join(compiler, 'compiler-manifest.cjs'))
21
+ )
22
+ return compiler;
23
+ const parent = path.dirname(directory);
24
+ if (parent === directory)
25
+ throw new Error('Cannot locate the UltraModern Octane compiler package.');
26
+ directory = parent;
27
+ }
28
+ }
29
+
30
+ /** Octane owns native compilation; UltraModern retains the application host. */
31
+ export function createOctaneCompilerPlugin(
32
+ options: NativeRendererCompilerOptions,
33
+ ): RsbuildPlugin {
34
+ return {
35
+ name: 'ultramodern:octane:compiler',
36
+ setup(api) {
37
+ const directory = pluginSourceDirectory();
38
+ const require = createRequire(path.join(directory, 'index.cjs'));
39
+ const {
40
+ OctaneCompilerManifestPlugin,
41
+ } = require('./compiler-manifest.cjs');
42
+ // The application installs the Octane compiler; resolve it lazily so
43
+ // descriptor readers never load it.
44
+ const {
45
+ inferRspackEnvironment,
46
+ OctaneRspackPlugin,
47
+ }: typeof import('@octanejs/rspack-plugin') = require('@octanejs/rspack-plugin');
48
+ api.modifyRsbuildConfig((config, { mergeRsbuildConfig }) =>
49
+ mergeRsbuildConfig(config, {
50
+ source: {
51
+ // Released native bindings export raw TypeScript and TSRX.
52
+ include: [/[/\\]@octanejs[/\\]/u],
53
+ },
54
+ }),
55
+ );
56
+ api.modifyRspackConfig((config, { environment: rsbuildEnvironment }) => {
57
+ // The Cloudflare SSR worker targets `webworker`; it still renders
58
+ // the server document rather than a client hydration bundle.
59
+ const environment =
60
+ rsbuildEnvironment.name === options.workerEnvironmentName
61
+ ? 'server'
62
+ : inferRspackEnvironment(config.target);
63
+ // Octane prepends '.tsrx' only when it is absent. Keep TypeScript
64
+ // modules first so an extensionless './link' cannot select a
65
+ // case-colliding Link.tsrx on case-insensitive file systems.
66
+ config.resolve ??= {};
67
+ const extensions = (config.resolve.extensions ?? []).filter(
68
+ extension => extension !== '.tsrx',
69
+ );
70
+ extensions.splice(
71
+ Math.max(extensions.indexOf('.ts'), extensions.indexOf('.tsx')) + 1,
72
+ 0,
73
+ '.tsrx',
74
+ );
75
+ config.resolve.extensions = extensions;
76
+ config.plugins ??= [];
77
+ config.plugins.push(
78
+ new OctaneRspackPlugin({
79
+ root: api.context.rootPath,
80
+ environment,
81
+ transpile: false,
82
+ }),
83
+ );
84
+ config.plugins.push(
85
+ new OctaneCompilerManifestPlugin({
86
+ emitClientManifest: environment === 'client',
87
+ root: api.context.rootPath,
88
+ runtimeVersion: OCTANE_RUNTIME_VERSION,
89
+ compilerVersion: OCTANE_COMPILER_VERSION,
90
+ rendererIdentities: options.rendererIdentities,
91
+ manifestFilename: octaneModuleManifestFileName,
92
+ validateManifest: validateOctaneModuleManifest,
93
+ }),
94
+ );
95
+ config.module ??= {};
96
+ config.module.rules ??= [];
97
+ // Rsbuild's existing SWC rule handles ordinary .ts/.tsx sources.
98
+ // TSRX needs the same stripping stage after Octane's pre-loader.
99
+ config.module.rules.push({
100
+ test: /\.tsrx$/u,
101
+ type: 'javascript/auto',
102
+ use: [
103
+ {
104
+ loader: 'builtin:swc-loader',
105
+ options: { detectSyntax: 'auto' },
106
+ },
107
+ ],
108
+ });
109
+ });
110
+ },
111
+ };
112
+ }
@@ -0,0 +1,272 @@
1
+ export type OctaneAppSourceOptions = {
2
+ appId: string;
3
+ title: string;
4
+ entryName?: string;
5
+ appRole?: 'shell' | 'vertical';
6
+ sourceExtension: '.tsx' | '.tsrx';
7
+ jsxImportSource: string;
8
+ capabilities: {
9
+ ssr: boolean;
10
+ federation: boolean;
11
+ };
12
+ };
13
+
14
+ export type OctaneAppSourceArtifact = {
15
+ path: string;
16
+ content: string;
17
+ };
18
+
19
+ export type OctaneAppSources = {
20
+ sourceExtension: '.tsx';
21
+ jsxImportSource: 'octane';
22
+ artifacts: OctaneAppSourceArtifact[];
23
+ };
24
+
25
+ /** The renderer adapter owns entry generation, the document and hydration. */
26
+ export function generateOctaneAppSources(
27
+ options: OctaneAppSourceOptions,
28
+ ): OctaneAppSources {
29
+ if ((options.entryName ?? 'main') !== 'main') {
30
+ throw new Error(
31
+ 'The Octane app template supports the main entry only. The create-time workspace writer places src/modern-app-env.d.ts, src/routes/index.css and the package manifest at the app root regardless of entryName, so it cannot yet route a second entry into src/<entryName>/. Add that entry by hand after generation (mirror src/routes under src/<entryName>/routes) until an admitted native multi-entry create template exists; the native entry generator itself already supports arbitrary entry names.',
32
+ );
33
+ }
34
+ if (!options.appId.trim() || !options.title.trim()) {
35
+ throw new Error('The Octane app template requires an appId and title.');
36
+ }
37
+ if (options.sourceExtension !== '.tsx') {
38
+ throw new Error(
39
+ 'The Octane app template requires the admitted .tsx source profile. .tsrx generation requires an admitted native template profile.',
40
+ );
41
+ }
42
+ if (options.jsxImportSource !== 'octane') {
43
+ throw new Error(
44
+ 'The Octane app template requires jsxImportSource: "octane" from the admitted profile.',
45
+ );
46
+ }
47
+ if (
48
+ typeof options.capabilities.ssr !== 'boolean' ||
49
+ typeof options.capabilities.federation !== 'boolean'
50
+ ) {
51
+ throw new Error('The Octane app template requires admitted capabilities.');
52
+ }
53
+ const title = JSON.stringify(options.title);
54
+ const aboutTitle = JSON.stringify(`${options.title} - About`);
55
+ const appId = JSON.stringify(options.appId);
56
+ return {
57
+ sourceExtension: '.tsx',
58
+ jsxImportSource: 'octane',
59
+ artifacts: [
60
+ {
61
+ path: 'src/routes/layout.tsx',
62
+ content: `import { Link, Outlet } from '@modern-js/renderer-octane/router';
63
+ import Stable from '../components/Stable';
64
+ import './index.css';
65
+
66
+ const appTitle = ${title};
67
+
68
+ export default function Layout() {
69
+ return (
70
+ <main data-testid="native-layout">
71
+ <header>
72
+ <p>{appTitle}</p>
73
+ <nav aria-label="Application">
74
+ <Link to="/">Home</Link>
75
+ <Link to="/about">About</Link>
76
+ ${options.capabilities.federation ? ' <Link to="/remotes">Federated applications</Link>\n' : ''}
77
+ </nav>
78
+ </header>
79
+ <Stable />
80
+ <Outlet />
81
+ </main>
82
+ );
83
+ }
84
+ `,
85
+ },
86
+ {
87
+ path: 'src/routes/page.tsx',
88
+ content: `import {
89
+ Link,
90
+ useLoaderData,
91
+ useOctaneRouteAction,
92
+ } from '@modern-js/renderer-octane/router';
93
+ import { useActionState } from 'octane';
94
+ import Counter from '../components/Counter';
95
+
96
+ const appId = ${appId};
97
+ const appTitle = ${title};
98
+
99
+ export default function HomePage() {
100
+ const data = useLoaderData({ strict: false });
101
+ const submit = useOctaneRouteAction();
102
+ const [result, action, pending] = useActionState(submit, undefined);
103
+ return (
104
+ <section data-app-id={appId} data-testid="native-route" data-renderer="octane">
105
+ <h1>{appTitle}</h1>
106
+ <Counter />
107
+ <pre data-testid="native-loader-value">{JSON.stringify(data)}</pre>
108
+ <form action={action}>
109
+ <label>
110
+ Name <input name="name" />
111
+ </label>
112
+ <button type="submit" disabled={pending}>
113
+ Save
114
+ </button>
115
+ <button type="submit" name="intent" value="redirect">
116
+ Save and redirect
117
+ </button>
118
+ </form>
119
+ <pre data-testid="native-action-value">{JSON.stringify(result)}</pre>
120
+ <Link to="/about">Open native about route</Link>
121
+ </section>
122
+ );
123
+ }
124
+ `,
125
+ },
126
+ {
127
+ path: 'src/routes/page.head.ts',
128
+ content: `import type { FileSystemRouteModule } from '@modern-js/renderer-octane/router';
129
+
130
+ export const head: NonNullable<FileSystemRouteModule['head']> = () => ({
131
+ meta: [
132
+ { title: ${title} },
133
+ { name: 'description', content: 'Built with the native Octane renderer' },
134
+ ],
135
+ });
136
+ `,
137
+ },
138
+ {
139
+ path: 'src/routes/about/page.tsx',
140
+ content: `import { Link } from '@modern-js/renderer-octane/router';
141
+
142
+ export default function AboutPage() {
143
+ return (
144
+ <section data-testid="native-about">
145
+ <h1>Native Octane navigation</h1>
146
+ <Link to="/">Home</Link>
147
+ </section>
148
+ );
149
+ }
150
+ `,
151
+ },
152
+ {
153
+ path: 'src/routes/about/page.head.ts',
154
+ content: `import type { FileSystemRouteModule } from '@modern-js/renderer-octane/router';
155
+
156
+ export const head: NonNullable<FileSystemRouteModule['head']> = () => ({
157
+ meta: [
158
+ { title: ${aboutTitle} },
159
+ { name: 'description', content: 'Built with the native Octane renderer' },
160
+ ],
161
+ });
162
+ `,
163
+ },
164
+ {
165
+ path: 'src/components/Counter.tsx',
166
+ content: `import { useState } from 'octane';
167
+
168
+ export default function Counter() {
169
+ const [count, setCount] = useState(0);
170
+ return (
171
+ <section data-testid="native-edited-component">
172
+ <span data-testid="native-hmr-marker">Counter before native edit</span>
173
+ <button type="button" onClick={() => setCount(value => value + 1)}>
174
+ Increment
175
+ </button>
176
+ <output data-testid="native-count">{count}</output>
177
+ </section>
178
+ );
179
+ }
180
+ `,
181
+ },
182
+ {
183
+ path: 'src/components/Stable.tsx',
184
+ content: `import { useState } from 'octane';
185
+
186
+ export default function Stable() {
187
+ const [count, setCount] = useState(0);
188
+ return (
189
+ <section data-testid="native-unaffected-component">
190
+ <button type="button" onClick={() => setCount(value => value + 1)}>
191
+ Increment stable state
192
+ </button>
193
+ <output data-testid="native-unaffected-count">{count}</output>
194
+ </section>
195
+ );
196
+ }
197
+ `,
198
+ },
199
+ {
200
+ path: 'src/routes/page.data.ts',
201
+ content: `import type { DataHandlerInput } from '@modern-js/renderer-core/data';
202
+
203
+ export function loader({ request }: DataHandlerInput) {
204
+ const url = new URL(request.url);
205
+ if (url.searchParams.get('case') === 'not-found')
206
+ throw new Response('Native route not found', { status: 404 });
207
+ if (url.searchParams.get('case') === 'error')
208
+ throw new Error('Native loader failure');
209
+ if (url.searchParams.get('case') === 'redirect')
210
+ return new Response(null, { status: 302, headers: { location: '/about' } });
211
+ return {
212
+ message: 'Native loader value',
213
+ privateValue: request.headers.get('x-conformance-private'),
214
+ };
215
+ }
216
+
217
+ export async function action({ request }: DataHandlerInput) {
218
+ const form = await request.formData();
219
+ const name = form.get('name');
220
+ if (typeof name !== 'string' || !name.trim())
221
+ return Response.json(
222
+ { fieldErrors: { name: 'Name required' } },
223
+ { status: 422 },
224
+ );
225
+ if (form.get('intent') === 'redirect')
226
+ return new Response(null, {
227
+ status: 303,
228
+ headers: {
229
+ location: '/about',
230
+ 'set-cookie': 'conformance-saved=1; Path=/; SameSite=Lax',
231
+ },
232
+ });
233
+ if (form.get('intent') === 'throw') throw new Error('Native action failure');
234
+ return {
235
+ saved: name,
236
+ privateValue: request.headers.get('x-conformance-private'),
237
+ };
238
+ }
239
+ `,
240
+ },
241
+ {
242
+ path: 'src/routes/error.tsx',
243
+ content: `import type { ErrorRouteComponent } from '@modern-js/renderer-octane/router';
244
+
245
+ const ErrorBoundary: ErrorRouteComponent = props => (
246
+ <section data-testid="native-error">
247
+ <h1>Something went wrong</h1>
248
+ <output>
249
+ {props.error instanceof Error ? props.error.message : String(props.error)}
250
+ </output>
251
+ </section>
252
+ );
253
+
254
+ export default ErrorBoundary;
255
+ `,
256
+ },
257
+ {
258
+ path: 'src/routes/not-found.tsx',
259
+ content: `import type { NotFoundRouteComponent } from '@modern-js/renderer-octane/router';
260
+
261
+ const NotFound: NotFoundRouteComponent = () => (
262
+ <section data-testid="native-not-found">
263
+ <h1>Page not found</h1>
264
+ </section>
265
+ );
266
+
267
+ export default NotFound;
268
+ `,
269
+ },
270
+ ],
271
+ };
272
+ }
@@ -0,0 +1,105 @@
1
+ import path from 'node:path';
2
+ import {
3
+ defineRendererAdapter,
4
+ type NativeRendererAdapter,
5
+ nativeRendererDependencies,
6
+ } from '@modern-js/renderer-core/adapter';
7
+ import { compilerArtifacts } from '../manifest';
8
+ import { createOctaneCompilerPlugin, pluginSourceDirectory } from './compiler';
9
+ import { generateOctaneAppSources } from './create';
10
+ import { octaneProfile } from './profile';
11
+ import { assertOctaneEntrySource } from './source';
12
+
13
+ /** The build-side Octane renderer adapter UltraModern loads for `renderer: 'octane'`. */
14
+ export const rendererAdapter: NativeRendererAdapter<'octane'> =
15
+ defineRendererAdapter({
16
+ name: 'octane',
17
+ kind: 'native',
18
+ profile: octaneProfile,
19
+ routerFrameworks: ['octane'],
20
+ runtime: {
21
+ package: 'octane',
22
+ bootstrap: '@modern-js/renderer-octane',
23
+ entryClient: '@modern-js/renderer-octane/entry-client',
24
+ entryServer: '@modern-js/renderer-octane/entry-server',
25
+ router: '@modern-js/renderer-octane/router',
26
+ i18n: '@modern-js/renderer-octane/i18n',
27
+ manifest: '@modern-js/renderer-octane/manifest',
28
+ },
29
+ federation: {
30
+ library: 'module',
31
+ ssr: true,
32
+ shared: [
33
+ 'octane',
34
+ 'octane/',
35
+ 'octane/internal/context',
36
+ 'octane/internal/signal-read',
37
+ 'octane/signals',
38
+ '@octanejs/tanstack-router',
39
+ '@octanejs/tanstack-router/',
40
+ '@modern-js/renderer-octane',
41
+ '@modern-js/renderer-octane/',
42
+ '@modern-js/renderer-octane/router',
43
+ '@modern-js/renderer-octane/federation',
44
+ '@modern-js/renderer-core',
45
+ '@modern-js/renderer-core/',
46
+ '@tanstack/router-core',
47
+ '@tanstack/history',
48
+ 'seroval',
49
+ 'seroval-plugins',
50
+ ],
51
+ // A remote can use a native API the local app never imports. Publish
52
+ // these providers from the host so their private hook state stays local.
53
+ sharedByEnvironment: {
54
+ client: {
55
+ 'octane/internal/client': 'octane/internal/client',
56
+ 'octane/signals/client': 'octane/signals/client',
57
+ 'octane/profiling': 'octane/profiling',
58
+ 'octane/hydration/streamed-signals':
59
+ 'octane/hydration/streamed-signals',
60
+ '@modern-js/renderer-octane/client':
61
+ '@modern-js/renderer-octane/client',
62
+ },
63
+ server: {
64
+ octane: 'octane/server',
65
+ 'octane/server': 'octane/server',
66
+ 'octane/internal/server': 'octane/internal/server',
67
+ 'octane/signals/server': 'octane/signals/server',
68
+ },
69
+ },
70
+ },
71
+ worker: { nativeDocuments: true, rsc: false },
72
+ lazyStyles: 'document',
73
+ // Hydration bytes belong to this exact native client compilation.
74
+ entryClient: {
75
+ declarations: 'declare const __webpack_hash__: string;\n',
76
+ fields: { nativeHydrationBuildId: '__webpack_hash__' },
77
+ },
78
+ svgComponentTemplate: () =>
79
+ path.join(pluginSourceDirectory(), 'svg-component-template.cjs'),
80
+ compiler: createOctaneCompilerPlugin,
81
+ artifacts: compilerArtifacts,
82
+ assertSupportedSource: assertOctaneEntrySource,
83
+ create: {
84
+ templates: { federation: true, workers: false },
85
+ dependencies: (profile, { typescriptVersion }) => ({
86
+ frameworkDependencies: [
87
+ '@modern-js/renderer-core',
88
+ '@modern-js/renderer-octane',
89
+ ],
90
+ dependencies: nativeRendererDependencies(profile),
91
+ devDependencies: {
92
+ [profile.compiler.name]: profile.compiler.version,
93
+ typescript: typescriptVersion,
94
+ },
95
+ typecheckCommand: 'octane-tsc --noEmit --project tsconfig.json',
96
+ tsconfig: {
97
+ tsrx: {
98
+ compiler: 'octane/compiler/volar',
99
+ platform: 'web',
100
+ },
101
+ },
102
+ }),
103
+ generateAppSources: generateOctaneAppSources,
104
+ },
105
+ });