vite-plugin-milpa 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 +21 -0
- package/README.md +101 -0
- package/index.d.ts +47 -0
- package/index.js +145 -0
- package/package.json +70 -0
- package/router.d.ts +31 -0
- package/router.js +123 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 @Calcifux (Carlos Guillermo Reyes Ramiro)
|
|
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
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# vite-plugin-milpa
|
|
2
|
+
|
|
3
|
+
> Vite integration for the [milpa](https://pypi.org/project/milpa-core/) framework (FastAPI + Jinja),
|
|
4
|
+
> in the spirit of `laravel-vite-plugin`. Docs below in Spanish — milpa's home language.
|
|
5
|
+
|
|
6
|
+
El `laravel-vite-plugin` de **milpa**: conecta tu app Vite (React, Vue, Svelte, vanilla…)
|
|
7
|
+
con el backend milpa, que sirve el shell Jinja e inyecta tus assets con su helper `vite()`.
|
|
8
|
+
|
|
9
|
+
```js
|
|
10
|
+
// vite.config.js — todo el pegamento en una llamada
|
|
11
|
+
import {defineConfig} from 'vite';
|
|
12
|
+
import react from '@vitejs/plugin-react';
|
|
13
|
+
import {milpa} from 'vite-plugin-milpa';
|
|
14
|
+
|
|
15
|
+
export default defineConfig({
|
|
16
|
+
plugins: [
|
|
17
|
+
react(),
|
|
18
|
+
milpa({
|
|
19
|
+
entry: 'src/main.jsx',
|
|
20
|
+
pwa: {shell: '/spa'}, // opcional — omite para un SPA sin PWA
|
|
21
|
+
}),
|
|
22
|
+
],
|
|
23
|
+
});
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
```jinja
|
|
27
|
+
{# El template Jinja de TU app (milpa lo sirve) #}
|
|
28
|
+
{{ vite_react_refresh(app='demo-spa') }}
|
|
29
|
+
{{ vite('src/main.jsx', app='demo-spa') }}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Qué hace
|
|
33
|
+
|
|
34
|
+
1. **`base` + `build.manifest` + entry** — lo que el helper `vite()` de milpa necesita para
|
|
35
|
+
emitir `<link>`/`<script>` hasheados en prod. La base se deriva de la carpeta:
|
|
36
|
+
`surcos/<app>` → `/vite/<app>/`.
|
|
37
|
+
2. **Build a `public/<app>` del proyecto** (estilo `mix.js` → `public/` de Laravel): milpa
|
|
38
|
+
monta `public/` completo y un solo mount sirve a todas las apps.
|
|
39
|
+
3. **Hot-file (modelo Laravel)** — `npm run dev` escribe `./hot` con la URL real del dev
|
|
40
|
+
server y lo borra al apagarse; milpa decide dev/prod por su existencia. Un hot-file POR
|
|
41
|
+
app: tu equipo en dev con HMR mientras los demás corren su build.
|
|
42
|
+
4. **PWA opcional** ([Serwist](https://serwist.pages.dev) `injectManifest`) — compila
|
|
43
|
+
`src/sw.js` a un `sw.js` ESTÁTICO junto al build, precachea el shell del backend y
|
|
44
|
+
prefija los precache entries con la base (los relativos 404ean bajo subpath). Solo en
|
|
45
|
+
build: en dev el SW pelearía con HMR.
|
|
46
|
+
5. **`ASSET_URL`** (env, solo build) — se antepone a la base derivada, igual que en
|
|
47
|
+
laravel-vite-plugin: deploy bajo sub-ruta de reverse proxy (`ASSET_URL=/nombre-reverse`)
|
|
48
|
+
o CDN, sin tocar el vite.config. Es la MISMA env var que lee el backend milpa en runtime.
|
|
49
|
+
|
|
50
|
+
## Opciones
|
|
51
|
+
|
|
52
|
+
| Opción | Default | Qué controla |
|
|
53
|
+
|---|---|---|
|
|
54
|
+
| `entry` | `'src/main.jsx'` | Entry point (va a `rollupOptions.input`) |
|
|
55
|
+
| `assetsUrl` | `${ASSET_URL}/vite/<carpeta>` | URL pública donde milpa sirve esta app |
|
|
56
|
+
| `publicDir` | `'../../public'` | `public/` del proyecto, relativo a la app |
|
|
57
|
+
| `hotFile` | `'hot'` | Ruta del hot-file, relativa a la app |
|
|
58
|
+
| `pwa` | `false` | `false` \| `{swSrc, swDest, shell, globDirectory}` |
|
|
59
|
+
|
|
60
|
+
Nada es mandatorio: cualquier opción se overridea, o no uses el plugin y configura a mano —
|
|
61
|
+
milpa solo necesita el manifest, la base y el hot-file.
|
|
62
|
+
|
|
63
|
+
## `vite-plugin-milpa/router` — file-based routing (react-router 7)
|
|
64
|
+
|
|
65
|
+
El subpath **runtime** del paquete (mismo patrón que `@serwist/vite/worker`): rutas por
|
|
66
|
+
convención de archivos para `createBrowserRouter` (modo library — el que usa un SPA servido
|
|
67
|
+
por un backend), sin plugin extra, sin codegen, sin virtual modules. ~70 líneas auditables.
|
|
68
|
+
|
|
69
|
+
```js
|
|
70
|
+
// src/router.jsx de TU app — los globs van en TU código (import.meta.glob es
|
|
71
|
+
// compile-time y resuelve relativo al archivo que lo contiene):
|
|
72
|
+
import {buildRoutes} from 'vite-plugin-milpa/router';
|
|
73
|
+
|
|
74
|
+
const routes = buildRoutes({
|
|
75
|
+
pages: import.meta.glob('./pages/**/*.jsx'), // rutas core del shell
|
|
76
|
+
modules: import.meta.glob('./modules/*/pages/**/*.jsx'), // rutas por módulo /<m>/...
|
|
77
|
+
});
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
| Convención | Resultado |
|
|
81
|
+
|---|---|
|
|
82
|
+
| `pages/acerca.jsx` | `/acerca` |
|
|
83
|
+
| `pages/index.jsx` | `/` (índice de su carpeta) |
|
|
84
|
+
| `pages/productos/[id].jsx` | `/productos/:id` (`useParams()`) |
|
|
85
|
+
| `modules/tienda/pages/index.jsx` | `/tienda` (espejo de `Modules/<X>/Http` del backend) |
|
|
86
|
+
| `modules/tienda/pages/_layout.jsx` | layout del módulo (envuelve sus rutas; opcional) |
|
|
87
|
+
| `_loquesea.jsx` | ignorado (prefijo `_` = no-ruta) |
|
|
88
|
+
|
|
89
|
+
Globs SIN eager ⇒ cada página es un chunk propio (code-splitting por ruta gratis). Acepta
|
|
90
|
+
`.jsx/.tsx/.js/.ts`. Peers: `react >=18` y `react-router >=7` — **opcionales**: un surco
|
|
91
|
+
vanilla usa el plugin sin React y sin warnings (solo los necesita quien importa `/router`).
|
|
92
|
+
|
|
93
|
+
## El lado milpa
|
|
94
|
+
|
|
95
|
+
Requiere milpa-core con el helper Vite (`Core/View/Vite.py`): settings `VITE_APPS_DIR`
|
|
96
|
+
(default `surcos`), `VITE_PUBLIC_DIR` (default `public`), `VITE_ASSETS_URL` (default `/vite`).
|
|
97
|
+
Sin apps detectadas, la integración simplemente no se activa.
|
|
98
|
+
|
|
99
|
+
## Licencia
|
|
100
|
+
|
|
101
|
+
[MIT](./LICENSE)
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// Tipos de vite-plugin-milpa (el plugin build-time). El fuente es JS plano
|
|
2
|
+
// auditable a propósito (lo que está en npm ES lo que corre); este .d.ts da el
|
|
3
|
+
// soporte TypeScript de primera. Subpath runtime: ver router.d.ts.
|
|
4
|
+
import type {Plugin} from 'vite';
|
|
5
|
+
|
|
6
|
+
export interface MilpaPwaOptions {
|
|
7
|
+
/** Fuente del Service Worker, relativo a la app. Default: 'src/sw.js'. */
|
|
8
|
+
swSrc?: string;
|
|
9
|
+
/** Nombre del SW dentro del build. Default: 'sw.js'. */
|
|
10
|
+
swDest?: string;
|
|
11
|
+
/**
|
|
12
|
+
* Ruta del shell del backend a precachear (cold-start offline + fallback),
|
|
13
|
+
* p. ej. '/spa'. Bajo reverse proxy con sub-ruta, antepón el prefijo del
|
|
14
|
+
* deploy (es build-time, igual que la base de los chunks).
|
|
15
|
+
*/
|
|
16
|
+
shell?: string;
|
|
17
|
+
/** Carpeta a globear para el precache. Default: el outDir del build. */
|
|
18
|
+
globDirectory?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface MilpaOptions {
|
|
22
|
+
/** Entry de la app. Default: 'src/main.jsx'. */
|
|
23
|
+
entry?: string;
|
|
24
|
+
/**
|
|
25
|
+
* Base pública EXPLÍCITA de los assets (toma control total). Default
|
|
26
|
+
* derivada: `${ASSET_URL}/vite/<nombre-de-la-carpeta>/` — ASSET_URL es la
|
|
27
|
+
* misma env var que lee el backend milpa en runtime (deploy bajo sub-ruta
|
|
28
|
+
* de reverse proxy o CDN).
|
|
29
|
+
*/
|
|
30
|
+
assetsUrl?: string | null;
|
|
31
|
+
/**
|
|
32
|
+
* Raíz public/ del PROYECTO backend (estilo Laravel), relativa a la app:
|
|
33
|
+
* el build cae en `<publicDir>/<app>/`. Default: '../../public'.
|
|
34
|
+
*/
|
|
35
|
+
publicDir?: string;
|
|
36
|
+
/** Ruta del hot-file que decide dev/prod (modelo Laravel). Default: 'hot'. */
|
|
37
|
+
hotFile?: string;
|
|
38
|
+
/** PWA opcional con Serwist (injectManifest). Default: false. */
|
|
39
|
+
pwa?: boolean | MilpaPwaOptions;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* TODO el pegamento backend-integration de milpa en una llamada (= el
|
|
44
|
+
* laravel-vite-plugin de milpa): base + manifest + outDir, hot-file para HMR,
|
|
45
|
+
* y PWA opcional. Cada opción se puede overridear; nada es mandatorio.
|
|
46
|
+
*/
|
|
47
|
+
export declare function milpa(options?: MilpaOptions): Plugin[];
|
package/index.js
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
// vite-plugin-milpa — el "laravel-vite-plugin" de milpa (tu mix.js, pero mínimo).
|
|
2
|
+
//
|
|
3
|
+
// TODO el pegamento backend-integration en UNA llamada en vite.config.js:
|
|
4
|
+
//
|
|
5
|
+
// import {milpa} from 'vite-plugin-milpa';
|
|
6
|
+
// export default defineConfig({
|
|
7
|
+
// plugins: [react(), milpa({pwa: {shell: '/spa'}})],
|
|
8
|
+
// });
|
|
9
|
+
//
|
|
10
|
+
// Qué hace por ti (cada pieza es lo que antes se configuraba a mano):
|
|
11
|
+
// 1. base + build.manifest + rollupOptions.input → lo que el helper Jinja
|
|
12
|
+
// vite() de milpa necesita para emitir <link>/<script> hasheados.
|
|
13
|
+
// La base default se DERIVA de la carpeta: surcos/<app> → /vite/<app>/
|
|
14
|
+
// (la misma convención con la que milpa monta cada app — microfrontends).
|
|
15
|
+
// 2. HOT-FILE (modelo Laravel): `npm run dev` escribe ./hot con la URL real
|
|
16
|
+
// del dev server y lo borra al apagarse; milpa decide dev/prod por su
|
|
17
|
+
// existencia. Un hot-file POR app: tu equipo en dev, los demás en build.
|
|
18
|
+
// 3. PWA opcional (Serwist injectManifest): compila src/sw.js → dist/sw.js
|
|
19
|
+
// ESTÁTICO, precachea el shell del backend, y PREFIJA los entries con la
|
|
20
|
+
// base (gotcha real: salen relativos y 404earían bajo subpath). Solo en
|
|
21
|
+
// build — en dev el SW pelearía con HMR.
|
|
22
|
+
//
|
|
23
|
+
// Nada es mandatorio: cualquier opción se overridea, o no uses el plugin y
|
|
24
|
+
// configura a mano — milpa solo necesita el manifest, la base y el hot-file.
|
|
25
|
+
|
|
26
|
+
import {writeFileSync, rmSync} from 'node:fs';
|
|
27
|
+
import {basename, resolve} from 'node:path';
|
|
28
|
+
import {serwist} from '@serwist/vite';
|
|
29
|
+
|
|
30
|
+
const DEFAULTS = {
|
|
31
|
+
entry: 'src/main.jsx',
|
|
32
|
+
assetsUrl: null, // null => derivada: /vite/<nombre-de-la-carpeta>
|
|
33
|
+
publicDir: '../../public', // raíz public/ del PROYECTO (estilo Laravel): el build
|
|
34
|
+
// de cada surco cae en public/<app>/ y milpa monta
|
|
35
|
+
// public/ completo — relativo a la carpeta de la app.
|
|
36
|
+
hotFile: 'hot',
|
|
37
|
+
pwa: false, // false | true | {swSrc, swDest, shell, ...}
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export function milpa(userOptions = {}) {
|
|
41
|
+
const options = {...DEFAULTS, ...userOptions};
|
|
42
|
+
const plugins = [configPlugin(options), hotFilePlugin(options)];
|
|
43
|
+
if (options.pwa) plugins.push(...pwaPlugins(options));
|
|
44
|
+
return plugins;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// public/<app> de ESTA app (el outDir del build), relativo al root del frontend.
|
|
48
|
+
function resolveOutDir(options, root) {
|
|
49
|
+
const appRoot = resolve(root ?? '.');
|
|
50
|
+
return resolve(appRoot, options.publicDir, basename(appRoot));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// La base de la app: explícita, o derivada de la carpeta (surcos/<app> → /vite/<app>/).
|
|
54
|
+
// ASSET_URL (env, solo build) se antepone a la derivada — el MISMO env var que lee
|
|
55
|
+
// el backend milpa en runtime, igual que laravel-vite-plugin: deploy bajo sub-ruta
|
|
56
|
+
// de reverse proxy (ASSET_URL=/nombre-reverse) o CDN, sin tocar el vite.config.
|
|
57
|
+
// Un assetsUrl explícito toma control total (no se le antepone nada).
|
|
58
|
+
function resolveBase(options, root) {
|
|
59
|
+
const assetUrl = (process.env.ASSET_URL ?? '').replace(/\/+$/, '');
|
|
60
|
+
const url = options.assetsUrl ?? `${assetUrl}/vite/${basename(resolve(root ?? '.'))}`;
|
|
61
|
+
return url.endsWith('/') ? url : `${url}/`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function configPlugin(options) {
|
|
65
|
+
return {
|
|
66
|
+
name: 'milpa:config',
|
|
67
|
+
config(config, {command}) {
|
|
68
|
+
return {
|
|
69
|
+
// En dev la base es '/' (los módulos salen del dev server);
|
|
70
|
+
// en build es donde milpa servirá dist/ (los chunks se
|
|
71
|
+
// referencian entre sí con esta base).
|
|
72
|
+
base: command === 'build' ? resolveBase(options, config.root) : '/',
|
|
73
|
+
build: {
|
|
74
|
+
manifest: true,
|
|
75
|
+
rollupOptions: {input: options.entry},
|
|
76
|
+
// El build cae en public/<app> del PROYECTO (como mix.js a
|
|
77
|
+
// public/ en Laravel). emptyOutDir explícito: Vite no limpia
|
|
78
|
+
// solo los outDir fuera del root del frontend.
|
|
79
|
+
outDir: resolveOutDir(options, config.root),
|
|
80
|
+
emptyOutDir: true,
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
},
|
|
84
|
+
configResolved(config) {
|
|
85
|
+
// El root REAL queda disponible para los demás sub-plugins (el
|
|
86
|
+
// manifestTransform de la PWA lo necesita y corre fuera de un hook
|
|
87
|
+
// con config a la mano).
|
|
88
|
+
options._root = config.root;
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function hotFilePlugin(options) {
|
|
94
|
+
let hotPath = options.hotFile;
|
|
95
|
+
return {
|
|
96
|
+
name: 'milpa:hot-file',
|
|
97
|
+
configResolved(config) {
|
|
98
|
+
hotPath = resolve(config.root, options.hotFile);
|
|
99
|
+
},
|
|
100
|
+
configureServer(server) {
|
|
101
|
+
server.httpServer?.once('listening', () => {
|
|
102
|
+
const address = server.httpServer.address();
|
|
103
|
+
const host = typeof address === 'object' && address.address !== '::' && address.address !== '0.0.0.0'
|
|
104
|
+
? address.address
|
|
105
|
+
: 'localhost';
|
|
106
|
+
writeFileSync(hotPath, `http://${host}:${address.port}`);
|
|
107
|
+
});
|
|
108
|
+
const clean = () => {
|
|
109
|
+
rmSync(hotPath, {force: true});
|
|
110
|
+
process.exit();
|
|
111
|
+
};
|
|
112
|
+
process.once('SIGINT', clean);
|
|
113
|
+
process.once('SIGTERM', clean);
|
|
114
|
+
process.once('exit', () => rmSync(hotPath, {force: true}));
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function pwaPlugins(options) {
|
|
120
|
+
const pwa = options.pwa === true ? {} : options.pwa;
|
|
121
|
+
const raw = serwist({
|
|
122
|
+
swSrc: pwa.swSrc ?? 'src/sw.js',
|
|
123
|
+
swDest: pwa.swDest ?? 'sw.js',
|
|
124
|
+
// Mismo destino que el build (public/<app>); se calcula desde el cwd del
|
|
125
|
+
// frontend (el gestor corre los scripts con cwd = carpeta de la app).
|
|
126
|
+
globDirectory: pwa.globDirectory ?? resolveOutDir(options, '.'),
|
|
127
|
+
injectionPoint: 'self.__SW_MANIFEST',
|
|
128
|
+
rollupFormat: 'iife',
|
|
129
|
+
// El shell HTML lo sirve el backend (no está en dist/): precachearlo da
|
|
130
|
+
// cold-start offline y el fallback de documentos del sw.js.
|
|
131
|
+
...(pwa.shell ? {additionalPrecacheEntries: [{url: pwa.shell, revision: `${Date.now()}`}]} : {}),
|
|
132
|
+
// GOTCHA (docs/prerelease/26): los entries salen RELATIVOS y resuelven
|
|
133
|
+
// contra la URL del SW → 404 bajo subpath. Se prefijan con la base real.
|
|
134
|
+
manifestTransforms: [
|
|
135
|
+
(entries) => ({
|
|
136
|
+
manifest: entries.map((entry) => (
|
|
137
|
+
entry.url.startsWith('/') ? entry : {...entry, url: `${resolveBase(options, options._root)}${entry.url}`}
|
|
138
|
+
)),
|
|
139
|
+
}),
|
|
140
|
+
],
|
|
141
|
+
});
|
|
142
|
+
// El SW solo se genera en BUILD: en dev serviría assets stale y pelearía
|
|
143
|
+
// con HMR. `apply: 'build'` es el switch idiomático de Vite para esto.
|
|
144
|
+
return (Array.isArray(raw) ? raw : [raw]).map((plugin) => ({apply: 'build', ...plugin}));
|
|
145
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "vite-plugin-milpa",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "El toolkit frontend del framework milpa (FastAPI + Jinja), estilo laravel-vite-plugin: hot-file para HMR, manifest para el helper vite() de Jinja, multi-app (surcos/), PWA opcional con Serwist, ASSET_URL para deploy bajo sub-ruta/CDN — y file-based routing para react-router 7 vía `vite-plugin-milpa/router`.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "index.js",
|
|
7
|
+
"types": "./index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./index.d.ts",
|
|
11
|
+
"default": "./index.js"
|
|
12
|
+
},
|
|
13
|
+
"./router": {
|
|
14
|
+
"types": "./router.d.ts",
|
|
15
|
+
"default": "./router.js"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"index.js",
|
|
20
|
+
"index.d.ts",
|
|
21
|
+
"router.js",
|
|
22
|
+
"router.d.ts"
|
|
23
|
+
],
|
|
24
|
+
"keywords": [
|
|
25
|
+
"vite-plugin",
|
|
26
|
+
"vite",
|
|
27
|
+
"milpa",
|
|
28
|
+
"fastapi",
|
|
29
|
+
"jinja",
|
|
30
|
+
"laravel-vite",
|
|
31
|
+
"backend-integration",
|
|
32
|
+
"pwa",
|
|
33
|
+
"serwist",
|
|
34
|
+
"microfrontends",
|
|
35
|
+
"file-based-routing",
|
|
36
|
+
"react-router"
|
|
37
|
+
],
|
|
38
|
+
"author": "Calcifux (Carlos Guillermo Reyes Ramiro)",
|
|
39
|
+
"license": "MIT",
|
|
40
|
+
"repository": {
|
|
41
|
+
"type": "git",
|
|
42
|
+
"url": "git+https://github.com/calcifux/vite-plugin-milpa.git"
|
|
43
|
+
},
|
|
44
|
+
"homepage": "https://github.com/calcifux/vite-plugin-milpa#readme",
|
|
45
|
+
"bugs": {
|
|
46
|
+
"url": "https://github.com/calcifux/vite-plugin-milpa/issues"
|
|
47
|
+
},
|
|
48
|
+
"engines": {
|
|
49
|
+
"node": ">=18"
|
|
50
|
+
},
|
|
51
|
+
"volta": {
|
|
52
|
+
"node": "22.22.2"
|
|
53
|
+
},
|
|
54
|
+
"peerDependencies": {
|
|
55
|
+
"react": ">=18",
|
|
56
|
+
"react-router": ">=7",
|
|
57
|
+
"vite": ">=5"
|
|
58
|
+
},
|
|
59
|
+
"peerDependenciesMeta": {
|
|
60
|
+
"react": {
|
|
61
|
+
"optional": true
|
|
62
|
+
},
|
|
63
|
+
"react-router": {
|
|
64
|
+
"optional": true
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
"dependencies": {
|
|
68
|
+
"@serwist/vite": "^9.5.11"
|
|
69
|
+
}
|
|
70
|
+
}
|
package/router.d.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// Tipos de vite-plugin-milpa/router (el subpath RUNTIME: file-based routing
|
|
2
|
+
// para react-router 7 en modo library). El fuente es JS plano auditable; los
|
|
3
|
+
// globs los llama el CONSUMIDOR (import.meta.glob es compile-time y resuelve
|
|
4
|
+
// relativo al archivo que lo contiene — no puede vivir en node_modules).
|
|
5
|
+
import type {RouteObject} from 'react-router';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* El mapa que devuelve import.meta.glob SIN eager: archivo → loader perezoso.
|
|
9
|
+
* Cada loader se vuelve un chunk propio (code-splitting por ruta).
|
|
10
|
+
*/
|
|
11
|
+
export type GlobMap = Record<string, () => Promise<unknown>>;
|
|
12
|
+
|
|
13
|
+
export interface BuildRoutesGlobs {
|
|
14
|
+
/**
|
|
15
|
+
* Glob de las páginas "core" del shell (carpeta pages/, recursivo).
|
|
16
|
+
* Convención: index → raíz de su carpeta, [id] → :id, prefijo _ → no-ruta.
|
|
17
|
+
*/
|
|
18
|
+
pages?: GlobMap;
|
|
19
|
+
/**
|
|
20
|
+
* Glob de las páginas por módulo (modules/<m>/pages/, recursivo): cada
|
|
21
|
+
* módulo se monta bajo /<m>; su _layout (opcional) envuelve sus rutas.
|
|
22
|
+
* Espejo de los Modules/<X>/Http del backend milpa.
|
|
23
|
+
*/
|
|
24
|
+
modules?: GlobMap;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Construye el árbol RouteObject[] de react-router desde tus globs. Llamar sin
|
|
29
|
+
* ninguno truena con instrucción (nunca falla en silencio).
|
|
30
|
+
*/
|
|
31
|
+
export declare function buildRoutes(globs?: BuildRoutesGlobs): RouteObject[];
|
package/router.js
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// vite-plugin-milpa/router — file-based routing para react-router 7 en modo
|
|
2
|
+
// LIBRARY (createBrowserRouter), pensado para SPAs servidas por un backend
|
|
3
|
+
// (milpa, Laravel, Django). Es el subpath RUNTIME del paquete: el plugin
|
|
4
|
+
// (index.js) corre en Node al buildear; esto se bundlea DENTRO de tu app
|
|
5
|
+
// (mismo patrón que @serwist/vite + @serwist/vite/worker).
|
|
6
|
+
//
|
|
7
|
+
// Convención (ESPEJO del backend milpa, que auto-monta Modules/<X>/Http):
|
|
8
|
+
//
|
|
9
|
+
// pages/**/* → rutas "core" del shell /acerca
|
|
10
|
+
// modules/<m>/pages/**/* → rutas del módulo <m> /<m>/...
|
|
11
|
+
//
|
|
12
|
+
// index.* → la raíz de su carpeta ('' como segmento)
|
|
13
|
+
// [id].* → segmento dinámico :id (useParams() en la página)
|
|
14
|
+
// _layout.* → layout del módulo (envuelve sus rutas; opcional)
|
|
15
|
+
// _otro.* → ignorado (prefijo '_' = no-ruta, como un parcial)
|
|
16
|
+
//
|
|
17
|
+
// 10 devs = 10 carpetas en modules/: cada quien dropea páginas en SU módulo y
|
|
18
|
+
// la ruta existe — nadie toca un router central ni pisa al vecino.
|
|
19
|
+
//
|
|
20
|
+
// POR QUÉ recibe los globs en vez de llamarlos (la decisión clave del paquete):
|
|
21
|
+
// import.meta.glob es compile-time de Vite y resuelve RELATIVO al archivo que lo
|
|
22
|
+
// contiene — si viviera aquí (node_modules) globearía este paquete, no tus pages
|
|
23
|
+
// (y Vite ni transforma globs cuyo importer está en node_modules; issue #2390).
|
|
24
|
+
// El glob va en TU código; aquí solo se interpreta el mapa {archivo: loader}:
|
|
25
|
+
//
|
|
26
|
+
// import {buildRoutes} from 'vite-plugin-milpa/router';
|
|
27
|
+
// const routes = buildRoutes({
|
|
28
|
+
// pages: import.meta.glob('./pages/**/*.jsx'),
|
|
29
|
+
// modules: import.meta.glob('./modules/*/pages/**/*.jsx'),
|
|
30
|
+
// });
|
|
31
|
+
//
|
|
32
|
+
// Globs SIN eager devuelven () => import(...): cada página es un chunk propio
|
|
33
|
+
// (code-splitting por ruta GRATIS, como el App Router de Next). Bonus: como el
|
|
34
|
+
// glob es tuyo, este módulo NO depende de Vite — sirve cualquier bundler que
|
|
35
|
+
// produzca el mismo mapa. JS plano sin JSX a propósito: createElement es el
|
|
36
|
+
// equivalente compilado de <X/> y los bundlers NO parsean JSX en .js de
|
|
37
|
+
// node_modules (esbuild: JSX solo en .jsx/.tsx; vite issue #8954).
|
|
38
|
+
|
|
39
|
+
import {createElement, lazy} from 'react';
|
|
40
|
+
|
|
41
|
+
// 'productos/[id].jsx' → 'productos/:id' · 'index.jsx' → '' · acepta .jsx/.tsx/.js/.ts
|
|
42
|
+
function toPath(relativeFile) {
|
|
43
|
+
return relativeFile
|
|
44
|
+
.replace(/\.(jsx|tsx|js|ts)$/, '')
|
|
45
|
+
.split('/')
|
|
46
|
+
.map((segment) => (segment === 'index' ? '' : segment.replace(/^\[(.+)\]$/, ':$1')))
|
|
47
|
+
.filter(Boolean)
|
|
48
|
+
.join('/');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function lazyElement(loader) {
|
|
52
|
+
return createElement(lazy(loader));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function toRoute(path, loader) {
|
|
56
|
+
return path === '' ? {index: true, element: lazyElement(loader)} : {path, element: lazyElement(loader)};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Basename con prefijo '_' = no-ruta (el _layout de módulo se trata aparte).
|
|
60
|
+
function isHidden(relativeFile) {
|
|
61
|
+
return relativeFile.split('/').pop().startsWith('_');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const PAGES_RE = /\/pages\/(.+)$/;
|
|
65
|
+
const MODULE_RE = /\/modules\/([^/]+)\/pages\/(.+)$/;
|
|
66
|
+
const LAYOUT_RE = /^_layout\.(jsx|tsx|js|ts)$/;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Construye el árbol de rutas (RouteObject[] de react-router) desde los mapas
|
|
70
|
+
* de import.meta.glob del CONSUMIDOR. Ambas claves son opcionales, pero llamar
|
|
71
|
+
* sin ninguna truena con instrucción (tenet milpa: nunca fallar en silencio).
|
|
72
|
+
*/
|
|
73
|
+
export function buildRoutes({pages, modules} = {}) {
|
|
74
|
+
if (pages === undefined && modules === undefined) {
|
|
75
|
+
throw new Error(
|
|
76
|
+
'buildRoutes necesita tus globs — en TU código (import.meta.glob resuelve relativo ' +
|
|
77
|
+
"al archivo que lo contiene): buildRoutes({pages: import.meta.glob('./pages/**/*.jsx'), " +
|
|
78
|
+
"modules: import.meta.glob('./modules/*/pages/**/*.jsx')}).",
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
const routes = [];
|
|
82
|
+
|
|
83
|
+
// Páginas core del shell (las del equipo "plataforma").
|
|
84
|
+
for (const [file, loader] of Object.entries(pages ?? {})) {
|
|
85
|
+
const match = file.match(PAGES_RE);
|
|
86
|
+
if (!match) {
|
|
87
|
+
throw new Error(`'${file}' no sigue la convención pages/ — ¿el glob de \`pages\` apunta a otra carpeta?`);
|
|
88
|
+
}
|
|
89
|
+
if (isHidden(match[1])) continue;
|
|
90
|
+
routes.push(toRoute(toPath(match[1]), loader));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Páginas por módulo, agrupadas y montadas bajo /<modulo>.
|
|
94
|
+
const byModule = new Map();
|
|
95
|
+
const layouts = new Map();
|
|
96
|
+
for (const [file, loader] of Object.entries(modules ?? {})) {
|
|
97
|
+
const match = file.match(MODULE_RE);
|
|
98
|
+
if (!match) {
|
|
99
|
+
throw new Error(
|
|
100
|
+
`'${file}' no sigue la convención modules/<m>/pages/ — ¿el glob de \`modules\` apunta a otra carpeta?`,
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
const [, moduleName, relativeFile] = match;
|
|
104
|
+
if (LAYOUT_RE.test(relativeFile)) {
|
|
105
|
+
layouts.set(moduleName, loader); // no es página: es el wrapper del módulo
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
if (isHidden(relativeFile)) continue;
|
|
109
|
+
if (!byModule.has(moduleName)) byModule.set(moduleName, []);
|
|
110
|
+
byModule.get(moduleName).push(toRoute(toPath(relativeFile), loader));
|
|
111
|
+
}
|
|
112
|
+
for (const [moduleName, children] of byModule) {
|
|
113
|
+
const layoutLoader = layouts.get(moduleName);
|
|
114
|
+
routes.push({
|
|
115
|
+
path: moduleName,
|
|
116
|
+
// Sin _layout, react-router renderiza los hijos directo (<Outlet/> implícito).
|
|
117
|
+
...(layoutLoader ? {element: lazyElement(layoutLoader)} : {}),
|
|
118
|
+
children,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return routes;
|
|
123
|
+
}
|