@lolyjs/core 0.2.0-alpha.21 → 0.2.0-alpha.22

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
@@ -392,6 +392,64 @@ export default function Dashboard() {
392
392
  }
393
393
  ```
394
394
 
395
+ ### 📄 Static Files & Assets
396
+
397
+ Loly serves static files from the `public/` directory at the root of your application. This is perfect for SEO files like `sitemap.xml`, `robots.txt`, favicons, and other static assets.
398
+
399
+ **How it works:**
400
+ - Files in `public/` are served at the root URL (e.g., `public/sitemap.xml` → `/sitemap.xml`)
401
+ - Static files have **priority over dynamic routes** - if a file exists in `public/`, it will be served instead of matching a route
402
+ - Perfect for SEO: Google automatically finds `sitemap.xml` and `robots.txt` at the root
403
+ - Works in both development and production environments
404
+ - Subdirectories are supported: `public/assets/logo.png` → `/assets/logo.png`
405
+
406
+ **Directory Structure:**
407
+ ```
408
+ public/
409
+ ├── sitemap.xml # Available at /sitemap.xml
410
+ ├── robots.txt # Available at /robots.txt
411
+ ├── favicon.ico # Available at /favicon.ico
412
+ └── assets/
413
+ ├── logo.png # Available at /assets/logo.png
414
+ └── images/ # Available at /assets/images/*
415
+ └── hero.jpg
416
+ ```
417
+
418
+ **SEO Example:**
419
+
420
+ Create `public/sitemap.xml`:
421
+ ```xml
422
+ <?xml version="1.0" encoding="UTF-8"?>
423
+ <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
424
+ <url>
425
+ <loc>https://example.com/</loc>
426
+ <lastmod>2024-01-01</lastmod>
427
+ <changefreq>daily</changefreq>
428
+ <priority>1.0</priority>
429
+ </url>
430
+ </urlset>
431
+ ```
432
+
433
+ Create `public/robots.txt`:
434
+ ```
435
+ User-agent: *
436
+ Allow: /
437
+
438
+ Sitemap: https://example.com/sitemap.xml
439
+ ```
440
+
441
+ Both files will be automatically available at `/sitemap.xml` and `/robots.txt` respectively, and search engines will find them at the standard locations.
442
+
443
+ **Configuration:**
444
+ The static directory can be customized in `loly.config.ts`:
445
+ ```tsx
446
+ export default {
447
+ directories: {
448
+ static: "public", // Default: "public"
449
+ },
450
+ } satisfies FrameworkConfig;
451
+ ```
452
+
395
453
  ### 🔌 API Routes
396
454
 
397
455
  Create RESTful APIs with flexible middleware support:
@@ -667,7 +725,7 @@ your-app/
667
725
  │ └── events.ts # WebSocket namespace /chat
668
726
  ├── components/ # React components
669
727
  ├── lib/ # Utilities
670
- ├── public/ # Static files
728
+ ├── public/ # Static files (served at root: /sitemap.xml, /robots.txt, etc.)
671
729
  ├── loly.config.ts # Framework configuration
672
730
  ├── init.server.ts # Server initialization (DB, services, etc.)
673
731
  └── package.json
package/dist/cli.cjs CHANGED
@@ -13754,51 +13754,88 @@ function createPathAliasPlugin(projectRoot, outDir) {
13754
13754
  } catch {
13755
13755
  }
13756
13756
  }
13757
+ function resolveAliasToRelative(importPath, sourceFile) {
13758
+ if (importPath.startsWith(".") || importPath.startsWith("/") || import_path25.default.isAbsolute(importPath) || importPath.includes("node_modules")) {
13759
+ return null;
13760
+ }
13761
+ for (const [aliasKey, aliasPath] of Object.entries(aliases)) {
13762
+ if (importPath.startsWith(aliasKey + "/") || importPath === aliasKey) {
13763
+ const restPath = importPath.startsWith(aliasKey + "/") ? importPath.slice(aliasKey.length + 1) : "";
13764
+ const resolvedPath = restPath ? import_path25.default.join(aliasPath, restPath) : aliasPath;
13765
+ let actualPath = null;
13766
+ const extensions = [".ts", ".tsx", ".js", ".jsx", ".json"];
13767
+ if (import_fs16.default.existsSync(resolvedPath) && import_fs16.default.statSync(resolvedPath).isDirectory()) {
13768
+ for (const ext of extensions) {
13769
+ const indexPath = import_path25.default.join(resolvedPath, `index${ext}`);
13770
+ if (import_fs16.default.existsSync(indexPath)) {
13771
+ actualPath = indexPath;
13772
+ break;
13773
+ }
13774
+ }
13775
+ } else {
13776
+ for (const ext of extensions) {
13777
+ const filePath = resolvedPath + ext;
13778
+ if (import_fs16.default.existsSync(filePath)) {
13779
+ actualPath = filePath;
13780
+ break;
13781
+ }
13782
+ }
13783
+ if (!actualPath && import_fs16.default.existsSync(resolvedPath)) {
13784
+ actualPath = resolvedPath;
13785
+ }
13786
+ }
13787
+ if (actualPath) {
13788
+ const relativePath = import_path25.default.relative(outDir, actualPath);
13789
+ const normalizedPath = relativePath.replace(/\\/g, "/");
13790
+ const finalPath = normalizedPath.startsWith(".") ? normalizedPath : `./${normalizedPath}`;
13791
+ const ext = import_path25.default.extname(finalPath);
13792
+ const pathWithoutExt = ext === ".json" ? finalPath : finalPath.slice(0, -ext.length);
13793
+ return pathWithoutExt;
13794
+ }
13795
+ }
13796
+ }
13797
+ return null;
13798
+ }
13757
13799
  return {
13758
13800
  name: "path-alias-resolver",
13759
13801
  setup(build) {
13760
- build.onResolve({ filter: /.*/ }, (args) => {
13761
- if (args.path.startsWith(".") || args.path.startsWith("/") || import_path25.default.isAbsolute(args.path) || args.path.includes("node_modules")) {
13802
+ build.onLoad({ filter: /\.(ts|tsx|js|jsx)$/ }, (args) => {
13803
+ const fileName = import_path25.default.basename(args.path);
13804
+ const isServerFile = SERVER_FILES.some((f) => fileName === `${f}.ts` || fileName === `${f}.tsx` || fileName === `${f}.js` || fileName === `${f}.jsx`);
13805
+ const isInProjectRoot = import_path25.default.dirname(args.path) === projectRoot;
13806
+ if (!isServerFile || !isInProjectRoot) {
13762
13807
  return null;
13763
13808
  }
13764
- for (const [aliasKey, aliasPath] of Object.entries(aliases)) {
13765
- if (args.path.startsWith(aliasKey + "/") || args.path === aliasKey) {
13766
- const restPath = args.path.startsWith(aliasKey + "/") ? args.path.slice(aliasKey.length + 1) : "";
13767
- const resolvedPath = restPath ? import_path25.default.join(aliasPath, restPath) : aliasPath;
13768
- let actualPath = null;
13769
- const extensions = [".ts", ".tsx", ".js", ".jsx", ".json"];
13770
- if (import_fs16.default.existsSync(resolvedPath) && import_fs16.default.statSync(resolvedPath).isDirectory()) {
13771
- for (const ext of extensions) {
13772
- const indexPath = import_path25.default.join(resolvedPath, `index${ext}`);
13773
- if (import_fs16.default.existsSync(indexPath)) {
13774
- actualPath = indexPath;
13775
- break;
13776
- }
13777
- }
13778
- } else {
13779
- for (const ext of extensions) {
13780
- const filePath = resolvedPath + ext;
13781
- if (import_fs16.default.existsSync(filePath)) {
13782
- actualPath = filePath;
13783
- break;
13784
- }
13785
- }
13786
- if (!actualPath && import_fs16.default.existsSync(resolvedPath)) {
13787
- actualPath = resolvedPath;
13788
- }
13789
- }
13790
- if (actualPath) {
13791
- const relativePath = import_path25.default.relative(outDir, actualPath);
13792
- const normalizedPath = relativePath.replace(/\\/g, "/");
13793
- const finalPath = normalizedPath.startsWith(".") ? normalizedPath : `./${normalizedPath}`;
13794
- const ext = import_path25.default.extname(finalPath);
13795
- const pathWithoutExt = ext === ".json" ? finalPath : finalPath.slice(0, -ext.length);
13796
- return {
13797
- path: pathWithoutExt,
13798
- namespace: "file"
13799
- };
13809
+ const contents = import_fs16.default.readFileSync(args.path, "utf-8");
13810
+ let transformed = contents;
13811
+ const aliasPatterns = Object.keys(aliases).sort((a, b) => b.length - a.length);
13812
+ for (const aliasKey of aliasPatterns) {
13813
+ const escapedAlias = aliasKey.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
13814
+ const aliasInQuotesPattern = new RegExp(
13815
+ `(['"\`])${escapedAlias}(/[^'"\`\\s]*)?(['"\`])`,
13816
+ "g"
13817
+ );
13818
+ transformed = transformed.replace(aliasInQuotesPattern, (match, quote1, rest, quote2) => {
13819
+ const fullPath = aliasKey + (rest || "");
13820
+ const resolved = resolveAliasToRelative(fullPath, args.path);
13821
+ if (resolved) {
13822
+ return `${quote1}${resolved}${quote2}`;
13800
13823
  }
13801
- }
13824
+ return match;
13825
+ });
13826
+ }
13827
+ return {
13828
+ contents: transformed,
13829
+ loader: import_path25.default.extname(args.path).slice(1)
13830
+ };
13831
+ });
13832
+ build.onResolve({ filter: /.*/ }, (args) => {
13833
+ const resolved = resolveAliasToRelative(args.path, args.importer || "");
13834
+ if (resolved) {
13835
+ return {
13836
+ path: resolved,
13837
+ namespace: "file"
13838
+ };
13802
13839
  }
13803
13840
  return null;
13804
13841
  });
