@lolyjs/core 0.3.0-alpha.4 → 0.3.0-alpha.6

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/cli.cjs CHANGED
@@ -9953,7 +9953,7 @@ var require_built3 = __commonJS({
9953
9953
  });
9954
9954
 
9955
9955
  // modules/cli/index.ts
9956
- var import_path37 = __toESM(require("path"));
9956
+ var import_path40 = __toESM(require("path"));
9957
9957
  var import_process = __toESM(require("process"));
9958
9958
 
9959
9959
  // modules/build/index.ts
@@ -11742,6 +11742,7 @@ async function processRewrites(urlPath, compiledRewrites, req) {
11742
11742
  const normalizedPath = urlPath.replace(/\/$/, "") || "/";
11743
11743
  if (normalizedPath.startsWith("/static/") || // Static assets (client.js, client.css, etc.)
11744
11744
  normalizedPath.startsWith("/__fw/") || // Framework internal routes (hot reload, etc.)
11745
+ normalizedPath.startsWith("/_loly/") || // Framework internal routes (image optimization, etc.)
11745
11746
  normalizedPath === "/favicon.ico" || // Favicon
11746
11747
  normalizedPath.startsWith("/wss/")) {
11747
11748
  if (process.env.NODE_ENV === "development") {
@@ -12946,7 +12947,7 @@ function createDocumentTree(options) {
12946
12947
  }),
12947
12948
  ...extraMetaTags,
12948
12949
  ...linkTags,
12949
- ...entrypointFiles.length > 0 ? entrypointFiles.slice(0, -1).map(
12950
+ ...entrypointFiles.length > 0 ? entrypointFiles.map(
12950
12951
  (file) => import_react.default.createElement("link", {
12951
12952
  key: `preload-${file}`,
12952
12953
  rel: "preload",
@@ -12967,6 +12968,13 @@ function createDocumentTree(options) {
12967
12968
  href: faviconPath,
12968
12969
  type: faviconType || (faviconPath.endsWith(".ico") ? "image/x-icon" : "image/png")
12969
12970
  }),
12971
+ // Preload CSS para evitar bloqueo de renderizado
12972
+ import_react.default.createElement("link", {
12973
+ key: "preload-css",
12974
+ rel: "preload",
12975
+ href: clientCssPath,
12976
+ as: "style"
12977
+ }),
12970
12978
  import_react.default.createElement("link", {
12971
12979
  rel: "stylesheet",
12972
12980
  href: clientCssPath
@@ -13168,12 +13176,12 @@ var DEFAULT_IGNORED_PATHS = [
13168
13176
  /^\/sockjs-node/
13169
13177
  // Hot reload websocket
13170
13178
  ];
13171
- function shouldIgnorePath(path34, ignoredPaths) {
13179
+ function shouldIgnorePath(path37, ignoredPaths) {
13172
13180
  return ignoredPaths.some((pattern) => {
13173
13181
  if (typeof pattern === "string") {
13174
- return path34 === pattern || path34.startsWith(pattern);
13182
+ return path37 === pattern || path37.startsWith(pattern);
13175
13183
  }
13176
- return pattern.test(path34);
13184
+ return pattern.test(path37);
13177
13185
  });
13178
13186
  }
13179
13187
  function requestLoggerMiddleware(options = {}) {
@@ -15136,7 +15144,20 @@ var DEFAULT_CONFIG2 = {
15136
15144
  ssr: true,
15137
15145
  ssg: true
15138
15146
  },
15139
- plugins: []
15147
+ plugins: [],
15148
+ images: {
15149
+ remotePatterns: [],
15150
+ domains: [],
15151
+ deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
15152
+ imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
15153
+ formats: ["image/webp", "image/avif"],
15154
+ quality: 75,
15155
+ minimumCacheTTL: 60,
15156
+ dangerouslyAllowSVG: false,
15157
+ contentSecurityPolicy: "default-src 'self'; script-src 'none'; sandbox;",
15158
+ maxWidth: 3840,
15159
+ maxHeight: 3840
15160
+ }
15140
15161
  };
15141
15162
  function deepMerge(target, source) {
15142
15163
  const result = { ...target };
@@ -15253,6 +15274,53 @@ function validateConfig(config, projectRoot) {
15253
15274
  if (typeof config.rendering.ssg !== "boolean") {
15254
15275
  errors.push("config.rendering.ssg must be a boolean");
15255
15276
  }
15277
+ if (config.images) {
15278
+ if (config.images.quality !== void 0) {
15279
+ if (typeof config.images.quality !== "number" || config.images.quality < 1 || config.images.quality > 100) {
15280
+ errors.push("config.images.quality must be a number between 1 and 100");
15281
+ }
15282
+ }
15283
+ if (config.images.minimumCacheTTL !== void 0) {
15284
+ if (typeof config.images.minimumCacheTTL !== "number" || config.images.minimumCacheTTL < 0) {
15285
+ errors.push("config.images.minimumCacheTTL must be a non-negative number");
15286
+ }
15287
+ }
15288
+ if (config.images.deviceSizes) {
15289
+ if (!Array.isArray(config.images.deviceSizes) || config.images.deviceSizes.some((s) => typeof s !== "number" || s <= 0)) {
15290
+ errors.push("config.images.deviceSizes must be an array of positive numbers");
15291
+ }
15292
+ }
15293
+ if (config.images.imageSizes) {
15294
+ if (!Array.isArray(config.images.imageSizes) || config.images.imageSizes.some((s) => typeof s !== "number" || s <= 0)) {
15295
+ errors.push("config.images.imageSizes must be an array of positive numbers");
15296
+ }
15297
+ }
15298
+ if (config.images.formats) {
15299
+ const validFormats = ["image/webp", "image/avif"];
15300
+ if (!Array.isArray(config.images.formats) || config.images.formats.some((f) => !validFormats.includes(f))) {
15301
+ errors.push(`config.images.formats must be an array containing only: ${validFormats.join(", ")}`);
15302
+ }
15303
+ }
15304
+ if (config.images.remotePatterns) {
15305
+ if (!Array.isArray(config.images.remotePatterns)) {
15306
+ errors.push("config.images.remotePatterns must be an array");
15307
+ } else {
15308
+ config.images.remotePatterns.forEach((pattern, idx) => {
15309
+ if (!pattern.hostname || typeof pattern.hostname !== "string") {
15310
+ errors.push(`config.images.remotePatterns[${idx}].hostname must be a non-empty string`);
15311
+ }
15312
+ if (pattern.protocol && !["http", "https"].includes(pattern.protocol)) {
15313
+ errors.push(`config.images.remotePatterns[${idx}].protocol must be 'http' or 'https'`);
15314
+ }
15315
+ });
15316
+ }
15317
+ }
15318
+ if (config.images.domains) {
15319
+ if (!Array.isArray(config.images.domains) || config.images.domains.some((d) => typeof d !== "string")) {
15320
+ errors.push("config.images.domains must be an array of strings");
15321
+ }
15322
+ }
15323
+ }
15256
15324
  if (errors.length > 0) {
15257
15325
  const errorMessage = [
15258
15326
  "\u274C Configuration validation failed:",
@@ -15386,8 +15454,8 @@ async function buildApp(options = {}) {
15386
15454
  }
15387
15455
 
15388
15456
  // src/server.ts
15389
- var import_fs26 = __toESM(require("fs"));
15390
- var import_path36 = __toESM(require("path"));
15457
+ var import_fs28 = __toESM(require("fs"));
15458
+ var import_path39 = __toESM(require("path"));
15391
15459
 
15392
15460
  // modules/server/setup.ts
15393
15461
  var import_express = __toESM(require("express"));
@@ -15470,7 +15538,7 @@ var ReaddirpStream = class extends import_node_stream.Readable {
15470
15538
  this._directoryFilter = normalizeFilter(opts.directoryFilter);
15471
15539
  const statMethod = opts.lstat ? import_promises.lstat : import_promises.stat;
15472
15540
  if (wantBigintFsStats) {
15473
- this._stat = (path34) => statMethod(path34, { bigint: true });
15541
+ this._stat = (path37) => statMethod(path37, { bigint: true });
15474
15542
  } else {
15475
15543
  this._stat = statMethod;
15476
15544
  }
@@ -15495,8 +15563,8 @@ var ReaddirpStream = class extends import_node_stream.Readable {
15495
15563
  const par = this.parent;
15496
15564
  const fil = par && par.files;
15497
15565
  if (fil && fil.length > 0) {
15498
- const { path: path34, depth } = par;
15499
- const slice = fil.splice(0, batch).map((dirent) => this._formatEntry(dirent, path34));
15566
+ const { path: path37, depth } = par;
15567
+ const slice = fil.splice(0, batch).map((dirent) => this._formatEntry(dirent, path37));
15500
15568
  const awaited = await Promise.all(slice);
15501
15569
  for (const entry of awaited) {
15502
15570
  if (!entry)
@@ -15536,20 +15604,20 @@ var ReaddirpStream = class extends import_node_stream.Readable {
15536
15604
  this.reading = false;
15537
15605
  }
15538
15606
  }
15539
- async _exploreDir(path34, depth) {
15607
+ async _exploreDir(path37, depth) {
15540
15608
  let files;
15541
15609
  try {
15542
- files = await (0, import_promises.readdir)(path34, this._rdOptions);
15610
+ files = await (0, import_promises.readdir)(path37, this._rdOptions);
15543
15611
  } catch (error) {
15544
15612
  this._onError(error);
15545
15613
  }
15546
- return { files, depth, path: path34 };
15614
+ return { files, depth, path: path37 };
15547
15615
  }
15548
- async _formatEntry(dirent, path34) {
15616
+ async _formatEntry(dirent, path37) {
15549
15617
  let entry;
15550
15618
  const basename3 = this._isDirent ? dirent.name : dirent;
15551
15619
  try {
15552
- const fullPath = (0, import_node_path.resolve)((0, import_node_path.join)(path34, basename3));
15620
+ const fullPath = (0, import_node_path.resolve)((0, import_node_path.join)(path37, basename3));
15553
15621
  entry = { path: (0, import_node_path.relative)(this._root, fullPath), fullPath, basename: basename3 };
15554
15622
  entry[this._statsProp] = this._isDirent ? dirent : await this._stat(fullPath);
15555
15623
  } catch (err) {
@@ -15949,16 +16017,16 @@ var delFromSet = (main, prop, item) => {
15949
16017
  };
15950
16018
  var isEmptySet = (val) => val instanceof Set ? val.size === 0 : !val;
15951
16019
  var FsWatchInstances = /* @__PURE__ */ new Map();
15952
- function createFsWatchInstance(path34, options, listener, errHandler, emitRaw) {
16020
+ function createFsWatchInstance(path37, options, listener, errHandler, emitRaw) {
15953
16021
  const handleEvent = (rawEvent, evPath) => {
15954
- listener(path34);
15955
- emitRaw(rawEvent, evPath, { watchedPath: path34 });
15956
- if (evPath && path34 !== evPath) {
15957
- fsWatchBroadcast(sysPath.resolve(path34, evPath), KEY_LISTENERS, sysPath.join(path34, evPath));
16022
+ listener(path37);
16023
+ emitRaw(rawEvent, evPath, { watchedPath: path37 });
16024
+ if (evPath && path37 !== evPath) {
16025
+ fsWatchBroadcast(sysPath.resolve(path37, evPath), KEY_LISTENERS, sysPath.join(path37, evPath));
15958
16026
  }
15959
16027
  };
15960
16028
  try {
15961
- return (0, import_fs23.watch)(path34, {
16029
+ return (0, import_fs23.watch)(path37, {
15962
16030
  persistent: options.persistent
15963
16031
  }, handleEvent);
15964
16032
  } catch (error) {
@@ -15974,12 +16042,12 @@ var fsWatchBroadcast = (fullPath, listenerType, val1, val2, val3) => {
15974
16042
  listener(val1, val2, val3);
15975
16043
  });
15976
16044
  };
15977
- var setFsWatchListener = (path34, fullPath, options, handlers) => {
16045
+ var setFsWatchListener = (path37, fullPath, options, handlers) => {
15978
16046
  const { listener, errHandler, rawEmitter } = handlers;
15979
16047
  let cont = FsWatchInstances.get(fullPath);
15980
16048
  let watcher;
15981
16049
  if (!options.persistent) {
15982
- watcher = createFsWatchInstance(path34, options, listener, errHandler, rawEmitter);
16050
+ watcher = createFsWatchInstance(path37, options, listener, errHandler, rawEmitter);
15983
16051
  if (!watcher)
15984
16052
  return;
15985
16053
  return watcher.close.bind(watcher);
@@ -15990,7 +16058,7 @@ var setFsWatchListener = (path34, fullPath, options, handlers) => {
15990
16058
  addAndConvert(cont, KEY_RAW, rawEmitter);
15991
16059
  } else {
15992
16060
  watcher = createFsWatchInstance(
15993
- path34,
16061
+ path37,
15994
16062
  options,
15995
16063
  fsWatchBroadcast.bind(null, fullPath, KEY_LISTENERS),
15996
16064
  errHandler,
@@ -16005,7 +16073,7 @@ var setFsWatchListener = (path34, fullPath, options, handlers) => {
16005
16073
  cont.watcherUnusable = true;
16006
16074
  if (isWindows && error.code === "EPERM") {
16007
16075
  try {
16008
- const fd = await (0, import_promises2.open)(path34, "r");
16076
+ const fd = await (0, import_promises2.open)(path37, "r");
16009
16077
  await fd.close();
16010
16078
  broadcastErr(error);
16011
16079
  } catch (err) {
@@ -16036,7 +16104,7 @@ var setFsWatchListener = (path34, fullPath, options, handlers) => {
16036
16104
  };
16037
16105
  };
16038
16106
  var FsWatchFileInstances = /* @__PURE__ */ new Map();
16039
- var setFsWatchFileListener = (path34, fullPath, options, handlers) => {
16107
+ var setFsWatchFileListener = (path37, fullPath, options, handlers) => {
16040
16108
  const { listener, rawEmitter } = handlers;
16041
16109
  let cont = FsWatchFileInstances.get(fullPath);
16042
16110
  const copts = cont && cont.options;
@@ -16058,7 +16126,7 @@ var setFsWatchFileListener = (path34, fullPath, options, handlers) => {
16058
16126
  });
16059
16127
  const currmtime = curr.mtimeMs;
16060
16128
  if (curr.size !== prev.size || currmtime > prev.mtimeMs || currmtime === 0) {
16061
- foreach(cont.listeners, (listener2) => listener2(path34, curr));
16129
+ foreach(cont.listeners, (listener2) => listener2(path37, curr));
16062
16130
  }
16063
16131
  })
16064
16132
  };
@@ -16086,13 +16154,13 @@ var NodeFsHandler = class {
16086
16154
  * @param listener on fs change
16087
16155
  * @returns closer for the watcher instance
16088
16156
  */
16089
- _watchWithNodeFs(path34, listener) {
16157
+ _watchWithNodeFs(path37, listener) {
16090
16158
  const opts = this.fsw.options;
16091
- const directory = sysPath.dirname(path34);
16092
- const basename3 = sysPath.basename(path34);
16159
+ const directory = sysPath.dirname(path37);
16160
+ const basename3 = sysPath.basename(path37);
16093
16161
  const parent = this.fsw._getWatchedDir(directory);
16094
16162
  parent.add(basename3);
16095
- const absolutePath = sysPath.resolve(path34);
16163
+ const absolutePath = sysPath.resolve(path37);
16096
16164
  const options = {
16097
16165
  persistent: opts.persistent
16098
16166
  };
@@ -16102,12 +16170,12 @@ var NodeFsHandler = class {
16102
16170
  if (opts.usePolling) {
16103
16171
  const enableBin = opts.interval !== opts.binaryInterval;
16104
16172
  options.interval = enableBin && isBinaryPath(basename3) ? opts.binaryInterval : opts.interval;
16105
- closer = setFsWatchFileListener(path34, absolutePath, options, {
16173
+ closer = setFsWatchFileListener(path37, absolutePath, options, {
16106
16174
  listener,
16107
16175
  rawEmitter: this.fsw._emitRaw
16108
16176
  });
16109
16177
  } else {
16110
- closer = setFsWatchListener(path34, absolutePath, options, {
16178
+ closer = setFsWatchListener(path37, absolutePath, options, {
16111
16179
  listener,
16112
16180
  errHandler: this._boundHandleError,
16113
16181
  rawEmitter: this.fsw._emitRaw
@@ -16129,7 +16197,7 @@ var NodeFsHandler = class {
16129
16197
  let prevStats = stats;
16130
16198
  if (parent.has(basename3))
16131
16199
  return;
16132
- const listener = async (path34, newStats) => {
16200
+ const listener = async (path37, newStats) => {
16133
16201
  if (!this.fsw._throttle(THROTTLE_MODE_WATCH, file, 5))
16134
16202
  return;
16135
16203
  if (!newStats || newStats.mtimeMs === 0) {
@@ -16143,11 +16211,11 @@ var NodeFsHandler = class {
16143
16211
  this.fsw._emit(EV.CHANGE, file, newStats2);
16144
16212
  }
16145
16213
  if ((isMacos || isLinux || isFreeBSD) && prevStats.ino !== newStats2.ino) {
16146
- this.fsw._closeFile(path34);
16214
+ this.fsw._closeFile(path37);
16147
16215
  prevStats = newStats2;
16148
16216
  const closer2 = this._watchWithNodeFs(file, listener);
16149
16217
  if (closer2)
16150
- this.fsw._addPathCloser(path34, closer2);
16218
+ this.fsw._addPathCloser(path37, closer2);
16151
16219
  } else {
16152
16220
  prevStats = newStats2;
16153
16221
  }
@@ -16179,7 +16247,7 @@ var NodeFsHandler = class {
16179
16247
  * @param item basename of this item
16180
16248
  * @returns true if no more processing is needed for this entry.
16181
16249
  */
16182
- async _handleSymlink(entry, directory, path34, item) {
16250
+ async _handleSymlink(entry, directory, path37, item) {
16183
16251
  if (this.fsw.closed) {
16184
16252
  return;
16185
16253
  }
@@ -16189,7 +16257,7 @@ var NodeFsHandler = class {
16189
16257
  this.fsw._incrReadyCount();
16190
16258
  let linkPath;
16191
16259
  try {
16192
- linkPath = await (0, import_promises2.realpath)(path34);
16260
+ linkPath = await (0, import_promises2.realpath)(path37);
16193
16261
  } catch (e) {
16194
16262
  this.fsw._emitReady();
16195
16263
  return true;
@@ -16199,12 +16267,12 @@ var NodeFsHandler = class {
16199
16267
  if (dir.has(item)) {
16200
16268
  if (this.fsw._symlinkPaths.get(full) !== linkPath) {
16201
16269
  this.fsw._symlinkPaths.set(full, linkPath);
16202
- this.fsw._emit(EV.CHANGE, path34, entry.stats);
16270
+ this.fsw._emit(EV.CHANGE, path37, entry.stats);
16203
16271
  }
16204
16272
  } else {
16205
16273
  dir.add(item);
16206
16274
  this.fsw._symlinkPaths.set(full, linkPath);
16207
- this.fsw._emit(EV.ADD, path34, entry.stats);
16275
+ this.fsw._emit(EV.ADD, path37, entry.stats);
16208
16276
  }
16209
16277
  this.fsw._emitReady();
16210
16278
  return true;
@@ -16233,9 +16301,9 @@ var NodeFsHandler = class {
16233
16301
  return;
16234
16302
  }
16235
16303
  const item = entry.path;
16236
- let path34 = sysPath.join(directory, item);
16304
+ let path37 = sysPath.join(directory, item);
16237
16305
  current.add(item);
16238
- if (entry.stats.isSymbolicLink() && await this._handleSymlink(entry, directory, path34, item)) {
16306
+ if (entry.stats.isSymbolicLink() && await this._handleSymlink(entry, directory, path37, item)) {
16239
16307
  return;
16240
16308
  }
16241
16309
  if (this.fsw.closed) {
@@ -16244,8 +16312,8 @@ var NodeFsHandler = class {
16244
16312
  }
16245
16313
  if (item === target || !target && !previous.has(item)) {
16246
16314
  this.fsw._incrReadyCount();
16247
- path34 = sysPath.join(dir, sysPath.relative(dir, path34));
16248
- this._addToNodeFs(path34, initialAdd, wh, depth + 1);
16315
+ path37 = sysPath.join(dir, sysPath.relative(dir, path37));
16316
+ this._addToNodeFs(path37, initialAdd, wh, depth + 1);
16249
16317
  }
16250
16318
  }).on(EV.ERROR, this._boundHandleError);
16251
16319
  return new Promise((resolve3, reject) => {
@@ -16314,13 +16382,13 @@ var NodeFsHandler = class {
16314
16382
  * @param depth Child path actually targeted for watch
16315
16383
  * @param target Child path actually targeted for watch
16316
16384
  */
16317
- async _addToNodeFs(path34, initialAdd, priorWh, depth, target) {
16385
+ async _addToNodeFs(path37, initialAdd, priorWh, depth, target) {
16318
16386
  const ready = this.fsw._emitReady;
16319
- if (this.fsw._isIgnored(path34) || this.fsw.closed) {
16387
+ if (this.fsw._isIgnored(path37) || this.fsw.closed) {
16320
16388
  ready();
16321
16389
  return false;
16322
16390
  }
16323
- const wh = this.fsw._getWatchHelpers(path34);
16391
+ const wh = this.fsw._getWatchHelpers(path37);
16324
16392
  if (priorWh) {
16325
16393
  wh.filterPath = (entry) => priorWh.filterPath(entry);
16326
16394
  wh.filterDir = (entry) => priorWh.filterDir(entry);
@@ -16336,8 +16404,8 @@ var NodeFsHandler = class {
16336
16404
  const follow = this.fsw.options.followSymlinks;
16337
16405
  let closer;
16338
16406
  if (stats.isDirectory()) {
16339
- const absPath = sysPath.resolve(path34);
16340
- const targetPath = follow ? await (0, import_promises2.realpath)(path34) : path34;
16407
+ const absPath = sysPath.resolve(path37);
16408
+ const targetPath = follow ? await (0, import_promises2.realpath)(path37) : path37;
16341
16409
  if (this.fsw.closed)
16342
16410
  return;
16343
16411
  closer = await this._handleDir(wh.watchPath, stats, initialAdd, depth, target, wh, targetPath);
@@ -16347,29 +16415,29 @@ var NodeFsHandler = class {
16347
16415
  this.fsw._symlinkPaths.set(absPath, targetPath);
16348
16416
  }
16349
16417
  } else if (stats.isSymbolicLink()) {
16350
- const targetPath = follow ? await (0, import_promises2.realpath)(path34) : path34;
16418
+ const targetPath = follow ? await (0, import_promises2.realpath)(path37) : path37;
16351
16419
  if (this.fsw.closed)
16352
16420
  return;
16353
16421
  const parent = sysPath.dirname(wh.watchPath);
16354
16422
  this.fsw._getWatchedDir(parent).add(wh.watchPath);
16355
16423
  this.fsw._emit(EV.ADD, wh.watchPath, stats);
16356
- closer = await this._handleDir(parent, stats, initialAdd, depth, path34, wh, targetPath);
16424
+ closer = await this._handleDir(parent, stats, initialAdd, depth, path37, wh, targetPath);
16357
16425
  if (this.fsw.closed)
16358
16426
  return;
16359
16427
  if (targetPath !== void 0) {
16360
- this.fsw._symlinkPaths.set(sysPath.resolve(path34), targetPath);
16428
+ this.fsw._symlinkPaths.set(sysPath.resolve(path37), targetPath);
16361
16429
  }
16362
16430
  } else {
16363
16431
  closer = this._handleFile(wh.watchPath, stats, initialAdd);
16364
16432
  }
16365
16433
  ready();
16366
16434
  if (closer)
16367
- this.fsw._addPathCloser(path34, closer);
16435
+ this.fsw._addPathCloser(path37, closer);
16368
16436
  return false;
16369
16437
  } catch (error) {
16370
16438
  if (this.fsw._handleError(error)) {
16371
16439
  ready();
16372
- return path34;
16440
+ return path37;
16373
16441
  }
16374
16442
  }
16375
16443
  }
@@ -16412,26 +16480,26 @@ function createPattern(matcher) {
16412
16480
  }
16413
16481
  return () => false;
16414
16482
  }
16415
- function normalizePath(path34) {
16416
- if (typeof path34 !== "string")
16483
+ function normalizePath(path37) {
16484
+ if (typeof path37 !== "string")
16417
16485
  throw new Error("string expected");
16418
- path34 = sysPath2.normalize(path34);
16419
- path34 = path34.replace(/\\/g, "/");
16486
+ path37 = sysPath2.normalize(path37);
16487
+ path37 = path37.replace(/\\/g, "/");
16420
16488
  let prepend = false;
16421
- if (path34.startsWith("//"))
16489
+ if (path37.startsWith("//"))
16422
16490
  prepend = true;
16423
16491
  const DOUBLE_SLASH_RE2 = /\/\//;
16424
- while (path34.match(DOUBLE_SLASH_RE2))
16425
- path34 = path34.replace(DOUBLE_SLASH_RE2, "/");
16492
+ while (path37.match(DOUBLE_SLASH_RE2))
16493
+ path37 = path37.replace(DOUBLE_SLASH_RE2, "/");
16426
16494
  if (prepend)
16427
- path34 = "/" + path34;
16428
- return path34;
16495
+ path37 = "/" + path37;
16496
+ return path37;
16429
16497
  }
16430
16498
  function matchPatterns(patterns, testString, stats) {
16431
- const path34 = normalizePath(testString);
16499
+ const path37 = normalizePath(testString);
16432
16500
  for (let index = 0; index < patterns.length; index++) {
16433
16501
  const pattern = patterns[index];
16434
- if (pattern(path34, stats)) {
16502
+ if (pattern(path37, stats)) {
16435
16503
  return true;
16436
16504
  }
16437
16505
  }
@@ -16471,19 +16539,19 @@ var toUnix = (string) => {
16471
16539
  }
16472
16540
  return str;
16473
16541
  };
16474
- var normalizePathToUnix = (path34) => toUnix(sysPath2.normalize(toUnix(path34)));
16475
- var normalizeIgnored = (cwd = "") => (path34) => {
16476
- if (typeof path34 === "string") {
16477
- return normalizePathToUnix(sysPath2.isAbsolute(path34) ? path34 : sysPath2.join(cwd, path34));
16542
+ var normalizePathToUnix = (path37) => toUnix(sysPath2.normalize(toUnix(path37)));
16543
+ var normalizeIgnored = (cwd = "") => (path37) => {
16544
+ if (typeof path37 === "string") {
16545
+ return normalizePathToUnix(sysPath2.isAbsolute(path37) ? path37 : sysPath2.join(cwd, path37));
16478
16546
  } else {
16479
- return path34;
16547
+ return path37;
16480
16548
  }
16481
16549
  };
16482
- var getAbsolutePath = (path34, cwd) => {
16483
- if (sysPath2.isAbsolute(path34)) {
16484
- return path34;
16550
+ var getAbsolutePath = (path37, cwd) => {
16551
+ if (sysPath2.isAbsolute(path37)) {
16552
+ return path37;
16485
16553
  }
16486
- return sysPath2.join(cwd, path34);
16554
+ return sysPath2.join(cwd, path37);
16487
16555
  };
16488
16556
  var EMPTY_SET = Object.freeze(/* @__PURE__ */ new Set());
16489
16557
  var DirEntry = class {
@@ -16538,10 +16606,10 @@ var DirEntry = class {
16538
16606
  var STAT_METHOD_F = "stat";
16539
16607
  var STAT_METHOD_L = "lstat";
16540
16608
  var WatchHelper = class {
16541
- constructor(path34, follow, fsw) {
16609
+ constructor(path37, follow, fsw) {
16542
16610
  this.fsw = fsw;
16543
- const watchPath = path34;
16544
- this.path = path34 = path34.replace(REPLACER_RE, "");
16611
+ const watchPath = path37;
16612
+ this.path = path37 = path37.replace(REPLACER_RE, "");
16545
16613
  this.watchPath = watchPath;
16546
16614
  this.fullWatchPath = sysPath2.resolve(watchPath);
16547
16615
  this.dirParts = [];
@@ -16663,20 +16731,20 @@ var FSWatcher = class extends import_events.EventEmitter {
16663
16731
  this._closePromise = void 0;
16664
16732
  let paths = unifyPaths(paths_);
16665
16733
  if (cwd) {
16666
- paths = paths.map((path34) => {
16667
- const absPath = getAbsolutePath(path34, cwd);
16734
+ paths = paths.map((path37) => {
16735
+ const absPath = getAbsolutePath(path37, cwd);
16668
16736
  return absPath;
16669
16737
  });
16670
16738
  }
16671
- paths.forEach((path34) => {
16672
- this._removeIgnoredPath(path34);
16739
+ paths.forEach((path37) => {
16740
+ this._removeIgnoredPath(path37);
16673
16741
  });
16674
16742
  this._userIgnored = void 0;
16675
16743
  if (!this._readyCount)
16676
16744
  this._readyCount = 0;
16677
16745
  this._readyCount += paths.length;
16678
- Promise.all(paths.map(async (path34) => {
16679
- const res = await this._nodeFsHandler._addToNodeFs(path34, !_internal, void 0, 0, _origAdd);
16746
+ Promise.all(paths.map(async (path37) => {
16747
+ const res = await this._nodeFsHandler._addToNodeFs(path37, !_internal, void 0, 0, _origAdd);
16680
16748
  if (res)
16681
16749
  this._emitReady();
16682
16750
  return res;
@@ -16698,17 +16766,17 @@ var FSWatcher = class extends import_events.EventEmitter {
16698
16766
  return this;
16699
16767
  const paths = unifyPaths(paths_);
16700
16768
  const { cwd } = this.options;
16701
- paths.forEach((path34) => {
16702
- if (!sysPath2.isAbsolute(path34) && !this._closers.has(path34)) {
16769
+ paths.forEach((path37) => {
16770
+ if (!sysPath2.isAbsolute(path37) && !this._closers.has(path37)) {
16703
16771
  if (cwd)
16704
- path34 = sysPath2.join(cwd, path34);
16705
- path34 = sysPath2.resolve(path34);
16772
+ path37 = sysPath2.join(cwd, path37);
16773
+ path37 = sysPath2.resolve(path37);
16706
16774
  }
16707
- this._closePath(path34);
16708
- this._addIgnoredPath(path34);
16709
- if (this._watched.has(path34)) {
16775
+ this._closePath(path37);
16776
+ this._addIgnoredPath(path37);
16777
+ if (this._watched.has(path37)) {
16710
16778
  this._addIgnoredPath({
16711
- path: path34,
16779
+ path: path37,
16712
16780
  recursive: true
16713
16781
  });
16714
16782
  }
@@ -16772,38 +16840,38 @@ var FSWatcher = class extends import_events.EventEmitter {
16772
16840
  * @param stats arguments to be passed with event
16773
16841
  * @returns the error if defined, otherwise the value of the FSWatcher instance's `closed` flag
16774
16842
  */
16775
- async _emit(event, path34, stats) {
16843
+ async _emit(event, path37, stats) {
16776
16844
  if (this.closed)
16777
16845
  return;
16778
16846
  const opts = this.options;
16779
16847
  if (isWindows)
16780
- path34 = sysPath2.normalize(path34);
16848
+ path37 = sysPath2.normalize(path37);
16781
16849
  if (opts.cwd)
16782
- path34 = sysPath2.relative(opts.cwd, path34);
16783
- const args = [path34];
16850
+ path37 = sysPath2.relative(opts.cwd, path37);
16851
+ const args = [path37];
16784
16852
  if (stats != null)
16785
16853
  args.push(stats);
16786
16854
  const awf = opts.awaitWriteFinish;
16787
16855
  let pw;
16788
- if (awf && (pw = this._pendingWrites.get(path34))) {
16856
+ if (awf && (pw = this._pendingWrites.get(path37))) {
16789
16857
  pw.lastChange = /* @__PURE__ */ new Date();
16790
16858
  return this;
16791
16859
  }
16792
16860
  if (opts.atomic) {
16793
16861
  if (event === EVENTS.UNLINK) {
16794
- this._pendingUnlinks.set(path34, [event, ...args]);
16862
+ this._pendingUnlinks.set(path37, [event, ...args]);
16795
16863
  setTimeout(() => {
16796
- this._pendingUnlinks.forEach((entry, path35) => {
16864
+ this._pendingUnlinks.forEach((entry, path38) => {
16797
16865
  this.emit(...entry);
16798
16866
  this.emit(EVENTS.ALL, ...entry);
16799
- this._pendingUnlinks.delete(path35);
16867
+ this._pendingUnlinks.delete(path38);
16800
16868
  });
16801
16869
  }, typeof opts.atomic === "number" ? opts.atomic : 100);
16802
16870
  return this;
16803
16871
  }
16804
- if (event === EVENTS.ADD && this._pendingUnlinks.has(path34)) {
16872
+ if (event === EVENTS.ADD && this._pendingUnlinks.has(path37)) {
16805
16873
  event = EVENTS.CHANGE;
16806
- this._pendingUnlinks.delete(path34);
16874
+ this._pendingUnlinks.delete(path37);
16807
16875
  }
16808
16876
  }
16809
16877
  if (awf && (event === EVENTS.ADD || event === EVENTS.CHANGE) && this._readyEmitted) {
@@ -16821,16 +16889,16 @@ var FSWatcher = class extends import_events.EventEmitter {
16821
16889
  this.emitWithAll(event, args);
16822
16890
  }
16823
16891
  };
16824
- this._awaitWriteFinish(path34, awf.stabilityThreshold, event, awfEmit);
16892
+ this._awaitWriteFinish(path37, awf.stabilityThreshold, event, awfEmit);
16825
16893
  return this;
16826
16894
  }
16827
16895
  if (event === EVENTS.CHANGE) {
16828
- const isThrottled = !this._throttle(EVENTS.CHANGE, path34, 50);
16896
+ const isThrottled = !this._throttle(EVENTS.CHANGE, path37, 50);
16829
16897
  if (isThrottled)
16830
16898
  return this;
16831
16899
  }
16832
16900
  if (opts.alwaysStat && stats === void 0 && (event === EVENTS.ADD || event === EVENTS.ADD_DIR || event === EVENTS.CHANGE)) {
16833
- const fullPath = opts.cwd ? sysPath2.join(opts.cwd, path34) : path34;
16901
+ const fullPath = opts.cwd ? sysPath2.join(opts.cwd, path37) : path37;
16834
16902
  let stats2;
16835
16903
  try {
16836
16904
  stats2 = await (0, import_promises3.stat)(fullPath);
@@ -16861,23 +16929,23 @@ var FSWatcher = class extends import_events.EventEmitter {
16861
16929
  * @param timeout duration of time to suppress duplicate actions
16862
16930
  * @returns tracking object or false if action should be suppressed
16863
16931
  */
16864
- _throttle(actionType, path34, timeout) {
16932
+ _throttle(actionType, path37, timeout) {
16865
16933
  if (!this._throttled.has(actionType)) {
16866
16934
  this._throttled.set(actionType, /* @__PURE__ */ new Map());
16867
16935
  }
16868
16936
  const action = this._throttled.get(actionType);
16869
16937
  if (!action)
16870
16938
  throw new Error("invalid throttle");
16871
- const actionPath = action.get(path34);
16939
+ const actionPath = action.get(path37);
16872
16940
  if (actionPath) {
16873
16941
  actionPath.count++;
16874
16942
  return false;
16875
16943
  }
16876
16944
  let timeoutObject;
16877
16945
  const clear = () => {
16878
- const item = action.get(path34);
16946
+ const item = action.get(path37);
16879
16947
  const count = item ? item.count : 0;
16880
- action.delete(path34);
16948
+ action.delete(path37);
16881
16949
  clearTimeout(timeoutObject);
16882
16950
  if (item)
16883
16951
  clearTimeout(item.timeoutObject);
@@ -16885,7 +16953,7 @@ var FSWatcher = class extends import_events.EventEmitter {
16885
16953
  };
16886
16954
  timeoutObject = setTimeout(clear, timeout);
16887
16955
  const thr = { timeoutObject, clear, count: 0 };
16888
- action.set(path34, thr);
16956
+ action.set(path37, thr);
16889
16957
  return thr;
16890
16958
  }
16891
16959
  _incrReadyCount() {
@@ -16899,44 +16967,44 @@ var FSWatcher = class extends import_events.EventEmitter {
16899
16967
  * @param event
16900
16968
  * @param awfEmit Callback to be called when ready for event to be emitted.
16901
16969
  */
16902
- _awaitWriteFinish(path34, threshold, event, awfEmit) {
16970
+ _awaitWriteFinish(path37, threshold, event, awfEmit) {
16903
16971
  const awf = this.options.awaitWriteFinish;
16904
16972
  if (typeof awf !== "object")
16905
16973
  return;
16906
16974
  const pollInterval = awf.pollInterval;
16907
16975
  let timeoutHandler;
16908
- let fullPath = path34;
16909
- if (this.options.cwd && !sysPath2.isAbsolute(path34)) {
16910
- fullPath = sysPath2.join(this.options.cwd, path34);
16976
+ let fullPath = path37;
16977
+ if (this.options.cwd && !sysPath2.isAbsolute(path37)) {
16978
+ fullPath = sysPath2.join(this.options.cwd, path37);
16911
16979
  }
16912
16980
  const now = /* @__PURE__ */ new Date();
16913
16981
  const writes = this._pendingWrites;
16914
16982
  function awaitWriteFinishFn(prevStat) {
16915
16983
  (0, import_fs24.stat)(fullPath, (err, curStat) => {
16916
- if (err || !writes.has(path34)) {
16984
+ if (err || !writes.has(path37)) {
16917
16985
  if (err && err.code !== "ENOENT")
16918
16986
  awfEmit(err);
16919
16987
  return;
16920
16988
  }
16921
16989
  const now2 = Number(/* @__PURE__ */ new Date());
16922
16990
  if (prevStat && curStat.size !== prevStat.size) {
16923
- writes.get(path34).lastChange = now2;
16991
+ writes.get(path37).lastChange = now2;
16924
16992
  }
16925
- const pw = writes.get(path34);
16993
+ const pw = writes.get(path37);
16926
16994
  const df = now2 - pw.lastChange;
16927
16995
  if (df >= threshold) {
16928
- writes.delete(path34);
16996
+ writes.delete(path37);
16929
16997
  awfEmit(void 0, curStat);
16930
16998
  } else {
16931
16999
  timeoutHandler = setTimeout(awaitWriteFinishFn, pollInterval, curStat);
16932
17000
  }
16933
17001
  });
16934
17002
  }
16935
- if (!writes.has(path34)) {
16936
- writes.set(path34, {
17003
+ if (!writes.has(path37)) {
17004
+ writes.set(path37, {
16937
17005
  lastChange: now,
16938
17006
  cancelWait: () => {
16939
- writes.delete(path34);
17007
+ writes.delete(path37);
16940
17008
  clearTimeout(timeoutHandler);
16941
17009
  return event;
16942
17010
  }
@@ -16947,8 +17015,8 @@ var FSWatcher = class extends import_events.EventEmitter {
16947
17015
  /**
16948
17016
  * Determines whether user has asked to ignore this path.
16949
17017
  */
16950
- _isIgnored(path34, stats) {
16951
- if (this.options.atomic && DOT_RE.test(path34))
17018
+ _isIgnored(path37, stats) {
17019
+ if (this.options.atomic && DOT_RE.test(path37))
16952
17020
  return true;
16953
17021
  if (!this._userIgnored) {
16954
17022
  const { cwd } = this.options;
@@ -16958,17 +17026,17 @@ var FSWatcher = class extends import_events.EventEmitter {
16958
17026
  const list = [...ignoredPaths.map(normalizeIgnored(cwd)), ...ignored];
16959
17027
  this._userIgnored = anymatch(list, void 0);
16960
17028
  }
16961
- return this._userIgnored(path34, stats);
17029
+ return this._userIgnored(path37, stats);
16962
17030
  }
16963
- _isntIgnored(path34, stat4) {
16964
- return !this._isIgnored(path34, stat4);
17031
+ _isntIgnored(path37, stat4) {
17032
+ return !this._isIgnored(path37, stat4);
16965
17033
  }
16966
17034
  /**
16967
17035
  * Provides a set of common helpers and properties relating to symlink handling.
16968
17036
  * @param path file or directory pattern being watched
16969
17037
  */
16970
- _getWatchHelpers(path34) {
16971
- return new WatchHelper(path34, this.options.followSymlinks, this);
17038
+ _getWatchHelpers(path37) {
17039
+ return new WatchHelper(path37, this.options.followSymlinks, this);
16972
17040
  }
16973
17041
  // Directory helpers
16974
17042
  // -----------------
@@ -17000,63 +17068,63 @@ var FSWatcher = class extends import_events.EventEmitter {
17000
17068
  * @param item base path of item/directory
17001
17069
  */
17002
17070
  _remove(directory, item, isDirectory) {
17003
- const path34 = sysPath2.join(directory, item);
17004
- const fullPath = sysPath2.resolve(path34);
17005
- isDirectory = isDirectory != null ? isDirectory : this._watched.has(path34) || this._watched.has(fullPath);
17006
- if (!this._throttle("remove", path34, 100))
17071
+ const path37 = sysPath2.join(directory, item);
17072
+ const fullPath = sysPath2.resolve(path37);
17073
+ isDirectory = isDirectory != null ? isDirectory : this._watched.has(path37) || this._watched.has(fullPath);
17074
+ if (!this._throttle("remove", path37, 100))
17007
17075
  return;
17008
17076
  if (!isDirectory && this._watched.size === 1) {
17009
17077
  this.add(directory, item, true);
17010
17078
  }
17011
- const wp = this._getWatchedDir(path34);
17079
+ const wp = this._getWatchedDir(path37);
17012
17080
  const nestedDirectoryChildren = wp.getChildren();
17013
- nestedDirectoryChildren.forEach((nested) => this._remove(path34, nested));
17081
+ nestedDirectoryChildren.forEach((nested) => this._remove(path37, nested));
17014
17082
  const parent = this._getWatchedDir(directory);
17015
17083
  const wasTracked = parent.has(item);
17016
17084
  parent.remove(item);
17017
17085
  if (this._symlinkPaths.has(fullPath)) {
17018
17086
  this._symlinkPaths.delete(fullPath);
17019
17087
  }
17020
- let relPath = path34;
17088
+ let relPath = path37;
17021
17089
  if (this.options.cwd)
17022
- relPath = sysPath2.relative(this.options.cwd, path34);
17090
+ relPath = sysPath2.relative(this.options.cwd, path37);
17023
17091
  if (this.options.awaitWriteFinish && this._pendingWrites.has(relPath)) {
17024
17092
  const event = this._pendingWrites.get(relPath).cancelWait();
17025
17093
  if (event === EVENTS.ADD)
17026
17094
  return;
17027
17095
  }
17028
- this._watched.delete(path34);
17096
+ this._watched.delete(path37);
17029
17097
  this._watched.delete(fullPath);
17030
17098
  const eventName = isDirectory ? EVENTS.UNLINK_DIR : EVENTS.UNLINK;
17031
- if (wasTracked && !this._isIgnored(path34))
17032
- this._emit(eventName, path34);
17033
- this._closePath(path34);
17099
+ if (wasTracked && !this._isIgnored(path37))
17100
+ this._emit(eventName, path37);
17101
+ this._closePath(path37);
17034
17102
  }
17035
17103
  /**
17036
17104
  * Closes all watchers for a path
17037
17105
  */
17038
- _closePath(path34) {
17039
- this._closeFile(path34);
17040
- const dir = sysPath2.dirname(path34);
17041
- this._getWatchedDir(dir).remove(sysPath2.basename(path34));
17106
+ _closePath(path37) {
17107
+ this._closeFile(path37);
17108
+ const dir = sysPath2.dirname(path37);
17109
+ this._getWatchedDir(dir).remove(sysPath2.basename(path37));
17042
17110
  }
17043
17111
  /**
17044
17112
  * Closes only file-specific watchers
17045
17113
  */
17046
- _closeFile(path34) {
17047
- const closers = this._closers.get(path34);
17114
+ _closeFile(path37) {
17115
+ const closers = this._closers.get(path37);
17048
17116
  if (!closers)
17049
17117
  return;
17050
17118
  closers.forEach((closer) => closer());
17051
- this._closers.delete(path34);
17119
+ this._closers.delete(path37);
17052
17120
  }
17053
- _addPathCloser(path34, closer) {
17121
+ _addPathCloser(path37, closer) {
17054
17122
  if (!closer)
17055
17123
  return;
17056
- let list = this._closers.get(path34);
17124
+ let list = this._closers.get(path37);
17057
17125
  if (!list) {
17058
17126
  list = [];
17059
- this._closers.set(path34, list);
17127
+ this._closers.set(path37, list);
17060
17128
  }
17061
17129
  list.push(closer);
17062
17130
  }
@@ -17426,11 +17494,11 @@ function createStrictRateLimiterFromConfig(config) {
17426
17494
  }
17427
17495
 
17428
17496
  // modules/server/middleware/auto-rate-limit.ts
17429
- function matchesStrictPattern(path34, patterns) {
17497
+ function matchesStrictPattern(path37, patterns) {
17430
17498
  for (const pattern of patterns) {
17431
17499
  const regexPattern = pattern.replace(/\*\*/g, ".*").replace(/\*/g, "[^/]*").replace(/\//g, "\\/");
17432
17500
  const regex = new RegExp(`^${regexPattern}$`);
17433
- if (regex.test(path34)) {
17501
+ if (regex.test(path37)) {
17434
17502
  return true;
17435
17503
  }
17436
17504
  }
@@ -17620,9 +17688,441 @@ async function handleApiRequest(options) {
17620
17688
  }
17621
17689
  }
17622
17690
 
17691
+ // modules/server/image-optimizer/index.ts
17692
+ var import_sharp = __toESM(require("sharp"));
17693
+ var import_fs27 = __toESM(require("fs"));
17694
+ var import_path37 = __toESM(require("path"));
17695
+
17696
+ // modules/server/image-optimizer/validation.ts
17697
+ var import_path35 = __toESM(require("path"));
17698
+ function isRemoteUrl(url) {
17699
+ return url.startsWith("http://") || url.startsWith("https://");
17700
+ }
17701
+ function sanitizeImagePath(imagePath) {
17702
+ const normalized = import_path35.default.normalize(imagePath).replace(/^(\.\.(\/|\\|$))+/, "");
17703
+ return normalized.replace(/^[/\\]+/, "");
17704
+ }
17705
+ function patternToRegex(pattern) {
17706
+ const parts = [];
17707
+ if (pattern.protocol) {
17708
+ parts.push(pattern.protocol === "https" ? "https" : "http");
17709
+ } else {
17710
+ parts.push("https?");
17711
+ }
17712
+ parts.push("://");
17713
+ let hostnamePattern = pattern.hostname.replace(/\./g, "\\.").replace(/\*\*/g, ".*").replace(/\*/g, "[^.]*");
17714
+ parts.push(hostnamePattern);
17715
+ if (pattern.port) {
17716
+ parts.push(`:${pattern.port}`);
17717
+ }
17718
+ if (pattern.pathname) {
17719
+ let pathnamePattern = pattern.pathname.replace(/\*\*/g, ".*").replace(/\*/g, "[^/]*");
17720
+ parts.push(pathnamePattern);
17721
+ } else {
17722
+ parts.push(".*");
17723
+ }
17724
+ const regexSource = `^${parts.join("")}`;
17725
+ return new RegExp(regexSource);
17726
+ }
17727
+ function validateRemoteUrl(url, config) {
17728
+ if (!config.remotePatterns && !config.domains) {
17729
+ return false;
17730
+ }
17731
+ try {
17732
+ const urlObj = new URL(url);
17733
+ const protocol = urlObj.protocol.replace(":", "");
17734
+ const hostname = urlObj.hostname;
17735
+ const port = urlObj.port || "";
17736
+ const pathname = urlObj.pathname;
17737
+ if (config.remotePatterns && config.remotePatterns.length > 0) {
17738
+ for (const pattern of config.remotePatterns) {
17739
+ const regex = patternToRegex(pattern);
17740
+ const testUrl = `${protocol}://${hostname}${port ? `:${port}` : ""}${pathname}`;
17741
+ if (regex.test(testUrl)) {
17742
+ if (pattern.protocol && pattern.protocol !== protocol) {
17743
+ continue;
17744
+ }
17745
+ if (pattern.port && pattern.port !== port) {
17746
+ continue;
17747
+ }
17748
+ return true;
17749
+ }
17750
+ }
17751
+ }
17752
+ if (config.domains && config.domains.length > 0) {
17753
+ for (const domain of config.domains) {
17754
+ const domainPattern = domain.replace(/\./g, "\\.").replace(/\*\*/g, ".*").replace(/\*/g, "[^.]*");
17755
+ const regex = new RegExp(`^${domainPattern}$`);
17756
+ if (regex.test(hostname)) {
17757
+ if (process.env.NODE_ENV === "production" && protocol !== "https") {
17758
+ continue;
17759
+ }
17760
+ return true;
17761
+ }
17762
+ }
17763
+ }
17764
+ return false;
17765
+ } catch (error) {
17766
+ return false;
17767
+ }
17768
+ }
17769
+ function validateImageDimensions(width, height, config) {
17770
+ const maxWidth = config.maxWidth || 3840;
17771
+ const maxHeight = config.maxHeight || 3840;
17772
+ if (width !== void 0 && (width <= 0 || width > maxWidth)) {
17773
+ return {
17774
+ valid: false,
17775
+ error: `Image width must be between 1 and ${maxWidth}, got ${width}`
17776
+ };
17777
+ }
17778
+ if (height !== void 0 && (height <= 0 || height > maxHeight)) {
17779
+ return {
17780
+ valid: false,
17781
+ error: `Image height must be between 1 and ${maxHeight}, got ${height}`
17782
+ };
17783
+ }
17784
+ return { valid: true };
17785
+ }
17786
+ function validateQuality(quality) {
17787
+ if (quality === void 0) {
17788
+ return { valid: true };
17789
+ }
17790
+ if (typeof quality !== "number" || quality < 1 || quality > 100) {
17791
+ return {
17792
+ valid: false,
17793
+ error: `Image quality must be between 1 and 100, got ${quality}`
17794
+ };
17795
+ }
17796
+ return { valid: true };
17797
+ }
17798
+
17799
+ // modules/server/image-optimizer/cache.ts
17800
+ var import_fs26 = __toESM(require("fs"));
17801
+ var import_path36 = __toESM(require("path"));
17802
+ var import_crypto = __toESM(require("crypto"));
17803
+ function generateCacheKey(src, width, height, quality, format) {
17804
+ const data = `${src}-${width || ""}-${height || ""}-${quality || ""}-${format || ""}`;
17805
+ return import_crypto.default.createHash("sha256").update(data).digest("hex");
17806
+ }
17807
+ function getCacheDir(projectRoot, config) {
17808
+ const buildDir = getBuildDir(projectRoot, config);
17809
+ return import_path36.default.join(buildDir, "cache", "images");
17810
+ }
17811
+ function ensureCacheDir(cacheDir) {
17812
+ if (!import_fs26.default.existsSync(cacheDir)) {
17813
+ import_fs26.default.mkdirSync(cacheDir, { recursive: true });
17814
+ }
17815
+ }
17816
+ function getCachedImagePath(cacheKey, extension, cacheDir) {
17817
+ return import_path36.default.join(cacheDir, `${cacheKey}.${extension}`);
17818
+ }
17819
+ function hasCachedImage(cacheKey, extension, cacheDir) {
17820
+ const cachedPath = getCachedImagePath(cacheKey, extension, cacheDir);
17821
+ return import_fs26.default.existsSync(cachedPath);
17822
+ }
17823
+ function readCachedImage(cacheKey, extension, cacheDir) {
17824
+ const cachedPath = getCachedImagePath(cacheKey, extension, cacheDir);
17825
+ try {
17826
+ if (import_fs26.default.existsSync(cachedPath)) {
17827
+ return import_fs26.default.readFileSync(cachedPath);
17828
+ }
17829
+ } catch (error) {
17830
+ console.warn(`[image-optimizer] Failed to read cached image: ${cachedPath}`, error);
17831
+ }
17832
+ return null;
17833
+ }
17834
+ function writeCachedImage(cacheKey, extension, cacheDir, imageBuffer) {
17835
+ ensureCacheDir(cacheDir);
17836
+ const cachedPath = getCachedImagePath(cacheKey, extension, cacheDir);
17837
+ try {
17838
+ import_fs26.default.writeFileSync(cachedPath, imageBuffer);
17839
+ } catch (error) {
17840
+ console.warn(`[image-optimizer] Failed to write cached image: ${cachedPath}`, error);
17841
+ }
17842
+ }
17843
+ function getImageMimeType(format) {
17844
+ const formatMap = {
17845
+ webp: "image/webp",
17846
+ avif: "image/avif",
17847
+ jpeg: "image/jpeg",
17848
+ jpg: "image/jpeg",
17849
+ png: "image/png",
17850
+ gif: "image/gif",
17851
+ svg: "image/svg+xml"
17852
+ };
17853
+ const normalized = format.toLowerCase();
17854
+ return formatMap[normalized] || "image/jpeg";
17855
+ }
17856
+ function getImageExtension(format) {
17857
+ const formatMap = {
17858
+ "image/webp": "webp",
17859
+ "image/avif": "avif",
17860
+ "image/jpeg": "jpg",
17861
+ "image/png": "png",
17862
+ "image/gif": "gif",
17863
+ "image/svg+xml": "svg",
17864
+ webp: "webp",
17865
+ avif: "avif",
17866
+ jpeg: "jpg",
17867
+ jpg: "jpg",
17868
+ png: "png",
17869
+ gif: "gif",
17870
+ svg: "svg"
17871
+ };
17872
+ const normalized = format.toLowerCase();
17873
+ return formatMap[normalized] || "jpg";
17874
+ }
17875
+
17876
+ // modules/server/image-optimizer/index.ts
17877
+ async function downloadRemoteImage(url, timeout = 1e4) {
17878
+ let fetchFn;
17879
+ try {
17880
+ if (typeof fetch !== "undefined") {
17881
+ fetchFn = fetch;
17882
+ } else {
17883
+ const { fetch: undiciFetch } = await import("undici");
17884
+ fetchFn = undiciFetch;
17885
+ }
17886
+ } catch (error) {
17887
+ throw new Error("Failed to load fetch implementation. Node 18+ required or install undici.");
17888
+ }
17889
+ const controller = new AbortController();
17890
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
17891
+ try {
17892
+ const response = await fetchFn(url, {
17893
+ signal: controller.signal,
17894
+ headers: {
17895
+ "User-Agent": "Loly-Image-Optimizer/1.0"
17896
+ }
17897
+ });
17898
+ clearTimeout(timeoutId);
17899
+ if (!response.ok) {
17900
+ throw new Error(`Failed to download image: ${response.status} ${response.statusText}`);
17901
+ }
17902
+ const arrayBuffer = await response.arrayBuffer();
17903
+ return Buffer.from(arrayBuffer);
17904
+ } catch (error) {
17905
+ clearTimeout(timeoutId);
17906
+ if (error instanceof Error && error.name === "AbortError") {
17907
+ throw new Error(`Image download timeout after ${timeout}ms`);
17908
+ }
17909
+ throw error;
17910
+ }
17911
+ }
17912
+ function readLocalImage(src, projectRoot, config) {
17913
+ const sanitized = sanitizeImagePath(src);
17914
+ const staticDir = getStaticDir(projectRoot, config);
17915
+ const staticPath = import_path37.default.join(staticDir, sanitized);
17916
+ if (import_fs27.default.existsSync(staticPath)) {
17917
+ return import_fs27.default.readFileSync(staticPath);
17918
+ }
17919
+ if (src.startsWith("/")) {
17920
+ const absolutePath = import_path37.default.join(projectRoot, sanitized);
17921
+ if (import_fs27.default.existsSync(absolutePath)) {
17922
+ return import_fs27.default.readFileSync(absolutePath);
17923
+ }
17924
+ }
17925
+ throw new Error(`Image not found: ${src}`);
17926
+ }
17927
+ function determineOutputFormat(sourceFormat, requestedFormat, config) {
17928
+ if (sourceFormat === "svg") {
17929
+ return "svg";
17930
+ }
17931
+ if (requestedFormat && requestedFormat !== "auto") {
17932
+ return requestedFormat;
17933
+ }
17934
+ const supportedFormats = config.formats || ["image/webp"];
17935
+ if (supportedFormats.includes("image/avif")) {
17936
+ return "avif";
17937
+ }
17938
+ if (supportedFormats.includes("image/webp")) {
17939
+ return "webp";
17940
+ }
17941
+ return sourceFormat === "svg" ? "jpeg" : sourceFormat;
17942
+ }
17943
+ async function optimizeImage(options, projectRoot, config) {
17944
+ const imageConfig = config.images || {};
17945
+ const dimValidation = validateImageDimensions(options.width, options.height, imageConfig);
17946
+ if (!dimValidation.valid) {
17947
+ throw new Error(dimValidation.error);
17948
+ }
17949
+ const qualityValidation = validateQuality(options.quality);
17950
+ if (!qualityValidation.valid) {
17951
+ throw new Error(qualityValidation.error);
17952
+ }
17953
+ if (isRemoteUrl(options.src)) {
17954
+ if (!validateRemoteUrl(options.src, imageConfig)) {
17955
+ throw new Error(`Remote image domain not allowed: ${options.src}`);
17956
+ }
17957
+ }
17958
+ const sourceFormat = import_path37.default.extname(options.src).slice(1).toLowerCase() || "jpeg";
17959
+ const outputFormat = determineOutputFormat(
17960
+ sourceFormat,
17961
+ options.format,
17962
+ imageConfig
17963
+ );
17964
+ const cacheKey = generateCacheKey(
17965
+ options.src,
17966
+ options.width,
17967
+ options.height,
17968
+ options.quality || imageConfig.quality || 75,
17969
+ outputFormat
17970
+ );
17971
+ const cacheDir = getCacheDir(projectRoot, config);
17972
+ const extension = getImageExtension(outputFormat);
17973
+ if (hasCachedImage(cacheKey, extension, cacheDir)) {
17974
+ const cached = readCachedImage(cacheKey, extension, cacheDir);
17975
+ if (cached) {
17976
+ const metadata2 = await (0, import_sharp.default)(cached).metadata();
17977
+ return {
17978
+ buffer: cached,
17979
+ format: outputFormat,
17980
+ mimeType: getImageMimeType(outputFormat),
17981
+ width: metadata2.width || options.width || 0,
17982
+ height: metadata2.height || options.height || 0
17983
+ };
17984
+ }
17985
+ }
17986
+ let imageBuffer;
17987
+ if (isRemoteUrl(options.src)) {
17988
+ imageBuffer = await downloadRemoteImage(options.src);
17989
+ } else {
17990
+ imageBuffer = readLocalImage(options.src, projectRoot, config);
17991
+ }
17992
+ if (outputFormat === "svg" || sourceFormat === "svg") {
17993
+ if (!imageConfig.dangerouslyAllowSVG) {
17994
+ throw new Error("SVG images are not allowed. Set images.dangerouslyAllowSVG to true to enable.");
17995
+ }
17996
+ return {
17997
+ buffer: imageBuffer,
17998
+ format: "svg",
17999
+ mimeType: "image/svg+xml",
18000
+ width: options.width || 0,
18001
+ height: options.height || 0
18002
+ };
18003
+ }
18004
+ let sharpInstance = (0, import_sharp.default)(imageBuffer);
18005
+ const metadata = await sharpInstance.metadata();
18006
+ if (options.width || options.height) {
18007
+ const fit = options.fit || "cover";
18008
+ sharpInstance = sharpInstance.resize(options.width, options.height, {
18009
+ fit,
18010
+ withoutEnlargement: true
18011
+ });
18012
+ }
18013
+ const quality = options.quality || imageConfig.quality || 75;
18014
+ switch (outputFormat) {
18015
+ case "webp":
18016
+ sharpInstance = sharpInstance.webp({ quality });
18017
+ break;
18018
+ case "avif":
18019
+ sharpInstance = sharpInstance.avif({ quality });
18020
+ break;
18021
+ case "jpeg":
18022
+ case "jpg":
18023
+ sharpInstance = sharpInstance.jpeg({ quality });
18024
+ break;
18025
+ case "png":
18026
+ sharpInstance = sharpInstance.png({ quality: Math.round(quality / 100 * 9) });
18027
+ break;
18028
+ default:
18029
+ sharpInstance = sharpInstance.jpeg({ quality });
18030
+ }
18031
+ const optimizedBuffer = await sharpInstance.toBuffer();
18032
+ const finalMetadata = await (0, import_sharp.default)(optimizedBuffer).metadata();
18033
+ writeCachedImage(cacheKey, extension, cacheDir, optimizedBuffer);
18034
+ return {
18035
+ buffer: optimizedBuffer,
18036
+ format: outputFormat,
18037
+ mimeType: getImageMimeType(outputFormat),
18038
+ width: finalMetadata.width || options.width || metadata.width || 0,
18039
+ height: finalMetadata.height || options.height || metadata.height || 0
18040
+ };
18041
+ }
18042
+
18043
+ // modules/server/handlers/image.ts
18044
+ async function handleImageRequest(options) {
18045
+ const { req, res, projectRoot, config } = options;
18046
+ try {
18047
+ const src = req.query.src;
18048
+ const width = req.query.w ? parseInt(req.query.w, 10) : void 0;
18049
+ const height = req.query.h ? parseInt(req.query.h, 10) : void 0;
18050
+ const quality = req.query.q ? parseInt(req.query.q, 10) : void 0;
18051
+ const format = req.query.format;
18052
+ const fit = req.query.fit;
18053
+ if (!src) {
18054
+ res.status(400).json({
18055
+ error: "Missing required parameter: src"
18056
+ });
18057
+ return;
18058
+ }
18059
+ if (typeof src !== "string") {
18060
+ res.status(400).json({
18061
+ error: "Parameter 'src' must be a string"
18062
+ });
18063
+ return;
18064
+ }
18065
+ const result = await optimizeImage(
18066
+ {
18067
+ src,
18068
+ width,
18069
+ height,
18070
+ quality,
18071
+ format,
18072
+ fit
18073
+ },
18074
+ projectRoot,
18075
+ config
18076
+ );
18077
+ const imageConfig = config.images || {};
18078
+ const cacheTTL = imageConfig.minimumCacheTTL || 60;
18079
+ res.setHeader("Content-Type", result.mimeType);
18080
+ res.setHeader("Content-Length", result.buffer.length);
18081
+ res.setHeader("Cache-Control", `public, max-age=${cacheTTL}, immutable`);
18082
+ res.setHeader("X-Content-Type-Options", "nosniff");
18083
+ res.send(result.buffer);
18084
+ } catch (error) {
18085
+ if (error instanceof Error) {
18086
+ if (error.message.includes("not allowed")) {
18087
+ res.status(403).json({
18088
+ error: "Forbidden",
18089
+ message: error.message
18090
+ });
18091
+ return;
18092
+ }
18093
+ if (error.message.includes("not found") || error.message.includes("Image not found")) {
18094
+ res.status(404).json({
18095
+ error: "Not Found",
18096
+ message: error.message
18097
+ });
18098
+ return;
18099
+ }
18100
+ if (error.message.includes("must be")) {
18101
+ res.status(400).json({
18102
+ error: "Bad Request",
18103
+ message: error.message
18104
+ });
18105
+ return;
18106
+ }
18107
+ if (error.message.includes("timeout") || error.message.includes("download")) {
18108
+ res.status(504).json({
18109
+ error: "Gateway Timeout",
18110
+ message: error.message
18111
+ });
18112
+ return;
18113
+ }
18114
+ }
18115
+ console.error("[image-optimizer] Error processing image:", error);
18116
+ res.status(500).json({
18117
+ error: "Internal Server Error",
18118
+ message: "Failed to process image"
18119
+ });
18120
+ }
18121
+ }
18122
+
17623
18123
  // modules/server/routes.ts
17624
18124
  init_globals();
17625
- var import_path35 = __toESM(require("path"));
18125
+ var import_path38 = __toESM(require("path"));
17626
18126
  var cachedRewriteLoader = null;
17627
18127
  var cachedProjectRoot = null;
17628
18128
  var cachedIsDev = null;
@@ -17650,10 +18150,20 @@ function setupRoutes(options) {
17650
18150
  } = options;
17651
18151
  const routeChunks = routeLoader.loadRouteChunks();
17652
18152
  const rewriteLoader = getRewriteLoader(projectRoot, isDev);
17653
- const ssgOutDir = import_path35.default.join(
17654
- config ? getBuildDir(projectRoot, config) : import_path35.default.join(projectRoot, BUILD_FOLDER_NAME),
18153
+ const ssgOutDir = import_path38.default.join(
18154
+ config ? getBuildDir(projectRoot, config) : import_path38.default.join(projectRoot, BUILD_FOLDER_NAME),
17655
18155
  "ssg"
17656
18156
  );
18157
+ if (config) {
18158
+ app.get("/_loly/image", async (req, res) => {
18159
+ await handleImageRequest({
18160
+ req,
18161
+ res,
18162
+ projectRoot,
18163
+ config
18164
+ });
18165
+ });
18166
+ }
17657
18167
  app.all("/api/*", async (req, res) => {
17658
18168
  const apiRoutes = isDev && getRoutes ? (await getRoutes()).apiRoutes : initialApiRoutes;
17659
18169
  const serverConfig = await getServerConfig(projectRoot);
@@ -18700,7 +19210,7 @@ var import_cors = __toESM(require("cors"));
18700
19210
  var import_helmet = __toESM(require("helmet"));
18701
19211
  var import_cookie_parser = __toESM(require("cookie-parser"));
18702
19212
  var import_compression = __toESM(require("compression"));
18703
- var import_crypto = __toESM(require("crypto"));
19213
+ var import_crypto2 = __toESM(require("crypto"));
18704
19214
  var setupApplication = async ({
18705
19215
  projectRoot
18706
19216
  }) => {
@@ -18809,7 +19319,7 @@ var setupApplication = async ({
18809
19319
  if (process.env.NODE_ENV !== "development" && security?.contentSecurityPolicy !== false) {
18810
19320
  app.use(
18811
19321
  (req, res, next) => {
18812
- const nonce = import_crypto.default.randomBytes(16).toString("base64");
19322
+ const nonce = import_crypto2.default.randomBytes(16).toString("base64");
18813
19323
  res.locals.nonce = nonce;
18814
19324
  next();
18815
19325
  }
@@ -18880,8 +19390,8 @@ var setupApplication = async ({
18880
19390
 
18881
19391
  // src/server.ts
18882
19392
  var import_dotenv2 = __toESM(require("dotenv"));
18883
- var envPath = import_path36.default.join(process.cwd(), ".env");
18884
- if (import_fs26.default.existsSync(envPath)) {
19393
+ var envPath = import_path39.default.join(process.cwd(), ".env");
19394
+ if (import_fs28.default.existsSync(envPath)) {
18885
19395
  import_dotenv2.default.config({ path: envPath });
18886
19396
  } else {
18887
19397
  import_dotenv2.default.config();
@@ -18902,8 +19412,8 @@ async function startServer(options = {}) {
18902
19412
  }
18903
19413
  const port = options.port ?? (process.env.PORT ? parseInt(process.env.PORT, 10) : void 0) ?? config.server.port;
18904
19414
  const host = process.env.HOST ?? (!isDev ? "0.0.0.0" : void 0) ?? config.server.host;
18905
- const appDir = options.appDir ?? (isDev ? getAppDir(projectRoot, config) : import_path36.default.join(getBuildDir(projectRoot, config), "server"));
18906
- if (!isDev && !import_fs26.default.existsSync(appDir)) {
19415
+ const appDir = options.appDir ?? (isDev ? getAppDir(projectRoot, config) : import_path39.default.join(getBuildDir(projectRoot, config), "server"));
19416
+ if (!isDev && !import_fs28.default.existsSync(appDir)) {
18907
19417
  logger4.error("Compiled directory not found", void 0, {
18908
19418
  buildDir: config.directories.build,
18909
19419
  appDir,
@@ -19099,7 +19609,7 @@ async function run() {
19099
19609
  }
19100
19610
  const args = parseArgs(argv.slice(1));
19101
19611
  const projectRoot = import_process.default.cwd();
19102
- const appDir = import_path37.default.resolve(projectRoot, args.appDir || "app");
19612
+ const appDir = import_path40.default.resolve(projectRoot, args.appDir || "app");
19103
19613
  const port = typeof args.port === "string" && args.port.trim().length > 0 ? Number(args.port) : 3e3;
19104
19614
  switch (command) {
19105
19615
  case "dev": {