@arponascension/express-inertia 1.0.0 → 1.5.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/dist/index.js CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  Object.defineProperty(exports, '__esModule', { value: true });
4
4
 
5
+ var crypto = require('crypto');
5
6
  var fs = require('fs');
6
7
  var ejs = require('ejs');
7
8
  var path = require('path');
@@ -12,6 +13,8 @@ var fs__default = /*#__PURE__*/_interopDefault(fs);
12
13
  var ejs__default = /*#__PURE__*/_interopDefault(ejs);
13
14
  var path__default = /*#__PURE__*/_interopDefault(path);
14
15
 
16
+ // src/middleware.ts
17
+
15
18
  // src/utils.ts
16
19
  function lazy(callback) {
17
20
  return {
@@ -137,6 +140,10 @@ function safeStringify(obj) {
137
140
  });
138
141
  }
139
142
  var DEFAULT_COMPONENT_NAME_PATTERN = /^[\w-]+(?:\/[\w-]+)*$/;
143
+ var DEFAULT_SECURITY_OPTIONS = {
144
+ validateComponentNames: true,
145
+ sanitizeViewData: true
146
+ };
140
147
  function validateComponentName(component, pattern = DEFAULT_COMPONENT_NAME_PATTERN) {
141
148
  if (!pattern.test(component)) {
142
149
  throw new TypeError(
@@ -171,8 +178,13 @@ function sanitizeViewData(data) {
171
178
  }
172
179
  return sanitize(data);
173
180
  }
181
+ var cryptoModule;
182
+ async function getCrypto() {
183
+ cryptoModule ??= await import('crypto');
184
+ return cryptoModule;
185
+ }
174
186
  async function generateSriHash(content) {
175
- const crypto = await import('crypto');
187
+ const crypto = await getCrypto();
176
188
  const hash = crypto.createHash("sha384").update(content);
177
189
  return `sha384-${hash.digest("base64")}`;
178
190
  }
@@ -236,6 +248,12 @@ var DEFAULT_CIRCUIT_OPTIONS = {
236
248
  cooldownMs: 3e4,
237
249
  probeDelayMs: 1e3
238
250
  };
251
+ var DEFAULT_RETRY_OPTIONS = {
252
+ maxRetries: 2,
253
+ baseDelayMs: 200,
254
+ maxDelayMs: 2e3,
255
+ retryableStatusCodes: [408, 429, 500, 502, 503, 504]
256
+ };
239
257
  var CircuitBreaker = class {
240
258
  constructor(options = DEFAULT_CIRCUIT_OPTIONS) {
241
259
  this.options = options;
@@ -325,16 +343,19 @@ function calculateBackoff(attempt, baseDelayMs, maxDelayMs) {
325
343
  // src/ssr.ts
326
344
  var DEFAULT_SSR_URL = "http://127.0.0.1:13714/render";
327
345
  var DEFAULT_TIMEOUT = 2e3;
346
+ var MAX_CIRCUIT_BREAKERS = 100;
328
347
  var logger = createLogger({ prefix: "express-inertia:ssr" });
329
- var DEFAULT_RETRY_OPTIONS = {
330
- maxRetries: 2,
331
- baseDelayMs: 200,
332
- maxDelayMs: 2e3,
333
- retryableStatusCodes: [408, 429, 500, 502, 503, 504]
334
- };
335
348
  var circuitBreakers = /* @__PURE__ */ new Map();
349
+ function serializeCircuitOptions(options) {
350
+ if (!options) return "";
351
+ const ordered = Object.keys(options).sort().reduce((acc, key) => {
352
+ acc[key] = options[key];
353
+ return acc;
354
+ }, {});
355
+ return JSON.stringify(ordered);
356
+ }
336
357
  function getCircuitBreaker(endpoint, options) {
337
- const key = options?.circuitBreaker ? `${endpoint}:${JSON.stringify(options.circuitBreaker)}` : endpoint;
358
+ const key = options?.circuitBreaker ? `${endpoint}:${serializeCircuitOptions(options.circuitBreaker)}` : endpoint;
338
359
  let breaker = circuitBreakers.get(key);
339
360
  if (!breaker) {
340
361
  const cbOptions = {
@@ -342,6 +363,12 @@ function getCircuitBreaker(endpoint, options) {
342
363
  ...options?.circuitBreaker
343
364
  };
344
365
  breaker = new CircuitBreaker(cbOptions);
366
+ if (circuitBreakers.size >= MAX_CIRCUIT_BREAKERS) {
367
+ const oldest = circuitBreakers.keys().next().value;
368
+ if (oldest !== void 0) {
369
+ circuitBreakers.delete(oldest);
370
+ }
371
+ }
345
372
  circuitBreakers.set(key, breaker);
346
373
  }
347
374
  return breaker;
@@ -411,26 +438,30 @@ async function renderHttpSSR(page, endpoint, timeout) {
411
438
  try {
412
439
  pageJson = JSON.stringify(page);
413
440
  } catch (err) {
414
- throw new Error(`Failed to serialize page for SSR: ${err.message}`);
441
+ clearTimeout(timer);
442
+ throw new Error(`Failed to serialize page for SSR: ${err instanceof Error ? err.message : String(err)}`);
415
443
  }
416
- const response = await fetch(endpoint, {
417
- method: "POST",
418
- headers: {
419
- "Content-Type": "application/json",
420
- Accept: "application/json"
421
- },
422
- body: pageJson,
423
- signal: controller.signal
424
- });
425
- clearTimeout(timer);
426
- if (!response.ok) {
427
- throw new Error(`SSR server responded with status: ${response.status} ${response.statusText}`);
444
+ try {
445
+ const response = await fetch(endpoint, {
446
+ method: "POST",
447
+ headers: {
448
+ "Content-Type": "application/json",
449
+ Accept: "application/json"
450
+ },
451
+ body: pageJson,
452
+ signal: controller.signal
453
+ });
454
+ if (!response.ok) {
455
+ throw new Error(`SSR server responded with status: ${response.status} ${response.statusText}`);
456
+ }
457
+ const data = await response.json();
458
+ return {
459
+ head: Array.isArray(data.head) ? data.head : [],
460
+ body: typeof data.body === "string" ? data.body : ""
461
+ };
462
+ } finally {
463
+ clearTimeout(timer);
428
464
  }
429
- const data = await response.json();
430
- return {
431
- head: Array.isArray(data.head) ? data.head : [],
432
- body: typeof data.body === "string" ? data.body : ""
433
- };
434
465
  }
435
466
  async function renderWithRetry(fn, retryOptions, context) {
436
467
  const options = {
@@ -455,12 +486,15 @@ async function renderWithRetry(fn, retryOptions, context) {
455
486
  attempt: attempt + 1,
456
487
  maxRetries: options.maxRetries,
457
488
  delayMs: Math.round(delay),
458
- error: err.message,
489
+ error: err instanceof Error ? err.message : String(err),
459
490
  component: context
460
491
  });
461
492
  await new Promise((resolve) => setTimeout(resolve, delay));
462
493
  }
463
494
  }
495
+ if (lastError === void 0) {
496
+ throw new Error("SSR renderWithRetry: no attempts executed");
497
+ }
464
498
  throw lastError;
465
499
  }
466
500
 
@@ -483,7 +517,7 @@ function createInertiaResponse(req, res, options, globalSharedProps = {}) {
483
517
  return currentVersion;
484
518
  }
485
519
  const renderFn = async (component, props = {}, viewData = {}) => {
486
- const securityOptions = options.security ?? { validateComponentNames: true };
520
+ const securityOptions = options.security ?? DEFAULT_SECURITY_OPTIONS;
487
521
  if (securityOptions.validateComponentNames !== false) {
488
522
  const pattern = securityOptions.componentNamePattern ?? DEFAULT_COMPONENT_NAME_PATTERN;
489
523
  validateComponentName(component, pattern);
@@ -548,8 +582,9 @@ function createInertiaResponse(req, res, options, globalSharedProps = {}) {
548
582
  } else if (options.viewData) {
549
583
  globalViewData = options.viewData;
550
584
  }
551
- const sanitizedViewData = securityOptions.validateComponentNames !== false ? sanitizeViewData(viewData) : viewData;
552
- const sanitizedGlobalViewData = securityOptions.validateComponentNames !== false ? sanitizeViewData(globalViewData) : globalViewData;
585
+ const shouldSanitizeViewData = securityOptions.sanitizeViewData !== false;
586
+ const sanitizedViewData = shouldSanitizeViewData ? sanitizeViewData(viewData) : viewData;
587
+ const sanitizedGlobalViewData = shouldSanitizeViewData ? sanitizeViewData(globalViewData) : globalViewData;
553
588
  const templateData = {
554
589
  ...res.locals,
555
590
  ...sanitizedGlobalViewData,
@@ -647,17 +682,17 @@ function createInertia(options = {}) {
647
682
  resetData
648
683
  };
649
684
  res.inertia = createInertiaResponse(req, res, options, { ...globalShared });
650
- const securityOptions = options.security ?? { validateComponentNames: true };
685
+ const securityOptions = options.security ?? DEFAULT_SECURITY_OPTIONS;
651
686
  if (securityOptions.validateComponentNames !== false && isInertia) {
652
687
  const pattern = securityOptions.componentNamePattern ?? DEFAULT_COMPONENT_NAME_PATTERN;
653
- const partialComponent2 = req.header("X-Inertia-Partial-Component");
654
- if (partialComponent2 && !pattern.test(partialComponent2)) {
688
+ const requestedPartialComponent = req.header("X-Inertia-Partial-Component");
689
+ if (requestedPartialComponent && !pattern.test(requestedPartialComponent)) {
655
690
  res.setHeader("X-Inertia-Location", req.originalUrl || req.url);
656
691
  res.status(409).end();
657
692
  return;
658
693
  }
659
694
  }
660
- if (isInertia && req.method === "GET" && options.version) {
695
+ if (isInertia && options.version) {
661
696
  let currentVersion = null;
662
697
  if (typeof options.version === "function") {
663
698
  currentVersion = await options.version();
@@ -697,19 +732,22 @@ function requestIdMiddleware() {
697
732
  };
698
733
  }
699
734
  function generateRequestId() {
700
- const timestamp = Date.now().toString(36);
701
- const random = Math.random().toString(36).slice(2, 8);
702
- return `${timestamp}-${random}`;
735
+ return crypto.randomUUID();
703
736
  }
704
737
 
705
738
  // src/directives.ts
706
739
  var customDirectives = {};
707
740
  function escapeJsString(str) {
708
- return str.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
741
+ return str.replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
709
742
  }
710
743
  function registerDirective(name, handler) {
711
744
  customDirectives[name] = handler;
712
745
  }
746
+ function clearCustomDirectives() {
747
+ for (const name of Object.keys(customDirectives)) {
748
+ delete customDirectives[name];
749
+ }
750
+ }
713
751
  function compileBladeDirectives(templateSource, localDirectives) {
714
752
  let compiled = templateSource;
715
753
  compiled = compiled.replace(
@@ -803,6 +841,8 @@ function resolveDevServerUrl(config, override) {
803
841
  }
804
842
  function inertiaVitePlugin(options = {}) {
805
843
  const logger2 = createLogger({ prefix: "express-inertia:vite" });
844
+ let cleanupRegistered = false;
845
+ let exitConfig = null;
806
846
  const write = (config, server) => {
807
847
  try {
808
848
  const hotFile = path__default.default.resolve(config.root, options.hotFile || "public/hot");
@@ -821,6 +861,11 @@ function inertiaVitePlugin(options = {}) {
821
861
  } catch {
822
862
  }
823
863
  };
864
+ const removeOnExit = () => {
865
+ if (exitConfig) {
866
+ remove(exitConfig);
867
+ }
868
+ };
824
869
  return {
825
870
  name: "express-inertia-hot-file",
826
871
  configResolved(config) {
@@ -832,9 +877,13 @@ function inertiaVitePlugin(options = {}) {
832
877
  },
833
878
  configureServer(server) {
834
879
  write(server.config, server);
880
+ exitConfig = server.config;
835
881
  server.httpServer?.once("listening", () => write(server.config, server));
836
882
  server.httpServer?.on("close", () => remove(server.config));
837
- process.once("exit", () => remove(server.config));
883
+ if (!cleanupRegistered) {
884
+ process.once("exit", removeOnExit);
885
+ cleanupRegistered = true;
886
+ }
838
887
  },
839
888
  buildStart() {
840
889
  }
@@ -857,7 +906,7 @@ var ViteHelper = class {
857
906
  ),
858
907
  base: config?.base || `/${buildDir}/`,
859
908
  manifest: config?.manifest || null,
860
- isDev: config?.isDev ?? false,
909
+ isDev: config?.isDev,
861
910
  devServerUrlOverride: config?.devServerUrlOverride || null
862
911
  };
863
912
  }
@@ -869,8 +918,8 @@ var ViteHelper = class {
869
918
  * NODE_ENV switch is required to toggle between dev and build assets.
870
919
  */
871
920
  isDev() {
872
- if (this.config.isDev === true) {
873
- return true;
921
+ if (this.config.isDev !== void 0) {
922
+ return this.config.isDev;
874
923
  }
875
924
  const candidatePaths = [
876
925
  this.config.hotFile,
@@ -904,6 +953,12 @@ var ViteHelper = class {
904
953
  }
905
954
  return this.config.devServerUrl;
906
955
  }
956
+ /**
957
+ * Get the production asset base path (always ends with a trailing slash).
958
+ */
959
+ getBasePath() {
960
+ return this.config.base.endsWith("/") ? this.config.base : `${this.config.base}/`;
961
+ }
907
962
  /**
908
963
  * Check if manifest file exists.
909
964
  */
@@ -944,7 +999,9 @@ var ViteHelper = class {
944
999
  this.manifestCache = parsed;
945
1000
  return parsed;
946
1001
  } catch (err) {
947
- throw new Error(`[express-inertia] Failed to parse Vite manifest at ${p}: ${err.message}`);
1002
+ throw new Error(
1003
+ `[express-inertia] Failed to parse Vite manifest at ${p}: ${err instanceof Error ? err.message : String(err)}`
1004
+ );
948
1005
  }
949
1006
  }
950
1007
  }
@@ -1050,26 +1107,49 @@ var defaultViteHelper = new ViteHelper();
1050
1107
 
1051
1108
  // src/engine.ts
1052
1109
  var engineCaches = /* @__PURE__ */ new Set();
1110
+ var engineTimestamps = /* @__PURE__ */ new Set();
1111
+ var TEMPLATE_SOURCE_KEY = "__express_inertia_template_source__";
1053
1112
  function createInertiaEngine(options) {
1054
1113
  const viteHelper = options?.vite ? createViteHelper(options.vite) : createViteHelper();
1055
1114
  const shouldCache = options?.cache ?? !viteHelper.isDev();
1056
1115
  const inertiaVersion = options?.inertiaVersion ?? 2;
1057
1116
  const defaultRootElement = options?.rootElement ?? true;
1058
1117
  const templateCache = /* @__PURE__ */ new Map();
1118
+ const templateTimestamps = /* @__PURE__ */ new Map();
1059
1119
  engineCaches.add(templateCache);
1120
+ engineTimestamps.add(templateTimestamps);
1060
1121
  return function inertiaBladeEngine(filePath, rawOptions, callback) {
1061
1122
  try {
1062
- let compiledTemplate = shouldCache ? templateCache.get(filePath) : void 0;
1123
+ const template = options?.templateSource;
1124
+ const hasTemplateSource = template !== void 0;
1125
+ const cacheKey = hasTemplateSource ? TEMPLATE_SOURCE_KEY : filePath;
1126
+ let compiledTemplate;
1127
+ let templateMtimeMs = -1;
1128
+ if (hasTemplateSource) {
1129
+ compiledTemplate = templateCache.get(cacheKey);
1130
+ } else if (shouldCache) {
1131
+ compiledTemplate = templateCache.get(cacheKey);
1132
+ } else {
1133
+ try {
1134
+ templateMtimeMs = fs__default.default.statSync(filePath).mtimeMs;
1135
+ } catch {
1136
+ templateMtimeMs = 0;
1137
+ }
1138
+ if (templateTimestamps.get(cacheKey) === templateMtimeMs) {
1139
+ compiledTemplate = templateCache.get(cacheKey);
1140
+ }
1141
+ }
1063
1142
  if (!compiledTemplate) {
1064
- const templateSource = options?.templateSource || fs__default.default.readFileSync(filePath, "utf-8");
1143
+ const templateSource = template !== void 0 ? template : fs__default.default.readFileSync(filePath, "utf-8");
1065
1144
  const compiledSource = options?.compileTemplate ? options.compileTemplate(templateSource) : compileBladeDirectives(templateSource, options?.directives);
1066
1145
  compiledTemplate = ejs__default.default.compile(compiledSource, {
1067
1146
  filename: filePath,
1068
- cache: shouldCache,
1147
+ cache: false,
1069
1148
  async: false
1070
1149
  });
1071
- if (shouldCache) {
1072
- templateCache.set(filePath, compiledTemplate);
1150
+ templateCache.set(cacheKey, compiledTemplate);
1151
+ if (!hasTemplateSource) {
1152
+ templateTimestamps.set(cacheKey, templateMtimeMs);
1073
1153
  }
1074
1154
  }
1075
1155
  const locals = { ...rawOptions };
@@ -1121,15 +1201,15 @@ ${scriptTag}`;
1121
1201
  };
1122
1202
  locals.csrf = () => {
1123
1203
  const token = locals._csrf || locals.csrfToken || (typeof locals.req?.csrfToken === "function" ? locals.req.csrfToken() : "") || "";
1124
- return token ? `<input type="hidden" name="_token" value="${token}">` : "";
1204
+ return token ? `<input type="hidden" name="_token" value="${escapeHtmlAttr(token)}">` : "";
1125
1205
  };
1126
1206
  locals.json = (value) => {
1127
1207
  return JSON.stringify(value);
1128
1208
  };
1129
1209
  if (!locals.routes) {
1130
- locals.routes = (group) => {
1210
+ locals.routes = (_group) => {
1131
1211
  if (locals.routeConfig || locals.ziggy) {
1132
- const routesData = JSON.stringify(locals.routeConfig || locals.ziggy);
1212
+ const routesData = JSON.stringify(locals.routeConfig || locals.ziggy).replace(/</g, "\\u003c");
1133
1213
  return `<script>const Ziggy = ${routesData};</script>`;
1134
1214
  }
1135
1215
  return "";
@@ -1138,7 +1218,7 @@ ${scriptTag}`;
1138
1218
  const html = compiledTemplate(locals);
1139
1219
  callback(null, html);
1140
1220
  } catch (err) {
1141
- callback(err);
1221
+ callback(err instanceof Error ? err : new Error(String(err)));
1142
1222
  }
1143
1223
  };
1144
1224
  }
@@ -1147,6 +1227,9 @@ function clearEngineCache() {
1147
1227
  for (const cache of engineCaches) {
1148
1228
  cache.clear();
1149
1229
  }
1230
+ for (const timestamps of engineTimestamps) {
1231
+ timestamps.clear();
1232
+ }
1150
1233
  }
1151
1234
 
1152
1235
  // src/prefetch.ts
@@ -1164,15 +1247,19 @@ var PrefetchHelper = class {
1164
1247
  const mode = options.mode || "prefetch";
1165
1248
  const tags = [];
1166
1249
  if (this.viteHelper.isDev()) {
1167
- const devUrl = this.viteHelper["getDevServerUrl"]().replace(/\/$/, "");
1250
+ const devUrl = this.viteHelper.getDevServerUrl().replace(/\/$/, "");
1168
1251
  for (const entry of entrypoints) {
1169
1252
  const cleanEntry = entry.replace(/^\//, "");
1170
- tags.push(`<link rel="${mode}" href="${devUrl}/${cleanEntry}" crossorigin>`);
1253
+ const attrs = [`rel="${mode}"`, `href="${devUrl}/${cleanEntry}"`];
1254
+ if (options.crossorigin) {
1255
+ attrs.push("crossorigin");
1256
+ }
1257
+ tags.push(`<link ${attrs.join(" ")}>`);
1171
1258
  }
1172
1259
  return tags.join("\n");
1173
1260
  }
1174
- const manifest = this.viteHelper["getManifest"]();
1175
- const basePath = this.viteHelper["config"].base.endsWith("/") ? this.viteHelper["config"].base : `${this.viteHelper["config"].base}/`;
1261
+ const manifest = this.viteHelper.getManifest();
1262
+ const basePath = this.viteHelper.getBasePath();
1176
1263
  const rendered = /* @__PURE__ */ new Set();
1177
1264
  for (const entry of entrypoints) {
1178
1265
  const cleanEntry = entry.replace(/^\//, "");
@@ -1225,10 +1312,12 @@ var src_default = createInertia;
1225
1312
 
1226
1313
  exports.CircuitBreaker = CircuitBreaker;
1227
1314
  exports.DEFAULT_COMPONENT_NAME_PATTERN = DEFAULT_COMPONENT_NAME_PATTERN;
1315
+ exports.DEFAULT_SECURITY_OPTIONS = DEFAULT_SECURITY_OPTIONS;
1228
1316
  exports.PrefetchHelper = PrefetchHelper;
1229
1317
  exports.ViteHelper = ViteHelper;
1230
1318
  exports.always = always;
1231
1319
  exports.calculateBackoff = calculateBackoff;
1320
+ exports.clearCustomDirectives = clearCustomDirectives;
1232
1321
  exports.clearEngineCache = clearEngineCache;
1233
1322
  exports.compileBladeDirectives = compileBladeDirectives;
1234
1323
  exports.createInertia = createInertia;