@bouygues-telecom/staticjs 0.1.15 → 1.0.1

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.
@@ -1,3 +1,11 @@
1
+ /**
2
+ * Load styles cache for a given project directory
3
+ * @param projectDir - The template directory path (e.g., "templates/react")
4
+ * @returns The styles cache object mapping page names to their style file paths
5
+ */
6
+ export declare const loadStylesCache: (projectDir: string) => {
7
+ [key: string]: string[];
8
+ };
1
9
  /**
2
10
  * Load cache entries with error handling and auto-generation
3
11
  * @param projectDir - The template directory path (e.g., "templates/react")
@@ -2,6 +2,7 @@ import fs from "fs";
2
2
  import path from "path";
3
3
  import { readPages } from "./readPages.js";
4
4
  import { CONFIG } from "../server/config/index.js";
5
+ import { findStyleFiles } from "./styleDiscovery.js";
5
6
  const generateExcludedFiles = (entries) => {
6
7
  const excludedFiles = [];
7
8
  Object.entries(entries).forEach(([name, path]) => {
@@ -26,6 +27,33 @@ const generateExcludedFiles = (entries) => {
26
27
  throw error; // Re-throw to handle it in the caller
27
28
  }
28
29
  };
30
+ /**
31
+ * Generate styles cache for all pages
32
+ * Maps each page name to its collected style files (from root layout to page)
33
+ */
34
+ const generateStylesCache = (entries, projectDir) => {
35
+ const stylesCache = {};
36
+ const rootDir = path.resolve(projectDir, "src");
37
+ Object.entries(entries).forEach(([pageName, pagePath]) => {
38
+ const styleFiles = findStyleFiles(pagePath, rootDir);
39
+ if (styleFiles.length > 0) {
40
+ stylesCache[pageName] = styleFiles;
41
+ }
42
+ });
43
+ try {
44
+ const cacheDir = path.resolve(projectDir, CONFIG.BUILD_DIR, "cache");
45
+ const stylesCachePath = path.resolve(cacheDir, "stylesCache.json");
46
+ if (!fs.existsSync(cacheDir)) {
47
+ fs.mkdirSync(cacheDir, { recursive: true });
48
+ }
49
+ fs.writeFileSync(stylesCachePath, JSON.stringify(stylesCache, null, 2), "utf8");
50
+ }
51
+ catch (error) {
52
+ console.error("Error generating styles cache:", error);
53
+ throw error;
54
+ }
55
+ return stylesCache;
56
+ };
29
57
  /**
30
58
  * Generate cache entries for a given template directory
31
59
  * @param projectDir - The template directory path (e.g., "templates/react")
@@ -56,12 +84,30 @@ const generateCacheEntries = (projectDir, verbose = false) => {
56
84
  // Write cache file
57
85
  fs.writeFileSync(cacheFilePath, JSON.stringify(entries, null, 2), "utf8");
58
86
  generateExcludedFiles(entries);
87
+ generateStylesCache(entries, projectDir);
59
88
  if (verbose) {
60
89
  console.log(` Generated cache file: ${cacheFilePath}`);
61
90
  console.log(` Found ${Object.keys(entries).length} page(s)\n`);
62
91
  }
63
92
  return entries;
64
93
  };
94
+ /**
95
+ * Load styles cache for a given project directory
96
+ * @param projectDir - The template directory path (e.g., "templates/react")
97
+ * @returns The styles cache object mapping page names to their style file paths
98
+ */
99
+ export const loadStylesCache = (projectDir) => {
100
+ const stylesCachePath = path.resolve(projectDir, CONFIG.BUILD_DIR, "cache/stylesCache.json");
101
+ try {
102
+ if (fs.existsSync(stylesCachePath)) {
103
+ return JSON.parse(fs.readFileSync(stylesCachePath, 'utf8'));
104
+ }
105
+ }
106
+ catch (error) {
107
+ // Styles cache doesn't exist or is invalid, return empty object
108
+ }
109
+ return {};
110
+ };
65
111
  /**
66
112
  * Load cache entries with error handling and auto-generation
67
113
  * @param projectDir - The template directory path (e.g., "templates/react")
@@ -11,8 +11,9 @@ interface IcreatePage {
11
11
  rootId: string;
12
12
  pageName: string;
13
13
  JSfileName: string | false;
14
+ CSSfileName?: string | false;
14
15
  returnHtml?: boolean;
15
16
  pageData?: any;
16
17
  }
17
- export declare const createPage: ({ data, AppComponent, PageComponent, initialDatasId, rootId, pageName, JSfileName, returnHtml, pageData, }: IcreatePage) => Promise<string | void>;
18
+ export declare const createPage: ({ data, AppComponent, PageComponent, initialDatasId, rootId, pageName, JSfileName, CSSfileName, returnHtml, pageData, }: IcreatePage) => Promise<string | void>;
18
19
  export {};
@@ -3,10 +3,12 @@ import path from "path";
3
3
  import React from "react";
4
4
  import ReactDOMServer from "react-dom/server";
5
5
  import { CONFIG } from "../server/config/index.js";
6
- export const createPage = async ({ data, AppComponent, PageComponent, initialDatasId, rootId, pageName, JSfileName, returnHtml = false, // Default to false for backward compatibility
6
+ export const createPage = async ({ data, AppComponent, PageComponent, initialDatasId, rootId, pageName, JSfileName, CSSfileName = false, // Default to false for backward compatibility
7
+ returnHtml = false, // Default to false for backward compatibility
7
8
  pageData = {}, // Default to empty object
8
9
  }) => {
9
10
  const template = `{{html}}
11
+ ${CSSfileName ? `<link rel="stylesheet" href="{{stylePath}}">` : ""}
10
12
  ${data ? `<script id=initial-data-{{initialDatasId}} type="application/json">${JSON.stringify(data)}</script>` : ""}
11
13
  ${JSfileName ? `<script type="module" src="{{scriptPath}}"></script>` : ""}
12
14
  `;
@@ -19,10 +21,15 @@ ${JSfileName ? `<script type="module" src="{{scriptPath}}"></script>` : ""}
19
21
  props: { data },
20
22
  pageData, // Pass pageData to AppComponent
21
23
  });
24
+ // Use JSfileName for script path if it's a string (for dynamic routes), otherwise use pageName
25
+ const scriptPath = typeof JSfileName === 'string' ? `/${JSfileName}.js` : `/${pageName}.js`;
26
+ // Use CSSfileName for style path if it's a string, otherwise use pageName
27
+ const stylePath = typeof CSSfileName === 'string' ? `/${CSSfileName}.css` : `/${pageName}.css`;
22
28
  const htmlContent = template
23
29
  .replace("{{initialDatasId}}", initialDatasId)
24
30
  .replace("{{html}}", ReactDOMServer.renderToString(component))
25
- .replace("{{scriptPath}}", `/${pageName}.js`);
31
+ .replace("{{scriptPath}}", scriptPath)
32
+ .replace("{{stylePath}}", stylePath);
26
33
  // Return HTML string for runtime rendering or write to file for build time
27
34
  if (returnHtml) {
28
35
  return htmlContent;
@@ -1,4 +1,3 @@
1
- process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
2
1
  import fs from "fs/promises";
3
2
  import crypto from "node:crypto";
4
3
  import path from "path";
@@ -7,6 +6,7 @@ import { createPage } from "./createPage.js";
7
6
  import { readPages } from "./readPages.js";
8
7
  import { CONFIG } from "../server/config/index.js";
9
8
  import { findClosestLayout } from "./layoutDiscovery.js";
9
+ import { hasStyles } from "./styleDiscovery.js";
10
10
  const rootDir = path.resolve(process.cwd(), "./src");
11
11
  async function loadJson(filePath) {
12
12
  try {
@@ -163,8 +163,9 @@ async function processPageRuntime(page, excludedJSFiles, params = {}) {
163
163
  throw new Error(`Failed to import PageComponent from ${page.pageName}.tsx`);
164
164
  }
165
165
  // Handle getStaticProps with or without dynamic params
166
+ const isDynamicRoute = Object.keys(params).length > 0;
166
167
  if (getStaticProps) {
167
- if (Object.keys(params).length > 0) {
168
+ if (isDynamicRoute) {
168
169
  // Dynamic route with params
169
170
  const { props } = await getStaticProps({ params });
170
171
  data = props.data;
@@ -175,6 +176,15 @@ async function processPageRuntime(page, excludedJSFiles, params = {}) {
175
176
  data = props.data;
176
177
  }
177
178
  }
179
+ // Determine JS file path: for dynamic routes, use parent path (e.g., partials/dynamic/1 -> partials/dynamic)
180
+ const jsFilePath = isDynamicRoute
181
+ ? page.pageName.replace(/\/[^/]+$/, '')
182
+ : page.pageName;
183
+ // Check if this page has styles (from page or layouts)
184
+ const pageHasStyles = hasStyles(absolutePath, rootDir);
185
+ const cssFilePath = pageHasStyles
186
+ ? (isDynamicRoute ? page.pageName.replace(/\/[^/]+$/, '') : page.pageName)
187
+ : false;
178
188
  // Generate HTML using createPage helper with returnHtml flag
179
189
  // Use dynamic import to load createPage from the template directory
180
190
  let htmlContent;
@@ -189,7 +199,8 @@ async function processPageRuntime(page, excludedJSFiles, params = {}) {
189
199
  initialDatasId,
190
200
  rootId,
191
201
  pageName: page.pageName,
192
- JSfileName: injectJS && fileName,
202
+ JSfileName: injectJS && jsFilePath,
203
+ CSSfileName: cssFilePath,
193
204
  returnHtml: true,
194
205
  });
195
206
  }
@@ -202,7 +213,8 @@ async function processPageRuntime(page, excludedJSFiles, params = {}) {
202
213
  initialDatasId,
203
214
  rootId,
204
215
  pageName: page.pageName,
205
- JSfileName: injectJS && fileName,
216
+ JSfileName: injectJS && jsFilePath,
217
+ CSSfileName: cssFilePath,
206
218
  returnHtml: true,
207
219
  });
208
220
  }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Discovers all style files (style.scss or style.css) by walking up the directory tree
3
+ * from the given page path. Returns files in order from root to page for proper CSS cascade.
4
+ */
5
+ export declare function findStyleFiles(pagePath: string, rootDir: string): string[];
6
+ /**
7
+ * Checks if a page has any styles (either direct or inherited from layouts)
8
+ */
9
+ export declare function hasStyles(pagePath: string, rootDir: string): boolean;
10
+ /**
11
+ * Gets style files as relative paths for use in Vite entries
12
+ */
13
+ export declare function getStyleFilesRelative(pagePath: string, rootDir: string, projectRoot: string): string[];
@@ -0,0 +1,61 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ /**
4
+ * Discovers all style files (style.scss or style.css) by walking up the directory tree
5
+ * from the given page path. Returns files in order from root to page for proper CSS cascade.
6
+ */
7
+ export function findStyleFiles(pagePath, rootDir) {
8
+ const styleFiles = [];
9
+ // Get the directory containing the page
10
+ const pageDir = path.dirname(pagePath);
11
+ // Collect all directories from root to page
12
+ const directories = [];
13
+ let currentDir = pageDir;
14
+ while (currentDir.startsWith(rootDir) || currentDir === rootDir) {
15
+ directories.unshift(currentDir); // Add to beginning for root-first order
16
+ const parentDir = path.dirname(currentDir);
17
+ if (parentDir === currentDir)
18
+ break; // Reached filesystem root
19
+ currentDir = parentDir;
20
+ }
21
+ // Also check the root src directory itself
22
+ if (!directories.includes(rootDir)) {
23
+ directories.unshift(rootDir);
24
+ }
25
+ // Check each directory for style files
26
+ for (const dir of directories) {
27
+ const styleFile = findStyleFileInDir(dir);
28
+ if (styleFile) {
29
+ styleFiles.push(styleFile);
30
+ }
31
+ }
32
+ return styleFiles;
33
+ }
34
+ /**
35
+ * Finds a style file in a directory, preferring .scss over .css
36
+ */
37
+ function findStyleFileInDir(dir) {
38
+ const scssPath = path.join(dir, "style.scss");
39
+ const cssPath = path.join(dir, "style.css");
40
+ // Prefer .scss over .css
41
+ if (fs.existsSync(scssPath)) {
42
+ return scssPath;
43
+ }
44
+ if (fs.existsSync(cssPath)) {
45
+ return cssPath;
46
+ }
47
+ return null;
48
+ }
49
+ /**
50
+ * Checks if a page has any styles (either direct or inherited from layouts)
51
+ */
52
+ export function hasStyles(pagePath, rootDir) {
53
+ return findStyleFiles(pagePath, rootDir).length > 0;
54
+ }
55
+ /**
56
+ * Gets style files as relative paths for use in Vite entries
57
+ */
58
+ export function getStyleFilesRelative(pagePath, rootDir, projectRoot) {
59
+ const absolutePaths = findStyleFiles(pagePath, rootDir);
60
+ return absolutePaths.map(p => path.relative(projectRoot, p));
61
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Build CSS for all pages with styles
3
+ */
4
+ declare function buildCss(): Promise<void>;
5
+ export { buildCss };
@@ -0,0 +1,79 @@
1
+ import fs from "fs/promises";
2
+ import fsSync from "fs";
3
+ import path from "path";
4
+ import { CONFIG } from "../server/config/index.js";
5
+ import { loadStylesCache, loadCacheEntries } from "../helpers/cachePages.js";
6
+ /**
7
+ * Compile a single SCSS/CSS file using Vite's internal sass handling
8
+ * Falls back to concatenating raw CSS if sass is not available
9
+ */
10
+ async function compileScss(filePath) {
11
+ try {
12
+ // Try to use sass for SCSS compilation
13
+ const sass = await import("sass");
14
+ const result = sass.compile(filePath, {
15
+ loadPaths: [path.resolve(CONFIG.PROJECT_ROOT, "src")],
16
+ style: "compressed",
17
+ });
18
+ return result.css;
19
+ }
20
+ catch (error) {
21
+ // If sass import fails or file is .css, read and return raw content
22
+ if (filePath.endsWith(".css")) {
23
+ return await fs.readFile(filePath, "utf-8");
24
+ }
25
+ // Re-throw SCSS compilation errors
26
+ if (error.message && !error.message.includes("Cannot find module")) {
27
+ throw error;
28
+ }
29
+ // If sass is not installed, try reading as raw CSS
30
+ console.warn("sass package not found, reading file as raw CSS");
31
+ return await fs.readFile(filePath, "utf-8");
32
+ }
33
+ }
34
+ /**
35
+ * Build CSS for all pages with styles
36
+ */
37
+ async function buildCss() {
38
+ // Ensure cache is generated
39
+ loadCacheEntries(CONFIG.PROJECT_ROOT);
40
+ const stylesCache = loadStylesCache(CONFIG.PROJECT_ROOT);
41
+ const pagesWithStyles = Object.keys(stylesCache);
42
+ if (pagesWithStyles.length === 0) {
43
+ console.log("No style files found, skipping CSS build.");
44
+ return;
45
+ }
46
+ console.log(`\nBuilding CSS for ${pagesWithStyles.length} page(s)...`);
47
+ for (const pageName of pagesWithStyles) {
48
+ const styleFiles = stylesCache[pageName];
49
+ try {
50
+ // Compile all style files and concatenate
51
+ const compiledStyles = [];
52
+ for (const styleFile of styleFiles) {
53
+ const compiled = await compileScss(styleFile);
54
+ compiledStyles.push(`/* Source: ${path.basename(styleFile)} */\n${compiled}`);
55
+ }
56
+ const finalCss = compiledStyles.join("\n\n");
57
+ // Determine output path (handle dynamic routes)
58
+ const outputName = pageName.replace(/\/\[[^\]]+\]$/, "");
59
+ const outputPath = path.join(CONFIG.PROJECT_ROOT, CONFIG.BUILD_DIR, `${outputName}.css`);
60
+ // Create directories if needed
61
+ const outputDir = path.dirname(outputPath);
62
+ if (!fsSync.existsSync(outputDir)) {
63
+ fsSync.mkdirSync(outputDir, { recursive: true });
64
+ }
65
+ // Write compiled CSS
66
+ await fs.writeFile(outputPath, finalCss, "utf-8");
67
+ console.log(`✓ ${outputName}.css`);
68
+ }
69
+ catch (error) {
70
+ console.error(`Error building CSS for ${pageName}:`, error);
71
+ }
72
+ }
73
+ }
74
+ // Run if executed directly
75
+ const isMainModule = import.meta.url === `file://${process.argv[1]}`;
76
+ if (isMainModule) {
77
+ buildCss().catch(console.error);
78
+ }
79
+ export { buildCss };
@@ -4,9 +4,8 @@ import path from "path";
4
4
  import React from "react";
