@nitronjs/framework 0.2.13 → 0.2.14

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/lib/Auth/Auth.js CHANGED
@@ -82,6 +82,7 @@ class Auth {
82
82
  const guard = guardName || authConfig.defaults.guard;
83
83
 
84
84
  req.session.set(`auth_${guard}`, null);
85
+ req.session.regenerate();
85
86
  }
86
87
 
87
88
  /**
@@ -116,8 +116,11 @@ class Builder {
116
116
  };
117
117
  } catch (error) {
118
118
  this.#cleanupTemp();
119
- if (!silent) console.log(`\n${COLORS.red}✖ Build failed: ${error.message}${COLORS.reset}\n`);
120
- if (this.#isDev && !silent) console.error(error);
119
+
120
+ if (!silent) {
121
+ console.log(this.#formatBuildError(error));
122
+ }
123
+
121
124
  return { success: false, error: error.message };
122
125
  }
123
126
  }
@@ -580,6 +583,63 @@ class Builder {
580
583
  }
581
584
  }
582
585
 
586
+ #formatBuildError(error) {
587
+ const R = COLORS.red, Y = COLORS.yellow, D = COLORS.dim, C = COLORS.reset;
588
+
589
+ // FileAnalyzer errors already have formatted messages
590
+ if (error.message?.includes("✖")) {
591
+ return `\n${error.message}\n`;
592
+ }
593
+
594
+ // esbuild errors — extract detailed info from .errors array
595
+ if (error.errors?.length > 0) {
596
+ let output = `\n${R}✖ Build failed${C}\n`;
597
+
598
+ for (const err of error.errors.slice(0, 5)) {
599
+ const loc = err.location;
600
+ output += `\n ${R}${err.text}${C}\n`;
601
+
602
+ if (loc) {
603
+ const file = loc.file ? path.relative(Paths.project, loc.file) : "unknown";
604
+ output += ` ${D}at${C} ${Y}${file}:${loc.line}:${loc.column}${C}\n`;
605
+
606
+ if (loc.lineText) {
607
+ output += ` ${D}${loc.lineText.trim()}${C}\n`;
608
+ }
609
+ }
610
+
611
+ if (err.notes?.length > 0) {
612
+ for (const note of err.notes) {
613
+ output += ` ${D}→ ${note.text}${C}\n`;
614
+ }
615
+ }
616
+ }
617
+
618
+ if (error.errors.length > 5) {
619
+ output += `\n ${D}... and ${error.errors.length - 5} more errors${C}\n`;
620
+ }
621
+
622
+ return output;
623
+ }
624
+
625
+ // Generic errors — provide hint based on common patterns
626
+ const msg = error.message || "Unknown error";
627
+ let output = `\n${R}✖ Build failed${C}\n\n ${msg}\n`;
628
+
629
+ if (msg.includes("ENOENT")) {
630
+ output += `\n ${Y}Hint:${C} A file or directory was not found. Check your import paths.\n`;
631
+ }
632
+ else if (msg.includes("Duplicate")) {
633
+ output += `\n ${Y}Hint:${C} Two views resolve to the same route. Rename one of them.\n`;
634
+ }
635
+ else if (msg.includes("Cannot find module")) {
636
+ const match = msg.match(/Cannot find module ['"]([^'"]+)['"]/);
637
+ if (match) output += `\n ${Y}Hint:${C} Module "${match[1]}" is not installed. Run npm install.\n`;
638
+ }
639
+
640
+ return output;
641
+ }
642
+
583
643
  #cleanupTemp() {
584
644
  const projectDir = path.normalize(Paths.project) + path.sep;
585
645
  const normalizedTemp = path.normalize(this.#paths.nitronTemp);
@@ -35,7 +35,7 @@ globalThis.csrf = () => getContext()?.csrf || '';
35
35
 
36
36
  globalThis.request = () => {
37
37
  const ctx = getContext();
38
- return ctx?.request || { params: {}, query: {}, url: '', method: 'GET', headers: {} };
38
+ return ctx?.request || { path: '', method: 'GET', query: {}, params: {}, headers: {}, cookies: {}, ip: '', isAjax: false, session: null, auth: null };
39
39
  };
40
40
 
41
41
  const DepthContext = React.createContext(false);
@@ -21,7 +21,14 @@ class DateTime {
21
21
  static #format(date, formatString) {
22
22
  const lang = locale[Config.get('app.locale', 'en')] || locale.en;
23
23
  const pad = (n) => String(n).padStart(2, '0');
24
-
24
+
25
+ // Replace multi-character tokens first to avoid per-character replacement
26
+ let result = formatString;
27
+ result = result.replace(/MMMM/g, '\x01MONTH\x01');
28
+ result = result.replace(/MMM/g, '\x01MONTHSHORT\x01');
29
+ result = result.replace(/DDDD/g, '\x01DAY\x01');
30
+ result = result.replace(/DDD/g, '\x01DAYSHORT\x01');
31
+
25
32
  const map = {
26
33
  'Y': date.getFullYear(),
27
34
  'y': String(date.getFullYear()).slice(-2),
@@ -37,8 +44,16 @@ class DateTime {
37
44
  'M': lang.months[date.getMonth()],
38
45
  'F': lang.monthsShort[date.getMonth()]
39
46
  };
40
-
41
- return formatString.replace(/[YynmdjHislDMF]/g, match => map[match]);
47
+
48
+ result = result.replace(/[YynmdjHislDMF]/g, match => map[match]);
49
+
50
+ // Restore multi-character tokens
51
+ result = result.replace(/\x01MONTH\x01/g, lang.months[date.getMonth()]);
52
+ result = result.replace(/\x01MONTHSHORT\x01/g, lang.monthsShort[date.getMonth()]);
53
+ result = result.replace(/\x01DAY\x01/g, lang.days[date.getDay()]);
54
+ result = result.replace(/\x01DAYSHORT\x01/g, lang.daysShort[date.getDay()]);
55
+
56
+ return result;
42
57
  }
43
58
 
44
59
  /**
@@ -107,10 +107,47 @@ class Server {
107
107
 
108
108
  this.#server.register(fastifyStatic, {
109
109
  root: Paths.public,
110
- decorateReply: false,
111
- index: false
110
+ decorateReply: true,
111
+ index: false,
112
+ wildcard: false
112
113
  });
113
114
 
115
+ // Register explicit routes for each public subdirectory.
116
+ // Routes like /storage/* have a static prefix and take priority over
117
+ // parametric routes (/:slug), preventing catch-all routes from
118
+ // intercepting static file requests.
119
+ if (fs.existsSync(Paths.public)) {
120
+ for (const entry of fs.readdirSync(Paths.public)) {
121
+ const fullPath = path.resolve(Paths.public, entry);
122
+
123
+ try {
124
+ if (!fs.statSync(fullPath).isDirectory()) continue;
125
+ }
126
+ catch {
127
+ continue;
128
+ }
129
+
130
+ this.#server.route({
131
+ method: ["GET", "HEAD"],
132
+ url: `/${entry}/*`,
133
+ handler(req, reply) {
134
+ return reply.sendFile(req.params["*"], fullPath);
135
+ }
136
+ });
137
+ }
138
+
139
+ // Fallback wildcard for root-level files (favicon.ico, robots.txt, etc.)
140
+ // Less specific than /:slug so it loses when parametric routes exist,
141
+ // but keeps backward compatibility for projects without catch-all routes.
142
+ this.#server.route({
143
+ method: ["GET", "HEAD"],
144
+ url: "/*",
145
+ handler(req, reply) {
146
+ return reply.sendFile(req.params["*"]);
147
+ }
148
+ });
149
+ }
150
+
114
151
  this.#server.register(fastifyCookie, {
115
152
  secret: process.env.APP_KEY,
116
153
  hook: "onRequest"
@@ -27,7 +27,7 @@ class FileStore {
27
27
  }
28
28
 
29
29
  try {
30
- const content = await fs.readFile(this.#filePath(sessionId), "utf8");
30
+ const content = await this.#retryOnLock(() => fs.readFile(this.#filePath(sessionId), "utf8"));
31
31
 
32
32
  if (!content?.trim()) {
33
33
  await this.delete(sessionId);
@@ -48,6 +48,8 @@ class FileStore {
48
48
  return null;
49
49
  }
50
50
 
51
+ console.error("[Session File] Read error:", err.message);
52
+
51
53
  return null;
52
54
  }
53
55
  }
@@ -67,10 +69,19 @@ class FileStore {
67
69
  const json = Environment.isDev ? JSON.stringify(data, null, 2) : JSON.stringify(data);
68
70
 
69
71
  try {
70
- await fs.writeFile(tempPath, json, "utf8");
72
+ await this.#retryOnLock(() => fs.writeFile(tempPath, json, "utf8"));
71
73
  await this.#atomicRename(tempPath, filePath);
72
74
  }
73
- catch {
75
+ catch (err) {
76
+ // Atomic rename failed — write directly as fallback to prevent session loss
77
+ try {
78
+ await this.#retryOnLock(() => fs.writeFile(filePath, json, "utf8"));
79
+ }
80
+ catch (writeErr) {
81
+ console.error("[Session File] Write fallback failed:", writeErr.message);
82
+ throw writeErr;
83
+ }
84
+
74
85
  await fs.unlink(tempPath).catch(() => {});
75
86
  }
76
87
  }
@@ -85,12 +96,12 @@ class FileStore {
85
96
  }
86
97
 
87
98
  try {
88
- await fs.unlink(this.#filePath(sessionId));
99
+ await this.#retryOnLock(() => fs.unlink(this.#filePath(sessionId)));
89
100
  }
90
101
  catch (err) {
91
- if (err.code !== "ENOENT") {
92
- console.error("[Session File] Delete error:", err.message);
93
- }
102
+ if (err.code === "ENOENT") return;
103
+
104
+ console.error("[Session File] Delete error:", err.message);
94
105
  }
95
106
  }
96
107
 
@@ -118,16 +129,24 @@ class FileStore {
118
129
  const lastActivity = data.lastActivity || data.createdAt;
119
130
 
120
131
  if (now - lastActivity > lifetime) {
121
- await fs.unlink(filePath);
132
+ await fs.unlink(filePath).catch(() => {});
122
133
  deleted++;
123
134
  }
124
135
  }
125
136
  catch {
137
+ // Corrupted or unreadable — try to clean up, skip if locked
126
138
  await fs.unlink(filePath).catch(() => {});
127
139
  deleted++;
128
140
  }
129
141
  }
130
142
 
143
+ // Clean leftover temp files
144
+ for (const file of files) {
145
+ if (file.endsWith(".tmp")) {
146
+ await fs.unlink(path.join(this.#storagePath, file)).catch(() => {});
147
+ }
148
+ }
149
+
131
150
  return deleted;
132
151
  }
133
152
  catch (err) {
@@ -198,15 +217,41 @@ class FileStore {
198
217
  await fs.rename(from, to);
199
218
  }
200
219
  catch (err) {
201
- if (err.code === "EPERM" || err.code === "EEXIST") {
202
- await fs.unlink(to).catch(() => {});
203
- await fs.rename(from, to);
220
+ if (err.code === "EPERM" || err.code === "EBUSY" || err.code === "EEXIST") {
221
+ await this.#retryOnLock(() => fs.copyFile(from, to));
222
+ await fs.unlink(from).catch(() => {});
204
223
  }
205
224
  else {
206
225
  throw err;
207
226
  }
208
227
  }
209
228
  }
229
+
230
+ /**
231
+ * Retries a file operation with exponential backoff on Windows lock errors.
232
+ * @private
233
+ * @param {Function} fn - Async function to retry
234
+ * @param {number} maxRetries - Maximum retry attempts (default: 4)
235
+ * @returns {Promise<*>}
236
+ */
237
+ async #retryOnLock(fn, maxRetries = 4) {
238
+ let delay = 50;
239
+
240
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
241
+ try {
242
+ return await fn();
243
+ }
244
+ catch (err) {
245
+ if ((err.code === "EBUSY" || err.code === "EPERM") && attempt < maxRetries) {
246
+ await new Promise(r => setTimeout(r, delay));
247
+ delay *= 2;
248
+ continue;
249
+ }
250
+
251
+ throw err;
252
+ }
253
+ }
254
+ }
210
255
  }
