@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.mjs CHANGED
@@ -1,7 +1,10 @@
1
+ import { randomUUID } from 'crypto';
1
2
  import fs from 'fs';
2
3
  import ejs from 'ejs';
3
4
  import path from 'path';
4
5
 
6
+ // src/middleware.ts
7
+
5
8
  // src/utils.ts
6
9
  function lazy(callback) {
7
10
  return {
@@ -127,6 +130,10 @@ function safeStringify(obj) {
127
130
  });
128
131
  }
129
132
  var DEFAULT_COMPONENT_NAME_PATTERN = /^[\w-]+(?:\/[\w-]+)*$/;
133
+ var DEFAULT_SECURITY_OPTIONS = {
134
+ validateComponentNames: true,
135
+ sanitizeViewData: true
136
+ };
130
137
  function validateComponentName(component, pattern = DEFAULT_COMPONENT_NAME_PATTERN) {
131
138
  if (!pattern.test(component)) {
132
139
  throw new TypeError(
@@ -161,8 +168,13 @@ function sanitizeViewData(data) {
161
168
  }
162
169
  return sanitize(data);
163
170
  }
171
+ var cryptoModule;
172
+ async function getCrypto() {
173
+ cryptoModule ??= await import('crypto');
174
+ return cryptoModule;
175
+ }
164
176
  async function generateSriHash(content) {
165
- const crypto = await import('crypto');
177
+ const crypto = await getCrypto();
166
178
  const hash = crypto.createHash("sha384").update(content);
167
179
  return `sha384-${hash.digest("base64")}`;
168
180
  }
@@ -226,6 +238,12 @@ var DEFAULT_CIRCUIT_OPTIONS = {
226
238
  cooldownMs: 3e4,
227
239
  probeDelayMs: 1e3
228
240
  };
241
+ var DEFAULT_RETRY_OPTIONS = {
242
+ maxRetries: 2,
243
+ baseDelayMs: 200,
244
+ maxDelayMs: 2e3,
245
+ retryableStatusCodes: [408, 429, 500, 502, 503, 504]
246
+ };
229
247
  var CircuitBreaker = class {
230
248
  constructor(options = DEFAULT_CIRCUIT_OPTIONS) {
231
249
  this.options = options;
@@ -315,16 +333,19 @@ function calculateBackoff(attempt, baseDelayMs, maxDelayMs) {
315
333
  // src/ssr.ts
316
334
  var DEFAULT_SSR_URL = "http://127.0.0.1:13714/render";
317
335
  var DEFAULT_TIMEOUT = 2e3;
336
+ var MAX_CIRCUIT_BREAKERS = 100;
318
337
  var logger = createLogger({ prefix: "express-inertia:ssr" });
319
- var DEFAULT_RETRY_OPTIONS = {
320
- maxRetries: 2,
321
- baseDelayMs: 200,
322
- maxDelayMs: 2e3,
323
- retryableStatusCodes: [408, 429, 500, 502, 503, 504]
324
- };
325
338
  var circuitBreakers = /* @__PURE__ */ new Map();
339
+ function serializeCircuitOptions(options) {
340
+ if (!options) return "";
341
+ const ordered = Object.keys(options).sort().reduce((acc, key) => {
342
+ acc[key] = options[key];
343
+ return acc;
344
+ }, {});
345
+ return JSON.stringify(ordered);
346
+ }
326
347
  function getCircuitBreaker(endpoint, options) {
327
- const key = options?.circuitBreaker ? `${endpoint}:${JSON.stringify(options.circuitBreaker)}` : endpoint;
348
+ const key = options?.circuitBreaker ? `${endpoint}:${serializeCircuitOptions(options.circuitBreaker)}` : endpoint;
328
349
  let breaker = circuitBreakers.get(key);
329
350
  if (!breaker) {
330
351
  const cbOptions = {
@@ -332,6 +353,12 @@ function getCircuitBreaker(endpoint, options) {
332
353
  ...options?.circuitBreaker
333
354
  };
334
355
  breaker = new CircuitBreaker(cbOptions);
356
+ if (circuitBreakers.size >= MAX_CIRCUIT_BREAKERS) {
357
+ const oldest = circuitBreakers.keys().next().value;
358
+ if (oldest !== void 0) {
359
+ circuitBreakers.delete(oldest);
360
+ }
361
+ }
335
362
  circuitBreakers.set(key, breaker);
336
363
  }
337
364
  return breaker;
@@ -401,26 +428,30 @@ async function renderHttpSSR(page, endpoint, timeout) {
401
428
  try {
402
429
  pageJson = JSON.stringify(page);
403
430
  } catch (err) {
404
- throw new Error(`Failed to serialize page for SSR: ${err.message}`);
431
+ clearTimeout(timer);
432
+ throw new Error(`Failed to serialize page for SSR: ${err instanceof Error ? err.message : String(err)}`);
405
433
  }
406
- const response = await fetch(endpoint, {
407
- method: "POST",
408
- headers: {
409
- "Content-Type": "application/json",
410
- Accept: "application/json"
411
- },
412
- body: pageJson,
413
- signal: controller.signal
414
- });
415
- clearTimeout(timer);
416
- if (!response.ok) {
417
- throw new Error(`SSR server responded with status: ${response.status} ${response.statusText}`);
434
+ try {
435
+ const response = await fetch(endpoint, {
436
+ method: "POST",
437
+ headers: {
438
+ "Content-Type": "application/json",
439
+ Accept: "application/json"
440
+ },
441
+ body: pageJson,
442
+ signal: controller.signal
443
+ });
444
+ if (!response.ok) {
445
+ throw new Error(`SSR server responded with status: ${response.status} ${response.statusText}`);
446
+ }
447
+ const data = await response.json();
448
+ return {
449
+ head: Array.isArray(data.head) ? data.head : [],
450
+ body: typeof data.body === "string" ? data.body : ""
451
+ };
452
+ } finally {
453
+ clearTimeout(timer);
418
454
  }
419
- const data = await response.json();
420
- return {
421
- head: Array.isArray(data.head) ? data.head : [],
422
- body: typeof data.body === "string" ? data.body : ""
423
- };
424
455
  }
425
456
  async function renderWithRetry(fn, retryOptions, context) {
426
457
  const options = {
@@ -445,12 +476,15 @@ async function renderWithRetry(fn, retryOptions, context) {
445
476
  attempt: attempt + 1,
446
477
  maxRetries: options.maxRetries,
447
478
  delayMs: Math.round(delay),
448
- error: err.message,
479
+ error: err instanceof Error ? err.message : String(err),
449
480
  component: context
450
481
  });
451
482
  await new Promise((resolve) => setTimeout(resolve, delay));
452
483
  }
453
484
  }
485
+ if (lastError === void 0) {
486
+ throw new Error("SSR renderWithRetry: no attempts executed");
487
+ }
454
488
  throw lastError;
455
489
  }
456
490
 
@@ -473,7 +507,7 @@ function createInertiaResponse(req, res, options, globalSharedProps = {}) {
473
507
  return currentVersion;
474
508
  }
475
509
  const renderFn = async (component, props = {}, viewData = {}) => {
476
- const securityOptions = options.security ?? { validateComponentNames: true };
510
+ const securityOptions = options.security ?? DEFAULT_SECURITY_OPTIONS;
477
511
  if (securityOptions.validateComponentNames !== false) {
478
512
  const pattern = securityOptions.componentNamePattern ?? DEFAULT_COMPONENT_NAME_PATTERN;
479
513
  validateComponentName(component, pattern);
@@ -538,8 +572,9 @@ function createInertiaResponse(req, res, options, globalSharedProps = {}) {
538
572
  } else if (options.viewData) {
539
573
  globalViewData = options.viewData;
540
574
  }
541
- const sanitizedViewData = securityOptions.validateComponentNames !== false ? sanitizeViewData(viewData) : viewData;
542
- const sanitizedGlobalViewData = securityOptions.validateComponentNames !== false ? sanitizeViewData(globalViewData) : globalViewData;
575
+ const shouldSanitizeViewData = securityOptions.sanitizeViewData !== false;
576
+ const sanitizedViewData = shouldSanitizeViewData ? sanitizeViewData(viewData) : viewData;
577
+ const sanitizedGlobalViewData = shouldSanitizeViewData ? sanitizeViewData(globalViewData) : globalViewData;
543
578
  const templateData = {
544
579
  ...res.locals,
545
580
  ...sanitizedGlobalViewData,
@@ -637,17 +672,17 @@ function createInertia(options = {}) {
637
672
  resetData
638
673
  };
639
674
  res.inertia = createInertiaResponse(req, res, options, { ...globalShared });
640
- const securityOptions = options.security ?? { validateComponentNames: true };
675
+ const securityOptions = options.security ?? DEFAULT_SECURITY_OPTIONS;
641
676
  if (securityOptions.validateComponentNames !== false && isInertia) {
642
677
  const pattern = securityOptions.componentNamePattern ?? DEFAULT_COMPONENT_NAME_PATTERN;
643
- const partialComponent2 = req.header("X-Inertia-Partial-Component");
644
- if (partialComponent2 && !pattern.test(partialComponent2)) {
678
+ const requestedPartialComponent = req.header("X-Inertia-Partial-Component");
679
+ if (requestedPartialComponent && !pattern.test(requestedPartialComponent)) {
645
680
  res.setHeader("X-Inertia-Location", req.originalUrl || req.url);
646
681
  res.status(409).end();
647
682
  return;
648
683
  }
649
684
  }
650
- if (isInertia && req.method === "GET" && options.version) {
685
+ if (isInertia && options.version) {
651
686
  let currentVersion = null;
652
687
  if (typeof options.version === "function") {
653
688
  currentVersion = await options.version();
@@ -687,19 +722,22 @@ function requestIdMiddleware() {
687
722
  };
688
723
  }
689
724
  function generateRequestId() {
690
- const timestamp = Date.now().toString(36);
691
- const random = Math.random().toString(36).slice(2, 8);
692
- return `${timestamp}-${random}`;
725
+ return randomUUID();
693
726
  }
694
727
 
695
728
  // src/directives.ts
696
729
  var customDirectives = {};
697
730
  function escapeJsString(str) {
698
- return str.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
731
+ return str.replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
699
732
  }
700
733
  function registerDirective(name, handler) {
701
734
  customDirectives[name] = handler;
702
735
  }
736
+ function clearCustomDirectives() {
737
+ for (const name of Object.keys(customDirectives)) {
738
+ delete customDirectives[name];
739
+ }
740
+ }
703
741
  function compileBladeDirectives(templateSource, localDirectives) {
704
742
  let compiled = templateSource;
705
743
  compiled = compiled.replace(
@@ -793,6 +831,8 @@ function resolveDevServerUrl(config, override) {
793
831
  }
794
832
  function inertiaVitePlugin(options = {}) {
795
833
  const logger2 = createLogger({ prefix: "express-inertia:vite" });
834
+ let cleanupRegistered = false;
835
+ let exitConfig = null;
796
836
  const write = (config, server) => {
797
837
  try {
798
838
  const hotFile = path.resolve(config.root, options.hotFile || "public/hot");
@@ -811,6 +851,11 @@ function inertiaVitePlugin(options = {}) {
811
851
  } catch {
812
852
  }
813
853
  };
854
+ const removeOnExit = () => {
855
+ if (exitConfig) {
856
+ remove(exitConfig);
857
+ }
858
+ };
814
859
  return {
815
860
  name: "express-inertia-hot-file",
816
861
  configResolved(config) {
@@ -822,9 +867,13 @@ function inertiaVitePlugin(options = {}) {
822
867
  },
823
868
  configureServer(server) {
824
869
  write(server.config, server);
870
+ exitConfig = server.config;
825
871
  server.httpServer?.once("listening", () => write(server.config, server));
826
872
  server.httpServer?.on("close", () => remove(server.config));
827
- process.once("exit", () => remove(server.config));
873
+ if (!cleanupRegistered) {
874
+ process.once("exit", removeOnExit);
875
+ cleanupRegistered = true;
876
+ }
828
877
  },
829
878
  buildStart() {
830
879
  }
@@ -847,7 +896,7 @@ var ViteHelper = class {
847
896
  ),
848
897
  base: config?.base || `/${buildDir}/`,
849
898
  manifest: config?.manifest || null,
850
- isDev: config?.isDev ?? false,
899
+ isDev: config?.isDev,
851
900
  devServerUrlOverride: config?.devServerUrlOverride || null
852
901
  };
853
902
  }
@@ -859,8 +908,8 @@ var ViteHelper = class {
859
908
  * NODE_ENV switch is required to toggle between dev and build assets.
860
909
  */
861
910
  isDev() {
862
- if (this.config.isDev === true) {
863
- return true;
911
+ if (this.config.isDev !== void 0) {
912
+ return this.config.isDev;
864
913
  }
865
914
  const candidatePaths = [
866
915
  this.config.hotFile,
@@ -894,6 +943,12 @@ var ViteHelper = class {
894
943
  }
895
944
  return this.config.devServerUrl;
896
945
  }
946
+ /**
947
+ * Get the production asset base path (always ends with a trailing slash).
948
+ */
949
+ getBasePath() {
950
+ return this.config.base.endsWith("/") ? this.config.base : `${this.config.base}/`;
951
+ }
897
952
  /**
898
953
  * Check if manifest file exists.
899
954
  */
@@ -934,7 +989,9 @@ var ViteHelper = class {
934
989
  this.manifestCache = parsed;
935
990
  return parsed;
936
991
  } catch (err) {
937
- throw new Error(`[express-inertia] Failed to parse Vite manifest at ${p}: ${err.message}`);
992
+ throw new Error(
993
+ `[express-inertia] Failed to parse Vite manifest at ${p}: ${err instanceof Error ? err.message : String(err)}`
994
+ );
938
995
  }
939
996
  }
940
997
  }
@@ -1040,26 +1097,49 @@ var defaultViteHelper = new ViteHelper();
1040
1097
 
1041
1098
  // src/engine.ts
1042
1099
  var engineCaches = /* @__PURE__ */ new Set();
1100
+ var engineTimestamps = /* @__PURE__ */ new Set();
1101
+ var TEMPLATE_SOURCE_KEY = "__express_inertia_template_source__";
1043
1102
  function createInertiaEngine(options) {
1044
1103
  const viteHelper = options?.vite ? createViteHelper(options.vite) : createViteHelper();
1045
1104
  const shouldCache = options?.cache ?? !viteHelper.isDev();
1046
1105
  const inertiaVersion = options?.inertiaVersion ?? 2;
1047
1106
  const defaultRootElement = options?.rootElement ?? true;
1048
1107
  const templateCache = /* @__PURE__ */ new Map();
1108
+ const templateTimestamps = /* @__PURE__ */ new Map();
1049
1109
  engineCaches.add(templateCache);
1110
+ engineTimestamps.add(templateTimestamps);
1050
1111
  return function inertiaBladeEngine(filePath, rawOptions, callback) {
1051
1112
  try {
1052
- let compiledTemplate = shouldCache ? templateCache.get(filePath) : void 0;
1113
+ const template = options?.templateSource;
1114
+ const hasTemplateSource = template !== void 0;
1115
+ const cacheKey = hasTemplateSource ? TEMPLATE_SOURCE_KEY : filePath;
1116
+ let compiledTemplate;
1117
+ let templateMtimeMs = -1;
1118
+ if (hasTemplateSource) {
1119
+ compiledTemplate = templateCache.get(cacheKey);
1120
+ } else if (shouldCache) {
1121
+ compiledTemplate = templateCache.get(cacheKey);
1122
+ } else {
1123
+ try {
1124
+ templateMtimeMs = fs.statSync(filePath).mtimeMs;
1125
+ } catch {
1126
+ templateMtimeMs = 0;
1127
+ }
1128
+ if (templateTimestamps.get(cacheKey) === templateMtimeMs) {
1129
+ compiledTemplate = templateCache.get(cacheKey);
1130
+ }
1131
+ }
1053
1132
  if (!compiledTemplate) {
1054
- const templateSource = options?.templateSource || fs.readFileSync(filePath, "utf-8");
1133
+ const templateSource = template !== void 0 ? template : fs.readFileSync(filePath, "utf-8");
1055
1134
  const compiledSource = options?.compileTemplate ? options.compileTemplate(templateSource) : compileBladeDirectives(templateSource, options?.directives);
1056
1135
  compiledTemplate = ejs.compile(compiledSource, {
1057
1136
  filename: filePath,
1058
- cache: shouldCache,
1137
+ cache: false,
1059
1138
  async: false
1060
1139
  });
1061
- if (shouldCache) {
1062
- templateCache.set(filePath, compiledTemplate);
1140
+ templateCache.set(cacheKey, compiledTemplate);
1141
+ if (!hasTemplateSource) {
1142
+ templateTimestamps.set(cacheKey, templateMtimeMs);
1063
1143
  }
1064
1144
  }
1065
1145
  const locals = { ...rawOptions };
@@ -1111,15 +1191,15 @@ ${scriptTag}`;
1111
1191
  };
1112
1192
  locals.csrf = () => {
1113
1193
  const token = locals._csrf || locals.csrfToken || (typeof locals.req?.csrfToken === "function" ? locals.req.csrfToken() : "") || "";
1114
- return token ? `<input type="hidden" name="_token" value="${token}">` : "";
1194
+ return token ? `<input type="hidden" name="_token" value="${escapeHtmlAttr(token)}">` : "";
1115
1195
  };
1116
1196
  locals.json = (value) => {
1117
1197
  return JSON.stringify(value);
1118
1198
  };
1119
1199
  if (!locals.routes) {
1120
- locals.routes = (group) => {
1200
+ locals.routes = (_group) => {
1121
1201
  if (locals.routeConfig || locals.ziggy) {
1122
- const routesData = JSON.stringify(locals.routeConfig || locals.ziggy);
1202
+ const routesData = JSON.stringify(locals.routeConfig || locals.ziggy).replace(/</g, "\\u003c");
1123
1203
  return `<script>const Ziggy = ${routesData};</script>`;
1124
1204
  }
1125
1205
  return "";
@@ -1128,7 +1208,7 @@ ${scriptTag}`;
1128
1208
  const html = compiledTemplate(locals);
1129
1209
  callback(null, html);
1130
1210
  } catch (err) {
1131
- callback(err);
1211
+ callback(err instanceof Error ? err : new Error(String(err)));
1132
1212
  }
1133
1213
  };
1134
1214
  }
@@ -1137,6 +1217,9 @@ function clearEngineCache() {
1137
1217
  for (const cache of engineCaches) {
1138
1218
  cache.clear();
1139
1219
  }
1220
+ for (const timestamps of engineTimestamps) {
1221
+ timestamps.clear();
1222
+ }
1140
1223
  }
1141
1224
 
1142
1225
  // src/prefetch.ts
@@ -1154,15 +1237,19 @@ var PrefetchHelper = class {
1154
1237
  const mode = options.mode || "prefetch";
1155
1238
  const tags = [];
1156
1239
  if (this.viteHelper.isDev()) {
1157
- const devUrl = this.viteHelper["getDevServerUrl"]().replace(/\/$/, "");
1240
+ const devUrl = this.viteHelper.getDevServerUrl().replace(/\/$/, "");
1158
1241
  for (const entry of entrypoints) {
1159
1242
  const cleanEntry = entry.replace(/^\//, "");
1160
- tags.push(`<link rel="${mode}" href="${devUrl}/${cleanEntry}" crossorigin>`);
1243
+ const attrs = [`rel="${mode}"`, `href="${devUrl}/${cleanEntry}"`];
1244
+ if (options.crossorigin) {
1245
+ attrs.push("crossorigin");
1246
+ }
1247
+ tags.push(`<link ${attrs.join(" ")}>`);
1161
1248
  }
1162
1249
  return tags.join("\n");
1163
1250
  }
1164
- const manifest = this.viteHelper["getManifest"]();
1165
- const basePath = this.viteHelper["config"].base.endsWith("/") ? this.viteHelper["config"].base : `${this.viteHelper["config"].base}/`;
1251
+ const manifest = this.viteHelper.getManifest();
1252
+ const basePath = this.viteHelper.getBasePath();
1166
1253
  const rendered = /* @__PURE__ */ new Set();
1167
1254
  for (const entry of entrypoints) {
1168
1255
  const cleanEntry = entry.replace(/^\//, "");
@@ -1213,6 +1300,6 @@ function createPrefetchHelper(viteHelper) {
1213
1300
  // src/index.ts
1214
1301
  var src_default = createInertia;
1215
1302
 
1216
- export { CircuitBreaker, DEFAULT_COMPONENT_NAME_PATTERN, PrefetchHelper, ViteHelper, always, calculateBackoff, clearEngineCache, compileBladeDirectives, createInertia, createInertiaEngine, createLogger, createPrefetchHelper, createViteHelper, src_default as default, defaultViteHelper, defer, escapeHtmlAttr, generateSriHash, getGlobalLogger, createInertia as inertia, inertiaEngine, inertiaVitePlugin, isAlways, isDeferred, isLazy, isMerge, isOptional, lazy, merge, optional, registerDirective, renderSSR, requestIdMiddleware, resetAllCircuitBreakers, resolveProps, safeStringify, sanitizeViewData, serializePage, setGlobalLogger, shouldRetry, validateComponentName };
1303
+ export { CircuitBreaker, DEFAULT_COMPONENT_NAME_PATTERN, DEFAULT_SECURITY_OPTIONS, PrefetchHelper, ViteHelper, always, calculateBackoff, clearCustomDirectives, clearEngineCache, compileBladeDirectives, createInertia, createInertiaEngine, createLogger, createPrefetchHelper, createViteHelper, src_default as default, defaultViteHelper, defer, escapeHtmlAttr, generateSriHash, getGlobalLogger, createInertia as inertia, inertiaEngine, inertiaVitePlugin, isAlways, isDeferred, isLazy, isMerge, isOptional, lazy, merge, optional, registerDirective, renderSSR, requestIdMiddleware, resetAllCircuitBreakers, resolveProps, safeStringify, sanitizeViewData, serializePage, setGlobalLogger, shouldRetry, validateComponentName };
1217
1304
  //# sourceMappingURL=index.mjs.map
1218
1305
  //# sourceMappingURL=index.mjs.map