odoro 1.0.9 → 2.0.0

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.
Files changed (85) hide show
  1. package/README.md +94 -0
  2. package/client.d.ts +90 -16
  3. package/dist/build-LZAUDJST.js +5 -0
  4. package/dist/{chunk-34RGOZFA.js → chunk-4D56Z7G3.js} +49 -50
  5. package/dist/{chunk-JMEHF3KN.js → chunk-7ZB5MAM6.js} +4 -4
  6. package/dist/{chunk-LHZGX5ML.js → chunk-OMHEQVIW.js} +295 -160
  7. package/dist/{chunk-3SZIN6VG.js → chunk-P3DJITWE.js} +10 -10
  8. package/dist/{chunk-ZVL7EXJO.js → chunk-PRWVZ2SM.js} +22 -14
  9. package/dist/chunk-Q2KCMTB5.js +741 -0
  10. package/dist/chunk-QOVBLN7A.js +241 -0
  11. package/dist/chunk-WWTDEV4Y.js +604 -0
  12. package/dist/cli.d.ts +12 -13
  13. package/dist/cli.js +84 -63
  14. package/dist/{commands-4JRBD55Z.js → commands-EUGVLQWL.js} +44 -45
  15. package/dist/{commands-AHMXBWQQ.js → commands-GYA7NTCX.js} +124 -124
  16. package/dist/{create-CYVSVDAD.js → create-QX5LEWMS.js} +158 -154
  17. package/dist/index.d.ts +603 -151
  18. package/dist/index.js +6 -6
  19. package/dist/{package-EIDLKAA6.js → package-T4Z7OBOH.js} +5 -5
  20. package/dist/preview-P4WY5GEY.js +4 -0
  21. package/dist/registry/index.d.ts +75 -76
  22. package/dist/registry/index.js +1 -1
  23. package/dist/{server-4UGN3SFS.js → server-2WP562EV.js} +3 -3
  24. package/package.json +5 -5
  25. package/templates/react-ts/README.md +71 -35
  26. package/templates/react-ts/_env.example +9 -0
  27. package/templates/react-ts/_variants/with-engine/src/background.tsx +118 -0
  28. package/templates/react-ts/_variants/without-libs/src/App.tsx +380 -0
  29. package/templates/react-ts/_variants/without-libs/src/background.tsx +24 -0
  30. package/templates/react-ts/_variants/without-libs/src/entry-server.tsx +39 -0
  31. package/templates/react-ts/_variants/without-libs/src/main.tsx +26 -0
  32. package/templates/{react-ts-server/_variantes/sans-libs/client → react-ts/_variants/without-libs}/src/styles.css +137 -137
  33. package/templates/react-ts/{_variantes/sans-routeur → _variants/without-router}/src/App.tsx +141 -141
  34. package/templates/react-ts/_variants/without-router/src/entry-server.tsx +39 -0
  35. package/templates/react-ts/index.html +4 -4
  36. package/templates/react-ts/odoro.config.ts +5 -0
  37. package/templates/react-ts/src/App.tsx +219 -209
  38. package/templates/react-ts/src/background.tsx +66 -0
  39. package/templates/react-ts/src/entry-server.tsx +85 -0
  40. package/templates/react-ts/src/main.tsx +13 -4
  41. package/templates/react-ts/src/router.tsx +65 -45
  42. package/templates/react-ts/src/styles.css +5 -5
  43. package/templates/react-ts-server/Dockerfile +9 -9
  44. package/templates/react-ts-server/README.md +97 -45
  45. package/templates/react-ts-server/_env.example +31 -33
  46. package/templates/react-ts-server/_variants/with-engine/client/src/background.tsx +118 -0
  47. package/templates/react-ts-server/_variants/without-libs/client/src/App.tsx +380 -0
  48. package/templates/react-ts-server/_variants/without-libs/client/src/background.tsx +24 -0
  49. package/templates/react-ts-server/_variants/without-libs/client/src/entry-server.tsx +39 -0
  50. package/templates/react-ts-server/_variants/without-libs/client/src/main.tsx +26 -0
  51. package/templates/{react-ts/_variantes/sans-libs → react-ts-server/_variants/without-libs/client}/src/styles.css +137 -137
  52. package/templates/react-ts-server/{_variantes/sans-routeur → _variants/without-router}/client/src/App.tsx +141 -141
  53. package/templates/react-ts-server/_variants/without-router/client/src/entry-server.tsx +39 -0
  54. package/templates/react-ts-server/client/index.html +4 -4
  55. package/templates/react-ts-server/client/src/App.tsx +228 -210
  56. package/templates/react-ts-server/client/src/account.tsx +267 -0
  57. package/templates/react-ts-server/client/src/auth.tsx +139 -0
  58. package/templates/react-ts-server/client/src/background.tsx +66 -0
  59. package/templates/react-ts-server/client/src/entry-server.tsx +92 -0
  60. package/templates/react-ts-server/client/src/main.tsx +17 -5
  61. package/templates/react-ts-server/client/src/router.tsx +77 -45
  62. package/templates/react-ts-server/client/src/styles.css +5 -5
  63. package/templates/react-ts-server/odoro.config.ts +7 -4
  64. package/templates/react-ts-server/package.json +2 -0
  65. package/templates/react-ts-server/scripts/dev.mjs +14 -14
  66. package/templates/react-ts-server/server/src/main.ts +119 -45
  67. package/templates/react-ts-server/server/src/modules/auth/index.ts +238 -0
  68. package/templates/react-ts-server/server/src/modules/auth/password.ts +122 -0
  69. package/templates/react-ts-server/server/src/modules/auth/store.ts +193 -0
  70. package/templates/react-ts-server/server/src/modules/health/index.ts +45 -46
  71. package/dist/build-JFQHODAT.js +0 -5
  72. package/dist/chunk-22KJTV2R.js +0 -380
  73. package/dist/chunk-DL3NPC4H.js +0 -113
  74. package/dist/chunk-TQUJ3MFS.js +0 -268
  75. package/dist/preview-7DKEAQOQ.js +0 -4
  76. package/templates/react-ts/_variantes/avec-moteur/src/fond.tsx +0 -118
  77. package/templates/react-ts/_variantes/sans-libs/src/App.tsx +0 -382
  78. package/templates/react-ts/_variantes/sans-libs/src/fond.tsx +0 -23
  79. package/templates/react-ts/_variantes/sans-libs/src/main.tsx +0 -17
  80. package/templates/react-ts/src/fond.tsx +0 -66
  81. package/templates/react-ts-server/_variantes/avec-moteur/client/src/fond.tsx +0 -118
  82. package/templates/react-ts-server/_variantes/sans-libs/client/src/App.tsx +0 -382
  83. package/templates/react-ts-server/_variantes/sans-libs/client/src/fond.tsx +0 -23
  84. package/templates/react-ts-server/_variantes/sans-libs/client/src/main.tsx +0 -17
  85. package/templates/react-ts-server/client/src/fond.tsx +0 -66
