@navette-js/wire 0.0.0-stage → 1.0.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 NavetteJS
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,9 @@
1
- # Temporary Holding Version
1
+ # @navette-js/wire
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Server-side runtime compatible with Livewire 3 for Node.js and Express.
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`.
6
+
7
+ **Beta:** the API may still change between beta releases.
8
+
9
+ License: MIT. Third-party notices: see `THIRD-PARTY-NOTICES.md` where present.
@@ -0,0 +1,12 @@
1
+ # Third-party notices
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.
4
+
5
+ | Work | Used for | License | License file |
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` |
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
+ | [Heroicons](https://github.com/tailwindlabs/heroicons) | SVG icons (`@navette-js/support/icons`) | MIT | `packages/support/icons/HEROICONS-LICENSE.md` |
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` |
11
+
12
+ Runtime dependencies installed from npm (Express, Knex, Objection, better-sqlite3, Tiptap, EJS, Busboy…) keep their own licenses.
@@ -0,0 +1,15 @@
1
+ import type { Request, Response, Router } from 'express';
2
+ import { type WireRuntime } from './index.ts';
3
+ /** Jeton CSRF par double soumission : cookie HttpOnly + jeton renvoyé dans le corps par livewire.js (`_token`). */
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. */
6
+ export declare function verifyCsrf(req: Request, token: unknown): boolean;
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…). */
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. */
11
+ canUpload?: (ctx: any) => boolean | Promise<boolean>): Router;
12
+ /** Balise <script> attendue par livewire.js (équivalent de FrontendAssets::js). */
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>. */
15
+ export declare function wireStyles(progressBarColor?: string): string;
package/lib/express.js ADDED
@@ -0,0 +1,92 @@
1
+ // Intégration Express du runtime : sert livewire.js embarqué, expose l'endpoint de mise à jour, protège par jeton CSRF.
2
+ import { randomBytes, timingSafeEqual } from 'node:crypto';
3
+ import { readFileSync } from 'node:fs';
4
+ import { fileURLToPath } from 'node:url';
5
+ import express from 'express';
6
+ import { WireError } from "./index.js";
7
+ const vendor = (f) => fileURLToPath(new URL(`../vendor/${f}`, import.meta.url));
8
+ const versionHash = JSON.parse(readFileSync(vendor('manifest.json'), 'utf8'))['/livewire.js'];
9
+ const COOKIE = 'navette_csrf';
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`). */
12
+ export function csrfToken(req, res) {
13
+ let token = readCookie(req, COOKIE) ?? res.locals.navetteCsrf;
14
+ if (!token) {
15
+ token = randomBytes(24).toString('base64url');
16
+ res.cookie(COOKIE, token, { httpOnly: true, sameSite: 'lax', secure: req.secure === true });
17
+ res.locals.navetteCsrf = token;
18
+ }
19
+ return token;
20
+ }
21
+ /** Vérifie le jeton envoyé (corps ou formulaire) contre le cookie, en temps constant. */
22
+ export function verifyCsrf(req, token) {
23
+ const cookie = Buffer.from(readCookie(req, COOKIE) ?? ''), sent = Buffer.from(String(token ?? ''));
24
+ return cookie.length > 0 && cookie.length === sent.length && timingSafeEqual(cookie, sent);
25
+ }
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…). */
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. */
30
+ canUpload = () => true) {
31
+ const router = express.Router();
32
+ router.get(`${prefix}/livewire.js`, (_req, res) => {
33
+ res.type('application/javascript').set('Cache-Control', 'public, max-age=31536000, immutable').sendFile(vendor('livewire.js'));
34
+ });
35
+ router.post(`${prefix}/update`, express.json({ limit: '2mb' }), async (req, res) => {
36
+ try {
37
+ if (!verifyCsrf(req, req.body?._token))
38
+ throw new WireError(419, 'Jeton CSRF invalide.');
39
+ const ctx = await makeContext(req, res);
40
+ res.json(await around(ctx, req, res, () => runtime.handle(req.body, ctx)));
41
+ }
42
+ catch (e) {
43
+ const status = e instanceof WireError ? e.status : 500;
44
+ if (status === 500)
45
+ console.error(e);
46
+ else if (process.env.NODE_ENV !== 'production')
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');
49
+ }
50
+ });
51
+ // Envoi de fichiers : adresse signée par `_startUpload`, jeton CSRF en en-tête, fichiers reçus dans le dossier temporaire.
52
+ router.post(`${prefix}/upload-file`, async (req, res) => {
53
+ try {
54
+ const store = runtime.uploads;
55
+ if (!store)
56
+ throw new WireError(404, 'Téléversement indisponible.');
57
+ if (!store.verifySignature(req.query.expires, req.query.signature))
58
+ throw new WireError(401, 'Adresse d\'envoi invalide ou expirée.');
59
+ if (!verifyCsrf(req, req.get('x-csrf-token')))
60
+ throw new WireError(419, 'Jeton CSRF invalide.');
61
+ if (!(await canUpload(await makeContext(req, res))))
62
+ throw new WireError(401, 'Non authentifié.');
63
+ res.json({ paths: await store.receive(req) });
64
+ }
65
+ catch (e) {
66
+ const status = e instanceof WireError ? e.status : Number(e?.status) || 500;
67
+ if (status === 500)
68
+ console.error(e);
69
+ res.status(status).json({ message: e instanceof Error ? e.message : 'Erreur' });
70
+ }
71
+ });
72
+ return router;
73
+ }
74
+ /** Balise <script> attendue par livewire.js (équivalent de FrontendAssets::js). */
75
+ export function wireScripts(req, res, prefix = '/livewire') {
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
+ }
78
+ /** Styles attendus par livewire.js (équivalent de FrontendAssets::styles) : à placer dans <head>. */
79
+ export function wireStyles(progressBarColor = '#2299dd') {
80
+ const BS = String.fromCharCode(92); // barre oblique inverse : échappe « : » et « . » dans les sélecteurs CSS
81
+ const sel = (n) => `[wire${BS}:${n.replaceAll('.', BS + '.')}]`;
82
+ const doubled = (names) => names.map((n) => sel(n) + sel(n)).join(', ');
83
+ return '<!-- Livewire Styles --><style>'
84
+ + doubled(['loading', 'loading.delay', 'loading.list-item', 'loading.inline-block', 'loading.inline', 'loading.block', 'loading.flex', 'loading.table', 'loading.grid', 'loading.inline-flex']) + ' {display: none;}'
85
+ + doubled(['loading.delay.none', 'loading.delay.shortest', 'loading.delay.shorter', 'loading.delay.short', 'loading.delay.default', 'loading.delay.long', 'loading.delay.longer', 'loading.delay.longest']) + ' {display: none;}'
86
+ + doubled(['offline']) + ' {display: none;}'
87
+ + sel('dirty') + ':not(textarea):not(input):not(select) {display: none;}'
88
+ + `:root {--livewire-progress-bar-color: ${progressBarColor};}`
89
+ + '[x-cloak="x-cloak"],[x-cloak=""] {display: none !important;}'
90
+ + sel('cloak').replace(']', '="wire:cloak"]') + ',' + sel('cloak').replace(']', '=""]') + ' {display: none !important;}'
91
+ + 'dialog#livewire-error::backdrop {background-color: rgba(0, 0, 0, .6);}</style>';
92
+ }
package/lib/index.d.ts ADDED
@@ -0,0 +1,149 @@
1
+ import { UploadStore } from './uploads.ts';
2
+ export { UploadStore, type TemporaryFile } from './uploads.ts';
3
+ export type Effects = {
4
+ html?: string;
5
+ returns?: unknown[];
6
+ dispatches?: {
7
+ name: string;
8
+ params: Record<string, unknown>;
9
+ self?: boolean;
10
+ to?: string;
11
+ }[];
12
+ redirect?: string;
13
+ redirectUsingNavigate?: boolean;
14
+ partials?: Record<string, string>;
15
+ [key: string]: unknown;
16
+ };
17
+ export type Snapshot = {
18
+ data: Record<string, unknown>;
19
+ memo: Record<string, any>;
20
+ checksum: string;
21
+ };
22
+ export type Call = {
23
+ method: string;
24
+ params: unknown[];
25
+ metadata?: Record<string, unknown>;
26
+ };
27
+ type ComponentClass = (new () => Component) & {
28
+ componentName?: string;
29
+ callable?: string[];
30
+ locked?: string[];
31
+ reactive?: string[];
32
+ listeners?: Record<string, string>;
33
+ };
34
+ export declare class WireError extends Error {
35
+ status: number;
36
+ constructor(status: number, message: string);
37
+ }
38
+ /** Classe de base d'un composant. Toute propriété propre énumérable ne commençant pas par `_` est de l'état public. */
39
+ export declare abstract class Component {
40
+ _id: string;
41
+ _effects: Effects;
42
+ _skipRender: boolean;
43
+ _errors: Record<string, string[]>;
44
+ /** Contexte de la requête HTTP en cours (requête, réponse, panel…), fourni par l'intégration. */
45
+ _ctx: any;
46
+ /** Runtime et requête d'origine : servent à monter des composants enfants pendant le rendu. */
47
+ _runtime: WireRuntime | null;
48
+ _request: {
49
+ path?: string;
50
+ method?: string;
51
+ };
52
+ /** Enfants rendus pendant cette requête, et enfants déjà présents dans le navigateur : clé → [balise, identifiant]. */
53
+ _children: Record<string, [string, string]>;
54
+ _previousChildren: Record<string, [string, string]>;
55
+ /** HTML affiché tant qu'un composant « lazy » n'est pas chargé : une seule racine. */
56
+ placeholder(): string | Promise<string>;
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.
61
+ */
62
+ mountChild(name: string, params: Record<string, unknown>, key: string, options?: {
63
+ lazy?: boolean;
64
+ }): Promise<string>;
65
+ /** Rend le HTML du composant : une seule racine. */
66
+ abstract render(): string | Promise<string>;
67
+ mount(_params: Record<string, unknown>): void | Promise<void>;
68
+ boot(): void | Promise<void>;
69
+ hydrate(): void | Promise<void>;
70
+ dehydrate(): void | Promise<void>;
71
+ get id(): string;
72
+ /** `self` : l'événement n'est reçu que par ce composant (équivalent de `->self()`). */
73
+ dispatch(name: string, params?: Record<string, unknown> | unknown[], options?: {
74
+ self?: boolean;
75
+ }): void;
76
+ redirect(url: string, navigate?: boolean): void;
77
+ skipRender(): void;
78
+ /** Rendu partiel à la Filament : seul le fragment `wire:partial="<nom>"` est renvoyé (effects.partials), sans re-rendre tout le composant. */
79
+ renderPartial(name: string, html: string): void;
80
+ addError(path: string, message: string): void;
81
+ resetErrorBag(): void;
82
+ }
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.
87
+ */
88
+ export declare function callableMethods(component: Component): Set<string>;
89
+ export declare function publicProperties(component: Component): string[];
90
+ export declare function dehydrateValue(value: unknown): unknown;
91
+ export declare function hydrateValue(value: unknown): unknown;
92
+ /** Équivalent de Utils::insertAttributesIntoHtmlRoot : pose des attributs sur la balise racine. */
93
+ export declare function insertAttributesIntoHtmlRoot(html: string, attributes: Record<string, unknown>): string;
94
+ export type RuntimeOptions = {
95
+ key: string;
96
+ locale?: string; /** Dossier des téléversements temporaires ; sans lui, les envois de fichiers sont refusés. */
97
+ uploads?: UploadStore;
98
+ uploadPrefix?: string;
99
+ };
100
+ export declare class WireRuntime {
101
+ private registry;
102
+ private options;
103
+ constructor(options: RuntimeOptions);
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. */
106
+ private requestScope;
107
+ passPropsToChild(childId: string, params: Record<string, unknown>): void;
108
+ register(name: string, cls: ComponentClass): this;
109
+ private make;
110
+ checksum(snapshot: Omit<Snapshot, 'checksum'>): string;
111
+ private snapshot;
112
+ /** Paramètres de montage d'un composant « lazy », signés : le navigateur les renvoie tels quels à `__lazyLoad` (conteneur `__mountParamsContainer` de Livewire). */
113
+ private encodeMountParams;
114
+ private decodeMountParams;
115
+ /** Comme Livewire : un paramètre de montage du même nom qu'une propriété publique y est copié. */
116
+ private assignMatchingProperties;
117
+ 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. */
120
+ mount(name: string, params?: Record<string, unknown>, request?: {
121
+ path?: string;
122
+ method?: string;
123
+ ctx?: unknown;
124
+ }): Promise<string>;
125
+ /** Comme mount(), mais rend aussi l'instance montée (pour lire son titre, par exemple). */
126
+ mountWithInstance(name: string, params?: Record<string, unknown>, request?: {
127
+ path?: string;
128
+ method?: string;
129
+ ctx?: unknown;
130
+ lazy?: boolean;
131
+ key?: string;
132
+ }): Promise<{
133
+ html: string;
134
+ component: Component;
135
+ }>;
136
+ /** Requête de mise à jour d'un composant : hydratation, mises à jour, appels, rendu, déshydratation. */
137
+ update(snapshot: Snapshot, updates: Record<string, unknown>, calls: Call[], ctx?: unknown): Promise<{
138
+ snapshot: Snapshot;
139
+ effects: Effects;
140
+ }>;
141
+ /** Point d'entrée HTTP : corps `{ _token, components: [{ snapshot, updates, calls }] }`. */
142
+ handle(body: any, ctx?: unknown): Promise<{
143
+ components: {
144
+ snapshot: string;
145
+ effects: Effects;
146
+ }[];
147
+ assets: unknown[];
148
+ }>;
149
+ }