@broxium/compiler 1.5.1 → 1.5.2
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 +69 -1
- package/dist/index.d.ts +69 -1
- package/dist/index.js +416 -15
- package/dist/index.mjs +409 -14
- package/package.json +3 -3
package/dist/index.d.mts
CHANGED
|
@@ -13,12 +13,17 @@ interface CompileInput {
|
|
|
13
13
|
interface CompileOutput {
|
|
14
14
|
serverJsPath: string;
|
|
15
15
|
clientJsPath: string;
|
|
16
|
+
/** Browser (canvas) bundle — uses window.__brodox_react globals, safe to load via Blob URL. */
|
|
17
|
+
browserJsPath: string;
|
|
16
18
|
/** Compiled CSS bundle path, or null if no CSS files were imported. */
|
|
17
19
|
cssPath: string | null;
|
|
18
20
|
serverJsName: string;
|
|
19
21
|
clientJsName: string;
|
|
22
|
+
browserJsName: string;
|
|
20
23
|
/** CSS bundle filename, or null if no CSS files were imported. */
|
|
21
24
|
cssName: string | null;
|
|
25
|
+
/** 6-char base36 FNV-1a scope ID used to prefix CSS selectors ([data-bc=ID]). */
|
|
26
|
+
scopeId: string;
|
|
22
27
|
compiledAt: Date;
|
|
23
28
|
}
|
|
24
29
|
|
|
@@ -26,4 +31,67 @@ declare class BrodoxCompiler {
|
|
|
26
31
|
compile(input: CompileInput): Promise<CompileOutput>;
|
|
27
32
|
}
|
|
28
33
|
|
|
29
|
-
|
|
34
|
+
declare function computeScopeId(slug: string, version: string): string;
|
|
35
|
+
declare function scopeCss(css: string, scopeId: string): string;
|
|
36
|
+
declare function minifyScopedCss(css: string, scopeId: string): Promise<string>;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Brodox Page-Level CSS Strategy Engine
|
|
40
|
+
*
|
|
41
|
+
* Decides how the merged CSS for an entire page is delivered to the browser.
|
|
42
|
+
* CSS is scoped per-component by the compiler ([data-bc=XXXXXX] selectors).
|
|
43
|
+
* After publish, all component CSS files are merged and this function chooses
|
|
44
|
+
* the optimal delivery strategy for the combined result.
|
|
45
|
+
*
|
|
46
|
+
* 'none' → no CSS at all → nothing emitted
|
|
47
|
+
* 'inline' → total ≤ INLINE_MAX_BYTES → single <style> in <head>
|
|
48
|
+
* 'separate' → INLINE_MAX < total ≤ CHUNK_MIN → one external <link> file
|
|
49
|
+
* 'chunks' → total > CHUNK_MIN_BYTES → N × CHUNK_SIZE_BYTES <link> files
|
|
50
|
+
* e.g. 95 KB → 3 × 25 KB + 1 × 20 KB
|
|
51
|
+
*
|
|
52
|
+
* Rationale for thresholds
|
|
53
|
+
* ─────────────────────────
|
|
54
|
+
* INLINE_MAX_BYTES 4 096 HTTP request overhead ≈ 100–300 ms RTT. Inlining
|
|
55
|
+
* 4 KB of CSS costs < 4 ms parse time — always faster
|
|
56
|
+
* than an extra round-trip for a small external file.
|
|
57
|
+
*
|
|
58
|
+
* CHUNK_MIN_BYTES 51 200 A single 50 KB external file is acceptable for one
|
|
59
|
+
* HTTP/2 request. Above 50 KB, splitting into parallel
|
|
60
|
+
* 25 KB chunks can start rendering sooner.
|
|
61
|
+
*
|
|
62
|
+
* CHUNK_SIZE_BYTES 25 600 25 KB per chunk — stays within HTTP/2's default
|
|
63
|
+
* initial window size and transfers in 1–2 TCP segments
|
|
64
|
+
* on most connections.
|
|
65
|
+
*/
|
|
66
|
+
declare const PAGE_CSS_THRESHOLDS: {
|
|
67
|
+
/** Embed as <style> when total page CSS ≤ this value (bytes). */
|
|
68
|
+
readonly INLINE_MAX_BYTES: 4096;
|
|
69
|
+
/** Switch to multi-chunk delivery when total page CSS > this value (bytes). */
|
|
70
|
+
readonly CHUNK_MIN_BYTES: 51200;
|
|
71
|
+
/** Maximum bytes per CSS chunk file. */
|
|
72
|
+
readonly CHUNK_SIZE_BYTES: 25600;
|
|
73
|
+
};
|
|
74
|
+
type PageCssStrategy = 'none' | 'inline' | 'separate' | 'chunks';
|
|
75
|
+
interface PageCssDecision {
|
|
76
|
+
strategy: PageCssStrategy;
|
|
77
|
+
/** 'inline': the full merged CSS to embed as <style>. */
|
|
78
|
+
inlineCss?: string;
|
|
79
|
+
/**
|
|
80
|
+
* 'separate': one-element array with the full merged CSS.
|
|
81
|
+
* 'chunks': N-element array, each ≤ CHUNK_SIZE_BYTES, split at rule boundaries.
|
|
82
|
+
* Undefined for 'none' and 'inline'.
|
|
83
|
+
*/
|
|
84
|
+
cssChunks?: string[];
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Choose the optimal CSS delivery strategy for the merged page CSS.
|
|
88
|
+
* Call this with the concatenation of all scoped component CSS for the page.
|
|
89
|
+
*/
|
|
90
|
+
declare function decidePageCssStrategy(mergedCss: string): PageCssDecision;
|
|
91
|
+
/**
|
|
92
|
+
* Split a CSS string into chunks of at most chunkSize bytes each.
|
|
93
|
+
* Splits are always made at the nearest `}` boundary so no rule is cut mid-way.
|
|
94
|
+
*/
|
|
95
|
+
declare function splitCssIntoChunks(css: string, chunkSize: number): string[];
|
|
96
|
+
|
|
97
|
+
export { BrodoxCompiler, type CompileInput, type CompileOutput, PAGE_CSS_THRESHOLDS, type PageCssDecision, type PageCssStrategy, computeScopeId, decidePageCssStrategy, minifyScopedCss, scopeCss, splitCssIntoChunks };
|
package/dist/index.d.ts
CHANGED
|
@@ -13,12 +13,17 @@ interface CompileInput {
|
|
|
13
13
|
interface CompileOutput {
|
|
14
14
|
serverJsPath: string;
|
|
15
15
|
clientJsPath: string;
|
|
16
|
+
/** Browser (canvas) bundle — uses window.__brodox_react globals, safe to load via Blob URL. */
|
|
17
|
+
browserJsPath: string;
|
|
16
18
|
/** Compiled CSS bundle path, or null if no CSS files were imported. */
|
|
17
19
|
cssPath: string | null;
|
|
18
20
|
serverJsName: string;
|
|
19
21
|
clientJsName: string;
|
|
22
|
+
browserJsName: string;
|
|
20
23
|
/** CSS bundle filename, or null if no CSS files were imported. */
|
|
21
24
|
cssName: string | null;
|
|
25
|
+
/** 6-char base36 FNV-1a scope ID used to prefix CSS selectors ([data-bc=ID]). */
|
|
26
|
+
scopeId: string;
|
|
22
27
|
compiledAt: Date;
|
|
23
28
|
}
|
|
24
29
|
|
|
@@ -26,4 +31,67 @@ declare class BrodoxCompiler {
|
|
|
26
31
|
compile(input: CompileInput): Promise<CompileOutput>;
|
|
27
32
|
}
|
|
28
33
|
|
|
29
|
-
|
|
34
|
+
declare function computeScopeId(slug: string, version: string): string;
|
|
35
|
+
declare function scopeCss(css: string, scopeId: string): string;
|
|
36
|
+
declare function minifyScopedCss(css: string, scopeId: string): Promise<string>;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Brodox Page-Level CSS Strategy Engine
|
|
40
|
+
*
|
|
41
|
+
* Decides how the merged CSS for an entire page is delivered to the browser.
|
|
42
|
+
* CSS is scoped per-component by the compiler ([data-bc=XXXXXX] selectors).
|
|
43
|
+
* After publish, all component CSS files are merged and this function chooses
|
|
44
|
+
* the optimal delivery strategy for the combined result.
|
|
45
|
+
*
|
|
46
|
+
* 'none' → no CSS at all → nothing emitted
|
|
47
|
+
* 'inline' → total ≤ INLINE_MAX_BYTES → single <style> in <head>
|
|
48
|
+
* 'separate' → INLINE_MAX < total ≤ CHUNK_MIN → one external <link> file
|
|
49
|
+
* 'chunks' → total > CHUNK_MIN_BYTES → N × CHUNK_SIZE_BYTES <link> files
|
|
50
|
+
* e.g. 95 KB → 3 × 25 KB + 1 × 20 KB
|
|
51
|
+
*
|
|
52
|
+
* Rationale for thresholds
|
|
53
|
+
* ─────────────────────────
|
|
54
|
+
* INLINE_MAX_BYTES 4 096 HTTP request overhead ≈ 100–300 ms RTT. Inlining
|
|
55
|
+
* 4 KB of CSS costs < 4 ms parse time — always faster
|
|
56
|
+
* than an extra round-trip for a small external file.
|
|
57
|
+
*
|
|
58
|
+
* CHUNK_MIN_BYTES 51 200 A single 50 KB external file is acceptable for one
|
|
59
|
+
* HTTP/2 request. Above 50 KB, splitting into parallel
|
|
60
|
+
* 25 KB chunks can start rendering sooner.
|
|
61
|
+
*
|
|
62
|
+
* CHUNK_SIZE_BYTES 25 600 25 KB per chunk — stays within HTTP/2's default
|
|
63
|
+
* initial window size and transfers in 1–2 TCP segments
|
|
64
|
+
* on most connections.
|
|
65
|
+
*/
|
|
66
|
+
declare const PAGE_CSS_THRESHOLDS: {
|
|
67
|
+
/** Embed as <style> when total page CSS ≤ this value (bytes). */
|
|
68
|
+
readonly INLINE_MAX_BYTES: 4096;
|
|
69
|
+
/** Switch to multi-chunk delivery when total page CSS > this value (bytes). */
|
|
70
|
+
readonly CHUNK_MIN_BYTES: 51200;
|
|
71
|
+
/** Maximum bytes per CSS chunk file. */
|
|
72
|
+
readonly CHUNK_SIZE_BYTES: 25600;
|
|
73
|
+
};
|
|
74
|
+
type PageCssStrategy = 'none' | 'inline' | 'separate' | 'chunks';
|
|
75
|
+
interface PageCssDecision {
|
|
76
|
+
strategy: PageCssStrategy;
|
|
77
|
+
/** 'inline': the full merged CSS to embed as <style>. */
|
|
78
|
+
inlineCss?: string;
|
|
79
|
+
/**
|
|
80
|
+
* 'separate': one-element array with the full merged CSS.
|
|
81
|
+
* 'chunks': N-element array, each ≤ CHUNK_SIZE_BYTES, split at rule boundaries.
|
|
82
|
+
* Undefined for 'none' and 'inline'.
|
|
83
|
+
*/
|
|
84
|
+
cssChunks?: string[];
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Choose the optimal CSS delivery strategy for the merged page CSS.
|
|
88
|
+
* Call this with the concatenation of all scoped component CSS for the page.
|
|
89
|
+
*/
|
|
90
|
+
declare function decidePageCssStrategy(mergedCss: string): PageCssDecision;
|
|
91
|
+
/**
|
|
92
|
+
* Split a CSS string into chunks of at most chunkSize bytes each.
|
|
93
|
+
* Splits are always made at the nearest `}` boundary so no rule is cut mid-way.
|
|
94
|
+
*/
|
|
95
|
+
declare function splitCssIntoChunks(css: string, chunkSize: number): string[];
|
|
96
|
+
|
|
97
|
+
export { BrodoxCompiler, type CompileInput, type CompileOutput, PAGE_CSS_THRESHOLDS, type PageCssDecision, type PageCssStrategy, computeScopeId, decidePageCssStrategy, minifyScopedCss, scopeCss, splitCssIntoChunks };
|
package/dist/index.js
CHANGED
|
@@ -30,12 +30,18 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
30
30
|
// src/index.ts
|
|
31
31
|
var index_exports = {};
|
|
32
32
|
__export(index_exports, {
|
|
33
|
-
BrodoxCompiler: () => BrodoxCompiler
|
|
33
|
+
BrodoxCompiler: () => BrodoxCompiler,
|
|
34
|
+
PAGE_CSS_THRESHOLDS: () => PAGE_CSS_THRESHOLDS,
|
|
35
|
+
computeScopeId: () => computeScopeId,
|
|
36
|
+
decidePageCssStrategy: () => decidePageCssStrategy,
|
|
37
|
+
minifyScopedCss: () => minifyScopedCss,
|
|
38
|
+
scopeCss: () => scopeCss,
|
|
39
|
+
splitCssIntoChunks: () => splitCssIntoChunks
|
|
34
40
|
});
|
|
35
41
|
module.exports = __toCommonJS(index_exports);
|
|
36
42
|
|
|
37
43
|
// src/compiler.ts
|
|
38
|
-
var
|
|
44
|
+
var esbuild2 = __toESM(require("esbuild"));
|
|
39
45
|
var import_promises3 = __toESM(require("fs/promises"));
|
|
40
46
|
var import_node_path3 = __toESM(require("path"));
|
|
41
47
|
var import_node_os = __toESM(require("os"));
|
|
@@ -236,6 +242,311 @@ export var ClientRender = Client;
|
|
|
236
242
|
};
|
|
237
243
|
}
|
|
238
244
|
|
|
245
|
+
// src/plugins/canvasBuildPlugin.ts
|
|
246
|
+
function canvasReactPlugin() {
|
|
247
|
+
return {
|
|
248
|
+
name: "brodox-canvas-react",
|
|
249
|
+
setup(build2) {
|
|
250
|
+
build2.onResolve({ filter: /^react\/jsx-runtime$/ }, () => ({
|
|
251
|
+
path: "react/jsx-runtime",
|
|
252
|
+
namespace: "brodox-canvas-jsx"
|
|
253
|
+
}));
|
|
254
|
+
build2.onLoad({ filter: /.*/, namespace: "brodox-canvas-jsx" }, () => ({
|
|
255
|
+
loader: "js",
|
|
256
|
+
// Use window.__brodox_jsx (ReactJSXRuntime) — the real jsx/jsxs functions
|
|
257
|
+
// behave differently from createElement for children handling.
|
|
258
|
+
contents: `
|
|
259
|
+
var _jx = window.__brodox_jsx;
|
|
260
|
+
export var jsx = _jx.jsx;
|
|
261
|
+
export var jsxs = _jx.jsxs;
|
|
262
|
+
export var Fragment = _jx.Fragment;
|
|
263
|
+
`.trim()
|
|
264
|
+
}));
|
|
265
|
+
build2.onResolve({ filter: /^react\/jsx-dev-runtime$/ }, () => ({
|
|
266
|
+
path: "react/jsx-dev-runtime",
|
|
267
|
+
namespace: "brodox-canvas-jsx-dev"
|
|
268
|
+
}));
|
|
269
|
+
build2.onLoad({ filter: /.*/, namespace: "brodox-canvas-jsx-dev" }, () => ({
|
|
270
|
+
loader: "js",
|
|
271
|
+
contents: `
|
|
272
|
+
var _jx = window.__brodox_jsx;
|
|
273
|
+
export var jsxDEV = _jx.jsx || _jx.jsxDEV;
|
|
274
|
+
export var Fragment = _jx.Fragment;
|
|
275
|
+
`.trim()
|
|
276
|
+
}));
|
|
277
|
+
build2.onResolve({ filter: /^react$/ }, () => ({
|
|
278
|
+
path: "react",
|
|
279
|
+
namespace: "brodox-canvas-react"
|
|
280
|
+
}));
|
|
281
|
+
build2.onLoad({ filter: /.*/, namespace: "brodox-canvas-react" }, () => ({
|
|
282
|
+
loader: "js",
|
|
283
|
+
contents: `
|
|
284
|
+
var _r = window.__brodox_react;
|
|
285
|
+
export default _r;
|
|
286
|
+
export var Children = _r.Children;
|
|
287
|
+
export var createElement = _r.createElement;
|
|
288
|
+
export var cloneElement = _r.cloneElement;
|
|
289
|
+
export var createContext = _r.createContext;
|
|
290
|
+
export var createRef = _r.createRef;
|
|
291
|
+
export var forwardRef = _r.forwardRef;
|
|
292
|
+
export var Fragment = _r.Fragment;
|
|
293
|
+
export var isValidElement = _r.isValidElement;
|
|
294
|
+
export var lazy = _r.lazy;
|
|
295
|
+
export var memo = _r.memo;
|
|
296
|
+
export var Suspense = _r.Suspense;
|
|
297
|
+
export var useCallback = _r.useCallback;
|
|
298
|
+
export var useContext = _r.useContext;
|
|
299
|
+
export var useDebugValue = _r.useDebugValue;
|
|
300
|
+
export var useEffect = _r.useEffect;
|
|
301
|
+
export var useId = _r.useId;
|
|
302
|
+
export var useImperativeHandle = _r.useImperativeHandle;
|
|
303
|
+
export var useLayoutEffect = _r.useLayoutEffect;
|
|
304
|
+
export var useMemo = _r.useMemo;
|
|
305
|
+
export var useReducer = _r.useReducer;
|
|
306
|
+
export var useRef = _r.useRef;
|
|
307
|
+
export var useState = _r.useState;
|
|
308
|
+
export var useSyncExternalStore= _r.useSyncExternalStore;
|
|
309
|
+
export var useTransition = _r.useTransition;
|
|
310
|
+
export var startTransition = _r.startTransition;
|
|
311
|
+
`.trim()
|
|
312
|
+
}));
|
|
313
|
+
build2.onResolve({ filter: /^react-dom(\/.*)?$/ }, () => ({
|
|
314
|
+
path: "react-dom",
|
|
315
|
+
namespace: "brodox-canvas-react-dom"
|
|
316
|
+
}));
|
|
317
|
+
build2.onLoad({ filter: /.*/, namespace: "brodox-canvas-react-dom" }, () => ({
|
|
318
|
+
loader: "js",
|
|
319
|
+
contents: `
|
|
320
|
+
var _rd = window.__brodox_react_dom;
|
|
321
|
+
export default _rd;
|
|
322
|
+
export var createPortal = _rd && _rd.createPortal;
|
|
323
|
+
export var flushSync = _rd && _rd.flushSync;
|
|
324
|
+
`.trim()
|
|
325
|
+
}));
|
|
326
|
+
}
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
function canvasRuntimePlugin() {
|
|
330
|
+
return {
|
|
331
|
+
name: "brodox-runtime-canvas-stub",
|
|
332
|
+
setup(build2) {
|
|
333
|
+
build2.onResolve({ filter: /^@broxium\/runtime$/ }, () => ({
|
|
334
|
+
path: "@broxium/runtime",
|
|
335
|
+
namespace: "brodox-runtime-canvas"
|
|
336
|
+
}));
|
|
337
|
+
build2.onLoad({ filter: /.*/, namespace: "brodox-runtime-canvas" }, () => ({
|
|
338
|
+
loader: "js",
|
|
339
|
+
contents: `
|
|
340
|
+
// brodox-runtime-canvas:@broxium/runtime
|
|
341
|
+
var React = window.__brodox_react;
|
|
342
|
+
var h = React.createElement;
|
|
343
|
+
var F = React.Fragment;
|
|
344
|
+
|
|
345
|
+
export function BrodoxHead() { return null; }
|
|
346
|
+
export function BrodoxFont() { return null; }
|
|
347
|
+
|
|
348
|
+
export function BrodoxLink({ href, children, className, style, target }) {
|
|
349
|
+
return h('a', { href, className, style, target }, children);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
export function BrodoxImage({ src, alt, width, height, fill, className, style }) {
|
|
353
|
+
var imgStyle = fill
|
|
354
|
+
? Object.assign({ width: '100%', height: '100%', objectFit: 'cover' }, style || {})
|
|
355
|
+
: (style || {});
|
|
356
|
+
return h('img', { src: src, alt: alt || '', width: fill ? undefined : width, height: fill ? undefined : height, className: className, style: imgStyle });
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
export function BrodoxRouter({ children }) { return h(F, null, children); }
|
|
360
|
+
|
|
361
|
+
export function useRouter() {
|
|
362
|
+
return { pathname: '/', params: {}, navigate: function() {}, back: function() {}, forward: function() {}, prefetch: function() {} };
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
export function useParams() { return {}; }
|
|
366
|
+
|
|
367
|
+
// In the canvas, Client and Server are both transparent: render children as-is.
|
|
368
|
+
export function Client({ children }) { return h(F, null, children); }
|
|
369
|
+
export function Server({ children }) { return h(F, null, children); }
|
|
370
|
+
export var ClientRender = Client;
|
|
371
|
+
`.trim()
|
|
372
|
+
}));
|
|
373
|
+
}
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// src/utils/scopeCss.ts
|
|
378
|
+
var esbuild = __toESM(require("esbuild"));
|
|
379
|
+
function computeScopeId(slug, version) {
|
|
380
|
+
let hash = 2166136261;
|
|
381
|
+
const input = `${slug}@${version}`;
|
|
382
|
+
for (let i = 0; i < input.length; i++) {
|
|
383
|
+
hash ^= input.charCodeAt(i);
|
|
384
|
+
hash = Math.imul(hash, 16777619) >>> 0;
|
|
385
|
+
}
|
|
386
|
+
return hash.toString(36).slice(0, 6).padStart(6, "0");
|
|
387
|
+
}
|
|
388
|
+
function scopeSelectors(selectorText, scope) {
|
|
389
|
+
return selectorText.split(",").map((s) => {
|
|
390
|
+
const t = s.trim();
|
|
391
|
+
if (!t) return t;
|
|
392
|
+
if (t === ":root") return scope;
|
|
393
|
+
if (t === "html" || t === "body") return t;
|
|
394
|
+
return `${scope} ${t}`;
|
|
395
|
+
}).join(",\n");
|
|
396
|
+
}
|
|
397
|
+
function scopeCss(css, scopeId) {
|
|
398
|
+
const scope = `[data-bc=${scopeId}]`;
|
|
399
|
+
return scopeBlock(css, scope);
|
|
400
|
+
}
|
|
401
|
+
function scopeBlock(css, scope) {
|
|
402
|
+
let i = 0;
|
|
403
|
+
let out = "";
|
|
404
|
+
const len = css.length;
|
|
405
|
+
while (i < len) {
|
|
406
|
+
const wsStart = i;
|
|
407
|
+
while (i < len && /\s/.test(css[i])) i++;
|
|
408
|
+
out += css.slice(wsStart, i);
|
|
409
|
+
if (i >= len) break;
|
|
410
|
+
if (css[i] === "/" && css[i + 1] === "*") {
|
|
411
|
+
const end = css.indexOf("*/", i + 2);
|
|
412
|
+
if (end === -1) {
|
|
413
|
+
out += css.slice(i);
|
|
414
|
+
break;
|
|
415
|
+
}
|
|
416
|
+
out += css.slice(i, end + 2);
|
|
417
|
+
i = end + 2;
|
|
418
|
+
continue;
|
|
419
|
+
}
|
|
420
|
+
if (css[i] === "@") {
|
|
421
|
+
let j = i + 1;
|
|
422
|
+
while (j < len && /[a-zA-Z-]/.test(css[j])) j++;
|
|
423
|
+
const atName = css.slice(i + 1, j).toLowerCase();
|
|
424
|
+
if (atName === "charset" || atName === "import") {
|
|
425
|
+
const semi = css.indexOf(";", j);
|
|
426
|
+
if (semi === -1) {
|
|
427
|
+
out += css.slice(i);
|
|
428
|
+
i = len;
|
|
429
|
+
} else {
|
|
430
|
+
out += css.slice(i, semi + 1);
|
|
431
|
+
i = semi + 1;
|
|
432
|
+
}
|
|
433
|
+
continue;
|
|
434
|
+
}
|
|
435
|
+
if (atName === "keyframes" || atName === "-webkit-keyframes" || atName === "-moz-keyframes" || atName === "font-face") {
|
|
436
|
+
while (j < len && css[j] !== "{") j++;
|
|
437
|
+
if (j >= len) {
|
|
438
|
+
out += css.slice(i);
|
|
439
|
+
i = len;
|
|
440
|
+
continue;
|
|
441
|
+
}
|
|
442
|
+
const { block, end: blockEnd } = readBlock(css, j);
|
|
443
|
+
out += css.slice(i, j) + block;
|
|
444
|
+
i = blockEnd;
|
|
445
|
+
continue;
|
|
446
|
+
}
|
|
447
|
+
if (atName === "media" || atName === "supports" || atName === "layer" || atName === "container") {
|
|
448
|
+
while (j < len && css[j] !== "{") j++;
|
|
449
|
+
if (j >= len) {
|
|
450
|
+
out += css.slice(i);
|
|
451
|
+
i = len;
|
|
452
|
+
continue;
|
|
453
|
+
}
|
|
454
|
+
const condition = css.slice(i, j);
|
|
455
|
+
const { body, end: blockEnd } = readBlockBody(css, j);
|
|
456
|
+
const scopedBody = scopeBlock(body, scope);
|
|
457
|
+
out += `${condition}{
|
|
458
|
+
${scopedBody}}
|
|
459
|
+
`;
|
|
460
|
+
i = blockEnd;
|
|
461
|
+
continue;
|
|
462
|
+
}
|
|
463
|
+
while (j < len && css[j] !== "{" && css[j] !== ";") j++;
|
|
464
|
+
if (j >= len) {
|
|
465
|
+
out += css.slice(i);
|
|
466
|
+
i = len;
|
|
467
|
+
continue;
|
|
468
|
+
}
|
|
469
|
+
if (css[j] === ";") {
|
|
470
|
+
out += css.slice(i, j + 1);
|
|
471
|
+
i = j + 1;
|
|
472
|
+
continue;
|
|
473
|
+
}
|
|
474
|
+
const { block: unknownBlock, end: unknownEnd } = readBlock(css, j);
|
|
475
|
+
out += css.slice(i, j) + unknownBlock;
|
|
476
|
+
i = unknownEnd;
|
|
477
|
+
continue;
|
|
478
|
+
}
|
|
479
|
+
let selectorEnd = i;
|
|
480
|
+
while (selectorEnd < len) {
|
|
481
|
+
if (css[selectorEnd] === "{") break;
|
|
482
|
+
if (css[selectorEnd] === "/" && css[selectorEnd + 1] === "*") {
|
|
483
|
+
const end = css.indexOf("*/", selectorEnd + 2);
|
|
484
|
+
selectorEnd = end === -1 ? len : end + 2;
|
|
485
|
+
continue;
|
|
486
|
+
}
|
|
487
|
+
selectorEnd++;
|
|
488
|
+
}
|
|
489
|
+
if (selectorEnd >= len) {
|
|
490
|
+
out += css.slice(i);
|
|
491
|
+
break;
|
|
492
|
+
}
|
|
493
|
+
const rawSelector = css.slice(i, selectorEnd);
|
|
494
|
+
const trimmedSelector = rawSelector.trim();
|
|
495
|
+
if (!trimmedSelector) {
|
|
496
|
+
i = selectorEnd;
|
|
497
|
+
continue;
|
|
498
|
+
}
|
|
499
|
+
const cleanSelector = trimmedSelector.replace(/\/\*.*?\*\//gs, "").trim();
|
|
500
|
+
const scoped = scopeSelectors(cleanSelector, scope);
|
|
501
|
+
const { body: declBody, end: declEnd } = readBlockBody(css, selectorEnd);
|
|
502
|
+
out += `${scoped} {
|
|
503
|
+
${declBody}}
|
|
504
|
+
`;
|
|
505
|
+
i = declEnd;
|
|
506
|
+
}
|
|
507
|
+
return out;
|
|
508
|
+
}
|
|
509
|
+
function readBlock(css, openBrace) {
|
|
510
|
+
let depth = 0;
|
|
511
|
+
let i = openBrace;
|
|
512
|
+
const len = css.length;
|
|
513
|
+
while (i < len) {
|
|
514
|
+
if (css[i] === "{") depth++;
|
|
515
|
+
else if (css[i] === "}") {
|
|
516
|
+
depth--;
|
|
517
|
+
if (depth === 0) {
|
|
518
|
+
return { block: css.slice(openBrace, i + 1), end: i + 1 };
|
|
519
|
+
}
|
|
520
|
+
} else if (css[i] === "/" && css[i + 1] === "*") {
|
|
521
|
+
const end = css.indexOf("*/", i + 2);
|
|
522
|
+
if (end === -1) {
|
|
523
|
+
i = len;
|
|
524
|
+
break;
|
|
525
|
+
}
|
|
526
|
+
i = end + 1;
|
|
527
|
+
}
|
|
528
|
+
i++;
|
|
529
|
+
}
|
|
530
|
+
return { block: css.slice(openBrace), end: len };
|
|
531
|
+
}
|
|
532
|
+
function readBlockBody(css, openBrace) {
|
|
533
|
+
const { block, end } = readBlock(css, openBrace);
|
|
534
|
+
const body = block.slice(1, -1);
|
|
535
|
+
return { body, end };
|
|
536
|
+
}
|
|
537
|
+
async function minifyScopedCss(css, scopeId) {
|
|
538
|
+
const scoped = scopeCss(css, scopeId);
|
|
539
|
+
try {
|
|
540
|
+
const result = await esbuild.transform(scoped, {
|
|
541
|
+
loader: "css",
|
|
542
|
+
minify: true
|
|
543
|
+
});
|
|
544
|
+
return result.code;
|
|
545
|
+
} catch {
|
|
546
|
+
return scoped;
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
239
550
|
// src/compiler.ts
|
|
240
551
|
var ENTRY_PRIORITY = [
|
|
241
552
|
"App.tsx",
|
|
@@ -282,6 +593,21 @@ function findReactNodeModules() {
|
|
|
282
593
|
}
|
|
283
594
|
return [];
|
|
284
595
|
}
|
|
596
|
+
function cssTextPlugin(scopeId) {
|
|
597
|
+
return {
|
|
598
|
+
name: "brodox-css-text",
|
|
599
|
+
setup(build2) {
|
|
600
|
+
build2.onLoad({ filter: /\.css$/ }, async (args) => {
|
|
601
|
+
const content = await import_promises3.default.readFile(args.path, "utf8");
|
|
602
|
+
const minified = await minifyScopedCss(content, scopeId);
|
|
603
|
+
return {
|
|
604
|
+
contents: `module.exports = ${JSON.stringify(minified)};`,
|
|
605
|
+
loader: "js"
|
|
606
|
+
};
|
|
607
|
+
});
|
|
608
|
+
}
|
|
609
|
+
};
|
|
610
|
+
}
|
|
285
611
|
var BrodoxCompiler = class {
|
|
286
612
|
async compile(input) {
|
|
287
613
|
const tmpDir = import_node_path3.default.join(import_node_os.default.tmpdir(), `brodox-compile-${input.slug}-${(0, import_node_crypto.randomUUID)()}`);
|
|
@@ -304,14 +630,17 @@ var BrodoxCompiler = class {
|
|
|
304
630
|
if (!entryPoint) {
|
|
305
631
|
throw new Error(`No entry file found in component files for ${input.slug}`);
|
|
306
632
|
}
|
|
633
|
+
const scopeId = computeScopeId(input.slug, input.version);
|
|
307
634
|
const safeName = `${input.slug}-v${input.version}`;
|
|
308
635
|
const serverJsName = `${safeName}.server.esm.js`;
|
|
309
636
|
const clientJsName = `${safeName}.client.esm.js`;
|
|
637
|
+
const browserJsName = `${safeName}.browser.js`;
|
|
310
638
|
const serverJsPath = import_node_path3.default.join(input.outputDir, serverJsName);
|
|
311
639
|
const clientJsPath = import_node_path3.default.join(input.outputDir, clientJsName);
|
|
640
|
+
const browserJsPath = import_node_path3.default.join(input.outputDir, browserJsName);
|
|
312
641
|
await import_promises3.default.mkdir(input.outputDir, { recursive: true });
|
|
313
642
|
const serverNodePaths = [...input.nodePaths ?? [], ...findReactNodeModules()];
|
|
314
|
-
await
|
|
643
|
+
await esbuild2.build({
|
|
315
644
|
entryPoints: [entryPoint],
|
|
316
645
|
bundle: true,
|
|
317
646
|
format: "esm",
|
|
@@ -321,13 +650,11 @@ var BrodoxCompiler = class {
|
|
|
321
650
|
nodePaths: serverNodePaths,
|
|
322
651
|
external: [],
|
|
323
652
|
// no externals — fully self-contained
|
|
324
|
-
plugins: [clientStubPlugin(), runtimeServerStubPlugin(serverNodePaths)],
|
|
653
|
+
plugins: [clientStubPlugin(), runtimeServerStubPlugin(serverNodePaths), cssTextPlugin(scopeId)],
|
|
325
654
|
outfile: serverJsPath,
|
|
326
655
|
minify: false,
|
|
327
656
|
sourcemap: false,
|
|
328
|
-
define: { "process.env.NODE_ENV": '"production"' }
|
|
329
|
-
loader: { ".css": "text" }
|
|
330
|
-
// CSS imports return empty string in server bundle
|
|
657
|
+
define: { "process.env.NODE_ENV": '"production"' }
|
|
331
658
|
});
|
|
332
659
|
const clientComponents = [];
|
|
333
660
|
for (const file of input.files) {
|
|
@@ -352,7 +679,7 @@ ${registryEntries}
|
|
|
352
679
|
await import_promises3.default.writeFile(registryEntryPath, registryWrapper, "utf8");
|
|
353
680
|
clientEntryPoint = registryEntryPath;
|
|
354
681
|
}
|
|
355
|
-
await
|
|
682
|
+
await esbuild2.build({
|
|
356
683
|
entryPoints: [clientEntryPoint],
|
|
357
684
|
bundle: true,
|
|
358
685
|
format: "esm",
|
|
@@ -360,21 +687,35 @@ ${registryEntries}
|
|
|
360
687
|
target: ["es2020", "chrome90", "firefox88", "safari14"],
|
|
361
688
|
jsx: "automatic",
|
|
362
689
|
external: CLIENT_EXTERNALS,
|
|
363
|
-
plugins: [serverStubPlugin()],
|
|
690
|
+
plugins: [serverStubPlugin(), cssTextPlugin(scopeId)],
|
|
364
691
|
outfile: clientJsPath,
|
|
365
692
|
minify: true,
|
|
366
693
|
sourcemap: false,
|
|
367
694
|
define: { "process.env.NODE_ENV": '"production"' },
|
|
368
|
-
banner: { js: 'import React from "react";' }
|
|
369
|
-
|
|
370
|
-
|
|
695
|
+
banner: { js: 'import React from "react";' }
|
|
696
|
+
});
|
|
697
|
+
await esbuild2.build({
|
|
698
|
+
entryPoints: [clientEntryPoint],
|
|
699
|
+
bundle: true,
|
|
700
|
+
format: "esm",
|
|
701
|
+
platform: "browser",
|
|
702
|
+
target: ["es2020", "chrome90", "firefox88", "safari14"],
|
|
703
|
+
jsx: "automatic",
|
|
704
|
+
external: [],
|
|
705
|
+
// no externals — everything inlined or replaced by plugins
|
|
706
|
+
plugins: [canvasReactPlugin(), canvasRuntimePlugin(), cssTextPlugin(scopeId)],
|
|
707
|
+
outfile: browserJsPath,
|
|
708
|
+
minify: true,
|
|
709
|
+
sourcemap: false,
|
|
710
|
+
define: { "process.env.NODE_ENV": '"production"' }
|
|
711
|
+
// no banner — canvasReactPlugin handles the React reference via window global
|
|
371
712
|
});
|
|
372
713
|
const hasCss = input.files.some((f) => /\.css$/.test(f.path));
|
|
373
714
|
const cssName = hasCss ? `${safeName}.css` : null;
|
|
374
715
|
const cssPath = hasCss ? import_node_path3.default.join(input.outputDir, cssName) : null;
|
|
375
716
|
if (hasCss && cssPath) {
|
|
376
717
|
try {
|
|
377
|
-
await
|
|
718
|
+
await esbuild2.build({
|
|
378
719
|
entryPoints: [entryPoint],
|
|
379
720
|
bundle: true,
|
|
380
721
|
format: "esm",
|
|
@@ -384,7 +725,8 @@ ${registryEntries}
|
|
|
384
725
|
plugins: [serverStubPlugin()],
|
|
385
726
|
outfile: cssPath.replace(/\.css$/, ".css.tmp.js"),
|
|
386
727
|
// esbuild needs a JS outfile
|
|
387
|
-
minify:
|
|
728
|
+
minify: false,
|
|
729
|
+
// we'll minify after scoping
|
|
388
730
|
sourcemap: false,
|
|
389
731
|
define: { "process.env.NODE_ENV": '"production"' },
|
|
390
732
|
loader: { ".css": "css" }
|
|
@@ -395,6 +737,12 @@ ${registryEntries}
|
|
|
395
737
|
} catch {
|
|
396
738
|
}
|
|
397
739
|
await import_promises3.default.rm(cssPath.replace(/\.css$/, ".css.tmp.js"), { force: true });
|
|
740
|
+
try {
|
|
741
|
+
const rawCss = await import_promises3.default.readFile(cssPath, "utf8");
|
|
742
|
+
const minifiedScoped = await minifyScopedCss(rawCss, scopeId);
|
|
743
|
+
await import_promises3.default.writeFile(cssPath, minifiedScoped, "utf8");
|
|
744
|
+
} catch {
|
|
745
|
+
}
|
|
398
746
|
} catch {
|
|
399
747
|
}
|
|
400
748
|
}
|
|
@@ -402,15 +750,68 @@ ${registryEntries}
|
|
|
402
750
|
return {
|
|
403
751
|
serverJsPath,
|
|
404
752
|
clientJsPath,
|
|
753
|
+
browserJsPath,
|
|
405
754
|
cssPath,
|
|
406
755
|
serverJsName,
|
|
407
756
|
clientJsName,
|
|
757
|
+
browserJsName,
|
|
408
758
|
cssName,
|
|
759
|
+
scopeId,
|
|
409
760
|
compiledAt: /* @__PURE__ */ new Date()
|
|
410
761
|
};
|
|
411
762
|
}
|
|
412
763
|
};
|
|
764
|
+
|
|
765
|
+
// src/utils/cssStrategy.ts
|
|
766
|
+
var PAGE_CSS_THRESHOLDS = {
|
|
767
|
+
/** Embed as <style> when total page CSS ≤ this value (bytes). */
|
|
768
|
+
INLINE_MAX_BYTES: 4096,
|
|
769
|
+
/** Switch to multi-chunk delivery when total page CSS > this value (bytes). */
|
|
770
|
+
CHUNK_MIN_BYTES: 51200,
|
|
771
|
+
/** Maximum bytes per CSS chunk file. */
|
|
772
|
+
CHUNK_SIZE_BYTES: 25600
|
|
773
|
+
};
|
|
774
|
+
function decidePageCssStrategy(mergedCss) {
|
|
775
|
+
const size = Buffer.byteLength(mergedCss, "utf8");
|
|
776
|
+
if (size === 0) return { strategy: "none" };
|
|
777
|
+
if (size <= PAGE_CSS_THRESHOLDS.INLINE_MAX_BYTES) {
|
|
778
|
+
return { strategy: "inline", inlineCss: mergedCss };
|
|
779
|
+
}
|
|
780
|
+
if (size > PAGE_CSS_THRESHOLDS.CHUNK_MIN_BYTES) {
|
|
781
|
+
return {
|
|
782
|
+
strategy: "chunks",
|
|
783
|
+
cssChunks: splitCssIntoChunks(mergedCss, PAGE_CSS_THRESHOLDS.CHUNK_SIZE_BYTES)
|
|
784
|
+
};
|
|
785
|
+
}
|
|
786
|
+
return { strategy: "separate", cssChunks: [mergedCss] };
|
|
787
|
+
}
|
|
788
|
+
function splitCssIntoChunks(css, chunkSize) {
|
|
789
|
+
const chunks = [];
|
|
790
|
+
let remaining = css.trim();
|
|
791
|
+
while (remaining.length > 0) {
|
|
792
|
+
const buf = Buffer.from(remaining, "utf8");
|
|
793
|
+
if (buf.byteLength <= chunkSize) {
|
|
794
|
+
chunks.push(remaining);
|
|
795
|
+
break;
|
|
796
|
+
}
|
|
797
|
+
const candidate = buf.slice(0, chunkSize).toString("utf8");
|
|
798
|
+
const lastBrace = candidate.lastIndexOf("}");
|
|
799
|
+
if (lastBrace < 0) {
|
|
800
|
+
chunks.push(remaining);
|
|
801
|
+
break;
|
|
802
|
+
}
|
|
803
|
+
chunks.push(remaining.slice(0, lastBrace + 1));
|
|
804
|
+
remaining = remaining.slice(lastBrace + 1).trim();
|
|
805
|
+
}
|
|
806
|
+
return chunks.filter((c) => c.length > 0);
|
|
807
|
+
}
|
|
413
808
|
// Annotate the CommonJS export names for ESM import in node:
|
|
414
809
|
0 && (module.exports = {
|
|
415
|
-
BrodoxCompiler
|
|
810
|
+
BrodoxCompiler,
|
|
811
|
+
PAGE_CSS_THRESHOLDS,
|
|
812
|
+
computeScopeId,
|
|
813
|
+
decidePageCssStrategy,
|
|
814
|
+
minifyScopedCss,
|
|
815
|
+
scopeCss,
|
|
816
|
+
splitCssIntoChunks
|
|
416
817
|
});
|
package/dist/index.mjs
CHANGED
|
@@ -6,7 +6,7 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
|
|
|
6
6
|
});
|
|
7
7
|
|
|
8
8
|
// src/compiler.ts
|
|
9
|
-
import * as
|
|
9
|
+
import * as esbuild2 from "esbuild";
|
|
10
10
|
import fs3 from "fs/promises";
|
|
11
11
|
import path3 from "path";
|
|
12
12
|
import os from "os";
|
|
@@ -207,6 +207,311 @@ export var ClientRender = Client;
|
|
|
207
207
|
};
|
|
208
208
|
}
|
|
209
209
|
|
|
210
|
+
// src/plugins/canvasBuildPlugin.ts
|
|
211
|
+
function canvasReactPlugin() {
|
|
212
|
+
return {
|
|
213
|
+
name: "brodox-canvas-react",
|
|
214
|
+
setup(build2) {
|
|
215
|
+
build2.onResolve({ filter: /^react\/jsx-runtime$/ }, () => ({
|
|
216
|
+
path: "react/jsx-runtime",
|
|
217
|
+
namespace: "brodox-canvas-jsx"
|
|
218
|
+
}));
|
|
219
|
+
build2.onLoad({ filter: /.*/, namespace: "brodox-canvas-jsx" }, () => ({
|
|
220
|
+
loader: "js",
|
|
221
|
+
// Use window.__brodox_jsx (ReactJSXRuntime) — the real jsx/jsxs functions
|
|
222
|
+
// behave differently from createElement for children handling.
|
|
223
|
+
contents: `
|
|
224
|
+
var _jx = window.__brodox_jsx;
|
|
225
|
+
export var jsx = _jx.jsx;
|
|
226
|
+
export var jsxs = _jx.jsxs;
|
|
227
|
+
export var Fragment = _jx.Fragment;
|
|
228
|
+
`.trim()
|
|
229
|
+
}));
|
|
230
|
+
build2.onResolve({ filter: /^react\/jsx-dev-runtime$/ }, () => ({
|
|
231
|
+
path: "react/jsx-dev-runtime",
|
|
232
|
+
namespace: "brodox-canvas-jsx-dev"
|
|
233
|
+
}));
|
|
234
|
+
build2.onLoad({ filter: /.*/, namespace: "brodox-canvas-jsx-dev" }, () => ({
|
|
235
|
+
loader: "js",
|
|
236
|
+
contents: `
|
|
237
|
+
var _jx = window.__brodox_jsx;
|
|
238
|
+
export var jsxDEV = _jx.jsx || _jx.jsxDEV;
|
|
239
|
+
export var Fragment = _jx.Fragment;
|
|
240
|
+
`.trim()
|
|
241
|
+
}));
|
|
242
|
+
build2.onResolve({ filter: /^react$/ }, () => ({
|
|
243
|
+
path: "react",
|
|
244
|
+
namespace: "brodox-canvas-react"
|
|
245
|
+
}));
|
|
246
|
+
build2.onLoad({ filter: /.*/, namespace: "brodox-canvas-react" }, () => ({
|
|
247
|
+
loader: "js",
|
|
248
|
+
contents: `
|
|
249
|
+
var _r = window.__brodox_react;
|
|
250
|
+
export default _r;
|
|
251
|
+
export var Children = _r.Children;
|
|
252
|
+
export var createElement = _r.createElement;
|
|
253
|
+
export var cloneElement = _r.cloneElement;
|
|
254
|
+
export var createContext = _r.createContext;
|
|
255
|
+
export var createRef = _r.createRef;
|
|
256
|
+
export var forwardRef = _r.forwardRef;
|
|
257
|
+
export var Fragment = _r.Fragment;
|
|
258
|
+
export var isValidElement = _r.isValidElement;
|
|
259
|
+
export var lazy = _r.lazy;
|
|
260
|
+
export var memo = _r.memo;
|
|
261
|
+
export var Suspense = _r.Suspense;
|
|
262
|
+
export var useCallback = _r.useCallback;
|
|
263
|
+
export var useContext = _r.useContext;
|
|
264
|
+
export var useDebugValue = _r.useDebugValue;
|
|
265
|
+
export var useEffect = _r.useEffect;
|
|
266
|
+
export var useId = _r.useId;
|
|
267
|
+
export var useImperativeHandle = _r.useImperativeHandle;
|
|
268
|
+
export var useLayoutEffect = _r.useLayoutEffect;
|
|
269
|
+
export var useMemo = _r.useMemo;
|
|
270
|
+
export var useReducer = _r.useReducer;
|
|
271
|
+
export var useRef = _r.useRef;
|
|
272
|
+
export var useState = _r.useState;
|
|
273
|
+
export var useSyncExternalStore= _r.useSyncExternalStore;
|
|
274
|
+
export var useTransition = _r.useTransition;
|
|
275
|
+
export var startTransition = _r.startTransition;
|
|
276
|
+
`.trim()
|
|
277
|
+
}));
|
|
278
|
+
build2.onResolve({ filter: /^react-dom(\/.*)?$/ }, () => ({
|
|
279
|
+
path: "react-dom",
|
|
280
|
+
namespace: "brodox-canvas-react-dom"
|
|
281
|
+
}));
|
|
282
|
+
build2.onLoad({ filter: /.*/, namespace: "brodox-canvas-react-dom" }, () => ({
|
|
283
|
+
loader: "js",
|
|
284
|
+
contents: `
|
|
285
|
+
var _rd = window.__brodox_react_dom;
|
|
286
|
+
export default _rd;
|
|
287
|
+
export var createPortal = _rd && _rd.createPortal;
|
|
288
|
+
export var flushSync = _rd && _rd.flushSync;
|
|
289
|
+
`.trim()
|
|
290
|
+
}));
|
|
291
|
+
}
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
function canvasRuntimePlugin() {
|
|
295
|
+
return {
|
|
296
|
+
name: "brodox-runtime-canvas-stub",
|
|
297
|
+
setup(build2) {
|
|
298
|
+
build2.onResolve({ filter: /^@broxium\/runtime$/ }, () => ({
|
|
299
|
+
path: "@broxium/runtime",
|
|
300
|
+
namespace: "brodox-runtime-canvas"
|
|
301
|
+
}));
|
|
302
|
+
build2.onLoad({ filter: /.*/, namespace: "brodox-runtime-canvas" }, () => ({
|
|
303
|
+
loader: "js",
|
|
304
|
+
contents: `
|
|
305
|
+
// brodox-runtime-canvas:@broxium/runtime
|
|
306
|
+
var React = window.__brodox_react;
|
|
307
|
+
var h = React.createElement;
|
|
308
|
+
var F = React.Fragment;
|
|
309
|
+
|
|
310
|
+
export function BrodoxHead() { return null; }
|
|
311
|
+
export function BrodoxFont() { return null; }
|
|
312
|
+
|
|
313
|
+
export function BrodoxLink({ href, children, className, style, target }) {
|
|
314
|
+
return h('a', { href, className, style, target }, children);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
export function BrodoxImage({ src, alt, width, height, fill, className, style }) {
|
|
318
|
+
var imgStyle = fill
|
|
319
|
+
? Object.assign({ width: '100%', height: '100%', objectFit: 'cover' }, style || {})
|
|
320
|
+
: (style || {});
|
|
321
|
+
return h('img', { src: src, alt: alt || '', width: fill ? undefined : width, height: fill ? undefined : height, className: className, style: imgStyle });
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
export function BrodoxRouter({ children }) { return h(F, null, children); }
|
|
325
|
+
|
|
326
|
+
export function useRouter() {
|
|
327
|
+
return { pathname: '/', params: {}, navigate: function() {}, back: function() {}, forward: function() {}, prefetch: function() {} };
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
export function useParams() { return {}; }
|
|
331
|
+
|
|
332
|
+
// In the canvas, Client and Server are both transparent: render children as-is.
|
|
333
|
+
export function Client({ children }) { return h(F, null, children); }
|
|
334
|
+
export function Server({ children }) { return h(F, null, children); }
|
|
335
|
+
export var ClientRender = Client;
|
|
336
|
+
`.trim()
|
|
337
|
+
}));
|
|
338
|
+
}
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// src/utils/scopeCss.ts
|
|
343
|
+
import * as esbuild from "esbuild";
|
|
344
|
+
function computeScopeId(slug, version) {
|
|
345
|
+
let hash = 2166136261;
|
|
346
|
+
const input = `${slug}@${version}`;
|
|
347
|
+
for (let i = 0; i < input.length; i++) {
|
|
348
|
+
hash ^= input.charCodeAt(i);
|
|
349
|
+
hash = Math.imul(hash, 16777619) >>> 0;
|
|
350
|
+
}
|
|
351
|
+
return hash.toString(36).slice(0, 6).padStart(6, "0");
|
|
352
|
+
}
|
|
353
|
+
function scopeSelectors(selectorText, scope) {
|
|
354
|
+
return selectorText.split(",").map((s) => {
|
|
355
|
+
const t = s.trim();
|
|
356
|
+
if (!t) return t;
|
|
357
|
+
if (t === ":root") return scope;
|
|
358
|
+
if (t === "html" || t === "body") return t;
|
|
359
|
+
return `${scope} ${t}`;
|
|
360
|
+
}).join(",\n");
|
|
361
|
+
}
|
|
362
|
+
function scopeCss(css, scopeId) {
|
|
363
|
+
const scope = `[data-bc=${scopeId}]`;
|
|
364
|
+
return scopeBlock(css, scope);
|
|
365
|
+
}
|
|
366
|
+
function scopeBlock(css, scope) {
|
|
367
|
+
let i = 0;
|
|
368
|
+
let out = "";
|
|
369
|
+
const len = css.length;
|
|
370
|
+
while (i < len) {
|
|
371
|
+
const wsStart = i;
|
|
372
|
+
while (i < len && /\s/.test(css[i])) i++;
|
|
373
|
+
out += css.slice(wsStart, i);
|
|
374
|
+
if (i >= len) break;
|
|
375
|
+
if (css[i] === "/" && css[i + 1] === "*") {
|
|
376
|
+
const end = css.indexOf("*/", i + 2);
|
|
377
|
+
if (end === -1) {
|
|
378
|
+
out += css.slice(i);
|
|
379
|
+
break;
|
|
380
|
+
}
|
|
381
|
+
out += css.slice(i, end + 2);
|
|
382
|
+
i = end + 2;
|
|
383
|
+
continue;
|
|
384
|
+
}
|
|
385
|
+
if (css[i] === "@") {
|
|
386
|
+
let j = i + 1;
|
|
387
|
+
while (j < len && /[a-zA-Z-]/.test(css[j])) j++;
|
|
388
|
+
const atName = css.slice(i + 1, j).toLowerCase();
|
|
389
|
+
if (atName === "charset" || atName === "import") {
|
|
390
|
+
const semi = css.indexOf(";", j);
|
|
391
|
+
if (semi === -1) {
|
|
392
|
+
out += css.slice(i);
|
|
393
|
+
i = len;
|
|
394
|
+
} else {
|
|
395
|
+
out += css.slice(i, semi + 1);
|
|
396
|
+
i = semi + 1;
|
|
397
|
+
}
|
|
398
|
+
continue;
|
|
399
|
+
}
|
|
400
|
+
if (atName === "keyframes" || atName === "-webkit-keyframes" || atName === "-moz-keyframes" || atName === "font-face") {
|
|
401
|
+
while (j < len && css[j] !== "{") j++;
|
|
402
|
+
if (j >= len) {
|
|
403
|
+
out += css.slice(i);
|
|
404
|
+
i = len;
|
|
405
|
+
continue;
|
|
406
|
+
}
|
|
407
|
+
const { block, end: blockEnd } = readBlock(css, j);
|
|
408
|
+
out += css.slice(i, j) + block;
|
|
409
|
+
i = blockEnd;
|
|
410
|
+
continue;
|
|
411
|
+
}
|
|
412
|
+
if (atName === "media" || atName === "supports" || atName === "layer" || atName === "container") {
|
|
413
|
+
while (j < len && css[j] !== "{") j++;
|
|
414
|
+
if (j >= len) {
|
|
415
|
+
out += css.slice(i);
|
|
416
|
+
i = len;
|
|
417
|
+
continue;
|
|
418
|
+
}
|
|
419
|
+
const condition = css.slice(i, j);
|
|
420
|
+
const { body, end: blockEnd } = readBlockBody(css, j);
|
|
421
|
+
const scopedBody = scopeBlock(body, scope);
|
|
422
|
+
out += `${condition}{
|
|
423
|
+
${scopedBody}}
|
|
424
|
+
`;
|
|
425
|
+
i = blockEnd;
|
|
426
|
+
continue;
|
|
427
|
+
}
|
|
428
|
+
while (j < len && css[j] !== "{" && css[j] !== ";") j++;
|
|
429
|
+
if (j >= len) {
|
|
430
|
+
out += css.slice(i);
|
|
431
|
+
i = len;
|
|
432
|
+
continue;
|
|
433
|
+
}
|
|
434
|
+
if (css[j] === ";") {
|
|
435
|
+
out += css.slice(i, j + 1);
|
|
436
|
+
i = j + 1;
|
|
437
|
+
continue;
|
|
438
|
+
}
|
|
439
|
+
const { block: unknownBlock, end: unknownEnd } = readBlock(css, j);
|
|
440
|
+
out += css.slice(i, j) + unknownBlock;
|
|
441
|
+
i = unknownEnd;
|
|
442
|
+
continue;
|
|
443
|
+
}
|
|
444
|
+
let selectorEnd = i;
|
|
445
|
+
while (selectorEnd < len) {
|
|
446
|
+
if (css[selectorEnd] === "{") break;
|
|
447
|
+
if (css[selectorEnd] === "/" && css[selectorEnd + 1] === "*") {
|
|
448
|
+
const end = css.indexOf("*/", selectorEnd + 2);
|
|
449
|
+
selectorEnd = end === -1 ? len : end + 2;
|
|
450
|
+
continue;
|
|
451
|
+
}
|
|
452
|
+
selectorEnd++;
|
|
453
|
+
}
|
|
454
|
+
if (selectorEnd >= len) {
|
|
455
|
+
out += css.slice(i);
|
|
456
|
+
break;
|
|
457
|
+
}
|
|
458
|
+
const rawSelector = css.slice(i, selectorEnd);
|
|
459
|
+
const trimmedSelector = rawSelector.trim();
|
|
460
|
+
if (!trimmedSelector) {
|
|
461
|
+
i = selectorEnd;
|
|
462
|
+
continue;
|
|
463
|
+
}
|
|
464
|
+
const cleanSelector = trimmedSelector.replace(/\/\*.*?\*\//gs, "").trim();
|
|
465
|
+
const scoped = scopeSelectors(cleanSelector, scope);
|
|
466
|
+
const { body: declBody, end: declEnd } = readBlockBody(css, selectorEnd);
|
|
467
|
+
out += `${scoped} {
|
|
468
|
+
${declBody}}
|
|
469
|
+
`;
|
|
470
|
+
i = declEnd;
|
|
471
|
+
}
|
|
472
|
+
return out;
|
|
473
|
+
}
|
|
474
|
+
function readBlock(css, openBrace) {
|
|
475
|
+
let depth = 0;
|
|
476
|
+
let i = openBrace;
|
|
477
|
+
const len = css.length;
|
|
478
|
+
while (i < len) {
|
|
479
|
+
if (css[i] === "{") depth++;
|
|
480
|
+
else if (css[i] === "}") {
|
|
481
|
+
depth--;
|
|
482
|
+
if (depth === 0) {
|
|
483
|
+
return { block: css.slice(openBrace, i + 1), end: i + 1 };
|
|
484
|
+
}
|
|
485
|
+
} else if (css[i] === "/" && css[i + 1] === "*") {
|
|
486
|
+
const end = css.indexOf("*/", i + 2);
|
|
487
|
+
if (end === -1) {
|
|
488
|
+
i = len;
|
|
489
|
+
break;
|
|
490
|
+
}
|
|
491
|
+
i = end + 1;
|
|
492
|
+
}
|
|
493
|
+
i++;
|
|
494
|
+
}
|
|
495
|
+
return { block: css.slice(openBrace), end: len };
|
|
496
|
+
}
|
|
497
|
+
function readBlockBody(css, openBrace) {
|
|
498
|
+
const { block, end } = readBlock(css, openBrace);
|
|
499
|
+
const body = block.slice(1, -1);
|
|
500
|
+
return { body, end };
|
|
501
|
+
}
|
|
502
|
+
async function minifyScopedCss(css, scopeId) {
|
|
503
|
+
const scoped = scopeCss(css, scopeId);
|
|
504
|
+
try {
|
|
505
|
+
const result = await esbuild.transform(scoped, {
|
|
506
|
+
loader: "css",
|
|
507
|
+
minify: true
|
|
508
|
+
});
|
|
509
|
+
return result.code;
|
|
510
|
+
} catch {
|
|
511
|
+
return scoped;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
210
515
|
// src/compiler.ts
|
|
211
516
|
var ENTRY_PRIORITY = [
|
|
212
517
|
"App.tsx",
|
|
@@ -253,6 +558,21 @@ function findReactNodeModules() {
|
|
|
253
558
|
}
|
|
254
559
|
return [];
|
|
255
560
|
}
|
|
561
|
+
function cssTextPlugin(scopeId) {
|
|
562
|
+
return {
|
|
563
|
+
name: "brodox-css-text",
|
|
564
|
+
setup(build2) {
|
|
565
|
+
build2.onLoad({ filter: /\.css$/ }, async (args) => {
|
|
566
|
+
const content = await fs3.readFile(args.path, "utf8");
|
|
567
|
+
const minified = await minifyScopedCss(content, scopeId);
|
|
568
|
+
return {
|
|
569
|
+
contents: `module.exports = ${JSON.stringify(minified)};`,
|
|
570
|
+
loader: "js"
|
|
571
|
+
};
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
};
|
|
575
|
+
}
|
|
256
576
|
var BrodoxCompiler = class {
|
|
257
577
|
async compile(input) {
|
|
258
578
|
const tmpDir = path3.join(os.tmpdir(), `brodox-compile-${input.slug}-${randomUUID()}`);
|
|
@@ -275,14 +595,17 @@ var BrodoxCompiler = class {
|
|
|
275
595
|
if (!entryPoint) {
|
|
276
596
|
throw new Error(`No entry file found in component files for ${input.slug}`);
|
|
277
597
|
}
|
|
598
|
+
const scopeId = computeScopeId(input.slug, input.version);
|
|
278
599
|
const safeName = `${input.slug}-v${input.version}`;
|
|
279
600
|
const serverJsName = `${safeName}.server.esm.js`;
|
|
280
601
|
const clientJsName = `${safeName}.client.esm.js`;
|
|
602
|
+
const browserJsName = `${safeName}.browser.js`;
|
|
281
603
|
const serverJsPath = path3.join(input.outputDir, serverJsName);
|
|
282
604
|
const clientJsPath = path3.join(input.outputDir, clientJsName);
|
|
605
|
+
const browserJsPath = path3.join(input.outputDir, browserJsName);
|
|
283
606
|
await fs3.mkdir(input.outputDir, { recursive: true });
|
|
284
607
|
const serverNodePaths = [...input.nodePaths ?? [], ...findReactNodeModules()];
|
|
285
|
-
await
|
|
608
|
+
await esbuild2.build({
|
|
286
609
|
entryPoints: [entryPoint],
|
|
287
610
|
bundle: true,
|
|
288
611
|
format: "esm",
|
|
@@ -292,13 +615,11 @@ var BrodoxCompiler = class {
|
|
|
292
615
|
nodePaths: serverNodePaths,
|
|
293
616
|
external: [],
|
|
294
617
|
// no externals — fully self-contained
|
|
295
|
-
plugins: [clientStubPlugin(), runtimeServerStubPlugin(serverNodePaths)],
|
|
618
|
+
plugins: [clientStubPlugin(), runtimeServerStubPlugin(serverNodePaths), cssTextPlugin(scopeId)],
|
|
296
619
|
outfile: serverJsPath,
|
|
297
620
|
minify: false,
|
|
298
621
|
sourcemap: false,
|
|
299
|
-
define: { "process.env.NODE_ENV": '"production"' }
|
|
300
|
-
loader: { ".css": "text" }
|
|
301
|
-
// CSS imports return empty string in server bundle
|
|
622
|
+
define: { "process.env.NODE_ENV": '"production"' }
|
|
302
623
|
});
|
|
303
624
|
const clientComponents = [];
|
|
304
625
|
for (const file of input.files) {
|
|
@@ -323,7 +644,7 @@ ${registryEntries}
|
|
|
323
644
|
await fs3.writeFile(registryEntryPath, registryWrapper, "utf8");
|
|
324
645
|
clientEntryPoint = registryEntryPath;
|
|
325
646
|
}
|
|
326
|
-
await
|
|
647
|
+
await esbuild2.build({
|
|
327
648
|
entryPoints: [clientEntryPoint],
|
|
328
649
|
bundle: true,
|
|
329
650
|
format: "esm",
|
|
@@ -331,21 +652,35 @@ ${registryEntries}
|
|
|
331
652
|
target: ["es2020", "chrome90", "firefox88", "safari14"],
|
|
332
653
|
jsx: "automatic",
|
|
333
654
|
external: CLIENT_EXTERNALS,
|
|
334
|
-
plugins: [serverStubPlugin()],
|
|
655
|
+
plugins: [serverStubPlugin(), cssTextPlugin(scopeId)],
|
|
335
656
|
outfile: clientJsPath,
|
|
336
657
|
minify: true,
|
|
337
658
|
sourcemap: false,
|
|
338
659
|
define: { "process.env.NODE_ENV": '"production"' },
|
|
339
|
-
banner: { js: 'import React from "react";' }
|
|
340
|
-
|
|
341
|
-
|
|
660
|
+
banner: { js: 'import React from "react";' }
|
|
661
|
+
});
|
|
662
|
+
await esbuild2.build({
|
|
663
|
+
entryPoints: [clientEntryPoint],
|
|
664
|
+
bundle: true,
|
|
665
|
+
format: "esm",
|
|
666
|
+
platform: "browser",
|
|
667
|
+
target: ["es2020", "chrome90", "firefox88", "safari14"],
|
|
668
|
+
jsx: "automatic",
|
|
669
|
+
external: [],
|
|
670
|
+
// no externals — everything inlined or replaced by plugins
|
|
671
|
+
plugins: [canvasReactPlugin(), canvasRuntimePlugin(), cssTextPlugin(scopeId)],
|
|
672
|
+
outfile: browserJsPath,
|
|
673
|
+
minify: true,
|
|
674
|
+
sourcemap: false,
|
|
675
|
+
define: { "process.env.NODE_ENV": '"production"' }
|
|
676
|
+
// no banner — canvasReactPlugin handles the React reference via window global
|
|
342
677
|
});
|
|
343
678
|
const hasCss = input.files.some((f) => /\.css$/.test(f.path));
|
|
344
679
|
const cssName = hasCss ? `${safeName}.css` : null;
|
|
345
680
|
const cssPath = hasCss ? path3.join(input.outputDir, cssName) : null;
|
|
346
681
|
if (hasCss && cssPath) {
|
|
347
682
|
try {
|
|
348
|
-
await
|
|
683
|
+
await esbuild2.build({
|
|
349
684
|
entryPoints: [entryPoint],
|
|
350
685
|
bundle: true,
|
|
351
686
|
format: "esm",
|
|
@@ -355,7 +690,8 @@ ${registryEntries}
|
|
|
355
690
|
plugins: [serverStubPlugin()],
|
|
356
691
|
outfile: cssPath.replace(/\.css$/, ".css.tmp.js"),
|
|
357
692
|
// esbuild needs a JS outfile
|
|
358
|
-
minify:
|
|
693
|
+
minify: false,
|
|
694
|
+
// we'll minify after scoping
|
|
359
695
|
sourcemap: false,
|
|
360
696
|
define: { "process.env.NODE_ENV": '"production"' },
|
|
361
697
|
loader: { ".css": "css" }
|
|
@@ -366,6 +702,12 @@ ${registryEntries}
|
|
|
366
702
|
} catch {
|
|
367
703
|
}
|
|
368
704
|
await fs3.rm(cssPath.replace(/\.css$/, ".css.tmp.js"), { force: true });
|
|
705
|
+
try {
|
|
706
|
+
const rawCss = await fs3.readFile(cssPath, "utf8");
|
|
707
|
+
const minifiedScoped = await minifyScopedCss(rawCss, scopeId);
|
|
708
|
+
await fs3.writeFile(cssPath, minifiedScoped, "utf8");
|
|
709
|
+
} catch {
|
|
710
|
+
}
|
|
369
711
|
} catch {
|
|
370
712
|
}
|
|
371
713
|
}
|
|
@@ -373,14 +715,67 @@ ${registryEntries}
|
|
|
373
715
|
return {
|
|
374
716
|
serverJsPath,
|
|
375
717
|
clientJsPath,
|
|
718
|
+
browserJsPath,
|
|
376
719
|
cssPath,
|
|
377
720
|
serverJsName,
|
|
378
721
|
clientJsName,
|
|
722
|
+
browserJsName,
|
|
379
723
|
cssName,
|
|
724
|
+
scopeId,
|
|
380
725
|
compiledAt: /* @__PURE__ */ new Date()
|
|
381
726
|
};
|
|
382
727
|
}
|
|
383
728
|
};
|
|
729
|
+
|
|
730
|
+
// src/utils/cssStrategy.ts
|
|
731
|
+
var PAGE_CSS_THRESHOLDS = {
|
|
732
|
+
/** Embed as <style> when total page CSS ≤ this value (bytes). */
|
|
733
|
+
INLINE_MAX_BYTES: 4096,
|
|
734
|
+
/** Switch to multi-chunk delivery when total page CSS > this value (bytes). */
|
|
735
|
+
CHUNK_MIN_BYTES: 51200,
|
|
736
|
+
/** Maximum bytes per CSS chunk file. */
|
|
737
|
+
CHUNK_SIZE_BYTES: 25600
|
|
738
|
+
};
|
|
739
|
+
function decidePageCssStrategy(mergedCss) {
|
|
740
|
+
const size = Buffer.byteLength(mergedCss, "utf8");
|
|
741
|
+
if (size === 0) return { strategy: "none" };
|
|
742
|
+
if (size <= PAGE_CSS_THRESHOLDS.INLINE_MAX_BYTES) {
|
|
743
|
+
return { strategy: "inline", inlineCss: mergedCss };
|
|
744
|
+
}
|
|
745
|
+
if (size > PAGE_CSS_THRESHOLDS.CHUNK_MIN_BYTES) {
|
|
746
|
+
return {
|
|
747
|
+
strategy: "chunks",
|
|
748
|
+
cssChunks: splitCssIntoChunks(mergedCss, PAGE_CSS_THRESHOLDS.CHUNK_SIZE_BYTES)
|
|
749
|
+
};
|
|
750
|
+
}
|
|
751
|
+
return { strategy: "separate", cssChunks: [mergedCss] };
|
|
752
|
+
}
|
|
753
|
+
function splitCssIntoChunks(css, chunkSize) {
|
|
754
|
+
const chunks = [];
|
|
755
|
+
let remaining = css.trim();
|
|
756
|
+
while (remaining.length > 0) {
|
|
757
|
+
const buf = Buffer.from(remaining, "utf8");
|
|
758
|
+
if (buf.byteLength <= chunkSize) {
|
|
759
|
+
chunks.push(remaining);
|
|
760
|
+
break;
|
|
761
|
+
}
|
|
762
|
+
const candidate = buf.slice(0, chunkSize).toString("utf8");
|
|
763
|
+
const lastBrace = candidate.lastIndexOf("}");
|
|
764
|
+
if (lastBrace < 0) {
|
|
765
|
+
chunks.push(remaining);
|
|
766
|
+
break;
|
|
767
|
+
}
|
|
768
|
+
chunks.push(remaining.slice(0, lastBrace + 1));
|
|
769
|
+
remaining = remaining.slice(lastBrace + 1).trim();
|
|
770
|
+
}
|
|
771
|
+
return chunks.filter((c) => c.length > 0);
|
|
772
|
+
}
|
|
384
773
|
export {
|
|
385
|
-
BrodoxCompiler
|
|
774
|
+
BrodoxCompiler,
|
|
775
|
+
PAGE_CSS_THRESHOLDS,
|
|
776
|
+
computeScopeId,
|
|
777
|
+
decidePageCssStrategy,
|
|
778
|
+
minifyScopedCss,
|
|
779
|
+
scopeCss,
|
|
780
|
+
splitCssIntoChunks
|
|
386
781
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@broxium/compiler",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.2",
|
|
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",
|
|
@@ -18,9 +18,9 @@
|
|
|
18
18
|
"esbuild": "^0.25.0"
|
|
19
19
|
},
|
|
20
20
|
"devDependencies": {
|
|
21
|
+
"@types/node": "^20.19.42",
|
|
21
22
|
"tsup": "^8.0.0",
|
|
22
|
-
"typescript": "^5.0.0"
|
|
23
|
-
"@types/node": "^20.0.0"
|
|
23
|
+
"typescript": "^5.0.0"
|
|
24
24
|
},
|
|
25
25
|
"scripts": {
|
|
26
26
|
"build": "tsup src/index.ts --format esm,cjs --dts",
|