@@ -0,0 +1,741 @@
1
+ #!/usr/bin/env node
2
+ import { extractEntries, cssProviderFor } from './chunk-OMHEQVIW.js';
3
+ import { transformHtmlWith, sourcePlugin, esbuildPluginsFrom, applyAlias, isBareSpecifier, readSuffix, textModule, urlModule, workerModule } from './chunk-WWTDEV4Y.js';
4
+ import { info, colors, size, success, duration } from './chunk-7ZB5MAM6.js';
5
+ import { existsSync } from 'fs';
6
+ import { mkdir, writeFile, readFile, rm, cp, stat } from 'fs/promises';
7
+ import { relative, resolve, dirname, join, basename, sep } from 'path';
8
+ import { gzipSync } from 'zlib';
9
+ import { build } from 'esbuild';
10
+ import { pathToFileURL } from 'url';
11
+
12
+ // src/build/prune.ts
13
+ var GROUPS = /* @__PURE__ */ new Set(["media", "supports", "layer", "container", "scope"]);
14
+ function parseCss(css) {
15
+ const nodes = [];
16
+ let i = 0;
17
+ let start = 0;
18
+ function advance() {
19
+ const c = css[i];
20
+ if (c === "\\") {
21
+ i += 2;
22
+ return;
23
+ }
24
+ if (c === "/" && css[i + 1] === "*") {
25
+ const end = css.indexOf("*/", i + 2);
26
+ i = end === -1 ? css.length : end + 2;
27
+ return;
28
+ }
29
+ if (c === '"' || c === "'") {
30
+ i += 1;
31
+ while (i < css.length && css[i] !== c) {
32
+ i += css[i] === "\\" ? 2 : 1;
33
+ }
34
+ i += 1;
35
+ return;
36
+ }
37
+ i += 1;
38
+ }
39
+ while (i < css.length) {
40
+ const c = css[i];
41
+ if (c === "{") {
42
+ const prelude = css.slice(start, i).trim();
43
+ let depth = 1;
44
+ i += 1;
45
+ const bodyStart = i;
46
+ while (i < css.length && depth > 0) {
47
+ const d = css[i];
48
+ if (d === "{") {
49
+ depth += 1;
50
+ i += 1;
51
+ } else if (d === "}") {
52
+ depth -= 1;
53
+ i += 1;
54
+ } else {
55
+ advance();
56
+ }
57
+ }
58
+ const body = css.slice(bodyStart, i - 1);
59
+ if (prelude.startsWith("@")) {
60
+ const name = /^@([a-zA-Z-]+)/.exec(prelude)?.[1] ?? "";
61
+ nodes.push(
62
+ GROUPS.has(name) ? { kind: "group", prelude, children: parseCss(body) } : (
63
+ // `@keyframes`, `@font-face`, `@property`: copied as they are.
64
+ { kind: "raw", text: `${prelude}{${body}}` }
65
+ )
66
+ );
67
+ } else if (prelude.length > 0) {
68
+ nodes.push({ kind: "rule", selector: prelude, body });
69
+ }
70
+ start = i;
71
+ continue;
72
+ }
73
+ if (c === ";" && css.slice(start, i).trim().startsWith("@")) {
74
+ nodes.push({ kind: "raw", text: `${css.slice(start, i).trim()};` });
75
+ i += 1;
76
+ start = i;
77
+ continue;
78
+ }
79
+ advance();
80
+ }
81
+ return nodes;
82
+ }
83
+ function writeCss(nodes) {
84
+ return nodes.map((node) => {
85
+ if (node.kind === "raw") return node.text;
86
+ if (node.kind === "rule") return `${node.selector}{${node.body}}`;
87
+ return `${node.prelude}{${writeCss(node.children)}}`;
88
+ }).join("\n");
89
+ }
90
+ function classesIn(selector) {
91
+ const classes = [];
92
+ const pattern = /(?<!\\)\.((?:\\.|[\w-])+)/g;
93
+ for (const found of selector.matchAll(pattern)) {
94
+ classes.push(found[1].replaceAll(/\\(.)/g, "$1"));
95
+ }
96
+ return classes;
97
+ }
98
+ function wordsIn(text) {
99
+ const words = /* @__PURE__ */ new Set();
100
+ const pattern = /[A-Za-z0-9_][A-Za-z0-9_:/.[\]%@-]*/g;
101
+ for (const found of text.matchAll(pattern)) words.add(found[0]);
102
+ return words;
103
+ }
104
+ function isUtility(className, prefix) {
105
+ const bare = className.slice(className.lastIndexOf(":") + 1);
106
+ return bare.startsWith(prefix);
107
+ }
108
+ function isKept(className, used, safelist) {
109
+ if (used.has(className)) return true;
110
+ for (const rule of safelist) {
111
+ if (typeof rule === "string" ? rule === className : rule.test(className)) return true;
112
+ }
113
+ return false;
114
+ }
115
+ function prune(css, sources, options = {}) {
116
+ const prefix = options.prefix ?? "o-";
117
+ const safelist = options.safelist ?? [];
118
+ const used = /* @__PURE__ */ new Set();
119
+ for (const source of sources) {
120
+ for (const word of wordsIn(source)) used.add(word);
121
+ }
122
+ const seen = /* @__PURE__ */ new Set();
123
+ const survivors = /* @__PURE__ */ new Set();
124
+ function filterSelector(selector) {
125
+ const kept = splitSelectors(selector).filter((one) => {
126
+ const utilities = classesIn(one).filter((c) => isUtility(c, prefix));
127
+ if (utilities.length === 0) return true;
128
+ for (const className of utilities) seen.add(className);
129
+ const survives = utilities.every((c) => isKept(c, used, safelist));
130
+ if (survives) for (const className of utilities) survivors.add(className);
131
+ return survives;
132
+ });
133
+ return kept.length === 0 ? void 0 : kept.join(",");
134
+ }
135
+ function filterNodes(nodes) {
136
+ const kept = [];
137
+ for (const node of nodes) {
138
+ if (node.kind === "raw") {
139
+ kept.push(node);
140
+ continue;
141
+ }
142
+ if (node.kind === "rule") {
143
+ const selector = filterSelector(node.selector);
144
+ if (selector !== void 0) kept.push({ ...node, selector });
145
+ continue;
146
+ }
147
+ const children = filterNodes(node.children);
148
+ if (children.length > 0) kept.push({ ...node, children });
149
+ }
150
+ return kept;
151
+ }
152
+ const pruned = writeCss(filterNodes(parseCss(css)));
153
+ return {
154
+ css: pruned,
155
+ bytesBefore: Buffer.byteLength(css),
156
+ bytesAfter: Buffer.byteLength(pruned),
157
+ kept: survivors.size,
158
+ removed: seen.size - survivors.size
159
+ };
160
+ }
161
+ function splitSelectors(selector) {
162
+ const parts = [];
163
+ let depth = 0;
164
+ let start = 0;
165
+ let i = 0;
166
+ while (i < selector.length) {
167
+ const c = selector[i];
168
+ if (c === "\\") {
169
+ i += 2;
170
+ continue;
171
+ }
172
+ if (c === '"' || c === "'") {
173
+ i += 1;
174
+ while (i < selector.length && selector[i] !== c) {
175
+ i += selector[i] === "\\" ? 2 : 1;
176
+ }
177
+ i += 1;
178
+ continue;
179
+ }
180
+ if (c === "(" || c === "[") depth += 1;
181
+ else if (c === ")" || c === "]") depth -= 1;
182
+ else if (c === "," && depth === 0) {
183
+ parts.push(selector.slice(start, i).trim());
184
+ start = i + 1;
185
+ }
186
+ i += 1;
187
+ }
188
+ parts.push(selector.slice(start).trim());
189
+ return parts.filter((part) => part.length > 0);
190
+ }
191
+
192
+ // src/build/alias-plugin.ts
193
+ function aliasPlugin(config) {
194
+ return {
195
+ name: "odoro-alias",
196
+ setup(builder) {
197
+ builder.onResolve({ filter: /.*/ }, (args) => {
198
+ if (args.kind === "entry-point") return null;
199
+ const aliased = applyAlias(args.path, config);
200
+ if (aliased === args.path || isBareSpecifier(aliased)) return null;
201
+ return builder.resolve(aliased, {
202
+ kind: "import-statement",
203
+ resolveDir: args.resolveDir,
204
+ importer: args.importer,
205
+ pluginData: { aliased: true }
206
+ });
207
+ });
208
+ }
209
+ };
210
+ }
211
+ function toUrlPath(path) {
212
+ return path.split(sep).join("/");
213
+ }
214
+ function outputPath(file, context) {
215
+ return toUrlPath(relative(context.assetsDir, resolve(context.root, file)));
216
+ }
217
+ function buildManifest(result, context) {
218
+ const outputs = result.metafile.outputs;
219
+ const keys = /* @__PURE__ */ new Map();
220
+ for (const [file, meta] of Object.entries(outputs)) {
221
+ if (!file.endsWith(".js")) continue;
222
+ keys.set(
223
+ file,
224
+ meta.entryPoint === void 0 ? outputPath(file, context) : toUrlPath(relative(context.root, resolve(context.root, meta.entryPoint)))
225
+ );
226
+ }
227
+ const entries = new Set((context.entries ?? []).map((entry) => resolve(entry)));
228
+ const manifest = {};
229
+ for (const [file, meta] of Object.entries(outputs)) {
230
+ if (!file.endsWith(".js")) continue;
231
+ const key = keys.get(file);
232
+ if (key === void 0) continue;
233
+ const statics = [];
234
+ const dynamics = [];
235
+ for (const imported of meta.imports) {
236
+ const target = keys.get(imported.path);
237
+ if (target === void 0 || target === key) continue;
238
+ if (imported.kind === "dynamic-import") dynamics.push(target);
239
+ else if (imported.kind === "import-statement") statics.push(target);
240
+ }
241
+ const stylesheets = meta.cssBundle === void 0 ? [] : [outputPath(meta.cssBundle, context)];
242
+ const source = meta.entryPoint === void 0 ? void 0 : resolve(context.root, meta.entryPoint);
243
+ manifest[key] = {
244
+ file: outputPath(file, context),
245
+ ...source === void 0 ? {} : { src: key, ...entries.has(source) ? { isEntry: true } : {} },
246
+ ...stylesheets.length > 0 ? { css: stylesheets } : {},
247
+ ...statics.length > 0 ? { imports: [...new Set(statics)].sort() } : {},
248
+ ...dynamics.length > 0 ? { dynamicImports: [...new Set(dynamics)].sort() } : {}
249
+ };
250
+ }
251
+ return manifest;
252
+ }
253
+ function chunksFor(result, entry, context) {
254
+ const outputs = result.metafile.outputs;
255
+ const seen = /* @__PURE__ */ new Set([entry]);
256
+ const stack = [entry];
257
+ const rendered = [];
258
+ while (stack.length > 0) {
259
+ const current = stack.pop();
260
+ if (current === void 0) continue;
261
+ for (const imported of outputs[current]?.imports ?? []) {
262
+ if (imported.kind !== "import-statement") continue;
263
+ if (!imported.path.endsWith(".js") || seen.has(imported.path)) continue;
264
+ seen.add(imported.path);
265
+ stack.push(imported.path);
266
+ rendered.push(outputPath(imported.path, context));
267
+ }
268
+ }
269
+ return rendered.sort();
270
+ }
271
+ function assetUrls(result, context, base) {
272
+ const rendered = /* @__PURE__ */ new Map();
273
+ for (const [file, meta] of Object.entries(result.metafile.outputs)) {
274
+ if (file.endsWith(".js") || file.endsWith(".css") || file.endsWith(".map")) {
275
+ continue;
276
+ }
277
+ const sources = Object.keys(meta.inputs);
278
+ if (sources.length !== 1) continue;
279
+ const source = sources[0];
280
+ if (source === void 0) continue;
281
+ rendered.set(
282
+ resolve(context.root, source),
283
+ `${base}assets/${outputPath(file, context)}`
284
+ );
285
+ }
286
+ return rendered;
287
+ }
288
+ var WORKER_UNAVAILABLE = `export default class OdoroWorker {
289
+ constructor() {
290
+ throw new Error(
291
+ '[odoro] A worker does not run during prerendering. ' +
292
+ 'Construct it from an effect, not at module load.',
293
+ )
294
+ }
295
+ }
296
+ `;
297
+ var NAMESPACES = {
298
+ worker: "odoro-worker",
299
+ raw: "odoro-raw",
300
+ url: "odoro-url"
301
+ };
302
+ async function buildWorker(file, config, outputs) {
303
+ const assetsDir = join(config.outDir, "assets");
304
+ const result = await build({
305
+ entryPoints: [file],
306
+ bundle: true,
307
+ format: "esm",
308
+ platform: "browser",
309
+ target: config.build.target,
310
+ minify: config.build.minify,
311
+ sourcemap: config.build.sourcemap,
312
+ metafile: true,
313
+ outdir: assetsDir,
314
+ absWorkingDir: config.root,
315
+ entryNames: "[name]-[hash]",
316
+ chunkNames: "chunk-[hash]",
317
+ assetNames: "[name]-[hash]",
318
+ logLevel: "silent",
319
+ define: {
320
+ "import.meta.env": JSON.stringify(config.envClient),
321
+ "process.env.NODE_ENV": JSON.stringify("production"),
322
+ ...config.define
323
+ },
324
+ plugins: [
325
+ aliasPlugin(config),
326
+ sourcePlugin({ root: config.root, plugins: config.plugins, dev: false }),
327
+ ...esbuildPluginsFrom(config.plugins)
328
+ ]
329
+ });
330
+ let entry;
331
+ for (const [output, meta] of Object.entries(result.metafile.outputs)) {
332
+ outputs.add(resolve(config.root, output));
333
+ if (meta.entryPoint === void 0 || !output.endsWith(".js")) continue;
334
+ if (resolve(config.root, meta.entryPoint) !== resolve(file)) continue;
335
+ entry = relative(assetsDir, resolve(config.root, output)).split(sep).join("/");
336
+ }
337
+ if (entry !== void 0) return entry;
338
+ throw new Error(`[odoro] The build of worker "${basename(file)}" produced nothing.`);
339
+ }
340
+ function assetsPlugin(config, options) {
341
+ const built = /* @__PURE__ */ new Map();
342
+ return {
343
+ name: "odoro-assets",
344
+ setup(builder) {
345
+ builder.onResolve({ filter: /\?(worker|raw|url)$/ }, async (args) => {
346
+ const read = readSuffix(args.path);
347
+ if (read === void 0) return null;
348
+ const resolved = await builder.resolve(read.path, {
349
+ kind: "import-statement",
350
+ resolveDir: args.resolveDir,
351
+ importer: args.importer
352
+ });
353
+ if (resolved.errors.length > 0) return { errors: resolved.errors };
354
+ return { path: resolved.path, namespace: NAMESPACES[read.suffix] };
355
+ });
356
+ builder.onLoad({ filter: /.*/, namespace: NAMESPACES.raw }, async (args) => ({
357
+ contents: textModule(await readFile(args.path, "utf8")),
358
+ loader: "js"
359
+ }));
360
+ builder.onLoad({ filter: /.*/, namespace: NAMESPACES.url }, async (args) => {
361
+ if (options.ssr === true) {
362
+ return {
363
+ contents: urlModule(options.urls?.get(resolve(args.path)) ?? ""),
364
+ loader: "js"
365
+ };
366
+ }
367
+ return { contents: await readFile(args.path), loader: "file" };
368
+ });
369
+ builder.onLoad({ filter: /.*/, namespace: NAMESPACES.worker }, async (args) => {
370
+ if (options.ssr === true) {
371
+ return { contents: WORKER_UNAVAILABLE, loader: "js" };
372
+ }
373
+ let building = built.get(args.path);
374
+ if (building === void 0) {
375
+ building = buildWorker(args.path, config, options.outputs);
376
+ built.set(args.path, building);
377
+ }
378
+ return {
379
+ contents: workerModule(`${config.base}assets/${await building}`),
380
+ loader: "js"
381
+ };
382
+ });
383
+ }
384
+ };
385
+ }
386
+
387
+ // src/build/prerender.ts
388
+ var CONTAINER = /(<div\b[^>]*\bid=["']([^"']+)["'][^>]*>)(\s*)(<\/div>)/i;
389
+ var TITLE = /[ \t]*<title\b[^>]*>[\s\S]*?<\/title>\n?/i;
390
+ function applyHead(page, head) {
391
+ if (head === "") return page;
392
+ const document = /<title\b/i.test(head) ? page.replace(TITLE, "") : page;
393
+ if (!document.includes("</head>")) return `${head}
394
+ ${document}`;
395
+ return document.replace("</head>", ` ${head}
396
+ </head>`);
397
+ }
398
+ async function loadEntry(config, assets) {
399
+ const settings = config.build.prerender;
400
+ if (settings === void 0) throw new Error("[odoro] Prerendering not requested.");
401
+ if (!existsSync(settings.entry)) {
402
+ throw new Error(
403
+ `[odoro] Prerendering requested, but "${settings.entry}" does not exist. This file must export \`render(url)\`, and may export \`routes\`.`
404
+ );
405
+ }
406
+ const directory = join(config.root, "node_modules", ".odoro");
407
+ const output = join(directory, `entry-server.${Date.now().toString(36)}.mjs`);
408
+ await mkdir(directory, { recursive: true });
409
+ const result = await build({
410
+ entryPoints: [settings.entry],
411
+ bundle: true,
412
+ format: "esm",
413
+ platform: "node",
414
+ target: "node20",
415
+ write: false,
416
+ jsx: "automatic",
417
+ logLevel: "silent",
418
+ absWorkingDir: config.root,
419
+ // Packages stay external: React must be **the one of the project**,
420
+ // otherwise two copies coexist and the render fails on invalid hooks.
421
+ packages: "external",
422
+ define: {
423
+ "import.meta.env": JSON.stringify({ ...config.envClient, SSR: true }),
424
+ "process.env.NODE_ENV": JSON.stringify("production"),
425
+ ...config.define
426
+ },
427
+ plugins: [
428
+ {
429
+ name: "odoro-server",
430
+ setup(builder) {
431
+ builder.onResolve({ filter: /\.css$/ }, () => ({
432
+ path: "odoro-empty",
433
+ namespace: "odoro-empty"
434
+ }));
435
+ builder.onLoad({ filter: /.*/, namespace: "odoro-empty" }, () => ({
436
+ contents: 'export default ""',
437
+ loader: "js"
438
+ }));
439
+ builder.onResolve(
440
+ { filter: /\.(svg|png|jpe?g|gif|webp|avif|ico|woff2?|mp4|webm)$/ },
441
+ async (args) => {
442
+ if (args.kind === "entry-point") return null;
443
+ const target = await builder.resolve(applyAlias(args.path, config), {
444
+ kind: "import-statement",
445
+ resolveDir: args.resolveDir,
446
+ importer: args.importer
447
+ });
448
+ if (target.errors.length > 0) return null;
449
+ return { path: target.path, namespace: "odoro-static-url" };
450
+ }
451
+ );
452
+ builder.onLoad({ filter: /.*/, namespace: "odoro-static-url" }, (args) => ({
453
+ contents: `export default ${JSON.stringify(
454
+ assets.urls?.get(args.path) ?? ""
455
+ )}`,
456
+ loader: "js"
457
+ }));
458
+ builder.onResolve({ filter: /.*/ }, (args) => {
459
+ if (args.kind === "entry-point") return null;
460
+ const alias = applyAlias(args.path, config);
461
+ if (alias === args.path || isBareSpecifier(alias)) return null;
462
+ return builder.resolve(alias, {
463
+ kind: "import-statement",
464
+ resolveDir: args.resolveDir,
465
+ importer: args.importer
466
+ });
467
+ });
468
+ }
469
+ },
470
+ assetsPlugin(config, { ...assets, ssr: true }),
471
+ sourcePlugin({
472
+ root: config.root,
473
+ plugins: config.plugins,
474
+ dev: false,
475
+ ssr: true
476
+ }),
477
+ ...esbuildPluginsFrom(config.plugins)
478
+ ]
479
+ });
480
+ const code = result.outputFiles[0]?.text;
481
+ if (code === void 0) {
482
+ throw new Error(`[odoro] "${settings.entry}" produced no code.`);
483
+ }
484
+ await writeFile(output, code, "utf8");
485
+ return {
486
+ module: await import(pathToFileURL(output).href),
487
+ cleanup: () => rm(output, { force: true })
488
+ };
489
+ }
490
+ function fileFor(route, outDir) {
491
+ const clean = route.split("?")[0]?.split("#")[0] ?? route;
492
+ const trimmed = clean.replace(/^\/+/, "").replace(/\/+$/, "");
493
+ return trimmed === "" ? join(outDir, "index.html") : join(outDir, trimmed, "index.html");
494
+ }
495
+ async function prerender(config, document, assets = { outputs: /* @__PURE__ */ new Set() }) {
496
+ const settings = config.build.prerender;
497
+ if (settings === void 0) return { pages: [] };
498
+ const { module, cleanup } = await loadEntry(config, assets);
499
+ try {
500
+ const render = module.render;
501
+ if (typeof render !== "function") {
502
+ throw new Error(`[odoro] "${settings.entry}" must export a "render(url)" function.`);
503
+ }
504
+ const routes = settings.routes.length > 0 ? settings.routes : module.routes ?? ["/"];
505
+ if (!CONTAINER.test(document)) {
506
+ throw new Error(
507
+ '[odoro] No empty container in "index.html": the prerender does not know where to write. Expected a tag such as <div id="root"></div>.'
508
+ );
509
+ }
510
+ const pages = [];
511
+ for (const route of routes) {
512
+ const output = await render(route);
513
+ const html = typeof output === "string" ? output : output.html;
514
+ const head = typeof output === "string" ? void 0 : output.head;
515
+ let page = document.replace(
516
+ CONTAINER,
517
+ (_whole, opening, _id, _blank, closing) => `${opening}${html}${closing}`
518
+ );
519
+ if (head !== void 0) page = applyHead(page, head);
520
+ page = await transformHtmlWith(config.plugins, page, { dev: false, route });
521
+ const file = fileFor(route, config.outDir);
522
+ await mkdir(dirname(file), { recursive: true });
523
+ await writeFile(file, page, "utf8");
524
+ pages.push(resolve(file));
525
+ }
526
+ info(
527
+ ` ${colors.dim("prerendered")} ${String(pages.length)} route${pages.length > 1 ? "s" : ""} \u2014 ${routes.join(", ")}`
528
+ );
529
+ return { pages };
530
+ } finally {
531
+ await cleanup();
532
+ }
533
+ }
534
+
535
+ // src/build/build.ts
536
+ function toPosix(path) {
537
+ return path.split(sep).join("/");
538
+ }
539
+ var COMPRESSIBLE = [".js", ".mjs", ".css", ".html", ".json", ".svg", ".txt", ".md"];
540
+ function outputsForEntry(result, entry, outDir, root) {
541
+ const toRelative = (file) => toPosix(relative(outDir, resolve(root, file)));
542
+ for (const [file, meta] of Object.entries(result.metafile.outputs)) {
543
+ if (meta.entryPoint === void 0) continue;
544
+ if (resolve(root, meta.entryPoint) !== resolve(entry)) continue;
545
+ if (!file.endsWith(".js")) continue;
546
+ return {
547
+ script: toRelative(file),
548
+ // The key of the report, as it is: it is what the chunks are followed by,
549
+ // and converting it would make it impossible to find.
550
+ key: file,
551
+ styles: meta.cssBundle === void 0 ? [] : [toRelative(meta.cssBundle)]
552
+ };
553
+ }
554
+ return { script: void 0, key: void 0, styles: [] };
555
+ }
556
+ async function trimStylesheets(result, config, html) {
557
+ const outputs = Object.keys(result.metafile.outputs).map((f) => resolve(config.root, f));
558
+ const stylesheets = outputs.filter((f) => f.endsWith(".css"));
559
+ if (stylesheets.length === 0) return;
560
+ const sources = [html];
561
+ for (const file of outputs) {
562
+ if (file.endsWith(".js")) sources.push(await readFile(file, "utf8"));
563
+ }
564
+ const provider = await cssProviderFor(config.root);
565
+ const used = /* @__PURE__ */ new Set();
566
+ if (provider !== void 0) {
567
+ for (const source of sources) for (const word of wordsIn(source)) used.add(word);
568
+ for (const kept of config.build.safelist) {
569
+ if (typeof kept === "string") used.add(kept);
570
+ }
571
+ }
572
+ for (const stylesheet of stylesheets) {
573
+ const before = await readFile(stylesheet, "utf8");
574
+ if (provider === void 0) {
575
+ const report = prune(before, sources, { safelist: config.build.safelist });
576
+ await writeFile(stylesheet, report.css, "utf8");
577
+ info(
578
+ ` ${colors.dim("pruning")} ${basename(stylesheet)} ${size(report.bytesBefore)} \u2192 ${size(report.bytesAfter)} ${colors.dim(`${String(report.kept)} classes kept`)}`
579
+ );
580
+ continue;
581
+ }
582
+ const base = prune(before, [], {}).css;
583
+ const utilities = provider.renderUtilitiesFor(used);
584
+ const after = `${base}
585
+ ${utilities}`;
586
+ await writeFile(stylesheet, after, "utf8");
587
+ info(
588
+ ` ${colors.dim("generating")} ${basename(stylesheet)} ${size(Buffer.byteLength(before))} \u2192 ${size(Buffer.byteLength(after))}`
589
+ );
590
+ }
591
+ }
592
+ async function buildProject(config) {
593
+ const started = Date.now();
594
+ const indexFile = join(config.root, "index.html");
595
+ if (!existsSync(indexFile)) {
596
+ throw new Error(`[odoro] No "index.html" at the project root (${config.root}).`);
597
+ }
598
+ const html = await readFile(indexFile, "utf8");
599
+ const entries = extractEntries(html, config.root);
600
+ if (entries.length === 0) {
601
+ throw new Error(
602
+ '[odoro] No entry point: "index.html" must hold a <script type="module" src="...">.'
603
+ );
604
+ }
605
+ await rm(config.outDir, { recursive: true, force: true });
606
+ await mkdir(config.outDir, { recursive: true });
607
+ const sideOutputs = /* @__PURE__ */ new Set();
608
+ const result = await build({
609
+ entryPoints: [...entries],
610
+ bundle: true,
611
+ format: "esm",
612
+ platform: "browser",
613
+ target: config.build.target,
614
+ splitting: true,
615
+ minify: config.build.minify,
616
+ sourcemap: config.build.sourcemap,
617
+ metafile: true,
618
+ outdir: join(config.outDir, "assets"),
619
+ absWorkingDir: config.root,
620
+ publicPath: `${config.base}assets`,
621
+ // The hashes make the files immutable: they can be cached indefinitely, and
622
+ // a deployment only invalidates what has changed.
623
+ entryNames: "[name]-[hash]",
624
+ chunkNames: "chunk-[hash]",
625
+ assetNames: "[name]-[hash]",
626
+ jsx: "automatic",
627
+ logLevel: "silent",
628
+ define: {
629
+ "import.meta.env": JSON.stringify(config.envClient),
630
+ "process.env.NODE_ENV": JSON.stringify("production"),
631
+ ...config.define
632
+ },
633
+ loader: {
634
+ ".svg": "file",
635
+ ".png": "file",
636
+ ".jpg": "file",
637
+ ".jpeg": "file",
638
+ ".gif": "file",
639
+ ".webp": "file",
640
+ ".avif": "file",
641
+ ".ico": "file",
642
+ ".woff": "file",
643
+ ".woff2": "file",
644
+ ".mp4": "file",
645
+ ".webm": "file"
646
+ },
647
+ plugins: [
648
+ // The order is that of resolution: the suffixes first, because they
649
+ // change the target; the aliases next; the pass over the sources last, it
650
+ // resolves nothing.
651
+ assetsPlugin(config, { outputs: sideOutputs }),
652
+ aliasPlugin(config),
653
+ sourcePlugin({ root: config.root, plugins: config.plugins, dev: false }),
654
+ ...esbuildPluginsFrom(config.plugins)
655
+ ]
656
+ });
657
+ if (config.build.prune) await trimStylesheets(result, config, html);
658
+ const assetsDir = join(config.outDir, "assets");
659
+ const context = { root: config.root, assetsDir, entries };
660
+ let output = html;
661
+ for (const entry of entries) {
662
+ const { script, key, styles } = outputsForEntry(result, entry, assetsDir, config.root);
663
+ if (script === void 0 || key === void 0) continue;
664
+ const original = new RegExp(
665
+ `<script[^>]*type=["']module["'][^>]*src=["'][^"']*${basename(entry)}["'][^>]*></script>`,
666
+ "i"
667
+ );
668
+ const chunks = config.build.preload ? chunksFor(result, key, context) : [];
669
+ const tags = [
670
+ ...styles.map(
671
+ (style) => `<link rel="stylesheet" href="${config.base}assets/${style}">`
672
+ ),
673
+ ...chunks.map(
674
+ (chunk) => `<link rel="modulepreload" crossorigin href="${config.base}assets/${chunk}">`
675
+ ),
676
+ `<script type="module" crossorigin src="${config.base}assets/${script}"></script>`
677
+ ].join("\n ");
678
+ output = output.replace(original, tags);
679
+ }
680
+ await writeFile(
681
+ join(config.outDir, "index.html"),
682
+ await transformHtmlWith(config.plugins, output, { dev: false, route: "/" }),
683
+ "utf8"
684
+ );
685
+ if (config.build.manifest) {
686
+ await writeFile(
687
+ join(config.outDir, "manifest.json"),
688
+ `${JSON.stringify(buildManifest(result, context), void 0, 2)}
689
+ `,
690
+ "utf8"
691
+ );
692
+ }
693
+ if (existsSync(config.publicDir)) {
694
+ await cp(config.publicDir, config.outDir, { recursive: true });
695
+ }
696
+ if (config.build.prerender !== void 0) {
697
+ await prerender(config, output, {
698
+ outputs: sideOutputs,
699
+ ssr: true,
700
+ urls: assetUrls(result, context, config.base)
701
+ });
702
+ }
703
+ const all = /* @__PURE__ */ new Set([
704
+ ...Object.keys(result.metafile.outputs).map((file) => resolve(config.root, file)),
705
+ ...sideOutputs
706
+ ]);
707
+ const files = (await Promise.all(
708
+ [...all].map(async (path) => {
709
+ const relativePath = toPosix(relative(config.outDir, path));
710
+ const bytes = (await stat(path)).size;
711
+ if (!COMPRESSIBLE.some((extension) => relativePath.endsWith(extension))) {
712
+ return { path: relativePath, bytes };
713
+ }
714
+ return {
715
+ path: relativePath,
716
+ bytes,
717
+ gzip: gzipSync(await readFile(path), { level: 9 }).byteLength
718
+ };
719
+ })
720
+ )).sort((a, b) => b.bytes - a.bytes);
721
+ return { outDir: config.outDir, files, elapsed: Date.now() - started };
722
+ }
723
+ function reportBuild(output, root) {
724
+ const directory = toPosix(relative(root, output.outDir)) || ".";
725
+ let total = 0;
726
+ let compressed = 0;
727
+ for (const file of output.files) {
728
+ if (file.path.endsWith(".map")) continue;
729
+ total += file.bytes;
730
+ compressed += file.gzip ?? file.bytes;
731
+ const weight = file.gzip === void 0 ? size(file.bytes) : `${size(file.bytes)} ${colors.dim("|")} ${size(file.gzip)} gzip`;
732
+ info(
733
+ ` ${colors.dim(`${directory}/`)}${file.path} ${colors.dim(weight)}`
734
+ );
735
+ }
736
+ success(
737
+ `built in ${duration(output.elapsed)} \u2014 ${size(total)} shipped, ${size(compressed)} over the wire` + colors.dim(" (source maps excluded)")
738
+ );
739
+ }
740
+
741
+ export { buildManifest, buildProject, chunksFor, prerender, reportBuild };