@broxium/compiler 1.3.0 → 1.3.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.
- package/dist/index.d.mts +25 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.js +351 -0
- package/dist/index.mjs +321 -0
- package/package.json +6 -2
- package/src/compiler.ts +0 -203
- package/src/index.ts +0 -2
- package/src/plugins/clientStubPlugin.ts +0 -50
- package/src/plugins/runtimeServerStubPlugin.ts +0 -136
- package/src/plugins/serverStubPlugin.ts +0 -20
- package/src/types.ts +0 -20
- package/tsconfig.json +0 -15
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
interface CompileInput {
|
|
2
|
+
componentId: number;
|
|
3
|
+
slug: string;
|
|
4
|
+
version: string;
|
|
5
|
+
files: Array<{
|
|
6
|
+
path: string;
|
|
7
|
+
content: string;
|
|
8
|
+
}>;
|
|
9
|
+
outputDir: string;
|
|
10
|
+
/** Extra node_modules directories esbuild uses to resolve React for the server bundle. */
|
|
11
|
+
nodePaths?: string[];
|
|
12
|
+
}
|
|
13
|
+
interface CompileOutput {
|
|
14
|
+
serverJsPath: string;
|
|
15
|
+
clientJsPath: string;
|
|
16
|
+
serverJsName: string;
|
|
17
|
+
clientJsName: string;
|
|
18
|
+
compiledAt: Date;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
declare class BrodoxCompiler {
|
|
22
|
+
compile(input: CompileInput): Promise<CompileOutput>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export { BrodoxCompiler, type CompileInput, type CompileOutput };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
interface CompileInput {
|
|
2
|
+
componentId: number;
|
|
3
|
+
slug: string;
|
|
4
|
+
version: string;
|
|
5
|
+
files: Array<{
|
|
6
|
+
path: string;
|
|
7
|
+
content: string;
|
|
8
|
+
}>;
|
|
9
|
+
outputDir: string;
|
|
10
|
+
/** Extra node_modules directories esbuild uses to resolve React for the server bundle. */
|
|
11
|
+
nodePaths?: string[];
|
|
12
|
+
}
|
|
13
|
+
interface CompileOutput {
|
|
14
|
+
serverJsPath: string;
|
|
15
|
+
clientJsPath: string;
|
|
16
|
+
serverJsName: string;
|
|
17
|
+
clientJsName: string;
|
|
18
|
+
compiledAt: Date;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
declare class BrodoxCompiler {
|
|
22
|
+
compile(input: CompileInput): Promise<CompileOutput>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export { BrodoxCompiler, type CompileInput, type CompileOutput };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
BrodoxCompiler: () => BrodoxCompiler
|
|
34
|
+
});
|
|
35
|
+
module.exports = __toCommonJS(index_exports);
|
|
36
|
+
|
|
37
|
+
// src/compiler.ts
|
|
38
|
+
var esbuild = __toESM(require("esbuild"));
|
|
39
|
+
var import_promises3 = __toESM(require("fs/promises"));
|
|
40
|
+
var import_node_path3 = __toESM(require("path"));
|
|
41
|
+
var import_node_os = __toESM(require("os"));
|
|
42
|
+
var import_node_crypto = require("crypto");
|
|
43
|
+
|
|
44
|
+
// src/plugins/clientStubPlugin.ts
|
|
45
|
+
var import_promises = __toESM(require("fs/promises"));
|
|
46
|
+
var import_node_path = __toESM(require("path"));
|
|
47
|
+
function isClientFile(content, filePath) {
|
|
48
|
+
const trimmed = content.trimStart();
|
|
49
|
+
if (trimmed.startsWith("'use client'") || trimmed.startsWith('"use client"')) return true;
|
|
50
|
+
return /\.client\.[jt]sx?$/.test(filePath);
|
|
51
|
+
}
|
|
52
|
+
function extractName(content, filePath) {
|
|
53
|
+
const m = content.match(/export\s+default\s+function\s+(\w+)/) ?? content.match(/(?:^|\n)function\s+(\w+)/) ?? content.match(/(?:^|\n)const\s+(\w+)\s*=/);
|
|
54
|
+
const base = import_node_path.default.basename(filePath, import_node_path.default.extname(filePath)).replace(/\.client$/, "");
|
|
55
|
+
return m?.[1] ?? base;
|
|
56
|
+
}
|
|
57
|
+
function clientStubPlugin() {
|
|
58
|
+
return {
|
|
59
|
+
name: "brodox-client-stub",
|
|
60
|
+
setup(build2) {
|
|
61
|
+
build2.onLoad({ filter: /\.(tsx?|jsx?)$/ }, async (args) => {
|
|
62
|
+
const content = await import_promises.default.readFile(args.path, "utf8");
|
|
63
|
+
if (isClientFile(content, args.path)) {
|
|
64
|
+
const name = extractName(content, args.path);
|
|
65
|
+
return {
|
|
66
|
+
contents: `
|
|
67
|
+
import React from 'react'
|
|
68
|
+
function ${name}() { return null }
|
|
69
|
+
${name}.displayName = "${name}"
|
|
70
|
+
export default ${name}
|
|
71
|
+
export function getServerData() { return {} }
|
|
72
|
+
`,
|
|
73
|
+
loader: "jsx"
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// src/plugins/serverStubPlugin.ts
|
|
82
|
+
var import_promises2 = __toESM(require("fs/promises"));
|
|
83
|
+
function serverStubPlugin() {
|
|
84
|
+
return {
|
|
85
|
+
name: "brodox-server-stub",
|
|
86
|
+
setup(build2) {
|
|
87
|
+
build2.onLoad({ filter: /\.(tsx?|jsx?)$/ }, async (args) => {
|
|
88
|
+
const content = await import_promises2.default.readFile(args.path, "utf8");
|
|
89
|
+
const trimmed = content.trimStart();
|
|
90
|
+
if (trimmed.startsWith("'use server'") || trimmed.startsWith('"use server"')) {
|
|
91
|
+
return {
|
|
92
|
+
contents: `export default function ServerStub() { return null }`,
|
|
93
|
+
loader: "jsx"
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// src/plugins/runtimeServerStubPlugin.ts
|
|
102
|
+
var import_node_path2 = __toESM(require("path"));
|
|
103
|
+
function runtimeServerStubPlugin(nodePaths = []) {
|
|
104
|
+
const resolveDir = nodePaths.length > 0 ? import_node_path2.default.dirname(nodePaths[0]) : process.cwd();
|
|
105
|
+
return {
|
|
106
|
+
name: "brodox-runtime-server-stub",
|
|
107
|
+
setup(build2) {
|
|
108
|
+
build2.onResolve({ filter: /^@broxium\/runtime$/ }, () => ({
|
|
109
|
+
path: "@broxium/runtime",
|
|
110
|
+
namespace: "brodox-runtime-server-stub"
|
|
111
|
+
}));
|
|
112
|
+
build2.onLoad({ filter: /.*/, namespace: "brodox-runtime-server-stub" }, () => ({
|
|
113
|
+
loader: "js",
|
|
114
|
+
resolveDir,
|
|
115
|
+
contents: `
|
|
116
|
+
import { createElement, Fragment, Children } from 'react';
|
|
117
|
+
var __islandSeq = 0;
|
|
118
|
+
function __nextIslandId() { return 'bi-' + (++__islandSeq) + '-' + Math.random().toString(36).slice(2,7); }
|
|
119
|
+
|
|
120
|
+
export function BrodoxImage({ src, alt, width, height, fill, className, style, priority, quality = 75, sizes }) {
|
|
121
|
+
const maxW = width || 1920;
|
|
122
|
+
const widths = [320, 640, 768, 1024, 1280, 1920].filter(w => w <= maxW);
|
|
123
|
+
if (!widths.length) widths.push(maxW);
|
|
124
|
+
const q = Math.min(100, Math.max(1, quality));
|
|
125
|
+
const enc = encodeURIComponent(src);
|
|
126
|
+
const optimisedSrc = '/api/image?src=' + enc + '&w=' + maxW + '&q=' + q + '&fmt=webp';
|
|
127
|
+
const srcSet = widths.map(w => '/api/image?src=' + enc + '&w=' + w + '&q=' + q + '&fmt=webp ' + w + 'w').join(', ');
|
|
128
|
+
const imgStyle = fill
|
|
129
|
+
? Object.assign({ position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'cover' }, style || {})
|
|
130
|
+
: (style || {});
|
|
131
|
+
return createElement('img', {
|
|
132
|
+
src: optimisedSrc, srcSet, sizes, alt: alt || '',
|
|
133
|
+
width: fill ? undefined : width, height: fill ? undefined : height,
|
|
134
|
+
loading: priority ? 'eager' : 'lazy', decoding: 'async',
|
|
135
|
+
className, style: imgStyle,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function BrodoxLink({ href, children, className, style }) {
|
|
140
|
+
return createElement('a', { href, className, style }, children);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function useRouter() {
|
|
144
|
+
return { pathname: '/', params: {}, navigate: function(){}, back: function(){}, forward: function(){}, prefetch: function(){} };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function useParams() { return {}; }
|
|
148
|
+
|
|
149
|
+
export function BrodoxHead() { return null; }
|
|
150
|
+
|
|
151
|
+
export function BrodoxFont() { return null; }
|
|
152
|
+
|
|
153
|
+
export function BrodoxRouter({ children }) {
|
|
154
|
+
return createElement(Fragment, null, children);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* <Client> \u2014 island boundary marker.
|
|
159
|
+
*
|
|
160
|
+
* During SSR emits an empty placeholder div plus a sibling
|
|
161
|
+
* <script type="application/json"> carrying the child's props.
|
|
162
|
+
* The IslandHydrator walks the live DOM and mounts the component from the
|
|
163
|
+
* parent bundle's __registry__ after page load.
|
|
164
|
+
*/
|
|
165
|
+
export function Client({ children }) {
|
|
166
|
+
var id = __nextIslandId();
|
|
167
|
+
var child = Children.only(children);
|
|
168
|
+
var compType = child.type;
|
|
169
|
+
var name = typeof compType !== 'string' && compType
|
|
170
|
+
? (compType.displayName || compType.name || 'Unknown')
|
|
171
|
+
: 'Unknown';
|
|
172
|
+
var props = child.props || {};
|
|
173
|
+
var safeProps = JSON.stringify(props)
|
|
174
|
+
.replace(/</g, '\\u003c')
|
|
175
|
+
.replace(/>/g, '\\u003e')
|
|
176
|
+
.replace(/&/g, '\\u0026');
|
|
177
|
+
return createElement(Fragment, null,
|
|
178
|
+
createElement('div', {
|
|
179
|
+
'data-brodox-island': id,
|
|
180
|
+
'data-hydration': 'load',
|
|
181
|
+
'data-client-js': '',
|
|
182
|
+
'data-component-slug': '',
|
|
183
|
+
'data-version': '',
|
|
184
|
+
'data-component': name,
|
|
185
|
+
}),
|
|
186
|
+
createElement('script', {
|
|
187
|
+
type: 'application/json',
|
|
188
|
+
'data-brodox-props': id,
|
|
189
|
+
dangerouslySetInnerHTML: { __html: safeProps },
|
|
190
|
+
})
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* <Server> \u2014 semantic server-only wrapper. Transparent passthrough.
|
|
196
|
+
*/
|
|
197
|
+
export function Server({ children }) {
|
|
198
|
+
return createElement(Fragment, null, children);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** @deprecated Use <Client> instead. */
|
|
202
|
+
export var ClientRender = Client;
|
|
203
|
+
`
|
|
204
|
+
}));
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// src/compiler.ts
|
|
210
|
+
var ENTRY_PRIORITY = [
|
|
211
|
+
"App.tsx",
|
|
212
|
+
"App.jsx",
|
|
213
|
+
"App.ts",
|
|
214
|
+
"App.js",
|
|
215
|
+
"index.tsx",
|
|
216
|
+
"index.jsx",
|
|
217
|
+
"index.ts",
|
|
218
|
+
"index.js"
|
|
219
|
+
];
|
|
220
|
+
function isClientFile2(content, filePath) {
|
|
221
|
+
const trimmed = content.trimStart();
|
|
222
|
+
if (trimmed.startsWith("'use client'") || trimmed.startsWith('"use client"')) return true;
|
|
223
|
+
return /\.client\.[jt]sx?$/.test(filePath);
|
|
224
|
+
}
|
|
225
|
+
function extractClientComponentName(content, filePath) {
|
|
226
|
+
const m = content.match(/export\s+default\s+function\s+(\w+)/) ?? content.match(/export\s+default\s+(\w+)\s*[;({]/) ?? content.match(/(?:^|\n)function\s+(\w+)/);
|
|
227
|
+
const base = import_node_path3.default.basename(filePath, import_node_path3.default.extname(filePath)).replace(/\.client$/, "");
|
|
228
|
+
return m?.[1] ?? base;
|
|
229
|
+
}
|
|
230
|
+
var CLIENT_EXTERNALS = [
|
|
231
|
+
"react",
|
|
232
|
+
"react-dom",
|
|
233
|
+
"react/jsx-runtime",
|
|
234
|
+
"react/jsx-dev-runtime",
|
|
235
|
+
"@broxium/runtime"
|
|
236
|
+
];
|
|
237
|
+
function findReactNodeModules() {
|
|
238
|
+
const fsSync = require("fs");
|
|
239
|
+
try {
|
|
240
|
+
const reactPkg = require.resolve("react/package.json");
|
|
241
|
+
return [import_node_path3.default.dirname(import_node_path3.default.dirname(reactPkg))];
|
|
242
|
+
} catch {
|
|
243
|
+
}
|
|
244
|
+
let dir = process.cwd();
|
|
245
|
+
for (let i = 0; i < 10; i++) {
|
|
246
|
+
const nm = import_node_path3.default.join(dir, "node_modules");
|
|
247
|
+
const reactPkg = import_node_path3.default.join(nm, "react", "package.json");
|
|
248
|
+
if (fsSync.existsSync(reactPkg)) return [nm];
|
|
249
|
+
const parent = import_node_path3.default.dirname(dir);
|
|
250
|
+
if (parent === dir) break;
|
|
251
|
+
dir = parent;
|
|
252
|
+
}
|
|
253
|
+
return [];
|
|
254
|
+
}
|
|
255
|
+
var BrodoxCompiler = class {
|
|
256
|
+
async compile(input) {
|
|
257
|
+
const tmpDir = import_node_path3.default.join(import_node_os.default.tmpdir(), `brodox-compile-${input.slug}-${(0, import_node_crypto.randomUUID)()}`);
|
|
258
|
+
await import_promises3.default.mkdir(tmpDir, { recursive: true });
|
|
259
|
+
for (const file of input.files) {
|
|
260
|
+
const filePath = import_node_path3.default.join(tmpDir, file.path);
|
|
261
|
+
await import_promises3.default.mkdir(import_node_path3.default.dirname(filePath), { recursive: true });
|
|
262
|
+
await import_promises3.default.writeFile(filePath, file.content, "utf8");
|
|
263
|
+
}
|
|
264
|
+
let entryPoint = null;
|
|
265
|
+
for (const candidate of ENTRY_PRIORITY) {
|
|
266
|
+
const full = import_node_path3.default.join(tmpDir, candidate);
|
|
267
|
+
try {
|
|
268
|
+
await import_promises3.default.access(full);
|
|
269
|
+
entryPoint = full;
|
|
270
|
+
break;
|
|
271
|
+
} catch {
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
if (!entryPoint) {
|
|
275
|
+
throw new Error(`No entry file found in component files for ${input.slug}`);
|
|
276
|
+
}
|
|
277
|
+
const safeName = `${input.slug}-v${input.version}`;
|
|
278
|
+
const serverJsName = `${safeName}.server.esm.js`;
|
|
279
|
+
const clientJsName = `${safeName}.client.esm.js`;
|
|
280
|
+
const serverJsPath = import_node_path3.default.join(input.outputDir, serverJsName);
|
|
281
|
+
const clientJsPath = import_node_path3.default.join(input.outputDir, clientJsName);
|
|
282
|
+
await import_promises3.default.mkdir(input.outputDir, { recursive: true });
|
|
283
|
+
const serverNodePaths = [...input.nodePaths ?? [], ...findReactNodeModules()];
|
|
284
|
+
await esbuild.build({
|
|
285
|
+
entryPoints: [entryPoint],
|
|
286
|
+
bundle: true,
|
|
287
|
+
format: "esm",
|
|
288
|
+
platform: "node",
|
|
289
|
+
target: "node20",
|
|
290
|
+
jsx: "automatic",
|
|
291
|
+
nodePaths: serverNodePaths,
|
|
292
|
+
external: [],
|
|
293
|
+
// no externals — fully self-contained
|
|
294
|
+
plugins: [clientStubPlugin(), runtimeServerStubPlugin(serverNodePaths)],
|
|
295
|
+
outfile: serverJsPath,
|
|
296
|
+
minify: false,
|
|
297
|
+
sourcemap: false,
|
|
298
|
+
define: { "process.env.NODE_ENV": '"production"' }
|
|
299
|
+
});
|
|
300
|
+
const clientComponents = [];
|
|
301
|
+
for (const file of input.files) {
|
|
302
|
+
if (isClientFile2(file.content, file.path)) {
|
|
303
|
+
const name = extractClientComponentName(file.content, file.path);
|
|
304
|
+
clientComponents.push({ name, filePath: file.path });
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
let clientEntryPoint = entryPoint;
|
|
308
|
+
if (clientComponents.length > 0) {
|
|
309
|
+
const entryRelative = import_node_path3.default.relative(tmpDir, entryPoint).replace(/\\/g, "/");
|
|
310
|
+
const importLines = clientComponents.map((c, i) => `import __reg${i}__ from './${c.filePath.replace(/\\/g, "/")}';`).join("\n");
|
|
311
|
+
const registryEntries = clientComponents.map((c, i) => ` '${c.name}': __reg${i}__`).join(",\n");
|
|
312
|
+
const registryWrapper = [
|
|
313
|
+
`export { default } from './${entryRelative}';`,
|
|
314
|
+
importLines,
|
|
315
|
+
`export const __registry__ = {
|
|
316
|
+
${registryEntries}
|
|
317
|
+
};`
|
|
318
|
+
].join("\n");
|
|
319
|
+
const registryEntryPath = import_node_path3.default.join(tmpDir, "__brodox_registry__.jsx");
|
|
320
|
+
await import_promises3.default.writeFile(registryEntryPath, registryWrapper, "utf8");
|
|
321
|
+
clientEntryPoint = registryEntryPath;
|
|
322
|
+
}
|
|
323
|
+
await esbuild.build({
|
|
324
|
+
entryPoints: [clientEntryPoint],
|
|
325
|
+
bundle: true,
|
|
326
|
+
format: "esm",
|
|
327
|
+
platform: "browser",
|
|
328
|
+
target: ["es2020", "chrome90", "firefox88", "safari14"],
|
|
329
|
+
jsx: "automatic",
|
|
330
|
+
external: CLIENT_EXTERNALS,
|
|
331
|
+
plugins: [serverStubPlugin()],
|
|
332
|
+
outfile: clientJsPath,
|
|
333
|
+
minify: true,
|
|
334
|
+
sourcemap: false,
|
|
335
|
+
define: { "process.env.NODE_ENV": '"production"' },
|
|
336
|
+
banner: { js: 'import React from "react";' }
|
|
337
|
+
});
|
|
338
|
+
await import_promises3.default.rm(tmpDir, { recursive: true, force: true });
|
|
339
|
+
return {
|
|
340
|
+
serverJsPath,
|
|
341
|
+
clientJsPath,
|
|
342
|
+
serverJsName,
|
|
343
|
+
clientJsName,
|
|
344
|
+
compiledAt: /* @__PURE__ */ new Date()
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
};
|
|
348
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
349
|
+
0 && (module.exports = {
|
|
350
|
+
BrodoxCompiler
|
|
351
|
+
});
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
2
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
3
|
+
}) : x)(function(x) {
|
|
4
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
5
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
// src/compiler.ts
|
|
9
|
+
import * as esbuild from "esbuild";
|
|
10
|
+
import fs3 from "fs/promises";
|
|
11
|
+
import path3 from "path";
|
|
12
|
+
import os from "os";
|
|
13
|
+
import { randomUUID } from "crypto";
|
|
14
|
+
|
|
15
|
+
// src/plugins/clientStubPlugin.ts
|
|
16
|
+
import fs from "fs/promises";
|
|
17
|
+
import path from "path";
|
|
18
|
+
function isClientFile(content, filePath) {
|
|
19
|
+
const trimmed = content.trimStart();
|
|
20
|
+
if (trimmed.startsWith("'use client'") || trimmed.startsWith('"use client"')) return true;
|
|
21
|
+
return /\.client\.[jt]sx?$/.test(filePath);
|
|
22
|
+
}
|
|
23
|
+
function extractName(content, filePath) {
|
|
24
|
+
const m = content.match(/export\s+default\s+function\s+(\w+)/) ?? content.match(/(?:^|\n)function\s+(\w+)/) ?? content.match(/(?:^|\n)const\s+(\w+)\s*=/);
|
|
25
|
+
const base = path.basename(filePath, path.extname(filePath)).replace(/\.client$/, "");
|
|
26
|
+
return m?.[1] ?? base;
|
|
27
|
+
}
|
|
28
|
+
function clientStubPlugin() {
|
|
29
|
+
return {
|
|
30
|
+
name: "brodox-client-stub",
|
|
31
|
+
setup(build2) {
|
|
32
|
+
build2.onLoad({ filter: /\.(tsx?|jsx?)$/ }, async (args) => {
|
|
33
|
+
const content = await fs.readFile(args.path, "utf8");
|
|
34
|
+
if (isClientFile(content, args.path)) {
|
|
35
|
+
const name = extractName(content, args.path);
|
|
36
|
+
return {
|
|
37
|
+
contents: `
|
|
38
|
+
import React from 'react'
|
|
39
|
+
function ${name}() { return null }
|
|
40
|
+
${name}.displayName = "${name}"
|
|
41
|
+
export default ${name}
|
|
42
|
+
export function getServerData() { return {} }
|
|
43
|
+
`,
|
|
44
|
+
loader: "jsx"
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// src/plugins/serverStubPlugin.ts
|
|
53
|
+
import fs2 from "fs/promises";
|
|
54
|
+
function serverStubPlugin() {
|
|
55
|
+
return {
|
|
56
|
+
name: "brodox-server-stub",
|
|
57
|
+
setup(build2) {
|
|
58
|
+
build2.onLoad({ filter: /\.(tsx?|jsx?)$/ }, async (args) => {
|
|
59
|
+
const content = await fs2.readFile(args.path, "utf8");
|
|
60
|
+
const trimmed = content.trimStart();
|
|
61
|
+
if (trimmed.startsWith("'use server'") || trimmed.startsWith('"use server"')) {
|
|
62
|
+
return {
|
|
63
|
+
contents: `export default function ServerStub() { return null }`,
|
|
64
|
+
loader: "jsx"
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// src/plugins/runtimeServerStubPlugin.ts
|
|
73
|
+
import path2 from "path";
|
|
74
|
+
function runtimeServerStubPlugin(nodePaths = []) {
|
|
75
|
+
const resolveDir = nodePaths.length > 0 ? path2.dirname(nodePaths[0]) : process.cwd();
|
|
76
|
+
return {
|
|
77
|
+
name: "brodox-runtime-server-stub",
|
|
78
|
+
setup(build2) {
|
|
79
|
+
build2.onResolve({ filter: /^@broxium\/runtime$/ }, () => ({
|
|
80
|
+
path: "@broxium/runtime",
|
|
81
|
+
namespace: "brodox-runtime-server-stub"
|
|
82
|
+
}));
|
|
83
|
+
build2.onLoad({ filter: /.*/, namespace: "brodox-runtime-server-stub" }, () => ({
|
|
84
|
+
loader: "js",
|
|
85
|
+
resolveDir,
|
|
86
|
+
contents: `
|
|
87
|
+
import { createElement, Fragment, Children } from 'react';
|
|
88
|
+
var __islandSeq = 0;
|
|
89
|
+
function __nextIslandId() { return 'bi-' + (++__islandSeq) + '-' + Math.random().toString(36).slice(2,7); }
|
|
90
|
+
|
|
91
|
+
export function BrodoxImage({ src, alt, width, height, fill, className, style, priority, quality = 75, sizes }) {
|
|
92
|
+
const maxW = width || 1920;
|
|
93
|
+
const widths = [320, 640, 768, 1024, 1280, 1920].filter(w => w <= maxW);
|
|
94
|
+
if (!widths.length) widths.push(maxW);
|
|
95
|
+
const q = Math.min(100, Math.max(1, quality));
|
|
96
|
+
const enc = encodeURIComponent(src);
|
|
97
|
+
const optimisedSrc = '/api/image?src=' + enc + '&w=' + maxW + '&q=' + q + '&fmt=webp';
|
|
98
|
+
const srcSet = widths.map(w => '/api/image?src=' + enc + '&w=' + w + '&q=' + q + '&fmt=webp ' + w + 'w').join(', ');
|
|
99
|
+
const imgStyle = fill
|
|
100
|
+
? Object.assign({ position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'cover' }, style || {})
|
|
101
|
+
: (style || {});
|
|
102
|
+
return createElement('img', {
|
|
103
|
+
src: optimisedSrc, srcSet, sizes, alt: alt || '',
|
|
104
|
+
width: fill ? undefined : width, height: fill ? undefined : height,
|
|
105
|
+
loading: priority ? 'eager' : 'lazy', decoding: 'async',
|
|
106
|
+
className, style: imgStyle,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function BrodoxLink({ href, children, className, style }) {
|
|
111
|
+
return createElement('a', { href, className, style }, children);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function useRouter() {
|
|
115
|
+
return { pathname: '/', params: {}, navigate: function(){}, back: function(){}, forward: function(){}, prefetch: function(){} };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function useParams() { return {}; }
|
|
119
|
+
|
|
120
|
+
export function BrodoxHead() { return null; }
|
|
121
|
+
|
|
122
|
+
export function BrodoxFont() { return null; }
|
|
123
|
+
|
|
124
|
+
export function BrodoxRouter({ children }) {
|
|
125
|
+
return createElement(Fragment, null, children);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* <Client> \u2014 island boundary marker.
|
|
130
|
+
*
|
|
131
|
+
* During SSR emits an empty placeholder div plus a sibling
|
|
132
|
+
* <script type="application/json"> carrying the child's props.
|
|
133
|
+
* The IslandHydrator walks the live DOM and mounts the component from the
|
|
134
|
+
* parent bundle's __registry__ after page load.
|
|
135
|
+
*/
|
|
136
|
+
export function Client({ children }) {
|
|
137
|
+
var id = __nextIslandId();
|
|
138
|
+
var child = Children.only(children);
|
|
139
|
+
var compType = child.type;
|
|
140
|
+
var name = typeof compType !== 'string' && compType
|
|
141
|
+
? (compType.displayName || compType.name || 'Unknown')
|
|
142
|
+
: 'Unknown';
|
|
143
|
+
var props = child.props || {};
|
|
144
|
+
var safeProps = JSON.stringify(props)
|
|
145
|
+
.replace(/</g, '\\u003c')
|
|
146
|
+
.replace(/>/g, '\\u003e')
|
|
147
|
+
.replace(/&/g, '\\u0026');
|
|
148
|
+
return createElement(Fragment, null,
|
|
149
|
+
createElement('div', {
|
|
150
|
+
'data-brodox-island': id,
|
|
151
|
+
'data-hydration': 'load',
|
|
152
|
+
'data-client-js': '',
|
|
153
|
+
'data-component-slug': '',
|
|
154
|
+
'data-version': '',
|
|
155
|
+
'data-component': name,
|
|
156
|
+
}),
|
|
157
|
+
createElement('script', {
|
|
158
|
+
type: 'application/json',
|
|
159
|
+
'data-brodox-props': id,
|
|
160
|
+
dangerouslySetInnerHTML: { __html: safeProps },
|
|
161
|
+
})
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* <Server> \u2014 semantic server-only wrapper. Transparent passthrough.
|
|
167
|
+
*/
|
|
168
|
+
export function Server({ children }) {
|
|
169
|
+
return createElement(Fragment, null, children);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** @deprecated Use <Client> instead. */
|
|
173
|
+
export var ClientRender = Client;
|
|
174
|
+
`
|
|
175
|
+
}));
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// src/compiler.ts
|
|
181
|
+
var ENTRY_PRIORITY = [
|
|
182
|
+
"App.tsx",
|
|
183
|
+
"App.jsx",
|
|
184
|
+
"App.ts",
|
|
185
|
+
"App.js",
|
|
186
|
+
"index.tsx",
|
|
187
|
+
"index.jsx",
|
|
188
|
+
"index.ts",
|
|
189
|
+
"index.js"
|
|
190
|
+
];
|
|
191
|
+
function isClientFile2(content, filePath) {
|
|
192
|
+
const trimmed = content.trimStart();
|
|
193
|
+
if (trimmed.startsWith("'use client'") || trimmed.startsWith('"use client"')) return true;
|
|
194
|
+
return /\.client\.[jt]sx?$/.test(filePath);
|
|
195
|
+
}
|
|
196
|
+
function extractClientComponentName(content, filePath) {
|
|
197
|
+
const m = content.match(/export\s+default\s+function\s+(\w+)/) ?? content.match(/export\s+default\s+(\w+)\s*[;({]/) ?? content.match(/(?:^|\n)function\s+(\w+)/);
|
|
198
|
+
const base = path3.basename(filePath, path3.extname(filePath)).replace(/\.client$/, "");
|
|
199
|
+
return m?.[1] ?? base;
|
|
200
|
+
}
|
|
201
|
+
var CLIENT_EXTERNALS = [
|
|
202
|
+
"react",
|
|
203
|
+
"react-dom",
|
|
204
|
+
"react/jsx-runtime",
|
|
205
|
+
"react/jsx-dev-runtime",
|
|
206
|
+
"@broxium/runtime"
|
|
207
|
+
];
|
|
208
|
+
function findReactNodeModules() {
|
|
209
|
+
const fsSync = __require("fs");
|
|
210
|
+
try {
|
|
211
|
+
const reactPkg = __require.resolve("react/package.json");
|
|
212
|
+
return [path3.dirname(path3.dirname(reactPkg))];
|
|
213
|
+
} catch {
|
|
214
|
+
}
|
|
215
|
+
let dir = process.cwd();
|
|
216
|
+
for (let i = 0; i < 10; i++) {
|
|
217
|
+
const nm = path3.join(dir, "node_modules");
|
|
218
|
+
const reactPkg = path3.join(nm, "react", "package.json");
|
|
219
|
+
if (fsSync.existsSync(reactPkg)) return [nm];
|
|
220
|
+
const parent = path3.dirname(dir);
|
|
221
|
+
if (parent === dir) break;
|
|
222
|
+
dir = parent;
|
|
223
|
+
}
|
|
224
|
+
return [];
|
|
225
|
+
}
|
|
226
|
+
var BrodoxCompiler = class {
|
|
227
|
+
async compile(input) {
|
|
228
|
+
const tmpDir = path3.join(os.tmpdir(), `brodox-compile-${input.slug}-${randomUUID()}`);
|
|
229
|
+
await fs3.mkdir(tmpDir, { recursive: true });
|
|
230
|
+
for (const file of input.files) {
|
|
231
|
+
const filePath = path3.join(tmpDir, file.path);
|
|
232
|
+
await fs3.mkdir(path3.dirname(filePath), { recursive: true });
|
|
233
|
+
await fs3.writeFile(filePath, file.content, "utf8");
|
|
234
|
+
}
|
|
235
|
+
let entryPoint = null;
|
|
236
|
+
for (const candidate of ENTRY_PRIORITY) {
|
|
237
|
+
const full = path3.join(tmpDir, candidate);
|
|
238
|
+
try {
|
|
239
|
+
await fs3.access(full);
|
|
240
|
+
entryPoint = full;
|
|
241
|
+
break;
|
|
242
|
+
} catch {
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
if (!entryPoint) {
|
|
246
|
+
throw new Error(`No entry file found in component files for ${input.slug}`);
|
|
247
|
+
}
|
|
248
|
+
const safeName = `${input.slug}-v${input.version}`;
|
|
249
|
+
const serverJsName = `${safeName}.server.esm.js`;
|
|
250
|
+
const clientJsName = `${safeName}.client.esm.js`;
|
|
251
|
+
const serverJsPath = path3.join(input.outputDir, serverJsName);
|
|
252
|
+
const clientJsPath = path3.join(input.outputDir, clientJsName);
|
|
253
|
+
await fs3.mkdir(input.outputDir, { recursive: true });
|
|
254
|
+
const serverNodePaths = [...input.nodePaths ?? [], ...findReactNodeModules()];
|
|
255
|
+
await esbuild.build({
|
|
256
|
+
entryPoints: [entryPoint],
|
|
257
|
+
bundle: true,
|
|
258
|
+
format: "esm",
|
|
259
|
+
platform: "node",
|
|
260
|
+
target: "node20",
|
|
261
|
+
jsx: "automatic",
|
|
262
|
+
nodePaths: serverNodePaths,
|
|
263
|
+
external: [],
|
|
264
|
+
// no externals — fully self-contained
|
|
265
|
+
plugins: [clientStubPlugin(), runtimeServerStubPlugin(serverNodePaths)],
|
|
266
|
+
outfile: serverJsPath,
|
|
267
|
+
minify: false,
|
|
268
|
+
sourcemap: false,
|
|
269
|
+
define: { "process.env.NODE_ENV": '"production"' }
|
|
270
|
+
});
|
|
271
|
+
const clientComponents = [];
|
|
272
|
+
for (const file of input.files) {
|
|
273
|
+
if (isClientFile2(file.content, file.path)) {
|
|
274
|
+
const name = extractClientComponentName(file.content, file.path);
|
|
275
|
+
clientComponents.push({ name, filePath: file.path });
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
let clientEntryPoint = entryPoint;
|
|
279
|
+
if (clientComponents.length > 0) {
|
|
280
|
+
const entryRelative = path3.relative(tmpDir, entryPoint).replace(/\\/g, "/");
|
|
281
|
+
const importLines = clientComponents.map((c, i) => `import __reg${i}__ from './${c.filePath.replace(/\\/g, "/")}';`).join("\n");
|
|
282
|
+
const registryEntries = clientComponents.map((c, i) => ` '${c.name}': __reg${i}__`).join(",\n");
|
|
283
|
+
const registryWrapper = [
|
|
284
|
+
`export { default } from './${entryRelative}';`,
|
|
285
|
+
importLines,
|
|
286
|
+
`export const __registry__ = {
|
|
287
|
+
${registryEntries}
|
|
288
|
+
};`
|
|
289
|
+
].join("\n");
|
|
290
|
+
const registryEntryPath = path3.join(tmpDir, "__brodox_registry__.jsx");
|
|
291
|
+
await fs3.writeFile(registryEntryPath, registryWrapper, "utf8");
|
|
292
|
+
clientEntryPoint = registryEntryPath;
|
|
293
|
+
}
|
|
294
|
+
await esbuild.build({
|
|
295
|
+
entryPoints: [clientEntryPoint],
|
|
296
|
+
bundle: true,
|
|
297
|
+
format: "esm",
|
|
298
|
+
platform: "browser",
|
|
299
|
+
target: ["es2020", "chrome90", "firefox88", "safari14"],
|
|
300
|
+
jsx: "automatic",
|
|
301
|
+
external: CLIENT_EXTERNALS,
|
|
302
|
+
plugins: [serverStubPlugin()],
|
|
303
|
+
outfile: clientJsPath,
|
|
304
|
+
minify: true,
|
|
305
|
+
sourcemap: false,
|
|
306
|
+
define: { "process.env.NODE_ENV": '"production"' },
|
|
307
|
+
banner: { js: 'import React from "react";' }
|
|
308
|
+
});
|
|
309
|
+
await fs3.rm(tmpDir, { recursive: true, force: true });
|
|
310
|
+
return {
|
|
311
|
+
serverJsPath,
|
|
312
|
+
clientJsPath,
|
|
313
|
+
serverJsName,
|
|
314
|
+
clientJsName,
|
|
315
|
+
compiledAt: /* @__PURE__ */ new Date()
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
};
|
|
319
|
+
export {
|
|
320
|
+
BrodoxCompiler
|
|
321
|
+
};
|
package/package.json
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@broxium/compiler",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.1",
|
|
4
4
|
"description": "Brodox component compiler — TSX to ESM server + client bundles",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"dist"
|
|
9
|
+
],
|
|
7
10
|
"exports": {
|
|
8
11
|
".": {
|
|
9
12
|
"types": "./dist/index.d.ts",
|
|
@@ -21,6 +24,7 @@
|
|
|
21
24
|
},
|
|
22
25
|
"scripts": {
|
|
23
26
|
"build": "tsup src/index.ts --format esm,cjs --dts",
|
|
24
|
-
"dev": "tsup src/index.ts --format esm,cjs --dts --watch"
|
|
27
|
+
"dev": "tsup src/index.ts --format esm,cjs --dts --watch",
|
|
28
|
+
"prepublishOnly": "npm run build"
|
|
25
29
|
}
|
|
26
30
|
}
|
package/src/compiler.ts
DELETED
|
@@ -1,203 +0,0 @@
|
|
|
1
|
-
import * as esbuild from 'esbuild'
|
|
2
|
-
import fs from 'node:fs/promises'
|
|
3
|
-
import path from 'node:path'
|
|
4
|
-
import os from 'node:os'
|
|
5
|
-
import { randomUUID } from 'node:crypto'
|
|
6
|
-
import { clientStubPlugin } from './plugins/clientStubPlugin'
|
|
7
|
-
import { serverStubPlugin } from './plugins/serverStubPlugin'
|
|
8
|
-
import { runtimeServerStubPlugin } from './plugins/runtimeServerStubPlugin'
|
|
9
|
-
import type { CompileInput, CompileOutput } from './types'
|
|
10
|
-
|
|
11
|
-
const ENTRY_PRIORITY = [
|
|
12
|
-
'App.tsx', 'App.jsx', 'App.ts', 'App.js',
|
|
13
|
-
'index.tsx', 'index.jsx', 'index.ts', 'index.js',
|
|
14
|
-
]
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Detect client component files by directive OR naming convention.
|
|
18
|
-
* *.client.(jsx|tsx) is the preferred convention — no 'use client' needed.
|
|
19
|
-
*/
|
|
20
|
-
function isClientFile(content: string, filePath: string): boolean {
|
|
21
|
-
const trimmed = content.trimStart()
|
|
22
|
-
if (trimmed.startsWith("'use client'") || trimmed.startsWith('"use client"')) return true
|
|
23
|
-
return /\.client\.[jt]sx?$/.test(filePath)
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Extract the default export function name from a client component file.
|
|
28
|
-
* Used to build the __registry__ export in the client bundle, keyed by the
|
|
29
|
-
* same name that clientStubPlugin sets as displayName in the server build.
|
|
30
|
-
* Strips .client from the basename fallback so "Navbar.client" → "Navbar".
|
|
31
|
-
*/
|
|
32
|
-
function extractClientComponentName(content: string, filePath: string): string {
|
|
33
|
-
const m = content.match(/export\s+default\s+function\s+(\w+)/)
|
|
34
|
-
?? content.match(/export\s+default\s+(\w+)\s*[;({]/)
|
|
35
|
-
?? content.match(/(?:^|\n)function\s+(\w+)/)
|
|
36
|
-
const base = path.basename(filePath, path.extname(filePath)).replace(/\.client$/, '')
|
|
37
|
-
return m?.[1] ?? base
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// ── Client build externals ─────────────────────────────────────────────────
|
|
41
|
-
// These are provided by the web-engine's import map in the browser.
|
|
42
|
-
const CLIENT_EXTERNALS = [
|
|
43
|
-
'react',
|
|
44
|
-
'react-dom',
|
|
45
|
-
'react/jsx-runtime',
|
|
46
|
-
'react/jsx-dev-runtime',
|
|
47
|
-
'@broxium/runtime',
|
|
48
|
-
]
|
|
49
|
-
|
|
50
|
-
// ── Server build: find React's node_modules directory ─────────────────────
|
|
51
|
-
// The server bundle must be self-contained (no bare module resolution from
|
|
52
|
-
// live-bundles/). We bundle React directly via esbuild's nodePaths so it
|
|
53
|
-
// can resolve 'react' during compilation. The bundled React is embedded
|
|
54
|
-
// in the output — no external resolution at runtime.
|
|
55
|
-
function findReactNodeModules(): string[] {
|
|
56
|
-
const fsSync = require('fs') as typeof import('fs')
|
|
57
|
-
|
|
58
|
-
// 1. Try require.resolve from the current module (works in most CJS contexts)
|
|
59
|
-
try {
|
|
60
|
-
const reactPkg = require.resolve('react/package.json')
|
|
61
|
-
return [path.dirname(path.dirname(reactPkg))]
|
|
62
|
-
} catch {}
|
|
63
|
-
|
|
64
|
-
// 2. Walk up the directory tree from process.cwd() looking for node_modules/react.
|
|
65
|
-
// When running inside brodox-core, react is installed there even if the
|
|
66
|
-
// current working directory is a sub-app that doesn't list react as a dep.
|
|
67
|
-
let dir = process.cwd()
|
|
68
|
-
for (let i = 0; i < 10; i++) {
|
|
69
|
-
const nm = path.join(dir, 'node_modules')
|
|
70
|
-
const reactPkg = path.join(nm, 'react', 'package.json')
|
|
71
|
-
if (fsSync.existsSync(reactPkg)) return [nm]
|
|
72
|
-
const parent = path.dirname(dir)
|
|
73
|
-
if (parent === dir) break
|
|
74
|
-
dir = parent
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
return []
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
export class BrodoxCompiler {
|
|
81
|
-
async compile(input: CompileInput): Promise<CompileOutput> {
|
|
82
|
-
const tmpDir = path.join(os.tmpdir(), `brodox-compile-${input.slug}-${randomUUID()}`)
|
|
83
|
-
await fs.mkdir(tmpDir, { recursive: true })
|
|
84
|
-
|
|
85
|
-
for (const file of input.files) {
|
|
86
|
-
const filePath = path.join(tmpDir, file.path)
|
|
87
|
-
await fs.mkdir(path.dirname(filePath), { recursive: true })
|
|
88
|
-
await fs.writeFile(filePath, file.content, 'utf8')
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
let entryPoint: string | null = null
|
|
92
|
-
for (const candidate of ENTRY_PRIORITY) {
|
|
93
|
-
const full = path.join(tmpDir, candidate)
|
|
94
|
-
try {
|
|
95
|
-
await fs.access(full)
|
|
96
|
-
entryPoint = full
|
|
97
|
-
break
|
|
98
|
-
} catch {}
|
|
99
|
-
}
|
|
100
|
-
if (!entryPoint) {
|
|
101
|
-
throw new Error(`No entry file found in component files for ${input.slug}`)
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
const safeName = `${input.slug}-v${input.version}`
|
|
105
|
-
const serverJsName = `${safeName}.server.esm.js`
|
|
106
|
-
const clientJsName = `${safeName}.client.esm.js`
|
|
107
|
-
const serverJsPath = path.join(input.outputDir, serverJsName)
|
|
108
|
-
const clientJsPath = path.join(input.outputDir, clientJsName)
|
|
109
|
-
|
|
110
|
-
await fs.mkdir(input.outputDir, { recursive: true })
|
|
111
|
-
|
|
112
|
-
// ── Server bundle ──────────────────────────────────────────────────────
|
|
113
|
-
// Self-contained: React is bundled in via nodePaths so the bundle loads
|
|
114
|
-
// from any directory without a node_modules beside it.
|
|
115
|
-
// nodePaths = caller-provided paths (from CompileInput) + auto-detected.
|
|
116
|
-
//
|
|
117
|
-
// @broxium/runtime → replaced by inline server stubs.
|
|
118
|
-
// 'use client' files → stubbed to null (render nothing server-side).
|
|
119
|
-
const serverNodePaths = [...(input.nodePaths ?? []), ...findReactNodeModules()]
|
|
120
|
-
|
|
121
|
-
await esbuild.build({
|
|
122
|
-
entryPoints: [entryPoint],
|
|
123
|
-
bundle: true,
|
|
124
|
-
format: 'esm',
|
|
125
|
-
platform: 'node',
|
|
126
|
-
target: 'node20',
|
|
127
|
-
jsx: 'automatic',
|
|
128
|
-
nodePaths: serverNodePaths,
|
|
129
|
-
external: [], // no externals — fully self-contained
|
|
130
|
-
plugins: [clientStubPlugin(), runtimeServerStubPlugin(serverNodePaths)],
|
|
131
|
-
outfile: serverJsPath,
|
|
132
|
-
minify: false,
|
|
133
|
-
sourcemap: false,
|
|
134
|
-
define: { 'process.env.NODE_ENV': '"production"' },
|
|
135
|
-
})
|
|
136
|
-
|
|
137
|
-
// ── Client bundle ──────────────────────────────────────────────────────
|
|
138
|
-
// React and @broxium/runtime are external — provided by the web-engine's
|
|
139
|
-
// import map (/static/react.js, /static/broxium-runtime.js).
|
|
140
|
-
// 'use server' files are stubbed to null — they never run in the browser.
|
|
141
|
-
//
|
|
142
|
-
// If the component has any "use client" files, we generate a registry
|
|
143
|
-
// wrapper entry that re-exports `default` plus a `__registry__` map.
|
|
144
|
-
// The IslandHydrator uses __registry__ to mount sub-islands created by
|
|
145
|
-
// <ClientRender> inside server-only parent components.
|
|
146
|
-
//
|
|
147
|
-
// banner: always inject `import React from 'react'` so components that
|
|
148
|
-
// call React.createElement() directly (without JSX syntax) work correctly.
|
|
149
|
-
const clientComponents: Array<{ name: string; filePath: string }> = []
|
|
150
|
-
for (const file of input.files) {
|
|
151
|
-
if (isClientFile(file.content, file.path)) {
|
|
152
|
-
const name = extractClientComponentName(file.content, file.path)
|
|
153
|
-
clientComponents.push({ name, filePath: file.path })
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
let clientEntryPoint = entryPoint
|
|
158
|
-
if (clientComponents.length > 0) {
|
|
159
|
-
const entryRelative = path.relative(tmpDir, entryPoint).replace(/\\/g, '/')
|
|
160
|
-
const importLines = clientComponents
|
|
161
|
-
.map((c, i) => `import __reg${i}__ from './${c.filePath.replace(/\\/g, '/')}';`)
|
|
162
|
-
.join('\n')
|
|
163
|
-
const registryEntries = clientComponents
|
|
164
|
-
.map((c, i) => ` '${c.name}': __reg${i}__`)
|
|
165
|
-
.join(',\n')
|
|
166
|
-
const registryWrapper = [
|
|
167
|
-
`export { default } from './${entryRelative}';`,
|
|
168
|
-
importLines,
|
|
169
|
-
`export const __registry__ = {\n${registryEntries}\n};`,
|
|
170
|
-
].join('\n')
|
|
171
|
-
|
|
172
|
-
const registryEntryPath = path.join(tmpDir, '__brodox_registry__.jsx')
|
|
173
|
-
await fs.writeFile(registryEntryPath, registryWrapper, 'utf8')
|
|
174
|
-
clientEntryPoint = registryEntryPath
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
await esbuild.build({
|
|
178
|
-
entryPoints: [clientEntryPoint],
|
|
179
|
-
bundle: true,
|
|
180
|
-
format: 'esm',
|
|
181
|
-
platform: 'browser',
|
|
182
|
-
target: ['es2020', 'chrome90', 'firefox88', 'safari14'],
|
|
183
|
-
jsx: 'automatic',
|
|
184
|
-
external: CLIENT_EXTERNALS,
|
|
185
|
-
plugins: [serverStubPlugin()],
|
|
186
|
-
outfile: clientJsPath,
|
|
187
|
-
minify: true,
|
|
188
|
-
sourcemap: false,
|
|
189
|
-
define: { 'process.env.NODE_ENV': '"production"' },
|
|
190
|
-
banner: { js: 'import React from "react";' },
|
|
191
|
-
})
|
|
192
|
-
|
|
193
|
-
await fs.rm(tmpDir, { recursive: true, force: true })
|
|
194
|
-
|
|
195
|
-
return {
|
|
196
|
-
serverJsPath,
|
|
197
|
-
clientJsPath,
|
|
198
|
-
serverJsName,
|
|
199
|
-
clientJsName,
|
|
200
|
-
compiledAt: new Date(),
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
}
|
package/src/index.ts
DELETED
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
import type { Plugin } from 'esbuild'
|
|
2
|
-
import fs from 'node:fs/promises'
|
|
3
|
-
import path from 'node:path'
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Detect client components by either:
|
|
7
|
-
* 1. 'use client' / "use client" directive at the top of the file (legacy)
|
|
8
|
-
* 2. *.client.(jsx|tsx|js|ts) filename convention (preferred — no directive needed)
|
|
9
|
-
*
|
|
10
|
-
* The naming convention lets developers avoid directives entirely and rely on
|
|
11
|
-
* <Client> / <Server> wrappers from @broxium/runtime for intent.
|
|
12
|
-
*/
|
|
13
|
-
function isClientFile(content: string, filePath: string): boolean {
|
|
14
|
-
const trimmed = content.trimStart()
|
|
15
|
-
if (trimmed.startsWith("'use client'") || trimmed.startsWith('"use client"')) return true
|
|
16
|
-
return /\.client\.[jt]sx?$/.test(filePath)
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function extractName(content: string, filePath: string): string {
|
|
20
|
-
const m = content.match(/export\s+default\s+function\s+(\w+)/)
|
|
21
|
-
?? content.match(/(?:^|\n)function\s+(\w+)/)
|
|
22
|
-
?? content.match(/(?:^|\n)const\s+(\w+)\s*=/)
|
|
23
|
-
// Strip .client from basename fallback so "Navbar.client" → "Navbar"
|
|
24
|
-
const base = path.basename(filePath, path.extname(filePath)).replace(/\.client$/, '')
|
|
25
|
-
return m?.[1] ?? base
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export function clientStubPlugin(): Plugin {
|
|
29
|
-
return {
|
|
30
|
-
name: 'brodox-client-stub',
|
|
31
|
-
setup(build) {
|
|
32
|
-
build.onLoad({ filter: /\.(tsx?|jsx?)$/ }, async (args) => {
|
|
33
|
-
const content = await fs.readFile(args.path, 'utf8')
|
|
34
|
-
if (isClientFile(content, args.path)) {
|
|
35
|
-
const name = extractName(content, args.path)
|
|
36
|
-
return {
|
|
37
|
-
contents: `
|
|
38
|
-
import React from 'react'
|
|
39
|
-
function ${name}() { return null }
|
|
40
|
-
${name}.displayName = "${name}"
|
|
41
|
-
export default ${name}
|
|
42
|
-
export function getServerData() { return {} }
|
|
43
|
-
`,
|
|
44
|
-
loader: 'jsx',
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
})
|
|
48
|
-
},
|
|
49
|
-
}
|
|
50
|
-
}
|
|
@@ -1,136 +0,0 @@
|
|
|
1
|
-
import type { Plugin } from 'esbuild'
|
|
2
|
-
import path from 'node:path'
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Replaces @broxium/runtime imports in the server bundle with server-safe
|
|
6
|
-
* inline implementations. This keeps the server bundle self-contained —
|
|
7
|
-
* no external module resolution needed from live-bundles/.
|
|
8
|
-
*
|
|
9
|
-
* BrodoxImage → <img> pointing to /api/image (WebP, srcset, lazy)
|
|
10
|
-
* BrodoxLink → <a href>
|
|
11
|
-
* Hooks → server-side no-ops / static defaults
|
|
12
|
-
* Client → island placeholder div + sibling props script
|
|
13
|
-
* Server → transparent passthrough (semantic marker only)
|
|
14
|
-
* ClientRender → alias for Client (deprecated)
|
|
15
|
-
*
|
|
16
|
-
* WHY nodePaths is required
|
|
17
|
-
* ─────────────────────────
|
|
18
|
-
* esbuild virtual modules (custom namespace) default to resolveDir="" which
|
|
19
|
-
* disables ALL bare-specifier resolution inside them. The stub content imports
|
|
20
|
-
* from 'react', so we derive a resolveDir from the caller-supplied nodePaths:
|
|
21
|
-
* the parent directory of the first node_modules entry that contains react is
|
|
22
|
-
* used as resolveDir so that esbuild can walk up and find node_modules/react.
|
|
23
|
-
*/
|
|
24
|
-
export function runtimeServerStubPlugin(nodePaths: string[] = []): Plugin {
|
|
25
|
-
// Each nodePaths entry is a node_modules directory, e.g.
|
|
26
|
-
// /path/to/brodox-web-engine/node_modules
|
|
27
|
-
// resolveDir must be the PARENT so node module resolution walks into it:
|
|
28
|
-
// /path/to/brodox-web-engine
|
|
29
|
-
const resolveDir = nodePaths.length > 0
|
|
30
|
-
? path.dirname(nodePaths[0])
|
|
31
|
-
: process.cwd()
|
|
32
|
-
|
|
33
|
-
return {
|
|
34
|
-
name: 'brodox-runtime-server-stub',
|
|
35
|
-
setup(build) {
|
|
36
|
-
build.onResolve({ filter: /^@broxium\/runtime$/ }, () => ({
|
|
37
|
-
path: '@broxium/runtime',
|
|
38
|
-
namespace: 'brodox-runtime-server-stub',
|
|
39
|
-
}))
|
|
40
|
-
|
|
41
|
-
build.onLoad({ filter: /.*/, namespace: 'brodox-runtime-server-stub' }, () => ({
|
|
42
|
-
loader: 'js',
|
|
43
|
-
resolveDir,
|
|
44
|
-
contents: `
|
|
45
|
-
import { createElement, Fragment, Children } from 'react';
|
|
46
|
-
var __islandSeq = 0;
|
|
47
|
-
function __nextIslandId() { return 'bi-' + (++__islandSeq) + '-' + Math.random().toString(36).slice(2,7); }
|
|
48
|
-
|
|
49
|
-
export function BrodoxImage({ src, alt, width, height, fill, className, style, priority, quality = 75, sizes }) {
|
|
50
|
-
const maxW = width || 1920;
|
|
51
|
-
const widths = [320, 640, 768, 1024, 1280, 1920].filter(w => w <= maxW);
|
|
52
|
-
if (!widths.length) widths.push(maxW);
|
|
53
|
-
const q = Math.min(100, Math.max(1, quality));
|
|
54
|
-
const enc = encodeURIComponent(src);
|
|
55
|
-
const optimisedSrc = '/api/image?src=' + enc + '&w=' + maxW + '&q=' + q + '&fmt=webp';
|
|
56
|
-
const srcSet = widths.map(w => '/api/image?src=' + enc + '&w=' + w + '&q=' + q + '&fmt=webp ' + w + 'w').join(', ');
|
|
57
|
-
const imgStyle = fill
|
|
58
|
-
? Object.assign({ position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'cover' }, style || {})
|
|
59
|
-
: (style || {});
|
|
60
|
-
return createElement('img', {
|
|
61
|
-
src: optimisedSrc, srcSet, sizes, alt: alt || '',
|
|
62
|
-
width: fill ? undefined : width, height: fill ? undefined : height,
|
|
63
|
-
loading: priority ? 'eager' : 'lazy', decoding: 'async',
|
|
64
|
-
className, style: imgStyle,
|
|
65
|
-
});
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
export function BrodoxLink({ href, children, className, style }) {
|
|
69
|
-
return createElement('a', { href, className, style }, children);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
export function useRouter() {
|
|
73
|
-
return { pathname: '/', params: {}, navigate: function(){}, back: function(){}, forward: function(){}, prefetch: function(){} };
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
export function useParams() { return {}; }
|
|
77
|
-
|
|
78
|
-
export function BrodoxHead() { return null; }
|
|
79
|
-
|
|
80
|
-
export function BrodoxFont() { return null; }
|
|
81
|
-
|
|
82
|
-
export function BrodoxRouter({ children }) {
|
|
83
|
-
return createElement(Fragment, null, children);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* <Client> — island boundary marker.
|
|
88
|
-
*
|
|
89
|
-
* During SSR emits an empty placeholder div plus a sibling
|
|
90
|
-
* <script type="application/json"> carrying the child's props.
|
|
91
|
-
* The IslandHydrator walks the live DOM and mounts the component from the
|
|
92
|
-
* parent bundle's __registry__ after page load.
|
|
93
|
-
*/
|
|
94
|
-
export function Client({ children }) {
|
|
95
|
-
var id = __nextIslandId();
|
|
96
|
-
var child = Children.only(children);
|
|
97
|
-
var compType = child.type;
|
|
98
|
-
var name = typeof compType !== 'string' && compType
|
|
99
|
-
? (compType.displayName || compType.name || 'Unknown')
|
|
100
|
-
: 'Unknown';
|
|
101
|
-
var props = child.props || {};
|
|
102
|
-
var safeProps = JSON.stringify(props)
|
|
103
|
-
.replace(/</g, '\\u003c')
|
|
104
|
-
.replace(/>/g, '\\u003e')
|
|
105
|
-
.replace(/&/g, '\\u0026');
|
|
106
|
-
return createElement(Fragment, null,
|
|
107
|
-
createElement('div', {
|
|
108
|
-
'data-brodox-island': id,
|
|
109
|
-
'data-hydration': 'load',
|
|
110
|
-
'data-client-js': '',
|
|
111
|
-
'data-component-slug': '',
|
|
112
|
-
'data-version': '',
|
|
113
|
-
'data-component': name,
|
|
114
|
-
}),
|
|
115
|
-
createElement('script', {
|
|
116
|
-
type: 'application/json',
|
|
117
|
-
'data-brodox-props': id,
|
|
118
|
-
dangerouslySetInnerHTML: { __html: safeProps },
|
|
119
|
-
})
|
|
120
|
-
);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
* <Server> — semantic server-only wrapper. Transparent passthrough.
|
|
125
|
-
*/
|
|
126
|
-
export function Server({ children }) {
|
|
127
|
-
return createElement(Fragment, null, children);
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
/** @deprecated Use <Client> instead. */
|
|
131
|
-
export var ClientRender = Client;
|
|
132
|
-
`,
|
|
133
|
-
}))
|
|
134
|
-
},
|
|
135
|
-
}
|
|
136
|
-
}
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import type { Plugin } from 'esbuild'
|
|
2
|
-
import fs from 'node:fs/promises'
|
|
3
|
-
|
|
4
|
-
export function serverStubPlugin(): Plugin {
|
|
5
|
-
return {
|
|
6
|
-
name: 'brodox-server-stub',
|
|
7
|
-
setup(build) {
|
|
8
|
-
build.onLoad({ filter: /\.(tsx?|jsx?)$/ }, async (args) => {
|
|
9
|
-
const content = await fs.readFile(args.path, 'utf8')
|
|
10
|
-
const trimmed = content.trimStart()
|
|
11
|
-
if (trimmed.startsWith("'use server'") || trimmed.startsWith('"use server"')) {
|
|
12
|
-
return {
|
|
13
|
-
contents: `export default function ServerStub() { return null }`,
|
|
14
|
-
loader: 'jsx',
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
})
|
|
18
|
-
},
|
|
19
|
-
}
|
|
20
|
-
}
|
package/src/types.ts
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
export interface CompileInput {
|
|
2
|
-
componentId: number
|
|
3
|
-
slug: string
|
|
4
|
-
version: string
|
|
5
|
-
files: Array<{
|
|
6
|
-
path: string
|
|
7
|
-
content: string
|
|
8
|
-
}>
|
|
9
|
-
outputDir: string
|
|
10
|
-
/** Extra node_modules directories esbuild uses to resolve React for the server bundle. */
|
|
11
|
-
nodePaths?: string[]
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export interface CompileOutput {
|
|
15
|
-
serverJsPath: string
|
|
16
|
-
clientJsPath: string
|
|
17
|
-
serverJsName: string
|
|
18
|
-
clientJsName: string
|
|
19
|
-
compiledAt: Date
|
|
20
|
-
}
|
package/tsconfig.json
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"target": "ES2020",
|
|
4
|
-
"lib": ["ES2020"],
|
|
5
|
-
"module": "ESNext",
|
|
6
|
-
"moduleResolution": "bundler",
|
|
7
|
-
"strict": true,
|
|
8
|
-
"declaration": true,
|
|
9
|
-
"esModuleInterop": true,
|
|
10
|
-
"skipLibCheck": true,
|
|
11
|
-
"outDir": "./dist"
|
|
12
|
-
},
|
|
13
|
-
"include": ["src"],
|
|
14
|
-
"exclude": ["node_modules", "dist"]
|
|
15
|
-
}
|