@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.
- package/_build/helpers/cachePages.d.ts +8 -0
- package/_build/helpers/cachePages.js +46 -0
- package/_build/helpers/createPage.d.ts +2 -1
- package/_build/helpers/createPage.js +9 -2
- package/_build/helpers/renderPageRuntime.js +16 -4
- package/_build/helpers/styleDiscovery.d.ts +13 -0
- package/_build/helpers/styleDiscovery.js +61 -0
- package/_build/scripts/build-css.d.ts +5 -0
- package/_build/scripts/build-css.js +79 -0
- package/_build/scripts/build-html.js +10 -4
- package/_build/scripts/cli.js +9 -2
- package/_build/server/config/index.d.ts +3 -0
- package/_build/server/config/index.js +108 -1
- package/_build/server/config/vite.config.js +18 -1
- package/_build/server/index.d.ts +1 -1
- package/_build/server/index.js +8 -3
- package/_build/server/middleware/hotReload.js +30 -22
- package/_build/server/middleware/runtime.d.ts +4 -0
- package/_build/server/middleware/runtime.js +126 -1
- package/_build/server/middleware/security.d.ts +2 -2
- package/_build/server/middleware/security.js +52 -8
- package/_build/server/routes/api.d.ts +6 -1
- package/_build/server/routes/api.js +52 -1
- package/_build/server/scripts/revalidate.js +82 -9
- package/_build/server/static/hot-reload-client.js +362 -0
- package/_build/server/utils/vite.js +23 -2
- package/package.json +9 -2
|
@@ -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,
|
|
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}}",
|
|
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 (
|
|
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 &&
|
|
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 &&
|
|
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,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
|
-
|
|
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 &&
|
|
127
|
+
JSfileName: injectJS && page.pageName,
|
|
128
|
+
CSSfileName: hasStyles && page.pageName,
|
|
123
129
|
pageData,
|
|
124
130
|
});
|
|
125
131
|
console.log(`✓ ${page.pageName}.html`);
|
package/_build/scripts/cli.js
CHANGED
|
@@ -45,14 +45,21 @@ program
|
|
|
45
45
|
stdio: 'inherit',
|
|
46
46
|
cwd: projectRoot
|
|
47
47
|
});
|
|
48
|
-
console.log("\n2️⃣ Building
|
|
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("\
|
|
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
|
-
|
|
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:
|
|
39
|
+
input: sanitizedEntries,
|
|
23
40
|
output: {
|
|
24
41
|
entryFileNames: "[name].js",
|
|
25
42
|
chunkFileNames: "assets/vendor-[hash].js",
|
package/_build/server/index.d.ts
CHANGED
|
@@ -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";
|
package/_build/server/index.js
CHANGED
|
@@ -39,12 +39,17 @@ export const createApp = async () => {
|
|
|
39
39
|
applyRateLimiting(app);
|
|
40
40
|
applyParsing(app);
|
|
41
41
|
applyLogging(app);
|
|
42
|
-
// Hot reload middleware
|
|
43
|
-
|
|
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)
|