@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.mjs CHANGED
@@ -9957,7 +9957,7 @@ var require_built3 = __commonJS({
9957
9957
  });
9958
9958
 
9959
9959
  // modules/cli/index.ts
9960
- import path33 from "path";
9960
+ import path36 from "path";
9961
9961
  import process2 from "process";
9962
9962
 
9963
9963
  // modules/build/index.ts
@@ -11746,6 +11746,7 @@ async function processRewrites(urlPath, compiledRewrites, req) {
11746
11746
  const normalizedPath = urlPath.replace(/\/$/, "") || "/";
11747
11747
  if (normalizedPath.startsWith("/static/") || // Static assets (client.js, client.css, etc.)
11748
11748
  normalizedPath.startsWith("/__fw/") || // Framework internal routes (hot reload, etc.)
11749
+ normalizedPath.startsWith("/_loly/") || // Framework internal routes (image optimization, etc.)
11749
11750
  normalizedPath === "/favicon.ico" || // Favicon
11750
11751
  normalizedPath.startsWith("/wss/")) {
11751
11752
  if (process.env.NODE_ENV === "development") {
@@ -12950,7 +12951,7 @@ function createDocumentTree(options) {
12950
12951
  }),
12951
12952
  ...extraMetaTags,
12952
12953
  ...linkTags,
12953
- ...entrypointFiles.length > 0 ? entrypointFiles.slice(0, -1).map(
12954
+ ...entrypointFiles.length > 0 ? entrypointFiles.map(
12954
12955
  (file) => React.createElement("link", {
12955
12956
  key: `preload-${file}`,
12956
12957
  rel: "preload",
@@ -12971,6 +12972,13 @@ function createDocumentTree(options) {
12971
12972
  href: faviconPath,
12972
12973
  type: faviconType || (faviconPath.endsWith(".ico") ? "image/x-icon" : "image/png")
12973
12974
  }),
12975
+ // Preload CSS para evitar bloqueo de renderizado
12976
+ React.createElement("link", {
12977
+ key: "preload-css",
12978
+ rel: "preload",
12979
+ href: clientCssPath,
12980
+ as: "style"
12981
+ }),
12974
12982
  React.createElement("link", {
12975
12983
  rel: "stylesheet",
12976
12984
  href: clientCssPath
@@ -13172,12 +13180,12 @@ var DEFAULT_IGNORED_PATHS = [
13172
13180
  /^\/sockjs-node/
13173
13181
  // Hot reload websocket
13174
13182
  ];
13175
- function shouldIgnorePath(path34, ignoredPaths) {
13183
+ function shouldIgnorePath(path37, ignoredPaths) {
13176
13184
  return ignoredPaths.some((pattern) => {
13177
13185
  if (typeof pattern === "string") {
13178
- return path34 === pattern || path34.startsWith(pattern);
13186
+ return path37 === pattern || path37.startsWith(pattern);
13179
13187
  }
13180
- return pattern.test(path34);
13188
+ return pattern.test(path37);
13181
13189
  });
13182
13190
  }
13183
13191
  function requestLoggerMiddleware(options = {}) {
@@ -15140,7 +15148,20 @@ var DEFAULT_CONFIG2 = {
15140
15148
  ssr: true,
15141
15149
  ssg: true
15142
15150
  },
15143
- plugins: []
15151
+ plugins: [],
15152
+ images: {
15153
+ remotePatterns: [],
15154
+ domains: [],
15155
+ deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
15156
+ imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
15157
+ formats: ["image/webp", "image/avif"],
15158
+ quality: 75,
15159
+ minimumCacheTTL: 60,
15160
+ dangerouslyAllowSVG: false,
15161
+ contentSecurityPolicy: "default-src 'self'; script-src 'none'; sandbox;",
15162
+ maxWidth: 3840,
15163
+ maxHeight: 3840
15164
+ }
15144
15165
  };
15145
15166
  function deepMerge(target, source) {
15146
15167
  const result = { ...target };
@@ -15257,6 +15278,53 @@ function validateConfig(config, projectRoot) {
15257
15278
  if (typeof config.rendering.ssg !== "boolean") {
15258
15279
  errors.push("config.rendering.ssg must be a boolean");
15259
15280
  }
15281
+ if (config.images) {
15282
+ if (config.images.quality !== void 0) {
15283
+ if (typeof config.images.quality !== "number" || config.images.quality < 1 || config.images.quality > 100) {
15284
+ errors.push("config.images.quality must be a number between 1 and 100");
15285
+ }
15286
+ }
15287
+ if (config.images.minimumCacheTTL !== void 0) {
15288
+ if (typeof config.images.minimumCacheTTL !== "number" || config.images.minimumCacheTTL < 0) {
15289
+ errors.push("config.images.minimumCacheTTL must be a non-negative number");
15290
+ }
15291
+ }
15292
+ if (config.images.deviceSizes) {
15293
+ if (!Array.isArray(config.images.deviceSizes) || config.images.deviceSizes.some((s) => typeof s !== "number" || s <= 0)) {
15294
+ errors.push("config.images.deviceSizes must be an array of positive numbers");
15295
+ }
15296
+ }
15297
+ if (config.images.imageSizes) {
15298
+ if (!Array.isArray(config.images.imageSizes) || config.images.imageSizes.some((s) => typeof s !== "number" || s <= 0)) {
15299
+ errors.push("config.images.imageSizes must be an array of positive numbers");
15300
+ }
15301
+ }
15302
+ if (config.images.formats) {
15303
+ const validFormats = ["image/webp", "image/avif"];
15304
+ if (!Array.isArray(config.images.formats) || config.images.formats.some((f) => !validFormats.includes(f))) {
15305
+ errors.push(`config.images.formats must be an array containing only: ${validFormats.join(", ")}`);
15306
+ }
15307
+ }
15308
+ if (config.images.remotePatterns) {
15309
+ if (!Array.isArray(config.images.remotePatterns)) {
15310
+ errors.push("config.images.remotePatterns must be an array");
15311
+ } else {
15312
+ config.images.remotePatterns.forEach((pattern, idx) => {
15313
+ if (!pattern.hostname || typeof pattern.hostname !== "string") {
15314
+ errors.push(`config.images.remotePatterns[${idx}].hostname must be a non-empty string`);
15315
+ }
15316
+ if (pattern.protocol && !["http", "https"].includes(pattern.protocol)) {
15317
+ errors.push(`config.images.remotePatterns[${idx}].protocol must be 'http' or 'https'`);
15318
+ }
15319
+ });
15320
+ }
15321
+ }
15322
+ if (config.images.domains) {
15323
+ if (!Array.isArray(config.images.domains) || config.images.domains.some((d) => typeof d !== "string")) {
15324
+ errors.push("config.images.domains must be an array of strings");
15325
+ }
15326
+ }
15327
+ }
15260
15328
  if (errors.length > 0) {
15261
15329
  const errorMessage = [
15262
15330
  "\u274C Configuration validation failed:",
@@ -15390,8 +15458,8 @@ async function buildApp(options = {}) {
15390
15458
  }
15391
15459
 
15392
15460
  // src/server.ts
15393
- import fs24 from "fs";
15394
- import path32 from "path";
15461
+ import fs26 from "fs";
15462
+ import path35 from "path";
15395
15463
 
15396
15464
  // modules/server/setup.ts
15397
15465
  import express from "express";
@@ -15474,7 +15542,7 @@ var ReaddirpStream = class extends Readable {
15474
15542
  this._directoryFilter = normalizeFilter(opts.directoryFilter);
15475
15543
  const statMethod = opts.lstat ? lstat : stat;
15476
15544
  if (wantBigintFsStats) {
15477
- this._stat = (path34) => statMethod(path34, { bigint: true });
15545
+ this._stat = (path37) => statMethod(path37, { bigint: true });
15478
15546
  } else {
15479
15547
  this._stat = statMethod;
15480
15548
  }
@@ -15499,8 +15567,8 @@ var ReaddirpStream = class extends Readable {
15499
15567
  const par = this.parent;
15500
15568
  const fil = par && par.files;
15501
15569
  if (fil && fil.length > 0) {
15502
- const { path: path34, depth } = par;
15503
- const slice = fil.splice(0, batch).map((dirent) => this._formatEntry(dirent, path34));
15570
+ const { path: path37, depth } = par;
15571
+ const slice = fil.splice(0, batch).map((dirent) => this._formatEntry(dirent, path37));
15504
15572
  const awaited = await Promise.all(slice);
15505
15573
  for (const entry of awaited) {
15506
15574
  if (!entry)
@@ -15540,20 +15608,20 @@ var ReaddirpStream = class extends Readable {
15540
15608
  this.reading = false;
15541
15609
  }
15542
15610
  }
15543
- async _exploreDir(path34, depth) {
15611
+ async _exploreDir(path37, depth) {
15544
15612
  let files;
15545
15613
  try {
15546
- files = await readdir(path34, this._rdOptions);
15614
+ files = await readdir(path37, this._rdOptions);
15547
15615
  } catch (error) {
15548
15616
  this._onError(error);
15549
15617
  }
15550
- return { files, depth, path: path34 };
15618
+ return { files, depth, path: path37 };
15551
15619
  }
15552
- async _formatEntry(dirent, path34) {
15620
+ async _formatEntry(dirent, path37) {
15553
15621
  let entry;
15554
15622
  const basename3 = this._isDirent ? dirent.name : dirent;
15555
15623
  try {
15556
- const fullPath = presolve(pjoin(path34, basename3));
15624
+ const fullPath = presolve(pjoin(path37, basename3));
15557
15625
  entry = { path: prelative(this._root, fullPath), fullPath, basename: basename3 };
15558
15626
  entry[this._statsProp] = this._isDirent ? dirent : await this._stat(fullPath);
15559
15627
  } catch (err) {
@@ -15953,16 +16021,16 @@ var delFromSet = (main, prop, item) => {
15953
16021
  };
15954
16022
  var isEmptySet = (val) => val instanceof Set ? val.size === 0 : !val;
15955
16023
  var FsWatchInstances = /* @__PURE__ */ new Map();
15956
- function createFsWatchInstance(path34, options, listener, errHandler, emitRaw) {
16024
+ function createFsWatchInstance(path37, options, listener, errHandler, emitRaw) {
15957
16025
  const handleEvent = (rawEvent, evPath) => {
15958
- listener(path34);
15959
- emitRaw(rawEvent, evPath, { watchedPath: path34 });
15960
- if (evPath && path34 !== evPath) {
15961
- fsWatchBroadcast(sysPath.resolve(path34, evPath), KEY_LISTENERS, sysPath.join(path34, evPath));
16026
+ listener(path37);
16027
+ emitRaw(rawEvent, evPath, { watchedPath: path37 });
16028
+ if (evPath && path37 !== evPath) {
16029
+ fsWatchBroadcast(sysPath.resolve(path37, evPath), KEY_LISTENERS, sysPath.join(path37, evPath));
15962
16030
  }
15963
16031
  };
15964
16032
  try {
15965
- return fs_watch(path34, {
16033
+ return fs_watch(path37, {
15966
16034
  persistent: options.persistent
15967
16035
  }, handleEvent);
15968
16036
  } catch (error) {
@@ -15978,12 +16046,12 @@ var fsWatchBroadcast = (fullPath, listenerType, val1, val2, val3) => {
15978
16046
  listener(val1, val2, val3);
15979
16047
  });
15980
16048
  };
15981
- var setFsWatchListener = (path34, fullPath, options, handlers) => {
16049
+ var setFsWatchListener = (path37, fullPath, options, handlers) => {
15982
16050
  const { listener, errHandler, rawEmitter } = handlers;
15983
16051
  let cont = FsWatchInstances.get(fullPath);
15984
16052
  let watcher;
15985
16053
  if (!options.persistent) {
15986
- watcher = createFsWatchInstance(path34, options, listener, errHandler, rawEmitter);
16054
+ watcher = createFsWatchInstance(path37, options, listener, errHandler, rawEmitter);
15987
16055
  if (!watcher)
15988
16056
  return;
15989
16057
  return watcher.close.bind(watcher);
@@ -15994,7 +16062,7 @@ var setFsWatchListener = (path34, fullPath, options, handlers) => {
15994
16062
  addAndConvert(cont, KEY_RAW, rawEmitter);
15995
16063
  } else {
15996
16064
  watcher = createFsWatchInstance(
15997
- path34,
16065
+ path37,
15998
16066
  options,
15999
16067
  fsWatchBroadcast.bind(null, fullPath, KEY_LISTENERS),
16000
16068
  errHandler,
@@ -16009,7 +16077,7 @@ var setFsWatchListener = (path34, fullPath, options, handlers) => {
16009
16077
  cont.watcherUnusable = true;
16010
16078
  if (isWindows && error.code === "EPERM") {
16011
16079
  try {
16012
- const fd = await open(path34, "r");
16080
+ const fd = await open(path37, "r");
16013
16081
  await fd.close();
16014
16082
  broadcastErr(error);
16015
16083
  } catch (err) {
@@ -16040,7 +16108,7 @@ var setFsWatchListener = (path34, fullPath, options, handlers) => {
16040
16108
  };
16041
16109
  };
16042
16110
  var FsWatchFileInstances = /* @__PURE__ */ new Map();
16043
- var setFsWatchFileListener = (path34, fullPath, options, handlers) => {
16111
+ var setFsWatchFileListener = (path37, fullPath, options, handlers) => {
16044
16112
  const { listener, rawEmitter } = handlers;
16045
16113
  let cont = FsWatchFileInstances.get(fullPath);
16046
16114
  const copts = cont && cont.options;
@@ -16062,7 +16130,7 @@ var setFsWatchFileListener = (path34, fullPath, options, handlers) => {
16062
16130
  });
16063
16131
  const currmtime = curr.mtimeMs;
16064
16132
  if (curr.size !== prev.size || currmtime > prev.mtimeMs || currmtime === 0) {
16065
- foreach(cont.listeners, (listener2) => listener2(path34, curr));
16133
+ foreach(cont.listeners, (listener2) => listener2(path37, curr));
16066
16134
  }
16067
16135
  })
16068
16136
  };
@@ -16090,13 +16158,13 @@ var NodeFsHandler = class {
16090
16158
  * @param listener on fs change
16091
16159
  * @returns closer for the watcher instance
16092
16160
  */
16093
- _watchWithNodeFs(path34, listener) {
16161
+ _watchWithNodeFs(path37, listener) {
16094
16162
  const opts = this.fsw.options;
16095
- const directory = sysPath.dirname(path34);
16096
- const basename3 = sysPath.basename(path34);
16163
+ const directory = sysPath.dirname(path37);
16164
+ const basename3 = sysPath.basename(path37);
16097
16165
  const parent = this.fsw._getWatchedDir(directory);
16098
16166
  parent.add(basename3);
16099
- const absolutePath = sysPath.resolve(path34);
16167
+ const absolutePath = sysPath.resolve(path37);
16100
16168
  const options = {
16101
16169
  persistent: opts.persistent
16102
16170
  };
@@ -16106,12 +16174,12 @@ var NodeFsHandler = class {
16106
16174
  if (opts.usePolling) {
16107
16175
  const enableBin = opts.interval !== opts.binaryInterval;
16108
16176
  options.interval = enableBin && isBinaryPath(basename3) ? opts.binaryInterval : opts.interval;
16109
- closer = setFsWatchFileListener(path34, absolutePath, options, {
16177
+ closer = setFsWatchFileListener(path37, absolutePath, options, {
16110
16178
  listener,
16111
16179
  rawEmitter: this.fsw._emitRaw
16112
16180
  });
16113
16181
  } else {
16114
- closer = setFsWatchListener(path34, absolutePath, options, {
16182
+ closer = setFsWatchListener(path37, absolutePath, options, {
16115
16183
  listener,
16116
16184
  errHandler: this._boundHandleError,
16117
16185
  rawEmitter: this.fsw._emitRaw
@@ -16133,7 +16201,7 @@ var NodeFsHandler = class {
16133
16201
  let prevStats = stats;
16134
16202
  if (parent.has(basename3))
16135
16203
  return;
16136
- const listener = async (path34, newStats) => {
16204
+ const listener = async (path37, newStats) => {
16137
16205
  if (!this.fsw._throttle(THROTTLE_MODE_WATCH, file, 5))
16138
16206
  return;
16139
16207
  if (!newStats || newStats.mtimeMs === 0) {
@@ -16147,11 +16215,11 @@ var NodeFsHandler = class {
16147
16215
  this.fsw._emit(EV.CHANGE, file, newStats2);
16148
16216
  }
16149
16217
  if ((isMacos || isLinux || isFreeBSD) && prevStats.ino !== newStats2.ino) {
16150
- this.fsw._closeFile(path34);
16218
+ this.fsw._closeFile(path37);
16151
16219
  prevStats = newStats2;
16152
16220
  const closer2 = this._watchWithNodeFs(file, listener);
16153
16221
  if (closer2)
16154
- this.fsw._addPathCloser(path34, closer2);
16222
+ this.fsw._addPathCloser(path37, closer2);
16155
16223
  } else {
16156
16224
  prevStats = newStats2;
16157
16225
  }
@@ -16183,7 +16251,7 @@ var NodeFsHandler = class {
16183
16251
  * @param item basename of this item
16184
16252
  * @returns true if no more processing is needed for this entry.
16185
16253
  */
16186
- async _handleSymlink(entry, directory, path34, item) {
16254
+ async _handleSymlink(entry, directory, path37, item) {
16187
16255
  if (this.fsw.closed) {
16188
16256
  return;
16189
16257
  }
@@ -16193,7 +16261,7 @@ var NodeFsHandler = class {
16193
16261
  this.fsw._incrReadyCount();
16194
16262
  let linkPath;
16195
16263
  try {
16196
- linkPath = await fsrealpath(path34);
16264
+ linkPath = await fsrealpath(path37);
16197
16265
  } catch (e) {
16198
16266
  this.fsw._emitReady();
16199
16267
  return true;
@@ -16203,12 +16271,12 @@ var NodeFsHandler = class {
16203
16271
  if (dir.has(item)) {
16204
16272
  if (this.fsw._symlinkPaths.get(full) !== linkPath) {
16205
16273
  this.fsw._symlinkPaths.set(full, linkPath);
16206
- this.fsw._emit(EV.CHANGE, path34, entry.stats);
16274
+ this.fsw._emit(EV.CHANGE, path37, entry.stats);
16207
16275
  }
16208
16276
  } else {
16209
16277
  dir.add(item);
16210
16278
  this.fsw._symlinkPaths.set(full, linkPath);
16211
- this.fsw._emit(EV.ADD, path34, entry.stats);
16279
+ this.fsw._emit(EV.ADD, path37, entry.stats);
16212
16280
  }
16213
16281
  this.fsw._emitReady();
16214
16282
  return true;
@@ -16237,9 +16305,9 @@ var NodeFsHandler = class {
16237
16305
  return;
16238
16306
  }
16239
16307
  const item = entry.path;
16240
- let path34 = sysPath.join(directory, item);
16308
+ let path37 = sysPath.join(directory, item);
16241
16309
  current.add(item);
16242
- if (entry.stats.isSymbolicLink() && await this._handleSymlink(entry, directory, path34, item)) {
16310
+ if (entry.stats.isSymbolicLink() && await this._handleSymlink(entry, directory, path37, item)) {
16243
16311
  return;
16244
16312
  }
16245
16313
  if (this.fsw.closed) {
@@ -16248,8 +16316,8 @@ var NodeFsHandler = class {
16248
16316
  }
16249
16317
  if (item === target || !target && !previous.has(item)) {
16250
16318
  this.fsw._incrReadyCount();
16251
- path34 = sysPath.join(dir, sysPath.relative(dir, path34));
16252
- this._addToNodeFs(path34, initialAdd, wh, depth + 1);
16319
+ path37 = sysPath.join(dir, sysPath.relative(dir, path37));
16320
+ this._addToNodeFs(path37, initialAdd, wh, depth + 1);
16253
16321
  }
16254
16322
  }).on(EV.ERROR, this._boundHandleError);
16255
16323
  return new Promise((resolve3, reject) => {
@@ -16318,13 +16386,13 @@ var NodeFsHandler = class {
16318
16386
  * @param depth Child path actually targeted for watch
16319
16387
  * @param target Child path actually targeted for watch
16320
16388
  */
16321
- async _addToNodeFs(path34, initialAdd, priorWh, depth, target) {
16389
+ async _addToNodeFs(path37, initialAdd, priorWh, depth, target) {
16322
16390
  const ready = this.fsw._emitReady;
16323
- if (this.fsw._isIgnored(path34) || this.fsw.closed) {
16391
+ if (this.fsw._isIgnored(path37) || this.fsw.closed) {
16324
16392
  ready();
16325
16393
  return false;
16326
16394
  }
16327
- const wh = this.fsw._getWatchHelpers(path34);
16395
+ const wh = this.fsw._getWatchHelpers(path37);
16328
16396
  if (priorWh) {
16329
16397
  wh.filterPath = (entry) => priorWh.filterPath(entry);
16330
16398
  wh.filterDir = (entry) => priorWh.filterDir(entry);
@@ -16340,8 +16408,8 @@ var NodeFsHandler = class {
16340
16408
  const follow = this.fsw.options.followSymlinks;
16341
16409
  let closer;
16342
16410
  if (stats.isDirectory()) {
16343
- const absPath = sysPath.resolve(path34);
16344
- const targetPath = follow ? await fsrealpath(path34) : path34;
16411
+ const absPath = sysPath.resolve(path37);
16412
+ const targetPath = follow ? await fsrealpath(path37) : path37;
16345
16413
  if (this.fsw.closed)
16346
16414
  return;
16347
16415
  closer = await this._handleDir(wh.watchPath, stats, initialAdd, depth, target, wh, targetPath);
@@ -16351,29 +16419,29 @@ var NodeFsHandler = class {
16351
16419
  this.fsw._symlinkPaths.set(absPath, targetPath);
16352
16420
  }
16353
16421
  } else if (stats.isSymbolicLink()) {
16354
- const targetPath = follow ? await fsrealpath(path34) : path34;
16422
+ const targetPath = follow ? await fsrealpath(path37) : path37;
16355
16423
  if (this.fsw.closed)
16356
16424
  return;
16357
16425
  const parent = sysPath.dirname(wh.watchPath);
16358
16426
  this.fsw._getWatchedDir(parent).add(wh.watchPath);
16359
16427
  this.fsw._emit(EV.ADD, wh.watchPath, stats);
16360
- closer = await this._handleDir(parent, stats, initialAdd, depth, path34, wh, targetPath);
16428
+ closer = await this._handleDir(parent, stats, initialAdd, depth, path37, wh, targetPath);
16361
16429
  if (this.fsw.closed)
16362
16430
  return;
16363
16431
  if (targetPath !== void 0) {
16364
- this.fsw._symlinkPaths.set(sysPath.resolve(path34), targetPath);
16432
+ this.fsw._symlinkPaths.set(sysPath.resolve(path37), targetPath);
16365
16433
  }
16366
16434
  } else {
16367
16435
  closer = this._handleFile(wh.watchPath, stats, initialAdd);
16368
16436
  }
16369
16437
  ready();
16370
16438
  if (closer)
16371
- this.fsw._addPathCloser(path34, closer);
16439
+ this.fsw._addPathCloser(path37, closer);
16372
16440
  return false;
16373
16441
  } catch (error) {
16374
16442
  if (this.fsw._handleError(error)) {
16375
16443
  ready();
16376
- return path34;
16444
+ return path37;
16377
16445
  }
16378
16446
  }
16379
16447
  }
@@ -16416,26 +16484,26 @@ function createPattern(matcher) {
16416
16484
  }
16417
16485
  return () => false;
16418
16486
  }
16419
- function normalizePath(path34) {
16420
- if (typeof path34 !== "string")
16487
+ function normalizePath(path37) {
16488
+ if (typeof path37 !== "string")
16421
16489
  throw new Error("string expected");
16422
- path34 = sysPath2.normalize(path34);
16423
- path34 = path34.replace(/\\/g, "/");
16490
+ path37 = sysPath2.normalize(path37);
16491
+ path37 = path37.replace(/\\/g, "/");
16424
16492
  let prepend = false;
16425
- if (path34.startsWith("//"))
16493
+ if (path37.startsWith("//"))
16426
16494
  prepend = true;
16427
16495
  const DOUBLE_SLASH_RE2 = /\/\//;
16428
- while (path34.match(DOUBLE_SLASH_RE2))
16429
- path34 = path34.replace(DOUBLE_SLASH_RE2, "/");
16496
+ while (path37.match(DOUBLE_SLASH_RE2))
16497
+ path37 = path37.replace(DOUBLE_SLASH_RE2, "/");
16430
16498
  if (prepend)
16431
- path34 = "/" + path34;
16432
- return path34;
16499
+ path37 = "/" + path37;
16500
+ return path37;
16433
16501
  }
16434
16502
  function matchPatterns(patterns, testString, stats) {
16435
- const path34 = normalizePath(testString);
16503
+ const path37 = normalizePath(testString);
16436
16504
  for (let index = 0; index < patterns.length; index++) {
16437
16505
  const pattern = patterns[index];
16438
- if (pattern(path34, stats)) {
16506
+ if (pattern(path37, stats)) {
16439
16507
  return true;
16440
16508
  }
16441
16509
  }
@@ -16475,19 +16543,19 @@ var toUnix = (string) => {
16475
16543
  }
16476
16544
  return str;
16477
16545
  };
16478
- var normalizePathToUnix = (path34) => toUnix(sysPath2.normalize(toUnix(path34)));
16479
- var normalizeIgnored = (cwd = "") => (path34) => {
16480
- if (typeof path34 === "string") {
16481
- return normalizePathToUnix(sysPath2.isAbsolute(path34) ? path34 : sysPath2.join(cwd, path34));
16546
+ var normalizePathToUnix = (path37) => toUnix(sysPath2.normalize(toUnix(path37)));
16547
+ var normalizeIgnored = (cwd = "") => (path37) => {
16548
+ if (typeof path37 === "string") {
16549
+ return normalizePathToUnix(sysPath2.isAbsolute(path37) ? path37 : sysPath2.join(cwd, path37));
16482
16550
  } else {
16483
- return path34;
16551
+ return path37;
16484
16552
  }
16485
16553
  };
16486
- var getAbsolutePath = (path34, cwd) => {
16487
- if (sysPath2.isAbsolute(path34)) {
16488
- return path34;
16554
+ var getAbsolutePath = (path37, cwd) => {
16555
+ if (sysPath2.isAbsolute(path37)) {
16556
+ return path37;
16489
16557
  }
16490
- return sysPath2.join(cwd, path34);
16558
+ return sysPath2.join(cwd, path37);
16491
16559
  };
16492
16560
  var EMPTY_SET = Object.freeze(/* @__PURE__ */ new Set());
16493
16561
  var DirEntry = class {
@@ -16542,10 +16610,10 @@ var DirEntry = class {
16542
16610
  var STAT_METHOD_F = "stat";
16543
16611
  var STAT_METHOD_L = "lstat";
16544
16612
  var WatchHelper = class {
16545
- constructor(path34, follow, fsw) {
16613
+ constructor(path37, follow, fsw) {
16546
16614
  this.fsw = fsw;
16547
- const watchPath = path34;
16548
- this.path = path34 = path34.replace(REPLACER_RE, "");
16615
+ const watchPath = path37;
16616
+ this.path = path37 = path37.replace(REPLACER_RE, "");
16549
16617
  this.watchPath = watchPath;
16550
16618
  this.fullWatchPath = sysPath2.resolve(watchPath);
16551
16619
  this.dirParts = [];
@@ -16667,20 +16735,20 @@ var FSWatcher = class extends EventEmitter {
16667
16735
  this._closePromise = void 0;
16668
16736
  let paths = unifyPaths(paths_);
16669
16737
  if (cwd) {
16670
- paths = paths.map((path34) => {
16671
- const absPath = getAbsolutePath(path34, cwd);
16738
+ paths = paths.map((path37) => {
16739
+ const absPath = getAbsolutePath(path37, cwd);
16672
16740
  return absPath;
16673
16741
  });
16674
16742
  }
16675
- paths.forEach((path34) => {
16676
- this._removeIgnoredPath(path34);
16743
+ paths.forEach((path37) => {
16744
+ this._removeIgnoredPath(path37);
16677
16745
  });
16678
16746
  this._userIgnored = void 0;
16679
16747
  if (!this._readyCount)
16680
16748
  this._readyCount = 0;
16681
16749
  this._readyCount += paths.length;
16682
- Promise.all(paths.map(async (path34) => {
16683
- const res = await this._nodeFsHandler._addToNodeFs(path34, !_internal, void 0, 0, _origAdd);
16750
+ Promise.all(paths.map(async (path37) => {
16751
+ const res = await this._nodeFsHandler._addToNodeFs(path37, !_internal, void 0, 0, _origAdd);
16684
16752
  if (res)
16685
16753
  this._emitReady();
16686
16754
  return res;
@@ -16702,17 +16770,17 @@ var FSWatcher = class extends EventEmitter {
16702
16770
  return this;
16703
16771
  const paths = unifyPaths(paths_);
16704
16772
  const { cwd } = this.options;
16705
- paths.forEach((path34) => {
16706
- if (!sysPath2.isAbsolute(path34) && !this._closers.has(path34)) {
16773
+ paths.forEach((path37) => {
16774
+ if (!sysPath2.isAbsolute(path37) && !this._closers.has(path37)) {
16707
16775
  if (cwd)
16708
- path34 = sysPath2.join(cwd, path34);
16709
- path34 = sysPath2.resolve(path34);
16776
+ path37 = sysPath2.join(cwd, path37);
16777
+ path37 = sysPath2.resolve(path37);
16710
16778
  }
16711
- this._closePath(path34);
16712
- this._addIgnoredPath(path34);
16713
- if (this._watched.has(path34)) {
16779
+ this._closePath(path37);
16780
+ this._addIgnoredPath(path37);
16781
+ if (this._watched.has(path37)) {
16714
16782
  this._addIgnoredPath({
16715
- path: path34,
16783
+ path: path37,
16716
16784
  recursive: true
16717
16785
  });
16718
16786
  }
@@ -16776,38 +16844,38 @@ var FSWatcher = class extends EventEmitter {
16776
16844
  * @param stats arguments to be passed with event
16777
16845
  * @returns the error if defined, otherwise the value of the FSWatcher instance's `closed` flag
16778
16846
  */
16779
- async _emit(event, path34, stats) {
16847
+ async _emit(event, path37, stats) {
16780
16848
  if (this.closed)
16781
16849
  return;
16782
16850
  const opts = this.options;
16783
16851
  if (isWindows)
16784
- path34 = sysPath2.normalize(path34);
16852
+ path37 = sysPath2.normalize(path37);
16785
16853
  if (opts.cwd)
16786
- path34 = sysPath2.relative(opts.cwd, path34);
16787
- const args = [path34];
16854
+ path37 = sysPath2.relative(opts.cwd, path37);
16855
+ const args = [path37];
16788
16856
  if (stats != null)
16789
16857
  args.push(stats);
16790
16858
  const awf = opts.awaitWriteFinish;
16791
16859
  let pw;
16792
- if (awf && (pw = this._pendingWrites.get(path34))) {
16860
+ if (awf && (pw = this._pendingWrites.get(path37))) {
16793
16861
  pw.lastChange = /* @__PURE__ */ new Date();
16794
16862
  return this;
16795
16863
  }
16796
16864
  if (opts.atomic) {
16797
16865
  if (event === EVENTS.UNLINK) {
16798
- this._pendingUnlinks.set(path34, [event, ...args]);
16866
+ this._pendingUnlinks.set(path37, [event, ...args]);
16799
16867
  setTimeout(() => {
16800
- this._pendingUnlinks.forEach((entry, path35) => {
16868
+ this._pendingUnlinks.forEach((entry, path38) => {
16801
16869
  this.emit(...entry);
16802
16870
  this.emit(EVENTS.ALL, ...entry);
16803
- this._pendingUnlinks.delete(path35);
16871
+ this._pendingUnlinks.delete(path38);
16804
16872
  });
16805
16873
  }, typeof opts.atomic === "number" ? opts.atomic : 100);
16806
16874
  return this;
16807
16875
  }
16808
- if (event === EVENTS.ADD && this._pendingUnlinks.has(path34)) {
16876
+ if (event === EVENTS.ADD && this._pendingUnlinks.has(path37)) {
16809
16877
  event = EVENTS.CHANGE;
16810
- this._pendingUnlinks.delete(path34);
16878
+ this._pendingUnlinks.delete(path37);
16811
16879
  }
16812
16880
  }
16813
16881
  if (awf && (event === EVENTS.ADD || event === EVENTS.CHANGE) && this._readyEmitted) {
@@ -16825,16 +16893,16 @@ var FSWatcher = class extends EventEmitter {
16825
16893
  this.emitWithAll(event, args);
16826
16894
  }
16827
16895
  };
16828
- this._awaitWriteFinish(path34, awf.stabilityThreshold, event, awfEmit);
16896
+ this._awaitWriteFinish(path37, awf.stabilityThreshold, event, awfEmit);
16829
16897
  return this;
16830
16898
  }
16831
16899
  if (event === EVENTS.CHANGE) {
16832
- const isThrottled = !this._throttle(EVENTS.CHANGE, path34, 50);
16900
+ const isThrottled = !this._throttle(EVENTS.CHANGE, path37, 50);
16833
16901
  if (isThrottled)
16834
16902
  return this;
16835
16903
  }
16836
16904
  if (opts.alwaysStat && stats === void 0 && (event === EVENTS.ADD || event === EVENTS.ADD_DIR || event === EVENTS.CHANGE)) {
16837
- const fullPath = opts.cwd ? sysPath2.join(opts.cwd, path34) : path34;
16905
+ const fullPath = opts.cwd ? sysPath2.join(opts.cwd, path37) : path37;
16838
16906
  let stats2;
16839
16907
  try {
16840
16908
  stats2 = await stat3(fullPath);
@@ -16865,23 +16933,23 @@ var FSWatcher = class extends EventEmitter {
16865
16933
  * @param timeout duration of time to suppress duplicate actions
16866
16934
  * @returns tracking object or false if action should be suppressed
16867
16935
  */
16868
- _throttle(actionType, path34, timeout) {
16936
+ _throttle(actionType, path37, timeout) {
16869
16937
  if (!this._throttled.has(actionType)) {
16870
16938
  this._throttled.set(actionType, /* @__PURE__ */ new Map());
16871
16939
  }
16872
16940
  const action = this._throttled.get(actionType);
16873
16941
  if (!action)
16874
16942
  throw new Error("invalid throttle");
16875
- const actionPath = action.get(path34);
16943
+ const actionPath = action.get(path37);
16876
16944
  if (actionPath) {
16877
16945
  actionPath.count++;
16878
16946
  return false;
16879
16947
  }
16880
16948
  let timeoutObject;
16881
16949
  const clear = () => {
16882
- const item = action.get(path34);
16950
+ const item = action.get(path37);
16883
16951
  const count = item ? item.count : 0;
16884
- action.delete(path34);
16952
+ action.delete(path37);
16885
16953
  clearTimeout(timeoutObject);
16886
16954
  if (item)
16887
16955
  clearTimeout(item.timeoutObject);
@@ -16889,7 +16957,7 @@ var FSWatcher = class extends EventEmitter {
16889
16957
  };
16890
16958
  timeoutObject = setTimeout(clear, timeout);
16891
16959
  const thr = { timeoutObject, clear, count: 0 };
16892
- action.set(path34, thr);
16960
+ action.set(path37, thr);
16893
16961
  return thr;
16894
16962
  }
16895
16963
  _incrReadyCount() {
@@ -16903,44 +16971,44 @@ var FSWatcher = class extends EventEmitter {
16903
16971
  * @param event
16904
16972
  * @param awfEmit Callback to be called when ready for event to be emitted.
16905
16973
  */
16906
- _awaitWriteFinish(path34, threshold, event, awfEmit) {
16974
+ _awaitWriteFinish(path37, threshold, event, awfEmit) {
16907
16975
  const awf = this.options.awaitWriteFinish;
16908
16976
  if (typeof awf !== "object")
16909
16977
  return;
16910
16978
  const pollInterval = awf.pollInterval;
16911
16979
  let timeoutHandler;
16912
- let fullPath = path34;
16913
- if (this.options.cwd && !sysPath2.isAbsolute(path34)) {
16914
- fullPath = sysPath2.join(this.options.cwd, path34);
16980
+ let fullPath = path37;
16981
+ if (this.options.cwd && !sysPath2.isAbsolute(path37)) {
16982
+ fullPath = sysPath2.join(this.options.cwd, path37);
16915
16983
  }
16916
16984
  const now = /* @__PURE__ */ new Date();
16917
16985
  const writes = this._pendingWrites;
16918
16986
  function awaitWriteFinishFn(prevStat) {
16919
16987
  statcb(fullPath, (err, curStat) => {
16920
- if (err || !writes.has(path34)) {
16988
+ if (err || !writes.has(path37)) {
16921
16989
  if (err && err.code !== "ENOENT")
16922
16990
  awfEmit(err);
16923
16991
  return;
16924
16992
  }
16925
16993
  const now2 = Number(/* @__PURE__ */ new Date());
16926
16994
  if (prevStat && curStat.size !== prevStat.size) {
16927
- writes.get(path34).lastChange = now2;
16995
+ writes.get(path37).lastChange = now2;
16928
16996
  }
16929
- const pw = writes.get(path34);
16997
+ const pw = writes.get(path37);
16930
16998
  const df = now2 - pw.lastChange;
16931
16999
  if (df >= threshold) {
16932
- writes.delete(path34);
17000
+ writes.delete(path37);
16933
17001
  awfEmit(void 0, curStat);
16934
17002
  } else {
16935
17003
  timeoutHandler = setTimeout(awaitWriteFinishFn, pollInterval, curStat);
16936
17004
  }
16937
17005
  });
16938
17006
  }
16939
- if (!writes.has(path34)) {
16940
- writes.set(path34, {
17007
+ if (!writes.has(path37)) {
17008
+ writes.set(path37, {
16941
17009
  lastChange: now,
16942
17010
  cancelWait: () => {
16943
- writes.delete(path34);
17011
+ writes.delete(path37);
16944
17012
  clearTimeout(timeoutHandler);
16945
17013
  return event;
16946
17014
  }
@@ -16951,8 +17019,8 @@ var FSWatcher = class extends EventEmitter {
16951
17019
  /**
16952
17020
  * Determines whether user has asked to ignore this path.
16953
17021
  */
16954
- _isIgnored(path34, stats) {
16955
- if (this.options.atomic && DOT_RE.test(path34))
17022
+ _isIgnored(path37, stats) {
17023
+ if (this.options.atomic && DOT_RE.test(path37))
16956
17024
  return true;
16957
17025
  if (!this._userIgnored) {
16958
17026
  const { cwd } = this.options;
@@ -16962,17 +17030,17 @@ var FSWatcher = class extends EventEmitter {
16962
17030
  const list = [...ignoredPaths.map(normalizeIgnored(cwd)), ...ignored];
16963
17031
  this._userIgnored = anymatch(list, void 0);
16964
17032
  }
16965
- return this._userIgnored(path34, stats);
17033
+ return this._userIgnored(path37, stats);
16966
17034
  }
16967
- _isntIgnored(path34, stat4) {
16968
- return !this._isIgnored(path34, stat4);
17035
+ _isntIgnored(path37, stat4) {
17036
+ return !this._isIgnored(path37, stat4);
16969
17037
  }
16970
17038
  /**
16971
17039
  * Provides a set of common helpers and properties relating to symlink handling.
16972
17040
  * @param path file or directory pattern being watched
16973
17041
  */
16974
- _getWatchHelpers(path34) {
16975
- return new WatchHelper(path34, this.options.followSymlinks, this);
17042
+ _getWatchHelpers(path37) {
17043
+ return new WatchHelper(path37, this.options.followSymlinks, this);
16976
17044
  }
16977
17045
  // Directory helpers
16978
17046
  // -----------------
@@ -17004,63 +17072,63 @@ var FSWatcher = class extends EventEmitter {
17004
17072
  * @param item base path of item/directory
17005
17073
  */
17006
17074
  _remove(directory, item, isDirectory) {
17007
- const path34 = sysPath2.join(directory, item);
17008
- const fullPath = sysPath2.resolve(path34);
17009
- isDirectory = isDirectory != null ? isDirectory : this._watched.has(path34) || this._watched.has(fullPath);
17010
- if (!this._throttle("remove", path34, 100))
17075
+ const path37 = sysPath2.join(directory, item);
17076
+ const fullPath = sysPath2.resolve(path37);
17077
+ isDirectory = isDirectory != null ? isDirectory : this._watched.has(path37) || this._watched.has(fullPath);
17078
+ if (!this._throttle("remove", path37, 100))
17011
17079
  return;
17012
17080
  if (!isDirectory && this._watched.size === 1) {
17013
17081
  this.add(directory, item, true);
17014
17082
  }
17015
- const wp = this._getWatchedDir(path34);
17083
+ const wp = this._getWatchedDir(path37);
17016
17084
  const nestedDirectoryChildren = wp.getChildren();
17017
- nestedDirectoryChildren.forEach((nested) => this._remove(path34, nested));
17085
+ nestedDirectoryChildren.forEach((nested) => this._remove(path37, nested));
17018
17086
  const parent = this._getWatchedDir(directory);
17019
17087
  const wasTracked = parent.has(item);
17020
17088
  parent.remove(item);
17021
17089
  if (this._symlinkPaths.has(fullPath)) {
17022
17090
  this._symlinkPaths.delete(fullPath);
17023
17091
  }
17024
- let relPath = path34;
17092
+ let relPath = path37;
17025
17093
  if (this.options.cwd)
17026
- relPath = sysPath2.relative(this.options.cwd, path34);
17094
+ relPath = sysPath2.relative(this.options.cwd, path37);
17027
17095
  if (this.options.awaitWriteFinish && this._pendingWrites.has(relPath)) {
17028
17096
  const event = this._pendingWrites.get(relPath).cancelWait();
17029
17097
  if (event === EVENTS.ADD)
17030
17098
  return;
17031
17099
  }
17032
- this._watched.delete(path34);
17100
+ this._watched.delete(path37);
17033
17101
  this._watched.delete(fullPath);
17034
17102
  const eventName = isDirectory ? EVENTS.UNLINK_DIR : EVENTS.UNLINK;
17035
- if (wasTracked && !this._isIgnored(path34))
17036
- this._emit(eventName, path34);
17037
- this._closePath(path34);
17103
+ if (wasTracked && !this._isIgnored(path37))
17104
+ this._emit(eventName, path37);
17105
+ this._closePath(path37);
17038
17106
  }
17039
17107
  /**
17040
17108
  * Closes all watchers for a path
17041
17109
  */
17042
- _closePath(path34) {
17043
- this._closeFile(path34);
17044
- const dir = sysPath2.dirname(path34);
17045
- this._getWatchedDir(dir).remove(sysPath2.basename(path34));
17110
+ _closePath(path37) {
17111
+ this._closeFile(path37);
17112
+ const dir = sysPath2.dirname(path37);
17113
+ this._getWatchedDir(dir).remove(sysPath2.basename(path37));
17046
17114
  }
17047
17115
  /**
17048
17116
  * Closes only file-specific watchers
17049
17117
  */
17050
- _closeFile(path34) {
17051
- const closers = this._closers.get(path34);
17118
+ _closeFile(path37) {
17119
+ const closers = this._closers.get(path37);
17052
17120
  if (!closers)
17053
17121
  return;
17054
17122
  closers.forEach((closer) => closer());
17055
- this._closers.delete(path34);
17123
+ this._closers.delete(path37);
17056
17124
  }
17057
- _addPathCloser(path34, closer) {
17125
+ _addPathCloser(path37, closer) {
17058
17126
  if (!closer)
17059
17127
  return;
17060
- let list = this._closers.get(path34);
17128
+ let list = this._closers.get(path37);
17061
17129
  if (!list) {
17062
17130
  list = [];
17063
- this._closers.set(path34, list);
17131
+ this._closers.set(path37, list);
17064
17132
  }
17065
17133
  list.push(closer);
17066
17134
  }
@@ -17430,11 +17498,11 @@ function createStrictRateLimiterFromConfig(config) {
17430
17498
  }
17431
17499
 
17432
17500
  // modules/server/middleware/auto-rate-limit.ts
17433
- function matchesStrictPattern(path34, patterns) {
17501
+ function matchesStrictPattern(path37, patterns) {
17434
17502
  for (const pattern of patterns) {
17435
17503
  const regexPattern = pattern.replace(/\*\*/g, ".*").replace(/\*/g, "[^/]*").replace(/\//g, "\\/");
17436
17504
  const regex = new RegExp(`^${regexPattern}$`);
17437
- if (regex.test(path34)) {
17505
+ if (regex.test(path37)) {
17438
17506
  return true;
17439
17507
  }
17440
17508
  }
@@ -17624,9 +17692,441 @@ async function handleApiRequest(options) {
17624
17692
  }
17625
17693
  }
17626
17694
 
17695
+ // modules/server/image-optimizer/index.ts
17696
+ import sharp from "sharp";
17697
+ import fs25 from "fs";
17698
+ import path33 from "path";
17699
+
17700
+ // modules/server/image-optimizer/validation.ts
17701
+ import path31 from "path";
17702
+ function isRemoteUrl(url) {
17703
+ return url.startsWith("http://") || url.startsWith("https://");
17704
+ }
17705
+ function sanitizeImagePath(imagePath) {
17706
+ const normalized = path31.normalize(imagePath).replace(/^(\.\.(\/|\\|$))+/, "");
17707
+ return normalized.replace(/^[/\\]+/, "");
17708
+ }
17709
+ function patternToRegex(pattern) {
17710
+ const parts = [];
17711
+ if (pattern.protocol) {
17712
+ parts.push(pattern.protocol === "https" ? "https" : "http");
17713
+ } else {
17714
+ parts.push("https?");
17715
+ }
17716
+ parts.push("://");
17717
+ let hostnamePattern = pattern.hostname.replace(/\./g, "\\.").replace(/\*\*/g, ".*").replace(/\*/g, "[^.]*");
17718
+ parts.push(hostnamePattern);
17719
+ if (pattern.port) {
17720
+ parts.push(`:${pattern.port}`);
17721
+ }
17722
+ if (pattern.pathname) {
17723
+ let pathnamePattern = pattern.pathname.replace(/\*\*/g, ".*").replace(/\*/g, "[^/]*");
17724
+ parts.push(pathnamePattern);
17725
+ } else {
17726
+ parts.push(".*");
17727
+ }
17728
+ const regexSource = `^${parts.join("")}`;
17729
+ return new RegExp(regexSource);
17730
+ }
17731
+ function validateRemoteUrl(url, config) {
17732
+ if (!config.remotePatterns && !config.domains) {
17733
+ return false;
17734
+ }
17735
+ try {
17736
+ const urlObj = new URL(url);
17737
+ const protocol = urlObj.protocol.replace(":", "");
17738
+ const hostname = urlObj.hostname;
17739
+ const port = urlObj.port || "";
17740
+ const pathname = urlObj.pathname;
17741
+ if (config.remotePatterns && config.remotePatterns.length > 0) {
17742
+ for (const pattern of config.remotePatterns) {
17743
+ const regex = patternToRegex(pattern);
17744
+ const testUrl = `${protocol}://${hostname}${port ? `:${port}` : ""}${pathname}`;
17745
+ if (regex.test(testUrl)) {
17746
+ if (pattern.protocol && pattern.protocol !== protocol) {
17747
+ continue;
17748
+ }
17749
+ if (pattern.port && pattern.port !== port) {
17750
+ continue;
17751
+ }
17752
+ return true;
17753
+ }
17754
+ }
17755
+ }
17756
+ if (config.domains && config.domains.length > 0) {
17757
+ for (const domain of config.domains) {
17758
+ const domainPattern = domain.replace(/\./g, "\\.").replace(/\*\*/g, ".*").replace(/\*/g, "[^.]*");
17759
+ const regex = new RegExp(`^${domainPattern}$`);
17760
+ if (regex.test(hostname)) {
17761
+ if (process.env.NODE_ENV === "production" && protocol !== "https") {
17762
+ continue;
17763
+ }
17764
+ return true;
17765
+ }
17766
+ }
17767
+ }
17768
+ return false;
17769
+ } catch (error) {
17770
+ return false;
17771
+ }
17772
+ }
17773
+ function validateImageDimensions(width, height, config) {
17774
+ const maxWidth = config.maxWidth || 3840;
17775
+ const maxHeight = config.maxHeight || 3840;
17776
+ if (width !== void 0 && (width <= 0 || width > maxWidth)) {
17777
+ return {
17778
+ valid: false,
17779
+ error: `Image width must be between 1 and ${maxWidth}, got ${width}`
17780
+ };
17781
+ }
17782
+ if (height !== void 0 && (height <= 0 || height > maxHeight)) {
17783
+ return {
17784
+ valid: false,
17785
+ error: `Image height must be between 1 and ${maxHeight}, got ${height}`
17786
+ };
17787
+ }
17788
+ return { valid: true };
17789
+ }
17790
+ function validateQuality(quality) {
17791
+ if (quality === void 0) {
17792
+ return { valid: true };
17793
+ }
17794
+ if (typeof quality !== "number" || quality < 1 || quality > 100) {
17795
+ return {
17796
+ valid: false,
17797
+ error: `Image quality must be between 1 and 100, got ${quality}`
17798
+ };
17799
+ }
17800
+ return { valid: true };
17801
+ }
17802
+
17803
+ // modules/server/image-optimizer/cache.ts
17804
+ import fs24 from "fs";
17805
+ import path32 from "path";
17806
+ import crypto from "crypto";
17807
+ function generateCacheKey(src, width, height, quality, format) {
17808
+ const data = `${src}-${width || ""}-${height || ""}-${quality || ""}-${format || ""}`;
17809
+ return crypto.createHash("sha256").update(data).digest("hex");
17810
+ }
17811
+ function getCacheDir(projectRoot, config) {
17812
+ const buildDir = getBuildDir(projectRoot, config);
17813
+ return path32.join(buildDir, "cache", "images");
17814
+ }
17815
+ function ensureCacheDir(cacheDir) {
17816
+ if (!fs24.existsSync(cacheDir)) {
17817
+ fs24.mkdirSync(cacheDir, { recursive: true });
17818
+ }
17819
+ }
17820
+ function getCachedImagePath(cacheKey, extension, cacheDir) {
17821
+ return path32.join(cacheDir, `${cacheKey}.${extension}`);
17822
+ }
17823
+ function hasCachedImage(cacheKey, extension, cacheDir) {
17824
+ const cachedPath = getCachedImagePath(cacheKey, extension, cacheDir);
17825
+ return fs24.existsSync(cachedPath);
17826
+ }
17827
+ function readCachedImage(cacheKey, extension, cacheDir) {
17828
+ const cachedPath = getCachedImagePath(cacheKey, extension, cacheDir);
17829
+ try {
17830
+ if (fs24.existsSync(cachedPath)) {
17831
+ return fs24.readFileSync(cachedPath);
17832
+ }
17833
+ } catch (error) {
17834
+ console.warn(`[image-optimizer] Failed to read cached image: ${cachedPath}`, error);
17835
+ }
17836
+ return null;
17837
+ }
17838
+ function writeCachedImage(cacheKey, extension, cacheDir, imageBuffer) {
17839
+ ensureCacheDir(cacheDir);
17840
+ const cachedPath = getCachedImagePath(cacheKey, extension, cacheDir);
17841
+ try {
17842
+ fs24.writeFileSync(cachedPath, imageBuffer);
17843
+ } catch (error) {
17844
+ console.warn(`[image-optimizer] Failed to write cached image: ${cachedPath}`, error);
17845
+ }
17846
+ }
17847
+ function getImageMimeType(format) {
17848
+ const formatMap = {
17849
+ webp: "image/webp",
17850
+ avif: "image/avif",
17851
+ jpeg: "image/jpeg",
17852
+ jpg: "image/jpeg",
17853
+ png: "image/png",
17854
+ gif: "image/gif",
17855
+ svg: "image/svg+xml"
17856
+ };
17857
+ const normalized = format.toLowerCase();
17858
+ return formatMap[normalized] || "image/jpeg";
17859
+ }
17860
+ function getImageExtension(format) {
17861
+ const formatMap = {
17862
+ "image/webp": "webp",
17863
+ "image/avif": "avif",
17864
+ "image/jpeg": "jpg",
17865
+ "image/png": "png",
17866
+ "image/gif": "gif",
17867
+ "image/svg+xml": "svg",
17868
+ webp: "webp",
17869
+ avif: "avif",
17870
+ jpeg: "jpg",
17871
+ jpg: "jpg",
17872
+ png: "png",
17873
+ gif: "gif",
17874
+ svg: "svg"
17875
+ };
17876
+ const normalized = format.toLowerCase();
17877
+ return formatMap[normalized] || "jpg";
17878
+ }
17879
+
17880
+ // modules/server/image-optimizer/index.ts
17881
+ async function downloadRemoteImage(url, timeout = 1e4) {
17882
+ let fetchFn;
17883
+ try {
17884
+ if (typeof fetch !== "undefined") {
17885
+ fetchFn = fetch;
17886
+ } else {
17887
+ const { fetch: undiciFetch } = await import("undici");
17888
+ fetchFn = undiciFetch;
17889
+ }
17890
+ } catch (error) {
17891
+ throw new Error("Failed to load fetch implementation. Node 18+ required or install undici.");
17892
+ }
17893
+ const controller = new AbortController();
17894
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
17895
+ try {
17896
+ const response = await fetchFn(url, {
17897
+ signal: controller.signal,
17898
+ headers: {
17899
+ "User-Agent": "Loly-Image-Optimizer/1.0"
17900
+ }
17901
+ });
17902
+ clearTimeout(timeoutId);
17903
+ if (!response.ok) {
17904
+ throw new Error(`Failed to download image: ${response.status} ${response.statusText}`);
17905
+ }
17906
+ const arrayBuffer = await response.arrayBuffer();
17907
+ return Buffer.from(arrayBuffer);
17908
+ } catch (error) {
17909
+ clearTimeout(timeoutId);
17910
+ if (error instanceof Error && error.name === "AbortError") {
17911
+ throw new Error(`Image download timeout after ${timeout}ms`);
17912
+ }
17913
+ throw error;
17914
+ }
17915
+ }
17916
+ function readLocalImage(src, projectRoot, config) {
17917
+ const sanitized = sanitizeImagePath(src);
17918
+ const staticDir = getStaticDir(projectRoot, config);
17919
+ const staticPath = path33.join(staticDir, sanitized);
17920
+ if (fs25.existsSync(staticPath)) {
17921
+ return fs25.readFileSync(staticPath);
17922
+ }
17923
+ if (src.startsWith("/")) {
17924
+ const absolutePath = path33.join(projectRoot, sanitized);
17925
+ if (fs25.existsSync(absolutePath)) {
17926
+ return fs25.readFileSync(absolutePath);
17927
+ }
17928
+ }
17929
+ throw new Error(`Image not found: ${src}`);
17930
+ }
17931
+ function determineOutputFormat(sourceFormat, requestedFormat, config) {
17932
+ if (sourceFormat === "svg") {
17933
+ return "svg";
17934
+ }
17935
+ if (requestedFormat && requestedFormat !== "auto") {
17936
+ return requestedFormat;
17937
+ }
17938
+ const supportedFormats = config.formats || ["image/webp"];
17939
+ if (supportedFormats.includes("image/avif")) {
17940
+ return "avif";
17941
+ }
17942
+ if (supportedFormats.includes("image/webp")) {
17943
+ return "webp";
17944
+ }
17945
+ return sourceFormat === "svg" ? "jpeg" : sourceFormat;
17946
+ }
17947
+ async function optimizeImage(options, projectRoot, config) {
17948
+ const imageConfig = config.images || {};
17949
+ const dimValidation = validateImageDimensions(options.width, options.height, imageConfig);
17950
+ if (!dimValidation.valid) {
17951
+ throw new Error(dimValidation.error);
17952
+ }
17953
+ const qualityValidation = validateQuality(options.quality);
17954
+ if (!qualityValidation.valid) {
17955
+ throw new Error(qualityValidation.error);
17956
+ }
17957
+ if (isRemoteUrl(options.src)) {
17958
+ if (!validateRemoteUrl(options.src, imageConfig)) {
17959
+ throw new Error(`Remote image domain not allowed: ${options.src}`);
17960
+ }
17961
+ }
17962
+ const sourceFormat = path33.extname(options.src).slice(1).toLowerCase() || "jpeg";
17963
+ const outputFormat = determineOutputFormat(
17964
+ sourceFormat,
17965
+ options.format,
17966
+ imageConfig
17967
+ );
17968
+ const cacheKey = generateCacheKey(
17969
+ options.src,
17970
+ options.width,
17971
+ options.height,
17972
+ options.quality || imageConfig.quality || 75,
17973
+ outputFormat
17974
+ );
17975
+ const cacheDir = getCacheDir(projectRoot, config);
17976
+ const extension = getImageExtension(outputFormat);
17977
+ if (hasCachedImage(cacheKey, extension, cacheDir)) {
17978
+ const cached = readCachedImage(cacheKey, extension, cacheDir);
17979
+ if (cached) {
17980
+ const metadata2 = await sharp(cached).metadata();
17981
+ return {
17982
+ buffer: cached,
17983
+ format: outputFormat,
17984
+ mimeType: getImageMimeType(outputFormat),
17985
+ width: metadata2.width || options.width || 0,
17986
+ height: metadata2.height || options.height || 0
17987
+ };
17988
+ }
17989
+ }
17990
+ let imageBuffer;
17991
+ if (isRemoteUrl(options.src)) {
17992
+ imageBuffer = await downloadRemoteImage(options.src);
17993
+ } else {
17994
+ imageBuffer = readLocalImage(options.src, projectRoot, config);
17995
+ }
17996
+ if (outputFormat === "svg" || sourceFormat === "svg") {
17997
+ if (!imageConfig.dangerouslyAllowSVG) {
17998
+ throw new Error("SVG images are not allowed. Set images.dangerouslyAllowSVG to true to enable.");
17999
+ }
18000
+ return {
18001
+ buffer: imageBuffer,
18002
+ format: "svg",
18003
+ mimeType: "image/svg+xml",
18004
+ width: options.width || 0,
18005
+ height: options.height || 0
18006
+ };
18007
+ }
18008
+ let sharpInstance = sharp(imageBuffer);
18009
+ const metadata = await sharpInstance.metadata();
18010
+ if (options.width || options.height) {
18011
+ const fit = options.fit || "cover";
18012
+ sharpInstance = sharpInstance.resize(options.width, options.height, {
18013
+ fit,
18014
+ withoutEnlargement: true
18015
+ });
18016
+ }
18017
+ const quality = options.quality || imageConfig.quality || 75;
18018
+ switch (outputFormat) {
18019
+ case "webp":
18020
+ sharpInstance = sharpInstance.webp({ quality });
18021
+ break;
18022
+ case "avif":
18023
+ sharpInstance = sharpInstance.avif({ quality });
18024
+ break;
18025
+ case "jpeg":
18026
+ case "jpg":
18027
+ sharpInstance = sharpInstance.jpeg({ quality });
18028
+ break;
18029
+ case "png":
18030
+ sharpInstance = sharpInstance.png({ quality: Math.round(quality / 100 * 9) });
18031
+ break;
18032
+ default:
18033
+ sharpInstance = sharpInstance.jpeg({ quality });
18034
+ }
18035
+ const optimizedBuffer = await sharpInstance.toBuffer();
18036
+ const finalMetadata = await sharp(optimizedBuffer).metadata();
18037
+ writeCachedImage(cacheKey, extension, cacheDir, optimizedBuffer);
18038
+ return {
18039
+ buffer: optimizedBuffer,
18040
+ format: outputFormat,
18041
+ mimeType: getImageMimeType(outputFormat),
18042
+ width: finalMetadata.width || options.width || metadata.width || 0,
18043
+ height: finalMetadata.height || options.height || metadata.height || 0
18044
+ };
18045
+ }
18046
+
18047
+ // modules/server/handlers/image.ts
18048
+ async function handleImageRequest(options) {
18049
+ const { req, res, projectRoot, config } = options;
18050
+ try {
18051
+ const src = req.query.src;
18052
+ const width = req.query.w ? parseInt(req.query.w, 10) : void 0;
18053
+ const height = req.query.h ? parseInt(req.query.h, 10) : void 0;
18054
+ const quality = req.query.q ? parseInt(req.query.q, 10) : void 0;
18055
+ const format = req.query.format;
18056
+ const fit = req.query.fit;
18057
+ if (!src) {
18058
+ res.status(400).json({
18059
+ error: "Missing required parameter: src"
18060
+ });
18061
+ return;
18062
+ }
18063
+ if (typeof src !== "string") {
18064
+ res.status(400).json({
18065
+ error: "Parameter 'src' must be a string"
18066
+ });
18067
+ return;
18068
+ }
18069
+ const result = await optimizeImage(
18070
+ {
18071
+ src,
18072
+ width,
18073
+ height,
18074
+ quality,
18075
+ format,
18076
+ fit
18077
+ },
18078
+ projectRoot,
18079
+ config
18080
+ );
18081
+ const imageConfig = config.images || {};
18082
+ const cacheTTL = imageConfig.minimumCacheTTL || 60;
18083
+ res.setHeader("Content-Type", result.mimeType);
18084
+ res.setHeader("Content-Length", result.buffer.length);
18085
+ res.setHeader("Cache-Control", `public, max-age=${cacheTTL}, immutable`);
18086
+ res.setHeader("X-Content-Type-Options", "nosniff");
18087
+ res.send(result.buffer);
18088
+ } catch (error) {
18089
+ if (error instanceof Error) {
18090
+ if (error.message.includes("not allowed")) {
18091
+ res.status(403).json({
18092
+ error: "Forbidden",
18093
+ message: error.message
18094
+ });
18095
+ return;
18096
+ }
18097
+ if (error.message.includes("not found") || error.message.includes("Image not found")) {
18098
+ res.status(404).json({
18099
+ error: "Not Found",
18100
+ message: error.message
18101
+ });
18102
+ return;
18103
+ }
18104
+ if (error.message.includes("must be")) {
18105
+ res.status(400).json({
18106
+ error: "Bad Request",
18107
+ message: error.message
18108
+ });
18109
+ return;
18110
+ }
18111
+ if (error.message.includes("timeout") || error.message.includes("download")) {
18112
+ res.status(504).json({
18113
+ error: "Gateway Timeout",
18114
+ message: error.message
18115
+ });
18116
+ return;
18117
+ }
18118
+ }
18119
+ console.error("[image-optimizer] Error processing image:", error);
18120
+ res.status(500).json({
18121
+ error: "Internal Server Error",
18122
+ message: "Failed to process image"
18123
+ });
18124
+ }
18125
+ }
18126
+
17627
18127
  // modules/server/routes.ts
17628
18128
  init_globals();
17629
- import path31 from "path";
18129
+ import path34 from "path";
17630
18130
  var cachedRewriteLoader = null;
17631
18131
  var cachedProjectRoot = null;
17632
18132
  var cachedIsDev = null;
@@ -17654,10 +18154,20 @@ function setupRoutes(options) {
17654
18154
  } = options;
17655
18155
  const routeChunks = routeLoader.loadRouteChunks();
17656
18156
  const rewriteLoader = getRewriteLoader(projectRoot, isDev);
17657
- const ssgOutDir = path31.join(
17658
- config ? getBuildDir(projectRoot, config) : path31.join(projectRoot, BUILD_FOLDER_NAME),
18157
+ const ssgOutDir = path34.join(
18158
+ config ? getBuildDir(projectRoot, config) : path34.join(projectRoot, BUILD_FOLDER_NAME),
17659
18159
  "ssg"
17660
18160
  );
18161
+ if (config) {
18162
+ app.get("/_loly/image", async (req, res) => {
18163
+ await handleImageRequest({
18164
+ req,
18165
+ res,
18166
+ projectRoot,
18167
+ config
18168
+ });
18169
+ });
18170
+ }
17661
18171
  app.all("/api/*", async (req, res) => {
17662
18172
  const apiRoutes = isDev && getRoutes ? (await getRoutes()).apiRoutes : initialApiRoutes;
17663
18173
  const serverConfig = await getServerConfig(projectRoot);
@@ -18704,7 +19214,7 @@ import cors from "cors";
18704
19214
  import helmet from "helmet";
18705
19215
  import cookieParser from "cookie-parser";
18706
19216
  import compression from "compression";
18707
- import crypto from "crypto";
19217
+ import crypto2 from "crypto";
18708
19218
  var setupApplication = async ({
18709
19219
  projectRoot
18710
19220
  }) => {
@@ -18813,7 +19323,7 @@ var setupApplication = async ({
18813
19323
  if (process.env.NODE_ENV !== "development" && security?.contentSecurityPolicy !== false) {
18814
19324
  app.use(
18815
19325
  (req, res, next) => {
18816
- const nonce = crypto.randomBytes(16).toString("base64");
19326
+ const nonce = crypto2.randomBytes(16).toString("base64");
18817
19327
  res.locals.nonce = nonce;
18818
19328
  next();
18819
19329
  }
@@ -18884,8 +19394,8 @@ var setupApplication = async ({
18884
19394
 
18885
19395
  // src/server.ts
18886
19396
  import dotenv2 from "dotenv";
18887
- var envPath = path32.join(process.cwd(), ".env");
18888
- if (fs24.existsSync(envPath)) {
19397
+ var envPath = path35.join(process.cwd(), ".env");
19398
+ if (fs26.existsSync(envPath)) {
18889
19399
  dotenv2.config({ path: envPath });
18890
19400
  } else {
18891
19401
  dotenv2.config();
@@ -18906,8 +19416,8 @@ async function startServer(options = {}) {
18906
19416
  }
18907
19417
  const port = options.port ?? (process.env.PORT ? parseInt(process.env.PORT, 10) : void 0) ?? config.server.port;
18908
19418
  const host = process.env.HOST ?? (!isDev ? "0.0.0.0" : void 0) ?? config.server.host;
18909
- const appDir = options.appDir ?? (isDev ? getAppDir(projectRoot, config) : path32.join(getBuildDir(projectRoot, config), "server"));
18910
- if (!isDev && !fs24.existsSync(appDir)) {
19419
+ const appDir = options.appDir ?? (isDev ? getAppDir(projectRoot, config) : path35.join(getBuildDir(projectRoot, config), "server"));
19420
+ if (!isDev && !fs26.existsSync(appDir)) {
18911
19421
  logger4.error("Compiled directory not found", void 0, {
18912
19422
  buildDir: config.directories.build,
18913
19423
  appDir,
@@ -19103,7 +19613,7 @@ async function run() {
19103
19613
  }
19104
19614
  const args = parseArgs(argv.slice(1));
19105
19615
  const projectRoot = process2.cwd();
19106
- const appDir = path33.resolve(projectRoot, args.appDir || "app");
19616
+ const appDir = path36.resolve(projectRoot, args.appDir || "app");
19107
19617
  const port = typeof args.port === "string" && args.port.trim().length > 0 ? Number(args.port) : 3e3;
19108
19618
  switch (command) {
19109
19619
  case "dev": {