@lolyjs/core 0.2.0-alpha.23 → 0.2.0-alpha.25

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/README.md CHANGED
@@ -408,13 +408,21 @@ Loly serves static files from the `public/` directory at the root of your applic
408
408
  public/
409
409
  ├── sitemap.xml # Available at /sitemap.xml
410
410
  ├── robots.txt # Available at /robots.txt
411
- ├── favicon.ico # Available at /favicon.ico
411
+ ├── favicon.ico # Available at /favicon.ico (or favicon.png)
412
+ ├── favicon.png # Available at /favicon.png (alternative to .ico)
412
413
  └── assets/
413
414
  ├── logo.png # Available at /assets/logo.png
414
415
  └── images/ # Available at /assets/images/*
415
416
  └── hero.jpg
416
417
  ```
417
418
 
419
+ **Favicon:**
420
+ Place your favicon in the `public/` directory as either `favicon.ico` or `favicon.png`. The framework automatically detects and includes it in the HTML head with the correct MIME type:
421
+ - `public/favicon.ico` → `/favicon.ico` (type: `image/x-icon`)
422
+ - `public/favicon.png` → `/favicon.png` (type: `image/png`)
423
+
424
+ If both exist, `favicon.ico` takes priority (checked first).
425
+
418
426
  **SEO Example:**
419
427
 
420
428
  Create `public/sitemap.xml`:
@@ -440,6 +448,12 @@ Sitemap: https://example.com/sitemap.xml
440
448
 
441
449
  Both files will be automatically available at `/sitemap.xml` and `/robots.txt` respectively, and search engines will find them at the standard locations.
442
450
 
451
+ **Important Notes:**
452
+ - **All static files** (including favicons) must be placed in the `public/` directory
453
+ - The framework **only** looks for favicons in `public/` (not in the root or `app/` directory)
454
+ - Favicons are automatically detected and included in the HTML `<head>` with the correct MIME type
455
+ - Static files have **priority over dynamic routes** - perfect for SEO files
456
+
443
457
  **Configuration:**
444
458
  The static directory can be customized in `loly.config.ts`:
445
459
  ```tsx