5
5
  import { createPage } from "../helpers/createPage.js";
6
6
  import { CONFIG } from "../server/config/index.js";
7
- import { loadCacheEntries } from "../helpers/cachePages.js";
7
+ import { loadCacheEntries, loadStylesCache } from "../helpers/cachePages.js";
8
8
  import { findClosestLayout } from "../helpers/layoutDiscovery.js";
9
- process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
10
9
  async function loadJson(filePath) {
11
10
  const data = await fs.readFile(filePath, "utf-8");
12
11
  return JSON.parse(data);
@@ -15,6 +14,7 @@ async function main() {
15
14
  loadCacheEntries(CONFIG.PROJECT_ROOT);
16
15
  const excludedJSFiles = await loadJson(path.join(CONFIG.PROJECT_ROOT, CONFIG.BUILD_DIR, "cache/excludedFiles.json"));
17
16
  const files = await loadJson(path.join(CONFIG.PROJECT_ROOT, CONFIG.BUILD_DIR, "cache/pagesCache.json"));
17
+ const stylesCache = loadStylesCache(CONFIG.PROJECT_ROOT);
18
18
  const processPage = async (page) => {
19
19
  try {
20
20
  let data;
@@ -57,6 +57,7 @@ async function main() {
57
57
  const getStaticProps = pageModule?.getStaticProps;
58
58
  const getStaticPaths = pageModule?.getStaticPaths;
59
59
  const injectJS = !excludedJSFiles.includes(page.pageName);
60
+ const hasStyles = stylesCache[page.pageName] && stylesCache[page.pageName].length > 0;
60
61
  const rootId = crypto
61
62
  .createHash("sha256")
62
63
  .update(`app-${absolutePath}`)
@@ -83,7 +84,10 @@ async function main() {
83
84
  if (slug) {
84
85
  const { props } = await getStaticProps(param);
85
86
  const pageName = page.pageName.replace(/\[.*?\]/, slug);
86
- const JSfileName = injectJS && fileName.replace(/\[(.*?)\]/g, "_$1_");
87
+ // For dynamic routes, JS file is at parent level (e.g., partials/dynamic.js)
88
+ const JSfileName = injectJS && page.pageName.replace(/\/\[[^\]]+\]$/, '');
89
+ // For dynamic routes, CSS file is at parent level like JS
90
+ const CSSfileName = hasStyles && page.pageName.replace(/\/\[[^\]]+\]$/, '');
87
91
  createPage({
88
92
  data: props.data,
89
93
  AppComponent,
@@ -92,6 +96,7 @@ async function main() {
92
96
  rootId,
93
97
  pageName,
94
98
  JSfileName: JSfileName,
99
+ CSSfileName: CSSfileName,
95
100
  pageData,
96
101
  });
97
102
  console.log(`✓ ${pageName}.html`);
@@ -119,7 +124,8 @@ async function main() {
119
124
  initialDatasId,
120
125
  rootId,
121
126
  pageName: page.pageName,
122
- JSfileName: injectJS && fileName,
127
+ JSfileName: injectJS && page.pageName,
128
+ CSSfileName: hasStyles && page.pageName,
123
129
  pageData,
124
130
  });
125
131
  console.log(`✓ ${page.pageName}.html`);
@@ -45,14 +45,21 @@ program
45
45
  stdio: 'inherit',
46
46
  cwd: projectRoot
47
47
  });
48
- console.log("\n2️⃣ Building assets with Vite...");
48
+ console.log("\n2️⃣ Building CSS from SCSS...");
49
+ const buildCssScript = path.join(libDir, 'scripts', 'build-css.js');
50
+ const cssBuildCommand = `npx tsx "${buildCssScript}"`;
51
+ execSync(cssBuildCommand, {
52
+ stdio: 'inherit',
53
+ cwd: projectRoot
54
+ });
55
+ console.log("\n3️⃣ Building assets with Vite...");
49
56
  const viteConfigPath = path.join(libDir, 'server', 'config', 'vite.config.js');
50
57
  const viteBuildCommand = `npx vite build --config "${viteConfigPath}"`;
51
58
  execSync(viteBuildCommand, {
52
59
  stdio: 'inherit',
53
60
  cwd: projectRoot
54
61
  });
55
- console.log("\n3️⃣ Cleanup...");
62
+ console.log("\n4️⃣ Cleanup...");
56
63
  const cacheDir = path.join(projectRoot, '_build', 'cache');
57
64
  if (fs.existsSync(cacheDir)) {
58
65
  const cleanupCommand = process.platform === 'win32'
@@ -8,6 +8,8 @@ export interface ServerConfig {
8
8
  RATE_LIMIT_WINDOW: number;
9
9
  RATE_LIMIT_MAX: number;
10
10
  REVALIDATE_RATE_LIMIT_MAX: number;
11
+ REVALIDATE_API_KEY: string;
12
+ CORS_ORIGINS: string[];
11
13
  CACHE_MAX_AGE: number;
12
14
  HOT_RELOAD_ENABLED: boolean;
13
15
  WEBSOCKET_ENABLED: boolean;
@@ -15,6 +17,7 @@ export interface ServerConfig {
15
17
  WEBSOCKET_PATH: string;
16
18
  FILE_WATCH_DEBOUNCE: number;
17
19
  }
20
+ export declare const DEFAULT_CONFIG: ServerConfig;
18
21
  export declare const CONFIG: ServerConfig;
19
22
  export declare const isDevelopment: boolean;
20
23
  export declare const isProduction: boolean;
@@ -3,7 +3,31 @@
3
3
  * Centralized configuration for the StaticJS React template server
4
4
  */
5
5
  import * as path from "node:path";
6
- export const CONFIG = {
6
+ import * as fs from "node:fs";
7
+ /**
8
+ * Type validators for each config key
9
+ * Keys in this object define the whitelist of allowed configuration keys
10
+ */
11
+ const CONFIG_VALIDATORS = {
12
+ PORT: (v) => typeof v === 'number' && v > 0 && v <= 65535,
13
+ NODE_ENV: (v) => typeof v === 'string' && ['development', 'production', 'test'].includes(v),
14
+ PROJECT_ROOT: (v) => typeof v === 'string' && v.length > 0 && v.length < 1024,
15
+ BUILD_DIR: (v) => typeof v === 'string' && /^[a-zA-Z0-9_-]+$/.test(v) && v.length < 64,
16
+ REQUEST_TIMEOUT: (v) => typeof v === 'number' && v > 0 && v <= 300000,
17
+ BODY_SIZE_LIMIT: (v) => typeof v === 'string' && /^\d+(kb|mb|gb)?$/i.test(v),
18
+ RATE_LIMIT_WINDOW: (v) => typeof v === 'number' && v > 0 && v <= 86400000,
19
+ RATE_LIMIT_MAX: (v) => typeof v === 'number' && v > 0 && v <= 10000,
20
+ REVALIDATE_RATE_LIMIT_MAX: (v) => typeof v === 'number' && v > 0 && v <= 1000,
21
+ REVALIDATE_API_KEY: (v) => typeof v === 'string' && v.length >= 16 && v.length <= 256,
22
+ CORS_ORIGINS: (v) => Array.isArray(v) && v.every((o) => typeof o === 'string' && /^https?:\/\/[a-zA-Z0-9.-]+(:\d+)?$/.test(o)),
23
+ CACHE_MAX_AGE: (v) => typeof v === 'number' && v >= 0 && v <= 31536000,
24
+ HOT_RELOAD_ENABLED: (v) => typeof v === 'boolean',
25
+ WEBSOCKET_ENABLED: (v) => typeof v === 'boolean',
26
+ FILE_WATCHING_ENABLED: (v) => typeof v === 'boolean',
27
+ WEBSOCKET_PATH: (v) => typeof v === 'string' && /^\/[a-zA-Z0-9_-]*$/.test(v),
28
+ FILE_WATCH_DEBOUNCE: (v) => typeof v === 'number' && v >= 0 && v <= 10000,
29
+ };
30
+ export const DEFAULT_CONFIG = {
7
31
  PORT: Number(process.env.PORT) || 3456,
8
32
  NODE_ENV: process.env.NODE_ENV || 'development',
9
33
  PROJECT_ROOT: path.resolve(process.cwd()),
@@ -13,6 +37,8 @@ export const CONFIG = {
13
37
  RATE_LIMIT_WINDOW: 15 * 60 * 1000, // 15 minutes
14
38
  RATE_LIMIT_MAX: 100, // requests per window
15
39
  REVALIDATE_RATE_LIMIT_MAX: 10, // stricter limit for revalidate endpoint
40
+ REVALIDATE_API_KEY: process.env.REVALIDATE_API_KEY || '', // API key for revalidate endpoint (required in production)
41
+ CORS_ORIGINS: process.env.CORS_ORIGINS?.split(',').filter(Boolean) || [], // Allowed CORS origins (empty = same-origin only in prod, localhost in dev)
16
42
  CACHE_MAX_AGE: process.env.NODE_ENV === 'production' ? 86400 : 0, // 1 day in prod, no cache in dev
17
43
  // Hot reload configuration
18
44
  HOT_RELOAD_ENABLED: process.env.NODE_ENV === 'development',
@@ -21,5 +47,86 @@ export const CONFIG = {
21
47
  WEBSOCKET_PATH: '/ws',
22
48
  FILE_WATCH_DEBOUNCE: 300, // milliseconds
23
49
  };
50
+ /**
51
+ * Validate and sanitize user configuration
52
+ * Only allows whitelisted keys with valid types
53
+ */
54
+ const validateUserConfig = (rawConfig) => {
55
+ if (typeof rawConfig !== 'object' || rawConfig === null) {
56
+ console.warn('[Config] Invalid config format: expected object');
57
+ return {};
58
+ }
59
+ const validatedConfig = {};
60
+ const configObj = rawConfig;
61
+ for (const [key, value] of Object.entries(configObj)) {
62
+ // Check if key is allowed (exists in validators)
63
+ if (!(key in CONFIG_VALIDATORS)) {
64
+ console.warn(`[Config] Ignoring unknown config key: ${key}`);
65
+ continue;
66
+ }
67
+ const typedKey = key;
68
+ const validator = CONFIG_VALIDATORS[typedKey];
69
+ // Validate value type
70
+ if (!validator(value)) {
71
+ console.warn(`[Config] Invalid value for ${key}: ${JSON.stringify(value)}`);
72
+ continue;
73
+ }
74
+ // Type assertion is safe here because we validated the value
75
+ validatedConfig[key] = value;
76
+ }
77
+ return validatedConfig;
78
+ };
79
+ /**
80
+ * Load user configuration from static.config.ts in the project root
81
+ * Validates all loaded values against a strict schema
82
+ */
83
+ const loadUserConfig = async () => {
84
+ const projectRoot = path.resolve(process.cwd());
85
+ const configPath = path.join(projectRoot, 'static.config.ts');
86
+ if (!fs.existsSync(configPath)) {
87
+ return {};
88
+ }
89
+ try {
90
+ const imported = await import(configPath);
91
+ const rawConfig = imported.default || imported.CONFIG || {};
92
+ // Validate and sanitize the loaded configuration
93
+ const validatedConfig = validateUserConfig(rawConfig);
94
+ if (Object.keys(validatedConfig).length > 0) {
95
+ console.log('[Config] Loaded user configuration from static.config.ts');
96
+ }
97
+ return validatedConfig;
98
+ }
99
+ catch (error) {
100
+ console.warn(`[Config] Failed to load static.config.ts: ${error.message}`);
101
+ return {};
102
+ }
103
+ };
104
+ /**
105
+ * Merge default config with user config
106
+ */
107
+ const mergeConfigs = (defaults, userConfig) => {
108
+ const merged = {
109
+ ...defaults,
110
+ ...userConfig,
111
+ };
112
+ // Recalculate derived values based on merged NODE_ENV if not explicitly set
113
+ const nodeEnv = merged.NODE_ENV;
114
+ if (userConfig.CACHE_MAX_AGE === undefined) {
115
+ merged.CACHE_MAX_AGE = nodeEnv === 'production' ? 86400 : 0;
116
+ }
117
+ if (userConfig.HOT_RELOAD_ENABLED === undefined) {
118
+ merged.HOT_RELOAD_ENABLED = nodeEnv === 'development';
119
+ }
120
+ if (userConfig.WEBSOCKET_ENABLED === undefined) {
121
+ merged.WEBSOCKET_ENABLED = nodeEnv === 'development';
122
+ }
123
+ if (userConfig.FILE_WATCHING_ENABLED === undefined) {
124
+ merged.FILE_WATCHING_ENABLED = nodeEnv === 'development';
125
+ }
126
+ return merged;
127
+ };
128
+ // Load user config and merge with defaults using top-level await
129
+ const userConfig = await loadUserConfig();
130
+ export const CONFIG = mergeConfigs(DEFAULT_CONFIG, userConfig);
24
131
  export const isDevelopment = CONFIG.NODE_ENV === 'development';
25
132
  export const isProduction = CONFIG.NODE_ENV === 'production';
@@ -5,6 +5,12 @@ import { loadCacheEntries } from "../../helpers/cachePages.js";
5
5
  import { CONFIG } from "./index";
6
6
  // Load cache entries using the refactored helper function
7
7
  const entries = loadCacheEntries(CONFIG.PROJECT_ROOT);
8
+ // Sanitize entry keys for Rollup: strip dynamic segments [param] and use parent folder name
9
+ // e.g., "partials/dynamic/[id]" -> "partials/dynamic"
10
+ const sanitizedEntries = Object.fromEntries(Object.entries(entries).map(([key, value]) => [
11
+ key.replace(/\/\[[^\]]+\]$/, ''),
12
+ value
13
+ ]));
8
14
  export default defineConfig(({ mode }) => {
9
15
  // Load environment variables from .env files
10
16
  // const env = loadEnv(mode, CONFIG.PROJECT_ROOT, '');
@@ -15,11 +21,22 @@ export default defineConfig(({ mode }) => {
15
21
  "@": path.resolve(CONFIG.PROJECT_ROOT, "src")
16
22
  },
17
23
  },
24
+ css: {
25
+ // Enable CSS source maps for development
26
+ devSourcemap: true,
27
+ preprocessorOptions: {
28
+ scss: {
29
+ // Use modern API and allow importing from src directory
30
+ api: "modern-compiler",
31
+ loadPaths: [path.resolve(CONFIG.PROJECT_ROOT, "src")],
32
+ },
33
+ },
34
+ },
18
35
  build: {
19
36
  outDir: path.resolve(CONFIG.PROJECT_ROOT, CONFIG.BUILD_DIR),
20
37
  emptyOutDir: false,
21
38
  rollupOptions: {
22
- input: entries,
39
+ input: sanitizedEntries,
23
40
  output: {
24
41
  entryFileNames: "[name].js",
25
42
  chunkFileNames: "assets/vendor-[hash].js",
@@ -13,7 +13,7 @@ export declare const createApp: () => Promise<Express>;
13
13
  * @returns Promise<Express> - Running Express application
14
14
  */
15
15
  export declare const startStaticJSServer: () => Promise<Express>;
16
- export { isDevelopment } from "./config/index.js";
16
+ export { CONFIG, DEFAULT_CONFIG, isDevelopment } from "./config/index.js";
17
17
  export type { ServerConfig } from "./config/index.js";
18
18
  export { initializeViteServer } from "./utils/vite.js";
19
19
  export { setupProcessHandlers, startServer } from "./utils/startup.js";
@@ -39,12 +39,17 @@ export const createApp = async () => {
39
39
  applyRateLimiting(app);
40
40
  applyParsing(app);
41
41
  applyLogging(app);
42
- // Hot reload middleware (development mode only) - MUST be before runtime
43
- applyHotReload(app);
42
+ // Hot reload static middleware MUST be applied before Vite to ensure proper serving
43
+ if (isDevelopment) {
44
+ const { hotReloadStaticMiddleware } = await import('./middleware/hotReload.js');
45
+ app.use(hotReloadStaticMiddleware);
46
+ }
44
47
  // Initialize Vite server and register JavaScript routes BEFORE runtime middleware
45
48
  if (isDevelopment) {
46
49
  await initializeViteServer(app);
47
50
  }
51
+ // Hot reload injection middleware (development mode only) - MUST be before runtime
52
+ applyHotReload(app);
48
53
  // Runtime rendering middleware (development mode only)
49
54
  // JavaScript routes are now registered before this middleware
50
55
  applyRuntime(app);
@@ -105,7 +110,7 @@ export const startStaticJSServer = async () => {
105
110
  }
106
111
  };
107
112
  // Export additional utilities for external use
108
- export { isDevelopment } from "./config/index.js";
113
+ export { CONFIG, DEFAULT_CONFIG, isDevelopment } from "./config/index.js";
109
114
  export { initializeViteServer } from "./utils/vite.js";
110
115
  export { setupProcessHandlers, startServer } from "./utils/startup.js";
111
116
  // Only start the server when this module is run directly (not when imported)