vanilla-jet 1.3.1 → 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.
@@ -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
+ }
@@ -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 != undefined) {
218
- _.each(item.require, function(dep) {
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 != undefined) {
237
- _.each(item.require, function(dep) {
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.includeAnim(anim);
344
+ animsString += obj.includeAnimation(anim);
345
345
  });
346
346
  let baseAnimsString = `<script>'${animsString}'</script>`;
347
347
  return baseAnimsString;
@@ -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
- this.init(res);
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
- template = 'pages/' + template;
79
-
80
- const acceptEncoding = request.acceptEncoding || [];
81
- if (acceptEncoding.includes('gzip')) {
82
- template = template + '.gz';
83
- obj.res.setHeader('Content-Encoding', 'gzip');
84
- } else {
85
- obj.res.setHeader('Content-Type', 'text/html');
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
- const filename = path.join(process.cwd(), 'public/' + template);
89
- const fileStream = fs.createReadStream(filename);
90
- fileStream.pipe(obj.res);
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;
@@ -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(req.url).replace('.', ''),
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
- // -- Check if the file is a gzip file
113
- if (request.acceptEncoding.includes('gzip') &&
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
- fs.stat(filename, (err, stats) => {
122
-
123
- if (err || !stats.isFile() || filePrivate) {
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
- const fileStream = fs.createReadStream(filename);
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
- extHeader['Content-Length'] = stats.size;
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', 'node_mudules'];
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]);
@@ -25,13 +25,14 @@ class Server {
25
25
 
26
26
  let obj = this,
27
27
  settings = options.settings,
28
- opts = settings[options.profile] || {},
28
+ opts = settings['profile'] || {},
29
29
  shared = settings['shared'] || {},
30
30
  security = settings['security'] || {};
31
31
 
32
32
  _.defaults(opts, {
33
33
  https_server: false,
34
- wsServer: false
34
+ wsServer: false,
35
+ enable_precompressed_negotiation: false
35
36
  });
36
37
  obj.options = opts;
37
38
 
package/gulpfile.js CHANGED
@@ -13,13 +13,6 @@ const del = require('del');
13
13
  const gulpif = require('gulp-if');
14
14
  const minimist = require('minimist');
15
15
 
16
- // Parse command line arguments
17
- const knownOptions = {
18
- string: 'env',
19
- default: { env: process.env.NODE_ENV || 'development' }
20
- };
21
- const options = minimist(process.argv.slice(2), knownOptions);
22
-
23
16
  // Helper functions
24
17
  function getCwd() {
25
18
  return process.cwd()
@@ -120,7 +113,7 @@ function compressCss() {
120
113
  // Template compilation
121
114
  function compileTemplates() {
122
115
  return gulp.src('.')
123
- .pipe(shell([`node .grunt/compile_html.js ${options.env}`]));
116
+ .pipe(shell([`node .grunt/compile_html.js`]));
124
117
  }
125
118
 
126
119
  // Watch task
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vanilla-jet",
3
- "version": "1.3.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": "npm run 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
  }