package/dist/cli.cjs CHANGED
@@ -11459,22 +11459,36 @@ function copyStaticAssets(projectRoot, outDir) {
11459
11459
  const assetsSrc = import_path12.default.join(projectRoot, "assets");
11460
11460
  const assetsDest = import_path12.default.join(outDir, "assets");
11461
11461
  copyDirRecursive(assetsSrc, assetsDest);
11462
- const appDir = import_path12.default.join(projectRoot, "app");
11462
+ const publicDir = import_path12.default.join(projectRoot, "public");
11463
11463
  const candidates = ["favicon.ico", "favicon.png"];
11464
11464
  for (const name of candidates) {
11465
- const fromApp = import_path12.default.join(appDir, name);
11466
- const fromRoot = import_path12.default.join(projectRoot, name);
11467
- let src = null;
11468
- if (import_fs10.default.existsSync(fromApp)) src = fromApp;
11469
- else if (import_fs10.default.existsSync(fromRoot)) src = fromRoot;
11470
- if (src) {
11465
+ const fromPublic = import_path12.default.join(publicDir, name);
11466
+ if (import_fs10.default.existsSync(fromPublic)) {
11471
11467
  const dest = import_path12.default.join(outDir, name);
11472
11468
  ensureDir(import_path12.default.dirname(dest));
11473
- import_fs10.default.copyFileSync(src, dest);
11469
+ import_fs10.default.copyFileSync(fromPublic, dest);
11474
11470
  break;
11475
11471
  }
11476
11472
  }
11477
11473
  }
11474
+ function getFaviconInfo(projectRoot, staticDir = "public", isDev = false) {
11475
+ const candidates = [
11476
+ { name: "favicon.ico", type: "image/x-icon" },
11477
+ { name: "favicon.png", type: "image/png" }
11478
+ ];
11479
+ const publicDir = import_path12.default.join(projectRoot, staticDir);
11480
+ for (const candidate of candidates) {
11481
+ const publicPath = import_path12.default.join(publicDir, candidate.name);
11482
+ if (import_fs10.default.existsSync(publicPath)) {
11483
+ return {
11484
+ path: `/${candidate.name}`,
11485
+ // Served at root from public/
11486
+ type: candidate.type
11487
+ };
11488
+ }
11489
+ }
11490
+ return null;
11491
+ }
11478
11492
  function generateAssetManifest(outDir, stats) {
11479
11493
  const manifest = {
11480
11494
  client: {
@@ -11959,8 +11973,12 @@ function createDocumentTree(options) {
11959
11973
  clientJsPath = "/static/client.js",
11960
11974
  clientCssPath = "/static/client.css",
11961
11975
  nonce,
11962
- includeInlineScripts = true
11976
+ includeInlineScripts = true,
11963
11977
  // Default true - scripts inline in body for both SSR and SSG
11978
+ faviconPath = FAVICON_PATH,
11979
+ // Default to /static/favicon.png for backward compatibility
11980
+ faviconType = "image/png"
11981
+ // Default to PNG for backward compatibility
11964
11982
  } = options;
11965
11983
  const metaObj = meta ?? null;
11966
11984
  const title = metaObj?.title ?? titleFallback ?? "My Framework Dev";
@@ -12267,10 +12285,11 @@ function createDocumentTree(options) {
12267
12285
  href: chunkHref,
12268
12286
  as: "script"
12269
12287
  }),
12270
- import_react.default.createElement("link", {
12288
+ faviconPath && import_react.default.createElement("link", {
12289
+ key: "favicon",
12271
12290
  rel: "icon",
12272
- href: FAVICON_PATH,
12273
- type: "image/png"
12291
+ href: faviconPath,
12292
+ type: faviconType || (faviconPath.endsWith(".ico") ? "image/x-icon" : "image/png")
12274
12293
  }),
12275
12294
  import_react.default.createElement("link", {
12276
12295
  rel: "stylesheet",
@@ -12800,7 +12819,7 @@ async function handlePageRequest(options) {
12800
12819
  const { errorPage, req, res, routeChunks, theme, projectRoot } = options;
12801
12820
  const reqLogger = getRequestLogger(req);
12802
12821
  if (errorPage) {
12803
- await renderErrorPageWithStream(errorPage, req, res, error, routeChunks || {}, theme, projectRoot, options.env);
12822
+ await renderErrorPageWithStream(errorPage, req, res, error, routeChunks || {}, theme, projectRoot, options.env, options.config);
12804
12823
  } else {
12805
12824
  reqLogger.error("Unhandled error in page request", error, {
12806
12825
  urlPath: options.urlPath,
@@ -12826,11 +12845,13 @@ async function handlePageRequestInternal(options) {
12826
12845
  env = "dev",
12827
12846
  ssgOutDir,
12828
12847
  theme,
12829
- projectRoot
12848
+ projectRoot,
12849
+ config
12830
12850
  } = options;
12831
12851
  const clientJsPath = env === "dev" ? "/static/client.js" : projectRoot ? getClientJsPath(projectRoot) : "/static/client.js";
12832
12852
  const clientCssPath = env === "dev" ? "/static/client.css" : projectRoot ? getClientCssPath(projectRoot) : "/static/client.css";
12833
12853
  const assetManifest = env === "prod" && projectRoot ? loadAssetManifest(projectRoot) : null;
12854
+ const faviconInfo = projectRoot && config ? getFaviconInfo(projectRoot, config.directories.static, env === "dev") : null;
12834
12855
  const isDataReq = isDataRequest(req);
12835
12856
  const skipLayoutHooks = isDataReq && req.headers["x-skip-layout-hooks"] === "true";
12836
12857
  if (env === "prod" && ssgOutDir) {
@@ -12945,7 +12966,9 @@ async function handlePageRequestInternal(options) {
12945
12966
  theme,
12946
12967
  clientJsPath,
12947
12968
  clientCssPath,
12948
- nonce: nonce2
12969
+ nonce: nonce2,
12970
+ faviconPath: faviconInfo?.path || null,
12971
+ faviconType: faviconInfo?.type || null
12949
12972
  });
12950
12973
  let didError2 = false;
12951
12974
  const { pipe: pipe2, abort: abort2 } = (0, import_server.renderToPipeableStream)(documentTree2, {
@@ -12960,7 +12983,7 @@ async function handlePageRequestInternal(options) {
12960
12983
  const reqLogger2 = getRequestLogger(req);
12961
12984
  reqLogger2.error("SSR shell error", err, { route: "not-found" });
12962
12985
  if (!res.headersSent && errorPage) {
12963
- renderErrorPageWithStream(errorPage, req, res, err, routeChunks, theme, projectRoot, env);
12986
+ renderErrorPageWithStream(errorPage, req, res, err, routeChunks, theme, projectRoot, env, config);
12964
12987
  } else if (!res.headersSent) {
12965
12988
  res.statusCode = 500;
12966
12989
  res.setHeader("Content-Type", "text/html; charset=utf-8");
@@ -13075,7 +13098,7 @@ async function handlePageRequestInternal(options) {
13075
13098
  return;
13076
13099
  } else {
13077
13100
  if (errorPage) {
13078
- await renderErrorPageWithStream(errorPage, req, res, error, routeChunks, theme, projectRoot, env);
13101
+ await renderErrorPageWithStream(errorPage, req, res, error, routeChunks, theme, projectRoot, env, config);
13079
13102
  return;
13080
13103
  } else {
13081
13104
  throw error;
@@ -13153,7 +13176,9 @@ async function handlePageRequestInternal(options) {
13153
13176
  theme,
13154
13177
  clientJsPath,
13155
13178
  clientCssPath,
13156
- nonce
13179
+ nonce,
13180
+ faviconPath: faviconInfo?.path || null,
13181
+ faviconType: faviconInfo?.type || null
13157
13182
  });
13158
13183
  let didError = false;
13159
13184
  const { pipe, abort } = (0, import_server.renderToPipeableStream)(documentTree, {
@@ -13201,7 +13226,7 @@ async function handlePageRequestInternal(options) {
13201
13226
  abort();
13202
13227
  });
13203
13228
  }
13204
- async function renderErrorPageWithStream(errorPage, req, res, error, routeChunks, theme, projectRoot, env = "dev") {
13229
+ async function renderErrorPageWithStream(errorPage, req, res, error, routeChunks, theme, projectRoot, env = "dev", config) {
13205
13230
  try {
13206
13231
  const isDataReq = isDataRequest(req);
13207
13232
  const skipLayoutHooks = isDataReq && req.headers["x-skip-layout-hooks"] === "true";
@@ -13212,6 +13237,7 @@ async function renderErrorPageWithStream(errorPage, req, res, error, routeChunks
13212
13237
  pathname: req.path,
13213
13238
  locals: { error }
13214
13239
  };
13240
+ const faviconInfo = projectRoot && config ? getFaviconInfo(projectRoot, config.directories.static, env === "dev") : null;
13215
13241
  const layoutProps = {};
13216
13242
  const reqLogger = getRequestLogger(req);
13217
13243
  if (!skipLayoutHooks && errorPage.layoutServerHooks && errorPage.layoutServerHooks.length > 0) {
@@ -13317,7 +13343,9 @@ async function renderErrorPageWithStream(errorPage, req, res, error, routeChunks
13317
13343
  theme,
13318
13344
  clientJsPath,
13319
13345
  clientCssPath,
13320
- nonce
13346
+ nonce,
13347
+ faviconPath: faviconInfo?.path || null,
13348
+ faviconType: faviconInfo?.type || null
13321
13349
  });
13322
13350
  let didError = false;
13323
13351
  const { pipe, abort } = (0, import_server.renderToPipeableStream)(documentTree, {
@@ -13361,11 +13389,12 @@ async function renderErrorPageWithStream(errorPage, req, res, error, routeChunks
13361
13389
  }
13362
13390
 
13363
13391
  // modules/build/ssg/renderer.ts
13364
- async function renderStaticRoute(projectRoot, ssgOutDir, route, urlPath, params) {
13392
+ async function renderStaticRoute(projectRoot, ssgOutDir, route, urlPath, params, config) {
13365
13393
  const routeChunks = loadChunksFromManifest(projectRoot);
13366
13394
  const assetManifest = loadAssetManifest(projectRoot);
13367
13395
  const clientJsPath = getClientJsPath(projectRoot);
13368
13396
  const clientCssPath = getClientCssPath(projectRoot);
13397
+ const faviconInfo = config ? getFaviconInfo(projectRoot, config.directories.static, false) : null;
13369
13398
  const chunkName = routeChunks[route.pattern];
13370
13399
  let chunkHref = null;
13371
13400
  if (chunkName != null) {
@@ -13473,8 +13502,10 @@ async function renderStaticRoute(projectRoot, ssgOutDir, route, urlPath, params)
13473
13502
  entrypointFiles,
13474
13503
  clientJsPath,
13475
13504
  clientCssPath,
13476
- includeInlineScripts: true
13505
+ includeInlineScripts: true,
13477
13506
  // SSG needs inline scripts (renderToString doesn't support bootstrapScripts)
13507
+ faviconPath: faviconInfo?.path || null,
13508
+ faviconType: faviconInfo?.type || null
13478
13509
  });
13479
13510
  const html = "<!DOCTYPE html>" + (0, import_server2.renderToString)(documentTree);
13480
13511
  const dir = pathToOutDir(ssgOutDir, urlPath);
@@ -13487,7 +13518,7 @@ async function renderStaticRoute(projectRoot, ssgOutDir, route, urlPath, params)
13487
13518
 
13488
13519
  // modules/build/ssg/builder.ts
13489
13520
  init_globals();
13490
- async function buildStaticPages(projectRoot, routes) {
13521
+ async function buildStaticPages(projectRoot, routes, config) {
13491
13522
  const ssgOutDir = import_path22.default.join(projectRoot, BUILD_FOLDER_NAME, "ssg");
13492
13523
  ensureDir(ssgOutDir);
13493
13524
  for (const route of routes) {
@@ -13536,7 +13567,7 @@ async function buildStaticPages(projectRoot, routes) {
13536
13567
  }
13537
13568
  for (const params of allParams) {
13538
13569
  const urlPath = buildPathFromPattern(route.pattern, params);
13539
- await renderStaticRoute(projectRoot, ssgOutDir, route, urlPath, params);
13570
+ await renderStaticRoute(projectRoot, ssgOutDir, route, urlPath, params, config);
13540
13571
  }
13541
13572
  }
13542
13573
  console.log(`\u2705 [framework][ssg] Finished building all static pages`);
@@ -14185,7 +14216,7 @@ async function buildApp(options = {}) {
14185
14216
  writeClientBoostrapManifest(projectRoot);
14186
14217
  writeClientRoutesManifest(routes, projectRoot);
14187
14218
  await buildClientBundle(projectRoot);
14188
- await buildStaticPages(projectRoot, routes);
14219
+ await buildStaticPages(projectRoot, routes, config);
14189
14220
  delete process.env.LOLY_BUILD;
14190
14221
  console.log(`[framework][build] Build completed successfully`);
14191
14222
  }
@@ -16427,7 +16458,8 @@ function setupRoutes(options) {
16427
16458
  env: isDev ? "dev" : "prod",
16428
16459
  ssgOutDir,
16429
16460
  theme: req.cookies?.theme || "light",
16430
- projectRoot
16461
+ projectRoot,
16462
+ config
16431
16463
  });
16432
16464
  });
16433
16465
  }
@@ -17041,7 +17073,6 @@ async function executeGuard(guardFn, ctx) {
17041
17073
  const guardCtx = {
17042
17074
  user: ctx.user,
17043
17075
  req: ctx.req,
17044
- socket: ctx.socket,
17045
17076
  namespace: ctx.pathname
17046
17077
  };
17047
17078
  const result = await guardFn(guardCtx);
@@ -17148,9 +17179,6 @@ var generateActions = (socket, namespace, presence) => {
17148
17179
  return {
17149
17180
  emit: async (event, payload) => {
17150
17181
  if (!presence) {
17151
- console.warn(
17152
- "[loly:realtime] toUser() requires presence manager. Make sure realtime is properly configured."
17153
- );
17154
17182
  return;
17155
17183
  }
17156
17184
  const socketIds = await presence.getSocketsForUser(userId);
@@ -17253,19 +17281,12 @@ async function setupWssEvents(options) {
17253
17281
  const subClient = pubClient.duplicate();
17254
17282
  io.adapter(createAdapter(pubClient, subClient));
17255
17283
  } catch (error) {
17256
- console.error(
17257
- "[loly:realtime] Failed to setup Redis adapter:",
17258
- error instanceof Error ? error.message : String(error)
17259
- );
17260
17284
  throw error;
17261
17285
  }
17262
17286
  }
17263
17287
  for (const wssRoute of wssRoutes) {
17264
17288
  const normalized = wssRoute.normalized;
17265
17289
  if (!normalized) {
17266
- console.warn(
17267
- `[loly:realtime] Skipping route ${wssRoute.pattern}: No normalized route definition`
17268
- );
17269
17290
  continue;
17270
17291
  }
17271
17292
  let namespacePath = normalized.namespace || wssRoute.pattern.replace(/^\/wss/, "");
@@ -17276,9 +17297,7 @@ async function setupWssEvents(options) {
17276
17297
  namespacePath = "/";
17277
17298
  }
17278
17299
  const namespace = io.of(namespacePath);
17279
- console.log(`[loly:realtime] Registered namespace: ${namespacePath} (from pattern: ${wssRoute.pattern})`);
17280
17300
  namespace.on("connection", async (socket) => {
17281
- console.log(`[loly:realtime] Client connected to namespace ${namespacePath}, socket: ${socket.id}`);
17282
17301
  const requestId = generateRequestId();
17283
17302
  socket.requestId = requestId;
17284
17303
  const log = createWssLogger(namespacePath, socket);
@@ -17290,8 +17309,6 @@ async function setupWssEvents(options) {
17290
17309
  await presence.addSocketForUser(String(user.id), socket.id);
17291
17310
  }
17292
17311
  const baseCtx = {
17293
- socket,
17294
- io: namespace.server,
17295
17312
  req: {
17296
17313
  headers: socket.handshake.headers,
17297
17314
  ip: socket.handshake.address,