@squeditor/squeditor-framework 1.0.2 → 1.0.3

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.
@@ -16,10 +16,11 @@ try {
16
16
  ffmpeg = require(path.join(projectRoot, 'node_modules/fluent-ffmpeg'));
17
17
  } catch (e) { }
18
18
 
19
- const customerBuildDir = path.join(projectRoot, 'build-customer-package');
19
+ const customerBuildDir = path.join(projectRoot, config.name || 'customer-package');
20
20
  const distDir = path.join(projectRoot, 'dist');
21
21
  const srcDir = path.join(projectRoot, 'src');
22
- const zipName = config.dist.zipName ? config.dist.zipName.replace('.zip', '-customer.zip') : 'customer-package.zip';
22
+ // Derive ZIP name from config.name directly to avoid double-suffix issues
23
+ const zipName = (config.name || 'customer-package') + '.zip';
23
24
  const zipPath = path.join(projectRoot, zipName);
24
25
 
25
26
  const mediaConfig = config.media || {};
@@ -116,40 +117,61 @@ async function walkAndProcessMedia(currentSrc, currentDest, baseDir) {
116
117
  async function createCustomerPackage() {
117
118
  console.log('[Squeditor] 📦 Assembling Customer Package...');
118
119
 
120
+ if (path.resolve(customerBuildDir) === path.resolve(projectRoot)) {
121
+ console.error(`[Squeditor] 🚨 CRITICAL ERROR: customerBuildDir resolves to the active project workspace root!`);
122
+ console.error(`[Squeditor] Aborting immediately to prevent recursive deletion. Please update 'name' in your squeditor.config.js so it does not target the parent folder (e.g., avoid '../folder-name').`);
123
+ process.exit(1);
124
+ }
125
+
119
126
  if (fs.existsSync(customerBuildDir)) {
120
127
  try {
121
- execSync(`rm -rf "${customerBuildDir}"`);
128
+ fs.rmSync(customerBuildDir, { recursive: true, force: true });
122
129
  } catch (e) {
123
- console.warn(` - ⚠️ Warning: Could not fully clean ${customerBuildDir}. You may need to run 'sudo rm -rf' on it manually.`);
130
+ console.warn(` - ⚠️ Warning: Could not fully clean ${customerBuildDir}. You may need to remove it manually.`);
124
131
  }
125
132
  }
126
133
  if (fs.existsSync(zipPath)) {
127
134
  fs.unlinkSync(zipPath);
128
135
  }
129
136
 
130
- fs.mkdirSync(path.join(customerBuildDir, 'src/assets'), { recursive: true });
137
+ fs.mkdirSync(path.join(customerBuildDir, 'dist/assets'), { recursive: true });
131
138
 
132
- console.log(' - Copying compiled HTML to src/');
139
+ console.log(' - Copying compiled HTML to dist/ and src/');
133
140
  const distHtmlFiles = fs.readdirSync(distDir).filter(file => file.endsWith('.html'));
141
+ fs.mkdirSync(path.join(customerBuildDir, 'src'), { recursive: true });
134
142
 
135
143
  distHtmlFiles.forEach(file => {
136
144
  let htmlContent = fs.readFileSync(path.join(distDir, file), 'utf8');
137
- htmlContent = htmlContent.replace(
138
- /<link rel="stylesheet" href="assets\/css\/main_css\.css">/g,
139
- '<link rel="stylesheet" href="assets/scss/main.scss">'
140
- );
141
- htmlContent = htmlContent.replace(
142
- /<script src="assets\/js\/uikit-components\.js"><\/script>/g,
143
- '<script type="module" src="assets/js/uikit-components.js"></script>'
144
- );
145
- fs.writeFileSync(path.join(customerBuildDir, 'src', file), htmlContent);
145
+ // dist/ gets production HTML referencing compiled CSS
146
+ fs.writeFileSync(path.join(customerBuildDir, 'dist', file), htmlContent);
147
+ // src/ gets dev HTML: rewrite CSS refs to raw SCSS for Vite HMR
148
+ let devHtml = htmlContent;
149
+ devHtml = devHtml.replace(/href="assets\/css\/main\.min\.css"/g, 'href="assets/scss/main.scss"');
150
+ devHtml = devHtml.replace(/href="assets\/css\/tailwind\.css"/g, 'href="assets/css/tailwind.css"');
151
+ fs.writeFileSync(path.join(customerBuildDir, 'src', file), devHtml);
146
152
  });
147
153
 
148
- console.log(' - Copying necessary assets to src/assets');
154
+ console.log(' - Copying necessary developer source files to src/assets');
149
155
  fs.mkdirSync(path.join(customerBuildDir, 'src/assets/css'), { recursive: true });
156
+ // Also copy dist production files
157
+ fs.mkdirSync(path.join(customerBuildDir, 'dist/assets/css'), { recursive: true });
158
+
150
159
  fs.copyFileSync(path.join(distDir, 'assets/css/tailwind.css'), path.join(customerBuildDir, 'src/assets/css/tailwind.css'));
160
+ fs.copyFileSync(path.join(distDir, 'assets/css/tailwind.css'), path.join(customerBuildDir, 'dist/assets/css/tailwind.css'));
161
+
151
162
  if (fs.existsSync(path.join(distDir, 'assets/css/squeditor-icons.css'))) {
152
163
  fs.copyFileSync(path.join(distDir, 'assets/css/squeditor-icons.css'), path.join(customerBuildDir, 'src/assets/css/squeditor-icons.css'));
164
+ fs.copyFileSync(path.join(distDir, 'assets/css/squeditor-icons.css'), path.join(customerBuildDir, 'dist/assets/css/squeditor-icons.css'));
165
+ }
166
+
167
+ if (fs.existsSync(path.join(distDir, 'assets/css/main.min.css'))) {
168
+ fs.copyFileSync(path.join(distDir, 'assets/css/main.min.css'), path.join(customerBuildDir, 'dist/assets/css/main.min.css'));
169
+ }
170
+
171
+ // slider.min.css contains the slider library CSS (Splide/Swiper)
172
+ if (fs.existsSync(path.join(distDir, 'assets/css/slider.min.css'))) {
173
+ fs.copyFileSync(path.join(distDir, 'assets/css/slider.min.css'), path.join(customerBuildDir, 'src/assets/css/slider.min.css'));
174
+ fs.copyFileSync(path.join(distDir, 'assets/css/slider.min.css'), path.join(customerBuildDir, 'dist/assets/css/slider.min.css'));
153
175
  }
154
176
 
155
177
  fs.cpSync(path.join(srcDir, 'assets/scss'), path.join(customerBuildDir, 'src/assets/scss'), { recursive: true });
@@ -167,13 +189,19 @@ async function createCustomerPackage() {
167
189
  }
168
190
 
169
191
  fs.mkdirSync(path.join(customerBuildDir, 'src/assets/js'), { recursive: true });
192
+ fs.mkdirSync(path.join(customerBuildDir, 'dist/assets/js'), { recursive: true });
170
193
  fs.copyFileSync(path.join(distDir, 'assets/js/uikit-components.js'), path.join(customerBuildDir, 'src/assets/js/uikit-components.js'));
194
+ fs.copyFileSync(path.join(distDir, 'assets/js/uikit-components.js'), path.join(customerBuildDir, 'dist/assets/js/uikit-components.js'));
171
195
  fs.copyFileSync(path.join(distDir, 'assets/js/main.js'), path.join(customerBuildDir, 'src/assets/js/main.js'));
196
+ fs.copyFileSync(path.join(distDir, 'assets/js/main.js'), path.join(customerBuildDir, 'dist/assets/js/main.js'));
172
197
 
173
198
  const staticDistPath = path.join(distDir, 'assets/static');
174
- const staticCustomerPath = path.join(customerBuildDir, 'src/assets/static');
199
+ const staticCustomerSourcePath = path.join(customerBuildDir, 'src/assets/static');
200
+ const staticCustomerDistPath = path.join(customerBuildDir, 'dist/assets/static');
175
201
  if (fs.existsSync(staticDistPath)) {
176
- await walkAndProcessMedia(staticDistPath, staticCustomerPath, staticDistPath);
202
+ await walkAndProcessMedia(staticDistPath, staticCustomerSourcePath, staticDistPath);
203
+ // Also move these final processed media assets directly from src/ back to dist/
204
+ fs.cpSync(staticCustomerSourcePath, staticCustomerDistPath, { recursive: true });
177
205
  }
178
206
 
179
207
  console.log(' - Generating lean package.json and vite.config.js');
@@ -256,13 +284,16 @@ ${rollupInputs} },
256
284
  const postcssConfig = `module.exports = {\n plugins: {\n tailwindcss: {},\n autoprefixer: {},\n },\n}`;
257
285
  fs.writeFileSync(path.join(customerBuildDir, 'postcss.config.js'), postcssConfig);
258
286
 
259
- const readmeContent = `# ${config.name} - Customer Package\n\nThis package contains everything you need to use, customize, and deploy your template.\n\n## Directory Structure\n- \`src/\`: Source HTML files.\n- \`src/assets/\`: Source files for customization (SCSS, JS, Images).\n\n## How to Customize Styles\n1. Install dependencies: \`npm install\`\n2. Run live development server: \`npm run dev\`\n3. Build production assets: \`npm run build\`\n`;
287
+ const readmeContent = `# ${config.name} - Customer Package\n\nThis package contains everything you need to use, customize, and deploy your template.\n\n## Directory Structure\n- \`src/\`: Developer Source files (\`npm run dev\` needed).\n- \`dist/\`: Production-ready compiled HTML snapshot (Drop into any hosting).\n\n## How to Customize Styles\n1. Install dependencies: \`npm install\`\n2. Run live development server: \`npm run dev\`\n3. Edit styles in \`src/assets/scss/main.scss\`\n4. Build production assets to \`dist/\`: \`npm run build\`\n`;
260
288
  fs.writeFileSync(path.join(customerBuildDir, 'README.md'), readmeContent);
261
289
 
290
+ // Format customer HTML files with Prettier (fallback in case snapshot.js Prettier was skipped)
262
291
  try {
263
292
  console.log(' - Formatting customer HTML files with Prettier...');
264
- execSync('npx prettier --write "src/**/*.html" --print-width 10000 --tab-width 4', { cwd: customerBuildDir, stdio: 'ignore' });
265
- } catch (e) { }
293
+ execSync('npx prettier --write "src/**/*.html" "dist/**/*.html" --print-width 10000 --tab-width 4', { cwd: customerBuildDir, stdio: 'ignore' });
294
+ } catch (e) {
295
+ console.warn(' - ⚠️ Prettier formatting skipped.');
296
+ }
266
297
 
