hadars 0.1.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/LICENSE +21 -0
- package/README.md +118 -0
- package/cli-bun.ts +13 -0
- package/cli-lib.ts +203 -0
- package/cli.ts +13 -0
- package/dist/cli.js +1441 -0
- package/dist/index.cjs +303 -0
- package/dist/index.d.ts +160 -0
- package/dist/index.js +263 -0
- package/dist/loader.cjs +34 -0
- package/dist/ssr-render-worker.js +92 -0
- package/dist/ssr-watch.js +345 -0
- package/dist/template.html +11 -0
- package/dist/utils/clientScript.tsx +58 -0
- package/index.ts +15 -0
- package/package.json +99 -0
- package/src/build.ts +716 -0
- package/src/index.tsx +41 -0
- package/src/ssr-render-worker.ts +138 -0
- package/src/ssr-watch.ts +56 -0
- package/src/types/global.d.ts +5 -0
- package/src/types/ninety.ts +116 -0
- package/src/utils/Head.tsx +357 -0
- package/src/utils/clientScript.tsx +58 -0
- package/src/utils/cookies.ts +16 -0
- package/src/utils/loadModule.ts +4 -0
- package/src/utils/loader.ts +41 -0
- package/src/utils/proxyHandler.tsx +101 -0
- package/src/utils/request.tsx +9 -0
- package/src/utils/response.tsx +198 -0
- package/src/utils/rspack.ts +359 -0
- package/src/utils/runtime.ts +19 -0
- package/src/utils/serve.ts +140 -0
- package/src/utils/staticFile.ts +48 -0
- package/src/utils/template.html +11 -0
- package/src/utils/upgradeRequest.tsx +19 -0
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import type React from "react";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import pathMod from "node:path";
|
|
4
|
+
import { pathToFileURL } from "node:url";
|
|
5
|
+
import type { AppHead, AppUnsuspend, HadarsRequest, HadarsEntryBase, HadarsEntryModule, HadarsProps, AppContext } from "../types/ninety";
|
|
6
|
+
|
|
7
|
+
// Resolve react-dom/server from the *project's* node_modules (process.cwd()) so
|
|
8
|
+
// the same React instance is used here as in the SSR bundle. Without this,
|
|
9
|
+
// when hadars is installed as a file: symlink the renderer ends up on a
|
|
10
|
+
// different React than the component, breaking hook calls.
|
|
11
|
+
let _renderToStaticMarkup: ((element: any) => string) | null = null;
|
|
12
|
+
async function getStaticMarkupRenderer(): Promise<(element: any) => string> {
|
|
13
|
+
if (!_renderToStaticMarkup) {
|
|
14
|
+
const req = createRequire(pathMod.resolve(process.cwd(), '__hadars_fake__.js'));
|
|
15
|
+
const resolved = req.resolve('react-dom/server');
|
|
16
|
+
const mod = await import(pathToFileURL(resolved).href);
|
|
17
|
+
_renderToStaticMarkup = mod.renderToStaticMarkup;
|
|
18
|
+
}
|
|
19
|
+
return _renderToStaticMarkup!;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface ReactResponseOptions {
|
|
23
|
+
document: {
|
|
24
|
+
body: React.FC<HadarsProps<object>>;
|
|
25
|
+
head?: () => Promise<React.ReactNode>;
|
|
26
|
+
lang?: string;
|
|
27
|
+
getInitProps: HadarsEntryModule<HadarsEntryBase>['getInitProps'];
|
|
28
|
+
getAfterRenderProps: HadarsEntryModule<HadarsEntryBase>['getAfterRenderProps'];
|
|
29
|
+
getFinalProps: HadarsEntryModule<HadarsEntryBase>['getFinalProps'];
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const getHeadHtml = (seoData: AppHead, renderToStaticMarkup: (el: any) => string): string => {
|
|
34
|
+
const metaEntries = Object.entries(seoData.meta)
|
|
35
|
+
const linkEntries = Object.entries(seoData.link)
|
|
36
|
+
const styleEntries = Object.entries(seoData.style)
|
|
37
|
+
const scriptEntries = Object.entries(seoData.script)
|
|
38
|
+
|
|
39
|
+
return renderToStaticMarkup(
|
|
40
|
+
<>
|
|
41
|
+
<title>{seoData.title}</title>
|
|
42
|
+
{
|
|
43
|
+
metaEntries.map( ([id, options]) => (
|
|
44
|
+
<meta
|
|
45
|
+
key={id}
|
|
46
|
+
id={ id }
|
|
47
|
+
{ ...options }
|
|
48
|
+
/>
|
|
49
|
+
) )
|
|
50
|
+
}
|
|
51
|
+
{linkEntries.map( ([id, options]) => (
|
|
52
|
+
<link
|
|
53
|
+
key={id}
|
|
54
|
+
id={ id }
|
|
55
|
+
{ ...options }
|
|
56
|
+
/>
|
|
57
|
+
))}
|
|
58
|
+
{styleEntries.map( ([id, options]) => (
|
|
59
|
+
<style
|
|
60
|
+
key={id}
|
|
61
|
+
id={id}
|
|
62
|
+
{ ...options }
|
|
63
|
+
/>
|
|
64
|
+
))}
|
|
65
|
+
{scriptEntries.map( ([id, options]) => (
|
|
66
|
+
<script
|
|
67
|
+
key={id}
|
|
68
|
+
id={id}
|
|
69
|
+
{ ...options }
|
|
70
|
+
/>
|
|
71
|
+
))}
|
|
72
|
+
</>
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
export const getReactResponse = async (
|
|
78
|
+
req: HadarsRequest,
|
|
79
|
+
opts: ReactResponseOptions,
|
|
80
|
+
): Promise<{
|
|
81
|
+
ReactPage: React.ReactElement,
|
|
82
|
+
status: number,
|
|
83
|
+
headHtml: string,
|
|
84
|
+
renderPayload: {
|
|
85
|
+
appProps: Record<string, unknown>;
|
|
86
|
+
clientProps: Record<string, unknown>;
|
|
87
|
+
};
|
|
88
|
+
}> => {
|
|
89
|
+
const App = opts.document.body
|
|
90
|
+
const { getInitProps, getAfterRenderProps, getFinalProps } = opts.document;
|
|
91
|
+
|
|
92
|
+
const renderToStaticMarkup = await getStaticMarkupRenderer();
|
|
93
|
+
|
|
94
|
+
// Per-request unsuspend context — populated by useServerData() hooks during render.
|
|
95
|
+
// Lifecycle passes always run on the main thread so the cache is directly accessible.
|
|
96
|
+
// Kept as a plain AppUnsuspend (no methods) so it is serializable via structuredClone
|
|
97
|
+
// for postMessage to worker threads.
|
|
98
|
+
const unsuspend: AppUnsuspend = {
|
|
99
|
+
cache: new Map(),
|
|
100
|
+
hasPending: false,
|
|
101
|
+
};
|
|
102
|
+
const processUnsuspend = async () => {
|
|
103
|
+
const pending = [...unsuspend.cache.values()]
|
|
104
|
+
.filter((e): e is { status: 'pending'; promise: Promise<unknown> } => e.status === 'pending')
|
|
105
|
+
.map(e => e.promise);
|
|
106
|
+
await Promise.all(pending);
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const context: AppContext = {
|
|
110
|
+
head: {
|
|
111
|
+
title: "Hadars App",
|
|
112
|
+
meta: {},
|
|
113
|
+
link: {},
|
|
114
|
+
style: {},
|
|
115
|
+
script: {},
|
|
116
|
+
status: 200,
|
|
117
|
+
},
|
|
118
|
+
_unsuspend: unsuspend,
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
let props: HadarsEntryBase = {
|
|
122
|
+
...(getInitProps ? await getInitProps(req) : {}),
|
|
123
|
+
location: req.location,
|
|
124
|
+
context,
|
|
125
|
+
} as HadarsEntryBase
|
|
126
|
+
|
|
127
|
+
// ── First lifecycle pass: useServerData render loop ───────────────────────
|
|
128
|
+
// Render the component repeatedly until all useServerData() promises have
|
|
129
|
+
// resolved. Each iteration discovers new pending promises; awaiting them
|
|
130
|
+
// before the next pass ensures every hook returns its value by the final
|
|
131
|
+
// iteration. Capped at 25 iterations as a safety guard against infinite loops.
|
|
132
|
+
let html = '';
|
|
133
|
+
let iters = 0;
|
|
134
|
+
do {
|
|
135
|
+
unsuspend.hasPending = false;
|
|
136
|
+
try {
|
|
137
|
+
(globalThis as any).__hadarsUnsuspend = unsuspend;
|
|
138
|
+
html = renderToStaticMarkup(<App {...(props as any)} />);
|
|
139
|
+
} finally {
|
|
140
|
+
(globalThis as any).__hadarsUnsuspend = null;
|
|
141
|
+
}
|
|
142
|
+
if (unsuspend.hasPending) await processUnsuspend();
|
|
143
|
+
} while (unsuspend.hasPending && ++iters < 25);
|
|
144
|
+
|
|
145
|
+
props = getAfterRenderProps ? await getAfterRenderProps(props, html) : props;
|
|
146
|
+
// Re-render to capture any head changes introduced by getAfterRenderProps.
|
|
147
|
+
try {
|
|
148
|
+
(globalThis as any).__hadarsUnsuspend = unsuspend;
|
|
149
|
+
renderToStaticMarkup(
|
|
150
|
+
<App {...({
|
|
151
|
+
...props,
|
|
152
|
+
location: req.location,
|
|
153
|
+
context,
|
|
154
|
+
})} />
|
|
155
|
+
);
|
|
156
|
+
} finally {
|
|
157
|
+
(globalThis as any).__hadarsUnsuspend = null;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Serialize resolved useServerData() values for client hydration.
|
|
161
|
+
// The client bootstrap reads __serverData and pre-populates the hook cache
|
|
162
|
+
// before hydrateRoot so that useServerData() returns the same values CSR.
|
|
163
|
+
const serverData: Record<string, unknown> = {};
|
|
164
|
+
for (const [k, v] of unsuspend.cache) {
|
|
165
|
+
if (v.status === 'fulfilled') serverData[k] = v.value;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const { context: _, ...restProps } = getFinalProps ? await getFinalProps(props) : props;
|
|
169
|
+
const clientProps = {
|
|
170
|
+
...restProps,
|
|
171
|
+
location: req.location,
|
|
172
|
+
...(Object.keys(serverData).length > 0 ? { __serverData: serverData } : {}),
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const ReactPage = (
|
|
176
|
+
<>
|
|
177
|
+
<div id="app">
|
|
178
|
+
<App {...({
|
|
179
|
+
...props,
|
|
180
|
+
location: req.location,
|
|
181
|
+
context,
|
|
182
|
+
})} />
|
|
183
|
+
</div>
|
|
184
|
+
<script id="hadars" type="application/json" dangerouslySetInnerHTML={{ __html: JSON.stringify({ hadars: { props: clientProps } }) }}></script>
|
|
185
|
+
</>
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
return {
|
|
189
|
+
ReactPage,
|
|
190
|
+
status: context.head.status,
|
|
191
|
+
headHtml: getHeadHtml(context.head, renderToStaticMarkup),
|
|
192
|
+
renderPayload: {
|
|
193
|
+
appProps: { ...props, location: req.location, context } as Record<string, unknown>,
|
|
194
|
+
clientProps: clientProps as Record<string, unknown>,
|
|
195
|
+
},
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
}
|
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
import rspack from "@rspack/core";
|
|
2
|
+
import type { Configuration, RuleSetLoaderWithOptions, RuleSetRule } from "@rspack/core";
|
|
3
|
+
import ReactRefreshPlugin from '@rspack/plugin-react-refresh';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import type { SwcPluginList } from '../types/ninety';
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import pathMod from "node:path";
|
|
8
|
+
import { existsSync } from "node:fs";
|
|
9
|
+
|
|
10
|
+
const __dirname = process.cwd();
|
|
11
|
+
const packageDir = pathMod.dirname(fileURLToPath(import.meta.url));
|
|
12
|
+
const clientScriptPath = pathMod.resolve(packageDir, 'template.html');
|
|
13
|
+
|
|
14
|
+
// When running from compiled dist/cli.js the loader is pre-built as loader.cjs.
|
|
15
|
+
// The .cjs extension forces CommonJS regardless of the package "type": "module".
|
|
16
|
+
// When running from source (bun/tsx) it falls back to loader.ts.
|
|
17
|
+
const loaderPath = existsSync(pathMod.resolve(packageDir, 'loader.cjs'))
|
|
18
|
+
? pathMod.resolve(packageDir, 'loader.cjs')
|
|
19
|
+
: pathMod.resolve(packageDir, 'loader.ts');
|
|
20
|
+
|
|
21
|
+
const getConfigBase = (mode: "development" | "production"): Omit<Configuration, "entry" | "output" | "plugins"> => {
|
|
22
|
+
const isDev = mode === 'development';
|
|
23
|
+
return {
|
|
24
|
+
experiments: {
|
|
25
|
+
css: true,
|
|
26
|
+
outputModule: true,
|
|
27
|
+
},
|
|
28
|
+
resolve: {
|
|
29
|
+
modules: [
|
|
30
|
+
path.resolve(__dirname, 'node_modules'),
|
|
31
|
+
// 'node_modules' (relative) enables the standard upward-traversal
|
|
32
|
+
// resolution so rspack can find transitive deps (e.g. webpack-dev-server)
|
|
33
|
+
// that live in a parent node_modules when running from a sub-project.
|
|
34
|
+
'node_modules',
|
|
35
|
+
],
|
|
36
|
+
tsConfig: path.resolve(__dirname, 'tsconfig.json'),
|
|
37
|
+
extensions: ['.tsx', '.ts', '.js', '.jsx'],
|
|
38
|
+
},
|
|
39
|
+
module: {
|
|
40
|
+
rules: [
|
|
41
|
+
{
|
|
42
|
+
test: /\.mdx?$/,
|
|
43
|
+
use: [
|
|
44
|
+
{
|
|
45
|
+
loader: '@mdx-js/loader',
|
|
46
|
+
options: {
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
],
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
test: /\.css$/,
|
|
53
|
+
use: ["postcss-loader"],
|
|
54
|
+
type: "css",
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
test: /\.svg$/i,
|
|
58
|
+
issuer: /\.[jt]sx?$/,
|
|
59
|
+
use: ['@svgr/webpack'],
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
test: /\.m?jsx?$/,
|
|
63
|
+
resolve: {
|
|
64
|
+
fullySpecified: false,
|
|
65
|
+
},
|
|
66
|
+
exclude: [loaderPath],
|
|
67
|
+
use: [
|
|
68
|
+
// Transforms loadModule('./path') based on build target.
|
|
69
|
+
// Runs before swc-loader (loaders execute right-to-left).
|
|
70
|
+
{
|
|
71
|
+
loader: loaderPath,
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
loader: 'builtin:swc-loader',
|
|
75
|
+
options: {
|
|
76
|
+
jsc: {
|
|
77
|
+
parser: {
|
|
78
|
+
syntax: 'ecmascript',
|
|
79
|
+
jsx: true,
|
|
80
|
+
},
|
|
81
|
+
transform: {
|
|
82
|
+
react: {
|
|
83
|
+
runtime: "automatic",
|
|
84
|
+
development: isDev,
|
|
85
|
+
refresh: isDev,
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
],
|
|
92
|
+
type: 'javascript/auto',
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
test: /\.tsx?$/,
|
|
96
|
+
resolve: {
|
|
97
|
+
fullySpecified: false,
|
|
98
|
+
},
|
|
99
|
+
exclude: [loaderPath],
|
|
100
|
+
use: [
|
|
101
|
+
{
|
|
102
|
+
loader: loaderPath,
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
loader: 'builtin:swc-loader',
|
|
106
|
+
options: {
|
|
107
|
+
jsc: {
|
|
108
|
+
parser: {
|
|
109
|
+
syntax: 'typescript',
|
|
110
|
+
tsx: true,
|
|
111
|
+
},
|
|
112
|
+
transform: {
|
|
113
|
+
react: {
|
|
114
|
+
runtime: "automatic",
|
|
115
|
+
development: isDev,
|
|
116
|
+
refresh: isDev,
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
],
|
|
123
|
+
type: 'javascript/auto',
|
|
124
|
+
},
|
|
125
|
+
],
|
|
126
|
+
},
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
type EntryOutput = Configuration["output"];
|
|
131
|
+
|
|
132
|
+
interface EntryOptions {
|
|
133
|
+
target: Configuration["target"],
|
|
134
|
+
output: EntryOutput,
|
|
135
|
+
mode: "development" | "production",
|
|
136
|
+
// optional swc plugins to pass to swc-loader
|
|
137
|
+
swcPlugins?: SwcPluginList,
|
|
138
|
+
// optional compile-time defines (e.g. { 'process.env.NODE_ENV': '"development"' })
|
|
139
|
+
define?: Record<string, string>;
|
|
140
|
+
base?: string;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const buildCompilerConfig = (
|
|
144
|
+
entry: string,
|
|
145
|
+
opts: EntryOptions,
|
|
146
|
+
includeHotPlugin: boolean,
|
|
147
|
+
): Configuration => {
|
|
148
|
+
const Config = getConfigBase(opts.mode);
|
|
149
|
+
const { base } = opts;
|
|
150
|
+
const isDev = opts.mode === 'development';
|
|
151
|
+
|
|
152
|
+
// shallow-clone base config to avoid mutating shared Config while preserving RegExp and plugin instances
|
|
153
|
+
const localConfig: any = {
|
|
154
|
+
...Config,
|
|
155
|
+
module: {
|
|
156
|
+
...Config.module,
|
|
157
|
+
rules: (Config.module && Array.isArray(Config.module.rules) ? Config.module.rules : []).map((r: any) => {
|
|
158
|
+
// shallow copy each rule and its 'use' array/entries so we can mutate safely
|
|
159
|
+
const nr: any = { ...r };
|
|
160
|
+
if (r && Array.isArray(r.use)) {
|
|
161
|
+
nr.use = r.use.map((u: any) => ({ ...(typeof u === 'object' ? u : { loader: u }) }));
|
|
162
|
+
}
|
|
163
|
+
return nr;
|
|
164
|
+
}),
|
|
165
|
+
},
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
// if swc plugins are provided, inject them into swc-loader options for js/jsx and ts/tsx rules
|
|
169
|
+
if (opts.swcPlugins && Array.isArray(opts.swcPlugins) && opts.swcPlugins.length > 0) {
|
|
170
|
+
const rules = localConfig.module && localConfig.module.rules;
|
|
171
|
+
if (Array.isArray(rules)) {
|
|
172
|
+
for (const rule of rules) {
|
|
173
|
+
const ruleUse = rule as RuleSetRule;
|
|
174
|
+
if (ruleUse.use && Array.isArray(ruleUse.use)) {
|
|
175
|
+
for (const entry of ruleUse.use ) {
|
|
176
|
+
const useEntry = entry as RuleSetLoaderWithOptions;
|
|
177
|
+
if (useEntry && useEntry.loader && typeof useEntry.loader === 'string' && useEntry.loader.includes('swc-loader')) {
|
|
178
|
+
const options = ( useEntry.options || {} ) as Record<string, any>;
|
|
179
|
+
useEntry.options = options;
|
|
180
|
+
useEntry.options.jsc = useEntry.options.jsc || {};
|
|
181
|
+
useEntry.options.jsc.experimental = useEntry.options.jsc.experimental || {};
|
|
182
|
+
// ensure plugins run before other transforms (important for Relay plugin)
|
|
183
|
+
useEntry.options.jsc.experimental.runPluginFirst = true;
|
|
184
|
+
// existing plugins may be present under jsc.experimental.plugins; merge them with provided ones
|
|
185
|
+
const existingPlugins = Array.isArray(useEntry.options.jsc.experimental.plugins) ? useEntry.options.jsc.experimental.plugins : [];
|
|
186
|
+
const incomingPlugins = Array.isArray(opts.swcPlugins) ? opts.swcPlugins : [];
|
|
187
|
+
// simple dedupe by plugin name (first element of tuple) to avoid duplicates
|
|
188
|
+
const seen = new Set<string>();
|
|
189
|
+
const merged: any[] = [];
|
|
190
|
+
for (const p of existingPlugins.concat(incomingPlugins)) {
|
|
191
|
+
// plugin can be [name, options] or string; normalize
|
|
192
|
+
const name = Array.isArray(p) && p.length > 0 ? String(p[0]) : String(p);
|
|
193
|
+
if (!seen.has(name)) {
|
|
194
|
+
seen.add(name);
|
|
195
|
+
merged.push(p);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
useEntry.options.jsc.experimental.plugins = merged;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// For server (SSR) builds we should avoid bundling react/react-dom so
|
|
207
|
+
// the runtime uses the same React instance as the host. If the output
|
|
208
|
+
// is a library/module (i.e. `opts.output.library` present or filename
|
|
209
|
+
// contains "ssr"), treat it as a server build and mark react/react-dom
|
|
210
|
+
// as externals and alias React imports to the project's node_modules.
|
|
211
|
+
const isServerBuild = Boolean(
|
|
212
|
+
(opts.output && typeof opts.output === 'object' && (opts.output.library || String(opts.output.filename || '').includes('ssr')))
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
const resolveAliases: Record<string, string> | undefined = isServerBuild ? {
|
|
216
|
+
// force all react imports to resolve to this project's react
|
|
217
|
+
react: path.resolve(process.cwd(), 'node_modules', 'react'),
|
|
218
|
+
'react-dom': path.resolve(process.cwd(), 'node_modules', 'react-dom'),
|
|
219
|
+
// also map react/jsx-runtime to avoid duplicates when automatic runtime is used
|
|
220
|
+
'react/jsx-runtime': path.resolve(process.cwd(), 'node_modules', 'react', 'jsx-runtime.js'),
|
|
221
|
+
'react/jsx-dev-runtime': path.resolve(process.cwd(), 'node_modules', 'react', 'jsx-dev-runtime.js'),
|
|
222
|
+
// ensure emotion packages resolve to the project's node_modules so we don't pick up a browser-specific entry
|
|
223
|
+
'@emotion/react': path.resolve(process.cwd(), 'node_modules', '@emotion', 'react'),
|
|
224
|
+
'@emotion/server': path.resolve(process.cwd(), 'node_modules', '@emotion', 'server'),
|
|
225
|
+
'@emotion/cache': path.resolve(process.cwd(), 'node_modules', '@emotion', 'cache'),
|
|
226
|
+
'@emotion/styled': path.resolve(process.cwd(), 'node_modules', '@emotion', 'styled'),
|
|
227
|
+
} : undefined;
|
|
228
|
+
|
|
229
|
+
const externals = isServerBuild ? [
|
|
230
|
+
'react',
|
|
231
|
+
'react-dom',
|
|
232
|
+
// keep common aliases external as well
|
|
233
|
+
'react/jsx-runtime',
|
|
234
|
+
'react/jsx-dev-runtime',
|
|
235
|
+
// emotion should be external on server builds to avoid client/browser code
|
|
236
|
+
'@emotion/react',
|
|
237
|
+
'@emotion/server',
|
|
238
|
+
'@emotion/cache',
|
|
239
|
+
'@emotion/styled',
|
|
240
|
+
] : undefined;
|
|
241
|
+
|
|
242
|
+
const extraPlugins: any[] = [];
|
|
243
|
+
if (opts.define && typeof opts.define === 'object') {
|
|
244
|
+
// rspack's DefinePlugin shape mirrors webpack's DefinePlugin
|
|
245
|
+
const DefinePlugin = (rspack as any).DefinePlugin || (rspack as any).plugins?.DefinePlugin;
|
|
246
|
+
if (DefinePlugin) {
|
|
247
|
+
extraPlugins.push(new DefinePlugin(opts.define));
|
|
248
|
+
} else {
|
|
249
|
+
// fallback: try to inject via plugin API name
|
|
250
|
+
extraPlugins.push({ name: 'DefinePlugin', value: opts.define });
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const resolveConfig: any = {
|
|
255
|
+
extensions: ['.tsx', '.ts', '.js', '.jsx'],
|
|
256
|
+
alias: resolveAliases,
|
|
257
|
+
// for server builds prefer the package "main"/"module" fields and avoid "browser" so we don't pick browser-specific entrypoints
|
|
258
|
+
mainFields: isServerBuild ? ['main', 'module'] : ['browser', 'module', 'main'],
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
return {
|
|
262
|
+
entry,
|
|
263
|
+
resolve: resolveConfig,
|
|
264
|
+
output: {
|
|
265
|
+
...opts.output,
|
|
266
|
+
clean: false,
|
|
267
|
+
},
|
|
268
|
+
mode: opts.mode,
|
|
269
|
+
externals,
|
|
270
|
+
plugins: [
|
|
271
|
+
new rspack.HtmlRspackPlugin({
|
|
272
|
+
publicPath: base || '/',
|
|
273
|
+
template: clientScriptPath,
|
|
274
|
+
scriptLoading: 'module',
|
|
275
|
+
filename: 'out.html',
|
|
276
|
+
inject: 'body',
|
|
277
|
+
}),
|
|
278
|
+
isDev && new ReactRefreshPlugin(),
|
|
279
|
+
includeHotPlugin && isDev && new rspack.HotModuleReplacementPlugin(),
|
|
280
|
+
...extraPlugins,
|
|
281
|
+
],
|
|
282
|
+
...localConfig,
|
|
283
|
+
// HMR is not implemented for module chunk format, so disable outputModule
|
|
284
|
+
// for client builds. SSR builds still need it for dynamic import() of exports.
|
|
285
|
+
experiments: {
|
|
286
|
+
...(localConfig.experiments || {}),
|
|
287
|
+
outputModule: isServerBuild,
|
|
288
|
+
},
|
|
289
|
+
};
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Creates a configured rspack compiler for the client bundle without running it.
|
|
294
|
+
* Intended for use with RspackDevServer for proper HMR support.
|
|
295
|
+
* HotModuleReplacementPlugin is intentionally omitted — RspackDevServer adds it automatically.
|
|
296
|
+
*/
|
|
297
|
+
export const createClientCompiler = (entry: string, opts: EntryOptions) => {
|
|
298
|
+
return rspack(buildCompilerConfig(entry, opts, false));
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
export const compileEntry = async (entry: string, opts: EntryOptions & { watch?: boolean, onChange?: (stats:any)=>void }) => {
|
|
302
|
+
const compiler = rspack(buildCompilerConfig(entry, opts, true));
|
|
303
|
+
|
|
304
|
+
// If watch mode is requested, start watching and invoke onChange for each rebuild.
|
|
305
|
+
// The returned promise resolves once the first build completes so callers can
|
|
306
|
+
// await initial build completion before starting their own server.
|
|
307
|
+
if (opts.watch) {
|
|
308
|
+
await new Promise((resolve, reject) => {
|
|
309
|
+
let first = true;
|
|
310
|
+
compiler.watch({}, (err: any, stats: any) => {
|
|
311
|
+
if (err) {
|
|
312
|
+
if (first) { first = false; reject(err); }
|
|
313
|
+
else { console.error('rspack watch error', err); }
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
console.log(stats?.toString({
|
|
318
|
+
colors: true,
|
|
319
|
+
modules: true,
|
|
320
|
+
children: true,
|
|
321
|
+
chunks: true,
|
|
322
|
+
chunkModules: true,
|
|
323
|
+
}));
|
|
324
|
+
|
|
325
|
+
if (first) {
|
|
326
|
+
first = false;
|
|
327
|
+
resolve(stats);
|
|
328
|
+
} else {
|
|
329
|
+
try {
|
|
330
|
+
opts.onChange && opts.onChange(stats);
|
|
331
|
+
} catch (e) {
|
|
332
|
+
console.error('onChange handler error', e);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
});
|
|
336
|
+
});
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// non-watch: do a single run and resolve when complete
|
|
341
|
+
await new Promise((resolve, reject) => {
|
|
342
|
+
compiler.run((err: any, stats: any) => {
|
|
343
|
+
if (err) {
|
|
344
|
+
reject(err);
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
console.log(stats?.toString({
|
|
349
|
+
colors: true,
|
|
350
|
+
modules: true,
|
|
351
|
+
children: true,
|
|
352
|
+
chunks: true,
|
|
353
|
+
chunkModules: true,
|
|
354
|
+
}));
|
|
355
|
+
|
|
356
|
+
resolve(stats);
|
|
357
|
+
});
|
|
358
|
+
});
|
|
359
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** True when running inside Bun. */
|
|
2
|
+
export const isBun = typeof (globalThis as any).Bun !== 'undefined';
|
|
3
|
+
|
|
4
|
+
/** True when running inside Deno. */
|
|
5
|
+
export const isDeno = typeof (globalThis as any).Deno !== 'undefined';
|
|
6
|
+
|
|
7
|
+
/** True when running inside Node.js (not Bun, not Deno). */
|
|
8
|
+
export const isNode = !isBun && !isDeno;
|
|
9
|
+
|
|
10
|
+
/** Returns a human-readable runtime identifier. */
|
|
11
|
+
export const getRuntimeName = (): 'bun' | 'deno' | 'node' =>
|
|
12
|
+
isBun ? 'bun' : isDeno ? 'deno' : 'node';
|
|
13
|
+
|
|
14
|
+
/** Returns a version string for the current runtime, e.g. "Bun 1.1.0". */
|
|
15
|
+
export const getRuntimeVersion = (): string => {
|
|
16
|
+
if (isBun) return `Bun ${(globalThis as any).Bun.version}`;
|
|
17
|
+
if (isDeno) return `Deno ${(globalThis as any).Deno.version.deno}`;
|
|
18
|
+
return `Node.js ${process.version}`;
|
|
19
|
+
};
|