odoro 0.1.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.
- package/LICENSE +12 -0
- package/client.d.ts +84 -0
- package/dist/build-SNTGJH2J.js +4 -0
- package/dist/chunk-2YDI5NKV.js +55 -0
- package/dist/chunk-G2WW7N4C.js +1872 -0
- package/dist/chunk-G5QXVBYT.js +1139 -0
- package/dist/chunk-JMEHF3KN.js +35 -0
- package/dist/chunk-PEMYUK2D.js +64 -0
- package/dist/chunk-T42X2NJN.js +98 -0
- package/dist/chunk-T6RLHSCW.js +123 -0
- package/dist/chunk-UGRSODHU.js +154 -0
- package/dist/cli.d.ts +40 -0
- package/dist/cli.js +236 -0
- package/dist/commands-FVAHVVVN.js +793 -0
- package/dist/commands-V44Y5G4F.js +245 -0
- package/dist/create-SH2M722Y.js +288 -0
- package/dist/index.d.ts +354 -0
- package/dist/index.js +7 -0
- package/dist/package-AUINPBEX.js +62 -0
- package/dist/preview-LOAO5Y6V.js +3 -0
- package/dist/registry/index.d.ts +316 -0
- package/dist/registry/index.js +2 -0
- package/dist/server-MZ76LPAG.js +3 -0
- package/package.json +57 -0
- package/templates/react-ts/README.md +52 -0
- package/templates/react-ts/_gitignore +8 -0
- package/templates/react-ts/index.html +13 -0
- package/templates/react-ts/odoro.config.ts +10 -0
- package/templates/react-ts/package.json +23 -0
- package/templates/react-ts/public/favicon.svg +4 -0
- package/templates/react-ts/src/App.tsx +66 -0
- package/templates/react-ts/src/main.tsx +18 -0
- package/templates/react-ts/src/odoro-env.d.ts +1 -0
- package/templates/react-ts/src/routes/About.tsx +19 -0
- package/templates/react-ts/src/routes/Home.tsx +80 -0
- package/templates/react-ts/src/routes/NotFound.tsx +14 -0
- package/templates/react-ts/src/styles.css +13 -0
- package/templates/react-ts/tsconfig.json +25 -0
- package/templates/react-ts-server/Dockerfile +51 -0
- package/templates/react-ts-server/README.md +87 -0
- package/templates/react-ts-server/_dockerignore +8 -0
- package/templates/react-ts-server/_env.example +78 -0
- package/templates/react-ts-server/_gitignore +8 -0
- package/templates/react-ts-server/client/index.html +13 -0
- package/templates/react-ts-server/client/public/favicon.svg +4 -0
- package/templates/react-ts-server/client/src/App.tsx +66 -0
- package/templates/react-ts-server/client/src/main.tsx +18 -0
- package/templates/react-ts-server/client/src/odoro-env.d.ts +1 -0
- package/templates/react-ts-server/client/src/routes/About.tsx +19 -0
- package/templates/react-ts-server/client/src/routes/Home.tsx +148 -0
- package/templates/react-ts-server/client/src/routes/NotFound.tsx +14 -0
- package/templates/react-ts-server/client/src/styles.css +13 -0
- package/templates/react-ts-server/odoro.config.ts +21 -0
- package/templates/react-ts-server/package.json +33 -0
- package/templates/react-ts-server/scripts/dev.mjs +74 -0
- package/templates/react-ts-server/server/src/main.ts +131 -0
- package/templates/react-ts-server/server/src/modules/health/index.ts +144 -0
- package/templates/react-ts-server/server/tsconfig.json +23 -0
- package/templates/react-ts-server/tsconfig.json +27 -0
|
@@ -0,0 +1,1139 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { info, error, success, duration, colors, warn } from './chunk-JMEHF3KN.js';
|
|
3
|
+
import { existsSync, readFileSync, statSync, watch, createReadStream } from 'fs';
|
|
4
|
+
import { readFile, rm, mkdir, writeFile } from 'fs/promises';
|
|
5
|
+
import { createServer, request } from 'http';
|
|
6
|
+
import { resolve, join, dirname, extname, relative } from 'path';
|
|
7
|
+
import { createHash } from 'crypto';
|
|
8
|
+
import { createRequire } from 'module';
|
|
9
|
+
import { fileURLToPath, pathToFileURL } from 'url';
|
|
10
|
+
import { build } from 'esbuild';
|
|
11
|
+
import { transformAsync } from '@babel/core';
|
|
12
|
+
import reactRefreshPlugin from 'react-refresh/babel';
|
|
13
|
+
|
|
14
|
+
// src/dev/client.ts
|
|
15
|
+
var HMR_STREAM_PATH = "/@odoro/hmr";
|
|
16
|
+
var HMR_CLIENT_PATH = "/@odoro/client";
|
|
17
|
+
var HMR_CLIENT_SOURCE = String.raw`
|
|
18
|
+
const OVERLAY_ID = 'odoro-error-overlay'
|
|
19
|
+
|
|
20
|
+
/** Contextes de rechargement, par URL de module. */
|
|
21
|
+
const contexts = new Map()
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Cree le contexte expose a un module via import.meta.hot.
|
|
25
|
+
* @param {string} url
|
|
26
|
+
*/
|
|
27
|
+
export function createHotContext(url) {
|
|
28
|
+
const existing = contexts.get(url)
|
|
29
|
+
if (existing) {
|
|
30
|
+
// Rechargement du meme module : les rappels de la version precedente sont
|
|
31
|
+
// executes puis oublies.
|
|
32
|
+
for (const callback of existing.disposers) {
|
|
33
|
+
try {
|
|
34
|
+
callback(existing.data)
|
|
35
|
+
} catch (cause) {
|
|
36
|
+
console.error('[odoro] echec du nettoyage de', url, cause)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
existing.disposers = []
|
|
40
|
+
existing.acceptors = []
|
|
41
|
+
return existing.api
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const context = { acceptors: [], disposers: [], data: {} }
|
|
45
|
+
|
|
46
|
+
context.api = {
|
|
47
|
+
get data() {
|
|
48
|
+
return context.data
|
|
49
|
+
},
|
|
50
|
+
accept(callback) {
|
|
51
|
+
context.acceptors.push(typeof callback === 'function' ? callback : () => {})
|
|
52
|
+
},
|
|
53
|
+
dispose(callback) {
|
|
54
|
+
context.disposers.push(callback)
|
|
55
|
+
},
|
|
56
|
+
invalidate() {
|
|
57
|
+
location.reload()
|
|
58
|
+
},
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
contexts.set(url, context)
|
|
62
|
+
return context.api
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Recharge un module et notifie ceux qui l'acceptent.
|
|
67
|
+
* @param {string} url
|
|
68
|
+
* @param {number} timestamp
|
|
69
|
+
*/
|
|
70
|
+
async function applyUpdate(url, timestamp) {
|
|
71
|
+
const context = contexts.get(url)
|
|
72
|
+
if (!context || context.acceptors.length === 0) {
|
|
73
|
+
location.reload()
|
|
74
|
+
return
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const acceptors = [...context.acceptors]
|
|
78
|
+
try {
|
|
79
|
+
const module = await import(url + (url.includes('?') ? '&' : '?') + 't=' + timestamp)
|
|
80
|
+
for (const accept of acceptors) accept(module)
|
|
81
|
+
console.log('[odoro] mis a jour', url)
|
|
82
|
+
} catch (cause) {
|
|
83
|
+
console.error('[odoro] echec de la mise a jour de', url, cause)
|
|
84
|
+
location.reload()
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Retire la surcouche d'erreur si elle est affichee. */
|
|
89
|
+
function clearOverlay() {
|
|
90
|
+
document.getElementById(OVERLAY_ID)?.remove()
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Affiche une erreur de compilation par-dessus la page.
|
|
95
|
+
* @param {string} message
|
|
96
|
+
* @param {string | undefined} file
|
|
97
|
+
*/
|
|
98
|
+
function showOverlay(message, file) {
|
|
99
|
+
clearOverlay()
|
|
100
|
+
const overlay = document.createElement('div')
|
|
101
|
+
overlay.id = OVERLAY_ID
|
|
102
|
+
overlay.setAttribute('role', 'alert')
|
|
103
|
+
overlay.style.cssText = [
|
|
104
|
+
'position:fixed',
|
|
105
|
+
'inset:0',
|
|
106
|
+
'z-index:2147483647',
|
|
107
|
+
'padding:2rem',
|
|
108
|
+
'overflow:auto',
|
|
109
|
+
'background:rgba(10,10,16,0.94)',
|
|
110
|
+
'color:#ffd7d7',
|
|
111
|
+
'font:14px/1.6 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace',
|
|
112
|
+
'white-space:pre-wrap',
|
|
113
|
+
].join(';')
|
|
114
|
+
|
|
115
|
+
const title = document.createElement('div')
|
|
116
|
+
title.textContent = file ? 'Erreur de compilation — ' + file : 'Erreur de compilation'
|
|
117
|
+
title.style.cssText = 'font-weight:700;margin-bottom:1rem;color:#ff9d9d'
|
|
118
|
+
|
|
119
|
+
const body = document.createElement('div')
|
|
120
|
+
body.textContent = message
|
|
121
|
+
|
|
122
|
+
const hint = document.createElement('div')
|
|
123
|
+
hint.textContent = 'Corrigez le fichier : cette surcouche disparaitra d elle-meme.'
|
|
124
|
+
hint.style.cssText = 'margin-top:1.5rem;opacity:0.6'
|
|
125
|
+
|
|
126
|
+
overlay.append(title, body, hint)
|
|
127
|
+
document.body.appendChild(overlay)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const source = new EventSource('__HMR_STREAM_PATH__')
|
|
131
|
+
|
|
132
|
+
source.addEventListener('message', (event) => {
|
|
133
|
+
const payload = JSON.parse(event.data)
|
|
134
|
+
|
|
135
|
+
switch (payload.type) {
|
|
136
|
+
case 'connected':
|
|
137
|
+
console.log('[odoro] rechargement a chaud connecte')
|
|
138
|
+
break
|
|
139
|
+
case 'update':
|
|
140
|
+
clearOverlay()
|
|
141
|
+
for (const update of payload.updates) {
|
|
142
|
+
void applyUpdate(update.url, update.timestamp)
|
|
143
|
+
}
|
|
144
|
+
break
|
|
145
|
+
case 'full-reload':
|
|
146
|
+
location.reload()
|
|
147
|
+
break
|
|
148
|
+
case 'error':
|
|
149
|
+
showOverlay(payload.message, payload.file)
|
|
150
|
+
break
|
|
151
|
+
default:
|
|
152
|
+
break
|
|
153
|
+
}
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
source.addEventListener('error', () => {
|
|
157
|
+
// EventSource se reconnecte seul ; on ne signale que la perte prolongee.
|
|
158
|
+
if (source.readyState === EventSource.CLOSED) {
|
|
159
|
+
console.warn('[odoro] connexion de rechargement perdue')
|
|
160
|
+
}
|
|
161
|
+
})
|
|
162
|
+
`.replace("__HMR_STREAM_PATH__", HMR_STREAM_PATH);
|
|
163
|
+
function hotPreamble(url) {
|
|
164
|
+
return `import { createHotContext as __odoroHot } from ${JSON.stringify(
|
|
165
|
+
HMR_CLIENT_PATH
|
|
166
|
+
)}
|
|
167
|
+
import.meta.hot = __odoroHot(${JSON.stringify(url)})
|
|
168
|
+
`;
|
|
169
|
+
}
|
|
170
|
+
var IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
171
|
+
var RESERVED = /* @__PURE__ */ new Set([
|
|
172
|
+
"break",
|
|
173
|
+
"case",
|
|
174
|
+
"catch",
|
|
175
|
+
"class",
|
|
176
|
+
"const",
|
|
177
|
+
"continue",
|
|
178
|
+
"debugger",
|
|
179
|
+
"default",
|
|
180
|
+
"delete",
|
|
181
|
+
"do",
|
|
182
|
+
"else",
|
|
183
|
+
"enum",
|
|
184
|
+
"export",
|
|
185
|
+
"extends",
|
|
186
|
+
"false",
|
|
187
|
+
"finally",
|
|
188
|
+
"for",
|
|
189
|
+
"function",
|
|
190
|
+
"if",
|
|
191
|
+
"import",
|
|
192
|
+
"in",
|
|
193
|
+
"instanceof",
|
|
194
|
+
"new",
|
|
195
|
+
"null",
|
|
196
|
+
"return",
|
|
197
|
+
"super",
|
|
198
|
+
"switch",
|
|
199
|
+
"this",
|
|
200
|
+
"throw",
|
|
201
|
+
"true",
|
|
202
|
+
"try",
|
|
203
|
+
"typeof",
|
|
204
|
+
"var",
|
|
205
|
+
"void",
|
|
206
|
+
"while",
|
|
207
|
+
"with",
|
|
208
|
+
"yield",
|
|
209
|
+
"let",
|
|
210
|
+
"static",
|
|
211
|
+
"await"
|
|
212
|
+
]);
|
|
213
|
+
function isCommonJsFile(file) {
|
|
214
|
+
if (file.endsWith(".mjs")) return false;
|
|
215
|
+
if (file.endsWith(".cjs")) return true;
|
|
216
|
+
let directory = dirname(file);
|
|
217
|
+
for (let depth = 0; depth < 20; depth += 1) {
|
|
218
|
+
const manifest = join(directory, "package.json");
|
|
219
|
+
if (existsSync(manifest)) {
|
|
220
|
+
try {
|
|
221
|
+
const parsed = JSON.parse(readFileSync(manifest, "utf8"));
|
|
222
|
+
return parsed.type !== "module";
|
|
223
|
+
} catch {
|
|
224
|
+
return true;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
const parent = dirname(directory);
|
|
228
|
+
if (parent === directory) break;
|
|
229
|
+
directory = parent;
|
|
230
|
+
}
|
|
231
|
+
return true;
|
|
232
|
+
}
|
|
233
|
+
function inspectDependency(specifier, root) {
|
|
234
|
+
const plain = { specifier, needsInterop: false, namedExports: [] };
|
|
235
|
+
const require2 = createRequire(join(root, "index.js"));
|
|
236
|
+
let resolved;
|
|
237
|
+
try {
|
|
238
|
+
resolved = require2.resolve(specifier);
|
|
239
|
+
} catch {
|
|
240
|
+
return plain;
|
|
241
|
+
}
|
|
242
|
+
if (!isCommonJsFile(resolved)) return plain;
|
|
243
|
+
let loaded;
|
|
244
|
+
try {
|
|
245
|
+
loaded = require2(specifier);
|
|
246
|
+
} catch {
|
|
247
|
+
return plain;
|
|
248
|
+
}
|
|
249
|
+
if (typeof loaded !== "object" && typeof loaded !== "function") return plain;
|
|
250
|
+
if (loaded === null) return plain;
|
|
251
|
+
const namedExports = Object.keys(loaded).filter(
|
|
252
|
+
(key) => key !== "default" && IDENTIFIER.test(key) && !RESERVED.has(key)
|
|
253
|
+
);
|
|
254
|
+
return { specifier, needsInterop: true, namedExports };
|
|
255
|
+
}
|
|
256
|
+
function renderInteropProxy(info2) {
|
|
257
|
+
const lines = [
|
|
258
|
+
`import cjs from ${JSON.stringify(info2.specifier)}`,
|
|
259
|
+
"export default cjs"
|
|
260
|
+
];
|
|
261
|
+
if (info2.namedExports.length > 0) {
|
|
262
|
+
lines.push(`export const { ${info2.namedExports.join(", ")} } = cjs`);
|
|
263
|
+
}
|
|
264
|
+
return `${lines.join("\n")}
|
|
265
|
+
`;
|
|
266
|
+
}
|
|
267
|
+
var DEPS_PREFIX = "/@deps/";
|
|
268
|
+
var INTERNAL_PREFIX = "/@odoro/";
|
|
269
|
+
var STYLE_EXTENSIONS = [".css"];
|
|
270
|
+
var ASSET_EXTENSIONS = [
|
|
271
|
+
".svg",
|
|
272
|
+
".png",
|
|
273
|
+
".jpg",
|
|
274
|
+
".jpeg",
|
|
275
|
+
".gif",
|
|
276
|
+
".webp",
|
|
277
|
+
".avif",
|
|
278
|
+
".ico",
|
|
279
|
+
".woff",
|
|
280
|
+
".woff2",
|
|
281
|
+
".mp4",
|
|
282
|
+
".webm"
|
|
283
|
+
];
|
|
284
|
+
function depFileName(specifier) {
|
|
285
|
+
return `${specifier.replace(/^@/, "").split("/").join("_")}.js`;
|
|
286
|
+
}
|
|
287
|
+
function isBareSpecifier(specifier) {
|
|
288
|
+
return !specifier.startsWith(".") && !specifier.startsWith("/") && !specifier.startsWith("\\") && !/^[a-zA-Z]:[\\/]/.test(specifier) && !specifier.startsWith("data:") && !specifier.startsWith("http:") && !specifier.startsWith("https:");
|
|
289
|
+
}
|
|
290
|
+
function hasExtension(path, extensions) {
|
|
291
|
+
const clean = path.split("?")[0] ?? path;
|
|
292
|
+
return extensions.some((extension) => clean.toLowerCase().endsWith(extension));
|
|
293
|
+
}
|
|
294
|
+
function fileToUrl(file, root) {
|
|
295
|
+
const relativePath = relative(root, file).split("\\").join("/");
|
|
296
|
+
if (!relativePath.startsWith("..")) return `/${relativePath}`;
|
|
297
|
+
return `/@fs/${file.split("\\").join("/").replace(/^\//, "")}`;
|
|
298
|
+
}
|
|
299
|
+
function urlToFile(url, root) {
|
|
300
|
+
const path = (url.split("?")[0] ?? url).split("#")[0] ?? url;
|
|
301
|
+
if (path.startsWith("/@fs/")) {
|
|
302
|
+
const absolute = path.slice("/@fs/".length);
|
|
303
|
+
return /^[a-zA-Z]:/.test(absolute) ? absolute : `/${absolute}`;
|
|
304
|
+
}
|
|
305
|
+
return resolve(root, `.${path}`);
|
|
306
|
+
}
|
|
307
|
+
function applyAlias(specifier, config) {
|
|
308
|
+
for (const [prefix, target] of Object.entries(config.alias)) {
|
|
309
|
+
if (specifier === prefix || specifier.startsWith(`${prefix}/`)) {
|
|
310
|
+
return resolve(
|
|
311
|
+
config.root,
|
|
312
|
+
target,
|
|
313
|
+
specifier.slice(prefix.length).replace(/^\//, "")
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
return specifier;
|
|
318
|
+
}
|
|
319
|
+
function externalizeImports(config, dependencies) {
|
|
320
|
+
return {
|
|
321
|
+
name: "odoro-externalize",
|
|
322
|
+
setup(builder) {
|
|
323
|
+
builder.onResolve({ filter: /.*/ }, async (args) => {
|
|
324
|
+
if (args.kind === "entry-point") return null;
|
|
325
|
+
if (args.pluginData?.resolving === true) {
|
|
326
|
+
return null;
|
|
327
|
+
}
|
|
328
|
+
if (args.path.startsWith(INTERNAL_PREFIX) || args.path.startsWith(DEPS_PREFIX)) {
|
|
329
|
+
return { path: args.path, external: true };
|
|
330
|
+
}
|
|
331
|
+
const aliased = applyAlias(args.path, config);
|
|
332
|
+
const isFileLike = hasExtension(aliased, STYLE_EXTENSIONS) || hasExtension(aliased, ASSET_EXTENSIONS);
|
|
333
|
+
if (isBareSpecifier(aliased) && !isFileLike) {
|
|
334
|
+
return { path: `${DEPS_PREFIX}${depFileName(aliased)}`, external: true };
|
|
335
|
+
}
|
|
336
|
+
const resolved = await builder.resolve(aliased, {
|
|
337
|
+
kind: "import-statement",
|
|
338
|
+
resolveDir: args.resolveDir,
|
|
339
|
+
importer: args.importer,
|
|
340
|
+
pluginData: { resolving: true }
|
|
341
|
+
});
|
|
342
|
+
if (resolved.errors.length > 0) {
|
|
343
|
+
return { path: args.path, external: true };
|
|
344
|
+
}
|
|
345
|
+
dependencies.add(resolved.path);
|
|
346
|
+
const url = fileToUrl(resolved.path, config.root);
|
|
347
|
+
return {
|
|
348
|
+
path: hasExtension(url, ASSET_EXTENSIONS) ? `${url}?import` : url,
|
|
349
|
+
external: true
|
|
350
|
+
};
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
async function transformModule(file, config, env) {
|
|
356
|
+
const dependencies = /* @__PURE__ */ new Set();
|
|
357
|
+
const result = await build({
|
|
358
|
+
entryPoints: [file],
|
|
359
|
+
bundle: true,
|
|
360
|
+
write: false,
|
|
361
|
+
format: "esm",
|
|
362
|
+
platform: "browser",
|
|
363
|
+
target: "es2022",
|
|
364
|
+
sourcemap: "inline",
|
|
365
|
+
jsx: "automatic",
|
|
366
|
+
jsxDev: true,
|
|
367
|
+
logLevel: "silent",
|
|
368
|
+
absWorkingDir: config.root,
|
|
369
|
+
define: {
|
|
370
|
+
"import.meta.env": JSON.stringify(env),
|
|
371
|
+
"process.env.NODE_ENV": JSON.stringify("development"),
|
|
372
|
+
...config.define
|
|
373
|
+
},
|
|
374
|
+
plugins: [externalizeImports(config, dependencies)]
|
|
375
|
+
});
|
|
376
|
+
const code = result.outputFiles[0]?.text;
|
|
377
|
+
if (code === void 0) {
|
|
378
|
+
throw new Error(`[odoro] La compilation de "${file}" n'a produit aucun code.`);
|
|
379
|
+
}
|
|
380
|
+
return { code, dependencies: [...dependencies] };
|
|
381
|
+
}
|
|
382
|
+
function wrapStyle(url, css) {
|
|
383
|
+
return `const id = ${JSON.stringify(`odoro-style:${url}`)}
|
|
384
|
+
const css = ${JSON.stringify(css)}
|
|
385
|
+
|
|
386
|
+
let element = document.querySelector(\`style[data-odoro-id="\${id}"]\`)
|
|
387
|
+
if (element === null) {
|
|
388
|
+
element = document.createElement('style')
|
|
389
|
+
element.setAttribute('data-odoro-id', id)
|
|
390
|
+
document.head.appendChild(element)
|
|
391
|
+
}
|
|
392
|
+
element.textContent = css
|
|
393
|
+
|
|
394
|
+
import.meta.hot?.accept()
|
|
395
|
+
import.meta.hot?.dispose(() => {
|
|
396
|
+
// La feuille suivante recreera l'element : le retirer evite d'empiler les
|
|
397
|
+
// regles mortes a chaque rechargement.
|
|
398
|
+
element?.remove()
|
|
399
|
+
})
|
|
400
|
+
`;
|
|
401
|
+
}
|
|
402
|
+
function wrapAsset(url) {
|
|
403
|
+
return `export default ${JSON.stringify(url)}
|
|
404
|
+
`;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// src/dev/deps.ts
|
|
408
|
+
var MANIFEST = "manifest.json";
|
|
409
|
+
function collectBareImports(config, found) {
|
|
410
|
+
return {
|
|
411
|
+
name: "odoro-scan-deps",
|
|
412
|
+
setup(builder) {
|
|
413
|
+
builder.onResolve({ filter: /.*/ }, (args) => {
|
|
414
|
+
if (args.kind === "entry-point") return null;
|
|
415
|
+
const aliased = applyAlias(args.path, config);
|
|
416
|
+
if (hasExtension(aliased, STYLE_EXTENSIONS) || hasExtension(aliased, ASSET_EXTENSIONS)) {
|
|
417
|
+
return { path: aliased, external: true };
|
|
418
|
+
}
|
|
419
|
+
if (!isBareSpecifier(aliased)) return null;
|
|
420
|
+
found.add(aliased);
|
|
421
|
+
return { path: aliased, external: true };
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
async function scanDependencies(config, entries) {
|
|
427
|
+
const found = /* @__PURE__ */ new Set();
|
|
428
|
+
const existing = entries.filter((entry) => existsSync(entry));
|
|
429
|
+
if (existing.length === 0) return [];
|
|
430
|
+
await build({
|
|
431
|
+
entryPoints: [...existing],
|
|
432
|
+
bundle: true,
|
|
433
|
+
write: false,
|
|
434
|
+
format: "esm",
|
|
435
|
+
platform: "browser",
|
|
436
|
+
logLevel: "silent",
|
|
437
|
+
absWorkingDir: config.root,
|
|
438
|
+
jsx: "automatic",
|
|
439
|
+
// Le serveur compile en JSX de developpement : sans ce reglage, le
|
|
440
|
+
// parcours chercherait `react/jsx-runtime` la ou le navigateur demandera
|
|
441
|
+
// `react/jsx-dev-runtime`, et la dependance manquerait a l'appel.
|
|
442
|
+
jsxDev: true,
|
|
443
|
+
plugins: [collectBareImports(config, found)]
|
|
444
|
+
});
|
|
445
|
+
return [...found].sort();
|
|
446
|
+
}
|
|
447
|
+
async function optimizeDeps(config, specifiers, force = false) {
|
|
448
|
+
const directory = join(config.root, "node_modules", ".odoro", "deps");
|
|
449
|
+
const sorted = [...specifiers].sort();
|
|
450
|
+
const hash = createHash("sha256").update(JSON.stringify(sorted)).update(await lockfileFingerprint(config.root)).update(entriesFingerprint(config.root, sorted)).update(engineVersion()).digest("hex").slice(0, 16);
|
|
451
|
+
const manifestPath = join(directory, MANIFEST);
|
|
452
|
+
if (!force && existsSync(manifestPath)) {
|
|
453
|
+
const previous = JSON.parse(await readFile(manifestPath, "utf8"));
|
|
454
|
+
if (previous.hash === hash) {
|
|
455
|
+
return { directory, specifiers: previous.specifiers, rebuilt: false };
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
await rm(directory, { recursive: true, force: true });
|
|
459
|
+
await mkdir(directory, { recursive: true });
|
|
460
|
+
if (sorted.length > 0) {
|
|
461
|
+
const proxies = join(directory, "proxies");
|
|
462
|
+
await mkdir(proxies, { recursive: true });
|
|
463
|
+
const entryPoints = [];
|
|
464
|
+
for (const specifier of sorted) {
|
|
465
|
+
const out = depFileName(specifier).replace(/\.js$/, "");
|
|
466
|
+
const info2 = inspectDependency(specifier, config.root);
|
|
467
|
+
if (!info2.needsInterop) {
|
|
468
|
+
entryPoints.push({ in: specifier, out });
|
|
469
|
+
continue;
|
|
470
|
+
}
|
|
471
|
+
const proxy = join(proxies, `${out}.js`);
|
|
472
|
+
await writeFile(proxy, renderInteropProxy(info2), "utf8");
|
|
473
|
+
entryPoints.push({ in: proxy, out });
|
|
474
|
+
}
|
|
475
|
+
await build({
|
|
476
|
+
// Les noms de sortie sont imposes : un specificateur a sous-chemin
|
|
477
|
+
// produirait sinon une arborescence, et deux paquets differents
|
|
478
|
+
// pourraient se disputer le meme nom de fichier.
|
|
479
|
+
entryPoints,
|
|
480
|
+
bundle: true,
|
|
481
|
+
format: "esm",
|
|
482
|
+
platform: "browser",
|
|
483
|
+
target: "es2022",
|
|
484
|
+
splitting: true,
|
|
485
|
+
outdir: directory,
|
|
486
|
+
absWorkingDir: config.root,
|
|
487
|
+
logLevel: "silent",
|
|
488
|
+
define: { "process.env.NODE_ENV": JSON.stringify("development") },
|
|
489
|
+
loader: { ".woff": "file", ".woff2": "file", ".svg": "dataurl" }
|
|
490
|
+
});
|
|
491
|
+
await rm(proxies, { recursive: true, force: true });
|
|
492
|
+
}
|
|
493
|
+
const manifest = { hash, specifiers: sorted };
|
|
494
|
+
await writeFile(manifestPath, JSON.stringify(manifest, null, 2), "utf8");
|
|
495
|
+
return { directory, specifiers: sorted, rebuilt: true };
|
|
496
|
+
}
|
|
497
|
+
var cachedVersion;
|
|
498
|
+
function engineVersion() {
|
|
499
|
+
if (cachedVersion !== void 0) return cachedVersion;
|
|
500
|
+
let directory = dirname(fileURLToPath(import.meta.url));
|
|
501
|
+
for (let depth = 0; depth < 6; depth += 1) {
|
|
502
|
+
const manifest = join(directory, "package.json");
|
|
503
|
+
if (existsSync(manifest)) {
|
|
504
|
+
try {
|
|
505
|
+
const parsed = JSON.parse(readFileSync(manifest, "utf8"));
|
|
506
|
+
if (parsed.name === "odoro") {
|
|
507
|
+
cachedVersion = parsed.version ?? "inconnue";
|
|
508
|
+
return cachedVersion;
|
|
509
|
+
}
|
|
510
|
+
} catch {
|
|
511
|
+
break;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
const parent = dirname(directory);
|
|
515
|
+
if (parent === directory) break;
|
|
516
|
+
directory = parent;
|
|
517
|
+
}
|
|
518
|
+
cachedVersion = "inconnue";
|
|
519
|
+
return cachedVersion;
|
|
520
|
+
}
|
|
521
|
+
function entriesFingerprint(root, specifiers) {
|
|
522
|
+
const resolver = createRequire(pathToFileURL(join(root, "package.json")));
|
|
523
|
+
const parts = [];
|
|
524
|
+
for (const name of packageNames(specifiers)) {
|
|
525
|
+
parts.push(`${name}:${packageFingerprint(name, resolver)}`);
|
|
526
|
+
}
|
|
527
|
+
return parts.join("|");
|
|
528
|
+
}
|
|
529
|
+
function packageNames(specifiers) {
|
|
530
|
+
const names = /* @__PURE__ */ new Set();
|
|
531
|
+
for (const specifier of specifiers) {
|
|
532
|
+
const segments = specifier.split("/");
|
|
533
|
+
names.add(
|
|
534
|
+
specifier.startsWith("@") && segments.length > 1 ? `${segments[0] ?? ""}/${segments[1] ?? ""}` : segments[0] ?? specifier
|
|
535
|
+
);
|
|
536
|
+
}
|
|
537
|
+
return [...names].sort();
|
|
538
|
+
}
|
|
539
|
+
function packageFingerprint(name, resolver) {
|
|
540
|
+
let manifestPath;
|
|
541
|
+
try {
|
|
542
|
+
manifestPath = resolver.resolve(`${name}/package.json`);
|
|
543
|
+
} catch {
|
|
544
|
+
return "introuvable";
|
|
545
|
+
}
|
|
546
|
+
let manifest;
|
|
547
|
+
try {
|
|
548
|
+
manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
|
|
549
|
+
} catch {
|
|
550
|
+
return "illisible";
|
|
551
|
+
}
|
|
552
|
+
const directory = dirname(manifestPath);
|
|
553
|
+
const targets = /* @__PURE__ */ new Set();
|
|
554
|
+
const collect = (node) => {
|
|
555
|
+
if (typeof node === "string") {
|
|
556
|
+
if (node.startsWith("./")) targets.add(node);
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
if (typeof node === "object" && node !== null) {
|
|
560
|
+
for (const value of Object.values(node)) collect(value);
|
|
561
|
+
}
|
|
562
|
+
};
|
|
563
|
+
collect(manifest.exports);
|
|
564
|
+
collect(manifest.main);
|
|
565
|
+
collect(manifest.module);
|
|
566
|
+
const parts = [String(statSafe(manifestPath))];
|
|
567
|
+
for (const target of [...targets].sort()) {
|
|
568
|
+
parts.push(`${target}:${String(statSafe(join(directory, target)))}`);
|
|
569
|
+
}
|
|
570
|
+
return parts.join(",");
|
|
571
|
+
}
|
|
572
|
+
function statSafe(path) {
|
|
573
|
+
try {
|
|
574
|
+
const stats = statSync(path);
|
|
575
|
+
return `${String(stats.mtimeMs)}-${String(stats.size)}`;
|
|
576
|
+
} catch {
|
|
577
|
+
return "0";
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
async function lockfileFingerprint(root) {
|
|
581
|
+
for (const name of ["pnpm-lock.yaml", "package-lock.json", "yarn.lock", "bun.lock"]) {
|
|
582
|
+
const file = join(root, name);
|
|
583
|
+
if (!existsSync(file)) continue;
|
|
584
|
+
return createHash("sha256").update(await readFile(file)).digest("hex");
|
|
585
|
+
}
|
|
586
|
+
return "sans-verrou";
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// src/dev/graph.ts
|
|
590
|
+
function detectSelfAccepting(source) {
|
|
591
|
+
return /import\s*\.\s*meta\s*\.\s*hot\s*\??\s*\.\s*accept\s*\(/.test(source);
|
|
592
|
+
}
|
|
593
|
+
var ModuleGraph = class {
|
|
594
|
+
nodes = /* @__PURE__ */ new Map();
|
|
595
|
+
/** Recupere un module, ou le cree s'il est inconnu. */
|
|
596
|
+
ensure(file, url) {
|
|
597
|
+
const existing = this.nodes.get(file);
|
|
598
|
+
if (existing !== void 0) return existing;
|
|
599
|
+
const node = {
|
|
600
|
+
file,
|
|
601
|
+
url,
|
|
602
|
+
importers: /* @__PURE__ */ new Set(),
|
|
603
|
+
imported: /* @__PURE__ */ new Set(),
|
|
604
|
+
selfAccepting: false,
|
|
605
|
+
code: void 0,
|
|
606
|
+
timestamp: Date.now()
|
|
607
|
+
};
|
|
608
|
+
this.nodes.set(file, node);
|
|
609
|
+
return node;
|
|
610
|
+
}
|
|
611
|
+
/** Recupere un module deja connu. */
|
|
612
|
+
get(file) {
|
|
613
|
+
return this.nodes.get(file);
|
|
614
|
+
}
|
|
615
|
+
/** Nombre de modules connus. */
|
|
616
|
+
get size() {
|
|
617
|
+
return this.nodes.size;
|
|
618
|
+
}
|
|
619
|
+
/**
|
|
620
|
+
* Remplace la liste des dependances d'un module, en tenant a jour les
|
|
621
|
+
* relations inverses.
|
|
622
|
+
*/
|
|
623
|
+
setDependencies(file, dependencies) {
|
|
624
|
+
const node = this.nodes.get(file);
|
|
625
|
+
if (node === void 0) return;
|
|
626
|
+
for (const previous of node.imported) {
|
|
627
|
+
if (!dependencies.includes(previous)) {
|
|
628
|
+
this.nodes.get(previous)?.importers.delete(file);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
node.imported.clear();
|
|
632
|
+
for (const dependency of dependencies) {
|
|
633
|
+
node.imported.add(dependency);
|
|
634
|
+
const target = this.nodes.get(dependency);
|
|
635
|
+
if (target !== void 0) target.importers.add(file);
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
/**
|
|
639
|
+
* Invalide un module et remonte la chaine de ses importateurs jusqu'a
|
|
640
|
+
* trouver, sur chaque branche, un module qui accepte les mises a jour.
|
|
641
|
+
*
|
|
642
|
+
* @returns Les modules a recharger cote client. Un tableau vide signifie
|
|
643
|
+
* qu'aucune frontiere n'accepte la mise a jour : il faut recharger la page.
|
|
644
|
+
*
|
|
645
|
+
* @example
|
|
646
|
+
* const boundaries = graph.invalidate('/projet/src/App.css')
|
|
647
|
+
*/
|
|
648
|
+
invalidate(file) {
|
|
649
|
+
const boundaries = [];
|
|
650
|
+
const seen = /* @__PURE__ */ new Set();
|
|
651
|
+
const timestamp = Date.now();
|
|
652
|
+
const walk = (current) => {
|
|
653
|
+
if (seen.has(current)) return true;
|
|
654
|
+
seen.add(current);
|
|
655
|
+
const node = this.nodes.get(current);
|
|
656
|
+
if (node === void 0) return false;
|
|
657
|
+
node.code = void 0;
|
|
658
|
+
node.timestamp = timestamp;
|
|
659
|
+
if (node.selfAccepting) {
|
|
660
|
+
boundaries.push(node);
|
|
661
|
+
return true;
|
|
662
|
+
}
|
|
663
|
+
if (node.importers.size === 0) return false;
|
|
664
|
+
let handled = true;
|
|
665
|
+
for (const importer of node.importers) {
|
|
666
|
+
if (!walk(importer)) handled = false;
|
|
667
|
+
}
|
|
668
|
+
return handled;
|
|
669
|
+
};
|
|
670
|
+
return walk(file) ? boundaries : [];
|
|
671
|
+
}
|
|
672
|
+
/** Oublie tous les modules. */
|
|
673
|
+
clear() {
|
|
674
|
+
this.nodes.clear();
|
|
675
|
+
}
|
|
676
|
+
};
|
|
677
|
+
var REFRESH_RUNTIME_PATH = "/@odoro/react-refresh";
|
|
678
|
+
var CANDIDATE_EXTENSIONS = [".tsx", ".jsx", ".ts", ".js", ".mjs"];
|
|
679
|
+
function isRefreshCandidate(file) {
|
|
680
|
+
const normalized = file.split("\\").join("/");
|
|
681
|
+
if (normalized.includes("/node_modules/")) return false;
|
|
682
|
+
return CANDIDATE_EXTENSIONS.some((extension) => normalized.endsWith(extension));
|
|
683
|
+
}
|
|
684
|
+
async function applyReactRefresh(code, file) {
|
|
685
|
+
const result = await transformAsync(code, {
|
|
686
|
+
filename: file,
|
|
687
|
+
babelrc: false,
|
|
688
|
+
configFile: false,
|
|
689
|
+
// La carte de source produite par la compilation precedente est reprise et
|
|
690
|
+
// fusionnee : sans cela, les numeros de ligne du debogueur designeraient le
|
|
691
|
+
// code instrumente plutot que la source.
|
|
692
|
+
inputSourceMap: true,
|
|
693
|
+
sourceMaps: "inline",
|
|
694
|
+
// `skipEnvCheck` leve un garde-fou destine aux configurations globales,
|
|
695
|
+
// qui refuse la transformation hors de NODE_ENV=development. Ici c'est le
|
|
696
|
+
// point d'application qui garantit la regle : cette fonction n'est appelee
|
|
697
|
+
// que par le serveur de developpement, jamais par la compilation de
|
|
698
|
+
// production. Sans cela, un `NODE_ENV=production odoro dev` — ou une suite
|
|
699
|
+
// de tests — echouerait au lieu de simplement instrumenter.
|
|
700
|
+
plugins: [[reactRefreshPlugin, { skipEnvCheck: true }]],
|
|
701
|
+
parserOpts: { sourceType: "module" }
|
|
702
|
+
});
|
|
703
|
+
return result?.code ?? code;
|
|
704
|
+
}
|
|
705
|
+
function hasRegisteredComponent(code) {
|
|
706
|
+
return code.includes("$RefreshReg$(");
|
|
707
|
+
}
|
|
708
|
+
function refreshPreamble(id) {
|
|
709
|
+
return `import * as __odoroRefresh from ${JSON.stringify(REFRESH_RUNTIME_PATH)}
|
|
710
|
+
const __odoroPrevReg = window.$RefreshReg$
|
|
711
|
+
const __odoroPrevSig = window.$RefreshSig$
|
|
712
|
+
window.$RefreshReg$ = (type, name) => __odoroRefresh.register(type, ${JSON.stringify(id)} + ' ' + name)
|
|
713
|
+
window.$RefreshSig$ = __odoroRefresh.createSignature
|
|
714
|
+
`;
|
|
715
|
+
}
|
|
716
|
+
function refreshEpilogue(id) {
|
|
717
|
+
return `
|
|
718
|
+
window.$RefreshReg$ = __odoroPrevReg
|
|
719
|
+
window.$RefreshSig$ = __odoroPrevSig
|
|
720
|
+
|
|
721
|
+
void import(import.meta.url).then((__odoroCurrent) => {
|
|
722
|
+
__odoroRefresh.registerExports(${JSON.stringify(id)}, __odoroCurrent)
|
|
723
|
+
import.meta.hot?.accept((__odoroNext) => {
|
|
724
|
+
if (!__odoroNext) return
|
|
725
|
+
const refus = __odoroRefresh.checkBoundary(__odoroCurrent, __odoroNext)
|
|
726
|
+
if (refus !== null) import.meta.hot.invalidate(refus)
|
|
727
|
+
else {
|
|
728
|
+
__odoroRefresh.registerExports(${JSON.stringify(id)}, __odoroNext)
|
|
729
|
+
__odoroRefresh.enqueueUpdate()
|
|
730
|
+
}
|
|
731
|
+
})
|
|
732
|
+
})
|
|
733
|
+
`;
|
|
734
|
+
}
|
|
735
|
+
var REFRESH_RUNTIME_SOURCE = `import runtime from 'react-refresh/runtime'
|
|
736
|
+
|
|
737
|
+
// Le crochet doit etre installe **avant** que React ne soit charge : c'est par
|
|
738
|
+
// lui que React signale les composants qu'il rend.
|
|
739
|
+
runtime.injectIntoGlobalHook(window)
|
|
740
|
+
|
|
741
|
+
// Valeurs neutres : un module non instrumente doit pouvoir s'evaluer sans que
|
|
742
|
+
// ces fonctions existent vraiment.
|
|
743
|
+
window.$RefreshReg$ = () => {}
|
|
744
|
+
window.$RefreshSig$ = () => (type) => type
|
|
745
|
+
|
|
746
|
+
export const register = runtime.register
|
|
747
|
+
export const createSignature = runtime.createSignatureFunctionForTransform
|
|
748
|
+
|
|
749
|
+
/**
|
|
750
|
+
* Enregistre les exports d'un module qui ressemblent a des composants.
|
|
751
|
+
*
|
|
752
|
+
* L'enregistrement par nom d'export complete celui pose dans le corps du
|
|
753
|
+
* module : un composant re-exporte depuis un autre fichier n'y apparaitrait
|
|
754
|
+
* pas.
|
|
755
|
+
*/
|
|
756
|
+
export function registerExports(id, exports) {
|
|
757
|
+
for (const key of Object.keys(exports)) {
|
|
758
|
+
const value = exports[key]
|
|
759
|
+
if (runtime.isLikelyComponentType(value)) {
|
|
760
|
+
runtime.register(value, id + ' export ' + key)
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
/**
|
|
766
|
+
* Verifie qu'un module peut etre remplace a chaud.
|
|
767
|
+
*
|
|
768
|
+
* @returns null si le remplacement est sur, sinon la raison du refus.
|
|
769
|
+
*/
|
|
770
|
+
export function checkBoundary(previous, next) {
|
|
771
|
+
const before = Object.keys(previous)
|
|
772
|
+
const after = Object.keys(next)
|
|
773
|
+
|
|
774
|
+
if (after.length === 0) return 'le module n exporte plus rien'
|
|
775
|
+
|
|
776
|
+
for (const key of after) {
|
|
777
|
+
if (!before.includes(key)) return 'nouvel export : ' + key
|
|
778
|
+
}
|
|
779
|
+
for (const key of before) {
|
|
780
|
+
if (!after.includes(key)) return 'export retire : ' + key
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
for (const key of after) {
|
|
784
|
+
const value = next[key]
|
|
785
|
+
if (runtime.isLikelyComponentType(value)) continue
|
|
786
|
+
// Un export qui n'est pas un composant ne peut etre conserve que s'il n'a
|
|
787
|
+
// pas change : sinon ses consommateurs garderaient l'ancienne valeur.
|
|
788
|
+
if (previous[key] !== value) return 'export non-composant modifie : ' + key
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
return null
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
let planned
|
|
795
|
+
const DEBOUNCE = 16
|
|
796
|
+
|
|
797
|
+
/** Regroupe les mises a jour d'une meme salve en un seul rafraichissement. */
|
|
798
|
+
export function enqueueUpdate() {
|
|
799
|
+
clearTimeout(planned)
|
|
800
|
+
planned = setTimeout(() => {
|
|
801
|
+
planned = undefined
|
|
802
|
+
runtime.performReactRefresh()
|
|
803
|
+
}, DEBOUNCE)
|
|
804
|
+
}
|
|
805
|
+
`;
|
|
806
|
+
var REFRESH_HTML_TAG = `<script type="module" src="${REFRESH_RUNTIME_PATH}"></script>`;
|
|
807
|
+
var cachedRuntime;
|
|
808
|
+
async function bundleRefreshRuntime() {
|
|
809
|
+
if (cachedRuntime !== void 0) return cachedRuntime;
|
|
810
|
+
const result = await build({
|
|
811
|
+
stdin: {
|
|
812
|
+
contents: REFRESH_RUNTIME_SOURCE,
|
|
813
|
+
resolveDir: dirname(fileURLToPath(import.meta.url)),
|
|
814
|
+
loader: "js",
|
|
815
|
+
sourcefile: "odoro-react-refresh.js"
|
|
816
|
+
},
|
|
817
|
+
bundle: true,
|
|
818
|
+
write: false,
|
|
819
|
+
format: "esm",
|
|
820
|
+
platform: "browser",
|
|
821
|
+
target: "es2022",
|
|
822
|
+
logLevel: "silent",
|
|
823
|
+
define: { "process.env.NODE_ENV": JSON.stringify("development") }
|
|
824
|
+
});
|
|
825
|
+
cachedRuntime = result.outputFiles[0]?.text ?? "";
|
|
826
|
+
return cachedRuntime;
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
// src/dev/server.ts
|
|
830
|
+
var MIME = {
|
|
831
|
+
".html": "text/html; charset=utf-8",
|
|
832
|
+
".js": "text/javascript; charset=utf-8",
|
|
833
|
+
".mjs": "text/javascript; charset=utf-8",
|
|
834
|
+
".json": "application/json; charset=utf-8",
|
|
835
|
+
".css": "text/css; charset=utf-8",
|
|
836
|
+
".svg": "image/svg+xml",
|
|
837
|
+
".png": "image/png",
|
|
838
|
+
".jpg": "image/jpeg",
|
|
839
|
+
".jpeg": "image/jpeg",
|
|
840
|
+
".gif": "image/gif",
|
|
841
|
+
".webp": "image/webp",
|
|
842
|
+
".avif": "image/avif",
|
|
843
|
+
".ico": "image/x-icon",
|
|
844
|
+
".woff": "font/woff",
|
|
845
|
+
".woff2": "font/woff2",
|
|
846
|
+
".mp4": "video/mp4",
|
|
847
|
+
".webm": "video/webm",
|
|
848
|
+
".txt": "text/plain; charset=utf-8"
|
|
849
|
+
};
|
|
850
|
+
var SCRIPT_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx", ".mjs"];
|
|
851
|
+
function cleanUrl(url) {
|
|
852
|
+
return (url.split("?")[0] ?? url).split("#")[0] ?? url;
|
|
853
|
+
}
|
|
854
|
+
function buildEnv(config) {
|
|
855
|
+
const env = {
|
|
856
|
+
MODE: "development",
|
|
857
|
+
DEV: true,
|
|
858
|
+
PROD: false,
|
|
859
|
+
BASE_URL: config.base
|
|
860
|
+
};
|
|
861
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
862
|
+
if (key.startsWith(config.envPrefix) && value !== void 0) env[key] = value;
|
|
863
|
+
}
|
|
864
|
+
return env;
|
|
865
|
+
}
|
|
866
|
+
function extractEntries(html, root) {
|
|
867
|
+
const entries = [];
|
|
868
|
+
for (const match of html.matchAll(
|
|
869
|
+
/<script[^>]*type=["']module["'][^>]*src=["']([^"']+)["']/gi
|
|
870
|
+
)) {
|
|
871
|
+
const source = match[1];
|
|
872
|
+
if (source === void 0 || /^https?:/.test(source)) continue;
|
|
873
|
+
entries.push(resolve(root, source.replace(/^\//, "")));
|
|
874
|
+
}
|
|
875
|
+
return entries;
|
|
876
|
+
}
|
|
877
|
+
function injectClient(html) {
|
|
878
|
+
const tags = [
|
|
879
|
+
REFRESH_HTML_TAG,
|
|
880
|
+
`<script type="module" src="${HMR_CLIENT_PATH}"></script>`
|
|
881
|
+
].join("\n ");
|
|
882
|
+
if (html.includes("</head>")) return html.replace("</head>", ` ${tags}
|
|
883
|
+
</head>`);
|
|
884
|
+
return `${tags}
|
|
885
|
+
${html}`;
|
|
886
|
+
}
|
|
887
|
+
async function startDevServer(config) {
|
|
888
|
+
const started = Date.now();
|
|
889
|
+
const graph = new ModuleGraph();
|
|
890
|
+
const env = buildEnv(config);
|
|
891
|
+
const clients = /* @__PURE__ */ new Set();
|
|
892
|
+
const indexFile = join(config.root, "index.html");
|
|
893
|
+
if (!existsSync(indexFile)) {
|
|
894
|
+
throw new Error(`[odoro] Aucun "index.html" a la racine du projet (${config.root}).`);
|
|
895
|
+
}
|
|
896
|
+
const refreshRuntime = await bundleRefreshRuntime();
|
|
897
|
+
const entries = extractEntries(await readFile(indexFile, "utf8"), config.root);
|
|
898
|
+
const specifiers = await scanDependencies(config, entries);
|
|
899
|
+
const deps = await optimizeDeps(config, specifiers);
|
|
900
|
+
if (deps.rebuilt && specifiers.length > 0) {
|
|
901
|
+
info(`${specifiers.length} dependances pre-compilees`);
|
|
902
|
+
}
|
|
903
|
+
const broadcast = (message) => {
|
|
904
|
+
const payload = `data: ${JSON.stringify(message)}
|
|
905
|
+
|
|
906
|
+
`;
|
|
907
|
+
for (const client of clients) client.write(payload);
|
|
908
|
+
};
|
|
909
|
+
const send = (response, body, type, status = 200) => {
|
|
910
|
+
response.writeHead(status, {
|
|
911
|
+
"Content-Type": type,
|
|
912
|
+
"Cache-Control": "no-cache"
|
|
913
|
+
});
|
|
914
|
+
response.end(body);
|
|
915
|
+
};
|
|
916
|
+
const serveScript = async (response, file) => {
|
|
917
|
+
const url2 = fileToUrl(file, config.root);
|
|
918
|
+
const node = graph.ensure(file, url2);
|
|
919
|
+
if (node.code === void 0) {
|
|
920
|
+
const source = await readFile(file, "utf8");
|
|
921
|
+
node.selfAccepting = detectSelfAccepting(source);
|
|
922
|
+
const { code, dependencies } = await transformModule(file, config, env);
|
|
923
|
+
graph.setDependencies(file, dependencies);
|
|
924
|
+
for (const dependency of dependencies) {
|
|
925
|
+
graph.ensure(dependency, fileToUrl(dependency, config.root)).importers.add(file);
|
|
926
|
+
}
|
|
927
|
+
let body = code;
|
|
928
|
+
if (isRefreshCandidate(file)) {
|
|
929
|
+
const instrumented = await applyReactRefresh(code, file);
|
|
930
|
+
if (hasRegisteredComponent(instrumented)) {
|
|
931
|
+
body = refreshPreamble(url2) + instrumented + refreshEpilogue(url2);
|
|
932
|
+
node.selfAccepting = true;
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
node.code = hotPreamble(url2) + body;
|
|
936
|
+
}
|
|
937
|
+
send(response, node.code, MIME[".js"]);
|
|
938
|
+
};
|
|
939
|
+
const serveStyle = async (response, file, direct) => {
|
|
940
|
+
const css = await readFile(file, "utf8");
|
|
941
|
+
if (direct) {
|
|
942
|
+
send(response, css, MIME[".css"]);
|
|
943
|
+
return;
|
|
944
|
+
}
|
|
945
|
+
const url2 = fileToUrl(file, config.root);
|
|
946
|
+
const node = graph.ensure(file, url2);
|
|
947
|
+
node.selfAccepting = true;
|
|
948
|
+
node.code = hotPreamble(url2) + wrapStyle(url2, css);
|
|
949
|
+
send(response, node.code, MIME[".js"]);
|
|
950
|
+
};
|
|
951
|
+
const serveFile = (response, file) => {
|
|
952
|
+
const type = MIME[extname(file).toLowerCase()] ?? "application/octet-stream";
|
|
953
|
+
response.writeHead(200, { "Content-Type": type, "Cache-Control": "no-cache" });
|
|
954
|
+
createReadStream(file).pipe(response);
|
|
955
|
+
};
|
|
956
|
+
const serveHtml = async (response) => {
|
|
957
|
+
const html = await readFile(indexFile, "utf8");
|
|
958
|
+
send(response, injectClient(html), MIME[".html"]);
|
|
959
|
+
};
|
|
960
|
+
const forward = (incoming, response, target) => {
|
|
961
|
+
const url2 = new URL(incoming.url ?? "/", target);
|
|
962
|
+
const proxied = request(
|
|
963
|
+
{
|
|
964
|
+
protocol: url2.protocol,
|
|
965
|
+
hostname: url2.hostname,
|
|
966
|
+
port: url2.port,
|
|
967
|
+
path: `${url2.pathname}${url2.search}`,
|
|
968
|
+
method: incoming.method,
|
|
969
|
+
headers: { ...incoming.headers, host: url2.host }
|
|
970
|
+
},
|
|
971
|
+
(upstream) => {
|
|
972
|
+
response.writeHead(upstream.statusCode ?? 502, upstream.headers);
|
|
973
|
+
upstream.pipe(response);
|
|
974
|
+
}
|
|
975
|
+
);
|
|
976
|
+
proxied.on("error", (cause) => {
|
|
977
|
+
warn(`proxy indisponible : ${target}`);
|
|
978
|
+
send(response, `Proxy indisponible : ${String(cause)}`, "text/plain", 502);
|
|
979
|
+
});
|
|
980
|
+
incoming.pipe(proxied);
|
|
981
|
+
};
|
|
982
|
+
const server = createServer((incoming, response) => {
|
|
983
|
+
void (async () => {
|
|
984
|
+
const url2 = incoming.url ?? "/";
|
|
985
|
+
const path = cleanUrl(url2);
|
|
986
|
+
try {
|
|
987
|
+
for (const [prefix, target] of Object.entries(config.server.proxy)) {
|
|
988
|
+
if (path.startsWith(prefix)) {
|
|
989
|
+
forward(incoming, response, target);
|
|
990
|
+
return;
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
if (path === REFRESH_RUNTIME_PATH) {
|
|
994
|
+
send(response, refreshRuntime, MIME[".js"] ?? "text/javascript");
|
|
995
|
+
return;
|
|
996
|
+
}
|
|
997
|
+
if (path === HMR_CLIENT_PATH) {
|
|
998
|
+
send(response, HMR_CLIENT_SOURCE, MIME[".js"] ?? "text/javascript");
|
|
999
|
+
return;
|
|
1000
|
+
}
|
|
1001
|
+
if (path === HMR_STREAM_PATH) {
|
|
1002
|
+
response.writeHead(200, {
|
|
1003
|
+
"Content-Type": "text/event-stream",
|
|
1004
|
+
"Cache-Control": "no-cache",
|
|
1005
|
+
Connection: "keep-alive"
|
|
1006
|
+
});
|
|
1007
|
+
response.write(`data: ${JSON.stringify({ type: "connected" })}
|
|
1008
|
+
|
|
1009
|
+
`);
|
|
1010
|
+
clients.add(response);
|
|
1011
|
+
incoming.on("close", () => clients.delete(response));
|
|
1012
|
+
return;
|
|
1013
|
+
}
|
|
1014
|
+
if (path.startsWith(DEPS_PREFIX)) {
|
|
1015
|
+
const specifier = path.slice(DEPS_PREFIX.length);
|
|
1016
|
+
const last = specifier.split("/").pop() ?? specifier;
|
|
1017
|
+
const name = last.endsWith(".js") ? last : depFileName(last);
|
|
1018
|
+
const file2 = join(deps.directory, name);
|
|
1019
|
+
if (existsSync(file2)) {
|
|
1020
|
+
serveFile(response, file2);
|
|
1021
|
+
return;
|
|
1022
|
+
}
|
|
1023
|
+
send(
|
|
1024
|
+
response,
|
|
1025
|
+
`throw new Error(${JSON.stringify(
|
|
1026
|
+
`[odoro] Dependance non pre-compilee : "${specifier}". Relancez le serveur.`
|
|
1027
|
+
)})`,
|
|
1028
|
+
MIME[".js"] ?? "text/javascript"
|
|
1029
|
+
);
|
|
1030
|
+
return;
|
|
1031
|
+
}
|
|
1032
|
+
if (path === "/" || path === "/index.html") {
|
|
1033
|
+
await serveHtml(response);
|
|
1034
|
+
return;
|
|
1035
|
+
}
|
|
1036
|
+
if (path.startsWith(INTERNAL_PREFIX)) {
|
|
1037
|
+
send(response, "Introuvable", "text/plain", 404);
|
|
1038
|
+
return;
|
|
1039
|
+
}
|
|
1040
|
+
const file = urlToFile(path, config.root);
|
|
1041
|
+
if (existsSync(file) && statSync(file).isFile()) {
|
|
1042
|
+
if (hasExtension(path, STYLE_EXTENSIONS)) {
|
|
1043
|
+
await serveStyle(response, file, url2.includes("?direct"));
|
|
1044
|
+
return;
|
|
1045
|
+
}
|
|
1046
|
+
if (hasExtension(path, ASSET_EXTENSIONS)) {
|
|
1047
|
+
if (url2.includes("?import")) {
|
|
1048
|
+
send(response, wrapAsset(path), MIME[".js"] ?? "text/javascript");
|
|
1049
|
+
} else {
|
|
1050
|
+
serveFile(response, file);
|
|
1051
|
+
}
|
|
1052
|
+
return;
|
|
1053
|
+
}
|
|
1054
|
+
if (hasExtension(path, SCRIPT_EXTENSIONS)) {
|
|
1055
|
+
await serveScript(response, file);
|
|
1056
|
+
return;
|
|
1057
|
+
}
|
|
1058
|
+
serveFile(response, file);
|
|
1059
|
+
return;
|
|
1060
|
+
}
|
|
1061
|
+
const publicFile = join(config.publicDir, path.replace(/^\//, ""));
|
|
1062
|
+
if (existsSync(publicFile) && statSync(publicFile).isFile()) {
|
|
1063
|
+
serveFile(response, publicFile);
|
|
1064
|
+
return;
|
|
1065
|
+
}
|
|
1066
|
+
if (!extname(path)) {
|
|
1067
|
+
await serveHtml(response);
|
|
1068
|
+
return;
|
|
1069
|
+
}
|
|
1070
|
+
send(response, "Introuvable", "text/plain", 404);
|
|
1071
|
+
} catch (cause) {
|
|
1072
|
+
const message = cause instanceof Error ? cause.message : String(cause);
|
|
1073
|
+
error(`echec du traitement de ${path}`, cause);
|
|
1074
|
+
broadcast({ type: "error", message, file: path });
|
|
1075
|
+
send(response, `Erreur : ${message}`, "text/plain", 500);
|
|
1076
|
+
}
|
|
1077
|
+
})();
|
|
1078
|
+
});
|
|
1079
|
+
let pending;
|
|
1080
|
+
const changed = /* @__PURE__ */ new Set();
|
|
1081
|
+
const watcher = watch(config.root, { recursive: true }, (_event, filename) => {
|
|
1082
|
+
if (filename === null) return;
|
|
1083
|
+
const normalized = filename.split("\\").join("/");
|
|
1084
|
+
if (normalized.includes("node_modules/") || normalized.startsWith(".git/") || normalized.startsWith("dist/")) {
|
|
1085
|
+
return;
|
|
1086
|
+
}
|
|
1087
|
+
changed.add(join(config.root, filename));
|
|
1088
|
+
clearTimeout(pending);
|
|
1089
|
+
pending = setTimeout(() => {
|
|
1090
|
+
const files = [...changed];
|
|
1091
|
+
changed.clear();
|
|
1092
|
+
const updates = [];
|
|
1093
|
+
let reload = false;
|
|
1094
|
+
for (const file of files) {
|
|
1095
|
+
if (file === indexFile) {
|
|
1096
|
+
reload = true;
|
|
1097
|
+
continue;
|
|
1098
|
+
}
|
|
1099
|
+
const boundaries = graph.invalidate(file);
|
|
1100
|
+
if (boundaries.length === 0) {
|
|
1101
|
+
if (graph.get(file) !== void 0) reload = true;
|
|
1102
|
+
continue;
|
|
1103
|
+
}
|
|
1104
|
+
for (const boundary of boundaries) {
|
|
1105
|
+
updates.push({ url: boundary.url, timestamp: boundary.timestamp });
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
if (reload) {
|
|
1109
|
+
info("rechargement de la page");
|
|
1110
|
+
broadcast({ type: "full-reload" });
|
|
1111
|
+
} else if (updates.length > 0) {
|
|
1112
|
+
info(
|
|
1113
|
+
`mise a jour a chaud : ${updates.map((update) => update.url).join(", ")}`
|
|
1114
|
+
);
|
|
1115
|
+
broadcast({ type: "update", updates });
|
|
1116
|
+
}
|
|
1117
|
+
}, 40);
|
|
1118
|
+
});
|
|
1119
|
+
await new Promise((resolveListen, rejectListen) => {
|
|
1120
|
+
server.once("error", rejectListen);
|
|
1121
|
+
server.listen(config.server.port, config.server.host, resolveListen);
|
|
1122
|
+
});
|
|
1123
|
+
const url = `http://${config.server.host}:${config.server.port}${config.base}`;
|
|
1124
|
+
success(`pret en ${duration(Date.now() - started)}`);
|
|
1125
|
+
info(` ${colors.cyan(url)}`);
|
|
1126
|
+
return {
|
|
1127
|
+
url,
|
|
1128
|
+
async close() {
|
|
1129
|
+
watcher.close();
|
|
1130
|
+
clearTimeout(pending);
|
|
1131
|
+
for (const client of clients) client.end();
|
|
1132
|
+
clients.clear();
|
|
1133
|
+
graph.clear();
|
|
1134
|
+
await new Promise((done) => server.close(() => done()));
|
|
1135
|
+
}
|
|
1136
|
+
};
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
export { ModuleGraph, applyAlias, depFileName, detectSelfAccepting, extractEntries, injectClient, isBareSpecifier, optimizeDeps, scanDependencies, startDevServer };
|