@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.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") {
@@ -13179,12 +13180,12 @@ var DEFAULT_IGNORED_PATHS = [
13179
13180
  /^\/sockjs-node/
13180
13181
  // Hot reload websocket
13181
13182
  ];
13182
- function shouldIgnorePath(path34, ignoredPaths) {
13183
+ function shouldIgnorePath(path37, ignoredPaths) {
13183
13184
  return ignoredPaths.some((pattern) => {
13184
13185
  if (typeof pattern === "string") {
13185
- return path34 === pattern || path34.startsWith(pattern);
13186
+ return path37 === pattern || path37.startsWith(pattern);
13186
13187
  }
13187
- return pattern.test(path34);
13188
+ return pattern.test(path37);
13188
13189
  });
13189
13190
  }
13190
13191
  function requestLoggerMiddleware(options = {}) {
@@ -15147,7 +15148,20 @@ var DEFAULT_CONFIG2 = {
15147
15148
  ssr: true,
15148
15149
  ssg: true
15149
15150
  },
15150
- 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
+ }
15151
15165
  };
15152
15166
  function deepMerge(target, source) {
15153
15167
  const result = { ...target };
@@ -15264,6 +15278,53 @@ function validateConfig(config, projectRoot) {
15264
15278
  if (typeof config.rendering.ssg !== "boolean") {
15265
15279
  errors.push("config.rendering.ssg must be a boolean");
15266
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
+ }
15267
15328
  if (errors.length > 0) {
15268
15329
  const errorMessage = [
15269
15330
  "\u274C Configuration validation failed:",
@@ -15397,8 +15458,8 @@ async function buildApp(options = {}) {
15397
15458
  }
15398
15459
 
15399
15460
  // src/server.ts
15400
- import fs24 from "fs";
15401
- import path32 from "path";
15461
+ import fs26 from "fs";
15462
+ import path35 from "path";
15402
15463
 
15403
15464
  // modules/server/setup.ts
15404
15465
  import express from "express";
@@ -15481,7 +15542,7 @@ var ReaddirpStream = class extends Readable {
15481
15542
  this._directoryFilter = normalizeFilter(opts.directoryFilter);
15482
15543
  const statMethod = opts.lstat ? lstat : stat;
15483
15544
  if (wantBigintFsStats) {
15484
- this._stat = (path34) => statMethod(path34, { bigint: true });
15545
+ this._stat = (path37) => statMethod(path37, { bigint: true });
15485
15546
  } else {
15486
15547
  this._stat = statMethod;
15487
15548
  }
@@ -15506,8 +15567,8 @@ var ReaddirpStream = class extends Readable {
15506
15567
  const par = this.parent;
15507
15568
  const fil = par && par.files;
15508
15569
  if (fil && fil.length > 0) {
15509
- const { path: path34, depth } = par;
15510
- 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));
15511
15572
  const awaited = await Promise.all(slice);
15512
15573
  for (const entry of awaited) {
15513
15574
  if (!entry)
@@ -15547,20 +15608,20 @@ var ReaddirpStream = class extends Readable {
15547
15608
  this.reading = false;
15548
15609
  }
15549
15610
  }
15550
- async _exploreDir(path34, depth) {
15611
+ async _exploreDir(path37, depth) {
15551
15612
  let files;
15552
15613
  try {
15553
- files = await readdir(path34, this._rdOptions);
15614
+ files = await readdir(path37, this._rdOptions);
15554
15615
  } catch (error) {
15555
15616
  this._onError(error);
15556
15617
  }
15557
- return { files, depth, path: path34 };
15618
+ return { files, depth, path: path37 };
15558
15619
  }
15559
- async _formatEntry(dirent, path34) {
15620
+ async _formatEntry(dirent, path37) {
15560
15621
  let entry;
15561
15622
  const basename3 = this._isDirent ? dirent.name : dirent;
15562
15623
  try {
15563
- const fullPath = presolve(pjoin(path34, basename3));
15624
+ const fullPath = presolve(pjoin(path37, basename3));
15564
15625
  entry = { path: prelative(this._root, fullPath), fullPath, basename: basename3 };
15565
15626
  entry[this._statsProp] = this._isDirent ? dirent : await this._stat(fullPath);
15566
15627
  } catch (err) {
@@ -15960,16 +16021,16 @@ var delFromSet = (main, prop, item) => {
15960
16021
  };
15961
16022
  var isEmptySet = (val) => val instanceof Set ? val.size === 0 : !val;
15962
16023
  var FsWatchInstances = /* @__PURE__ */ new Map();
15963
- function createFsWatchInstance(path34, options, listener, errHandler, emitRaw) {
16024
+ function createFsWatchInstance(path37, options, listener, errHandler, emitRaw) {
15964
16025
  const handleEvent = (rawEvent, evPath) => {
15965
- listener(path34);
15966
- emitRaw(rawEvent, evPath, { watchedPath: path34 });
15967
- if (evPath && path34 !== evPath) {
15968
- 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));
15969
16030
  }
15970
16031
  };
15971
16032
  try {
15972
- return fs_watch(path34, {
16033
+ return fs_watch(path37, {
15973
16034
  persistent: options.persistent
15974
16035
  }, handleEvent);
15975
16036
  } catch (error) {
@@ -15985,12 +16046,12 @@ var fsWatchBroadcast = (fullPath, listenerType, val1, val2, val3) => {
15985
16046
  listener(val1, val2, val3);
15986
16047
  });
15987
16048
  };
15988
- var setFsWatchListener = (path34, fullPath, options, handlers) => {
16049
+ var setFsWatchListener = (path37, fullPath, options, handlers) => {
15989
16050
  const { listener, errHandler, rawEmitter } = handlers;
15990
16051
  let cont = FsWatchInstances.get(fullPath);
15991
16052
  let watcher;
15992
16053
  if (!options.persistent) {
15993
- watcher = createFsWatchInstance(path34, options, listener, errHandler, rawEmitter);
16054
+ watcher = createFsWatchInstance(path37, options, listener, errHandler, rawEmitter);
15994
16055
  if (!watcher)
15995
16056
  return;
15996
16057
  return watcher.close.bind(watcher);
@@ -16001,7 +16062,7 @@ var setFsWatchListener = (path34, fullPath, options, handlers) => {
16001
16062
  addAndConvert(cont, KEY_RAW, rawEmitter);
16002
16063
  } else {
16003
16064
  watcher = createFsWatchInstance(
16004
- path34,
16065
+ path37,
16005
16066
  options,
16006
16067
  fsWatchBroadcast.bind(null, fullPath, KEY_LISTENERS),
16007
16068
  errHandler,
@@ -16016,7 +16077,7 @@ var setFsWatchListener = (path34, fullPath, options, handlers) => {
16016
16077
  cont.watcherUnusable = true;
16017
16078
  if (isWindows && error.code === "EPERM") {
16018
16079
  try {
16019
- const fd = await open(path34, "r");
16080
+ const fd = await open(path37, "r");
16020
16081
  await fd.close();
16021
16082
  broadcastErr(error);
16022
16083
  } catch (err) {
@@ -16047,7 +16108,7 @@ var setFsWatchListener = (path34, fullPath, options, handlers) => {
16047
16108
  };
16048
16109
  };
16049
16110
  var FsWatchFileInstances = /* @__PURE__ */ new Map();
16050
- var setFsWatchFileListener = (path34, fullPath, options, handlers) => {
16111
+ var setFsWatchFileListener = (path37, fullPath, options, handlers) => {
16051
16112
  const { listener, rawEmitter } = handlers;
16052
16113
  let cont = FsWatchFileInstances.get(fullPath);
16053
16114
  const copts = cont && cont.options;
@@ -16069,7 +16130,7 @@ var setFsWatchFileListener = (path34, fullPath, options, handlers) => {
16069
16130
  });
16070
16131
  const currmtime = curr.mtimeMs;
16071
16132
  if (curr.size !== prev.size || currmtime > prev.mtimeMs || currmtime === 0) {
16072
- foreach(cont.listeners, (listener2) => listener2(path34, curr));
16133
+ foreach(cont.listeners, (listener2) => listener2(path37, curr));
16073
16134
  }
16074
16135
  })
16075
16136
  };
@@ -16097,13 +16158,13 @@ var NodeFsHandler = class {
16097
16158
  * @param listener on fs change
16098
16159
  * @returns closer for the watcher instance
16099
16160
  */
16100
- _watchWithNodeFs(path34, listener) {
16161
+ _watchWithNodeFs(path37, listener) {
16101
16162
  const opts = this.fsw.options;
16102
- const directory = sysPath.dirname(path34);
16103
- const basename3 = sysPath.basename(path34);
16163
+ const directory = sysPath.dirname(path37);
16164
+ const basename3 = sysPath.basename(path37);
16104
16165
  const parent = this.fsw._getWatchedDir(directory);
16105
16166
  parent.add(basename3);
16106
- const absolutePath = sysPath.resolve(path34);
16167
+ const absolutePath = sysPath.resolve(path37);
16107
16168
  const options = {
16108
16169
  persistent: opts.persistent
16109
16170
  };
@@ -16113,12 +16174,12 @@ var NodeFsHandler = class {
16113
16174
  if (opts.usePolling) {
16114
16175
  const enableBin = opts.interval !== opts.binaryInterval;
16115
16176
  options.interval = enableBin && isBinaryPath(basename3) ? opts.binaryInterval : opts.interval;
16116
- closer = setFsWatchFileListener(path34, absolutePath, options, {
16177
+ closer = setFsWatchFileListener(path37, absolutePath, options, {
16117
16178
  listener,
16118
16179
  rawEmitter: this.fsw._emitRaw
16119
16180
  });
16120
16181
  } else {
16121
- closer = setFsWatchListener(path34, absolutePath, options, {
16182
+ closer = setFsWatchListener(path37, absolutePath, options, {
16122
16183
  listener,
16123
16184
  errHandler: this._boundHandleError,
16124
16185
  rawEmitter: this.fsw._emitRaw
@@ -16140,7 +16201,7 @@ var NodeFsHandler = class {
16140
16201
  let prevStats = stats;
16141
16202
  if (parent.has(basename3))
16142
16203
  return;
16143
- const listener = async (path34, newStats) => {
16204
+ const listener = async (path37, newStats) => {
16144
16205
  if (!this.fsw._throttle(THROTTLE_MODE_WATCH, file, 5))
16145
16206
  return;
16146
16207
  if (!newStats || newStats.mtimeMs === 0) {
@@ -16154,11 +16215,11 @@ var NodeFsHandler = class {
16154
16215
  this.fsw._emit(EV.CHANGE, file, newStats2);
16155
16216
  }
16156
16217
  if ((isMacos || isLinux || isFreeBSD) && prevStats.ino !== newStats2.ino) {
16157
- this.fsw._closeFile(path34);
16218
+ this.fsw._closeFile(path37);
16158
16219
  prevStats = newStats2;
16159
16220
  const closer2 = this._watchWithNodeFs(file, listener);
16160
16221
  if (closer2)
16161
- this.fsw._addPathCloser(path34, closer2);
16222
+ this.fsw._addPathCloser(path37, closer2);
16162
16223
  } else {
16163
16224
  prevStats = newStats2;
16164
16225
  }
@@ -16190,7 +16251,7 @@ var NodeFsHandler = class {
16190
16251
  * @param item basename of this item
16191
16252
  * @returns true if no more processing is needed for this entry.
16192
16253
  */
16193
- async _handleSymlink(entry, directory, path34, item) {
16254
+ async _handleSymlink(entry, directory, path37, item) {
16194
16255
  if (this.fsw.closed) {
16195
16256
  return;
16196
16257
  }
@@ -16200,7 +16261,7 @@ var NodeFsHandler = class {
16200
16261
  this.fsw._incrReadyCount();
16201
16262
  let linkPath;
16202
16263
  try {
16203
- linkPath = await fsrealpath(path34);
16264
+ linkPath = await fsrealpath(path37);
16204
16265
  } catch (e) {
16205
16266
  this.fsw._emitReady();
16206
16267
  return true;
@@ -16210,12 +16271,12 @@ var NodeFsHandler = class {
16210
16271
  if (dir.has(item)) {
16211
16272
  if (this.fsw._symlinkPaths.get(full) !== linkPath) {
16212
16273
  this.fsw._symlinkPaths.set(full, linkPath);
16213
- this.fsw._emit(EV.CHANGE, path34, entry.stats);
16274
+ this.fsw._emit(EV.CHANGE, path37, entry.stats);
16214
16275
  }
16215
16276
  } else {
16216
16277
  dir.add(item);
16217
16278
  this.fsw._symlinkPaths.set(full, linkPath);
16218
- this.fsw._emit(EV.ADD, path34, entry.stats);
16279
+ this.fsw._emit(EV.ADD, path37, entry.stats);
16219
16280
  }
16220
16281
  this.fsw._emitReady();
16221
16282
  return true;
@@ -16244,9 +16305,9 @@ var NodeFsHandler = class {
16244
16305
  return;
16245
16306
  }
16246
16307
  const item = entry.path;
16247
- let path34 = sysPath.join(directory, item);
16308
+ let path37 = sysPath.join(directory, item);
16248
16309
  current.add(item);
16249
- if (entry.stats.isSymbolicLink() && await this._handleSymlink(entry, directory, path34, item)) {
16310
+ if (entry.stats.isSymbolicLink() && await this._handleSymlink(entry, directory, path37, item)) {
16250
16311
  return;
16251
16312
  }
16252
16313
  if (this.fsw.closed) {
@@ -16255,8 +16316,8 @@ var NodeFsHandler = class {
16255
16316
  }
16256
16317
  if (item === target || !target && !previous.has(item)) {
16257
16318
  this.fsw._incrReadyCount();
16258
- path34 = sysPath.join(dir, sysPath.relative(dir, path34));
16259
- this._addToNodeFs(path34, initialAdd, wh, depth + 1);
16319
+ path37 = sysPath.join(dir, sysPath.relative(dir, path37));
16320
+ this._addToNodeFs(path37, initialAdd, wh, depth + 1);
16260
16321
  }
16261
16322
  }).on(EV.ERROR, this._boundHandleError);
16262
16323
  return new Promise((resolve3, reject) => {
@@ -16325,13 +16386,13 @@ var NodeFsHandler = class {
16325
16386
  * @param depth Child path actually targeted for watch
16326
16387
  * @param target Child path actually targeted for watch
16327
16388
  */
16328
- async _addToNodeFs(path34, initialAdd, priorWh, depth, target) {
16389
+ async _addToNodeFs(path37, initialAdd, priorWh, depth, target) {
16329
16390
  const ready = this.fsw._emitReady;
16330
- if (this.fsw._isIgnored(path34) || this.fsw.closed) {
16391
+ if (this.fsw._isIgnored(path37) || this.fsw.closed) {
16331
16392
  ready();
16332
16393
  return false;
16333
16394
  }
16334
- const wh = this.fsw._getWatchHelpers(path34);
16395
+ const wh = this.fsw._getWatchHelpers(path37);
16335
16396
  if (priorWh) {
16336
16397
  wh.filterPath = (entry) => priorWh.filterPath(entry);
16337
16398
  wh.filterDir = (entry) => priorWh.filterDir(entry);
@@ -16347,8 +16408,8 @@ var NodeFsHandler = class {
16347
16408
  const follow = this.fsw.options.followSymlinks;
16348
16409
  let closer;
16349
16410
  if (stats.isDirectory()) {
16350
- const absPath = sysPath.resolve(path34);
16351
- const targetPath = follow ? await fsrealpath(path34) : path34;
16411
+ const absPath = sysPath.resolve(path37);
16412
+ const targetPath = follow ? await fsrealpath(path37) : path37;
16352
16413
  if (this.fsw.closed)
16353
16414
  return;
16354
16415
  closer = await this._handleDir(wh.watchPath, stats, initialAdd, depth, target, wh, targetPath);
@@ -16358,29 +16419,29 @@ var NodeFsHandler = class {
16358
16419
  this.fsw._symlinkPaths.set(absPath, targetPath);
16359
16420
  }
16360
16421
  } else if (stats.isSymbolicLink()) {
16361
- const targetPath = follow ? await fsrealpath(path34) : path34;
16422
+ const targetPath = follow ? await fsrealpath(path37) : path37;
16362
16423
  if (this.fsw.closed)
16363
16424
  return;
16364
16425
  const parent = sysPath.dirname(wh.watchPath);
16365
16426
  this.fsw._getWatchedDir(parent).add(wh.watchPath);
16366
16427
  this.fsw._emit(EV.ADD, wh.watchPath, stats);
16367
- closer = await this._handleDir(parent, stats, initialAdd, depth, path34, wh, targetPath);
16428
+ closer = await this._handleDir(parent, stats, initialAdd, depth, path37, wh, targetPath);
16368
16429
  if (this.fsw.closed)
16369
16430
  return;
16370
16431
  if (targetPath !== void 0) {
16371
- this.fsw._symlinkPaths.set(sysPath.resolve(path34), targetPath);
16432
+ this.fsw._symlinkPaths.set(sysPath.resolve(path37), targetPath);
16372
16433
  }
16373
16434
  } else {
16374
16435
  closer = this._handleFile(wh.watchPath, stats, initialAdd);
16375
16436
  }
16376
16437
  ready();
16377
16438
  if (closer)
16378
- this.fsw._addPathCloser(path34, closer);
16439
+ this.fsw._addPathCloser(path37, closer);
16379
16440
  return false;
16380
16441
  } catch (error) {
16381
16442
  if (this.fsw._handleError(error)) {
16382
16443
  ready();
16383
- return path34;
16444
+ return path37;
16384
16445
  }
16385
16446
  }
16386
16447
  }
@@ -16423,26 +16484,26 @@ function createPattern(matcher) {
16423
16484
  }
16424
16485
  return () => false;
16425
16486
  }
16426
- function normalizePath(path34) {
16427
- if (typeof path34 !== "string")
16487
+ function normalizePath(path37) {
16488
+ if (typeof path37 !== "string")
16428
16489
  throw new Error("string expected");
16429
- path34 = sysPath2.normalize(path34);
16430
- path34 = path34.replace(/\\/g, "/");
16490
+ path37 = sysPath2.normalize(path37);
16491
+ path37 = path37.replace(/\\/g, "/");
16431
16492
  let prepend = false;
16432
- if (path34.startsWith("//"))
16493
+ if (path37.startsWith("//"))
16433
16494
  prepend = true;
16434
16495
  const DOUBLE_SLASH_RE2 = /\/\//;
16435
- while (path34.match(DOUBLE_SLASH_RE2))
16436
- path34 = path34.replace(DOUBLE_SLASH_RE2, "/");
16496
+ while (path37.match(DOUBLE_SLASH_RE2))
16497
+ path37 = path37.replace(DOUBLE_SLASH_RE2, "/");
16437
16498
  if (prepend)
16438
- path34 = "/" + path34;
16439
- return path34;
16499
+ path37 = "/" + path37;
16500
+ return path37;
16440
16501
  }
16441
16502
  function matchPatterns(patterns, testString, stats) {
16442
- const path34 = normalizePath(testString);
16503
+ const path37 = normalizePath(testString);
16443
16504
  for (let index = 0; index < patterns.length; index++) {
16444
16505
  const pattern = patterns[index];
16445
- if (pattern(path34, stats)) {
16506
+ if (pattern(path37, stats)) {
16446
16507
  return true;
16447
16508
  }
16448
16509
  }
@@ -16482,19 +16543,19 @@ var toUnix = (string) => {
16482
16543
  }
16483
16544
  return str;
16484
16545
  };
16485
- var normalizePathToUnix = (path34) => toUnix(sysPath2.normalize(toUnix(path34)));
16486
- var normalizeIgnored = (cwd = "") => (path34) => {
16487
- if (typeof path34 === "string") {
16488
- 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));
16489
16550
  } else {
16490
- return path34;
16551
+ return path37;
16491
16552
  }
16492
16553
  };
16493
- var getAbsolutePath = (path34, cwd) => {
16494
- if (sysPath2.isAbsolute(path34)) {
16495
- return path34;
16554
+ var getAbsolutePath = (path37, cwd) => {
16555
+ if (sysPath2.isAbsolute(path37)) {
16556
+ return path37;
16496
16557
  }
16497
- return sysPath2.join(cwd, path34);
16558
+ return sysPath2.join(cwd, path37);
16498
16559
  };
16499
16560
  var EMPTY_SET = Object.freeze(/* @__PURE__ */ new Set());
16500
16561
  var DirEntry = class {
@@ -16549,10 +16610,10 @@ var DirEntry = class {
16549
16610
  var STAT_METHOD_F = "stat";
16550
16611
  var STAT_METHOD_L = "lstat";
16551
16612
  var WatchHelper = class {
16552
- constructor(path34, follow, fsw) {
16613
+ constructor(path37, follow, fsw) {
16553
16614
  this.fsw = fsw;
16554
- const watchPath = path34;
16555
- this.path = path34 = path34.replace(REPLACER_RE, "");
16615
+ const watchPath = path37;
16616
+ this.path = path37 = path37.replace(REPLACER_RE, "");
16556
16617
  this.watchPath = watchPath;
16557
16618
  this.fullWatchPath = sysPath2.resolve(watchPath);
16558
16619
  this.dirParts = [];
@@ -16674,20 +16735,20 @@ var FSWatcher = class extends EventEmitter {
16674
16735
  this._closePromise = void 0;
16675
16736
  let paths = unifyPaths(paths_);
16676
16737
  if (cwd) {
16677
- paths = paths.map((path34) => {
16678
- const absPath = getAbsolutePath(path34, cwd);
16738
+ paths = paths.map((path37) => {
16739
+ const absPath = getAbsolutePath(path37, cwd);
16679
16740
  return absPath;
16680
16741
  });
16681
16742
  }
16682
- paths.forEach((path34) => {
16683
- this._removeIgnoredPath(path34);
16743
+ paths.forEach((path37) => {
16744
+ this._removeIgnoredPath(path37);
16684
16745
  });
16685
16746
  this._userIgnored = void 0;
16686
16747
  if (!this._readyCount)
16687
16748
  this._readyCount = 0;
16688
16749
  this._readyCount += paths.length;
16689
- Promise.all(paths.map(async (path34) => {
16690
- 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);
16691
16752
  if (res)
16692
16753
  this._emitReady();
16693
16754
  return res;
@@ -16709,17 +16770,17 @@ var FSWatcher = class extends EventEmitter {
16709
16770
  return this;
16710
16771
  const paths = unifyPaths(paths_);
16711
16772
  const { cwd } = this.options;
16712
- paths.forEach((path34) => {
16713
- if (!sysPath2.isAbsolute(path34) && !this._closers.has(path34)) {
16773
+ paths.forEach((path37) => {
16774
+ if (!sysPath2.isAbsolute(path37) && !this._closers.has(path37)) {
16714
16775
  if (cwd)
16715
- path34 = sysPath2.join(cwd, path34);
16716
- path34 = sysPath2.resolve(path34);
16776
+ path37 = sysPath2.join(cwd, path37);
16777
+ path37 = sysPath2.resolve(path37);
16717
16778
  }
16718
- this._closePath(path34);
16719
- this._addIgnoredPath(path34);
16720
- if (this._watched.has(path34)) {
16779
+ this._closePath(path37);
16780
+ this._addIgnoredPath(path37);
16781
+ if (this._watched.has(path37)) {
16721
16782
  this._addIgnoredPath({
16722
- path: path34,
16783
+ path: path37,
16723
16784
  recursive: true
16724
16785
  });
16725
16786
  }
@@ -16783,38 +16844,38 @@ var FSWatcher = class extends EventEmitter {
16783
16844
  * @param stats arguments to be passed with event
16784
16845
  * @returns the error if defined, otherwise the value of the FSWatcher instance's `closed` flag
16785
16846
  */
16786
- async _emit(event, path34, stats) {
16847
+ async _emit(event, path37, stats) {
16787
16848
  if (this.closed)
16788
16849
  return;
16789
16850
  const opts = this.options;
16790
16851
  if (isWindows)
16791
- path34 = sysPath2.normalize(path34);
16852
+ path37 = sysPath2.normalize(path37);
16792
16853
  if (opts.cwd)
16793
- path34 = sysPath2.relative(opts.cwd, path34);
16794
- const args = [path34];
16854
+ path37 = sysPath2.relative(opts.cwd, path37);
16855
+ const args = [path37];
16795
16856
  if (stats != null)
16796
16857
  args.push(stats);
16797
16858
  const awf = opts.awaitWriteFinish;
16798
16859
  let pw;
16799
- if (awf && (pw = this._pendingWrites.get(path34))) {
16860
+ if (awf && (pw = this._pendingWrites.get(path37))) {
16800
16861
  pw.lastChange = /* @__PURE__ */ new Date();
16801
16862
  return this;
16802
16863
  }
16803
16864
  if (opts.atomic) {
16804
16865
  if (event === EVENTS.UNLINK) {
16805
- this._pendingUnlinks.set(path34, [event, ...args]);
16866
+ this._pendingUnlinks.set(path37, [event, ...args]);
16806
16867
  setTimeout(() => {
16807
- this._pendingUnlinks.forEach((entry, path35) => {
16868
+ this._pendingUnlinks.forEach((entry, path38) => {
16808
16869
  this.emit(...entry);
16809
16870
  this.emit(EVENTS.ALL, ...entry);
16810
- this._pendingUnlinks.delete(path35);
16871
+ this._pendingUnlinks.delete(path38);
16811
16872
  });
16812
16873
  }, typeof opts.atomic === "number" ? opts.atomic : 100);
16813
16874
  return this;
16814
16875
  }
16815
- if (event === EVENTS.ADD && this._pendingUnlinks.has(path34)) {
16876
+ if (event === EVENTS.ADD && this._pendingUnlinks.has(path37)) {
16816
16877
  event = EVENTS.CHANGE;
16817
- this._pendingUnlinks.delete(path34);
16878
+ this._pendingUnlinks.delete(path37);
16818
16879
  }
16819
16880
  }
16820
16881
  if (awf && (event === EVENTS.ADD || event === EVENTS.CHANGE) && this._readyEmitted) {
@@ -16832,16 +16893,16 @@ var FSWatcher = class extends EventEmitter {
16832
16893
  this.emitWithAll(event, args);
16833
16894
  }
16834
16895
  };
16835
- this._awaitWriteFinish(path34, awf.stabilityThreshold, event, awfEmit);
16896
+ this._awaitWriteFinish(path37, awf.stabilityThreshold, event, awfEmit);
16836
16897
  return this;
16837
16898
  }
16838
16899
  if (event === EVENTS.CHANGE) {
16839
- const isThrottled = !this._throttle(EVENTS.CHANGE, path34, 50);
16900
+ const isThrottled = !this._throttle(EVENTS.CHANGE, path37, 50);
16840
16901
  if (isThrottled)
16841
16902
  return this;
16842
16903
  }
16843
16904
  if (opts.alwaysStat && stats === void 0 && (event === EVENTS.ADD || event === EVENTS.ADD_DIR || event === EVENTS.CHANGE)) {
16844
- const fullPath = opts.cwd ? sysPath2.join(opts.cwd, path34) : path34;
16905
+ const fullPath = opts.cwd ? sysPath2.join(opts.cwd, path37) : path37;
16845
16906
  let stats2;
16846
16907
  try {
16847
16908
  stats2 = await stat3(fullPath);
@@ -16872,23 +16933,23 @@ var FSWatcher = class extends EventEmitter {
16872
16933
  * @param timeout duration of time to suppress duplicate actions
16873
16934
  * @returns tracking object or false if action should be suppressed
16874
16935
  */
16875
- _throttle(actionType, path34, timeout) {
16936
+ _throttle(actionType, path37, timeout) {
16876
16937
  if (!this._throttled.has(actionType)) {
16877
16938
  this._throttled.set(actionType, /* @__PURE__ */ new Map());
16878
16939
  }
16879
16940
  const action = this._throttled.get(actionType);
16880
16941
  if (!action)
16881
16942
  throw new Error("invalid throttle");
16882
- const actionPath = action.get(path34);
16943
+ const actionPath = action.get(path37);
16883
16944
  if (actionPath) {
16884
16945
  actionPath.count++;
16885
16946
  return false;
16886
16947
  }
16887
16948
  let timeoutObject;
16888
16949
  const clear = () => {
16889
- const item = action.get(path34);
16950
+ const item = action.get(path37);
16890
16951
  const count = item ? item.count : 0;
16891
- action.delete(path34);
16952
+ action.delete(path37);
16892
16953
  clearTimeout(timeoutObject);
16893
16954
  if (item)
16894
16955
  clearTimeout(item.timeoutObject);
@@ -16896,7 +16957,7 @@ var FSWatcher = class extends EventEmitter {
16896
16957
  };
16897
16958
  timeoutObject = setTimeout(clear, timeout);
16898
16959
  const thr = { timeoutObject, clear, count: 0 };
16899
- action.set(path34, thr);
16960
+ action.set(path37, thr);
16900
16961
  return thr;
16901
16962
  }
16902
16963
  _incrReadyCount() {
@@ -16910,44 +16971,44 @@ var FSWatcher = class extends EventEmitter {
16910
16971
  * @param event
16911
16972
  * @param awfEmit Callback to be called when ready for event to be emitted.
16912
16973
  */
16913
- _awaitWriteFinish(path34, threshold, event, awfEmit) {
16974
+ _awaitWriteFinish(path37, threshold, event, awfEmit) {
16914
16975
  const awf = this.options.awaitWriteFinish;
16915
16976
  if (typeof awf !== "object")
16916
16977
  return;
16917
16978
  const pollInterval = awf.pollInterval;
16918
16979
  let timeoutHandler;
16919
- let fullPath = path34;
16920
- if (this.options.cwd && !sysPath2.isAbsolute(path34)) {
16921
- 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);
16922
16983
  }
16923
16984
  const now = /* @__PURE__ */ new Date();
16924
16985
  const writes = this._pendingWrites;
16925
16986
  function awaitWriteFinishFn(prevStat) {
16926
16987
  statcb(fullPath, (err, curStat) => {
16927
- if (err || !writes.has(path34)) {
16988
+ if (err || !writes.has(path37)) {
16928
16989
  if (err && err.code !== "ENOENT")
16929
16990
  awfEmit(err);
16930
16991
  return;
16931
16992
  }
16932
16993
  const now2 = Number(/* @__PURE__ */ new Date());
16933
16994
  if (prevStat && curStat.size !== prevStat.size) {
16934
- writes.get(path34).lastChange = now2;
16995
+ writes.get(path37).lastChange = now2;
16935
16996
  }
16936
- const pw = writes.get(path34);
16997
+ const pw = writes.get(path37);
16937
16998
  const df = now2 - pw.lastChange;
16938
16999
  if (df >= threshold) {
16939
- writes.delete(path34);
17000
+ writes.delete(path37);
16940
17001
  awfEmit(void 0, curStat);
16941
17002
  } else {
16942
17003
  timeoutHandler = setTimeout(awaitWriteFinishFn, pollInterval, curStat);
16943
17004
  }
16944
17005
  });
16945
17006
  }
16946
- if (!writes.has(path34)) {
16947
- writes.set(path34, {
17007
+ if (!writes.has(path37)) {
17008
+ writes.set(path37, {
16948
17009
  lastChange: now,
16949
17010
  cancelWait: () => {
16950
- writes.delete(path34);
17011
+ writes.delete(path37);
16951
17012
  clearTimeout(timeoutHandler);
16952
17013
  return event;
16953
17014
  }
@@ -16958,8 +17019,8 @@ var FSWatcher = class extends EventEmitter {
16958
17019
  /**
16959
17020
  * Determines whether user has asked to ignore this path.
16960
17021
  */
16961
- _isIgnored(path34, stats) {
16962
- if (this.options.atomic && DOT_RE.test(path34))
17022
+ _isIgnored(path37, stats) {
17023
+ if (this.options.atomic && DOT_RE.test(path37))
16963
17024
  return true;
16964
17025
  if (!this._userIgnored) {
16965
17026
  const { cwd } = this.options;
@@ -16969,17 +17030,17 @@ var FSWatcher = class extends EventEmitter {
16969
17030
  const list = [...ignoredPaths.map(normalizeIgnored(cwd)), ...ignored];
16970
17031
  this._userIgnored = anymatch(list, void 0);
16971
17032
  }
16972
- return this._userIgnored(path34, stats);
17033
+ return this._userIgnored(path37, stats);
16973
17034
  }
16974
- _isntIgnored(path34, stat4) {
16975
- return !this._isIgnored(path34, stat4);
17035
+ _isntIgnored(path37, stat4) {
17036
+ return !this._isIgnored(path37, stat4);
16976
17037
  }
16977
17038
  /**
16978
17039
  * Provides a set of common helpers and properties relating to symlink handling.
16979
17040
  * @param path file or directory pattern being watched
16980
17041
  */
16981
- _getWatchHelpers(path34) {
16982
- return new WatchHelper(path34, this.options.followSymlinks, this);
17042
+ _getWatchHelpers(path37) {
17043
+ return new WatchHelper(path37, this.options.followSymlinks, this);
16983
17044
  }
16984
17045
  // Directory helpers
16985
17046
  // -----------------
@@ -17011,63 +17072,63 @@ var FSWatcher = class extends EventEmitter {
17011
17072
  * @param item base path of item/directory
17012
17073
  */
17013
17074
  _remove(directory, item, isDirectory) {
17014
- const path34 = sysPath2.join(directory, item);
17015
- const fullPath = sysPath2.resolve(path34);
17016
- isDirectory = isDirectory != null ? isDirectory : this._watched.has(path34) || this._watched.has(fullPath);
17017
- 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))
17018
17079
  return;
17019
17080
  if (!isDirectory && this._watched.size === 1) {
17020
17081
  this.add(directory, item, true);
17021
17082
  }
17022
- const wp = this._getWatchedDir(path34);
17083
+ const wp = this._getWatchedDir(path37);
17023
17084
  const nestedDirectoryChildren = wp.getChildren();
17024
- nestedDirectoryChildren.forEach((nested) => this._remove(path34, nested));
17085
+ nestedDirectoryChildren.forEach((nested) => this._remove(path37, nested));
17025
17086
  const parent = this._getWatchedDir(directory);
17026
17087
  const wasTracked = parent.has(item);
17027
17088
  parent.remove(item);
17028
17089
  if (this._symlinkPaths.has(fullPath)) {
17029
17090
  this._symlinkPaths.delete(fullPath);
17030
17091
  }
17031
- let relPath = path34;
17092
+ let relPath = path37;
17032
17093
  if (this.options.cwd)
17033
- relPath = sysPath2.relative(this.options.cwd, path34);
17094
+ relPath = sysPath2.relative(this.options.cwd, path37);
17034
17095
  if (this.options.awaitWriteFinish && this._pendingWrites.has(relPath)) {
17035
17096
  const event = this._pendingWrites.get(relPath).cancelWait();
17036
17097
  if (event === EVENTS.ADD)
17037
17098
  return;
17038
17099
  }
17039
- this._watched.delete(path34);
17100
+ this._watched.delete(path37);
17040
17101
  this._watched.delete(fullPath);
17041
17102
  const eventName = isDirectory ? EVENTS.UNLINK_DIR : EVENTS.UNLINK;
17042
- if (wasTracked && !this._isIgnored(path34))
17043
- this._emit(eventName, path34);
17044
- this._closePath(path34);
17103
+ if (wasTracked && !this._isIgnored(path37))
17104
+ this._emit(eventName, path37);
17105
+ this._closePath(path37);
17045
17106
  }
17046
17107
  /**
17047
17108
  * Closes all watchers for a path
17048
17109
  */
17049
- _closePath(path34) {
17050
- this._closeFile(path34);
17051
- const dir = sysPath2.dirname(path34);
17052
- 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));
17053
17114
  }
17054
17115
  /**
17055
17116
  * Closes only file-specific watchers
17056
17117
  */
17057
- _closeFile(path34) {
17058
- const closers = this._closers.get(path34);
17118
+ _closeFile(path37) {
17119
+ const closers = this._closers.get(path37);
17059
17120
  if (!closers)
17060
17121
  return;
17061
17122
  closers.forEach((closer) => closer());
17062
- this._closers.delete(path34);
17123
+ this._closers.delete(path37);
17063
17124
  }
17064
- _addPathCloser(path34, closer) {
17125
+ _addPathCloser(path37, closer) {
17065
17126
  if (!closer)
17066
17127
  return;
17067
- let list = this._closers.get(path34);
17128
+ let list = this._closers.get(path37);
17068
17129
  if (!list) {
17069
17130
  list = [];
17070
- this._closers.set(path34, list);
17131
+ this._closers.set(path37, list);
17071
17132
  }
17072
17133
  list.push(closer);
17073
17134
  }
@@ -17437,11 +17498,11 @@ function createStrictRateLimiterFromConfig(config) {
17437
17498
  }
17438
17499
 
17439
17500
  // modules/server/middleware/auto-rate-limit.ts
17440
- function matchesStrictPattern(path34, patterns) {
17501
+ function matchesStrictPattern(path37, patterns) {
17441
17502
  for (const pattern of patterns) {
17442
17503
  const regexPattern = pattern.replace(/\*\*/g, ".*").replace(/\*/g, "[^/]*").replace(/\//g, "\\/");
17443
17504
  const regex = new RegExp(`^${regexPattern}$`);
17444
- if (regex.test(path34)) {
17505
+ if (regex.test(path37)) {
17445
17506
  return true;
17446
17507
  }
17447
17508
  }
@@ -17631,9 +17692,441 @@ async function handleApiRequest(options) {
17631
17692
  }
17632
17693
  }
17633
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
+
17634
18127
  // modules/server/routes.ts
17635
18128
  init_globals();
17636
- import path31 from "path";
18129
+ import path34 from "path";
17637
18130
  var cachedRewriteLoader = null;
17638
18131
  var cachedProjectRoot = null;
17639
18132
  var cachedIsDev = null;
@@ -17661,10 +18154,20 @@ function setupRoutes(options) {
17661
18154
  } = options;
17662
18155
  const routeChunks = routeLoader.loadRouteChunks();
17663
18156
  const rewriteLoader = getRewriteLoader(projectRoot, isDev);
17664
- const ssgOutDir = path31.join(
17665
- 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),
17666
18159
  "ssg"
17667
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
+ }
17668
18171
  app.all("/api/*", async (req, res) => {
17669
18172
  const apiRoutes = isDev && getRoutes ? (await getRoutes()).apiRoutes : initialApiRoutes;
17670
18173
  const serverConfig = await getServerConfig(projectRoot);
@@ -18711,7 +19214,7 @@ import cors from "cors";
18711
19214
  import helmet from "helmet";
18712
19215
  import cookieParser from "cookie-parser";
18713
19216
  import compression from "compression";
18714
- import crypto from "crypto";
19217
+ import crypto2 from "crypto";
18715
19218
  var setupApplication = async ({
18716
19219
  projectRoot
18717
19220
  }) => {
@@ -18820,7 +19323,7 @@ var setupApplication = async ({
18820
19323
  if (process.env.NODE_ENV !== "development" && security?.contentSecurityPolicy !== false) {
18821
19324
  app.use(
18822
19325
  (req, res, next) => {
18823
- const nonce = crypto.randomBytes(16).toString("base64");
19326
+ const nonce = crypto2.randomBytes(16).toString("base64");
18824
19327
  res.locals.nonce = nonce;
18825
19328
  next();
18826
19329
  }
@@ -18891,8 +19394,8 @@ var setupApplication = async ({
18891
19394
 
18892
19395
  // src/server.ts
18893
19396
  import dotenv2 from "dotenv";
18894
- var envPath = path32.join(process.cwd(), ".env");
18895
- if (fs24.existsSync(envPath)) {
19397
+ var envPath = path35.join(process.cwd(), ".env");
19398
+ if (fs26.existsSync(envPath)) {
18896
19399
  dotenv2.config({ path: envPath });
18897
19400
  } else {
18898
19401
  dotenv2.config();
@@ -18913,8 +19416,8 @@ async function startServer(options = {}) {
18913
19416
  }
18914
19417
  const port = options.port ?? (process.env.PORT ? parseInt(process.env.PORT, 10) : void 0) ?? config.server.port;
18915
19418
  const host = process.env.HOST ?? (!isDev ? "0.0.0.0" : void 0) ?? config.server.host;
18916
- const appDir = options.appDir ?? (isDev ? getAppDir(projectRoot, config) : path32.join(getBuildDir(projectRoot, config), "server"));
18917
- 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)) {
18918
19421
  logger4.error("Compiled directory not found", void 0, {
18919
19422
  buildDir: config.directories.build,
18920
19423
  appDir,
@@ -19110,7 +19613,7 @@ async function run() {
19110
19613
  }
19111
19614
  const args = parseArgs(argv.slice(1));
19112
19615
  const projectRoot = process2.cwd();
19113
- const appDir = path33.resolve(projectRoot, args.appDir || "app");
19616
+ const appDir = path36.resolve(projectRoot, args.appDir || "app");
19114
19617
  const port = typeof args.port === "string" && args.port.trim().length > 0 ? Number(args.port) : 3e3;
19115
19618
  switch (command) {
19116
19619
  case "dev": {