@navette-js/wire 1.0.0-beta.1 → 1.0.0-beta.3

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/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Server-side runtime compatible with Livewire 3 for Node.js and Express.
4
4
 
5
- Part of [Navette](https://github.com/NavetteJS/navette), a Node.js port of [Filament](https://filamentphp.com). Install the whole framework with `npm install @navette-js/panels`.
5
+ Part of [Navette](https://github.com/NavetteJS/navette), admin panels for Node.js. Start a project with `npm create navette@latest`, or install the framework with `npm install @navette-js/panels`.
6
6
 
7
7
  **Beta:** the API may still change between beta releases.
8
8
 
@@ -1,10 +1,10 @@
1
1
  # Third-party notices
2
2
 
3
- Navette is an independent Node.js port of Filament. It is not affiliated with or endorsed by the Filament, Livewire or Heroicons projects. It reuses the following works, each under its own license; the full license texts ship next to the files they cover.
3
+ Navette is an independent project, not affiliated with or endorsed by the Filament, Livewire or Heroicons projects. It reuses the following works, each under its own license; the full license texts ship next to the files they cover.
4
4
 
5
5
  | Work | Used for | License | License file |
6
6
  |---|---|---|---|
7
- | [Filament](https://github.com/filamentphp/filament) © Filament | HTML markup ported from its Blade views, compiled CSS and JavaScript (`@navette-js/panels/dist/filament`), English translations (`@navette-js/support/data`) | MIT | `packages/panels/dist/FILAMENT-LICENSE.md` |
7
+ | [Filament](https://github.com/filamentphp/filament) © Filament | HTML markup derived from its Blade views, compiled CSS and JavaScript (`@navette-js/panels/dist/filament`), English translations (`@navette-js/support/data`) | MIT | `packages/panels/dist/FILAMENT-LICENSE.md` |
8
8
  | [Livewire](https://github.com/livewire/livewire) © Caleb Porzio | Client script `livewire.js` (`@navette-js/wire/vendor`); the server runtime follows its protocol | MIT | `packages/wire/vendor/LIVEWIRE-LICENSE.md` |
9
9
  | [Heroicons](https://github.com/tailwindlabs/heroicons) | SVG icons (`@navette-js/support/icons`) | MIT | `packages/support/icons/HEROICONS-LICENSE.md` |
10
10
  | [Inter](https://github.com/rsms/inter) © The Inter Project Authors | Default font files (`@navette-js/panels/dist/filament/fonts`) | SIL Open Font License 1.1 | `packages/panels/dist/filament/fonts/INTER-LICENSE.txt` |
package/lib/express.d.ts CHANGED
@@ -1,15 +1,15 @@
1
1
  import type { Request, Response, Router } from 'express';
2
2
  import { type WireRuntime } from './index.ts';
3
- /** Jeton CSRF par double soumission : cookie HttpOnly + jeton renvoyé dans le corps par livewire.js (`_token`). */
3
+ /** Double-submit CSRF token: an HttpOnly cookie, plus the token livewire.js sends back in the body (`_token`). */
4
4
  export declare function csrfToken(req: Request, res: Response): string;
5
- /** Vérifie le jeton envoyé (corps ou formulaire) contre le cookie, en temps constant. */
5
+ /** Checks the submitted token (body or form field) against the cookie, in constant time. */
6
6
  export declare function verifyCsrf(req: Request, token: unknown): boolean;
7
7
  export declare function wireRouter(runtime: WireRuntime, prefix?: string, makeContext?: (req: Request, res: Response) => unknown | Promise<unknown>,
8
- /** Exécute le traitement dans un contexte de requête et permet d'enrichir la réponse (notifications…). */
8
+ /** Wraps the handling in a request context and can enrich the response (notifications…). */
9
9
  around?: (ctx: any, req: Request, res: Response, handle: () => Promise<any>) => Promise<any>,
10
- /** Autorise un envoi de fichier (par exemple : utilisateur connecté). Sans cette fonction, tout porteur d'une adresse signée et du jeton CSRF peut envoyer. */
10
+ /** Authorizes a file upload (e.g. signed-in user). Without it, anyone holding a signed URL and the CSRF token can upload. */
11
11
  canUpload?: (ctx: any) => boolean | Promise<boolean>): Router;
12
- /** Balise <script> attendue par livewire.js (équivalent de FrontendAssets::js). */
12
+ /** `<script>` tag that loads livewire.js with its CSRF token and endpoint URLs. */
13
13
  export declare function wireScripts(req: Request, res: Response, prefix?: string): string;
14
- /** Styles attendus par livewire.js (équivalent de FrontendAssets::styles) : à placer dans <head>. */
14
+ /** Styles livewire.js expects (loading states, cloak, progress bar); place them in `<head>`. */
15
15
  export declare function wireStyles(progressBarColor?: string): string;
package/lib/express.js CHANGED
@@ -1,4 +1,4 @@
1
- // Intégration Express du runtime : sert livewire.js embarqué, expose l'endpoint de mise à jour, protège par jeton CSRF.
1
+ // Express integration: serves the bundled livewire.js and exposes the CSRF-protected update and upload endpoints.
2
2
  import { randomBytes, timingSafeEqual } from 'node:crypto';
3
3
  import { readFileSync } from 'node:fs';
4
4
  import { fileURLToPath } from 'node:url';
@@ -8,7 +8,7 @@ const vendor = (f) => fileURLToPath(new URL(`../vendor/${f}`, import.meta.url));
8
8
  const versionHash = JSON.parse(readFileSync(vendor('manifest.json'), 'utf8'))['/livewire.js'];
9
9
  const COOKIE = 'navette_csrf';
10
10
  const readCookie = (req, name) => (req.headers.cookie ?? '').split(';').map((c) => c.trim().split('=')).find(([k]) => k === name)?.[1];
11
- /** Jeton CSRF par double soumission : cookie HttpOnly + jeton renvoyé dans le corps par livewire.js (`_token`). */
11
+ /** Double-submit CSRF token: an HttpOnly cookie, plus the token livewire.js sends back in the body (`_token`). */
12
12
  export function csrfToken(req, res) {
13
13
  let token = readCookie(req, COOKIE) ?? res.locals.navetteCsrf;
14
14
  if (!token) {
@@ -18,15 +18,15 @@ export function csrfToken(req, res) {
18
18
  }
19
19
  return token;
20
20
  }
21
- /** Vérifie le jeton envoyé (corps ou formulaire) contre le cookie, en temps constant. */
21
+ /** Checks the submitted token (body or form field) against the cookie, in constant time. */
22
22
  export function verifyCsrf(req, token) {
23
23
  const cookie = Buffer.from(readCookie(req, COOKIE) ?? ''), sent = Buffer.from(String(token ?? ''));
24
24
  return cookie.length > 0 && cookie.length === sent.length && timingSafeEqual(cookie, sent);
25
25
  }
26
26
  export function wireRouter(runtime, prefix = '/livewire', makeContext = (req, res) => ({ req, res }),
27
- /** Exécute le traitement dans un contexte de requête et permet d'enrichir la réponse (notifications…). */
27
+ /** Wraps the handling in a request context and can enrich the response (notifications…). */
28
28
  around = (_ctx, _req, _res, handle) => handle(),
29
- /** Autorise un envoi de fichier (par exemple : utilisateur connecté). Sans cette fonction, tout porteur d'une adresse signée et du jeton CSRF peut envoyer. */
29
+ /** Authorizes a file upload (e.g. signed-in user). Without it, anyone holding a signed URL and the CSRF token can upload. */
30
30
  canUpload = () => true) {
31
31
  const router = express.Router();
32
32
  router.get(`${prefix}/livewire.js`, (_req, res) => {
@@ -35,7 +35,7 @@ canUpload = () => true) {
35
35
  router.post(`${prefix}/update`, express.json({ limit: '2mb' }), async (req, res) => {
36
36
  try {
37
37
  if (!verifyCsrf(req, req.body?._token))
38
- throw new WireError(419, 'Jeton CSRF invalide.');
38
+ throw new WireError(419, 'Invalid CSRF token.');
39
39
  const ctx = await makeContext(req, res);
40
40
  res.json(await around(ctx, req, res, () => runtime.handle(req.body, ctx)));
41
41
  }
@@ -45,39 +45,39 @@ canUpload = () => true) {
45
45
  console.error(e);
46
46
  else if (process.env.NODE_ENV !== 'production')
47
47
  console.warn(`[wire] ${status} ${e.message} — calls=${JSON.stringify(req.body?.components?.map((c) => c.calls))} updates=${JSON.stringify(req.body?.components?.map((c) => c.updates))}`);
48
- res.status(status).send(e instanceof Error ? e.message : 'Erreur');
48
+ res.status(status).send(e instanceof Error ? e.message : 'Error');
49
49
  }
50
50
  });
51
- // Envoi de fichiers : adresse signée par `_startUpload`, jeton CSRF en en-tête, fichiers reçus dans le dossier temporaire.
51
+ // File uploads: URL signed by `_startUpload`, CSRF token in a header, files stored in the temporary directory.
52
52
  router.post(`${prefix}/upload-file`, async (req, res) => {
53
53
  try {
54
54
  const store = runtime.uploads;
55
55
  if (!store)
56
- throw new WireError(404, 'Téléversement indisponible.');
56
+ throw new WireError(404, 'Uploads are not available.');
57
57
  if (!store.verifySignature(req.query.expires, req.query.signature))
58
- throw new WireError(401, 'Adresse d\'envoi invalide ou expirée.');
58
+ throw new WireError(401, 'Invalid or expired upload URL.');
59
59
  if (!verifyCsrf(req, req.get('x-csrf-token')))
60
- throw new WireError(419, 'Jeton CSRF invalide.');
60
+ throw new WireError(419, 'Invalid CSRF token.');
61
61
  if (!(await canUpload(await makeContext(req, res))))
62
- throw new WireError(401, 'Non authentifié.');
62
+ throw new WireError(401, 'Unauthenticated.');
63
63
  res.json({ paths: await store.receive(req) });
64
64
  }
65
65
  catch (e) {
66
66
  const status = e instanceof WireError ? e.status : Number(e?.status) || 500;
67
67
  if (status === 500)
68
68
  console.error(e);
69
- res.status(status).json({ message: e instanceof Error ? e.message : 'Erreur' });
69
+ res.status(status).json({ message: e instanceof Error ? e.message : 'Error' });
70
70
  }
71
71
  });
72
72
  return router;
73
73
  }
74
- /** Balise <script> attendue par livewire.js (équivalent de FrontendAssets::js). */
74
+ /** `<script>` tag that loads livewire.js with its CSRF token and endpoint URLs. */
75
75
  export function wireScripts(req, res, prefix = '/livewire') {
76
76
  return `<script src="${prefix}/livewire.js?id=${versionHash}" data-csrf="${csrfToken(req, res)}" data-module-url="${prefix}" data-update-uri="${prefix}/update" data-navigate-once="true"></script>`;
77
77
  }
78
- /** Styles attendus par livewire.js (équivalent de FrontendAssets::styles) : à placer dans <head>. */
78
+ /** Styles livewire.js expects (loading states, cloak, progress bar); place them in `<head>`. */
79
79
  export function wireStyles(progressBarColor = '#2299dd') {
80
- const BS = String.fromCharCode(92); // barre oblique inverse : échappe « : » et « . » dans les sélecteurs CSS
80
+ const BS = String.fromCharCode(92); // backslash: escapes `:` and `.` in CSS selectors
81
81
  const sel = (n) => `[wire${BS}:${n.replaceAll('.', BS + '.')}]`;
82
82
  const doubled = (names) => names.map((n) => sel(n) + sel(n)).join(', ');
83
83
  return '<!-- Livewire Styles --><style>'
package/lib/index.d.ts CHANGED
@@ -35,65 +35,65 @@ export declare class WireError extends Error {
35
35
  status: number;
36
36
  constructor(status: number, message: string);
37
37
  }
38
- /** Classe de base d'un composant. Toute propriété propre énumérable ne commençant pas par `_` est de l'état public. */
38
+ /** Base class of a component. Every own enumerable property not starting with `_` is public state. */
39
39
  export declare abstract class Component {
40
40
  _id: string;
41
41
  _effects: Effects;
42
42
  _skipRender: boolean;
43
43
  _errors: Record<string, string[]>;
44
- /** Contexte de la requête HTTP en cours (requête, réponse, panel…), fourni par l'intégration. */
44
+ /** Context of the current HTTP request (request, response, panel…), supplied by the integration. */
45
45
  _ctx: any;
46
- /** Runtime et requête d'origine : servent à monter des composants enfants pendant le rendu. */
46
+ /** Runtime and originating request, used to mount child components while rendering. */
47
47
  _runtime: WireRuntime | null;
48
48
  _request: {
49
49
  path?: string;
50
50
  method?: string;
51
51
  };
52
- /** Enfants rendus pendant cette requête, et enfants déjà présents dans le navigateur : clé → [balise, identifiant]. */
52
+ /** Children rendered during this request, and children already in the browser: key → [tag, id]. */
53
53
  _children: Record<string, [string, string]>;
54
54
  _previousChildren: Record<string, [string, string]>;
55
- /** HTML affiché tant qu'un composant « lazy » n'est pas chargé : une seule racine. */
55
+ /** HTML shown until a lazy component is loaded; must have a single root. */
56
56
  placeholder(): string | Promise<string>;
57
57
  /**
58
- * Monte un composant enfant et rend son HTML, à insérer dans le rendu du parent (équivalent de `@livewire`).
59
- * Si le navigateur possède déjà cet enfant (même clé), seule une souche est rendue : le client conserve l'enfant tel quel.
60
- * `lazy` : l'enfant affiche son `placeholder()` et se charge quand il devient visible.
58
+ * Mounts a child component and returns its HTML, to insert into the parent's render.
59
+ * If the browser already has this child (same key), only a stub is rendered and the client keeps the child as is.
60
+ * `lazy`: the child shows its `placeholder()` and loads once it becomes visible.
61
61
  */
62
62
  mountChild(name: string, params: Record<string, unknown>, key: string, options?: {
63
63
  lazy?: boolean;
64
64
  }): Promise<string>;
65
- /** Rend le HTML du composant : une seule racine. */
65
+ /** Renders the component's HTML; must have a single root element. */
66
66
  abstract render(): string | Promise<string>;
67
67
  mount(_params: Record<string, unknown>): void | Promise<void>;
68
68
  boot(): void | Promise<void>;
69
69
  hydrate(): void | Promise<void>;
70
70
  dehydrate(): void | Promise<void>;
71
71
  get id(): string;
72
- /** `self` : l'événement n'est reçu que par ce composant (équivalent de `->self()`). */
72
+ /** Dispatches a browser event; with `self`, only this component receives it. */
73
73
  dispatch(name: string, params?: Record<string, unknown> | unknown[], options?: {
74
74
  self?: boolean;
75
75
  }): void;
76
76
  redirect(url: string, navigate?: boolean): void;
77
77
  skipRender(): void;
78
- /** Rendu partiel à la Filament : seul le fragment `wire:partial="<nom>"` est renvoyé (effects.partials), sans re-rendre tout le composant. */
78
+ /** Partial render: only the `wire:partial="<name>"` fragment is sent back (effects.partials), the component is not re-rendered. */
79
79
  renderPartial(name: string, html: string): void;
80
80
  addError(path: string, message: string): void;
81
81
  resetErrorBag(): void;
82
82
  }
83
83
  /**
84
- * Méthodes appelables depuis le navigateur : uniquement celles que la classe (ou une classe parente) nomme dans `static callable`.
85
- * TypeScript efface `protected` et `private` à l'exécution : sans cette liste, toute méthode interne serait appelable par une
86
- * requête forgée, et sa valeur de retour renvoyée au navigateur.
84
+ * Methods callable from the browser: only those the class (or a parent class) lists in `static callable`.
85
+ * TypeScript erases `protected` and `private` at runtime: without this allowlist, any internal method could be called by a
86
+ * forged request and its return value sent back to the browser.
87
87
  */
88
88
  export declare function callableMethods(component: Component): Set<string>;
89
89
  export declare function publicProperties(component: Component): string[];
90
90
  export declare function dehydrateValue(value: unknown): unknown;
91
91
  export declare function hydrateValue(value: unknown): unknown;
92
- /** Équivalent de Utils::insertAttributesIntoHtmlRoot : pose des attributs sur la balise racine. */
92
+ /** Adds attributes to the root element of an HTML fragment. */
93
93
  export declare function insertAttributesIntoHtmlRoot(html: string, attributes: Record<string, unknown>): string;
94
94
  export type RuntimeOptions = {
95
95
  key: string;
96
- locale?: string; /** Dossier des téléversements temporaires ; sans lui, les envois de fichiers sont refusés. */
96
+ locale?: string; /** Temporary upload store; without it, file uploads are refused. */
97
97
  uploads?: UploadStore;
98
98
  uploadPrefix?: string;
99
99
  };
@@ -102,27 +102,32 @@ export declare class WireRuntime {
102
102
  private options;
103
103
  constructor(options: RuntimeOptions);
104
104
  get uploads(): UploadStore | null;
105
- /** Paramètres passés par un parent à ses enfants déjà présents, pendant la requête en cours : identifiant de l'enfant → paramètres. */
105
+ /** Params passed by a parent to its existing children during the current request: child id → params. */
106
106
  private requestScope;
107
107
  passPropsToChild(childId: string, params: Record<string, unknown>): void;
108
+ private names;
108
109
  register(name: string, cls: ComponentClass): this;
110
+ /** Name under which this runtime registered a class. A class shared by several runtimes (one per panel) keeps one name per runtime. */
111
+ nameOf(cls: ComponentClass): string;
109
112
  private make;
110
113
  checksum(snapshot: Omit<Snapshot, 'checksum'>): string;
111
114
  private snapshot;
112
- /** Paramètres de montage d'un composant « lazy », signés : le navigateur les renvoie tels quels à `__lazyLoad` (conteneur `__mountParamsContainer` de Livewire). */
115
+ /** Signed mount params of a lazy component, in a `__mountParamsContainer`: the browser sends them back unchanged to `__lazyLoad`. */
113
116
  private encodeMountParams;
114
117
  private decodeMountParams;
115
- /** Comme Livewire : un paramètre de montage du même nom qu'une propriété publique y est copié. */
118
+ /** A mount param named like a public property is copied into it. */
116
119
  private assignMatchingProperties;
117
120
  private renderRoot;
118
- /** Premier rendu : HTML avec wire:snapshot et wire:effects sur la racine.
119
- * Le mémo ne porte `lazyLoaded` / `lazyIsolated` que pour un composant réellement « lazy » : leur seule présence fait ignorer les événements au client. */
121
+ /**
122
+ * Initial render: HTML with wire:snapshot and wire:effects on the root.
123
+ * The memo only carries `lazyLoaded` / `lazyIsolated` for lazy components: their mere presence makes the client ignore events.
124
+ */
120
125
  mount(name: string, params?: Record<string, unknown>, request?: {
121
126
  path?: string;
122
127
  method?: string;
123
128
  ctx?: unknown;
124
129
  }): Promise<string>;
125
- /** Comme mount(), mais rend aussi l'instance montée (pour lire son titre, par exemple). */
130
+ /** Like mount(), but also returns the mounted instance (to read its title, for example). */
126
131
  mountWithInstance(name: string, params?: Record<string, unknown>, request?: {
127
132
  path?: string;
128
133
  method?: string;
@@ -133,12 +138,12 @@ export declare class WireRuntime {
133
138
  html: string;
134
139
  component: Component;
135
140
  }>;
136
- /** Requête de mise à jour d'un composant : hydratation, mises à jour, appels, rendu, déshydratation. */
141
+ /** Component update request: hydrate, apply updates, run calls, render, dehydrate. */
137
142
  update(snapshot: Snapshot, updates: Record<string, unknown>, calls: Call[], ctx?: unknown): Promise<{
138
143
  snapshot: Snapshot;
139
144
  effects: Effects;
140
145
  }>;
141
- /** Point d'entrée HTTP : corps `{ _token, components: [{ snapshot, updates, calls }] }`. */
146
+ /** HTTP entry point; body: `{ _token, components: [{ snapshot, updates, calls }] }`. */
142
147
  handle(body: any, ctx?: unknown): Promise<{
143
148
  components: {
144
149
  snapshot: string;
package/lib/index.js CHANGED
@@ -1,5 +1,4 @@
1
- // @navette-js/wire — runtime serveur de composants à état, compatible avec le protocole du client livewire.js 4.4.8.
2
- // Prototype du jalon 0 (ADR 0001). Référence : sources/demo/vendor/livewire/livewire/src/Mechanisms/HandleComponents.
1
+ // @navette-js/wire — server runtime for stateful components, speaking the Livewire client protocol (livewire.js 4.4.8).
3
2
  import { AsyncLocalStorage } from 'node:async_hooks';
4
3
  import { createHmac, randomBytes, timingSafeEqual } from 'node:crypto';
5
4
  import { UploadStore } from "./uploads.js";
@@ -8,34 +7,34 @@ export class WireError extends Error {
8
7
  status;
9
8
  constructor(status, message) { super(message); this.status = status; }
10
9
  }
11
- /** Classe de base d'un composant. Toute propriété propre énumérable ne commençant pas par `_` est de l'état public. */
10
+ /** Base class of a component. Every own enumerable property not starting with `_` is public state. */
12
11
  export class Component {
13
12
  _id = '';
14
13
  _effects = {};
15
14
  _skipRender = false;
16
15
  _errors = {};
17
- /** Contexte de la requête HTTP en cours (requête, réponse, panel…), fourni par l'intégration. */
16
+ /** Context of the current HTTP request (request, response, panel…), supplied by the integration. */
18
17
  _ctx = null;
19
- /** Runtime et requête d'origine : servent à monter des composants enfants pendant le rendu. */
18
+ /** Runtime and originating request, used to mount child components while rendering. */
20
19
  _runtime = null;
21
20
  _request = {};
22
- /** Enfants rendus pendant cette requête, et enfants déjà présents dans le navigateur : clé → [balise, identifiant]. */
21
+ /** Children rendered during this request, and children already in the browser: key → [tag, id]. */
23
22
  _children = {};
24
23
  _previousChildren = {};
25
- /** HTML affiché tant qu'un composant « lazy » n'est pas chargé : une seule racine. */
24
+ /** HTML shown until a lazy component is loaded; must have a single root. */
26
25
  placeholder() { return '<div></div>'; }
27
26
  /**
28
- * Monte un composant enfant et rend son HTML, à insérer dans le rendu du parent (équivalent de `@livewire`).
29
- * Si le navigateur possède déjà cet enfant (même clé), seule une souche est rendue : le client conserve l'enfant tel quel.
30
- * `lazy` : l'enfant affiche son `placeholder()` et se charge quand il devient visible.
27
+ * Mounts a child component and returns its HTML, to insert into the parent's render.
28
+ * If the browser already has this child (same key), only a stub is rendered and the client keeps the child as is.
29
+ * `lazy`: the child shows its `placeholder()` and loads once it becomes visible.
31
30
  */
32
31
  async mountChild(name, params = {}, key, options = {}) {
33
32
  if (!this._runtime)
34
- throw new Error('mountChild() : composant hors runtime.');
33
+ throw new Error('mountChild(): component is not attached to a runtime.');
35
34
  const previous = this._previousChildren[key];
36
35
  if (previous) {
37
36
  this._children[key] = previous;
38
- // Propriétés réactives : l'enfant, mis à jour dans la même requête, reprendra ces valeurs (équivalent de #[Reactive]).
37
+ // Reactive props: the child, updated later in the same request, picks these values up.
39
38
  this._runtime.passPropsToChild(previous[1], params);
40
39
  return `<${previous[0]} wire:id="${previous[1]}" wire:name="${escapeAttr(name)}" wire:key="${escapeAttr(key)}"></${previous[0]}>`;
41
40
  }
@@ -48,7 +47,7 @@ export class Component {
48
47
  hydrate() { }
49
48
  dehydrate() { }
50
49
  get id() { return this._id; }
51
- /** `self` : l'événement n'est reçu que par ce composant (équivalent de `->self()`). */
50
+ /** Dispatches a browser event; with `self`, only this component receives it. */
52
51
  dispatch(name, params = {}, options = {}) {
53
52
  (this._effects.dispatches ??= []).push({ name, params: params, ...(options.self ? { self: true } : {}) });
54
53
  }
@@ -58,18 +57,18 @@ export class Component {
58
57
  this._effects.redirectUsingNavigate = true;
59
58
  }
60
59
  skipRender() { this._skipRender = true; }
61
- /** Rendu partiel à la Filament : seul le fragment `wire:partial="<nom>"` est renvoyé (effects.partials), sans re-rendre tout le composant. */
60
+ /** Partial render: only the `wire:partial="<name>"` fragment is sent back (effects.partials), the component is not re-rendered. */
62
61
  renderPartial(name, html) { (this._effects.partials ??= {})[name] = html; this._skipRender = true; }
63
62
  addError(path, message) { (this._errors[path] ??= []).push(message); }
64
63
  resetErrorBag() { this._errors = {}; }
65
64
  }
66
65
  const LIFECYCLE = new Set(['constructor', 'render', 'mount', 'boot', 'hydrate', 'dehydrate', 'dispatch', 'redirect', 'skipRender', 'renderPartial', 'addError', 'resetErrorBag', 'mountChild', 'placeholder']);
67
- // Seuls les hooks de propriété (updatingFoo / updatedFoo) sont réservés par préfixe : `mountAction`, par exemple, est une méthode publique ordinaire.
66
+ // Only property hooks (updatingFoo / updatedFoo) are reserved by prefix: `mountAction`, for example, is an ordinary public method.
68
67
  const isLifecycleHook = (m) => LIFECYCLE.has(m) || /^(updating|updated)[A-Z]/.test(m) || m.startsWith('_');
69
68
  /**
70
- * Méthodes appelables depuis le navigateur : uniquement celles que la classe (ou une classe parente) nomme dans `static callable`.
71
- * TypeScript efface `protected` et `private` à l'exécution : sans cette liste, toute méthode interne serait appelable par une
72
- * requête forgée, et sa valeur de retour renvoyée au navigateur.
69
+ * Methods callable from the browser: only those the class (or a parent class) lists in `static callable`.
70
+ * TypeScript erases `protected` and `private` at runtime: without this allowlist, any internal method could be called by a
71
+ * forged request and its return value sent back to the browser.
73
72
  */
74
73
  export function callableMethods(component) {
75
74
  const out = new Set();
@@ -85,7 +84,7 @@ export function callableMethods(component) {
85
84
  export function publicProperties(component) {
86
85
  return Object.keys(component).filter((k) => !k.startsWith('_'));
87
86
  }
88
- // --- Synthétiseurs : les tableaux et objets voyagent sous la forme [valeur, { s: 'arr' }] (HandleSynths de Livewire).
87
+ // Wire format: arrays and objects travel as [value, { s: 'arr' }] tuples.
89
88
  export function dehydrateValue(value) {
90
89
  if (value === undefined)
91
90
  return null;
@@ -105,11 +104,11 @@ export function hydrateValue(value) {
105
104
  return value;
106
105
  }
107
106
  const escapeAttr = (s) => s.replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/'/g, '&#039;');
108
- /** Équivalent de Utils::insertAttributesIntoHtmlRoot : pose des attributs sur la balise racine. */
107
+ /** Adds attributes to the root element of an HTML fragment. */
109
108
  export function insertAttributesIntoHtmlRoot(html, attributes) {
110
109
  const m = /^\s*<([a-zA-Z0-9-]+)/.exec(html);
111
110
  if (!m)
112
- throw new WireError(500, 'Le rendu d\'un composant doit commencer par une balise racine.');
111
+ throw new WireError(500, 'A component render must start with a root element.');
113
112
  const attrs = Object.entries(attributes)
114
113
  .map(([k, v]) => `${k}="${escapeAttr(typeof v === 'string' ? v : JSON.stringify(v))}"`)
115
114
  .join(' ');
@@ -131,11 +130,11 @@ function setByPath(target, segments, value) {
131
130
  else
132
131
  node[last] = value;
133
132
  }
134
- /** Identifiant alphanumérique de 20 caractères, comme Str::random(20) de Livewire (il sert dans des id HTML et des sélecteurs). */
133
+ /** 20-character alphanumeric id (used in HTML ids and CSS selectors, so no symbols). */
135
134
  const randomId = () => Array.from(randomBytes(20), (b) => 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'[b % 62]).join('');
136
135
  const MAGIC_ACTIONS = new Set(['$refresh', '$set', '$sync', '$commit']);
137
136
  const studly = (s) => s.replace(/(^|[_.-])(\w)/g, (_, __, c) => c.toUpperCase());
138
- /** Faux si la valeur contient, à n'importe quelle profondeur, une référence de fichier temporaire qui ne désigne aucun fichier reçu. */
137
+ /** False if the value contains, at any depth, a temporary file reference that does not point to a received file. */
139
138
  function uploadReferencesAreValid(value, store, depth = 0) {
140
139
  if (typeof value === 'string')
141
140
  return value.startsWith('livewire-files:') ? false : value.startsWith('livewire-file:') ? !!store?.resolve(value) : true;
@@ -145,25 +144,29 @@ function uploadReferencesAreValid(value, store, depth = 0) {
145
144
  return false;
146
145
  return Object.values(value).every((v) => uploadReferencesAreValid(v, store, depth + 1));
147
146
  }
148
- /** Propriétés réactives d'un composant (`static reactive`) : fixées par le parent, jamais par le navigateur. */
147
+ /** Reactive props of a component (`static reactive`): set by the parent, never by the browser. */
149
148
  const reactiveProps = (component) => component.constructor.reactive ?? [];
150
149
  export class WireRuntime {
151
150
  registry = new Map();
152
151
  options;
153
152
  constructor(options) { this.options = options; }
154
153
  get uploads() { return this.options.uploads ?? null; }
155
- /** Paramètres passés par un parent à ses enfants déjà présents, pendant la requête en cours : identifiant de l'enfant → paramètres. */
154
+ /** Params passed by a parent to its existing children during the current request: child id → params. */
156
155
  requestScope = new AsyncLocalStorage();
157
156
  passPropsToChild(childId, params) { this.requestScope.getStore()?.childProps.set(childId, params); }
158
- register(name, cls) { this.registry.set(name, cls); cls.componentName = name; return this; }
157
+ names = new Map();
158
+ register(name, cls) { this.registry.set(name, cls); if (!this.names.has(cls))
159
+ this.names.set(cls, name); cls.componentName = name; return this; }
160
+ /** Name under which this runtime registered a class. A class shared by several runtimes (one per panel) keeps one name per runtime. */
161
+ nameOf(cls) { return this.names.get(cls) ?? cls.componentName ?? ''; }
159
162
  make(name) {
160
163
  const cls = this.registry.get(name);
161
164
  if (!cls)
162
- throw new WireError(404, `Composant inconnu : ${name}`);
165
+ throw new WireError(404, `Unknown component: ${name}`);
163
166
  return new cls();
164
167
  }
165
168
  checksum(snapshot) {
166
- const { children: _children, ...memo } = snapshot.memo; // les enfants peuvent être modifiés par le client
169
+ const { children: _children, ...memo } = snapshot.memo; // the client may rewrite `children`, so it is not signed
167
170
  return createHmac('sha256', this.options.key).update(JSON.stringify({ data: snapshot.data, memo })).digest('hex');
168
171
  }
169
172
  snapshot(component, name, memo) {
@@ -173,7 +176,7 @@ export class WireRuntime {
173
176
  const body = { data, memo: { id: component.id, name, ...memo, errors: component._errors } };
174
177
  return { ...body, checksum: this.checksum(body) };
175
178
  }
176
- /** Paramètres de montage d'un composant « lazy », signés : le navigateur les renvoie tels quels à `__lazyLoad` (conteneur `__mountParamsContainer` de Livewire). */
179
+ /** Signed mount params of a lazy component, in a `__mountParamsContainer`: the browser sends them back unchanged to `__lazyLoad`. */
177
180
  encodeMountParams(name, params, request) {
178
181
  const body = { data: { forMount: dehydrateValue(params), forComponent: name }, memo: { id: randomId(), name: '__mountParamsContainer', path: request.path ?? '/', method: request.method ?? 'GET', release: 'a-a-a' } };
179
182
  return Buffer.from(JSON.stringify({ ...body, checksum: this.checksum(body) })).toString('base64');
@@ -184,19 +187,19 @@ export class WireRuntime {
184
187
  container = JSON.parse(Buffer.from(String(encoded), 'base64').toString('utf8'));
185
188
  }
186
189
  catch {
187
- throw new WireError(404, 'Paramètres de montage illisibles.');
190
+ throw new WireError(404, 'Unreadable mount params.');
188
191
  }
189
192
  if (!container || typeof container.data !== 'object' || typeof container.memo !== 'object' || typeof container.checksum !== 'string')
190
- throw new WireError(404, 'Paramètres de montage invalides.');
193
+ throw new WireError(404, 'Invalid mount params.');
191
194
  const expected = Buffer.from(this.checksum(container)), given = Buffer.from(container.checksum);
192
195
  if (expected.length !== given.length || !timingSafeEqual(expected, given))
193
- throw new WireError(419, 'Somme de contrôle des paramètres de montage invalide.');
194
- // Les paramètres signés pour un composant ne valent pas pour un autre.
196
+ throw new WireError(419, 'Invalid mount params checksum.');
197
+ // Params signed for one component are not valid for another.
195
198
  if (container.memo.name !== '__mountParamsContainer' || container.data.forComponent !== name)
196
- throw new WireError(403, 'Paramètres de montage destinés à un autre composant.');
199
+ throw new WireError(403, 'Mount params were issued for another component.');
197
200
  return (hydrateValue(container.data.forMount) ?? {});
198
201
  }
199
- /** Comme Livewire : un paramètre de montage du même nom qu'une propriété publique y est copié. */
202
+ /** A mount param named like a public property is copied into it. */
200
203
  assignMatchingProperties(component, params) {
201
204
  const known = new Set(publicProperties(component));
202
205
  for (const [k, v] of Object.entries(params))
@@ -207,16 +210,17 @@ export class WireRuntime {
207
210
  const html = await component.render();
208
211
  return insertAttributesIntoHtmlRoot(html, { 'wire:id': component.id, 'wire:name': name });
209
212
  }
210
- /** Premier rendu : HTML avec wire:snapshot et wire:effects sur la racine.
211
- * Le mémo ne porte `lazyLoaded` / `lazyIsolated` que pour un composant réellement « lazy » : leur seule présence fait ignorer les événements au client. */
213
+ /**
214
+ * Initial render: HTML with wire:snapshot and wire:effects on the root.
215
+ * The memo only carries `lazyLoaded` / `lazyIsolated` for lazy components: their mere presence makes the client ignore events.
216
+ */
212
217
  async mount(name, params = {}, request = {}) {
213
218
  return (await this.mountWithInstance(name, params, request)).html;
214
219
  }
215
- /** Comme mount(), mais rend aussi l'instance montée (pour lire son titre, par exemple). */
220
+ /** Like mount(), but also returns the mounted instance (to read its title, for example). */
216
221
  async mountWithInstance(name, params = {}, request = {}) {
217
222
  const component = this.make(name);
218
223
  component._ctx = request.ctx ?? null;
219
- // Identifiant alphanumérique de 20 caractères, comme Str::random(20) de Livewire (il sert dans des id HTML et des sélecteurs).
220
224
  component._id = randomId();
221
225
  component._runtime = this;
222
226
  component._request = { path: request.path, method: request.method };
@@ -224,7 +228,7 @@ export class WireRuntime {
224
228
  this.assignMatchingProperties(component, params);
225
229
  let html;
226
230
  if (request.lazy) {
227
- // Composant « lazy » : `mount()` et le rendu attendent l'appel `__lazyLoad` du navigateur.
231
+ // Lazy component: `mount()` and rendering wait for the browser's `__lazyLoad` call.
228
232
  const placeholder = insertAttributesIntoHtmlRoot(await component.placeholder(), { 'x-intersect': `$wire.__lazyLoad('${this.encodeMountParams(name, params, request)}')` });
229
233
  html = insertAttributesIntoHtmlRoot(placeholder, { 'wire:id': component.id, 'wire:name': name });
230
234
  }
@@ -238,19 +242,19 @@ export class WireRuntime {
238
242
  ...(request.lazy ? { lazyLoaded: false, lazyIsolated: true } : {}), ...(reactiveProps(component).length ? { props: reactiveProps(component) } : {}), locale: this.options.locale ?? 'en', islands: [],
239
243
  });
240
244
  const { html: _h, ...effects } = component._effects;
241
- // Écouteurs d'événements (équivalent de #[On]) : le client appellera `__dispatch(événement, paramètres)`.
245
+ // Event listeners: the client calls `__dispatch(event, params)` when one fires.
242
246
  const listeners = Object.keys(component.constructor.listeners ?? {});
243
247
  if (listeners.length)
244
248
  effects.listeners = listeners;
245
249
  return { html: insertAttributesIntoHtmlRoot(html, { ...(request.key !== undefined ? { 'wire:key': request.key } : {}), 'wire:snapshot': JSON.stringify(snapshot), 'wire:effects': JSON.stringify(effects) }), component };
246
250
  }
247
- /** Requête de mise à jour d'un composant : hydratation, mises à jour, appels, rendu, déshydratation. */
251
+ /** Component update request: hydrate, apply updates, run calls, render, dehydrate. */
248
252
  async update(snapshot, updates, calls, ctx = null) {
249
253
  if (!snapshot || typeof snapshot.data !== 'object' || typeof snapshot.memo !== 'object' || typeof snapshot.checksum !== 'string')
250
- throw new WireError(404, 'Snapshot invalide.');
254
+ throw new WireError(404, 'Invalid snapshot.');
251
255
  const expected = Buffer.from(this.checksum(snapshot)), given = Buffer.from(snapshot.checksum);
252
256
  if (expected.length !== given.length || !timingSafeEqual(expected, given))
253
- throw new WireError(419, 'Somme de contrôle du snapshot invalide.');
257
+ throw new WireError(419, 'Invalid snapshot checksum.');
254
258
  const name = snapshot.memo.name;
255
259
  const component = this.make(name);
256
260
  const cls = component.constructor;
@@ -259,7 +263,7 @@ export class WireRuntime {
259
263
  component._errors = { ...(snapshot.memo.errors ?? {}) };
260
264
  component._runtime = this;
261
265
  component._request = { path: snapshot.memo.path, method: snapshot.memo.method };
262
- // `memo.children` n'est pas couvert par la somme de contrôle (le client le tient à jour) : seules les entrées bien formées sont retenues.
266
+ // `memo.children` is not covered by the checksum (the client maintains it): only well-formed entries are kept.
263
267
  if (snapshot.memo.children && typeof snapshot.memo.children === 'object' && !Array.isArray(snapshot.memo.children)) {
264
268
  for (const [key, value] of Object.entries(snapshot.memo.children)) {
265
269
  if (Array.isArray(value) && /^[a-zA-Z][a-zA-Z0-9-]*$/.test(String(value[0])) && /^[A-Za-z0-9]{1,64}$/.test(String(value[1])))
@@ -272,7 +276,7 @@ export class WireRuntime {
272
276
  for (const [k, v] of Object.entries(snapshot.data))
273
277
  if (known.has(k))
274
278
  component[k] = hydrateValue(v);
275
- // Propriétés réactives : elles ne viennent que du parent, rendu plus tôt dans la même requête.
279
+ // Reactive props only come from the parent, rendered earlier in the same request.
276
280
  const reactive = new Set(reactiveProps(component));
277
281
  const passed = this.requestScope.getStore()?.childProps.get(component.id);
278
282
  if (passed)
@@ -285,14 +289,14 @@ export class WireRuntime {
285
289
  const segments = path.split('.');
286
290
  const prop = segments[0];
287
291
  if (!known.has(prop))
288
- throw new WireError(404, `Propriété publique introuvable : ${prop}`);
292
+ throw new WireError(404, `Public property not found: ${prop}`);
289
293
  if (cls.locked?.includes(prop))
290
- throw new WireError(403, `Propriété verrouillée : ${prop}`);
294
+ throw new WireError(403, `Locked property: ${prop}`);
291
295
  if (reactive.has(prop))
292
- throw new WireError(403, `Propriété réactive, fixée par le composant parent : ${prop}`);
293
- // Un segment comme `__proto__` écrirait dans le prototype partagé par tous les objets du serveur.
296
+ throw new WireError(403, `Reactive property, set by the parent component: ${prop}`);
297
+ // A segment such as `__proto__` would write into the prototype shared by every object on the server.
294
298
  if (segments.some((segment) => segment === '__proto__' || segment === 'constructor' || segment === 'prototype'))
295
- throw new WireError(403, `Chemin d'état refusé : ${path}`);
299
+ throw new WireError(403, `State path rejected: ${path}`);
296
300
  const anyC = component;
297
301
  await anyC.updating?.(path, value);
298
302
  await anyC[`updating${studly(prop)}`]?.(value, segments.slice(1).join('.') || null);
@@ -301,24 +305,24 @@ export class WireRuntime {
301
305
  await anyC[`updated${studly(prop)}`]?.(value, segments.slice(1).join('.') || null);
302
306
  };
303
307
  for (const [path, value] of Object.entries(updates)) {
304
- // Une référence de fichier temporaire renvoyée par le navigateur doit désigner un fichier réellement reçu.
308
+ // A temporary file reference sent by the browser must point to a file that was actually received.
305
309
  if (!uploadReferencesAreValid(value, this.uploads))
306
- throw new WireError(403, `Référence de fichier refusée : ${path}`);
310
+ throw new WireError(403, `File reference rejected: ${path}`);
307
311
  await applyUpdate(path, value);
308
312
  }
309
313
  const allowed = callableMethods(component);
310
314
  const returns = [];
311
315
  for (const call of calls) {
312
316
  if (typeof call?.method !== 'string' || !Array.isArray(call.params))
313
- throw new WireError(404, 'Appel invalide.');
314
- // Actions magiques : sans effet côté serveur, les valeurs voyagent dans `updates` (SupportMagicActions de Livewire).
317
+ throw new WireError(404, 'Invalid call.');
318
+ // Magic actions are no-ops on the server: their values travel in `updates`.
315
319
  if (MAGIC_ACTIONS.has(call.method)) {
316
320
  returns.push(null);
317
321
  continue;
318
322
  }
319
323
  if (call.method === '__lazyLoad') {
320
324
  if (!awaitsLazyLoad || lazyLoaded)
321
- throw new WireError(404, 'Ce composant n\'attend pas de chargement différé.');
325
+ throw new WireError(404, 'This component is not waiting for a lazy load.');
322
326
  const params = this.decodeMountParams(name, call.params[0]);
323
327
  this.assignMatchingProperties(component, params);
324
328
  await component.mount(params);
@@ -326,15 +330,15 @@ export class WireRuntime {
326
330
  returns.push(null);
327
331
  continue;
328
332
  }
329
- // Tant qu'il n'est pas chargé, un composant « lazy » n'a pas été monté : rien d'autre n'est appelable.
333
+ // Until it is loaded, a lazy component has not been mounted: nothing else is callable.
330
334
  if (awaitsLazyLoad && !lazyLoaded)
331
- throw new WireError(404, 'Composant non chargé.');
332
- // --- téléversement de fichiers (port de WithFileUploads) : la cible est un chemin d'état, soumis aux mêmes gardes qu'une mise à jour
335
+ throw new WireError(404, 'Component not loaded yet.');
336
+ // File uploads: the target is a state path, subject to the same guards as an update.
333
337
  if (call.method === '_startUpload' || call.method === '_finishUpload' || call.method === '_uploadErrored' || call.method === '_removeUpload') {
334
338
  const store = this.uploads;
335
339
  const target = call.params[0];
336
340
  if (!store || typeof target !== 'string' || target.length > 500)
337
- throw new WireError(404, 'Téléversement indisponible.');
341
+ throw new WireError(404, 'Uploads are not available.');
338
342
  if (call.method === '_startUpload') {
339
343
  component.dispatch('upload:generatedSignedUrl', { name: target, url: store.signedUrl(this.options.uploadPrefix ?? '/livewire') }, { self: true });
340
344
  component._skipRender = true;
@@ -342,7 +346,7 @@ export class WireRuntime {
342
346
  else if (call.method === '_finishUpload') {
343
347
  const ids = Array.isArray(call.params[1]) ? call.params[1].map(String) : [];
344
348
  if (!ids.length || ids.some((id) => !store.resolve(id)))
345
- throw new WireError(404, 'Fichier temporaire introuvable.');
349
+ throw new WireError(404, 'Temporary file not found.');
346
350
  await applyUpdate(target, call.params[2] === true ? ids.map((id) => UploadStore.reference(id)) : UploadStore.reference(ids[0]));
347
351
  component.dispatch('upload:finished', { name: target, tmpFilenames: ids }, { self: true });
348
352
  }
@@ -360,16 +364,16 @@ export class WireRuntime {
360
364
  if (call.method === '__dispatch') {
361
365
  const handler = cls.listeners?.[String(call.params[0])];
362
366
  if (!handler)
363
- throw new WireError(404, `Aucun écouteur pour l'événement : ${String(call.params[0])}`);
367
+ throw new WireError(404, `No listener for event: ${String(call.params[0])}`);
364
368
  const args = call.params[1] && typeof call.params[1] === 'object' ? Object.values(call.params[1]) : [];
365
369
  returns.push((await component[handler](...args)) ?? null);
366
370
  continue;
367
371
  }
368
372
  if (!allowed.has(call.method)) {
369
- // Aide au développement : la méthode existe mais n'est pas déclarée appelable.
373
+ // Development hint: the method exists but is not declared callable.
370
374
  if (process.env.NODE_ENV !== 'production' && typeof component[call.method] === 'function' && !isLifecycleHook(call.method))
371
- console.warn(`[wire] ${name} : « ${call.method} » existe mais n'est pas dans static callable — appel refusé.`);
372
- throw new WireError(404, `Méthode introuvable : ${call.method}`);
375
+ console.warn(`[wire] ${name}: "${call.method}" exists but is not listed in static callable; call rejected.`);
376
+ throw new WireError(404, `Method not found: ${call.method}`);
373
377
  }
374
378
  returns.push((await component[call.method](...call.params)) ?? null);
375
379
  }
@@ -384,7 +388,7 @@ export class WireRuntime {
384
388
  }
385
389
  await component.dehydrate();
386
390
  const { id: _i, name: _n, errors: _e, ...memo } = snapshot.memo;
387
- // Après un rendu complet, la liste des enfants est celle qui vient d'être rendue ; sinon elle est conservée.
391
+ // After a full render, the children list is the one just rendered; otherwise it is kept.
388
392
  if (rendered)
389
393
  memo.children = Object.keys(component._children).length ? component._children : [];
390
394
  if (lazyLoaded) {
@@ -393,17 +397,17 @@ export class WireRuntime {
393
397
  }
394
398
  return { snapshot: this.snapshot(component, name, memo), effects };
395
399
  }
396
- /** Point d'entrée HTTP : corps `{ _token, components: [{ snapshot, updates, calls }] }`. */
400
+ /** HTTP entry point; body: `{ _token, components: [{ snapshot, updates, calls }] }`. */
397
401
  async handle(body, ctx = null) {
398
402
  const list = body?.components;
399
403
  if (!Array.isArray(list) || list.length === 0)
400
- throw new WireError(404, 'Charge utile invalide.');
404
+ throw new WireError(404, 'Invalid payload.');
401
405
  const components = [];
402
- // Les composants d'une requête sont traités dans l'ordre : un parent passe ses propriétés réactives aux enfants qui le suivent.
406
+ // Components are processed in order so a parent can pass its reactive props to the children that follow it.
403
407
  await this.requestScope.run({ childProps: new Map() }, async () => {
404
408
  for (const c of list) {
405
409
  if (typeof c?.snapshot !== 'string' || typeof c.updates !== 'object' || !Array.isArray(c.calls))
406
- throw new WireError(404, 'Charge utile invalide.');
410
+ throw new WireError(404, 'Invalid payload.');
407
411
  const { snapshot, effects } = await this.update(JSON.parse(c.snapshot), c.updates ?? {}, c.calls, ctx);
408
412
  components.push({ snapshot: JSON.stringify(snapshot), effects });
409
413
  }
package/lib/uploads.d.ts CHANGED
@@ -22,16 +22,16 @@ export declare class UploadStore {
22
22
  private urlTtl;
23
23
  constructor(dir: string, key: string, options?: UploadOptions);
24
24
  private sign;
25
- /** Adresse d'envoi signée, valable quelques minutes. */
25
+ /** Signed upload URL, valid for a few minutes (`urlTtl`). */
26
26
  signedUrl(prefix?: string): string;
27
27
  verifySignature(expires: unknown, signature: unknown): boolean;
28
- /** Reçoit un envoi `multipart/form-data` (champ `files[]`) et rend les identifiants temporaires, dans l'ordre. */
28
+ /** Receives a `multipart/form-data` upload (`files[]` field) and returns the temporary ids, in order. */
29
29
  receive(req: IncomingMessage): Promise<string[]>;
30
- /** Fichier temporaire désigné par une valeur d'état (`livewire-file:<id>`) ou un identifiant, s'il existe encore. */
30
+ /** Temporary file referenced by a state value (`livewire-file:<id>`) or a bare id, if it still exists. */
31
31
  resolve(value: unknown): TemporaryFile | null;
32
32
  static isTemporary(value: unknown): boolean;
33
33
  static reference(id: string): string;
34
34
  remove(id: string): void;
35
- /** Supprime les fichiers temporaires plus vieux que la durée de conservation. */
35
+ /** Deletes temporary files older than the retention time (`ttl`). */
36
36
  private sweep;
37
37
  }
package/lib/uploads.js CHANGED
@@ -1,6 +1,5 @@
1
- // Téléversements temporaires (équivalent de WithFileUploads / TemporaryUploadedFile de Livewire).
2
- // Le navigateur demande une adresse signée (`_startUpload`), y envoie les fichiers, puis annonce leurs noms temporaires (`_finishUpload`) :
3
- // la propriété visée reçoit alors `livewire-file:<identifiant>`. Le fichier ne quitte le dossier temporaire que si l'application l'enregistre.
1
+ // Temporary uploads. The browser asks for a signed URL (`_startUpload`), posts the files to it, then reports their temporary ids
2
+ // (`_finishUpload`): the target property then holds `livewire-file:<id>`. A file only leaves the temporary directory if the app stores it.
4
3
  import busboy from 'busboy';
5
4
  import { createHmac, randomBytes, timingSafeEqual } from 'node:crypto';
6
5
  import { createWriteStream, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from 'node:fs';
@@ -8,7 +7,7 @@ import { open } from 'node:fs/promises';
8
7
  import path from 'node:path';
9
8
  const PREFIX = 'livewire-file:';
10
9
  const ID = /^[a-f0-9]{32}$/;
11
- /** Types reconnus à leur signature binaire : le type annoncé par le navigateur n'est jamais cru. */
10
+ /** Types detected from their magic bytes: the type announced by the browser is never trusted. */
12
11
  const SIGNATURES = [
13
12
  { type: 'image/jpeg', extension: 'jpg', test: (b) => b[0] === 0xff && b[1] === 0xd8 && b[2] === 0xff },
14
13
  { type: 'image/png', extension: 'png', test: (b) => b.subarray(0, 8).equals(Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])) },
@@ -17,7 +16,7 @@ const SIGNATURES = [
17
16
  { type: 'application/pdf', extension: 'pdf', test: (b) => b.subarray(0, 5).toString('latin1') === '%PDF-' },
18
17
  { type: 'application/zip', extension: 'zip', test: (b) => b[0] === 0x50 && b[1] === 0x4b && b[2] === 0x03 && b[3] === 0x04 },
19
18
  ];
20
- /** Texte brut : uniquement pour ces extensions, et seulement si le début du fichier ne contient pas d'octet nul. */
19
+ /** Plain text: only for these extensions, and only if the start of the file contains no null byte. */
21
20
  const TEXT = { txt: 'text/plain', csv: 'text/csv', json: 'application/json', md: 'text/markdown' };
22
21
  export class UploadStore {
23
22
  dir;
@@ -29,14 +28,14 @@ export class UploadStore {
29
28
  constructor(dir, key, options = {}) {
30
29
  this.dir = dir;
31
30
  this.key = key;
32
- this.maxFileSize = options.maxFileSize ?? 12 * 1024 * 1024; // 12 Mo, comme la règle par défaut de Livewire
31
+ this.maxFileSize = options.maxFileSize ?? 12 * 1024 * 1024; // 12 MB
33
32
  this.maxFiles = options.maxFiles ?? 20;
34
33
  this.ttl = options.ttl ?? 24 * 60 * 60 * 1000;
35
34
  this.urlTtl = options.urlTtl ?? 5 * 60 * 1000;
36
35
  mkdirSync(dir, { recursive: true });
37
36
  }
38
37
  sign(expires) { return createHmac('sha256', this.key).update(`upload:${expires}`).digest('hex'); }
39
- /** Adresse d'envoi signée, valable quelques minutes. */
38
+ /** Signed upload URL, valid for a few minutes (`urlTtl`). */
40
39
  signedUrl(prefix = '/livewire') {
41
40
  const expires = Date.now() + this.urlTtl;
42
41
  return `${prefix}/upload-file?expires=${expires}&signature=${this.sign(expires)}`;
@@ -48,7 +47,7 @@ export class UploadStore {
48
47
  const expected = Buffer.from(this.sign(at)), given = Buffer.from(String(signature ?? ''));
49
48
  return expected.length === given.length && timingSafeEqual(expected, given);
50
49
  }
51
- /** Reçoit un envoi `multipart/form-data` (champ `files[]`) et rend les identifiants temporaires, dans l'ordre. */
50
+ /** Receives a `multipart/form-data` upload (`files[]` field) and returns the temporary ids, in order. */
52
51
  receive(req) {
53
52
  this.sweep();
54
53
  return new Promise((resolve, reject) => {
@@ -61,7 +60,7 @@ export class UploadStore {
61
60
  parser = busboy({ headers: req.headers, limits: { fileSize: this.maxFileSize, files: this.maxFiles, fields: 5 } });
62
61
  }
63
62
  catch {
64
- reject(Object.assign(new Error('Envoi invalide.'), { status: 422 }));
63
+ reject(Object.assign(new Error('Invalid upload.'), { status: 422 }));
65
64
  return;
66
65
  }
67
66
  parser.on('file', (_field, stream, info) => {
@@ -69,8 +68,8 @@ export class UploadStore {
69
68
  const target = path.join(this.dir, id);
70
69
  ids.push(id);
71
70
  const out = createWriteStream(target, { flags: 'wx' });
72
- stream.on('limit', () => fail('Fichier trop volumineux.'));
73
- pending.push(new Promise((done) => { out.on('close', done); out.on('error', () => { fail('Écriture impossible.'); done(); }); }).then(async () => {
71
+ stream.on('limit', () => fail('File too large.'));
72
+ pending.push(new Promise((done) => { out.on('close', done); out.on('error', () => { fail('Could not write the file.'); done(); }); }).then(async () => {
74
73
  if (failure)
75
74
  return;
76
75
  const detected = await detect(target, info.filename);
@@ -78,14 +77,14 @@ export class UploadStore {
78
77
  }));
79
78
  stream.pipe(out);
80
79
  });
81
- parser.on('filesLimit', () => fail('Trop de fichiers.'));
82
- parser.on('error', () => fail('Envoi invalide.'));
80
+ parser.on('filesLimit', () => fail('Too many files.'));
81
+ parser.on('error', () => fail('Invalid upload.'));
83
82
  parser.on('close', async () => {
84
83
  await Promise.all(pending);
85
84
  if (failure || !ids.length) {
86
85
  for (const id of ids)
87
86
  this.remove(id);
88
- reject(failure ?? Object.assign(new Error('Aucun fichier.'), { status: 422 }));
87
+ reject(failure ?? Object.assign(new Error('No file.'), { status: 422 }));
89
88
  return;
90
89
  }
91
90
  resolve(ids);
@@ -93,7 +92,7 @@ export class UploadStore {
93
92
  req.pipe(parser);
94
93
  });
95
94
  }
96
- /** Fichier temporaire désigné par une valeur d'état (`livewire-file:<id>`) ou un identifiant, s'il existe encore. */
95
+ /** Temporary file referenced by a state value (`livewire-file:<id>`) or a bare id, if it still exists. */
97
96
  resolve(value) {
98
97
  const id = typeof value === 'string' ? (value.startsWith(PREFIX) ? value.slice(PREFIX.length) : value) : '';
99
98
  if (!ID.test(id))
@@ -117,7 +116,7 @@ export class UploadStore {
117
116
  for (const suffix of ['', '.json'])
118
117
  rmSync(path.join(this.dir, id + suffix), { force: true });
119
118
  }
120
- /** Supprime les fichiers temporaires plus vieux que la durée de conservation. */
119
+ /** Deletes temporary files older than the retention time (`ttl`). */
121
120
  sweep() {
122
121
  const limit = Date.now() - this.ttl;
123
122
  try {
@@ -127,7 +126,7 @@ export class UploadStore {
127
126
  rmSync(file, { force: true });
128
127
  }
129
128
  }
130
- catch { /* dossier inaccessible : rien à nettoyer */ }
129
+ catch { /* directory unreadable: nothing to sweep */ }
131
130
  }
132
131
  }
133
132
  async function detect(file, filename) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@navette-js/wire",
3
- "version": "1.0.0-beta.1",
3
+ "version": "1.0.0-beta.3",
4
4
  "description": "Server-side runtime compatible with Livewire 3 for Node.js and Express.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -36,7 +36,6 @@
36
36
  },
37
37
  "keywords": [
38
38
  "navette",
39
- "filament",
40
39
  "admin",
41
40
  "panel",
42
41
  "livewire",