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
package/dist/cli.js
ADDED
|
@@ -0,0 +1,1441 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// cli-lib.ts
|
|
4
|
+
import { existsSync as existsSync3 } from "node:fs";
|
|
5
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
6
|
+
import { resolve, join } from "node:path";
|
|
7
|
+
|
|
8
|
+
// src/utils/proxyHandler.tsx
|
|
9
|
+
var cloneHeaders = (headers) => {
|
|
10
|
+
return new Headers(headers);
|
|
11
|
+
};
|
|
12
|
+
var getCORSHeaders = (req) => {
|
|
13
|
+
const origin = req.headers.get("Origin") || "*";
|
|
14
|
+
return {
|
|
15
|
+
"Access-Control-Allow-Origin": origin,
|
|
16
|
+
"Access-Control-Allow-Methods": "GET,HEAD,PUT,PATCH,POST,DELETE",
|
|
17
|
+
"Access-Control-Allow-Headers": req.headers.get("Access-Control-Request-Headers") || "*",
|
|
18
|
+
"Access-Control-Allow-Credentials": "true"
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
var createProxyHandler = (options) => {
|
|
22
|
+
const { proxy, proxyCORS } = options;
|
|
23
|
+
if (!proxy) {
|
|
24
|
+
return () => void 0;
|
|
25
|
+
}
|
|
26
|
+
if (typeof proxy === "function") {
|
|
27
|
+
return async (req) => {
|
|
28
|
+
if (req.method === "OPTIONS" && options.proxyCORS) {
|
|
29
|
+
return new Response(null, {
|
|
30
|
+
status: 204,
|
|
31
|
+
headers: getCORSHeaders(req)
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
const res = await proxy(req);
|
|
35
|
+
if (res && proxyCORS) {
|
|
36
|
+
const modifiedHeaders = new Headers(res.headers);
|
|
37
|
+
Object.entries(getCORSHeaders(req)).forEach(([key, value]) => {
|
|
38
|
+
modifiedHeaders.set(key, value);
|
|
39
|
+
});
|
|
40
|
+
return new Response(res.body, {
|
|
41
|
+
status: res.status,
|
|
42
|
+
statusText: res.statusText,
|
|
43
|
+
headers: modifiedHeaders
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
return res || void 0;
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
const proxyRules = Object.entries(proxy).sort((a, b) => b[0].length - a[0].length);
|
|
50
|
+
return async (req) => {
|
|
51
|
+
if (req.method === "OPTIONS" && options.proxyCORS) {
|
|
52
|
+
return new Response(null, {
|
|
53
|
+
status: 204,
|
|
54
|
+
headers: getCORSHeaders(req)
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
for (const [path2, target] of proxyRules) {
|
|
58
|
+
if (req.pathname.startsWith(path2)) {
|
|
59
|
+
const targetURL = new URL(target);
|
|
60
|
+
targetURL.pathname = targetURL.pathname.replace(/\/$/, "") + req.pathname.slice(path2.length);
|
|
61
|
+
targetURL.search = req.search;
|
|
62
|
+
const sendHeaders = cloneHeaders(req.headers);
|
|
63
|
+
sendHeaders.set("Host", targetURL.host);
|
|
64
|
+
const proxyReq = new Request(targetURL.toString(), {
|
|
65
|
+
method: req.method,
|
|
66
|
+
headers: sendHeaders,
|
|
67
|
+
body: ["GET", "HEAD"].includes(req.method) ? void 0 : req.body,
|
|
68
|
+
redirect: "follow"
|
|
69
|
+
});
|
|
70
|
+
const res = await fetch(proxyReq);
|
|
71
|
+
if (proxyCORS) {
|
|
72
|
+
Object.entries(getCORSHeaders(req)).forEach(([key, value]) => {
|
|
73
|
+
res.headers.set(key, value);
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
const body = await res.arrayBuffer();
|
|
77
|
+
const clonedRes = new Headers(res.headers);
|
|
78
|
+
clonedRes.delete("content-length");
|
|
79
|
+
clonedRes.delete("content-encoding");
|
|
80
|
+
return new Response(body, {
|
|
81
|
+
status: res.status,
|
|
82
|
+
statusText: res.statusText,
|
|
83
|
+
headers: clonedRes
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return void 0;
|
|
88
|
+
};
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
// src/utils/cookies.ts
|
|
92
|
+
var parseCookies = (cookieString) => {
|
|
93
|
+
const cookies = {};
|
|
94
|
+
if (!cookieString) {
|
|
95
|
+
return cookies;
|
|
96
|
+
}
|
|
97
|
+
const pairs = cookieString.split(";");
|
|
98
|
+
for (const pair of pairs) {
|
|
99
|
+
const index = pair.indexOf("=");
|
|
100
|
+
if (index > -1) {
|
|
101
|
+
const key = pair.slice(0, index).trim();
|
|
102
|
+
const value = pair.slice(index + 1).trim();
|
|
103
|
+
cookies[key] = decodeURIComponent(value);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return cookies;
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
// src/utils/request.tsx
|
|
110
|
+
var parseRequest = (request) => {
|
|
111
|
+
const url = new URL(request.url);
|
|
112
|
+
const cookies = request.headers.get("Cookie") || "";
|
|
113
|
+
const cookieRecord = parseCookies(cookies);
|
|
114
|
+
return Object.assign(request, { pathname: url.pathname, search: url.search, location: url.pathname + url.search, cookies: cookieRecord });
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
// src/utils/upgradeRequest.tsx
|
|
118
|
+
var upgradeHandler = (options) => {
|
|
119
|
+
const { wsPath = "/ws" } = options;
|
|
120
|
+
if (options.websocket) {
|
|
121
|
+
return (req, ctx) => {
|
|
122
|
+
if (req.pathname === wsPath) {
|
|
123
|
+
return ctx.upgrade(req);
|
|
124
|
+
}
|
|
125
|
+
return false;
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
return null;
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
// src/utils/response.tsx
|
|
132
|
+
import { createRequire } from "node:module";
|
|
133
|
+
import pathMod from "node:path";
|
|
134
|
+
import { pathToFileURL } from "node:url";
|
|
135
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
136
|
+
var _renderToStaticMarkup = null;
|
|
137
|
+
async function getStaticMarkupRenderer() {
|
|
138
|
+
if (!_renderToStaticMarkup) {
|
|
139
|
+
const req = createRequire(pathMod.resolve(process.cwd(), "__hadars_fake__.js"));
|
|
140
|
+
const resolved = req.resolve("react-dom/server");
|
|
141
|
+
const mod = await import(pathToFileURL(resolved).href);
|
|
142
|
+
_renderToStaticMarkup = mod.renderToStaticMarkup;
|
|
143
|
+
}
|
|
144
|
+
return _renderToStaticMarkup;
|
|
145
|
+
}
|
|
146
|
+
var getHeadHtml = (seoData, renderToStaticMarkup) => {
|
|
147
|
+
const metaEntries = Object.entries(seoData.meta);
|
|
148
|
+
const linkEntries = Object.entries(seoData.link);
|
|
149
|
+
const styleEntries = Object.entries(seoData.style);
|
|
150
|
+
const scriptEntries = Object.entries(seoData.script);
|
|
151
|
+
return renderToStaticMarkup(
|
|
152
|
+
/* @__PURE__ */ jsxs(Fragment, { children: [
|
|
153
|
+
/* @__PURE__ */ jsx("title", { children: seoData.title }),
|
|
154
|
+
metaEntries.map(([id, options]) => /* @__PURE__ */ jsx(
|
|
155
|
+
"meta",
|
|
156
|
+
{
|
|
157
|
+
id,
|
|
158
|
+
...options
|
|
159
|
+
},
|
|
160
|
+
id
|
|
161
|
+
)),
|
|
162
|
+
linkEntries.map(([id, options]) => /* @__PURE__ */ jsx(
|
|
163
|
+
"link",
|
|
164
|
+
{
|
|
165
|
+
id,
|
|
166
|
+
...options
|
|
167
|
+
},
|
|
168
|
+
id
|
|
169
|
+
)),
|
|
170
|
+
styleEntries.map(([id, options]) => /* @__PURE__ */ jsx(
|
|
171
|
+
"style",
|
|
172
|
+
{
|
|
173
|
+
id,
|
|
174
|
+
...options
|
|
175
|
+
},
|
|
176
|
+
id
|
|
177
|
+
)),
|
|
178
|
+
scriptEntries.map(([id, options]) => /* @__PURE__ */ jsx(
|
|
179
|
+
"script",
|
|
180
|
+
{
|
|
181
|
+
id,
|
|
182
|
+
...options
|
|
183
|
+
},
|
|
184
|
+
id
|
|
185
|
+
))
|
|
186
|
+
] })
|
|
187
|
+
);
|
|
188
|
+
};
|
|
189
|
+
var getReactResponse = async (req, opts) => {
|
|
190
|
+
const App = opts.document.body;
|
|
191
|
+
const { getInitProps, getAfterRenderProps, getFinalProps } = opts.document;
|
|
192
|
+
const renderToStaticMarkup = await getStaticMarkupRenderer();
|
|
193
|
+
const unsuspend = {
|
|
194
|
+
cache: /* @__PURE__ */ new Map(),
|
|
195
|
+
hasPending: false
|
|
196
|
+
};
|
|
197
|
+
const processUnsuspend = async () => {
|
|
198
|
+
const pending = [...unsuspend.cache.values()].filter((e) => e.status === "pending").map((e) => e.promise);
|
|
199
|
+
await Promise.all(pending);
|
|
200
|
+
};
|
|
201
|
+
const context = {
|
|
202
|
+
head: {
|
|
203
|
+
title: "Hadars App",
|
|
204
|
+
meta: {},
|
|
205
|
+
link: {},
|
|
206
|
+
style: {},
|
|
207
|
+
script: {},
|
|
208
|
+
status: 200
|
|
209
|
+
},
|
|
210
|
+
_unsuspend: unsuspend
|
|
211
|
+
};
|
|
212
|
+
let props = {
|
|
213
|
+
...getInitProps ? await getInitProps(req) : {},
|
|
214
|
+
location: req.location,
|
|
215
|
+
context
|
|
216
|
+
};
|
|
217
|
+
let html = "";
|
|
218
|
+
let iters = 0;
|
|
219
|
+
do {
|
|
220
|
+
unsuspend.hasPending = false;
|
|
221
|
+
try {
|
|
222
|
+
globalThis.__hadarsUnsuspend = unsuspend;
|
|
223
|
+
html = renderToStaticMarkup(/* @__PURE__ */ jsx(App, { ...props }));
|
|
224
|
+
} finally {
|
|
225
|
+
globalThis.__hadarsUnsuspend = null;
|
|
226
|
+
}
|
|
227
|
+
if (unsuspend.hasPending)
|
|
228
|
+
await processUnsuspend();
|
|
229
|
+
} while (unsuspend.hasPending && ++iters < 25);
|
|
230
|
+
props = getAfterRenderProps ? await getAfterRenderProps(props, html) : props;
|
|
231
|
+
try {
|
|
232
|
+
globalThis.__hadarsUnsuspend = unsuspend;
|
|
233
|
+
renderToStaticMarkup(
|
|
234
|
+
/* @__PURE__ */ jsx(App, { ...{
|
|
235
|
+
...props,
|
|
236
|
+
location: req.location,
|
|
237
|
+
context
|
|
238
|
+
} })
|
|
239
|
+
);
|
|
240
|
+
} finally {
|
|
241
|
+
globalThis.__hadarsUnsuspend = null;
|
|
242
|
+
}
|
|
243
|
+
const serverData = {};
|
|
244
|
+
for (const [k, v] of unsuspend.cache) {
|
|
245
|
+
if (v.status === "fulfilled")
|
|
246
|
+
serverData[k] = v.value;
|
|
247
|
+
}
|
|
248
|
+
const { context: _, ...restProps } = getFinalProps ? await getFinalProps(props) : props;
|
|
249
|
+
const clientProps = {
|
|
250
|
+
...restProps,
|
|
251
|
+
location: req.location,
|
|
252
|
+
...Object.keys(serverData).length > 0 ? { __serverData: serverData } : {}
|
|
253
|
+
};
|
|
254
|
+
const ReactPage = /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
255
|
+
/* @__PURE__ */ jsx("div", { id: "app", children: /* @__PURE__ */ jsx(App, { ...{
|
|
256
|
+
...props,
|
|
257
|
+
location: req.location,
|
|
258
|
+
context
|
|
259
|
+
} }) }),
|
|
260
|
+
/* @__PURE__ */ jsx("script", { id: "hadars", type: "application/json", dangerouslySetInnerHTML: { __html: JSON.stringify({ hadars: { props: clientProps } }) } })
|
|
261
|
+
] });
|
|
262
|
+
return {
|
|
263
|
+
ReactPage,
|
|
264
|
+
status: context.head.status,
|
|
265
|
+
headHtml: getHeadHtml(context.head, renderToStaticMarkup),
|
|
266
|
+
renderPayload: {
|
|
267
|
+
appProps: { ...props, location: req.location, context },
|
|
268
|
+
clientProps
|
|
269
|
+
}
|
|
270
|
+
};
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
// src/utils/rspack.ts
|
|
274
|
+
import rspack from "@rspack/core";
|
|
275
|
+
import ReactRefreshPlugin from "@rspack/plugin-react-refresh";
|
|
276
|
+
import path from "node:path";
|
|
277
|
+
import { fileURLToPath } from "node:url";
|
|
278
|
+
import pathMod2 from "node:path";
|
|
279
|
+
import { existsSync } from "node:fs";
|
|
280
|
+
var __dirname = process.cwd();
|
|
281
|
+
var packageDir = pathMod2.dirname(fileURLToPath(import.meta.url));
|
|
282
|
+
var clientScriptPath = pathMod2.resolve(packageDir, "template.html");
|
|
283
|
+
var loaderPath = existsSync(pathMod2.resolve(packageDir, "loader.cjs")) ? pathMod2.resolve(packageDir, "loader.cjs") : pathMod2.resolve(packageDir, "loader.ts");
|
|
284
|
+
var getConfigBase = (mode) => {
|
|
285
|
+
const isDev = mode === "development";
|
|
286
|
+
return {
|
|
287
|
+
experiments: {
|
|
288
|
+
css: true,
|
|
289
|
+
outputModule: true
|
|
290
|
+
},
|
|
291
|
+
resolve: {
|
|
292
|
+
modules: [
|
|
293
|
+
path.resolve(__dirname, "node_modules"),
|
|
294
|
+
// 'node_modules' (relative) enables the standard upward-traversal
|
|
295
|
+
// resolution so rspack can find transitive deps (e.g. webpack-dev-server)
|
|
296
|
+
// that live in a parent node_modules when running from a sub-project.
|
|
297
|
+
"node_modules"
|
|
298
|
+
],
|
|
299
|
+
tsConfig: path.resolve(__dirname, "tsconfig.json"),
|
|
300
|
+
extensions: [".tsx", ".ts", ".js", ".jsx"]
|
|
301
|
+
},
|
|
302
|
+
module: {
|
|
303
|
+
rules: [
|
|
304
|
+
{
|
|
305
|
+
test: /\.mdx?$/,
|
|
306
|
+
use: [
|
|
307
|
+
{
|
|
308
|
+
loader: "@mdx-js/loader",
|
|
309
|
+
options: {}
|
|
310
|
+
}
|
|
311
|
+
]
|
|
312
|
+
},
|
|
313
|
+
{
|
|
314
|
+
test: /\.css$/,
|
|
315
|
+
use: ["postcss-loader"],
|
|
316
|
+
type: "css"
|
|
317
|
+
},
|
|
318
|
+
{
|
|
319
|
+
test: /\.svg$/i,
|
|
320
|
+
issuer: /\.[jt]sx?$/,
|
|
321
|
+
use: ["@svgr/webpack"]
|
|
322
|
+
},
|
|
323
|
+
{
|
|
324
|
+
test: /\.m?jsx?$/,
|
|
325
|
+
resolve: {
|
|
326
|
+
fullySpecified: false
|
|
327
|
+
},
|
|
328
|
+
exclude: [loaderPath],
|
|
329
|
+
use: [
|
|
330
|
+
// Transforms loadModule('./path') based on build target.
|
|
331
|
+
// Runs before swc-loader (loaders execute right-to-left).
|
|
332
|
+
{
|
|
333
|
+
loader: loaderPath
|
|
334
|
+
},
|
|
335
|
+
{
|
|
336
|
+
loader: "builtin:swc-loader",
|
|
337
|
+
options: {
|
|
338
|
+
jsc: {
|
|
339
|
+
parser: {
|
|
340
|
+
syntax: "ecmascript",
|
|
341
|
+
jsx: true
|
|
342
|
+
},
|
|
343
|
+
transform: {
|
|
344
|
+
react: {
|
|
345
|
+
runtime: "automatic",
|
|
346
|
+
development: isDev,
|
|
347
|
+
refresh: isDev
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
],
|
|
354
|
+
type: "javascript/auto"
|
|
355
|
+
},
|
|
356
|
+
{
|
|
357
|
+
test: /\.tsx?$/,
|
|
358
|
+
resolve: {
|
|
359
|
+
fullySpecified: false
|
|
360
|
+
},
|
|
361
|
+
exclude: [loaderPath],
|
|
362
|
+
use: [
|
|
363
|
+
{
|
|
364
|
+
loader: loaderPath
|
|
365
|
+
},
|
|
366
|
+
{
|
|
367
|
+
loader: "builtin:swc-loader",
|
|
368
|
+
options: {
|
|
369
|
+
jsc: {
|
|
370
|
+
parser: {
|
|
371
|
+
syntax: "typescript",
|
|
372
|
+
tsx: true
|
|
373
|
+
},
|
|
374
|
+
transform: {
|
|
375
|
+
react: {
|
|
376
|
+
runtime: "automatic",
|
|
377
|
+
development: isDev,
|
|
378
|
+
refresh: isDev
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
],
|
|
385
|
+
type: "javascript/auto"
|
|
386
|
+
}
|
|
387
|
+
]
|
|
388
|
+
}
|
|
389
|
+
};
|
|
390
|
+
};
|
|
391
|
+
var buildCompilerConfig = (entry, opts, includeHotPlugin) => {
|
|
392
|
+
const Config = getConfigBase(opts.mode);
|
|
393
|
+
const { base } = opts;
|
|
394
|
+
const isDev = opts.mode === "development";
|
|
395
|
+
const localConfig = {
|
|
396
|
+
...Config,
|
|
397
|
+
module: {
|
|
398
|
+
...Config.module,
|
|
399
|
+
rules: (Config.module && Array.isArray(Config.module.rules) ? Config.module.rules : []).map((r) => {
|
|
400
|
+
const nr = { ...r };
|
|
401
|
+
if (r && Array.isArray(r.use)) {
|
|
402
|
+
nr.use = r.use.map((u) => ({ ...typeof u === "object" ? u : { loader: u } }));
|
|
403
|
+
}
|
|
404
|
+
return nr;
|
|
405
|
+
})
|
|
406
|
+
}
|
|
407
|
+
};
|
|
408
|
+
if (opts.swcPlugins && Array.isArray(opts.swcPlugins) && opts.swcPlugins.length > 0) {
|
|
409
|
+
const rules = localConfig.module && localConfig.module.rules;
|
|
410
|
+
if (Array.isArray(rules)) {
|
|
411
|
+
for (const rule of rules) {
|
|
412
|
+
const ruleUse = rule;
|
|
413
|
+
if (ruleUse.use && Array.isArray(ruleUse.use)) {
|
|
414
|
+
for (const entry2 of ruleUse.use) {
|
|
415
|
+
const useEntry = entry2;
|
|
416
|
+
if (useEntry && useEntry.loader && typeof useEntry.loader === "string" && useEntry.loader.includes("swc-loader")) {
|
|
417
|
+
const options = useEntry.options || {};
|
|
418
|
+
useEntry.options = options;
|
|
419
|
+
useEntry.options.jsc = useEntry.options.jsc || {};
|
|
420
|
+
useEntry.options.jsc.experimental = useEntry.options.jsc.experimental || {};
|
|
421
|
+
useEntry.options.jsc.experimental.runPluginFirst = true;
|
|
422
|
+
const existingPlugins = Array.isArray(useEntry.options.jsc.experimental.plugins) ? useEntry.options.jsc.experimental.plugins : [];
|
|
423
|
+
const incomingPlugins = Array.isArray(opts.swcPlugins) ? opts.swcPlugins : [];
|
|
424
|
+
const seen = /* @__PURE__ */ new Set();
|
|
425
|
+
const merged = [];
|
|
426
|
+
for (const p of existingPlugins.concat(incomingPlugins)) {
|
|
427
|
+
const name = Array.isArray(p) && p.length > 0 ? String(p[0]) : String(p);
|
|
428
|
+
if (!seen.has(name)) {
|
|
429
|
+
seen.add(name);
|
|
430
|
+
merged.push(p);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
useEntry.options.jsc.experimental.plugins = merged;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
const isServerBuild = Boolean(
|
|
441
|
+
opts.output && typeof opts.output === "object" && (opts.output.library || String(opts.output.filename || "").includes("ssr"))
|
|
442
|
+
);
|
|
443
|
+
const resolveAliases = isServerBuild ? {
|
|
444
|
+
// force all react imports to resolve to this project's react
|
|
445
|
+
react: path.resolve(process.cwd(), "node_modules", "react"),
|
|
446
|
+
"react-dom": path.resolve(process.cwd(), "node_modules", "react-dom"),
|
|
447
|
+
// also map react/jsx-runtime to avoid duplicates when automatic runtime is used
|
|
448
|
+
"react/jsx-runtime": path.resolve(process.cwd(), "node_modules", "react", "jsx-runtime.js"),
|
|
449
|
+
"react/jsx-dev-runtime": path.resolve(process.cwd(), "node_modules", "react", "jsx-dev-runtime.js"),
|
|
450
|
+
// ensure emotion packages resolve to the project's node_modules so we don't pick up a browser-specific entry
|
|
451
|
+
"@emotion/react": path.resolve(process.cwd(), "node_modules", "@emotion", "react"),
|
|
452
|
+
"@emotion/server": path.resolve(process.cwd(), "node_modules", "@emotion", "server"),
|
|
453
|
+
"@emotion/cache": path.resolve(process.cwd(), "node_modules", "@emotion", "cache"),
|
|
454
|
+
"@emotion/styled": path.resolve(process.cwd(), "node_modules", "@emotion", "styled")
|
|
455
|
+
} : void 0;
|
|
456
|
+
const externals = isServerBuild ? [
|
|
457
|
+
"react",
|
|
458
|
+
"react-dom",
|
|
459
|
+
// keep common aliases external as well
|
|
460
|
+
"react/jsx-runtime",
|
|
461
|
+
"react/jsx-dev-runtime",
|
|
462
|
+
// emotion should be external on server builds to avoid client/browser code
|
|
463
|
+
"@emotion/react",
|
|
464
|
+
"@emotion/server",
|
|
465
|
+
"@emotion/cache",
|
|
466
|
+
"@emotion/styled"
|
|
467
|
+
] : void 0;
|
|
468
|
+
const extraPlugins = [];
|
|
469
|
+
if (opts.define && typeof opts.define === "object") {
|
|
470
|
+
const DefinePlugin = rspack.DefinePlugin || rspack.plugins?.DefinePlugin;
|
|
471
|
+
if (DefinePlugin) {
|
|
472
|
+
extraPlugins.push(new DefinePlugin(opts.define));
|
|
473
|
+
} else {
|
|
474
|
+
extraPlugins.push({ name: "DefinePlugin", value: opts.define });
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
const resolveConfig = {
|
|
478
|
+
extensions: [".tsx", ".ts", ".js", ".jsx"],
|
|
479
|
+
alias: resolveAliases,
|
|
480
|
+
// for server builds prefer the package "main"/"module" fields and avoid "browser" so we don't pick browser-specific entrypoints
|
|
481
|
+
mainFields: isServerBuild ? ["main", "module"] : ["browser", "module", "main"]
|
|
482
|
+
};
|
|
483
|
+
return {
|
|
484
|
+
entry,
|
|
485
|
+
resolve: resolveConfig,
|
|
486
|
+
output: {
|
|
487
|
+
...opts.output,
|
|
488
|
+
clean: false
|
|
489
|
+
},
|
|
490
|
+
mode: opts.mode,
|
|
491
|
+
externals,
|
|
492
|
+
plugins: [
|
|
493
|
+
new rspack.HtmlRspackPlugin({
|
|
494
|
+
publicPath: base || "/",
|
|
495
|
+
template: clientScriptPath,
|
|
496
|
+
scriptLoading: "module",
|
|
497
|
+
filename: "out.html",
|
|
498
|
+
inject: "body"
|
|
499
|
+
}),
|
|
500
|
+
isDev && new ReactRefreshPlugin(),
|
|
501
|
+
includeHotPlugin && isDev && new rspack.HotModuleReplacementPlugin(),
|
|
502
|
+
...extraPlugins
|
|
503
|
+
],
|
|
504
|
+
...localConfig,
|
|
505
|
+
// HMR is not implemented for module chunk format, so disable outputModule
|
|
506
|
+
// for client builds. SSR builds still need it for dynamic import() of exports.
|
|
507
|
+
experiments: {
|
|
508
|
+
...localConfig.experiments || {},
|
|
509
|
+
outputModule: isServerBuild
|
|
510
|
+
}
|
|
511
|
+
};
|
|
512
|
+
};
|
|
513
|
+
var createClientCompiler = (entry, opts) => {
|
|
514
|
+
return rspack(buildCompilerConfig(entry, opts, false));
|
|
515
|
+
};
|
|
516
|
+
var compileEntry = async (entry, opts) => {
|
|
517
|
+
const compiler = rspack(buildCompilerConfig(entry, opts, true));
|
|
518
|
+
if (opts.watch) {
|
|
519
|
+
await new Promise((resolve2, reject) => {
|
|
520
|
+
let first = true;
|
|
521
|
+
compiler.watch({}, (err, stats) => {
|
|
522
|
+
if (err) {
|
|
523
|
+
if (first) {
|
|
524
|
+
first = false;
|
|
525
|
+
reject(err);
|
|
526
|
+
} else {
|
|
527
|
+
console.error("rspack watch error", err);
|
|
528
|
+
}
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
console.log(stats?.toString({
|
|
532
|
+
colors: true,
|
|
533
|
+
modules: true,
|
|
534
|
+
children: true,
|
|
535
|
+
chunks: true,
|
|
536
|
+
chunkModules: true
|
|
537
|
+
}));
|
|
538
|
+
if (first) {
|
|
539
|
+
first = false;
|
|
540
|
+
resolve2(stats);
|
|
541
|
+
} else {
|
|
542
|
+
try {
|
|
543
|
+
opts.onChange && opts.onChange(stats);
|
|
544
|
+
} catch (e) {
|
|
545
|
+
console.error("onChange handler error", e);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
});
|
|
549
|
+
});
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
552
|
+
await new Promise((resolve2, reject) => {
|
|
553
|
+
compiler.run((err, stats) => {
|
|
554
|
+
if (err) {
|
|
555
|
+
reject(err);
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
console.log(stats?.toString({
|
|
559
|
+
colors: true,
|
|
560
|
+
modules: true,
|
|
561
|
+
children: true,
|
|
562
|
+
chunks: true,
|
|
563
|
+
chunkModules: true
|
|
564
|
+
}));
|
|
565
|
+
resolve2(stats);
|
|
566
|
+
});
|
|
567
|
+
});
|
|
568
|
+
};
|
|
569
|
+
|
|
570
|
+
// src/utils/runtime.ts
|
|
571
|
+
var isBun = typeof globalThis.Bun !== "undefined";
|
|
572
|
+
var isDeno = typeof globalThis.Deno !== "undefined";
|
|
573
|
+
var isNode = !isBun && !isDeno;
|
|
574
|
+
|
|
575
|
+
// src/utils/serve.ts
|
|
576
|
+
function nodeReadableToWebStream(readable) {
|
|
577
|
+
const enc = new TextEncoder();
|
|
578
|
+
return new ReadableStream({
|
|
579
|
+
start(controller) {
|
|
580
|
+
readable.on("data", (chunk) => {
|
|
581
|
+
controller.enqueue(
|
|
582
|
+
typeof chunk === "string" ? enc.encode(chunk) : new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength)
|
|
583
|
+
);
|
|
584
|
+
});
|
|
585
|
+
readable.on("end", () => controller.close());
|
|
586
|
+
readable.on("error", (err) => controller.error(err));
|
|
587
|
+
},
|
|
588
|
+
cancel() {
|
|
589
|
+
readable.destroy?.();
|
|
590
|
+
}
|
|
591
|
+
});
|
|
592
|
+
}
|
|
593
|
+
var noopCtx = { upgrade: () => false };
|
|
594
|
+
async function serve(port, fetchHandler, websocket) {
|
|
595
|
+
if (isBun) {
|
|
596
|
+
globalThis.Bun.serve({
|
|
597
|
+
port,
|
|
598
|
+
websocket,
|
|
599
|
+
async fetch(req, server2) {
|
|
600
|
+
const ctx = { upgrade: (r) => server2.upgrade(r) };
|
|
601
|
+
return await fetchHandler(req, ctx) ?? void 0;
|
|
602
|
+
}
|
|
603
|
+
});
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
if (isDeno) {
|
|
607
|
+
globalThis.Deno.serve({
|
|
608
|
+
port,
|
|
609
|
+
handler: async (req) => {
|
|
610
|
+
const res = await fetchHandler(req, noopCtx);
|
|
611
|
+
return res ?? new Response("Not Found", { status: 404 });
|
|
612
|
+
}
|
|
613
|
+
});
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
const { createServer } = await import("node:http");
|
|
617
|
+
const server = createServer(async (nodeReq, nodeRes) => {
|
|
618
|
+
try {
|
|
619
|
+
const chunks = [];
|
|
620
|
+
if (!["GET", "HEAD"].includes(nodeReq.method ?? "GET")) {
|
|
621
|
+
for await (const chunk of nodeReq) {
|
|
622
|
+
chunks.push(chunk);
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
const body = chunks.length > 0 ? Buffer.concat(chunks) : void 0;
|
|
626
|
+
const url = `http://localhost:${port}${nodeReq.url ?? "/"}`;
|
|
627
|
+
const reqInit = {
|
|
628
|
+
method: nodeReq.method ?? "GET",
|
|
629
|
+
headers: new Headers(nodeReq.headers)
|
|
630
|
+
};
|
|
631
|
+
if (body) {
|
|
632
|
+
reqInit.body = body;
|
|
633
|
+
reqInit.duplex = "half";
|
|
634
|
+
}
|
|
635
|
+
const req = new Request(url, reqInit);
|
|
636
|
+
const res = await fetchHandler(req, noopCtx);
|
|
637
|
+
const response = res ?? new Response("Not Found", { status: 404 });
|
|
638
|
+
const headers = {};
|
|
639
|
+
response.headers.forEach((v, k) => {
|
|
640
|
+
headers[k] = v;
|
|
641
|
+
});
|
|
642
|
+
nodeRes.writeHead(response.status, headers);
|
|
643
|
+
if (response.body) {
|
|
644
|
+
const reader = response.body.getReader();
|
|
645
|
+
while (true) {
|
|
646
|
+
const { done, value } = await reader.read();
|
|
647
|
+
if (done)
|
|
648
|
+
break;
|
|
649
|
+
await new Promise(
|
|
650
|
+
(resolve2, reject) => nodeRes.write(value, (err) => err ? reject(err) : resolve2())
|
|
651
|
+
);
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
} catch (err) {
|
|
655
|
+
console.error("[hadars] request error", err);
|
|
656
|
+
if (!nodeRes.headersSent)
|
|
657
|
+
nodeRes.writeHead(500);
|
|
658
|
+
} finally {
|
|
659
|
+
nodeRes.end();
|
|
660
|
+
}
|
|
661
|
+
});
|
|
662
|
+
await new Promise((resolve2, reject) => {
|
|
663
|
+
server.listen(port, () => resolve2());
|
|
664
|
+
server.once("error", reject);
|
|
665
|
+
});
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
// src/utils/staticFile.ts
|
|
669
|
+
import { readFile, stat } from "node:fs/promises";
|
|
670
|
+
var MIME = {
|
|
671
|
+
html: "text/html; charset=utf-8",
|
|
672
|
+
htm: "text/html; charset=utf-8",
|
|
673
|
+
css: "text/css",
|
|
674
|
+
js: "application/javascript",
|
|
675
|
+
mjs: "application/javascript",
|
|
676
|
+
cjs: "application/javascript",
|
|
677
|
+
json: "application/json",
|
|
678
|
+
map: "application/json",
|
|
679
|
+
png: "image/png",
|
|
680
|
+
jpg: "image/jpeg",
|
|
681
|
+
jpeg: "image/jpeg",
|
|
682
|
+
gif: "image/gif",
|
|
683
|
+
webp: "image/webp",
|
|
684
|
+
svg: "image/svg+xml",
|
|
685
|
+
ico: "image/x-icon",
|
|
686
|
+
woff: "font/woff",
|
|
687
|
+
woff2: "font/woff2",
|
|
688
|
+
ttf: "font/ttf",
|
|
689
|
+
otf: "font/otf",
|
|
690
|
+
txt: "text/plain",
|
|
691
|
+
xml: "application/xml",
|
|
692
|
+
pdf: "application/pdf"
|
|
693
|
+
};
|
|
694
|
+
async function tryServeFile(filePath) {
|
|
695
|
+
try {
|
|
696
|
+
await stat(filePath);
|
|
697
|
+
} catch {
|
|
698
|
+
return null;
|
|
699
|
+
}
|
|
700
|
+
try {
|
|
701
|
+
const data = await readFile(filePath);
|
|
702
|
+
const ext = filePath.split(".").pop()?.toLowerCase() ?? "";
|
|
703
|
+
const contentType = MIME[ext] ?? "application/octet-stream";
|
|
704
|
+
return new Response(data, { headers: { "Content-Type": contentType } });
|
|
705
|
+
} catch {
|
|
706
|
+
return null;
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
// src/build.ts
|
|
711
|
+
import { RspackDevServer } from "@rspack/dev-server";
|
|
712
|
+
import pathMod3 from "node:path";
|
|
713
|
+
import { fileURLToPath as fileURLToPath2, pathToFileURL as pathToFileURL2 } from "node:url";
|
|
714
|
+
import { createRequire as createRequire2 } from "node:module";
|
|
715
|
+
import crypto from "node:crypto";
|
|
716
|
+
import fs from "node:fs/promises";
|
|
717
|
+
import { existsSync as existsSync2 } from "node:fs";
|
|
718
|
+
import os from "node:os";
|
|
719
|
+
import { spawn } from "node:child_process";
|
|
720
|
+
import cluster from "node:cluster";
|
|
721
|
+
var encoder = new TextEncoder();
|
|
722
|
+
var HEAD_MARKER = '<meta name="NINETY_HEAD">';
|
|
723
|
+
var BODY_MARKER = '<meta name="NINETY_BODY">';
|
|
724
|
+
var _renderToReadableStream = null;
|
|
725
|
+
async function getReadableStreamRenderer() {
|
|
726
|
+
if (!_renderToReadableStream) {
|
|
727
|
+
const req = createRequire2(pathMod3.resolve(process.cwd(), "__hadars_fake__.js"));
|
|
728
|
+
const resolved = req.resolve("react-dom/server.browser");
|
|
729
|
+
const mod = await import(pathToFileURL2(resolved).href);
|
|
730
|
+
_renderToReadableStream = mod.renderToReadableStream;
|
|
731
|
+
}
|
|
732
|
+
return _renderToReadableStream;
|
|
733
|
+
}
|
|
734
|
+
var _renderToString = null;
|
|
735
|
+
async function getRenderToString() {
|
|
736
|
+
if (!_renderToString) {
|
|
737
|
+
const req = createRequire2(pathMod3.resolve(process.cwd(), "__hadars_fake__.js"));
|
|
738
|
+
const resolved = req.resolve("react-dom/server");
|
|
739
|
+
const mod = await import(pathToFileURL2(resolved).href);
|
|
740
|
+
_renderToString = mod.renderToString;
|
|
741
|
+
}
|
|
742
|
+
return _renderToString;
|
|
743
|
+
}
|
|
744
|
+
var RenderWorkerPool = class {
|
|
745
|
+
workers = [];
|
|
746
|
+
pending = /* @__PURE__ */ new Map();
|
|
747
|
+
nextId = 0;
|
|
748
|
+
rrIndex = 0;
|
|
749
|
+
constructor(workerPath, size, ssrBundlePath) {
|
|
750
|
+
this._init(workerPath, size, ssrBundlePath);
|
|
751
|
+
}
|
|
752
|
+
_init(workerPath, size, ssrBundlePath) {
|
|
753
|
+
import("node:worker_threads").then(({ Worker }) => {
|
|
754
|
+
for (let i = 0; i < size; i++) {
|
|
755
|
+
const w = new Worker(workerPath, { workerData: { ssrBundlePath } });
|
|
756
|
+
w.on("message", (msg) => {
|
|
757
|
+
const { id, type, html, error, chunk } = msg;
|
|
758
|
+
const p = this.pending.get(id);
|
|
759
|
+
if (!p)
|
|
760
|
+
return;
|
|
761
|
+
if (p.kind === "renderStream") {
|
|
762
|
+
if (type === "chunk") {
|
|
763
|
+
p.controller.enqueue(chunk);
|
|
764
|
+
return;
|
|
765
|
+
}
|
|
766
|
+
this.pending.delete(id);
|
|
767
|
+
if (type === "done")
|
|
768
|
+
p.controller.close();
|
|
769
|
+
else
|
|
770
|
+
p.controller.error(new Error(error ?? "Stream error"));
|
|
771
|
+
return;
|
|
772
|
+
}
|
|
773
|
+
this.pending.delete(id);
|
|
774
|
+
if (error)
|
|
775
|
+
p.reject(new Error(error));
|
|
776
|
+
else
|
|
777
|
+
p.resolve(html);
|
|
778
|
+
});
|
|
779
|
+
w.on("error", (err) => {
|
|
780
|
+
console.error("[hadars] Render worker error:", err);
|
|
781
|
+
});
|
|
782
|
+
this.workers.push(w);
|
|
783
|
+
}
|
|
784
|
+
}).catch((err) => {
|
|
785
|
+
console.error("[hadars] Failed to initialise render worker pool:", err);
|
|
786
|
+
});
|
|
787
|
+
}
|
|
788
|
+
nextWorker() {
|
|
789
|
+
const w = this.workers[this.rrIndex % this.workers.length];
|
|
790
|
+
this.rrIndex++;
|
|
791
|
+
return w;
|
|
792
|
+
}
|
|
793
|
+
/** Offload a full renderToString call. Returns the HTML string. */
|
|
794
|
+
renderString(appProps, clientProps) {
|
|
795
|
+
return new Promise((resolve2, reject) => {
|
|
796
|
+
const id = this.nextId++;
|
|
797
|
+
this.pending.set(id, { kind: "renderString", resolve: resolve2, reject });
|
|
798
|
+
this.nextWorker().postMessage({ id, type: "renderString", appProps, clientProps });
|
|
799
|
+
});
|
|
800
|
+
}
|
|
801
|
+
/** Offload a renderToReadableStream call. Returns a ReadableStream fed by
|
|
802
|
+
* worker chunk messages. */
|
|
803
|
+
renderStream(appProps, clientProps) {
|
|
804
|
+
let controller;
|
|
805
|
+
const stream = new ReadableStream({
|
|
806
|
+
start: (ctrl) => {
|
|
807
|
+
controller = ctrl;
|
|
808
|
+
}
|
|
809
|
+
});
|
|
810
|
+
const id = this.nextId++;
|
|
811
|
+
this.pending.set(id, { kind: "renderStream", controller });
|
|
812
|
+
this.nextWorker().postMessage({ id, type: "renderStream", appProps, clientProps });
|
|
813
|
+
return stream;
|
|
814
|
+
}
|
|
815
|
+
async terminate() {
|
|
816
|
+
await Promise.all(this.workers.map((w) => w.terminate()));
|
|
817
|
+
}
|
|
818
|
+
};
|
|
819
|
+
async function buildSsrResponse(ReactPage, headHtml, status, getPrecontentHtml, streaming, renderPool, renderPayload) {
|
|
820
|
+
const renderToString = !streaming && !renderPool ? await getRenderToString() : null;
|
|
821
|
+
const renderReadableStream = streaming && !renderPool ? await getReadableStreamRenderer() : null;
|
|
822
|
+
const unsuspendForRender = renderPayload?.appProps?.context?._unsuspend ?? null;
|
|
823
|
+
if (!streaming) {
|
|
824
|
+
const [precontentHtml, postContent] = await getPrecontentHtml(headHtml);
|
|
825
|
+
let bodyHtml;
|
|
826
|
+
if (renderPool && renderPayload) {
|
|
827
|
+
bodyHtml = await renderPool.renderString(renderPayload.appProps, renderPayload.clientProps);
|
|
828
|
+
} else {
|
|
829
|
+
try {
|
|
830
|
+
globalThis.__hadarsUnsuspend = unsuspendForRender;
|
|
831
|
+
bodyHtml = renderToString(ReactPage);
|
|
832
|
+
} finally {
|
|
833
|
+
globalThis.__hadarsUnsuspend = null;
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
return new Response(precontentHtml + bodyHtml + postContent, {
|
|
837
|
+
headers: { "Content-Type": "text/html; charset=utf-8" },
|
|
838
|
+
status
|
|
839
|
+
});
|
|
840
|
+
}
|
|
841
|
+
const responseStream = new ReadableStream({
|
|
842
|
+
async start(controller) {
|
|
843
|
+
const [precontentHtml, postContent] = await getPrecontentHtml(headHtml);
|
|
844
|
+
controller.enqueue(encoder.encode(precontentHtml));
|
|
845
|
+
let bodyStream;
|
|
846
|
+
if (renderPool && renderPayload) {
|
|
847
|
+
bodyStream = renderPool.renderStream(renderPayload.appProps, renderPayload.clientProps);
|
|
848
|
+
} else {
|
|
849
|
+
let streamPromise;
|
|
850
|
+
try {
|
|
851
|
+
globalThis.__hadarsUnsuspend = unsuspendForRender;
|
|
852
|
+
streamPromise = renderReadableStream(ReactPage);
|
|
853
|
+
} finally {
|
|
854
|
+
globalThis.__hadarsUnsuspend = null;
|
|
855
|
+
}
|
|
856
|
+
bodyStream = await streamPromise;
|
|
857
|
+
}
|
|
858
|
+
const reader = bodyStream.getReader();
|
|
859
|
+
while (true) {
|
|
860
|
+
const { done, value } = await reader.read();
|
|
861
|
+
if (done)
|
|
862
|
+
break;
|
|
863
|
+
controller.enqueue(value);
|
|
864
|
+
}
|
|
865
|
+
controller.enqueue(encoder.encode(postContent));
|
|
866
|
+
controller.close();
|
|
867
|
+
}
|
|
868
|
+
});
|
|
869
|
+
return new Response(responseStream, {
|
|
870
|
+
headers: { "Content-Type": "text/html; charset=utf-8" },
|
|
871
|
+
status
|
|
872
|
+
});
|
|
873
|
+
}
|
|
874
|
+
var makePrecontentHtmlGetter = (htmlFilePromise) => {
|
|
875
|
+
let preHead = null;
|
|
876
|
+
let postHead = null;
|
|
877
|
+
let postContent = null;
|
|
878
|
+
return async (headHtml) => {
|
|
879
|
+
if (preHead === null || postHead === null || postContent === null) {
|
|
880
|
+
const html = await htmlFilePromise;
|
|
881
|
+
const headEnd = html.indexOf(HEAD_MARKER);
|
|
882
|
+
const contentStart = html.indexOf(BODY_MARKER);
|
|
883
|
+
preHead = html.slice(0, headEnd);
|
|
884
|
+
postHead = html.slice(headEnd + HEAD_MARKER.length, contentStart);
|
|
885
|
+
postContent = html.slice(contentStart + BODY_MARKER.length);
|
|
886
|
+
}
|
|
887
|
+
return [preHead + headHtml + postHead, postContent];
|
|
888
|
+
};
|
|
889
|
+
};
|
|
890
|
+
var SSR_FILENAME = "index.ssr.js";
|
|
891
|
+
var __dirname2 = process.cwd();
|
|
892
|
+
var getSuffix = (mode) => mode === "development" ? `?v=${Date.now()}` : "";
|
|
893
|
+
var HadarsFolder = "./.hadars";
|
|
894
|
+
var StaticPath = `${HadarsFolder}/static`;
|
|
895
|
+
var validateOptions = (options) => {
|
|
896
|
+
if (!options.entry) {
|
|
897
|
+
throw new Error("Entry file is required");
|
|
898
|
+
}
|
|
899
|
+
if (options.mode !== "development" && options.mode !== "production") {
|
|
900
|
+
throw new Error("Mode must be either 'development' or 'production'");
|
|
901
|
+
}
|
|
902
|
+
};
|
|
903
|
+
var resolveWorkerCmd = (packageDir2) => {
|
|
904
|
+
const tsPath = pathMod3.resolve(packageDir2, "ssr-watch.ts");
|
|
905
|
+
const jsPath = pathMod3.resolve(packageDir2, "ssr-watch.js");
|
|
906
|
+
if (isBun && existsSync2(tsPath)) {
|
|
907
|
+
return ["bun", tsPath];
|
|
908
|
+
}
|
|
909
|
+
if (isDeno && existsSync2(tsPath)) {
|
|
910
|
+
return ["deno", "run", "--allow-all", tsPath];
|
|
911
|
+
}
|
|
912
|
+
if (existsSync2(tsPath)) {
|
|
913
|
+
const allArgs = [...process.execArgv, process.argv[1] ?? ""];
|
|
914
|
+
const hasTsx = allArgs.some((a) => a.includes("tsx"));
|
|
915
|
+
const hasTsNode = allArgs.some((a) => a.includes("ts-node"));
|
|
916
|
+
if (hasTsx)
|
|
917
|
+
return ["tsx", tsPath];
|
|
918
|
+
if (hasTsNode)
|
|
919
|
+
return ["ts-node", tsPath];
|
|
920
|
+
}
|
|
921
|
+
if (existsSync2(jsPath)) {
|
|
922
|
+
return ["node", jsPath];
|
|
923
|
+
}
|
|
924
|
+
throw new Error(
|
|
925
|
+
`[hadars] SSR worker not found. Expected:
|
|
926
|
+
${jsPath}
|
|
927
|
+
Run "npm run build:cli" to compile it, or launch hadars via a TypeScript runner:
|
|
928
|
+
npx tsx cli.ts dev`
|
|
929
|
+
);
|
|
930
|
+
};
|
|
931
|
+
var dev = async (options) => {
|
|
932
|
+
await fs.rm(HadarsFolder, { recursive: true, force: true });
|
|
933
|
+
let { port = 9090, baseURL = "" } = options;
|
|
934
|
+
console.log(`Starting Hadars on port ${port}`);
|
|
935
|
+
validateOptions(options);
|
|
936
|
+
const handleProxy = createProxyHandler(options);
|
|
937
|
+
const handleWS = upgradeHandler(options);
|
|
938
|
+
const handler = options.fetch;
|
|
939
|
+
const entry = pathMod3.resolve(__dirname2, options.entry);
|
|
940
|
+
const hmrPort = options.hmrPort ?? port + 1;
|
|
941
|
+
const packageDir2 = pathMod3.dirname(fileURLToPath2(import.meta.url));
|
|
942
|
+
const clientScriptPath2 = pathMod3.resolve(packageDir2, "utils", "clientScript.tsx");
|
|
943
|
+
const headPath = pathMod3.resolve(packageDir2, "utils", "Head");
|
|
944
|
+
let clientScript = "";
|
|
945
|
+
try {
|
|
946
|
+
clientScript = (await fs.readFile(clientScriptPath2, "utf-8")).replace("$_MOD_PATH$", entry + getSuffix(options.mode)).replace("$_HEAD_PATH$", headPath);
|
|
947
|
+
} catch (err) {
|
|
948
|
+
console.error("Failed to read client script from package dist, falling back to src", err);
|
|
949
|
+
throw err;
|
|
950
|
+
}
|
|
951
|
+
const tmpFilePath = pathMod3.join(os.tmpdir(), `hadars-client-${Date.now()}.tsx`);
|
|
952
|
+
await fs.writeFile(tmpFilePath, clientScript);
|
|
953
|
+
let ssrBuildId = Date.now();
|
|
954
|
+
const clientCompiler = createClientCompiler(tmpFilePath, {
|
|
955
|
+
target: "web",
|
|
956
|
+
output: {
|
|
957
|
+
filename: "index.js",
|
|
958
|
+
path: pathMod3.resolve(__dirname2, StaticPath)
|
|
959
|
+
},
|
|
960
|
+
base: baseURL,
|
|
961
|
+
mode: "development",
|
|
962
|
+
swcPlugins: options.swcPlugins,
|
|
963
|
+
define: options.define
|
|
964
|
+
});
|
|
965
|
+
const devServer = new RspackDevServer({
|
|
966
|
+
port: hmrPort,
|
|
967
|
+
hot: true,
|
|
968
|
+
liveReload: false,
|
|
969
|
+
client: {
|
|
970
|
+
webSocketURL: `ws://localhost:${hmrPort}/ws`
|
|
971
|
+
},
|
|
972
|
+
devMiddleware: {
|
|
973
|
+
writeToDisk: true
|
|
974
|
+
},
|
|
975
|
+
headers: { "Access-Control-Allow-Origin": "*" },
|
|
976
|
+
allowedHosts: "all"
|
|
977
|
+
}, clientCompiler);
|
|
978
|
+
console.log(`Starting HMR dev server on port ${hmrPort}`);
|
|
979
|
+
await new Promise((resolve2, reject) => {
|
|
980
|
+
let resolved = false;
|
|
981
|
+
clientCompiler.hooks.done.tap("initial-build", (stats) => {
|
|
982
|
+
if (!resolved) {
|
|
983
|
+
resolved = true;
|
|
984
|
+
console.log(stats.toString({ colors: true }));
|
|
985
|
+
resolve2();
|
|
986
|
+
}
|
|
987
|
+
});
|
|
988
|
+
devServer.start().catch(reject);
|
|
989
|
+
});
|
|
990
|
+
const workerCmd = resolveWorkerCmd(packageDir2);
|
|
991
|
+
console.log("Spawning SSR worker:", workerCmd.join(" "), "entry:", entry);
|
|
992
|
+
const child = spawn(workerCmd[0], [
|
|
993
|
+
...workerCmd.slice(1),
|
|
994
|
+
`--entry=${entry}`,
|
|
995
|
+
`--outDir=${HadarsFolder}`,
|
|
996
|
+
`--outFile=${SSR_FILENAME}`,
|
|
997
|
+
`--base=${baseURL}`
|
|
998
|
+
], { stdio: "pipe" });
|
|
999
|
+
child.stdin?.end();
|
|
1000
|
+
const stdoutWebStream = nodeReadableToWebStream(child.stdout);
|
|
1001
|
+
const stderrWebStream = nodeReadableToWebStream(child.stderr);
|
|
1002
|
+
const marker = "ssr-watch: initial-build-complete";
|
|
1003
|
+
const rebuildMarker = "ssr-watch: SSR rebuilt";
|
|
1004
|
+
const decoder = new TextDecoder();
|
|
1005
|
+
let gotMarker = false;
|
|
1006
|
+
let stdoutReader = null;
|
|
1007
|
+
try {
|
|
1008
|
+
stdoutReader = stdoutWebStream.getReader();
|
|
1009
|
+
let buf = "";
|
|
1010
|
+
const start = Date.now();
|
|
1011
|
+
const timeoutMs = 2e4;
|
|
1012
|
+
while (Date.now() - start < timeoutMs) {
|
|
1013
|
+
const { done, value } = await stdoutReader.read();
|
|
1014
|
+
if (done) {
|
|
1015
|
+
stdoutReader = null;
|
|
1016
|
+
break;
|
|
1017
|
+
}
|
|
1018
|
+
const chunk = decoder.decode(value, { stream: true });
|
|
1019
|
+
buf += chunk;
|
|
1020
|
+
try {
|
|
1021
|
+
process.stdout.write(chunk);
|
|
1022
|
+
} catch (e) {
|
|
1023
|
+
}
|
|
1024
|
+
if (buf.includes(marker)) {
|
|
1025
|
+
gotMarker = true;
|
|
1026
|
+
break;
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
if (!gotMarker) {
|
|
1030
|
+
console.warn("SSR worker did not signal initial build completion within timeout");
|
|
1031
|
+
}
|
|
1032
|
+
} catch (err) {
|
|
1033
|
+
console.error("Error reading SSR worker output", err);
|
|
1034
|
+
stdoutReader = null;
|
|
1035
|
+
}
|
|
1036
|
+
if (stdoutReader) {
|
|
1037
|
+
const reader = stdoutReader;
|
|
1038
|
+
(async () => {
|
|
1039
|
+
try {
|
|
1040
|
+
while (true) {
|
|
1041
|
+
const { done, value } = await reader.read();
|
|
1042
|
+
if (done)
|
|
1043
|
+
break;
|
|
1044
|
+
const chunk = decoder.decode(value, { stream: true });
|
|
1045
|
+
try {
|
|
1046
|
+
process.stdout.write(chunk);
|
|
1047
|
+
} catch (e) {
|
|
1048
|
+
}
|
|
1049
|
+
if (chunk.includes(rebuildMarker)) {
|
|
1050
|
+
ssrBuildId = Date.now();
|
|
1051
|
+
console.log("[hadars] SSR bundle updated, build id:", ssrBuildId);
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
} catch (e) {
|
|
1055
|
+
}
|
|
1056
|
+
})();
|
|
1057
|
+
}
|
|
1058
|
+
(async () => {
|
|
1059
|
+
try {
|
|
1060
|
+
const r = stderrWebStream.getReader();
|
|
1061
|
+
while (true) {
|
|
1062
|
+
const { done, value } = await r.read();
|
|
1063
|
+
if (done)
|
|
1064
|
+
break;
|
|
1065
|
+
try {
|
|
1066
|
+
process.stderr.write(decoder.decode(value));
|
|
1067
|
+
} catch (e) {
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
} catch (e) {
|
|
1071
|
+
}
|
|
1072
|
+
})();
|
|
1073
|
+
const getPrecontentHtml = makePrecontentHtmlGetter(
|
|
1074
|
+
fs.readFile(pathMod3.join(__dirname2, StaticPath, "out.html"), "utf-8")
|
|
1075
|
+
);
|
|
1076
|
+
await serve(port, async (req, ctx) => {
|
|
1077
|
+
const request = parseRequest(req);
|
|
1078
|
+
if (handler) {
|
|
1079
|
+
const res = await handler(request);
|
|
1080
|
+
if (res)
|
|
1081
|
+
return res;
|
|
1082
|
+
}
|
|
1083
|
+
if (handleWS && handleWS(request, ctx))
|
|
1084
|
+
return void 0;
|
|
1085
|
+
const proxied = await handleProxy(request);
|
|
1086
|
+
if (proxied)
|
|
1087
|
+
return proxied;
|
|
1088
|
+
const url = new URL(request.url);
|
|
1089
|
+
const path2 = url.pathname;
|
|
1090
|
+
const staticRes = await tryServeFile(pathMod3.join(__dirname2, StaticPath, path2));
|
|
1091
|
+
if (staticRes)
|
|
1092
|
+
return staticRes;
|
|
1093
|
+
const projectStaticPath = pathMod3.resolve(process.cwd(), "static");
|
|
1094
|
+
if (path2 === "/" || path2 === "") {
|
|
1095
|
+
const indexRes = await tryServeFile(pathMod3.join(projectStaticPath, "index.html"));
|
|
1096
|
+
if (indexRes)
|
|
1097
|
+
return indexRes;
|
|
1098
|
+
}
|
|
1099
|
+
const projectRes = await tryServeFile(pathMod3.join(projectStaticPath, path2));
|
|
1100
|
+
if (projectRes)
|
|
1101
|
+
return projectRes;
|
|
1102
|
+
const ssrComponentPath = pathMod3.join(__dirname2, HadarsFolder, SSR_FILENAME);
|
|
1103
|
+
const importPath = pathToFileURL2(ssrComponentPath).href + `?t=${ssrBuildId}`;
|
|
1104
|
+
const {
|
|
1105
|
+
default: Component,
|
|
1106
|
+
getInitProps,
|
|
1107
|
+
getAfterRenderProps,
|
|
1108
|
+
getFinalProps
|
|
1109
|
+
} = await import(importPath);
|
|
1110
|
+
const { ReactPage, status, headHtml } = await getReactResponse(request, {
|
|
1111
|
+
document: {
|
|
1112
|
+
body: Component,
|
|
1113
|
+
lang: "en",
|
|
1114
|
+
getInitProps,
|
|
1115
|
+
getAfterRenderProps,
|
|
1116
|
+
getFinalProps
|
|
1117
|
+
}
|
|
1118
|
+
});
|
|
1119
|
+
return buildSsrResponse(ReactPage, headHtml, status, getPrecontentHtml, options.streaming === true);
|
|
1120
|
+
}, options.websocket);
|
|
1121
|
+
};
|
|
1122
|
+
var build = async (options) => {
|
|
1123
|
+
validateOptions(options);
|
|
1124
|
+
const entry = pathMod3.resolve(__dirname2, options.entry);
|
|
1125
|
+
const packageDir2 = pathMod3.dirname(fileURLToPath2(import.meta.url));
|
|
1126
|
+
const clientScriptPath2 = pathMod3.resolve(packageDir2, "utils", "clientScript.js");
|
|
1127
|
+
const headPath = pathMod3.resolve(packageDir2, "utils", "Head");
|
|
1128
|
+
let clientScript = "";
|
|
1129
|
+
try {
|
|
1130
|
+
clientScript = (await fs.readFile(clientScriptPath2, "utf-8")).replace("$_MOD_PATH$", entry + getSuffix(options.mode)).replace("$_HEAD_PATH$", headPath);
|
|
1131
|
+
} catch (err) {
|
|
1132
|
+
const srcClientPath = pathMod3.resolve(packageDir2, "utils", "clientScript.tsx");
|
|
1133
|
+
clientScript = (await fs.readFile(srcClientPath, "utf-8")).replace("$_MOD_PATH$", entry + `?v=${Date.now()}`).replace("$_HEAD_PATH$", pathMod3.resolve(packageDir2, "utils", "Head"));
|
|
1134
|
+
}
|
|
1135
|
+
const tmpFilePath = pathMod3.join(os.tmpdir(), `hadars-client-${Date.now()}.tsx`);
|
|
1136
|
+
await fs.writeFile(tmpFilePath, clientScript);
|
|
1137
|
+
const randomStr = crypto.randomBytes(6).toString("hex");
|
|
1138
|
+
console.log("Building client and server bundles in parallel...");
|
|
1139
|
+
await Promise.all([
|
|
1140
|
+
compileEntry(tmpFilePath, {
|
|
1141
|
+
target: "web",
|
|
1142
|
+
output: {
|
|
1143
|
+
filename: `index-${randomStr}.js`,
|
|
1144
|
+
path: pathMod3.resolve(__dirname2, StaticPath)
|
|
1145
|
+
},
|
|
1146
|
+
base: options.baseURL,
|
|
1147
|
+
mode: "production",
|
|
1148
|
+
swcPlugins: options.swcPlugins,
|
|
1149
|
+
define: options.define
|
|
1150
|
+
}),
|
|
1151
|
+
compileEntry(pathMod3.resolve(__dirname2, options.entry), {
|
|
1152
|
+
output: {
|
|
1153
|
+
iife: false,
|
|
1154
|
+
filename: SSR_FILENAME,
|
|
1155
|
+
path: pathMod3.resolve(__dirname2, HadarsFolder),
|
|
1156
|
+
publicPath: "",
|
|
1157
|
+
library: { type: "module" }
|
|
1158
|
+
},
|
|
1159
|
+
base: options.baseURL,
|
|
1160
|
+
target: "node",
|
|
1161
|
+
mode: "production",
|
|
1162
|
+
swcPlugins: options.swcPlugins,
|
|
1163
|
+
define: options.define
|
|
1164
|
+
})
|
|
1165
|
+
]);
|
|
1166
|
+
await fs.rm(tmpFilePath);
|
|
1167
|
+
await fs.writeFile(
|
|
1168
|
+
pathMod3.join(__dirname2, HadarsFolder, "hadars.json"),
|
|
1169
|
+
JSON.stringify({ buildId: randomStr })
|
|
1170
|
+
);
|
|
1171
|
+
console.log("Build complete.");
|
|
1172
|
+
};
|
|
1173
|
+
var run = async (options) => {
|
|
1174
|
+
validateOptions(options);
|
|
1175
|
+
let { port = 9090, workers = 1 } = options;
|
|
1176
|
+
if (isNode && workers > 1 && cluster.isPrimary) {
|
|
1177
|
+
console.log(`[hadars] Starting ${workers} worker processes on port ${port}`);
|
|
1178
|
+
for (let i = 0; i < workers; i++) {
|
|
1179
|
+
cluster.fork();
|
|
1180
|
+
}
|
|
1181
|
+
cluster.on("exit", (worker, code, signal) => {
|
|
1182
|
+
console.warn(`[hadars] Worker ${worker.process.pid} exited (${signal ?? code}), restarting...`);
|
|
1183
|
+
cluster.fork();
|
|
1184
|
+
});
|
|
1185
|
+
await new Promise(() => {
|
|
1186
|
+
});
|
|
1187
|
+
return;
|
|
1188
|
+
}
|
|
1189
|
+
const handleProxy = createProxyHandler(options);
|
|
1190
|
+
const handleWS = upgradeHandler(options);
|
|
1191
|
+
const handler = options.fetch;
|
|
1192
|
+
console.log(`Starting Hadars (run) on port ${port}`);
|
|
1193
|
+
let renderPool;
|
|
1194
|
+
if (!isNode && workers > 1) {
|
|
1195
|
+
const packageDir2 = pathMod3.dirname(fileURLToPath2(import.meta.url));
|
|
1196
|
+
const workerJs = pathMod3.resolve(packageDir2, "ssr-render-worker.js");
|
|
1197
|
+
const workerTs = pathMod3.resolve(packageDir2, "src", "ssr-render-worker.ts");
|
|
1198
|
+
const workerFile = existsSync2(workerJs) ? workerJs : workerTs;
|
|
1199
|
+
const ssrBundlePath = pathMod3.resolve(__dirname2, HadarsFolder, SSR_FILENAME);
|
|
1200
|
+
renderPool = new RenderWorkerPool(workerFile, workers, ssrBundlePath);
|
|
1201
|
+
console.log(`[hadars] SSR render pool: ${workers} worker threads`);
|
|
1202
|
+
}
|
|
1203
|
+
const getPrecontentHtml = makePrecontentHtmlGetter(
|
|
1204
|
+
fs.readFile(pathMod3.join(__dirname2, StaticPath, "out.html"), "utf-8")
|
|
1205
|
+
);
|
|
1206
|
+
await serve(port, async (req, ctx) => {
|
|
1207
|
+
const request = parseRequest(req);
|
|
1208
|
+
if (handler) {
|
|
1209
|
+
const res = await handler(request);
|
|
1210
|
+
if (res)
|
|
1211
|
+
return res;
|
|
1212
|
+
}
|
|
1213
|
+
if (handleWS && handleWS(request, ctx))
|
|
1214
|
+
return void 0;
|
|
1215
|
+
const proxied = await handleProxy(request);
|
|
1216
|
+
if (proxied)
|
|
1217
|
+
return proxied;
|
|
1218
|
+
const url = new URL(request.url);
|
|
1219
|
+
const path2 = url.pathname;
|
|
1220
|
+
const staticRes = await tryServeFile(pathMod3.join(__dirname2, StaticPath, path2));
|
|
1221
|
+
if (staticRes)
|
|
1222
|
+
return staticRes;
|
|
1223
|
+
if (path2 === "/" || path2 === "") {
|
|
1224
|
+
const indexRes = await tryServeFile(pathMod3.join(__dirname2, StaticPath, "index.html"));
|
|
1225
|
+
if (indexRes)
|
|
1226
|
+
return indexRes;
|
|
1227
|
+
}
|
|
1228
|
+
const projectStaticPath = pathMod3.resolve(process.cwd(), "static");
|
|
1229
|
+
const projectRes = await tryServeFile(pathMod3.join(projectStaticPath, path2));
|
|
1230
|
+
if (projectRes)
|
|
1231
|
+
return projectRes;
|
|
1232
|
+
const routeClean = path2.replace(/(^\/|\/$)/g, "");
|
|
1233
|
+
if (routeClean) {
|
|
1234
|
+
const routeRes = await tryServeFile(
|
|
1235
|
+
pathMod3.join(__dirname2, StaticPath, routeClean, "index.html")
|
|
1236
|
+
);
|
|
1237
|
+
if (routeRes)
|
|
1238
|
+
return routeRes;
|
|
1239
|
+
}
|
|
1240
|
+
const componentPath = pathToFileURL2(
|
|
1241
|
+
pathMod3.resolve(__dirname2, HadarsFolder, SSR_FILENAME)
|
|
1242
|
+
).href;
|
|
1243
|
+
const {
|
|
1244
|
+
default: Component,
|
|
1245
|
+
getInitProps,
|
|
1246
|
+
getAfterRenderProps,
|
|
1247
|
+
getFinalProps
|
|
1248
|
+
} = await import(componentPath);
|
|
1249
|
+
const { ReactPage, status, headHtml, renderPayload } = await getReactResponse(request, {
|
|
1250
|
+
document: {
|
|
1251
|
+
body: Component,
|
|
1252
|
+
lang: "en",
|
|
1253
|
+
getInitProps,
|
|
1254
|
+
getAfterRenderProps,
|
|
1255
|
+
getFinalProps
|
|
1256
|
+
}
|
|
1257
|
+
});
|
|
1258
|
+
return buildSsrResponse(ReactPage, headHtml, status, getPrecontentHtml, options.streaming === true, renderPool, renderPayload);
|
|
1259
|
+
}, options.websocket);
|
|
1260
|
+
};
|
|
1261
|
+
|
|
1262
|
+
// cli-lib.ts
|
|
1263
|
+
var SUPPORTED = ["hadars.config.js", "hadars.config.mjs", "hadars.config.cjs", "hadars.config.ts"];
|
|
1264
|
+
function findConfig(cwd) {
|
|
1265
|
+
for (const name of SUPPORTED) {
|
|
1266
|
+
const p = resolve(cwd, name);
|
|
1267
|
+
if (existsSync3(p))
|
|
1268
|
+
return p;
|
|
1269
|
+
}
|
|
1270
|
+
return null;
|
|
1271
|
+
}
|
|
1272
|
+
async function dev2(config) {
|
|
1273
|
+
await dev({
|
|
1274
|
+
...config,
|
|
1275
|
+
baseURL: "",
|
|
1276
|
+
mode: "development"
|
|
1277
|
+
});
|
|
1278
|
+
}
|
|
1279
|
+
async function build2(config) {
|
|
1280
|
+
await build({
|
|
1281
|
+
...config,
|
|
1282
|
+
mode: "production"
|
|
1283
|
+
});
|
|
1284
|
+
}
|
|
1285
|
+
async function run2(config) {
|
|
1286
|
+
await run({
|
|
1287
|
+
...config,
|
|
1288
|
+
mode: "production"
|
|
1289
|
+
});
|
|
1290
|
+
}
|
|
1291
|
+
async function loadConfig(configPath) {
|
|
1292
|
+
const url = `file://${configPath}`;
|
|
1293
|
+
const mod = await import(url);
|
|
1294
|
+
return mod && (mod.default ?? mod);
|
|
1295
|
+
}
|
|
1296
|
+
var TEMPLATES = {
|
|
1297
|
+
"package.json": (name) => JSON.stringify({
|
|
1298
|
+
name,
|
|
1299
|
+
version: "0.1.0",
|
|
1300
|
+
type: "module",
|
|
1301
|
+
private: true,
|
|
1302
|
+
scripts: {
|
|
1303
|
+
dev: "hadars dev",
|
|
1304
|
+
build: "hadars build",
|
|
1305
|
+
start: "hadars run"
|
|
1306
|
+
},
|
|
1307
|
+
dependencies: {
|
|
1308
|
+
"hadars": "latest",
|
|
1309
|
+
react: "^19.0.0",
|
|
1310
|
+
"react-dom": "^19.0.0"
|
|
1311
|
+
}
|
|
1312
|
+
}, null, 2) + "\n",
|
|
1313
|
+
"hadars.config.ts": () => `import type { HadarsOptions } from 'hadars';
|
|
1314
|
+
|
|
1315
|
+
const config: HadarsOptions = {
|
|
1316
|
+
entry: 'src/App.tsx',
|
|
1317
|
+
port: 3000,
|
|
1318
|
+
};
|
|
1319
|
+
|
|
1320
|
+
export default config;
|
|
1321
|
+
`,
|
|
1322
|
+
"tsconfig.json": () => JSON.stringify({
|
|
1323
|
+
compilerOptions: {
|
|
1324
|
+
lib: ["ESNext", "DOM"],
|
|
1325
|
+
target: "ESNext",
|
|
1326
|
+
module: "Preserve",
|
|
1327
|
+
moduleDetection: "force",
|
|
1328
|
+
jsx: "react-jsx",
|
|
1329
|
+
moduleResolution: "bundler",
|
|
1330
|
+
allowImportingTsExtensions: true,
|
|
1331
|
+
verbatimModuleSyntax: true,
|
|
1332
|
+
noEmit: true,
|
|
1333
|
+
strict: true,
|
|
1334
|
+
skipLibCheck: true
|
|
1335
|
+
}
|
|
1336
|
+
}, null, 2) + "\n",
|
|
1337
|
+
".gitignore": () => `node_modules/
|
|
1338
|
+
.hadars/
|
|
1339
|
+
dist/
|
|
1340
|
+
`,
|
|
1341
|
+
"src/App.tsx": () => `import React from 'react';
|
|
1342
|
+
import { HadarsContext, HadarsHead, type HadarsApp } from 'hadars';
|
|
1343
|
+
|
|
1344
|
+
const App: HadarsApp<{}> = ({ context }) => (
|
|
1345
|
+
<HadarsContext context={context}>
|
|
1346
|
+
<HadarsHead status={200}>
|
|
1347
|
+
<title>My App</title>
|
|
1348
|
+
</HadarsHead>
|
|
1349
|
+
<main>
|
|
1350
|
+
<h1>Hello from hadars!</h1>
|
|
1351
|
+
<p>Edit <code>src/App.tsx</code> to get started.</p>
|
|
1352
|
+
</main>
|
|
1353
|
+
</HadarsContext>
|
|
1354
|
+
);
|
|
1355
|
+
|
|
1356
|
+
export default App;
|
|
1357
|
+
`
|
|
1358
|
+
};
|
|
1359
|
+
async function createProject(name, cwd) {
|
|
1360
|
+
const dir = resolve(cwd, name);
|
|
1361
|
+
if (existsSync3(dir)) {
|
|
1362
|
+
console.error(`Directory already exists: ${dir}`);
|
|
1363
|
+
process.exit(1);
|
|
1364
|
+
}
|
|
1365
|
+
console.log(`Creating hadars project in ${dir}`);
|
|
1366
|
+
await mkdir(join(dir, "src"), { recursive: true });
|
|
1367
|
+
for (const [file, template] of Object.entries(TEMPLATES)) {
|
|
1368
|
+
const content = template(name);
|
|
1369
|
+
await writeFile(join(dir, file), content, "utf-8");
|
|
1370
|
+
console.log(` created ${file}`);
|
|
1371
|
+
}
|
|
1372
|
+
console.log(`
|
|
1373
|
+
Done! Next steps:
|
|
1374
|
+
|
|
1375
|
+
cd ${name}
|
|
1376
|
+
npm install # or: bun install / pnpm install
|
|
1377
|
+
npm run dev # or: bun run dev
|
|
1378
|
+
`);
|
|
1379
|
+
}
|
|
1380
|
+
function usage() {
|
|
1381
|
+
console.log("Usage: hadars <new <name> | dev | build | run>");
|
|
1382
|
+
}
|
|
1383
|
+
async function runCli(argv, cwd = process.cwd()) {
|
|
1384
|
+
const cmd = argv[2];
|
|
1385
|
+
if (cmd === "new") {
|
|
1386
|
+
const name = argv[3];
|
|
1387
|
+
if (!name) {
|
|
1388
|
+
console.error("Usage: hadars new <project-name>");
|
|
1389
|
+
process.exit(1);
|
|
1390
|
+
}
|
|
1391
|
+
try {
|
|
1392
|
+
await createProject(name, cwd);
|
|
1393
|
+
} catch (err) {
|
|
1394
|
+
console.error("Failed to create project:", err?.message ?? err);
|
|
1395
|
+
process.exit(2);
|
|
1396
|
+
}
|
|
1397
|
+
return;
|
|
1398
|
+
}
|
|
1399
|
+
if (!cmd || !["dev", "build", "run"].includes(cmd)) {
|
|
1400
|
+
usage();
|
|
1401
|
+
process.exit(1);
|
|
1402
|
+
}
|
|
1403
|
+
const configPath = findConfig(cwd);
|
|
1404
|
+
if (!configPath) {
|
|
1405
|
+
console.log(`No hadars.config.* found in ${cwd}`);
|
|
1406
|
+
console.log("Proceeding with default behavior (no config)");
|
|
1407
|
+
return;
|
|
1408
|
+
}
|
|
1409
|
+
try {
|
|
1410
|
+
const cfg = await loadConfig(configPath);
|
|
1411
|
+
console.log(`Loaded config from ${configPath}`);
|
|
1412
|
+
switch (cmd) {
|
|
1413
|
+
case "dev":
|
|
1414
|
+
console.log("Starting development server...");
|
|
1415
|
+
await dev2(cfg);
|
|
1416
|
+
break;
|
|
1417
|
+
case "build":
|
|
1418
|
+
console.log("Building project...");
|
|
1419
|
+
await build2(cfg);
|
|
1420
|
+
console.log("Build complete");
|
|
1421
|
+
process.exit(0);
|
|
1422
|
+
case "run":
|
|
1423
|
+
console.log("Running project...");
|
|
1424
|
+
await run2(cfg);
|
|
1425
|
+
break;
|
|
1426
|
+
}
|
|
1427
|
+
} catch (err) {
|
|
1428
|
+
console.error("Failed to load config:", err?.message ?? err);
|
|
1429
|
+
process.exit(2);
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
// cli.ts
|
|
1434
|
+
runCli(process.argv).catch((err) => {
|
|
1435
|
+
console.error(err);
|
|
1436
|
+
try {
|
|
1437
|
+
process.exit(1);
|
|
1438
|
+
} catch (_) {
|
|
1439
|
+
throw err;
|
|
1440
|
+
}
|
|
1441
|
+
});
|