267
298
  console.log(`[Squeditor] 📦 Zipping Customer Package to ${zipName}...`);
268
299
  try {
@@ -20,7 +20,7 @@ try {
20
20
  // Explicitly clean up any .DS_Store files that might have been created by the OS
21
21
  execSync(`find "${distDir}" -name ".DS_Store" -delete`, { stdio: 'inherit' });
22
22
  execSync(`cd "${distDir}" && zip -r -9 "${zipPath}" . -x "*.DS_Store" -x "*/.DS_Store"`, { stdio: 'inherit' });
23
- console.log(`[Squeditor] ✅ Customer Ready: ${zipName}`);
23
+ console.log(`[Squeditor] ✅ Dist ZIP Ready: ${zipName}`);
24
24
  } catch (e) {
25
25
  console.error(`[Squeditor] ❌ Failed to create ZIP archive using system zip.`, e);
26
26
  }
@@ -52,7 +52,8 @@ const frameworkTargetDir = path.resolve(process.cwd(), 'squeditor-framework');
52
52
 
53
53
  if (!fs.existsSync(frameworkTargetDir)) {
54
54
  console.log(`[Squeditor] Installing local framework core at ./squeditor-framework...`);
55
- const ignoreCoreList = ['project-template', 'showcase', 'node_modules', '.git', '.github'];
55
+ // Pass the name of the target directory to the ignore list to prevent infinite loop
56
+ const ignoreCoreList = ['project-template', 'showcase', 'node_modules', '.git', '.github', 'squeditor-framework'];
56
57
  copyDirectory(frameworkSourceDir, frameworkTargetDir, ignoreCoreList);
57
58
  }
58
59
 
@@ -71,8 +72,8 @@ if (fs.existsSync(pkgJsonPath)) {
71
72
  const configPath = path.join(targetDir, 'squeditor.config.js');
72
73
  if (fs.existsSync(configPath)) {
73
74
  let configContent = fs.readFileSync(configPath, 'utf8');
74
- configContent = configContent.replace(/name:\s*['"][^'"]+['"]/, `name: '${projectName}'`);
75
- configContent = configContent.replace(/zipName:\s*['"][^'"]+['"]/, `zipName: '${projectName}.zip'`);
75
+ configContent = configContent.replace(/name:\s*['"][^'"]+['"]/, `name: '${projectName}-customer'`);
76
+ configContent = configContent.replace(/zipName:\s*['"][^'"]+['"]/, `zipName: '${projectName}-customer.zip'`);
76
77
  fs.writeFileSync(configPath, configContent);
77
78
  }
78
79
 
@@ -1,49 +1,114 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
3
  const http = require('http');
4
- const { spawn } = require('child_process');
4
+ const { spawn, execSync } = require('child_process');
5
5
 
6
6
  const projectRoot = process.cwd();
7
7
  const config = require(path.join(projectRoot, 'squeditor.config.js'));
8
+ const resolvePages = require('./utils/resolve-pages');
9
+ const getAvailablePort = require('./get-port');
8
10
 
9
- const { baseUrl, pages, outputDir, rewriteExtension } = config.snapshot;
11
+ const { outputDir, rewriteExtension } = config.snapshot;
10
12
  const distDir = path.join(projectRoot, outputDir);
11
13
 
12
- fs.mkdirSync(distDir, { recursive: true });
14
+ // Build a reverse lookup: pagePath -> themeKey
15
+ // This allows snapshot.pages to be the AUTHORITATIVE list while themes are applied per-page
16
+ function buildThemeLookup() {
17
+ const lookup = {};
18
+ if (config.themes) {
19
+ for (const [themeKey, theme] of Object.entries(config.themes)) {
20
+ const resolvedThemePages = resolvePages(theme.pages || [], projectRoot);
21
+ for (const p of resolvedThemePages) {
22
+ lookup[p] = { themeKey, distSubfolder: theme.distSubfolder || '' };
23
+ }
24
+ }
25
+ }
26
+ return lookup;
27
+ }
13
28
 
14
- // Start PHP built-in server
15
- const phpServer = spawn('php', [
16
- '-S', '127.0.0.1:' + config.devServer.port,
17
- '-t', path.join(projectRoot, 'src')
18
- ], {
19
- stdio: 'ignore',
20
- env: { ...process.env, SQUEDITOR_SNAPSHOT: '1' }
21
- });
29
+ // Poll the PHP server until it responds (max ~10 seconds)
30
+ function waitForServer(url, maxRetries = 50, interval = 200) {
31
+ return new Promise((resolve, reject) => {
32
+ let attempts = 0;
33
+ const check = () => {
34
+ http.get(url, (res) => {
35
+ resolve();
36
+ }).on('error', () => {
37
+ attempts++;
38
+ if (attempts >= maxRetries) {
39
+ reject(new Error('PHP server failed to start within timeout'));
40
+ } else {
41
+ setTimeout(check, interval);
42
+ }
43
+ });
44
+ };
45
+ check();
46
+ });
47
+ }
48
+
49
+ async function runSnapshot() {
50
+ fs.mkdirSync(distDir, { recursive: true });
22
51
 
23
- // Wait for PHP server to start
24
- setTimeout(async () => {
25
- console.log('[Squeditor] 📸 Starting snapshot...');
52
+ // Use a dynamic port to avoid collisions with a running dev server
53
+ const snapshotPort = await getAvailablePort(config.devServer.port + 100);
54
+ const snapshotBaseUrl = `http://127.0.0.1:${snapshotPort}`;
26
55
 
27
- const themes = config.themes || { default: { pages: config.snapshot.pages, distSubfolder: '' } };
56
+ // Resolve dev-router path for proper .html and extensionless URL handling
57
+ const fwRoot = path.resolve(projectRoot, config.framework);
58
+ const devRouterPath = path.join(fwRoot, 'scripts/dev-router.php');
59
+
60
+ // Start PHP built-in server WITH the dev-router
61
+ const phpServer = spawn('php', [
62
+ '-S', `127.0.0.1:${snapshotPort}`,
63
+ '-t', path.join(projectRoot, 'src'),
64
+ devRouterPath
65
+ ], {
66
+ stdio: 'ignore',
67
+ env: { ...process.env, SQUEDITOR_SNAPSHOT: '1' }
68
+ });
28
69
 
29
- for (const [themeKey, theme] of Object.entries(themes)) {
30
- const themeDistDir = path.join(distDir, theme.distSubfolder || '');
31
- fs.mkdirSync(themeDistDir, { recursive: true });
70
+ try {
71
+ // Wait for PHP server to be ready instead of a fixed timeout
72
+ await waitForServer(snapshotBaseUrl);
32
73
 
33
- for (const pagePath of theme.pages) {
74
+ console.log('[Squeditor] 📸 Starting snapshot...');
75
+
76
+ // snapshot.pages is the AUTHORITATIVE list of pages to capture
77
+ const allPages = resolvePages(config.snapshot.pages || ['*'], projectRoot);
78
+ const themeLookup = buildThemeLookup();
79
+
80
+ for (const pagePath of allPages) {
34
81
  try {
82
+ // Determine which theme applies to this page (default if not in any theme)
83
+ const themeInfo = themeLookup[pagePath] || { themeKey: 'default', distSubfolder: '' };
84
+ const { themeKey, distSubfolder } = themeInfo;
85
+
35
86
  console.log(`[Squeditor] Fetching ${pagePath} (Theme: ${themeKey})...`);
36
- const urlToFetch = `${baseUrl}${pagePath}?theme=${themeKey}&snapshot=1`;
87
+
88
+ // Resolve fetch URI: '/' -> '/index.php'
89
+ let fetchUri = pagePath;
90
+ if (fetchUri === '/') fetchUri = '/index.php';
91
+
92
+ const normalizedPagePath = fetchUri.startsWith('/') ? fetchUri : `/${fetchUri}`;
93
+ const urlToFetch = `${snapshotBaseUrl}${normalizedPagePath}?theme=${themeKey}&snapshot=1`;
37
94
  const html = await fetchPage(urlToFetch);
38
- const rewrittenHtml = rewriteLinks(html);
39
95
 
40
- // Remove leading slash for local save path
41
- let savePath = pagePath.startsWith('/') ? pagePath.slice(1) : pagePath;
42
- if (savePath === '') savePath = 'index.html';
43
- if (rewriteExtension && savePath.endsWith('.php')) {
44
- savePath = savePath.replace(/\.php$/, '.html');
96
+ // Construct save path
97
+ let savePath = pagePath;
98
+ if (savePath === '/' || savePath === '') {
99
+ savePath = 'index.html';
100
+ } else {
101
+ savePath = savePath.startsWith('/') ? savePath.slice(1) : savePath;
102
+ if (rewriteExtension && savePath.endsWith('.php')) {
103
+ savePath = savePath.replace(/\.php$/, '.html');
104
+ }
45
105
  }
46
106
 
107
+ const themeDistDir = path.join(distDir, distSubfolder);
108
+ fs.mkdirSync(themeDistDir, { recursive: true });
109
+
110
+ const rewrittenHtml = rewriteLinks(html, savePath, distSubfolder);
111
+
47
112
  const fullPath = path.join(themeDistDir, savePath);
48
113
  fs.mkdirSync(path.dirname(fullPath), { recursive: true });
49
114
  fs.writeFileSync(fullPath, rewrittenHtml);
@@ -51,15 +116,32 @@ setTimeout(async () => {
51
116
  console.error(`[Squeditor] Failed to snapshot ${pagePath}:`, err.message);
52
117
  }
53
118
  }
119
+
120
+ // Format all generated HTML with Prettier
121
+ try {
122
+ console.log('[Squeditor] 💅 Formatting dist HTML with Prettier...');
123
+ execSync(`npx prettier --write "${outputDir}/**/*.html" --print-width 10000 --tab-width 4`, { cwd: projectRoot, stdio: 'ignore' });
124
+ } catch (e) {
125
+ console.warn('[Squeditor] ⚠️ Prettier formatting skipped (not installed or errored).');
126
+ }
127
+
128
+ console.log('[Squeditor] 🏁 Snapshot complete.');
129
+ } finally {
130
+ phpServer.kill();
54
131
  }
132
+ }
55
133
 
56
- phpServer.kill();
57
- console.log('[Squeditor] 🏁 Snapshot complete.');
58
- }, 1500);
134
+ runSnapshot().catch(err => {
135
+ console.error('[Squeditor] Snapshot failed:', err.message);
136
+ process.exit(1);
137
+ });
59
138
 
60
139
  function fetchPage(url) {
61
140
  return new Promise((resolve, reject) => {
62
141
  http.get(url, (res) => {
142
+ if (res.statusCode < 200 || res.statusCode >= 300) {
143
+ return reject(new Error(`Status Code: ${res.statusCode} for ${url}`));
144
+ }
63
145
  let data = '';
64
146
  res.on('data', chunk => data += chunk);
65
147
  res.on('end', () => resolve(data));
@@ -67,8 +149,27 @@ function fetchPage(url) {
67
149
  });
68
150
  }
69
151
 
70
- function rewriteLinks(html) {
71
- if (!rewriteExtension) return html;
72
- // Replace .php hrefs with .html
73
- return html.replace(/href="([^"]*?)\.php([^"]*?)"/g, 'href="$1.html$2"');
152
+ function rewriteLinks(html, savePath, distSubfolder) {
153
+ let result = html;
154
+
155
+ // Calculate relative path back to dist root
156
+ const subfolderDepth = distSubfolder ? distSubfolder.split('/').filter(Boolean).length : 0;
157
+ const savePathDepth = path.dirname(savePath) === '.' ? 0 : path.dirname(savePath).split('/').length;
158
+
159
+ const totalDepth = subfolderDepth + savePathDepth;
160
+ const prefix = totalDepth > 0 ? '../'.repeat(totalDepth) : '';
161
+
162
+ // Rewrite root-relative and purely relative references to depth-adjusted references
163
+ result = result.replace(/(href|src)=["']\/?([^"']+)["']/g, (match, attr, targetPath) => {
164
+ if (targetPath.startsWith('http') || targetPath.startsWith('//') || targetPath.startsWith('#')) {
165
+ return match;
166
+ }
167
+ return `${attr}="${prefix}${targetPath}"`;
168
+ });
169
+
170
+ if (rewriteExtension) {
171
+ result = result.replace(/href=["']([^"']*?)\.php([^"']*?)["']/g, 'href="$1.html$2"');
172
+ }
173
+
174
+ return result;
74
175
  }
@@ -0,0 +1,47 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ module.exports = function resolvePages(pagesConfig, projectRoot) {
5
+ if (!pagesConfig || !Array.isArray(pagesConfig)) return [];
6
+
7
+ // Check if any element has a glob (* or !)
8
+ const hasGlob = pagesConfig.some(p => p.includes('*') || p.startsWith('!'));
9
+
10
+ if (hasGlob) {
11
+ let micromatch;
12
+ try {
13
+ micromatch = require(path.join(projectRoot, 'node_modules/micromatch'));
14
+ } catch (e) {
15
+ console.warn('[Squeditor] micromatch not found. Please run npm install.');
16
+ return [];
17
+ }
18
+
19
+ const srcDir = path.join(projectRoot, 'src');
20
+ if (!fs.existsSync(srcDir)) return [];
21
+
22
+ // Exclude known non-page PHP files that should never be snapshotted
23
+ const NON_PAGE_FILES = ['init.php'];
24
+ const allFiles = fs.readdirSync(srcDir)
25
+ .filter(f => f.endsWith('.php') && !NON_PAGE_FILES.includes(f));
26
+
27
+ // Micromatch expects naked filenames logically.
28
+ // We ensure config patterns like '/' become 'index.php' for matching,
29
+ // and remove leading slashes so they match naked filenames natively.
30
+ const normalizedConfig = pagesConfig.map(p => {
31
+ if (p === '/') return 'index.php';
32
+ if (p.startsWith('/')) return p.slice(1);
33
+ return p;
34
+ });
35
+
36
+ const matched = micromatch(allFiles, normalizedConfig);
37
+
38
+ // Convert back to URL-style paths that the rest of the application expects
39
+ return matched.map(f => f === 'index.php' ? '/' : `/${f}`);
40
+ } else {
41
+ // Direct explicit list (legacy map support)
42
+ return pagesConfig.map(p => {
43
+ if (p === '/') return '/';
44
+ return p.startsWith('/') ? p : `/${p}`;
45
+ });
46
+ }
47
+ };