@lolyjs/core 0.3.0-alpha.5 → 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") {
@@ -13175,12 +13176,12 @@ var DEFAULT_IGNORED_PATHS = [
13175
13176
  /^\/sockjs-node/
13176
13177
  // Hot reload websocket
13177
13178
  ];
13178
- function shouldIgnorePath(path34, ignoredPaths) {
13179
+ function shouldIgnorePath(path37, ignoredPaths) {
13179
13180
  return ignoredPaths.some((pattern) => {
13180
13181
  if (typeof pattern === "string") {
13181
- return path34 === pattern || path34.startsWith(pattern);
13182
+ return path37 === pattern || path37.startsWith(pattern);
13182
13183
  }
13183
- return pattern.test(path34);
13184
+ return pattern.test(path37);
13184
13185
  });
13185
13186
  }
13186
13187
  function requestLoggerMiddleware(options = {}) {
@@ -15143,7 +15144,20 @@ var DEFAULT_CONFIG2 = {
15143
15144
  ssr: true,
15144
15145
  ssg: true
15145
15146
  },
15146
- 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
+ }
15147
15161
  };
15148
15162
  function deepMerge(target, source) {
15149
15163
  const result = { ...target };
@@ -15260,6 +15274,53 @@ function validateConfig(config, projectRoot) {
15260
15274
  if (typeof config.rendering.ssg !== "boolean") {
15261
15275
  errors.push("config.rendering.ssg must be a boolean");
15262
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
+ }
15263
15324
  if (errors.length > 0) {
15264
15325
  const errorMessage = [
15265
15326
  "\u274C Configuration validation failed:",
@@ -15393,8 +15454,8 @@ async function buildApp(options = {}) {
15393
15454
  }
15394
15455
 
15395
15456
  // src/server.ts
15396
- var import_fs26 = __toESM(require("fs"));
15397
- var import_path36 = __toESM(require("path"));
15457
+ var import_fs28 = __toESM(require("fs"));
15458
+ var import_path39 = __toESM(require("path"));
15398
15459
 
15399
15460
  // modules/server/setup.ts
15400
15461
  var import_express = __toESM(require("express"));
@@ -15477,7 +15538,7 @@ var ReaddirpStream = class extends import_node_stream.Readable {
15477
15538
  this._directoryFilter = normalizeFilter(opts.directoryFilter);
15478
15539
  const statMethod = opts.lstat ? import_promises.lstat : import_promises.stat;
15479
15540
  if (wantBigintFsStats) {
15480
- this._stat = (path34) => statMethod(path34, { bigint: true });
15541
+ this._stat = (path37) => statMethod(path37, { bigint: true });
15481
15542
  } else {
15482
15543
  this._stat = statMethod;
15483
15544
  }
@@ -15502,8 +15563,8 @@ var ReaddirpStream = class extends import_node_stream.Readable {
15502
15563
  const par = this.parent;
15503
15564
  const fil = par && par.files;
15504
15565
  if (fil && fil.length > 0) {
15505
- const { path: path34, depth } = par;
15506
- 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));
15507
15568
  const awaited = await Promise.all(slice);
15508
15569
  for (const entry of awaited) {
15509
15570
  if (!entry)
@@ -15543,20 +15604,20 @@ var ReaddirpStream = class extends import_node_stream.Readable {
15543
15604
  this.reading = false;
15544
15605
  }
15545
15606
  }
15546
- async _exploreDir(path34, depth) {
15607
+ async _exploreDir(path37, depth) {
15547
15608
  let files;
15548
15609
  try {
15549
- files = await (0, import_promises.readdir)(path34, this._rdOptions);
15610
+ files = await (0, import_promises.readdir)(path37, this._rdOptions);
15550
15611
  } catch (error) {
15551
15612
  this._onError(error);
15552
15613
  }
15553
- return { files, depth, path: path34 };
15614
+ return { files, depth, path: path37 };
15554
15615
  }
15555
- async _formatEntry(dirent, path34) {
15616
+ async _formatEntry(dirent, path37) {
15556
15617
  let entry;
15557
15618
  const basename3 = this._isDirent ? dirent.name : dirent;
15558
15619
  try {
15559
- 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));
15560
15621
  entry = { path: (0, import_node_path.relative)(this._root, fullPath), fullPath, basename: basename3 };
15561
15622
  entry[this._statsProp] = this._isDirent ? dirent : await this._stat(fullPath);
15562
15623
  } catch (err) {
@@ -15956,16 +16017,16 @@ var delFromSet = (main, prop, item) => {
15956
16017
  };
15957
16018
  var isEmptySet = (val) => val instanceof Set ? val.size === 0 : !val;
15958
16019
  var FsWatchInstances = /* @__PURE__ */ new Map();
15959
- function createFsWatchInstance(path34, options, listener, errHandler, emitRaw) {
16020
+ function createFsWatchInstance(path37, options, listener, errHandler, emitRaw) {
15960
16021
  const handleEvent = (rawEvent, evPath) => {
15961
- listener(path34);
15962
- emitRaw(rawEvent, evPath, { watchedPath: path34 });
15963
- if (evPath && path34 !== evPath) {
15964
- 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));
15965
16026
  }
15966
16027
  };
15967
16028
  try {
15968
- return (0, import_fs23.watch)(path34, {
16029
+ return (0, import_fs23.watch)(path37, {
15969
16030
  persistent: options.persistent
15970
16031
  }, handleEvent);
15971
16032
  } catch (error) {
@@ -15981,12 +16042,12 @@ var fsWatchBroadcast = (fullPath, listenerType, val1, val2, val3) => {
15981
16042
  listener(val1, val2, val3);
15982
16043
  });
15983
16044
  };
15984
- var setFsWatchListener = (path34, fullPath, options, handlers) => {
16045
+ var setFsWatchListener = (path37, fullPath, options, handlers) => {
15985
16046
  const { listener, errHandler, rawEmitter } = handlers;
15986
16047
  let cont = FsWatchInstances.get(fullPath);
15987
16048
  let watcher;
15988
16049
  if (!options.persistent) {
15989
- watcher = createFsWatchInstance(path34, options, listener, errHandler, rawEmitter);
16050
+ watcher = createFsWatchInstance(path37, options, listener, errHandler, rawEmitter);
15990
16051
  if (!watcher)
15991
16052
  return;
15992
16053
  return watcher.close.bind(watcher);
@@ -15997,7 +16058,7 @@ var setFsWatchListener = (path34, fullPath, options, handlers) => {
15997
16058
  addAndConvert(cont, KEY_RAW, rawEmitter);
15998
16059
  } else {
15999
16060
  watcher = createFsWatchInstance(
16000
- path34,
16061
+ path37,
16001
16062
  options,
16002
16063
  fsWatchBroadcast.bind(null, fullPath, KEY_LISTENERS),
16003
16064
  errHandler,
@@ -16012,7 +16073,7 @@ var setFsWatchListener = (path34, fullPath, options, handlers) => {
16012
16073
  cont.watcherUnusable = true;
16013
16074
  if (isWindows && error.code === "EPERM") {
16014
16075
  try {
16015
- const fd = await (0, import_promises2.open)(path34, "r");
16076
+ const fd = await (0, import_promises2.open)(path37, "r");
16016
16077
  await fd.close();
16017
16078
  broadcastErr(error);
16018
16079
  } catch (err) {
@@ -16043,7 +16104,7 @@ var setFsWatchListener = (path34, fullPath, options, handlers) => {
16043
16104
  };
16044
16105
  };
16045
16106
  var FsWatchFileInstances = /* @__PURE__ */ new Map();
16046
- var setFsWatchFileListener = (path34, fullPath, options, handlers) => {
16107
+ var setFsWatchFileListener = (path37, fullPath, options, handlers) => {
16047
16108
  const { listener, rawEmitter } = handlers;
16048
16109
  let cont = FsWatchFileInstances.get(fullPath);
16049
16110
  const copts = cont && cont.options;
@@ -16065,7 +16126,7 @@ var setFsWatchFileListener = (path34, fullPath, options, handlers) => {
16065
16126
  });
16066
16127
  const currmtime = curr.mtimeMs;
16067
16128
  if (curr.size !== prev.size || currmtime > prev.mtimeMs || currmtime === 0) {
16068
- foreach(cont.listeners, (listener2) => listener2(path34, curr));
16129
+ foreach(cont.listeners, (listener2) => listener2(path37, curr));
16069
16130
  }
16070
16131
  })
16071
16132
  };
@@ -16093,13 +16154,13 @@ var NodeFsHandler = class {
16093
16154
  * @param listener on fs change
16094
16155
  * @returns closer for the watcher instance
16095
16156
  */
16096
- _watchWithNodeFs(path34, listener) {
16157
+ _watchWithNodeFs(path37, listener) {
16097
16158
  const opts = this.fsw.options;
16098
- const directory = sysPath.dirname(path34);
16099
- const basename3 = sysPath.basename(path34);
16159
+ const directory = sysPath.dirname(path37);
16160
+ const basename3 = sysPath.basename(path37);
16100
16161
  const parent = this.fsw._getWatchedDir(directory);
16101
16162
  parent.add(basename3);
16102
- const absolutePath = sysPath.resolve(path34);
16163
+ const absolutePath = sysPath.resolve(path37);
16103
16164
  const options = {
16104
16165
  persistent: opts.persistent
16105
16166
  };
@@ -16109,12 +16170,12 @@ var NodeFsHandler = class {
16109
16170
  if (opts.usePolling) {
16110
16171
  const enableBin = opts.interval !== opts.binaryInterval;
16111
16172
  options.interval = enableBin && isBinaryPath(basename3) ? opts.binaryInterval : opts.interval;
16112
- closer = setFsWatchFileListener(path34, absolutePath, options, {
16173
+ closer = setFsWatchFileListener(path37, absolutePath, options, {
16113
16174
  listener,
16114
16175
  rawEmitter: this.fsw._emitRaw
16115
16176
  });
16116
16177
  } else {
16117
- closer = setFsWatchListener(path34, absolutePath, options, {
16178
+ closer = setFsWatchListener(path37, absolutePath, options, {
16118
16179
  listener,
16119
16180
  errHandler: this._boundHandleError,
16120
16181
  rawEmitter: this.fsw._emitRaw
@@ -16136,7 +16197,7 @@ var NodeFsHandler = class {
16136
16197
  let prevStats = stats;
16137
16198
  if (parent.has(basename3))
16138
16199
  return;
16139
- const listener = async (path34, newStats) => {
16200
+ const listener = async (path37, newStats) => {
16140
16201
  if (!this.fsw._throttle(THROTTLE_MODE_WATCH, file, 5))
16141
16202
  return;
16142
16203
  if (!newStats || newStats.mtimeMs === 0) {
@@ -16150,11 +16211,11 @@ var NodeFsHandler = class {
16150
16211
  this.fsw._emit(EV.CHANGE, file, newStats2);
16151
16212
  }
16152
16213
  if ((isMacos || isLinux || isFreeBSD) && prevStats.ino !== newStats2.ino) {
16153
- this.fsw._closeFile(path34);
16214
+ this.fsw._closeFile(path37);
16154
16215
  prevStats = newStats2;
16155
16216
  const closer2 = this._watchWithNodeFs(file, listener);
16156
16217
  if (closer2)
16157
- this.fsw._addPathCloser(path34, closer2);
16218
+ this.fsw._addPathCloser(path37, closer2);
16158
16219
  } else {
16159
16220
  prevStats = newStats2;
16160
16221
  }
@@ -16186,7 +16247,7 @@ var NodeFsHandler = class {
16186
16247
  * @param item basename of this item
16187
16248
  * @returns true if no more processing is needed for this entry.
16188
16249
  */
16189
- async _handleSymlink(entry, directory, path34, item) {
16250
+ async _handleSymlink(entry, directory, path37, item) {
16190
16251
  if (this.fsw.closed) {
16191
16252
  return;
16192
16253
  }
@@ -16196,7 +16257,7 @@ var NodeFsHandler = class {
16196
16257
  this.fsw._incrReadyCount();
16197
16258
  let linkPath;
16198
16259
  try {
16199
- linkPath = await (0, import_promises2.realpath)(path34);
16260
+ linkPath = await (0, import_promises2.realpath)(path37);
16200
16261
  } catch (e) {
16201
16262
  this.fsw._emitReady();
16202
16263
  return true;
@@ -16206,12 +16267,12 @@ var NodeFsHandler = class {
16206
16267
  if (dir.has(item)) {
16207
16268
  if (this.fsw._symlinkPaths.get(full) !== linkPath) {
16208
16269
  this.fsw._symlinkPaths.set(full, linkPath);
16209
- this.fsw._emit(EV.CHANGE, path34, entry.stats);
16270
+ this.fsw._emit(EV.CHANGE, path37, entry.stats);
16210
16271
  }
16211
16272
  } else {
16212
16273
  dir.add(item);
16213
16274
  this.fsw._symlinkPaths.set(full, linkPath);
16214
- this.fsw._emit(EV.ADD, path34, entry.stats);
16275
+ this.fsw._emit(EV.ADD, path37, entry.stats);
16215
16276
  }
16216
16277
  this.fsw._emitReady();
16217
16278
  return true;
@@ -16240,9 +16301,9 @@ var NodeFsHandler = class {
16240
16301
  return;
16241
16302
  }
16242
16303
  const item = entry.path;
16243
- let path34 = sysPath.join(directory, item);
16304
+ let path37 = sysPath.join(directory, item);
16244
16305
  current.add(item);
16245
- if (entry.stats.isSymbolicLink() && await this._handleSymlink(entry, directory, path34, item)) {
16306
+ if (entry.stats.isSymbolicLink() && await this._handleSymlink(entry, directory, path37, item)) {
16246
16307
  return;
16247
16308
  }
16248
16309
  if (this.fsw.closed) {
@@ -16251,8 +16312,8 @@ var NodeFsHandler = class {
16251
16312
  }
16252
16313
  if (item === target || !target && !previous.has(item)) {
16253
16314
  this.fsw._incrReadyCount();
16254
- path34 = sysPath.join(dir, sysPath.relative(dir, path34));
16255
- this._addToNodeFs(path34, initialAdd, wh, depth + 1);
16315
+ path37 = sysPath.join(dir, sysPath.relative(dir, path37));
16316
+ this._addToNodeFs(path37, initialAdd, wh, depth + 1);
16256
16317
  }
16257
16318
  }).on(EV.ERROR, this._boundHandleError);
16258
16319
  return new Promise((resolve3, reject) => {
@@ -16321,13 +16382,13 @@ var NodeFsHandler = class {
16321
16382
  * @param depth Child path actually targeted for watch
16322
16383
  * @param target Child path actually targeted for watch
16323
16384
  */
16324
- async _addToNodeFs(path34, initialAdd, priorWh, depth, target) {
16385
+ async _addToNodeFs(path37, initialAdd, priorWh, depth, target) {
16325
16386
  const ready = this.fsw._emitReady;
16326
- if (this.fsw._isIgnored(path34) || this.fsw.closed) {
16387
+ if (this.fsw._isIgnored(path37) || this.fsw.closed) {
16327
16388
  ready();
16328
16389
  return false;
16329
16390
  }
16330
- const wh = this.fsw._getWatchHelpers(path34);
16391
+ const wh = this.fsw._getWatchHelpers(path37);
16331
16392
  if (priorWh) {
16332
16393
  wh.filterPath = (entry) => priorWh.filterPath(entry);
16333
16394
  wh.filterDir = (entry) => priorWh.filterDir(entry);
@@ -16343,8 +16404,8 @@ var NodeFsHandler = class {
16343
16404
  const follow = this.fsw.options.followSymlinks;
16344
16405
  let closer;
16345
16406
  if (stats.isDirectory()) {
16346
- const absPath = sysPath.resolve(path34);
16347
- 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;
16348
16409
  if (this.fsw.closed)
16349
16410
  return;
16350
16411
  closer = await this._handleDir(wh.watchPath, stats, initialAdd, depth, target, wh, targetPath);
@@ -16354,29 +16415,29 @@ var NodeFsHandler = class {
16354
16415
  this.fsw._symlinkPaths.set(absPath, targetPath);
16355
16416
  }
16356
16417
  } else if (stats.isSymbolicLink()) {
16357
- const targetPath = follow ? await (0, import_promises2.realpath)(path34) : path34;
16418
+ const targetPath = follow ? await (0, import_promises2.realpath)(path37) : path37;
16358
16419
  if (this.fsw.closed)
16359
16420
  return;
16360
16421
  const parent = sysPath.dirname(wh.watchPath);
16361
16422
  this.fsw._getWatchedDir(parent).add(wh.watchPath);
16362
16423
  this.fsw._emit(EV.ADD, wh.watchPath, stats);
16363
- closer = await this._handleDir(parent, stats, initialAdd, depth, path34, wh, targetPath);
16424
+ closer = await this._handleDir(parent, stats, initialAdd, depth, path37, wh, targetPath);
16364
16425
  if (this.fsw.closed)
16365
16426
  return;
16366
16427
  if (targetPath !== void 0) {
16367
- this.fsw._symlinkPaths.set(sysPath.resolve(path34), targetPath);
16428
+ this.fsw._symlinkPaths.set(sysPath.resolve(path37), targetPath);
16368
16429
  }
16369
16430
  } else {
16370
16431
  closer = this._handleFile(wh.watchPath, stats, initialAdd);
16371
16432
  }
16372
16433
  ready();
16373
16434
  if (closer)
16374
- this.fsw._addPathCloser(path34, closer);
16435
+ this.fsw._addPathCloser(path37, closer);
16375
16436
  return false;
16376
16437
  } catch (error) {
16377
16438
  if (this.fsw._handleError(error)) {
16378
16439
  ready();
16379
- return path34;
16440
+ return path37;
16380
16441
  }
16381
16442
  }
16382
16443
  }
@@ -16419,26 +16480,26 @@ function createPattern(matcher) {
16419
16480
  }
16420
16481
  return () => false;
16421
16482
  }
16422
- function normalizePath(path34) {
16423
- if (typeof path34 !== "string")
16483
+ function normalizePath(path37) {
16484
+ if (typeof path37 !== "string")
16424
16485
  throw new Error("string expected");
16425
- path34 = sysPath2.normalize(path34);
16426
- path34 = path34.replace(/\\/g, "/");
16486
+ path37 = sysPath2.normalize(path37);
16487
+ path37 = path37.replace(/\\/g, "/");
16427
16488
  let prepend = false;
16428
- if (path34.startsWith("//"))
16489
+ if (path37.startsWith("//"))
16429
16490
  prepend = true;
16430
16491
  const DOUBLE_SLASH_RE2 = /\/\//;
16431
- while (path34.match(DOUBLE_SLASH_RE2))
16432
- path34 = path34.replace(DOUBLE_SLASH_RE2, "/");
16492
+ while (path37.match(DOUBLE_SLASH_RE2))
16493
+ path37 = path37.replace(DOUBLE_SLASH_RE2, "/");
16433
16494
  if (prepend)
16434
- path34 = "/" + path34;
16435
- return path34;
16495
+ path37 = "/" + path37;
16496
+ return path37;
16436
16497
  }
16437
16498
  function matchPatterns(patterns, testString, stats) {
16438
- const path34 = normalizePath(testString);
16499
+ const path37 = normalizePath(testString);
16439
16500
  for (let index = 0; index < patterns.length; index++) {
16440
16501
  const pattern = patterns[index];
16441
- if (pattern(path34, stats)) {
16502
+ if (pattern(path37, stats)) {
16442
16503
  return true;
16443
16504
  }
16444
16505
  }
@@ -16478,19 +16539,19 @@ var toUnix = (string) => {
16478
16539
  }
16479
16540
  return str;
16480
16541
  };
16481
- var normalizePathToUnix = (path34) => toUnix(sysPath2.normalize(toUnix(path34)));
16482
- var normalizeIgnored = (cwd = "") => (path34) => {
16483
- if (typeof path34 === "string") {
16484
- 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));
16485
16546
  } else {
16486
- return path34;
16547
+ return path37;
16487
16548
  }
16488
16549
  };
16489
- var getAbsolutePath = (path34, cwd) => {
16490
- if (sysPath2.isAbsolute(path34)) {
16491
- return path34;
16550
+ var getAbsolutePath = (path37, cwd) => {
16551
+ if (sysPath2.isAbsolute(path37)) {
16552
+ return path37;
16492
16553
  }
16493
- return sysPath2.join(cwd, path34);
16554
+ return sysPath2.join(cwd, path37);
16494
16555
  };
16495
16556
  var EMPTY_SET = Object.freeze(/* @__PURE__ */ new Set());
16496
16557
  var DirEntry = class {
@@ -16545,10 +16606,10 @@ var DirEntry = class {
16545
16606
  var STAT_METHOD_F = "stat";
16546
16607
  var STAT_METHOD_L = "lstat";
16547
16608
  var WatchHelper = class {
16548
- constructor(path34, follow, fsw) {
16609
+ constructor(path37, follow, fsw) {
16549
16610
  this.fsw = fsw;
16550
- const watchPath = path34;
16551
- this.path = path34 = path34.replace(REPLACER_RE, "");
16611
+ const watchPath = path37;
16612
+ this.path = path37 = path37.replace(REPLACER_RE, "");
16552
16613
  this.watchPath = watchPath;
16553
16614
  this.fullWatchPath = sysPath2.resolve(watchPath);
16554
16615
  this.dirParts = [];
@@ -16670,20 +16731,20 @@ var FSWatcher = class extends import_events.EventEmitter {
16670
16731
  this._closePromise = void 0;
16671
16732
  let paths = unifyPaths(paths_);
16672
16733
  if (cwd) {
16673
- paths = paths.map((path34) => {
16674
- const absPath = getAbsolutePath(path34, cwd);
16734
+ paths = paths.map((path37) => {
16735
+ const absPath = getAbsolutePath(path37, cwd);
16675
16736
  return absPath;
16676
16737
  });
16677
16738
  }
16678
- paths.forEach((path34) => {
16679
- this._removeIgnoredPath(path34);
16739
+ paths.forEach((path37) => {
16740
+ this._removeIgnoredPath(path37);
16680
16741
  });
16681
16742
  this._userIgnored = void 0;
16682
16743
  if (!this._readyCount)
16683
16744
  this._readyCount = 0;
16684
16745
  this._readyCount += paths.length;
16685
- Promise.all(paths.map(async (path34) => {
16686
- 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);
16687
16748
  if (res)
16688
16749
  this._emitReady();
16689
16750
  return res;
@@ -16705,17 +16766,17 @@ var FSWatcher = class extends import_events.EventEmitter {
16705
16766
  return this;
16706
16767
  const paths = unifyPaths(paths_);
16707
16768
  const { cwd } = this.options;
16708
- paths.forEach((path34) => {
16709
- if (!sysPath2.isAbsolute(path34) && !this._closers.has(path34)) {
16769
+ paths.forEach((path37) => {
16770
+ if (!sysPath2.isAbsolute(path37) && !this._closers.has(path37)) {
16710
16771
  if (cwd)
16711
- path34 = sysPath2.join(cwd, path34);
16712
- path34 = sysPath2.resolve(path34);
16772
+ path37 = sysPath2.join(cwd, path37);
16773
+ path37 = sysPath2.resolve(path37);
16713
16774
  }
16714
- this._closePath(path34);
16715
- this._addIgnoredPath(path34);
16716
- if (this._watched.has(path34)) {
16775
+ this._closePath(path37);
16776
+ this._addIgnoredPath(path37);
16777
+ if (this._watched.has(path37)) {
16717
16778
  this._addIgnoredPath({
16718
- path: path34,
16779
+ path: path37,
16719
16780
  recursive: true
16720
16781
  });
16721
16782
  }
@@ -16779,38 +16840,38 @@ var FSWatcher = class extends import_events.EventEmitter {
16779
16840
  * @param stats arguments to be passed with event
16780
16841
  * @returns the error if defined, otherwise the value of the FSWatcher instance's `closed` flag
16781
16842
  */
16782
- async _emit(event, path34, stats) {
16843
+ async _emit(event, path37, stats) {
16783
16844
  if (this.closed)
16784
16845
  return;
16785
16846
  const opts = this.options;
16786
16847
  if (isWindows)
16787
- path34 = sysPath2.normalize(path34);
16848
+ path37 = sysPath2.normalize(path37);
16788
16849
  if (opts.cwd)
16789
- path34 = sysPath2.relative(opts.cwd, path34);
16790
- const args = [path34];
16850
+ path37 = sysPath2.relative(opts.cwd, path37);
16851
+ const args = [path37];
16791
16852
  if (stats != null)
16792
16853
  args.push(stats);
16793
16854
  const awf = opts.awaitWriteFinish;
16794
16855
  let pw;
16795
- if (awf && (pw = this._pendingWrites.get(path34))) {
16856
+ if (awf && (pw = this._pendingWrites.get(path37))) {
16796
16857
  pw.lastChange = /* @__PURE__ */ new Date();
16797
16858
  return this;
16798
16859
  }
16799
16860
  if (opts.atomic) {
16800
16861
  if (event === EVENTS.UNLINK) {
16801
- this._pendingUnlinks.set(path34, [event, ...args]);
16862
+ this._pendingUnlinks.set(path37, [event, ...args]);
16802
16863
  setTimeout(() => {
16803
- this._pendingUnlinks.forEach((entry, path35) => {
16864
+ this._pendingUnlinks.forEach((entry, path38) => {
16804
16865
  this.emit(...entry);
16805
16866
  this.emit(EVENTS.ALL, ...entry);
16806
- this._pendingUnlinks.delete(path35);
16867
+ this._pendingUnlinks.delete(path38);
16807
16868
  });
16808
16869
  }, typeof opts.atomic === "number" ? opts.atomic : 100);
16809
16870
  return this;
16810
16871
  }
16811
- if (event === EVENTS.ADD && this._pendingUnlinks.has(path34)) {
16872
+ if (event === EVENTS.ADD && this._pendingUnlinks.has(path37)) {
16812
16873
  event = EVENTS.CHANGE;
16813
- this._pendingUnlinks.delete(path34);
16874
+ this._pendingUnlinks.delete(path37);
16814
16875
  }
16815
16876
  }
16816
16877
  if (awf && (event === EVENTS.ADD || event === EVENTS.CHANGE) && this._readyEmitted) {
@@ -16828,16 +16889,16 @@ var FSWatcher = class extends import_events.EventEmitter {
16828
16889
  this.emitWithAll(event, args);
16829
16890
  }
16830
16891
  };
16831
- this._awaitWriteFinish(path34, awf.stabilityThreshold, event, awfEmit);
16892
+ this._awaitWriteFinish(path37, awf.stabilityThreshold, event, awfEmit);
16832
16893
  return this;
16833
16894
  }
16834
16895
  if (event === EVENTS.CHANGE) {
16835
- const isThrottled = !this._throttle(EVENTS.CHANGE, path34, 50);
16896
+ const isThrottled = !this._throttle(EVENTS.CHANGE, path37, 50);
16836
16897
  if (isThrottled)
16837
16898
  return this;
16838
16899
  }
16839
16900
  if (opts.alwaysStat && stats === void 0 && (event === EVENTS.ADD || event === EVENTS.ADD_DIR || event === EVENTS.CHANGE)) {
16840
- const fullPath = opts.cwd ? sysPath2.join(opts.cwd, path34) : path34;
16901
+ const fullPath = opts.cwd ? sysPath2.join(opts.cwd, path37) : path37;
16841
16902
  let stats2;
16842
16903
  try {
16843
16904
  stats2 = await (0, import_promises3.stat)(fullPath);
@@ -16868,23 +16929,23 @@ var FSWatcher = class extends import_events.EventEmitter {
16868
16929
  * @param timeout duration of time to suppress duplicate actions
16869
16930
  * @returns tracking object or false if action should be suppressed
16870
16931
  */
16871
- _throttle(actionType, path34, timeout) {
16932
+ _throttle(actionType, path37, timeout) {
16872
16933
  if (!this._throttled.has(actionType)) {
16873
16934
  this._throttled.set(actionType, /* @__PURE__ */ new Map());
16874
16935
  }
16875
16936
  const action = this._throttled.get(actionType);
16876
16937
  if (!action)
16877
16938
  throw new Error("invalid throttle");
16878
- const actionPath = action.get(path34);
16939
+ const actionPath = action.get(path37);
16879
16940
  if (actionPath) {
16880
16941
  actionPath.count++;
16881
16942
  return false;
16882
16943
  }
16883
16944
  let timeoutObject;
16884
16945
  const clear = () => {
16885
- const item = action.get(path34);
16946
+ const item = action.get(path37);
16886
16947
  const count = item ? item.count : 0;
16887
- action.delete(path34);
16948
+ action.delete(path37);
16888
16949
  clearTimeout(timeoutObject);
16889
16950
  if (item)
16890
16951
  clearTimeout(item.timeoutObject);
@@ -16892,7 +16953,7 @@ var FSWatcher = class extends import_events.EventEmitter {
16892
16953
  };
16893
16954
  timeoutObject = setTimeout(clear, timeout);
16894
16955
  const thr = { timeoutObject, clear, count: 0 };
16895
- action.set(path34, thr);
16956
+ action.set(path37, thr);
16896
16957
  return thr;
16897
16958
  }
16898
16959
  _incrReadyCount() {
@@ -16906,44 +16967,44 @@ var FSWatcher = class extends import_events.EventEmitter {
16906
16967
  * @param event
16907
16968
  * @param awfEmit Callback to be called when ready for event to be emitted.
16908
16969
  */
16909
- _awaitWriteFinish(path34, threshold, event, awfEmit) {
16970
+ _awaitWriteFinish(path37, threshold, event, awfEmit) {
16910
16971
  const awf = this.options.awaitWriteFinish;
16911
16972
  if (typeof awf !== "object")
16912
16973
  return;
16913
16974
  const pollInterval = awf.pollInterval;
16914
16975
  let timeoutHandler;
16915
- let fullPath = path34;
16916
- if (this.options.cwd && !sysPath2.isAbsolute(path34)) {
16917
- 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);
16918
16979
  }
16919
16980
  const now = /* @__PURE__ */ new Date();
16920
16981
  const writes = this._pendingWrites;
16921
16982
  function awaitWriteFinishFn(prevStat) {
16922
16983
  (0, import_fs24.stat)(fullPath, (err, curStat) => {
16923
- if (err || !writes.has(path34)) {
16984
+ if (err || !writes.has(path37)) {
16924
16985
  if (err && err.code !== "ENOENT")
16925
16986
  awfEmit(err);
16926
16987
  return;
16927
16988
  }
16928
16989
  const now2 = Number(/* @__PURE__ */ new Date());
16929
16990
  if (prevStat && curStat.size !== prevStat.size) {
16930
- writes.get(path34).lastChange = now2;
16991
+ writes.get(path37).lastChange = now2;
16931
16992
  }
16932
- const pw = writes.get(path34);
16993
+ const pw = writes.get(path37);
16933
16994
  const df = now2 - pw.lastChange;
16934
16995
  if (df >= threshold) {
16935
- writes.delete(path34);
16996
+ writes.delete(path37);
16936
16997
  awfEmit(void 0, curStat);
16937
16998
  } else {
16938
16999
  timeoutHandler = setTimeout(awaitWriteFinishFn, pollInterval, curStat);
16939
17000
  }
16940
17001
  });
16941
17002
  }
16942
- if (!writes.has(path34)) {
16943
- writes.set(path34, {
17003
+ if (!writes.has(path37)) {
17004
+ writes.set(path37, {
16944
17005
  lastChange: now,
16945
17006
  cancelWait: () => {
16946
- writes.delete(path34);
17007
+ writes.delete(path37);
16947
17008
  clearTimeout(timeoutHandler);
16948
17009
  return event;
16949
17010
  }
@@ -16954,8 +17015,8 @@ var FSWatcher = class extends import_events.EventEmitter {
16954
17015
  /**
16955
17016
  * Determines whether user has asked to ignore this path.
16956
17017
  */
16957
- _isIgnored(path34, stats) {
16958
- if (this.options.atomic && DOT_RE.test(path34))
17018
+ _isIgnored(path37, stats) {
17019
+ if (this.options.atomic && DOT_RE.test(path37))
16959
17020
  return true;
16960
17021
  if (!this._userIgnored) {
16961
17022
  const { cwd } = this.options;
@@ -16965,17 +17026,17 @@ var FSWatcher = class extends import_events.EventEmitter {
16965
17026
  const list = [...ignoredPaths.map(normalizeIgnored(cwd)), ...ignored];
16966
17027
  this._userIgnored = anymatch(list, void 0);
16967
17028
  }
16968
- return this._userIgnored(path34, stats);
17029
+ return this._userIgnored(path37, stats);
16969
17030
  }
16970
- _isntIgnored(path34, stat4) {
16971
- return !this._isIgnored(path34, stat4);
17031
+ _isntIgnored(path37, stat4) {
17032
+ return !this._isIgnored(path37, stat4);
16972
17033
  }
16973
17034
  /**
16974
17035
  * Provides a set of common helpers and properties relating to symlink handling.
16975
17036
  * @param path file or directory pattern being watched
16976
17037
  */
16977
- _getWatchHelpers(path34) {
16978
- return new WatchHelper(path34, this.options.followSymlinks, this);
17038
+ _getWatchHelpers(path37) {
17039
+ return new WatchHelper(path37, this.options.followSymlinks, this);
16979
17040
  }
16980
17041
  // Directory helpers
16981
17042
  // -----------------
@@ -17007,63 +17068,63 @@ var FSWatcher = class extends import_events.EventEmitter {
17007
17068
  * @param item base path of item/directory
17008
17069
  */
17009
17070
  _remove(directory, item, isDirectory) {
17010
- const path34 = sysPath2.join(directory, item);
17011
- const fullPath = sysPath2.resolve(path34);
17012
- isDirectory = isDirectory != null ? isDirectory : this._watched.has(path34) || this._watched.has(fullPath);
17013
- 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))
17014
17075
  return;
17015
17076
  if (!isDirectory && this._watched.size === 1) {
17016
17077
  this.add(directory, item, true);
17017
17078
  }
17018
- const wp = this._getWatchedDir(path34);
17079
+ const wp = this._getWatchedDir(path37);
17019
17080
  const nestedDirectoryChildren = wp.getChildren();
17020
- nestedDirectoryChildren.forEach((nested) => this._remove(path34, nested));
17081
+ nestedDirectoryChildren.forEach((nested) => this._remove(path37, nested));
17021
17082
  const parent = this._getWatchedDir(directory);
17022
17083
  const wasTracked = parent.has(item);
17023
17084
  parent.remove(item);
17024
17085
  if (this._symlinkPaths.has(fullPath)) {
17025
17086
  this._symlinkPaths.delete(fullPath);
17026
17087
  }
17027
- let relPath = path34;
17088
+ let relPath = path37;
17028
17089
  if (this.options.cwd)
17029
- relPath = sysPath2.relative(this.options.cwd, path34);
17090
+ relPath = sysPath2.relative(this.options.cwd, path37);
17030
17091
  if (this.options.awaitWriteFinish && this._pendingWrites.has(relPath)) {
17031
17092
  const event = this._pendingWrites.get(relPath).cancelWait();
17032
17093
  if (event === EVENTS.ADD)
17033
17094
  return;
17034
17095
  }
17035
- this._watched.delete(path34);
17096
+ this._watched.delete(path37);
17036
17097
  this._watched.delete(fullPath);
17037
17098
  const eventName = isDirectory ? EVENTS.UNLINK_DIR : EVENTS.UNLINK;
17038
- if (wasTracked && !this._isIgnored(path34))
17039
- this._emit(eventName, path34);
17040
- this._closePath(path34);
17099
+ if (wasTracked && !this._isIgnored(path37))
17100
+ this._emit(eventName, path37);
17101
+ this._closePath(path37);
17041
17102
  }
17042
17103
  /**
17043
17104
  * Closes all watchers for a path
17044
17105
  */
17045
- _closePath(path34) {
17046
- this._closeFile(path34);
17047
- const dir = sysPath2.dirname(path34);
17048
- 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));
17049
17110
  }
17050
17111
  /**
17051
17112
  * Closes only file-specific watchers
17052
17113
  */
17053
- _closeFile(path34) {
17054
- const closers = this._closers.get(path34);
17114
+ _closeFile(path37) {
17115
+ const closers = this._closers.get(path37);
17055
17116
  if (!closers)
17056
17117
  return;
17057
17118
  closers.forEach((closer) => closer());
17058
- this._closers.delete(path34);
17119
+ this._closers.delete(path37);
17059
17120
  }
17060
- _addPathCloser(path34, closer) {
17121
+ _addPathCloser(path37, closer) {
17061
17122
  if (!closer)
17062
17123
  return;
17063
- let list = this._closers.get(path34);
17124
+ let list = this._closers.get(path37);
17064
17125
  if (!list) {
17065
17126
  list = [];
17066
- this._closers.set(path34, list);
17127
+ this._closers.set(path37, list);
17067
17128
  }
17068
17129
  list.push(closer);
17069
17130
  }
@@ -17433,11 +17494,11 @@ function createStrictRateLimiterFromConfig(config) {
17433
17494
  }
17434
17495
 
17435
17496
  // modules/server/middleware/auto-rate-limit.ts
17436
- function matchesStrictPattern(path34, patterns) {
17497
+ function matchesStrictPattern(path37, patterns) {
17437
17498
  for (const pattern of patterns) {
17438
17499
  const regexPattern = pattern.replace(/\*\*/g, ".*").replace(/\*/g, "[^/]*").replace(/\//g, "\\/");
17439
17500
  const regex = new RegExp(`^${regexPattern}$`);
17440
- if (regex.test(path34)) {
17501
+ if (regex.test(path37)) {
17441
17502
  return true;
17442
17503
  }
17443
17504
  }
@@ -17627,9 +17688,441 @@ async function handleApiRequest(options) {
17627
17688
  }
17628
17689
  }
17629
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
+
17630
18123
  // modules/server/routes.ts
17631
18124
  init_globals();
17632
- var import_path35 = __toESM(require("path"));
18125
+ var import_path38 = __toESM(require("path"));
17633
18126
  var cachedRewriteLoader = null;
17634
18127
  var cachedProjectRoot = null;
17635
18128
  var cachedIsDev = null;
@@ -17657,10 +18150,20 @@ function setupRoutes(options) {
17657
18150
  } = options;
17658
18151
  const routeChunks = routeLoader.loadRouteChunks();
17659
18152
  const rewriteLoader = getRewriteLoader(projectRoot, isDev);
17660
- const ssgOutDir = import_path35.default.join(
17661
- 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),
17662
18155
  "ssg"
17663
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
+ }
17664
18167
  app.all("/api/*", async (req, res) => {
17665
18168
  const apiRoutes = isDev && getRoutes ? (await getRoutes()).apiRoutes : initialApiRoutes;
17666
18169
  const serverConfig = await getServerConfig(projectRoot);
@@ -18707,7 +19210,7 @@ var import_cors = __toESM(require("cors"));
18707
19210
  var import_helmet = __toESM(require("helmet"));
18708
19211
  var import_cookie_parser = __toESM(require("cookie-parser"));
18709
19212
  var import_compression = __toESM(require("compression"));
18710
- var import_crypto = __toESM(require("crypto"));
19213
+ var import_crypto2 = __toESM(require("crypto"));
18711
19214
  var setupApplication = async ({
18712
19215
  projectRoot
18713
19216
  }) => {
@@ -18816,7 +19319,7 @@ var setupApplication = async ({
18816
19319
  if (process.env.NODE_ENV !== "development" && security?.contentSecurityPolicy !== false) {
18817
19320
  app.use(
18818
19321
  (req, res, next) => {
18819
- const nonce = import_crypto.default.randomBytes(16).toString("base64");
19322
+ const nonce = import_crypto2.default.randomBytes(16).toString("base64");
18820
19323
  res.locals.nonce = nonce;
18821
19324
  next();
18822
19325
  }
@@ -18887,8 +19390,8 @@ var setupApplication = async ({
18887
19390
 
18888
19391
  // src/server.ts
18889
19392
  var import_dotenv2 = __toESM(require("dotenv"));
18890
- var envPath = import_path36.default.join(process.cwd(), ".env");
18891
- if (import_fs26.default.existsSync(envPath)) {
19393
+ var envPath = import_path39.default.join(process.cwd(), ".env");
19394
+ if (import_fs28.default.existsSync(envPath)) {
18892
19395
  import_dotenv2.default.config({ path: envPath });
18893
19396
  } else {
18894
19397
  import_dotenv2.default.config();
@@ -18909,8 +19412,8 @@ async function startServer(options = {}) {
18909
19412
  }
18910
19413
  const port = options.port ?? (process.env.PORT ? parseInt(process.env.PORT, 10) : void 0) ?? config.server.port;
18911
19414
  const host = process.env.HOST ?? (!isDev ? "0.0.0.0" : void 0) ?? config.server.host;
18912
- const appDir = options.appDir ?? (isDev ? getAppDir(projectRoot, config) : import_path36.default.join(getBuildDir(projectRoot, config), "server"));
18913
- 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)) {
18914
19417
  logger4.error("Compiled directory not found", void 0, {
18915
19418
  buildDir: config.directories.build,
18916
19419
  appDir,
@@ -19106,7 +19609,7 @@ async function run() {
19106
19609
  }
19107
19610
  const args = parseArgs(argv.slice(1));
19108
19611
  const projectRoot = import_process.default.cwd();
19109
- const appDir = import_path37.default.resolve(projectRoot, args.appDir || "app");
19612
+ const appDir = import_path40.default.resolve(projectRoot, args.appDir || "app");
19110
19613
  const port = typeof args.port === "string" && args.port.trim().length > 0 ? Number(args.port) : 3e3;
19111
19614
  switch (command) {
19112
19615
  case "dev": {