octane 0.1.24 → 0.1.25
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/README.md +4 -0
- package/dist/compiler/bundler.js +32 -15
- package/dist/compiler/register.js +144 -0
- package/dist/compiler/runtime-requests.js +36 -0
- package/dist/compiler/slot-hooks.js +15 -3
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -17,6 +17,10 @@ wrapper is narrower: the wrapper must be locally declared in a fully compiled
|
|
|
17
17
|
parameter to a supported hook. This package ships both the runtime and compiler,
|
|
18
18
|
with the compiler exposed at `octane/compiler`.
|
|
19
19
|
|
|
20
|
+
Direct Node or Bun server scripts can preload `octane/compiler/register` to
|
|
21
|
+
compile imported Octane components without going through Vite. See the
|
|
22
|
+
[SSR guide](https://github.com/octanejs/octane/blob/main/docs/ssr.md#run-an-ssg-script-directly).
|
|
23
|
+
|
|
20
24
|
For the full story, see the
|
|
21
25
|
[main README](https://github.com/octanejs/octane#readme).
|
|
22
26
|
|
package/dist/compiler/bundler.js
CHANGED
|
@@ -31,6 +31,7 @@ import { findLeadingJsxImportSourcePragma } from './pragma.js';
|
|
|
31
31
|
import { normalizeUniversalRuntime } from './universal-runtime.js';
|
|
32
32
|
import { formatCompileDiagnostic } from './native-change-diagnostics.js';
|
|
33
33
|
import { findVoidComponentImports, findVoidRootImports, slotHooks } from './slot-hooks.js';
|
|
34
|
+
import { rewriteServerRuntimeRequests } from './runtime-requests.js';
|
|
34
35
|
import { assertStrongMode } from './strong-mode.js';
|
|
35
36
|
import {
|
|
36
37
|
assertNoLiveClientOnlyImports,
|
|
@@ -848,6 +849,20 @@ class OctaneBundlerCompiler {
|
|
|
848
849
|
options.universalRuntime ?? this.defaults.universalRuntime,
|
|
849
850
|
);
|
|
850
851
|
const filename = this._canonicalModuleId(file);
|
|
852
|
+
const targetRuntimeRequests = (source, kind) => {
|
|
853
|
+
if (environment !== 'server' || options.explicitRuntimeRequests !== true) return null;
|
|
854
|
+
const runtimeResult = rewriteServerRuntimeRequests(source, filename);
|
|
855
|
+
if (runtimeResult === null) return null;
|
|
856
|
+
return {
|
|
857
|
+
code: runtimeResult.code,
|
|
858
|
+
map: runtimeResult.map,
|
|
859
|
+
kind,
|
|
860
|
+
...finishMetadata(collected),
|
|
861
|
+
};
|
|
862
|
+
};
|
|
863
|
+
const passThrough = () => {
|
|
864
|
+
return targetRuntimeRequests(code, 'runtime-requests') ?? this._passThrough(code, collected);
|
|
865
|
+
};
|
|
851
866
|
const clientOnlyImports =
|
|
852
867
|
environment === 'server' && Array.isArray(options.clientOnlyImports)
|
|
853
868
|
? options.clientOnlyImports
|
|
@@ -974,22 +989,22 @@ class OctaneBundlerCompiler {
|
|
|
974
989
|
if (this.requireDirective && !pragmaOwned && this._isProjectOwnedSource(file)) {
|
|
975
990
|
this._warnUnmarkedOctaneImport(code, filename);
|
|
976
991
|
}
|
|
977
|
-
return
|
|
992
|
+
return passThrough();
|
|
978
993
|
}
|
|
979
994
|
|
|
980
995
|
if (plainHelperSource) {
|
|
981
|
-
if (/\/\/\s*octane-no-slot\b/.test(code)) return
|
|
996
|
+
if (/\/\/\s*octane-no-slot\b/.test(code)) return passThrough();
|
|
982
997
|
if (this.exclude.some((path) => file.includes(path))) {
|
|
983
998
|
// Same conflict diagnostic as the full-compile gate: an ownership
|
|
984
999
|
// pragma inside an excluded path must not fail silent.
|
|
985
1000
|
if (this.requireDirective) {
|
|
986
1001
|
this._warnExcludedPragmaConflict(file, filename, pragmaOwned);
|
|
987
1002
|
}
|
|
988
|
-
return
|
|
1003
|
+
return passThrough();
|
|
989
1004
|
}
|
|
990
|
-
if (!/from\s*['"]octane['"]/.test(code)) return
|
|
1005
|
+
if (!/from\s*['"]octane['"]/.test(code)) return passThrough();
|
|
991
1006
|
if (!this._isInstalledOctaneSource(file, collected)) {
|
|
992
|
-
return
|
|
1007
|
+
return passThrough();
|
|
993
1008
|
}
|
|
994
1009
|
// Hook slotting is an Octane-ownership rewrite, so the ownership
|
|
995
1010
|
// gate applies to it exactly as to full compilation: an unmarked
|
|
@@ -997,7 +1012,7 @@ class OctaneBundlerCompiler {
|
|
|
997
1012
|
// pragma diagnostic), a pragma-marked one gets its hook slots.
|
|
998
1013
|
if (this.requireDirective && !pragmaOwned && this._isProjectOwnedSource(file)) {
|
|
999
1014
|
this._warnUnmarkedOctaneImport(code, filename);
|
|
1000
|
-
return
|
|
1015
|
+
return passThrough();
|
|
1001
1016
|
}
|
|
1002
1017
|
if (this._hasManualHookSlots(file, collected)) {
|
|
1003
1018
|
// Hand-slotted bindings still own their authored policy. Opting one
|
|
@@ -1008,7 +1023,7 @@ class OctaneBundlerCompiler {
|
|
|
1008
1023
|
strong,
|
|
1009
1024
|
});
|
|
1010
1025
|
}
|
|
1011
|
-
return
|
|
1026
|
+
return passThrough();
|
|
1012
1027
|
}
|
|
1013
1028
|
const profileFilename = profile ? this._profileModuleId(file, collected) : undefined;
|
|
1014
1029
|
const specializeVoidRoot =
|
|
@@ -1025,16 +1040,18 @@ class OctaneBundlerCompiler {
|
|
|
1025
1040
|
}
|
|
1026
1041
|
: {}),
|
|
1027
1042
|
});
|
|
1028
|
-
if (out === null) return
|
|
1029
|
-
return
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1043
|
+
if (out === null) return passThrough();
|
|
1044
|
+
return (
|
|
1045
|
+
targetRuntimeRequests(out.code, 'slots') ?? {
|
|
1046
|
+
code: out.code,
|
|
1047
|
+
map: out.map,
|
|
1048
|
+
kind: 'slots',
|
|
1049
|
+
...finishMetadata(collected),
|
|
1050
|
+
}
|
|
1051
|
+
);
|
|
1035
1052
|
}
|
|
1036
1053
|
|
|
1037
|
-
return
|
|
1054
|
+
return passThrough();
|
|
1038
1055
|
}
|
|
1039
1056
|
}
|
|
1040
1057
|
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Synchronous server-script compiler registration.
|
|
3
|
+
*
|
|
4
|
+
* Import this module before a Node or Bun entry point to compile authored
|
|
5
|
+
* `.tsrx`/`.tsx` modules and slot Octane hooks in plain `.ts`/`.js` helpers.
|
|
6
|
+
* Direct execution is a server pipeline, so authored `octane` runtime imports
|
|
7
|
+
* resolve to `octane/server`, matching the server code the compiler emits.
|
|
8
|
+
*/
|
|
9
|
+
import * as nodeFs from 'node:fs';
|
|
10
|
+
import * as nodeModule from 'node:module';
|
|
11
|
+
import * as nodePath from 'node:path';
|
|
12
|
+
import * as nodeUrl from 'node:url';
|
|
13
|
+
import { createOctaneCompiler } from './bundler.js';
|
|
14
|
+
|
|
15
|
+
const compiler = createOctaneCompiler({
|
|
16
|
+
root: process.cwd(),
|
|
17
|
+
environment: 'server',
|
|
18
|
+
hmr: false,
|
|
19
|
+
dev: false,
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const extensionlessCandidates = ['.tsrx', '.tsx', '.ts', '.mts', '.mjs', '.js'];
|
|
23
|
+
|
|
24
|
+
function isFile(path) {
|
|
25
|
+
try {
|
|
26
|
+
return nodeFs.statSync(path).isFile();
|
|
27
|
+
} catch {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function candidateUrl(specifier, parentURL) {
|
|
33
|
+
if (parentURL === undefined || !parentURL.startsWith('file:')) return null;
|
|
34
|
+
if (!specifier.startsWith('.') && !specifier.startsWith('/') && !specifier.startsWith('file:')) {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
let requested;
|
|
39
|
+
try {
|
|
40
|
+
requested = new URL(specifier, parentURL);
|
|
41
|
+
} catch {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
if (requested.protocol !== 'file:') return null;
|
|
45
|
+
|
|
46
|
+
const requestedPath = nodeUrl.fileURLToPath(requested);
|
|
47
|
+
const extension = nodePath.extname(requestedPath);
|
|
48
|
+
const candidates = [];
|
|
49
|
+
if (extension === '') {
|
|
50
|
+
for (const candidateExtension of extensionlessCandidates) {
|
|
51
|
+
candidates.push(requestedPath + candidateExtension);
|
|
52
|
+
}
|
|
53
|
+
for (const candidateExtension of extensionlessCandidates) {
|
|
54
|
+
candidates.push(nodePath.join(requestedPath, 'index' + candidateExtension));
|
|
55
|
+
}
|
|
56
|
+
} else if (extension === '.js') {
|
|
57
|
+
const stem = requestedPath.slice(0, -extension.length);
|
|
58
|
+
candidates.push(stem + '.ts', stem + '.tsx');
|
|
59
|
+
} else if (extension === '.mjs') {
|
|
60
|
+
candidates.push(requestedPath.slice(0, -extension.length) + '.mts');
|
|
61
|
+
} else if (extension === '.cjs') {
|
|
62
|
+
candidates.push(requestedPath.slice(0, -extension.length) + '.cts');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
for (const candidate of candidates) {
|
|
66
|
+
if (!isFile(candidate)) continue;
|
|
67
|
+
const resolved = nodeUrl.pathToFileURL(candidate);
|
|
68
|
+
resolved.search = requested.search;
|
|
69
|
+
resolved.hash = requested.hash;
|
|
70
|
+
return resolved.href;
|
|
71
|
+
}
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function sourceText(source) {
|
|
76
|
+
if (typeof source === 'string') return source;
|
|
77
|
+
if (source === undefined) return null;
|
|
78
|
+
return new TextDecoder().decode(source);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function resolve(specifier, context, nextResolve) {
|
|
82
|
+
const runtimeRequest = compiler.resolveRuntimeRequest(specifier, 'server');
|
|
83
|
+
if (runtimeRequest !== null) return nextResolve(runtimeRequest, context);
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
return nextResolve(specifier, context);
|
|
87
|
+
} catch (error) {
|
|
88
|
+
const url = candidateUrl(specifier, context.parentURL);
|
|
89
|
+
if (url !== null) return { url, shortCircuit: true };
|
|
90
|
+
throw error;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function load(url, context, nextLoad) {
|
|
95
|
+
if (!url.startsWith('file:')) return nextLoad(url, context);
|
|
96
|
+
const filename = nodeUrl.fileURLToPath(url);
|
|
97
|
+
|
|
98
|
+
if (filename.endsWith('.tsrx') || filename.endsWith('.tsx')) {
|
|
99
|
+
const source = nodeFs.readFileSync(filename, 'utf8');
|
|
100
|
+
const result = compiler.transform(source, filename, { environment: 'server' });
|
|
101
|
+
if (result === null || result.kind === 'none') return nextLoad(url, context);
|
|
102
|
+
return { format: 'module', source: result.code, shortCircuit: true };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if ((filename.endsWith('.ts') && !filename.endsWith('.d.ts')) || filename.endsWith('.js')) {
|
|
106
|
+
const loaded = nextLoad(url, context);
|
|
107
|
+
const source = sourceText(loaded.source);
|
|
108
|
+
if (source === null) return loaded;
|
|
109
|
+
const result = compiler.transform(source, filename, { environment: 'server' });
|
|
110
|
+
if (result === null || result.kind === 'none') return loaded;
|
|
111
|
+
return { ...loaded, source: result.code, shortCircuit: true };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return nextLoad(url, context);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function registerBunPlugin() {
|
|
118
|
+
globalThis.Bun.plugin({
|
|
119
|
+
name: 'octane-compiler',
|
|
120
|
+
setup(build) {
|
|
121
|
+
build.onLoad({ filter: /\.(?:tsrx|tsx|ts|js)$/, namespace: 'file' }, ({ path }) => {
|
|
122
|
+
const source = nodeFs.readFileSync(path, 'utf8');
|
|
123
|
+
const result = compiler.transform(source, path, {
|
|
124
|
+
environment: 'server',
|
|
125
|
+
explicitRuntimeRequests: true,
|
|
126
|
+
});
|
|
127
|
+
const loader =
|
|
128
|
+
path.endsWith('.tsx') || path.endsWith('.tsrx')
|
|
129
|
+
? 'tsx'
|
|
130
|
+
: path.endsWith('.ts')
|
|
131
|
+
? 'ts'
|
|
132
|
+
: 'js';
|
|
133
|
+
if (result === null || result.kind === 'none') return { contents: source, loader };
|
|
134
|
+
return {
|
|
135
|
+
contents: result.code,
|
|
136
|
+
loader: result.kind === 'slots' || result.kind === 'runtime-requests' ? loader : 'js',
|
|
137
|
+
};
|
|
138
|
+
});
|
|
139
|
+
},
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (typeof globalThis.Bun === 'object') registerBunPlugin();
|
|
144
|
+
else nodeModule.registerHooks({ resolve, load });
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { initSync as initModuleLexer, parse as lexModule } from 'es-module-lexer';
|
|
2
|
+
|
|
3
|
+
let moduleLexerInitialized = false;
|
|
4
|
+
|
|
5
|
+
/** Target bare Octane ESM requests at the explicit server runtime. */
|
|
6
|
+
export function rewriteServerRuntimeRequests(source, id) {
|
|
7
|
+
if (!source.includes('octane')) return null;
|
|
8
|
+
if (!moduleLexerInitialized) {
|
|
9
|
+
initModuleLexer();
|
|
10
|
+
moduleLexerInitialized = true;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
let imports;
|
|
14
|
+
try {
|
|
15
|
+
[imports] = lexModule(source, id);
|
|
16
|
+
} catch {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
let code = source;
|
|
21
|
+
let changed = false;
|
|
22
|
+
for (let index = imports.length - 1; index >= 0; index--) {
|
|
23
|
+
const request = imports[index];
|
|
24
|
+
if (request.n !== 'octane') continue;
|
|
25
|
+
if (request.d === -1) {
|
|
26
|
+
code = code.slice(0, request.s) + 'octane/server' + code.slice(request.e);
|
|
27
|
+
changed = true;
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
const quote = source[request.s];
|
|
31
|
+
if (quote !== "'" && quote !== '"' && quote !== '`') continue;
|
|
32
|
+
code = code.slice(0, request.s) + `${quote}octane/server${quote}` + code.slice(request.e);
|
|
33
|
+
changed = true;
|
|
34
|
+
}
|
|
35
|
+
return changed ? { code, map: null } : null;
|
|
36
|
+
}
|
|
@@ -27,6 +27,13 @@ function octaneHookLocals(ast) {
|
|
|
27
27
|
let importsHook = false;
|
|
28
28
|
let hasOctaneImport = false;
|
|
29
29
|
for (const node of ast.body || []) {
|
|
30
|
+
if (
|
|
31
|
+
(node.type === 'ExportNamedDeclaration' || node.type === 'ExportAllDeclaration') &&
|
|
32
|
+
node.source?.value === 'octane'
|
|
33
|
+
) {
|
|
34
|
+
hasOctaneImport = true;
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
30
37
|
if (node.type !== 'ImportDeclaration' || node.source?.value !== 'octane') continue;
|
|
31
38
|
hasOctaneImport = true;
|
|
32
39
|
for (const sp of node.specifiers || []) {
|
|
@@ -1025,7 +1032,9 @@ export function slotHooks(source, id, options) {
|
|
|
1025
1032
|
!options?.profile &&
|
|
1026
1033
|
typeof options?.isVoidComponentImport === 'function' &&
|
|
1027
1034
|
importInfo.hasOctaneImport;
|
|
1028
|
-
if (!importInfo.importsHook && !canSpecializeRoot)
|
|
1035
|
+
if (!importInfo.importsHook && !canSpecializeRoot) {
|
|
1036
|
+
return null;
|
|
1037
|
+
}
|
|
1029
1038
|
// The parsed tree is never mutated: annotateHookCalls returns a COW-rebuilt
|
|
1030
1039
|
// module whose hook calls carry their `_octane*` props (start/end offsets are
|
|
1031
1040
|
// preserved, so the text edits below stay valid), with the dependency
|
|
@@ -1072,8 +1081,11 @@ export function slotHooks(source, id, options) {
|
|
|
1072
1081
|
// Apply insertions right-to-left so earlier offsets stay valid.
|
|
1073
1082
|
st.edits.sort((a, b) => b.pos - a.pos);
|
|
1074
1083
|
let code = source;
|
|
1075
|
-
for (const
|
|
1076
|
-
code =
|
|
1084
|
+
for (const edit of st.edits) {
|
|
1085
|
+
code =
|
|
1086
|
+
code.slice(0, edit.pos) +
|
|
1087
|
+
edit.text +
|
|
1088
|
+
code.slice(edit.end === undefined ? edit.pos : edit.end);
|
|
1077
1089
|
}
|
|
1078
1090
|
|
|
1079
1091
|
// APPEND the slot consts (rather than prepend) so every original line number
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "octane",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.25",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
7
7
|
"engines": {
|
|
8
|
-
"node": ">=22"
|
|
8
|
+
"node": ">=22.22.2"
|
|
9
9
|
},
|
|
10
10
|
"description": "A fast, modern UI framework and the successor to Inferno",
|
|
11
11
|
"author": {
|
|
@@ -85,6 +85,7 @@
|
|
|
85
85
|
},
|
|
86
86
|
"./compiler": "./dist/compiler/index.js",
|
|
87
87
|
"./compiler/bundler": "./dist/compiler/bundler.js",
|
|
88
|
+
"./compiler/register": "./dist/compiler/register.js",
|
|
88
89
|
"./compiler/renderers": "./dist/compiler/renderers.js",
|
|
89
90
|
"./compiler/vite": {
|
|
90
91
|
"types": "./dist/compiler/vite.d.ts",
|
|
@@ -99,6 +100,7 @@
|
|
|
99
100
|
"@tsrx/core": "^0.1.56",
|
|
100
101
|
"@types/react": "^19.2.17",
|
|
101
102
|
"devalue": "^5.8.2",
|
|
103
|
+
"es-module-lexer": "^1.7.0",
|
|
102
104
|
"esrap": "^2.3.0"
|
|
103
105
|
},
|
|
104
106
|
"peerDependencies": {
|