@@ -14101,6 +14138,9 @@ function getAppDir(projectRoot, config) {
14101
14138
  function getBuildDir(projectRoot, config) {
14102
14139
  return import_path26.default.join(projectRoot, config.directories.build);
14103
14140
  }
14141
+ function getStaticDir(projectRoot, config) {
14142
+ return import_path26.default.resolve(projectRoot, config.directories.static);
14143
+ }
14104
14144
 
14105
14145
  // modules/build/index.ts
14106
14146
  async function buildApp(options = {}) {
@@ -14151,12 +14191,13 @@ async function buildApp(options = {}) {
14151
14191
  }
14152
14192
 
14153
14193
  // src/server.ts
14154
- var import_fs20 = __toESM(require("fs"));
14194
+ var import_fs21 = __toESM(require("fs"));
14155
14195
  var import_path31 = __toESM(require("path"));
14156
14196
 
14157
14197
  // modules/server/setup.ts
14158
14198
  var import_express = __toESM(require("express"));
14159
14199
  var import_path29 = __toESM(require("path"));
14200
+ var import_fs20 = __toESM(require("fs"));
14160
14201
 
14161
14202
  // ../../node_modules/.pnpm/chokidar@4.0.3/node_modules/chokidar/esm/index.js
14162
14203
  var import_fs19 = require("fs");
@@ -15998,8 +16039,22 @@ function clearAppRequireCache(appDir) {
15998
16039
 
15999
16040
  // modules/server/setup.ts
16000
16041
  init_globals();
16042
+ function setupStaticFiles(app, projectRoot, config) {
16043
+ if (!config) return;
16044
+ const staticDir = getStaticDir(projectRoot, config);
16045
+ if (import_fs20.default.existsSync(staticDir)) {
16046
+ app.use(
16047
+ import_express.default.static(staticDir, {
16048
+ // In production, add caching headers for better performance
16049
+ maxAge: process.env.NODE_ENV === "production" ? "1y" : 0,
16050
+ immutable: process.env.NODE_ENV === "production"
16051
+ })
16052
+ );
16053
+ }
16054
+ }
16001
16055
  function setupServer(app, options) {
16002
16056
  const { projectRoot, appDir, isDev, config } = options;
16057
+ setupStaticFiles(app, projectRoot, config);
16003
16058
  const routeLoader = isDev ? new FilesystemRouteLoader(appDir, projectRoot) : new ManifestRouteLoader(projectRoot);
16004
16059
  if (isDev) {
16005
16060
  let getRoutes2 = function() {
@@ -17562,7 +17617,7 @@ var setupApplication = async ({
17562
17617
  // src/server.ts
17563
17618
  var import_dotenv2 = __toESM(require("dotenv"));
17564
17619
  var envPath = import_path31.default.join(process.cwd(), ".env");
17565
- if (import_fs20.default.existsSync(envPath)) {
17620
+ if (import_fs21.default.existsSync(envPath)) {
17566
17621
  import_dotenv2.default.config({ path: envPath });
17567
17622
  } else {
17568
17623
  import_dotenv2.default.config();
@@ -17584,7 +17639,7 @@ async function startServer(options = {}) {
17584
17639
  const port = options.port ?? (process.env.PORT ? parseInt(process.env.PORT, 10) : void 0) ?? config.server.port;
17585
17640
  const host = process.env.HOST ?? (!isDev ? "0.0.0.0" : void 0) ?? config.server.host;
17586
17641
  const appDir = options.appDir ?? (isDev ? getAppDir(projectRoot, config) : import_path31.default.join(getBuildDir(projectRoot, config), "server"));
17587
- if (!isDev && !import_fs20.default.existsSync(appDir)) {
17642
+ if (!isDev && !import_fs21.default.existsSync(appDir)) {
17588
17643
  logger4.error("Compiled directory not found", void 0, {
17589
17644
  buildDir: config.directories.build,
17590
17645
  appDir,