211
256
 
212
257
  export default FileStore;
@@ -179,23 +179,21 @@ class SessionManager {
179
179
  });
180
180
 
181
181
  server.addHook("onSend", async (request, response, payload) => {
182
- if (request.session?.shouldRegenerate()) {
183
- response.setCookie(manager.cookieName, request.session.id, manager.cookieConfig);
184
- }
182
+ if (!request.session) return payload;
185
183
 
186
- return payload;
187
- });
184
+ try {
185
+ await manager.persist(request.session);
188
186
 
189
- server.addHook("onResponse", async (request, response) => {
190
- if (!request.session || response.statusCode >= 400) {
191
- return;
187
+ if (request.session.shouldRegenerate()) {
188
+ response.setCookie(manager.cookieName, request.session.id, manager.cookieConfig);
189
+ await manager.deleteOld(request.session.getOldId());
190
+ }
192
191
  }
193
-
194
- if (request.session.shouldRegenerate()) {
195
- await manager.deleteOld(request.session.getOldId());
192
+ catch (err) {
193
+ console.error("[Session] Persist error:", err.message);
196
194
  }
197
195
 
198
- await manager.persist(request.session);
196
+ return payload;
199
197
  });
200
198
 
201
199
  manager.#startGC();
@@ -17,6 +17,10 @@ class Session {
17
17
  this.#data = sessionData.data || {};
18
18
  this.#createdAt = sessionData.createdAt || Date.now();
19
19
  this.#lastActivity = sessionData.lastActivity || null;
20
+
21
+ // Age flash data: previous request's flash becomes readable, then expires.
22
+ this.#data._previousFlash = this.#data._flash || {};
23
+ this.#data._flash = {};
20
24
  }
21
25
 
22
26
  get id() {
@@ -50,11 +54,14 @@ class Session {
50
54
  }
51
55
 
52
56
  /**
53
- * Gets all session data (copy).
57
+ * Gets all session data for persistence.
58
+ * Excludes aged flash data that should not survive another request.
54
59
  * @returns {Object}
55
60
  */
56
61
  all() {
57
- return { ...this.#data };
62
+ const { _previousFlash, ...rest } = this.#data;
63
+
64
+ return rest;
58
65
  }
59
66
 
60
67
  /**
@@ -100,32 +107,21 @@ class Session {
100
107
  }
101
108
 
102
109
  /**
103
- * Stores a flash message (available only for next request).
110
+ * Stores a flash message (available only for the next request).
104
111
  * @param {string} key
105
112
  * @param {*} value
106
113
  */
107
114
  flash(key, value) {
108
- if (!this.#data._flash) {
109
- this.#data._flash = {};
110
- }
111
-
112
115
  this.#data._flash[key] = value;
113
116
  }
114
117
 
115
118
  /**
116
- * Gets and removes a flash message.
119
+ * Gets a flash message from the previous request.
117
120
  * @param {string} key
118
121
  * @returns {*}
119
122
  */
120
123
  getFlash(key) {
121
- if (!this.#data._flash?.[key]) {
122
- return undefined;
123
- }
124
-
125
- const value = this.#data._flash[key];
126
- delete this.#data._flash[key];
127
-
128
- return value;
124
+ return this.#data._previousFlash?.[key];
129
125
  }
130
126
 
131
127
  /**
package/lib/View/View.js CHANGED
@@ -291,94 +291,102 @@ class View {
291
291
  throw new Error(`View not found: ${name}`);
292
292
  }
293
293
 
294
- const mod = await import(pathToFileURL(viewPath).href + `?t=${Date.now()}`);
295
- if (!mod.default) {
296
- throw new Error("View must have a default export");
297
- }
298
-
299
- const layoutChain = entry?.layouts || [];
300
- const layoutModules = [];
301
-
302
- for (const layoutName of layoutChain) {
303
- const layoutPath = path.join(baseDir, layoutName + ".js");
304
- if (existsSync(layoutPath)) {
305
- const layoutMod = await import(pathToFileURL(layoutPath).href + `?t=${Date.now()}`);
306
- if (layoutMod.default) {
307
- layoutModules.push(layoutMod);
308
- }
309
- }
310
- }
311
-
312
294
  const nonce = randomBytes(16).toString("hex");
313
295
  const ctx = {
314
296
  nonce,
315
297
  csrf,
316
298
  props: {},
317
299
  request: fastifyRequest ? {
318
- params: fastifyRequest.params || {},
319
- query: fastifyRequest.query || {},
320
- url: fastifyRequest.url || '',
300
+ path: (fastifyRequest.url || '').split('?')[0],
321
301
  method: fastifyRequest.method || 'GET',
322
- headers: fastifyRequest.headers || {}
302
+ query: fastifyRequest.query || {},
303
+ params: fastifyRequest.params || {},
304
+ headers: fastifyRequest.headers || {},
305
+ cookies: fastifyRequest.cookies || {},
306
+ ip: fastifyRequest.ip || '',
307
+ isAjax: fastifyRequest.headers?.['x-requested-with'] === 'XMLHttpRequest',
308
+ session: fastifyRequest.session || null,
309
+ auth: fastifyRequest.auth || null,
323
310
  } : null
324
311
  };
325
- let html = "";
326
- let collectedProps = {};
327
312
 
328
- try {
329
- const Component = mod.default;
330
- let element;
331
-
332
- if (Component[MARK]) {
333
- ctx.props[":R0:"] = this.#sanitizeProps(params);
334
- element = React.createElement(
335
- "div",
336
- {
337
- "data-cid": ":R0:",
338
- "data-island": Component.displayName || Component.name || "Anonymous"
339
- },
340
- React.createElement(Component, params)
341
- );
342
- } else {
343
- element = React.createElement(Component, params);
313
+ return Context.run(ctx, async () => {
314
+ const mod = await import(pathToFileURL(viewPath).href + `?t=${Date.now()}`);
315
+ if (!mod.default) {
316
+ throw new Error("View must have a default export");
344
317
  }
345
318
 
346
- if (layoutModules.length > 0) {
347
- element = React.createElement("div", { "data-nitron-slot": "page" }, element);
348
- }
319
+ const layoutChain = entry?.layouts || [];
320
+ const layoutModules = [];
349
321
 
350
- for (let i = layoutModules.length - 1; i >= 0; i--) {
351
- const LayoutComponent = layoutModules[i].default;
352
- element = React.createElement(LayoutComponent, { children: element });
322
+ for (const layoutName of layoutChain) {
323
+ const layoutPath = path.join(baseDir, layoutName + ".js");
324
+ if (existsSync(layoutPath)) {
325
+ const layoutMod = await import(pathToFileURL(layoutPath).href + `?t=${Date.now()}`);
326
+ if (layoutMod.default) {
327
+ layoutModules.push(layoutMod);
328
+ }
329
+ }
353
330
  }
354
331
 
355
- html = await Context.run(ctx, () => this.#renderToHtml(element));
356
- collectedProps = ctx.props;
357
- } catch (error) {
358
- const componentName = mod.default?.displayName || mod.default?.name || "Unknown";
359
- const errorDetails = this.#parseReactError(error);
360
-
361
- Log.error("Render Error", {
362
- view: name,
363
- component: componentName,
364
- error: error.message,
365
- cause: errorDetails.cause,
366
- location: errorDetails.location,
367
- stack: error.stack
368
- });
332
+ let html = "";
333
+ let collectedProps = {};
369
334
 
370
- error.statusCode = error.statusCode || 500;
371
- throw error;
372
- }
335
+ try {
336
+ const Component = mod.default;
337
+ let element;
338
+
339
+ if (Component[MARK]) {
340
+ ctx.props[":R0:"] = this.#sanitizeProps(params);
341
+ element = React.createElement(
342
+ "div",
343
+ {
344
+ "data-cid": ":R0:",
345
+ "data-island": Component.displayName || Component.name || "Anonymous"
346
+ },
347
+ React.createElement(Component, params)
348
+ );
349
+ } else {
350
+ element = React.createElement(Component, params);
351
+ }
373
352
 
374
- const mergedMeta = this.#mergeMeta(layoutModules, mod.Meta, params);
353
+ if (layoutModules.length > 0) {
354
+ element = React.createElement("div", { "data-nitron-slot": "page" }, element);
355
+ }
375
356
 
376
- return {
377
- html,
378
- nonce,
379
- meta: mergedMeta,
380
- props: collectedProps
381
- };
357
+ for (let i = layoutModules.length - 1; i >= 0; i--) {
358
+ const LayoutComponent = layoutModules[i].default;
359
+ element = React.createElement(LayoutComponent, { children: element });
360
+ }
361
+
362
+ html = await this.#renderToHtml(element);
363
+ collectedProps = ctx.props;
364
+ } catch (error) {
365
+ const componentName = mod.default?.displayName || mod.default?.name || "Unknown";
366
+ const errorDetails = this.#parseReactError(error);
367
+
368
+ Log.error("Render Error", {
369
+ view: name,
370
+ component: componentName,
371
+ error: error.message,
372
+ cause: errorDetails.cause,
373
+ location: errorDetails.location,
374
+ stack: error.stack
375
+ });
376
+
377
+ error.statusCode = error.statusCode || 500;
378
+ throw error;
379
+ }
380
+
381
+ const mergedMeta = this.#mergeMeta(layoutModules, mod.Meta, params);
382
+
383
+ return {
384
+ html,
385
+ nonce,
386
+ meta: mergedMeta,
387
+ props: collectedProps
388
+ };
389
+ });
382
390
  }
383
391
 
384
392
  static #mergeMeta(layoutModules, viewMeta, params = {}) {
@@ -715,7 +723,7 @@ class View {
715
723
  const devIndicator = this.#isDev ? this.#generateDevIndicator(nonceAttr) : "";
716
724
 
717
725
  return `<!DOCTYPE html>
718
- <html lang="en">
726
+ <html lang="${meta?.lang || "en"}">
719
727
  <head>
720
728
  ${this.#generateHead(meta, css)}
721
729
  </head>
@@ -738,6 +746,14 @@ ${vendorScript}${hmrScript}${hydrateScript}${spaScript}${devIndicator}
738
746
  parts.push(`<meta name="description" content="${escapeHtml(meta.description)}">`);
739
747
  }
740
748
 
749
+ if (meta.keywords) {
750
+ parts.push(`<meta name="keywords" content="${escapeHtml(meta.keywords)}">`);
751
+ }
752
+
753
+ if (meta.favicon) {
754
+ parts.push(`<link rel="icon" href="${escapeHtml(meta.favicon)}">`);
755
+ }
756
+
741
757
  for (const href of css) {
742
758
  parts.push(`<link rel="stylesheet" href="${escapeHtml(href)}">`);
743
759
  }
package/lib/index.d.ts CHANGED
@@ -224,7 +224,7 @@ export class Lang {
224
224
  export class DateTime {
225
225
  static toSQL(timestamp?: number | null): string;
226
226
  static getTime(sqlDateTime?: string | null): number;
227
- static getDate(timestamp?: number | null, format?: string): string;
227
+ static getDate(timestamp?: string | number | null, format?: string): string;
228
228
  static addDays(days: number): string;
229
229
  static addHours(hours: number): string;
230
230
  static subDays(days: number): string;
@@ -503,7 +503,6 @@ declare global {
503
503
  method: string;
504
504
  query: Record<string, any>;
505
505
  params: Record<string, any>;
506
- body: Record<string, any>;
507
506
  headers: Record<string, string>;
508
507
  cookies: Record<string, string>;
509
508
  ip: string;
@@ -515,5 +514,13 @@ declare global {
515
514
  forget(key: string): void;
516
515
  flash(key: string, value: any): void;
517
516
  };
517
+ auth: {
518
+ guard(name?: string): {
519
+ user<T = any>(): Promise<T | null>;
520
+ check(): boolean;
521
+ };
522
+ user<T = any>(): Promise<T | null>;
523
+ check(): boolean;
524
+ };
518
525
  };
519
526
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nitronjs/framework",
3
- "version": "0.2.13",
3
+ "version": "0.2.14",
4
4
  "description": "NitronJS is a modern and extensible Node.js MVC framework built on Fastify. It focuses on clean architecture, modular structure, and developer productivity, offering built-in routing, middleware, configuration management, CLI tooling, and native React integration for scalable full-stack applications.",
5
5
  "bin": {
6
6
  "njs": "./cli/njs.js"
@@ -12,6 +12,7 @@
12
12
  "forceConsistentCasingInFileNames": true,
13
13
  "noEmit": true,
14
14
  "baseUrl": ".",
15
+ "allowJs": true,
15
16
  "paths": {
16
17
  "@/*": ["./*"],
17
18
  "@css/*": ["./resources/css/*"],