vanilla-jet 1.3.2 → 1.4.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/.github/workflows/deploy.yml +18 -11
- package/.scripts/run_vite.js +36 -0
- package/CHANGELOG.md +95 -0
- package/README.md +143 -5
- package/ROADMAP_INTEGRAL.md +248 -0
- package/bin.js +16 -0
- package/docs/deployment/Dockerfile.app.example +19 -0
- package/docs/deployment/docker-compose.nginx-node.example.yml +23 -0
- package/docs/deployment/nginx.default.conf.example +36 -0
- package/framework/dipper.js +5 -5
- package/framework/response.js +92 -15
- package/framework/router.js +198 -16
- package/framework/server.js +2 -1
- package/package.json +15 -12
- package/vite.config.js +184 -0
|
@@ -5,38 +5,45 @@ on:
|
|
|
5
5
|
branches:
|
|
6
6
|
- main
|
|
7
7
|
|
|
8
|
+
permissions:
|
|
9
|
+
id-token: write
|
|
10
|
+
contents: write
|
|
11
|
+
|
|
8
12
|
jobs:
|
|
9
13
|
release:
|
|
10
14
|
name: Publish
|
|
11
15
|
runs-on: ubuntu-latest
|
|
12
|
-
environment: rep
|
|
13
16
|
|
|
14
17
|
steps:
|
|
15
18
|
- name: Check out the repository
|
|
16
|
-
uses: actions/checkout@
|
|
19
|
+
uses: actions/checkout@v4
|
|
17
20
|
|
|
18
21
|
- name: Setup Node.js
|
|
19
|
-
uses: actions/setup-node@
|
|
22
|
+
uses: actions/setup-node@v4
|
|
20
23
|
with:
|
|
21
|
-
node-version: '
|
|
22
|
-
registry-url: 'https://registry.npmjs.org
|
|
24
|
+
node-version: '24'
|
|
25
|
+
registry-url: 'https://registry.npmjs.org'
|
|
23
26
|
|
|
24
27
|
- name: Install dependencies
|
|
25
|
-
run: npm
|
|
28
|
+
run: npm ci
|
|
29
|
+
|
|
30
|
+
- name: Build
|
|
31
|
+
run: npm run build --if-present
|
|
32
|
+
|
|
33
|
+
- name: Test
|
|
34
|
+
run: npm test
|
|
26
35
|
|
|
27
36
|
- name: Publish to npm
|
|
28
|
-
env:
|
|
29
|
-
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
30
37
|
run: npm publish
|
|
31
38
|
|
|
32
39
|
- name: Configure git identity
|
|
33
40
|
run: |
|
|
34
|
-
git config
|
|
35
|
-
git config
|
|
41
|
+
git config user.email "actions@github.com"
|
|
42
|
+
git config user.name "GitHub Actions"
|
|
36
43
|
|
|
37
44
|
- name: Get version from package.json
|
|
38
45
|
id: get_version
|
|
39
|
-
run: echo "
|
|
46
|
+
run: echo "version=$(node -p 'require(\"./package.json\").version')" >> "$GITHUB_OUTPUT"
|
|
40
47
|
|
|
41
48
|
- name: Check if tag exists
|
|
42
49
|
run: |
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const { spawnSync } = require('child_process');
|
|
3
|
+
|
|
4
|
+
function resolveConsumerRoot() {
|
|
5
|
+
return process.cwd()
|
|
6
|
+
.replace('/node_modules/vanilla-jet', '')
|
|
7
|
+
.replace('/.scripts', '')
|
|
8
|
+
.replace('/.grunt', '');
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function main() {
|
|
12
|
+
const action = process.argv[2] || 'dev';
|
|
13
|
+
const viteCommand = action === 'build' ? 'build' : 'serve';
|
|
14
|
+
const packageRoot = path.resolve(__dirname, '..');
|
|
15
|
+
const consumerRoot = resolveConsumerRoot();
|
|
16
|
+
const vitePackageJson = require.resolve('vite/package.json', { paths: [packageRoot] });
|
|
17
|
+
const viteBin = path.join(path.dirname(vitePackageJson), 'bin/vite.js');
|
|
18
|
+
const configFile = path.join(packageRoot, 'vite.config.js');
|
|
19
|
+
|
|
20
|
+
const args = [viteBin, viteCommand, '--config', configFile];
|
|
21
|
+
if (viteCommand === 'serve') {
|
|
22
|
+
args.push('--host');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const result = spawnSync(process.execPath, args, {
|
|
26
|
+
stdio: 'inherit',
|
|
27
|
+
cwd: consumerRoot,
|
|
28
|
+
env: Object.assign({}, process.env, {
|
|
29
|
+
VANILLAJET_PACKAGE_ROOT: packageRoot
|
|
30
|
+
})
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
process.exit(typeof result.status === 'number' ? result.status : 1);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
main();
|
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable project changes are documented in this file.
|
|
4
|
+
|
|
5
|
+
The format follows a structure inspired by Keep a Changelog and semantic versioning.
|
|
6
|
+
|
|
7
|
+
## [1.4.0] - 2026-02-19
|
|
8
|
+
|
|
9
|
+
### Highlights
|
|
10
|
+
|
|
11
|
+
- Added `dev:vite` and `build:vite` scripts to introduce a Vite-first JS/LESS pipeline without removing legacy Gulp commands.
|
|
12
|
+
- Added `vite.config.js` and `.scripts/run_vite.js` to support consumer-root execution, compatibility output in `public/`, and a dev helper route (`/__vanillajet__/`).
|
|
13
|
+
- Added CLI support in `bin.js` for `npx vanilla-jet dev:vite` and `npx vanilla-jet build:vite`.
|
|
14
|
+
- Updated `README.md` to document legacy vs modern workflows for HU 2.1.
|
|
15
|
+
|
|
16
|
+
## [1.3.6] - 2026-02-19
|
|
17
|
+
|
|
18
|
+
### Highlights (v1.3.6)
|
|
19
|
+
|
|
20
|
+
- Fixed protected directory typo in `framework/router.js`: `node_mudules` -> `node_modules`.
|
|
21
|
+
- Fixed stability issues in `framework/dipper.js`:
|
|
22
|
+
- `includeAnimations()` now calls the existing `includeAnimation()`.
|
|
23
|
+
- `dequeueStyle()` and `dequeueScript()` now read `item.requires` correctly.
|
|
24
|
+
- Dependency dequeue now runs only when explicitly requested (`dependencies === true`).
|
|
25
|
+
- Fixed `package.json` recursive `test` script to avoid infinite loop and keep `npm test` stable.
|
|
26
|
+
- Completed HU 1.4 hardening milestone and updated roadmap tracking.
|
|
27
|
+
|
|
28
|
+
### Compatibility notes (v1.3.6)
|
|
29
|
+
|
|
30
|
+
- No public API changes.
|
|
31
|
+
- No route contract changes.
|
|
32
|
+
- This patch only hardens runtime behavior and developer workflow reliability.
|
|
33
|
+
|
|
34
|
+
## [1.3.5] - 2026-02-19
|
|
35
|
+
|
|
36
|
+
### Highlights (v1.3.5)
|
|
37
|
+
|
|
38
|
+
- Added safe precompressed fallback in `response.render()` for HTML templates: `.br` (when enabled and accepted) -> `.gz` (when accepted) -> original HTML.
|
|
39
|
+
- Wired `Response` to server profile options, so HTML negotiation follows `settings.profile.enable_precompressed_negotiation`.
|
|
40
|
+
- Added `Vary: Accept-Encoding` for negotiated HTML responses and support for `Accept-Encoding` quality params.
|
|
41
|
+
- Updated documentation and roadmap status for HU 1.3.
|
|
42
|
+
|
|
43
|
+
## [1.3.4] - 2026-02-19
|
|
44
|
+
|
|
45
|
+
### Highlights (v1.3.4)
|
|
46
|
+
|
|
47
|
+
- Added optional static precompressed negotiation flag: `settings.profile.enable_precompressed_negotiation`.
|
|
48
|
+
- Static files now resolve safely with fallback chain: `.br` (when enabled and accepted) -> `.gz` (when accepted) -> original file.
|
|
49
|
+
- Static compression negotiation now handles `Accept-Encoding` values with quality params (for example `gzip;q=1.0`).
|
|
50
|
+
- `Vary: Accept-Encoding` is set for negotiated static responses.
|
|
51
|
+
|
|
52
|
+
## [1.3.3] - 2026-02-18
|
|
53
|
+
|
|
54
|
+
### Added
|
|
55
|
+
|
|
56
|
+
- Static metadata in-memory cache in `framework/router.js` (`size`, `lastModified`, `etag`) to reduce repeated filesystem work.
|
|
57
|
+
- Conditional request handling for static files using `If-None-Match` and `If-Modified-Since`.
|
|
58
|
+
- Static response headers: `ETag`, `Last-Modified`, and `Cache-Control: no-cache, must-revalidate`.
|
|
59
|
+
|
|
60
|
+
### Changed
|
|
61
|
+
|
|
62
|
+
- Static files can now return `304 Not Modified` when validators match, reducing transfer for repeated requests.
|
|
63
|
+
- Conditional requests force metadata refresh before deciding `304`, so clients can still see new content without hard reload.
|
|
64
|
+
|
|
65
|
+
### Compatibility notes
|
|
66
|
+
|
|
67
|
+
- No route or filename contract changes.
|
|
68
|
+
- No impact on dynamic endpoints behavior.
|
|
69
|
+
|
|
70
|
+
## [1.3.2] - 2026-02-18
|
|
71
|
+
|
|
72
|
+
### Current documented state
|
|
73
|
+
|
|
74
|
+
- Released version `1.3.2` of `vanilla-jet`.
|
|
75
|
+
- The framework exports `Server` from `index.js` for simple integration in Node.js projects.
|
|
76
|
+
- Includes a Gulp-based build pipeline for:
|
|
77
|
+
- JavaScript minification and concatenation.
|
|
78
|
+
- LESS/CSS compilation and minification.
|
|
79
|
+
- HTML template compilation with Nunjucks.
|
|
80
|
+
- Generation of compressed `.gz` artifacts.
|
|
81
|
+
- Includes HTTP server and optional HTTPS server (self-managed certificates) in `framework/server.js`.
|
|
82
|
+
- Includes internal router with Backbone-style route support (`:param`, `*splat`, optional segments).
|
|
83
|
+
- Includes resource utilities (scripts, styles, meta tags, sentry, environment) in `framework/dipper.js`.
|
|
84
|
+
|
|
85
|
+
### Compatibility note
|
|
86
|
+
|
|
87
|
+
- This version keeps the historical behavior expected by existing projects.
|
|
88
|
+
- Future improvements are planned with backward compatibility in mind. See `ROADMAP_INTEGRAL.md`.
|
|
89
|
+
|
|
90
|
+
[1.3.3]: https://github.com/nalancer08/VanillaJet/releases/tag/v1.3.3
|
|
91
|
+
[1.3.2]: https://github.com/nalancer08/VanillaJet/releases/tag/v1.3.2
|
|
92
|
+
[1.3.4]: https://github.com/nalancer08/VanillaJet/releases/tag/v1.3.4
|
|
93
|
+
[1.3.5]: https://github.com/nalancer08/VanillaJet/releases/tag/v1.3.5
|
|
94
|
+
[1.3.6]: https://github.com/nalancer08/VanillaJet/releases/tag/v1.3.6
|
|
95
|
+
[1.4.0]: https://github.com/nalancer08/VanillaJet/releases/tag/v1.4.0
|
package/README.md
CHANGED
|
@@ -1,6 +1,144 @@
|
|
|
1
|
-
<p align="center">
|
|
2
|
-
<img src="https://github.com/nalancer08/App-Builders/blob/master/Logos/logo_monocromatico_horizontal_.png">
|
|
3
|
-
</p>
|
|
4
|
-
|
|
5
1
|
# VanillaJet
|
|
6
|
-
|
|
2
|
+
|
|
3
|
+
Node.js framework for building SPA applications with a JS/CSS/HTML build pipeline, HTTP/HTTPS server, internal router, and template rendering utilities.
|
|
4
|
+
|
|
5
|
+

|
|
6
|
+
|
|
7
|
+
## Current version
|
|
8
|
+
|
|
9
|
+
- Version: `1.4.0`
|
|
10
|
+
- Changelog: see [`CHANGELOG.md`](./CHANGELOG.md)
|
|
11
|
+
- Improvement plan (performance and backward compatibility): see `ROADMAP_INTEGRAL.md`
|
|
12
|
+
|
|
13
|
+
## Requirements
|
|
14
|
+
|
|
15
|
+
- Node.js `>=16` recommended
|
|
16
|
+
- npm `>=8`
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm install vanilla-jet
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
If you are working in this local repository:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npm install
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Quick start
|
|
31
|
+
|
|
32
|
+
### 1) Export the server from your project
|
|
33
|
+
|
|
34
|
+
```js
|
|
35
|
+
const { Server } = require('vanilla-jet');
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### 2) Define endpoints (classes)
|
|
39
|
+
|
|
40
|
+
Each endpoint should expose a `name` and register routes with the router in the constructor.
|
|
41
|
+
|
|
42
|
+
```js
|
|
43
|
+
class AppEndpoint {
|
|
44
|
+
constructor(router) {
|
|
45
|
+
this.name = 'AppEndpoint';
|
|
46
|
+
router.addRoute('get', '/', 'AppEndpoint.index');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
index(request, response) {
|
|
50
|
+
response.setBody('Hello VanillaJet');
|
|
51
|
+
response.respond();
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### 3) Start the server
|
|
57
|
+
|
|
58
|
+
```js
|
|
59
|
+
const { Server } = require('vanilla-jet');
|
|
60
|
+
const Config = require('./config');
|
|
61
|
+
|
|
62
|
+
new Server(Config, [AppEndpoint]).start();
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Available commands
|
|
66
|
+
|
|
67
|
+
From this repository:
|
|
68
|
+
|
|
69
|
+
- `npm run setup`: generates a base `vanillaJet.package.json` if it does not exist.
|
|
70
|
+
- `npm run dev`: legacy Gulp build + watcher for development.
|
|
71
|
+
- `npm run dev:vite`: modern Vite dev server for JS/LESS DX (keeps Nunjucks flow unchanged).
|
|
72
|
+
- `npm run build:vite`: modern Vite build for JS/LESS output in `public/`.
|
|
73
|
+
- `npm run build:qa`: build for QA.
|
|
74
|
+
- `npm run build:staging`: build for staging.
|
|
75
|
+
- `npm run build:prod`: build for production.
|
|
76
|
+
|
|
77
|
+
As CLI (`bin.js`):
|
|
78
|
+
|
|
79
|
+
- `npx vanilla-jet setup`
|
|
80
|
+
- `npx vanilla-jet dev`
|
|
81
|
+
- `npx vanilla-jet build`
|
|
82
|
+
- `npx vanilla-jet dev:vite`
|
|
83
|
+
- `npx vanilla-jet build:vite`
|
|
84
|
+
|
|
85
|
+
## Legacy vs Vite mode
|
|
86
|
+
|
|
87
|
+
- Legacy (`npm run dev` / `npx vanilla-jet dev`):
|
|
88
|
+
- Keeps full historical Gulp pipeline (JS minify+concat, LESS, templates, gzip artifacts).
|
|
89
|
+
- Recommended when you need 100% historical behavior.
|
|
90
|
+
- Vite (`npm run dev:vite` / `npx vanilla-jet dev:vite`):
|
|
91
|
+
- Focuses on JS/LESS developer experience with faster feedback.
|
|
92
|
+
- Does not replace Nunjucks compilation in this stage (HU 2.1), so template flow remains legacy.
|
|
93
|
+
- Dev helper page: open `http://localhost:5173/__vanillajet__/` to load the Vite entry.
|
|
94
|
+
- Vite build (`npm run build:vite` / `npx vanilla-jet build:vite`):
|
|
95
|
+
- Generates `public/scripts/vanilla.min.js` and `public/styles/app.min.css`.
|
|
96
|
+
- Keeps existing legacy build commands available and unchanged.
|
|
97
|
+
|
|
98
|
+
## Expected consumer project structure
|
|
99
|
+
|
|
100
|
+
VanillaJet expects a structure similar to:
|
|
101
|
+
|
|
102
|
+
- `assets/pages/home.html`
|
|
103
|
+
- `assets/templates/**/*.html`
|
|
104
|
+
- `assets/scripts/**/*.js`
|
|
105
|
+
- `assets/styles/less/admin.less`
|
|
106
|
+
- `public/` (compiled output)
|
|
107
|
+
- `config.js`
|
|
108
|
+
- `vanillaJet.package.json`
|
|
109
|
+
|
|
110
|
+
## Build pipeline (summary)
|
|
111
|
+
|
|
112
|
+
- Minifies JS and concatenates into `public/scripts/vanilla.min.js`
|
|
113
|
+
- Compiles LESS and generates `public/styles/app.min.css`
|
|
114
|
+
- Compiles templates and generates `public/pages/home.html`
|
|
115
|
+
- Generates `.gz` versions of JS/CSS/HTML for compressed delivery
|
|
116
|
+
|
|
117
|
+
## Compression negotiation (optional)
|
|
118
|
+
|
|
119
|
+
You can enable precompressed static negotiation from `settings.profile`:
|
|
120
|
+
|
|
121
|
+
```js
|
|
122
|
+
module.exports = {
|
|
123
|
+
settings: {
|
|
124
|
+
profile: {
|
|
125
|
+
// Enables priority: .br -> .gz -> original file
|
|
126
|
+
enable_precompressed_negotiation: true
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Behavior details:
|
|
133
|
+
|
|
134
|
+
- Default (`false`): keeps existing gzip behavior for supported static assets.
|
|
135
|
+
- Enabled (`true`): if client accepts Brotli, server tries `.br` first.
|
|
136
|
+
- Safe fallback: if `.br` or `.gz` does not exist, server serves the original file.
|
|
137
|
+
- HTML rendering (`response.render`) also uses safe runtime fallback for precompressed templates (`.br`/`.gz`/original).
|
|
138
|
+
|
|
139
|
+
## Additional documentation
|
|
140
|
+
|
|
141
|
+
- Router: `docs/router.md`
|
|
142
|
+
- Version history: [`CHANGELOG.md`](./CHANGELOG.md)
|
|
143
|
+
- Roadmap and improvements: `ROADMAP_INTEGRAL.md`
|
|
144
|
+
- Deployment templates (nginx + docker): `docs/deployment/`
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
# ROADMAP INTEGRAL - VanillaJet
|
|
2
|
+
|
|
3
|
+
Documento canonico de planeacion por epicas e historias.
|
|
4
|
+
Cada historia incluye su ciclo completo: fases, tareas, entregables, metricas, criterios y documentacion.
|
|
5
|
+
|
|
6
|
+
## Objetivo
|
|
7
|
+
|
|
8
|
+
- Modernizar el framework con `Vite` como base de DX.
|
|
9
|
+
- Reducir dependencia de Node para servir frontend.
|
|
10
|
+
- Adoptar `nginx` y Docker al final, con evidencia y sin romper legacy.
|
|
11
|
+
|
|
12
|
+
## Reglas de ejecucion
|
|
13
|
+
|
|
14
|
+
- Retrocompatibilidad primero.
|
|
15
|
+
- Cambios sensibles con flags.
|
|
16
|
+
- Todo cambio con medicion antes/despues.
|
|
17
|
+
- Entregas pequenas y reversibles.
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## EPIC 1 - Estabilidad base
|
|
22
|
+
|
|
23
|
+
### HU 1.1 - Cache metadata + 304 (completada `v1.3.3`)
|
|
24
|
+
|
|
25
|
+
#### Fases
|
|
26
|
+
- F1: cache metadata.
|
|
27
|
+
- F2: validacion condicional.
|
|
28
|
+
- F3: no-regresion.
|
|
29
|
+
|
|
30
|
+
#### Tareas
|
|
31
|
+
- Cache `size/mtime/etag`.
|
|
32
|
+
- Soporte `If-None-Match` y `If-Modified-Since`.
|
|
33
|
+
|
|
34
|
+
#### Entregables
|
|
35
|
+
- `framework/router.js` actualizado.
|
|
36
|
+
|
|
37
|
+
#### Metricas
|
|
38
|
+
- Menor latencia y menor I/O en estaticos repetidos.
|
|
39
|
+
|
|
40
|
+
#### Criterios
|
|
41
|
+
- `304` correcto.
|
|
42
|
+
- Sin impacto en dinamico.
|
|
43
|
+
|
|
44
|
+
#### Documentacion
|
|
45
|
+
- `CHANGELOG.md`.
|
|
46
|
+
|
|
47
|
+
### HU 1.2 - Negociacion `br/gz` estaticos (completada `v1.3.4`)
|
|
48
|
+
|
|
49
|
+
#### Fases
|
|
50
|
+
- F1: flag opt-in.
|
|
51
|
+
- F2: fallback seguro.
|
|
52
|
+
- F3: validacion.
|
|
53
|
+
|
|
54
|
+
#### Tareas
|
|
55
|
+
- `.br -> .gz -> original`.
|
|
56
|
+
- `Vary: Accept-Encoding`.
|
|
57
|
+
|
|
58
|
+
#### Entregables
|
|
59
|
+
- `framework/router.js`.
|
|
60
|
+
|
|
61
|
+
#### Metricas
|
|
62
|
+
- Menor transferencia en cliente.
|
|
63
|
+
|
|
64
|
+
#### Criterios
|
|
65
|
+
- Fallback sin 404.
|
|
66
|
+
|
|
67
|
+
#### Documentacion
|
|
68
|
+
- `README.md`, `CHANGELOG.md`.
|
|
69
|
+
|
|
70
|
+
### HU 1.3 - Fallback precompressed HTML (completada `v1.3.5`)
|
|
71
|
+
|
|
72
|
+
#### Fases
|
|
73
|
+
- F1: fallback runtime HTML.
|
|
74
|
+
- F2: pruebas de ausencia de artefactos.
|
|
75
|
+
- F3: release.
|
|
76
|
+
|
|
77
|
+
#### Tareas
|
|
78
|
+
- Resolver `.br/.gz/original` en `render`.
|
|
79
|
+
|
|
80
|
+
#### Entregables
|
|
81
|
+
- `framework/response.js`.
|
|
82
|
+
|
|
83
|
+
#### Metricas
|
|
84
|
+
- Menor peso de HTML inicial.
|
|
85
|
+
|
|
86
|
+
#### Criterios
|
|
87
|
+
- Sin errores por artefactos faltantes.
|
|
88
|
+
|
|
89
|
+
#### Documentacion
|
|
90
|
+
- `README.md`, `CHANGELOG.md`.
|
|
91
|
+
|
|
92
|
+
### HU 1.4 - Hardening de bugs conocidos (completada `v1.3.6`)
|
|
93
|
+
|
|
94
|
+
#### Fases
|
|
95
|
+
- F1: correccion.
|
|
96
|
+
- F2: smoke tests.
|
|
97
|
+
- F3: patch release.
|
|
98
|
+
|
|
99
|
+
#### Tareas
|
|
100
|
+
- [x] `node_mudules -> node_modules`.
|
|
101
|
+
- [x] fixes en `dipper`.
|
|
102
|
+
- [x] fix script `test`.
|
|
103
|
+
|
|
104
|
+
#### Entregables
|
|
105
|
+
- Patch de estabilidad.
|
|
106
|
+
|
|
107
|
+
#### Metricas
|
|
108
|
+
- Menos errores silenciosos en runtime/build.
|
|
109
|
+
|
|
110
|
+
#### Criterios
|
|
111
|
+
- [x] `npm test` estable.
|
|
112
|
+
|
|
113
|
+
#### Documentacion
|
|
114
|
+
- `CHANGELOG.md` con nota de compatibilidad.
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## EPIC 2 - Vite first (foco actual)
|
|
119
|
+
|
|
120
|
+
### HU 2.1 - `dev:vite` y `build:vite` sin romper legacy (completada `v1.4.0`)
|
|
121
|
+
|
|
122
|
+
#### Fases
|
|
123
|
+
- F1: baseline.
|
|
124
|
+
- F2: integrar scripts Vite.
|
|
125
|
+
- F3: coexistencia con legacy.
|
|
126
|
+
- F4: validacion en consumidor real.
|
|
127
|
+
|
|
128
|
+
#### Tareas
|
|
129
|
+
- [x] Config Vite (JS/LESS).
|
|
130
|
+
- [x] Mantener Nunjucks en esta etapa.
|
|
131
|
+
- [x] Documentar `dev` legacy vs `dev:vite`.
|
|
132
|
+
|
|
133
|
+
#### Entregables
|
|
134
|
+
- Config y scripts de Vite.
|
|
135
|
+
|
|
136
|
+
#### Metricas
|
|
137
|
+
- Arranque dev >= 40% mas rapido.
|
|
138
|
+
- Rebuild incremental >= 50% mas rapido.
|
|
139
|
+
|
|
140
|
+
#### Criterios
|
|
141
|
+
- HMR estable.
|
|
142
|
+
- Legacy intacto.
|
|
143
|
+
|
|
144
|
+
#### Documentacion
|
|
145
|
+
- `README.md` + `CHANGELOG.md`.
|
|
146
|
+
|
|
147
|
+
### HU 2.2 - Node deja de servir frontend en modo moderno (pendiente)
|
|
148
|
+
|
|
149
|
+
#### Fases
|
|
150
|
+
- F1: contrato `legacy` vs `modern`.
|
|
151
|
+
- F2: flag de transicion.
|
|
152
|
+
- F3: validacion de compatibilidad.
|
|
153
|
+
|
|
154
|
+
#### Tareas
|
|
155
|
+
- Node solo API/dinamico en modo moderno.
|
|
156
|
+
- Frontend servido por Vite (dev) y luego Nginx (prod).
|
|
157
|
+
|
|
158
|
+
#### Entregables
|
|
159
|
+
- Contrato por entorno.
|
|
160
|
+
|
|
161
|
+
#### Metricas
|
|
162
|
+
- Menos carga de static serving en Node.
|
|
163
|
+
|
|
164
|
+
#### Criterios
|
|
165
|
+
- Modo moderno sin dependencia de `response.render()`.
|
|
166
|
+
- Modo legacy intacto.
|
|
167
|
+
|
|
168
|
+
#### Documentacion
|
|
169
|
+
- Guia de migracion de modo.
|
|
170
|
+
|
|
171
|
+
---
|
|
172
|
+
|
|
173
|
+
## EPIC 3 - Benchmark y decision gate
|
|
174
|
+
|
|
175
|
+
### HU 3.1 - Go/No-Go de Nginx basado en datos (pendiente)
|
|
176
|
+
|
|
177
|
+
#### Fases
|
|
178
|
+
- F1: dise;o benchmark A/B.
|
|
179
|
+
- F2: ejecucion.
|
|
180
|
+
- F3: analisis.
|
|
181
|
+
- F4: decision.
|
|
182
|
+
|
|
183
|
+
#### Tareas
|
|
184
|
+
- Medir p50/p95/p99, throughput, CPU, memoria.
|
|
185
|
+
- Definir umbrales minimos de aprobacion.
|
|
186
|
+
|
|
187
|
+
#### Entregables
|
|
188
|
+
- Reporte benchmark.
|
|
189
|
+
- Decision log Go/No-Go.
|
|
190
|
+
|
|
191
|
+
#### Criterios
|
|
192
|
+
- Decision trazable con evidencia.
|
|
193
|
+
|
|
194
|
+
#### Documentacion
|
|
195
|
+
- Documento benchmark + resumen tecnico.
|
|
196
|
+
|
|
197
|
+
---
|
|
198
|
+
|
|
199
|
+
## EPIC 4 - Adopcion final de infraestructura
|
|
200
|
+
|
|
201
|
+
### HU 4.1 - Nginx oficial para consumidores (pendiente)
|
|
202
|
+
|
|
203
|
+
#### Fases
|
|
204
|
+
- F1: template.
|
|
205
|
+
- F2: staging.
|
|
206
|
+
- F3: guia de operacion.
|
|
207
|
+
|
|
208
|
+
#### Tareas
|
|
209
|
+
- `try_files`, SPA fallback, proxy a Node, cache y precompressed.
|
|
210
|
+
|
|
211
|
+
#### Entregables
|
|
212
|
+
- Template y guia.
|
|
213
|
+
|
|
214
|
+
#### Documentacion
|
|
215
|
+
- `docs/deployment/` + `README.md`.
|
|
216
|
+
|
|
217
|
+
### HU 4.2 - Docker de referencia (pendiente)
|
|
218
|
+
|
|
219
|
+
#### Fases
|
|
220
|
+
- F1: Dockerfile.
|
|
221
|
+
- F2: docker-compose.
|
|
222
|
+
- F3: validacion server.
|
|
223
|
+
|
|
224
|
+
#### Tareas
|
|
225
|
+
- Definir imagenes, puertos y variables.
|
|
226
|
+
- Documentar rollback.
|
|
227
|
+
|
|
228
|
+
#### Entregables
|
|
229
|
+
- Dockerfile y compose de referencia.
|
|
230
|
+
|
|
231
|
+
#### Documentacion
|
|
232
|
+
- `docs/deployment/` + `README.md`.
|
|
233
|
+
|
|
234
|
+
---
|
|
235
|
+
|
|
236
|
+
## Secuencia oficial de ejecucion
|
|
237
|
+
|
|
238
|
+
1. HU 1.4
|
|
239
|
+
2. HU 2.1
|
|
240
|
+
3. HU 2.2
|
|
241
|
+
4. HU 3.1
|
|
242
|
+
5. HU 4.1
|
|
243
|
+
6. HU 4.2
|
|
244
|
+
|
|
245
|
+
## Estado global
|
|
246
|
+
|
|
247
|
+
- Completado: HU 1.1, HU 1.2, HU 1.3, HU 1.4, HU 2.1.
|
|
248
|
+
- Pendiente: HU 2.2, HU 3.1, HU 4.1, HU 4.2.
|
package/bin.js
CHANGED
|
@@ -28,4 +28,20 @@ switch (args[0]) {
|
|
|
28
28
|
console.error('Error executing gulp:', error.message);
|
|
29
29
|
}
|
|
30
30
|
break;
|
|
31
|
+
|
|
32
|
+
case 'dev:vite':
|
|
33
|
+
try {
|
|
34
|
+
execSync('node ./.scripts/run_vite.js dev', { stdio: 'inherit', cwd: __dirname });
|
|
35
|
+
} catch (error) {
|
|
36
|
+
console.error('Error executing Vite dev:', error.message);
|
|
37
|
+
}
|
|
38
|
+
break;
|
|
39
|
+
|
|
40
|
+
case 'build:vite':
|
|
41
|
+
try {
|
|
42
|
+
execSync('node ./.scripts/run_vite.js build', { stdio: 'inherit', cwd: __dirname });
|
|
43
|
+
} catch (error) {
|
|
44
|
+
console.error('Error executing Vite build:', error.message);
|
|
45
|
+
}
|
|
46
|
+
break;
|
|
31
47
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
FROM node:20-alpine
|
|
2
|
+
|
|
3
|
+
WORKDIR /app
|
|
4
|
+
|
|
5
|
+
# Install dependencies first for better layer cache
|
|
6
|
+
COPY package*.json ./
|
|
7
|
+
RUN npm ci
|
|
8
|
+
|
|
9
|
+
# Copy app source
|
|
10
|
+
COPY . .
|
|
11
|
+
|
|
12
|
+
# Build command should generate static output under /app/public
|
|
13
|
+
# Replace with your app build script (for example: npm run build:vite)
|
|
14
|
+
RUN npm run build:vite || npm run build:prod
|
|
15
|
+
|
|
16
|
+
EXPOSE 8080
|
|
17
|
+
|
|
18
|
+
# Replace with your actual backend start command
|
|
19
|
+
CMD ["node", "index.js"]
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
version: "3.9"
|
|
2
|
+
|
|
3
|
+
services:
|
|
4
|
+
app:
|
|
5
|
+
build:
|
|
6
|
+
context: .
|
|
7
|
+
dockerfile: docs/deployment/Dockerfile.app.example
|
|
8
|
+
container_name: vanillajet-app
|
|
9
|
+
environment:
|
|
10
|
+
- PORT=8080
|
|
11
|
+
expose:
|
|
12
|
+
- "8080"
|
|
13
|
+
|
|
14
|
+
nginx:
|
|
15
|
+
image: nginx:1.27-alpine
|
|
16
|
+
container_name: vanillajet-nginx
|
|
17
|
+
depends_on:
|
|
18
|
+
- app
|
|
19
|
+
ports:
|
|
20
|
+
- "80:80"
|
|
21
|
+
volumes:
|
|
22
|
+
- ./public:/app/public:ro
|
|
23
|
+
- ./docs/deployment/nginx.default.conf.example:/etc/nginx/conf.d/default.conf:ro
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
server {
|
|
2
|
+
listen 80;
|
|
3
|
+
server_name _;
|
|
4
|
+
|
|
5
|
+
# Build output served directly by nginx
|
|
6
|
+
root /app/public;
|
|
7
|
+
index pages/home.html;
|
|
8
|
+
|
|
9
|
+
# API and dynamic routes handled by VanillaJet backend
|
|
10
|
+
location /api/ {
|
|
11
|
+
proxy_pass http://app:8080;
|
|
12
|
+
proxy_http_version 1.1;
|
|
13
|
+
proxy_set_header Host $host;
|
|
14
|
+
proxy_set_header X-Real-IP $remote_addr;
|
|
15
|
+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
16
|
+
proxy_set_header X-Forwarded-Proto $scheme;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
# Static assets with long cache
|
|
20
|
+
location ~* \.(?:css|js|png|jpg|jpeg|gif|svg|webp|ico|ttf|otf|woff|woff2)$ {
|
|
21
|
+
try_files $uri =404;
|
|
22
|
+
expires 30d;
|
|
23
|
+
add_header Cache-Control "public, max-age=2592000, immutable";
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
# HTML should revalidate frequently
|
|
27
|
+
location ~* \.html$ {
|
|
28
|
+
try_files $uri =404;
|
|
29
|
+
add_header Cache-Control "no-cache, must-revalidate";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
# SPA fallback to main page
|
|
33
|
+
location / {
|
|
34
|
+
try_files $uri $uri/ /pages/home.html;
|
|
35
|
+
}
|
|
36
|
+
}
|
package/framework/dipper.js
CHANGED
|
@@ -214,8 +214,8 @@ Dipper.prototype.dequeueStyle = function(name, dependencies) {
|
|
|
214
214
|
if (obj.styles[name] != undefined) {
|
|
215
215
|
if (obj.enqueued_styles[name] != undefined) {
|
|
216
216
|
var item = obj.styles[name];
|
|
217
|
-
if (dependencies
|
|
218
|
-
_.each(item.
|
|
217
|
+
if (dependencies === true && Array.isArray(item.requires)) {
|
|
218
|
+
_.each(item.requires, function(dep) {
|
|
219
219
|
obj.dequeueStyle(dep);
|
|
220
220
|
});
|
|
221
221
|
}
|
|
@@ -233,8 +233,8 @@ Dipper.prototype.dequeueScript = function(name, dependencies) {
|
|
|
233
233
|
if (obj.scripts[name] != undefined) {
|
|
234
234
|
if (obj.enqueued_scripts[name] != undefined) {
|
|
235
235
|
var item = obj.scripts[name];
|
|
236
|
-
if (dependencies
|
|
237
|
-
_.each(item.
|
|
236
|
+
if (dependencies === true && Array.isArray(item.requires)) {
|
|
237
|
+
_.each(item.requires, function(dep) {
|
|
238
238
|
obj.dequeueScript(dep);
|
|
239
239
|
});
|
|
240
240
|
}
|
|
@@ -341,7 +341,7 @@ Dipper.prototype.includeAnimations = function() {
|
|
|
341
341
|
keys = Object.keys(obj.anims);
|
|
342
342
|
|
|
343
343
|
_.each(keys, function(anim) {
|
|
344
|
-
animsString += obj.
|
|
344
|
+
animsString += obj.includeAnimation(anim);
|
|
345
345
|
});
|
|
346
346
|
let baseAnimsString = `<script>'${animsString}'</script>`;
|
|
347
347
|
return baseAnimsString;
|
package/framework/response.js
CHANGED
|
@@ -3,18 +3,22 @@ let _ = require('underscore');
|
|
|
3
3
|
|
|
4
4
|
class Response {
|
|
5
5
|
|
|
6
|
-
constructor(res) {
|
|
6
|
+
constructor(res, options) {
|
|
7
7
|
|
|
8
8
|
this.res = null;
|
|
9
9
|
this.body = '';
|
|
10
10
|
this.status = 200;
|
|
11
11
|
this.headers = [];
|
|
12
12
|
this.autoRespond = true;
|
|
13
|
-
|
|
13
|
+
this.options = {};
|
|
14
|
+
this.init(res, options);
|
|
14
15
|
}
|
|
15
16
|
|
|
16
|
-
init(res) {
|
|
17
|
+
init(res, options) {
|
|
17
18
|
this.res = res;
|
|
19
|
+
this.options = Object.assign({
|
|
20
|
+
enable_precompressed_negotiation: false
|
|
21
|
+
}, options || {});
|
|
18
22
|
}
|
|
19
23
|
|
|
20
24
|
setBody(body) {
|
|
@@ -75,20 +79,93 @@ class Response {
|
|
|
75
79
|
let obj = this,
|
|
76
80
|
path = require("path"),
|
|
77
81
|
fs = require("fs");
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
82
|
+
let templatePath = 'pages/' + template;
|
|
83
|
+
let acceptEncoding = request.acceptEncoding || [];
|
|
84
|
+
let allowBrotli = Boolean(obj.options.enable_precompressed_negotiation);
|
|
85
|
+
let baseFilename = path.join(process.cwd(), 'public', templatePath);
|
|
86
|
+
let candidates = [];
|
|
87
|
+
|
|
88
|
+
if (allowBrotli && obj.supportsEncoding(acceptEncoding, 'br')) {
|
|
89
|
+
candidates.push({ filename: baseFilename + '.br', encoding: 'br' });
|
|
86
90
|
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
+
if (obj.supportsEncoding(acceptEncoding, 'gzip')) {
|
|
92
|
+
candidates.push({ filename: baseFilename + '.gz', encoding: 'gzip' });
|
|
93
|
+
}
|
|
94
|
+
candidates.push({ filename: baseFilename, encoding: '' });
|
|
95
|
+
|
|
96
|
+
let hasNegotiation = candidates.some((candidate) => candidate.encoding !== '');
|
|
97
|
+
obj.resolveFirstAvailableFile(candidates, (err, selectedFile) => {
|
|
98
|
+
if (err || !selectedFile) {
|
|
99
|
+
return obj.error404();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
obj.res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
103
|
+
if (hasNegotiation) {
|
|
104
|
+
obj.res.setHeader('Vary', 'Accept-Encoding');
|
|
105
|
+
}
|
|
106
|
+
if (selectedFile.encoding) {
|
|
107
|
+
obj.res.setHeader('Content-Encoding', selectedFile.encoding);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
let fileStream = fs.createReadStream(selectedFile.filename);
|
|
111
|
+
fileStream.on('error', () => {
|
|
112
|
+
obj.error404();
|
|
113
|
+
});
|
|
114
|
+
fileStream.pipe(obj.res);
|
|
115
|
+
});
|
|
91
116
|
}
|
|
117
|
+
|
|
118
|
+
resolveFirstAvailableFile(candidates, callback) {
|
|
119
|
+
let fs = require("fs");
|
|
120
|
+
let index = 0;
|
|
121
|
+
function resolveCandidate() {
|
|
122
|
+
let currentCandidate = candidates[index];
|
|
123
|
+
if (!currentCandidate) {
|
|
124
|
+
return callback(new Error('File not found'));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
fs.stat(currentCandidate.filename, (err, stats) => {
|
|
128
|
+
if (!err && stats && stats.isFile()) {
|
|
129
|
+
return callback(null, currentCandidate);
|
|
130
|
+
}
|
|
131
|
+
index = index + 1;
|
|
132
|
+
resolveCandidate();
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
resolveCandidate();
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
supportsEncoding(acceptEncoding, encoding) {
|
|
140
|
+
if (!Array.isArray(acceptEncoding)) {
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
let normalizedEncoding = String(encoding).toLowerCase();
|
|
145
|
+
return acceptEncoding.some((entry) => {
|
|
146
|
+
if (entry == null) {
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
let token = String(entry).toLowerCase();
|
|
150
|
+
let parts = token.split(';');
|
|
151
|
+
if (parts[0] !== normalizedEncoding) {
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
let qValue = 1;
|
|
156
|
+
for (let idx = 1; idx < parts.length; idx = idx + 1) {
|
|
157
|
+
let part = parts[idx];
|
|
158
|
+
if (part.startsWith('q=')) {
|
|
159
|
+
let parsedQValue = parseFloat(part.slice(2));
|
|
160
|
+
if (!Number.isNaN(parsedQValue)) {
|
|
161
|
+
qValue = parsedQValue;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return qValue > 0;
|
|
167
|
+
});
|
|
168
|
+
}
|
|
92
169
|
}
|
|
93
170
|
|
|
94
171
|
module.exports = Response;
|
package/framework/router.js
CHANGED
|
@@ -16,6 +16,8 @@ class Router {
|
|
|
16
16
|
this.defaultRoute = '';
|
|
17
17
|
this.server = server;
|
|
18
18
|
this.cwd = process.cwd();
|
|
19
|
+
this.staticMetadataCache = new Map();
|
|
20
|
+
this.staticFileWatchers = new Map();
|
|
19
21
|
this.mimes = {
|
|
20
22
|
'png': 'image/png',
|
|
21
23
|
'webp': 'image/webp',
|
|
@@ -32,6 +34,7 @@ class Router {
|
|
|
32
34
|
};
|
|
33
35
|
this.compressionMimes = [ 'css', 'js' ];
|
|
34
36
|
this.compressionFiles = [ 'vanilla.min.js', 'app.min.css' ];
|
|
37
|
+
this.enablePrecompressedNegotiation = Boolean(server?.options?.enable_precompressed_negotiation);
|
|
35
38
|
}
|
|
36
39
|
|
|
37
40
|
routeToRegExp(route) {
|
|
@@ -71,7 +74,7 @@ class Router {
|
|
|
71
74
|
|
|
72
75
|
let obj = this;
|
|
73
76
|
let isMatch = false;
|
|
74
|
-
let response = new Response(res);
|
|
77
|
+
let response = new Response(res, obj.server.options);
|
|
75
78
|
let request = new Request(req, {
|
|
76
79
|
onDataReceived: function () {
|
|
77
80
|
if (request.path == '') { request.path = obj.defaultRoute; }
|
|
@@ -93,7 +96,7 @@ class Router {
|
|
|
93
96
|
// -- Check static files
|
|
94
97
|
if (!handled && !isMatch) {
|
|
95
98
|
|
|
96
|
-
let ext = path.extname(
|
|
99
|
+
let ext = path.extname(request.path).replace('.', ''),
|
|
97
100
|
extHandled = false,
|
|
98
101
|
extHeader = {};
|
|
99
102
|
|
|
@@ -109,29 +112,47 @@ class Router {
|
|
|
109
112
|
filename = path.join(rep, route),
|
|
110
113
|
filePrivate = obj.isProtectedFile(route);
|
|
111
114
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
obj.compressionMimes.includes(ext) &&
|
|
115
|
-
obj.compressionFiles.includes(filename.split('/').pop())
|
|
116
|
-
) {
|
|
117
|
-
filename = filename + '.gz';
|
|
118
|
-
extHeader['Content-Encoding'] = 'gzip';
|
|
115
|
+
if (filePrivate) {
|
|
116
|
+
return obj.onNotFound(response);
|
|
119
117
|
}
|
|
120
118
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
119
|
+
let staticCandidates = obj.getStaticCandidates(request, ext, filename);
|
|
120
|
+
let hasConditionalHeaders = Boolean(req.headers['if-none-match'] || req.headers['if-modified-since']);
|
|
121
|
+
obj.resolveFirstAvailableStaticFile(staticCandidates, hasConditionalHeaders, (err, staticFile) => {
|
|
122
|
+
if (err || !staticFile) {
|
|
124
123
|
return obj.onNotFound(response);
|
|
125
124
|
}
|
|
126
|
-
|
|
125
|
+
|
|
126
|
+
let staticHeaders = Object.assign({}, extHeader);
|
|
127
|
+
if (staticFile.contentEncoding) {
|
|
128
|
+
staticHeaders['Content-Encoding'] = staticFile.contentEncoding;
|
|
129
|
+
}
|
|
130
|
+
if (staticCandidates.some((candidate) => candidate.contentEncoding)) {
|
|
131
|
+
staticHeaders['Vary'] = 'Accept-Encoding';
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
let metadata = staticFile.metadata;
|
|
135
|
+
staticHeaders['Content-Length'] = metadata.size;
|
|
136
|
+
staticHeaders['ETag'] = metadata.etag;
|
|
137
|
+
staticHeaders['Last-Modified'] = metadata.lastModified;
|
|
138
|
+
// Force revalidation to keep clients fresh without hard reload.
|
|
139
|
+
staticHeaders['Cache-Control'] = 'no-cache, must-revalidate';
|
|
140
|
+
|
|
141
|
+
if (obj.isNotModified(req, metadata)) {
|
|
142
|
+
let notModifiedHeaders = Object.assign({}, staticHeaders);
|
|
143
|
+
delete notModifiedHeaders['Content-Length'];
|
|
144
|
+
res.writeHead(304, notModifiedHeaders);
|
|
145
|
+
return res.end();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const fileStream = fs.createReadStream(staticFile.filename);
|
|
127
149
|
fileStream.on('error', (streamErr) => {
|
|
128
150
|
console.error("Error reading file:", streamErr);
|
|
129
151
|
res.writeHead(500);
|
|
130
152
|
res.end('Server Error');
|
|
131
153
|
});
|
|
132
154
|
|
|
133
|
-
|
|
134
|
-
res.writeHead(200, extHeader);
|
|
155
|
+
res.writeHead(200, staticHeaders);
|
|
135
156
|
fileStream.pipe(res);
|
|
136
157
|
res.on('close', () => {});
|
|
137
158
|
});
|
|
@@ -142,8 +163,169 @@ class Router {
|
|
|
142
163
|
isMatch = false;
|
|
143
164
|
}
|
|
144
165
|
|
|
166
|
+
getStaticFileMetadata(filename, forceRefresh, callback) {
|
|
167
|
+
let obj = this;
|
|
168
|
+
forceRefresh = forceRefresh || false;
|
|
169
|
+
let cachedMetadata = obj.staticMetadataCache.get(filename);
|
|
170
|
+
if (cachedMetadata && !forceRefresh) {
|
|
171
|
+
return callback(null, cachedMetadata);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
fs.stat(filename, (err, stats) => {
|
|
175
|
+
if (err || !stats.isFile()) {
|
|
176
|
+
return callback(err || new Error('File not found'));
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
let metadata = {
|
|
180
|
+
size: stats.size,
|
|
181
|
+
lastModified: stats.mtime.toUTCString(),
|
|
182
|
+
etag: `W/"${stats.size}-${Math.floor(stats.mtimeMs)}"`
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
obj.staticMetadataCache.set(filename, metadata);
|
|
186
|
+
obj.watchStaticFile(filename);
|
|
187
|
+
callback(null, metadata);
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
getStaticCandidates(request, ext, filename) {
|
|
192
|
+
let obj = this;
|
|
193
|
+
let candidates = [{ filename: filename, contentEncoding: '' }];
|
|
194
|
+
let isCompressible = obj.compressionMimes.includes(ext) && obj.compressionFiles.includes(path.basename(filename));
|
|
195
|
+
if (!isCompressible) {
|
|
196
|
+
return candidates;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
let compressedCandidates = [];
|
|
200
|
+
if (obj.enablePrecompressedNegotiation && obj.supportsEncoding(request.acceptEncoding, 'br')) {
|
|
201
|
+
compressedCandidates.push({
|
|
202
|
+
filename: filename + '.br',
|
|
203
|
+
contentEncoding: 'br'
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (obj.supportsEncoding(request.acceptEncoding, 'gzip')) {
|
|
208
|
+
compressedCandidates.push({
|
|
209
|
+
filename: filename + '.gz',
|
|
210
|
+
contentEncoding: 'gzip'
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return compressedCandidates.concat(candidates);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
resolveFirstAvailableStaticFile(candidates, forceRefresh, callback) {
|
|
218
|
+
let obj = this;
|
|
219
|
+
let index = 0;
|
|
220
|
+
function resolveCandidate() {
|
|
221
|
+
let currentCandidate = candidates[index];
|
|
222
|
+
if (!currentCandidate) {
|
|
223
|
+
return callback(new Error('No static file found'));
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
obj.getStaticFileMetadata(currentCandidate.filename, forceRefresh, (err, metadata) => {
|
|
227
|
+
if (!err && metadata) {
|
|
228
|
+
return callback(null, {
|
|
229
|
+
filename: currentCandidate.filename,
|
|
230
|
+
contentEncoding: currentCandidate.contentEncoding,
|
|
231
|
+
metadata: metadata
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
index = index + 1;
|
|
235
|
+
resolveCandidate();
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
resolveCandidate();
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
supportsEncoding(acceptEncoding, encoding) {
|
|
243
|
+
if (!Array.isArray(acceptEncoding)) {
|
|
244
|
+
return false;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
let normalizedEncoding = String(encoding).toLowerCase();
|
|
248
|
+
return acceptEncoding.some((entry) => {
|
|
249
|
+
if (entry == null) {
|
|
250
|
+
return false;
|
|
251
|
+
}
|
|
252
|
+
let token = String(entry).toLowerCase();
|
|
253
|
+
let parts = token.split(';');
|
|
254
|
+
if (parts[0] !== normalizedEncoding) {
|
|
255
|
+
return false;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
let qValue = 1;
|
|
259
|
+
for (let idx = 1; idx < parts.length; idx = idx + 1) {
|
|
260
|
+
let part = parts[idx];
|
|
261
|
+
if (part.startsWith('q=')) {
|
|
262
|
+
let parsedQValue = parseFloat(part.slice(2));
|
|
263
|
+
if (!Number.isNaN(parsedQValue)) {
|
|
264
|
+
qValue = parsedQValue;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return qValue > 0;
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
watchStaticFile(filename) {
|
|
274
|
+
let obj = this;
|
|
275
|
+
if (obj.staticFileWatchers.has(filename)) {
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
try {
|
|
280
|
+
let watcher = fs.watch(filename, (eventType) => {
|
|
281
|
+
obj.staticMetadataCache.delete(filename);
|
|
282
|
+
if (eventType === 'rename') {
|
|
283
|
+
let renamedWatcher = obj.staticFileWatchers.get(filename);
|
|
284
|
+
if (renamedWatcher) {
|
|
285
|
+
renamedWatcher.close();
|
|
286
|
+
}
|
|
287
|
+
obj.staticFileWatchers.delete(filename);
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
watcher.on('error', () => {
|
|
292
|
+
obj.staticMetadataCache.delete(filename);
|
|
293
|
+
let activeWatcher = obj.staticFileWatchers.get(filename);
|
|
294
|
+
if (activeWatcher) {
|
|
295
|
+
activeWatcher.close();
|
|
296
|
+
}
|
|
297
|
+
obj.staticFileWatchers.delete(filename);
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
obj.staticFileWatchers.set(filename, watcher);
|
|
301
|
+
} catch (err) {
|
|
302
|
+
// If watch cannot be created, keep runtime behavior and continue.
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
isNotModified(req, metadata) {
|
|
307
|
+
let ifNoneMatch = req.headers['if-none-match'];
|
|
308
|
+
if (ifNoneMatch) {
|
|
309
|
+
let etags = ifNoneMatch.split(',').map((etag) => etag.trim());
|
|
310
|
+
if (etags.includes(metadata.etag)) {
|
|
311
|
+
return true;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
let ifModifiedSince = req.headers['if-modified-since'];
|
|
316
|
+
if (ifModifiedSince) {
|
|
317
|
+
let requestModifiedSince = new Date(ifModifiedSince).getTime();
|
|
318
|
+
let fileModifiedAt = new Date(metadata.lastModified).getTime();
|
|
319
|
+
if (!Number.isNaN(requestModifiedSince) && requestModifiedSince >= fileModifiedAt) {
|
|
320
|
+
return true;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
return false;
|
|
325
|
+
}
|
|
326
|
+
|
|
145
327
|
isProtectedFile(route) {
|
|
146
|
-
let protectedDirs = ['framework', 'external', '
|
|
328
|
+
let protectedDirs = ['framework', 'external', 'node_modules'];
|
|
147
329
|
let routeParts = route.split('/');
|
|
148
330
|
if (routeParts[1] != undefined && routeParts.length > 2) {
|
|
149
331
|
return protectedDirs.includes(routeParts[1]);
|
package/framework/server.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vanilla-jet",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "VannilaJet framework",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -9,10 +9,12 @@
|
|
|
9
9
|
"scripts": {
|
|
10
10
|
"setup": "node ./.scripts/generate_packages_json.js",
|
|
11
11
|
"dev": "gulp dev --env development",
|
|
12
|
+
"dev:vite": "node ./.scripts/run_vite.js dev",
|
|
13
|
+
"build:vite": "node ./.scripts/run_vite.js build",
|
|
12
14
|
"build:qa": "gulp build --env qa",
|
|
13
15
|
"build:staging": "gulp build --env staging",
|
|
14
16
|
"build:prod": "gulp build --env production",
|
|
15
|
-
"test": "
|
|
17
|
+
"test": "node -e \"console.log('No automated tests configured yet')\""
|
|
16
18
|
},
|
|
17
19
|
"repository": {
|
|
18
20
|
"type": "git",
|
|
@@ -31,15 +33,6 @@
|
|
|
31
33
|
"dependencies": {
|
|
32
34
|
"blueimp-md5": "2.19.0",
|
|
33
35
|
"chalk": "4.1.2",
|
|
34
|
-
"html-minifier-terser": "7.2.0",
|
|
35
|
-
"js-beautify": "1.15.4",
|
|
36
|
-
"jsrsasign": "11.1.0",
|
|
37
|
-
"jwt-simple": "0.5.6",
|
|
38
|
-
"minimist": "1.2.8",
|
|
39
|
-
"nodemon": "3.1.10",
|
|
40
|
-
"nunjucks": "3.2.4",
|
|
41
|
-
"underscore": ">= 1.12.x",
|
|
42
|
-
"zlib": "1.0.5",
|
|
43
36
|
"del": "^6.0.0",
|
|
44
37
|
"gulp": "^4.0.2",
|
|
45
38
|
"gulp-clean-css": "^4.3.0",
|
|
@@ -52,6 +45,16 @@
|
|
|
52
45
|
"gulp-rename": "^2.0.0",
|
|
53
46
|
"gulp-shell": "^0.8.0",
|
|
54
47
|
"gulp-uglify": "^3.0.2",
|
|
55
|
-
"gulp-watch": "^5.0.1"
|
|
48
|
+
"gulp-watch": "^5.0.1",
|
|
49
|
+
"html-minifier-terser": "7.2.0",
|
|
50
|
+
"js-beautify": "1.15.4",
|
|
51
|
+
"jsrsasign": "11.1.0",
|
|
52
|
+
"jwt-simple": "0.5.6",
|
|
53
|
+
"minimist": "1.2.8",
|
|
54
|
+
"nodemon": "3.1.10",
|
|
55
|
+
"nunjucks": "3.2.4",
|
|
56
|
+
"underscore": ">= 1.12.x",
|
|
57
|
+
"vite": "^7.3.1",
|
|
58
|
+
"zlib": "1.0.5"
|
|
56
59
|
}
|
|
57
60
|
}
|
package/vite.config.js
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { defineConfig } = require('vite');
|
|
4
|
+
|
|
5
|
+
function resolveConsumerRoot() {
|
|
6
|
+
return process.cwd()
|
|
7
|
+
.replace('/node_modules/vanilla-jet', '')
|
|
8
|
+
.replace('/.scripts', '')
|
|
9
|
+
.replace('/.grunt', '');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function collectScriptFiles(dirPath) {
|
|
13
|
+
if (!fs.existsSync(dirPath)) {
|
|
14
|
+
return [];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const files = [];
|
|
18
|
+
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
|
|
19
|
+
entries.forEach((entry) => {
|
|
20
|
+
const entryPath = path.join(dirPath, entry.name);
|
|
21
|
+
if (entry.isDirectory()) {
|
|
22
|
+
files.push(...collectScriptFiles(entryPath));
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (entry.isFile() && entry.name.endsWith('.js')) {
|
|
27
|
+
files.push(entryPath);
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
return files;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function normalize(filePath) {
|
|
34
|
+
return filePath.split(path.sep).join('/');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function scriptPriority(filePath) {
|
|
38
|
+
const normalized = normalize(filePath);
|
|
39
|
+
if (normalized.includes('/assets/scripts/controllers/')) return 0;
|
|
40
|
+
if (normalized.includes('/assets/scripts/views/')) return 1;
|
|
41
|
+
if (normalized.includes('/assets/scripts/api/')) return 2;
|
|
42
|
+
return 3;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function includeInBundle(filePath) {
|
|
46
|
+
const normalized = normalize(filePath);
|
|
47
|
+
if (normalized.includes('/assets/scripts/core/')) return false;
|
|
48
|
+
if (normalized.includes('/assets/scripts/plugins/')) return false;
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function writeVirtualEntry(virtualEntryPath, orderedScripts, lessEntryPath, hasLess) {
|
|
53
|
+
const imports = orderedScripts.map((scriptPath, index) => {
|
|
54
|
+
const importPath = normalize(scriptPath) + '?raw';
|
|
55
|
+
return `import __vanillajet_script_${index} from ${JSON.stringify(importPath)};`;
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
if (hasLess) {
|
|
59
|
+
imports.push(`import ${JSON.stringify(normalize(lessEntryPath))};`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const mappings = orderedScripts.map((scriptPath, index) => {
|
|
63
|
+
const rel = normalize(path.relative(path.dirname(virtualEntryPath), scriptPath));
|
|
64
|
+
return `{ source: __vanillajet_script_${index}, file: ${JSON.stringify(rel)} }`;
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const lines = [
|
|
68
|
+
'/* Auto-generated by VanillaJet Vite config. */',
|
|
69
|
+
...imports,
|
|
70
|
+
'',
|
|
71
|
+
'function executeGlobalScript(sourceCode, sourceFile) {',
|
|
72
|
+
' const tag = document.createElement("script");',
|
|
73
|
+
' tag.type = "text/javascript";',
|
|
74
|
+
' tag.setAttribute("data-vanillajet-source", sourceFile);',
|
|
75
|
+
' tag.text = sourceCode;',
|
|
76
|
+
' document.head.appendChild(tag);',
|
|
77
|
+
' document.head.removeChild(tag);',
|
|
78
|
+
'}',
|
|
79
|
+
'',
|
|
80
|
+
`const scriptMap = [${mappings.join(', ')}];`,
|
|
81
|
+
'scriptMap.forEach(({ source, file }) => executeGlobalScript(source, file));',
|
|
82
|
+
'',
|
|
83
|
+
'if (import.meta.hot) {',
|
|
84
|
+
' import.meta.hot.accept(() => {',
|
|
85
|
+
' window.location.reload();',
|
|
86
|
+
' });',
|
|
87
|
+
'}'
|
|
88
|
+
];
|
|
89
|
+
|
|
90
|
+
fs.mkdirSync(path.dirname(virtualEntryPath), { recursive: true });
|
|
91
|
+
fs.writeFileSync(virtualEntryPath, lines.join('\n'), 'utf8');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
module.exports = defineConfig(({ command, mode }) => {
|
|
95
|
+
const rootDir = resolveConsumerRoot();
|
|
96
|
+
const scriptsDir = path.join(rootDir, 'assets/scripts');
|
|
97
|
+
const lessEntryPath = path.join(rootDir, 'assets/styles/less/admin.less');
|
|
98
|
+
const virtualEntryPath = path.join(rootDir, '.vanillajet/vite-entry.js');
|
|
99
|
+
|
|
100
|
+
const scripts = collectScriptFiles(scriptsDir)
|
|
101
|
+
.filter(includeInBundle)
|
|
102
|
+
.sort((left, right) => {
|
|
103
|
+
const priorityDiff = scriptPriority(left) - scriptPriority(right);
|
|
104
|
+
if (priorityDiff !== 0) {
|
|
105
|
+
return priorityDiff;
|
|
106
|
+
}
|
|
107
|
+
return normalize(left).localeCompare(normalize(right));
|
|
108
|
+
});
|
|
109
|
+
const hasLess = fs.existsSync(lessEntryPath);
|
|
110
|
+
const hasSources = scripts.length > 0 || hasLess;
|
|
111
|
+
|
|
112
|
+
writeVirtualEntry(virtualEntryPath, scripts, lessEntryPath, hasLess);
|
|
113
|
+
|
|
114
|
+
const helperPath = '/__vanillajet__/';
|
|
115
|
+
const helperHtml = [
|
|
116
|
+
'<!doctype html>',
|
|
117
|
+
'<html lang="en">',
|
|
118
|
+
'<head>',
|
|
119
|
+
' <meta charset="utf-8" />',
|
|
120
|
+
' <meta name="viewport" content="width=device-width, initial-scale=1" />',
|
|
121
|
+
' <title>VanillaJet Vite Dev</title>',
|
|
122
|
+
'</head>',
|
|
123
|
+
'<body>',
|
|
124
|
+
' <h3>VanillaJet Vite dev helper</h3>',
|
|
125
|
+
' <p>This page only loads JS/LESS from assets for DX.</p>',
|
|
126
|
+
' <p>Nunjucks templates and legacy Node routes remain unchanged.</p>',
|
|
127
|
+
` <script type="module" src="${normalize(path.relative(rootDir, virtualEntryPath)).startsWith('.') ? '/' + normalize(path.relative(rootDir, virtualEntryPath)).replace(/^\.\//, '') : '/' + normalize(path.relative(rootDir, virtualEntryPath))}"></script>`,
|
|
128
|
+
'</body>',
|
|
129
|
+
'</html>'
|
|
130
|
+
].join('\n');
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
root: rootDir,
|
|
134
|
+
publicDir: false,
|
|
135
|
+
server: {
|
|
136
|
+
host: true,
|
|
137
|
+
port: 5173
|
|
138
|
+
},
|
|
139
|
+
build: {
|
|
140
|
+
outDir: path.join(rootDir, 'public'),
|
|
141
|
+
emptyOutDir: false,
|
|
142
|
+
sourcemap: mode !== 'production',
|
|
143
|
+
rollupOptions: {
|
|
144
|
+
input: virtualEntryPath,
|
|
145
|
+
output: {
|
|
146
|
+
entryFileNames: 'scripts/vanilla.min.js',
|
|
147
|
+
chunkFileNames: 'scripts/chunks/[name]-[hash].js',
|
|
148
|
+
assetFileNames: (assetInfo) => {
|
|
149
|
+
if (assetInfo.name && assetInfo.name.endsWith('.css')) {
|
|
150
|
+
return 'styles/app.min.css';
|
|
151
|
+
}
|
|
152
|
+
return 'assets/[name]-[hash][extname]';
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
plugins: [
|
|
158
|
+
{
|
|
159
|
+
name: 'vanillajet-dev-helper',
|
|
160
|
+
configureServer(server) {
|
|
161
|
+
if (!hasSources) {
|
|
162
|
+
server.config.logger.warn(
|
|
163
|
+
'[vanillajet] No JS/LESS sources found under assets/. Vite will run with an empty entry.'
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
server.middlewares.use((req, res, next) => {
|
|
168
|
+
if (req.url === helperPath) {
|
|
169
|
+
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
170
|
+
res.end(helperHtml);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
next();
|
|
174
|
+
});
|
|
175
|
+
},
|
|
176
|
+
buildStart() {
|
|
177
|
+
if (command === 'build' && !hasSources) {
|
|
178
|
+
this.warn('[vanillajet] No JS/LESS sources found under assets/. Build will output an empty JS bundle.');
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
]
|
|
183
|
+
};
|
|
184
|
+
});
|