@squeditor/squeditor-framework 1.0.2 → 1.0.4

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.
Files changed (33) hide show
  1. package/README.md +22 -31
  2. package/package.json +1 -1
  3. package/php/functions.php +45 -0
  4. package/project-template/package.json +3 -1
  5. package/project-template/squeditor.config.js +21 -9
  6. package/project-template/src/assets/js/_slider_dynamic.js +2 -0
  7. package/project-template/src/assets/js/gsap-init.js +28 -1
  8. package/project-template/src/assets/js/main.js +1 -0
  9. package/project-template/src/assets/js/modules/splide-init.js +207 -0
  10. package/project-template/src/assets/js/modules/swiper-init.js +216 -0
  11. package/project-template/src/assets/js/uikit-components.js +27 -21
  12. package/project-template/src/assets/scss/_base.scss +0 -9
  13. package/project-template/src/assets/scss/_components.scss +107 -2
  14. package/project-template/src/assets/scss/_swiper.scss +30 -0
  15. package/project-template/src/assets/scss/main.scss +2 -1
  16. package/project-template/src/assets/scss/themes/_two.scss +95 -0
  17. package/project-template/src/assets/static/images/og-default.png +0 -0
  18. package/project-template/src/assets/static/images/placeholder.png +0 -0
  19. package/project-template/src/index.php +5 -5
  20. package/project-template/src/init.php +38 -2
  21. package/project-template/src/page-templates/head.php +9 -1
  22. package/project-template/src/slider-test.php +87 -0
  23. package/project-template/src/template-parts/header.php +11 -11
  24. package/project-template/tailwind.config.js +29 -1
  25. package/scripts/build-components.js +78 -1
  26. package/scripts/copy-static.js +26 -0
  27. package/scripts/dev-router.php +10 -1
  28. package/scripts/dev.js +30 -1
  29. package/scripts/package-customer.js +52 -21
  30. package/scripts/package-dist.js +1 -1
  31. package/scripts/scaffold.js +4 -3
  32. package/scripts/snapshot.js +134 -33
  33. package/scripts/utils/resolve-pages.js +47 -0
@@ -6,6 +6,7 @@ const projectRoot = process.cwd();
6
6
  const config = require(path.join(projectRoot, 'squeditor.config.js'));
7
7
  const fwRoot = path.resolve(projectRoot, config.framework); // resolves ..
8
8
  const manifest = require(path.join(fwRoot, 'uikit-manifest.json'));
9
+ const resolvePages = require('./utils/resolve-pages');
9
10
 
10
11
 
11
12
  const selectedComponents = config.components || [];
@@ -82,12 +83,34 @@ console.log(` Components included: _core, ${selectedComponents.join(', ')}`);
82
83
  // Generate src/config/active-components.php for the style-guide page
83
84
  const phpConfigDir = path.join(projectRoot, 'src/config');
84
85
  fs.mkdirSync(phpConfigDir, { recursive: true });
85
- const phpConfig = `<?php\n// Auto-generated by build-components.js DO NOT EDIT\n$active_components = ${JSON.stringify(selectedComponents)};\n`;
86
+ const phpComponentsArray = `[${selectedComponents.map(c => `'${c}'`).join(', ')}]`;
87
+ const phpConfig = `<?php\n// Auto-generated by build-components.js — DO NOT EDIT\n$active_components = ${phpComponentsArray};\n`;
86
88
  fs.writeFileSync(
87
89
  path.join(phpConfigDir, 'active-components.php'),
88
90
  phpConfig
89
91
  );
90
92
 
93
+ // Generate src/config/active-themes.php for automatic PHP dev server theme detection
94
+ // Map each page to its corresponding theme identifier
95
+ const themePageMapping = {};
96
+ if (config.themes) {
97
+ Object.keys(config.themes).forEach(themeKey => {
98
+ const rawPages = config.themes[themeKey].pages || [];
99
+ const absolutePages = resolvePages(rawPages, projectRoot);
100
+ absolutePages.forEach(page => {
101
+ // Clean leading slashes for normalization if desired, but here we just map exact values
102
+ themePageMapping[page] = themeKey;
103
+ });
104
+ });
105
+ }
106
+ const phpThemesArrayParts = Object.entries(themePageMapping).map(([page, theme]) => `'${page}' => '${theme}'`);
107
+ const phpThemesConfig = `<?php\n// Auto-generated by build-components.js — DO NOT EDIT\n$active_themes = [${phpThemesArrayParts.join(', ')}];\n`;
108
+ fs.writeFileSync(
109
+ path.join(phpConfigDir, 'active-themes.php'),
110
+ phpThemesConfig
111
+ );
112
+
113
+
91
114
  // Inject all themes into main.scss so live-switching works in the Style Guide
92
115
  const mainScssPath = path.join(projectRoot, 'src/assets/scss/main.scss');
93
116
  if (fs.existsSync(mainScssPath) && config.themes) {
@@ -107,3 +130,57 @@ if (fs.existsSync(mainScssPath) && config.themes) {
107
130
  fs.writeFileSync(mainScssPath, mainScss);
108
131
  console.log(`[Squeditor] 🎨 Injected themes: ${Object.keys(config.themes).join(', ')}`);
109
132
  }
133
+
134
+ // Generate Dynamic Slider Config Import
135
+ const sliderConfig = config.slider || { library: false };
136
+ const dynamicSliderPath = path.join(outputJsDir, '_slider_dynamic.js');
137
+ let sliderImportCode = '// Auto-generated by build-components.js — DO NOT EDIT\n';
138
+ if (sliderConfig.library === 'swiper') {
139
+ sliderImportCode += 'import \'./modules/swiper-init.js\';\n';
140
+ } else if (sliderConfig.library === 'splide') {
141
+ sliderImportCode += 'import \'./modules/splide-init.js\';\n';
142
+ }
143
+ fs.writeFileSync(dynamicSliderPath, sliderImportCode);
144
+ console.log(`[Squeditor] 🎠 Injected slider library: ${sliderConfig.library || 'none'}`);
145
+
146
+ // Copy slider library CSS to src/assets/css/slider.min.css
147
+ // This keeps the CSS separate from main.js and gives it a clear, descriptive filename
148
+ const sliderCssDest = path.join(projectRoot, 'src/assets/css/slider.min.css');
149
+ if (sliderConfig.library === 'splide') {
150
+ const splideCssPath = path.join(projectRoot, 'node_modules/@splidejs/splide/dist/css/splide.min.css');
151
+ if (fs.existsSync(splideCssPath)) {
152
+ fs.copyFileSync(splideCssPath, sliderCssDest);
153
+ console.log('[Squeditor] 📎 Copied Splide CSS → src/assets/css/slider.min.css');
154
+ } else {
155
+ console.warn('[Squeditor] ⚠️ Splide CSS not found at expected path.');
156
+ fs.writeFileSync(sliderCssDest, '/* Splide CSS not found */\n');
157
+ }
158
+ } else if (sliderConfig.library === 'swiper') {
159
+ // Concatenate all Swiper CSS modules into a single file
160
+ const swiperCssParts = [
161
+ 'node_modules/swiper/swiper.min.css',
162
+ 'node_modules/swiper/modules/navigation.min.css',
163
+ 'node_modules/swiper/modules/pagination.min.css',
164
+ 'node_modules/swiper/modules/effect-fade.min.css',
165
+ 'node_modules/swiper/modules/free-mode.min.css',
166
+ ];
167
+ let combinedCss = '/* Swiper CSS - auto-generated by build-components.js */\n';
168
+ for (const part of swiperCssParts) {
169
+ const fullPath = path.join(projectRoot, part);
170
+ if (fs.existsSync(fullPath)) {
171
+ combinedCss += fs.readFileSync(fullPath, 'utf8') + '\n';
172
+ }
173
+ }
174
+ fs.writeFileSync(sliderCssDest, combinedCss);
175
+ console.log('[Squeditor] 📎 Copied Swiper CSS → src/assets/css/slider.min.css');
176
+ } else {
177
+ // No slider configured — write an empty placeholder so head.php link doesn't 404
178
+ fs.writeFileSync(sliderCssDest, '/* No slider library configured */\n');
179
+ }
180
+
181
+ // Generate src/config/active-slider.php for conditional CSS loading in head.php
182
+ const phpSliderConfig = `<?php\n// Auto-generated by build-components.js — DO NOT EDIT\n$active_slider = ${sliderConfig.library ? `'${sliderConfig.library}'` : 'false'};\n`;
183
+ fs.writeFileSync(
184
+ path.join(phpConfigDir, 'active-slider.php'),
185
+ phpSliderConfig
186
+ );
@@ -145,6 +145,32 @@ async function run() {
145
145
  } else {
146
146
  console.warn(`[Squeditor] ⚠️ Source directory for static assets not found: ${srcDir}`);
147
147
  }
148
+
149
+ // Post-Vite-build CSS renaming for clearer dist output
150
+ const distCssDir = path.join(projectRoot, config.snapshot.outputDir, 'assets/css');
151
+
152
+ // Rename main_css.css → main.min.css (SCSS entry output)
153
+ const mainCssSrc = path.join(distCssDir, 'main_css.css');
154
+ const mainCssDest = path.join(distCssDir, 'main.min.css');
155
+ if (fs.existsSync(mainCssSrc)) {
156
+ fs.renameSync(mainCssSrc, mainCssDest);
157
+ console.log('[Squeditor] 📎 Renamed main_css.css → main.min.css');
158
+ }
159
+
160
+ // Remove stale main.css (previously contained CSS-in-JS extracted by Vite, now handled separately)
161
+ const staleMainCss = path.join(distCssDir, 'main.css');
162
+ if (fs.existsSync(staleMainCss)) {
163
+ fs.unlinkSync(staleMainCss);
164
+ }
165
+
166
+ // Copy slider.min.css to dist (generated by build-components.js)
167
+ const sliderCssSrc = path.join(projectRoot, 'src/assets/css/slider.min.css');
168
+ const sliderCssDest = path.join(distCssDir, 'slider.min.css');
169
+ if (fs.existsSync(sliderCssSrc)) {
170
+ fs.mkdirSync(distCssDir, { recursive: true });
171
+ fs.copyFileSync(sliderCssSrc, sliderCssDest);
172
+ console.log('[Squeditor] 📎 Copied slider.min.css → dist/assets/css/');
173
+ }
148
174
  }
149
175
 
150
176
  run();
@@ -19,5 +19,14 @@ if (preg_match('/\.html$/', $uri)) {
19
19
  }
20
20
  }
21
21
 
22
- // 3. Let PHP's core handle index.php fallback for directories or 404s
22
+ // 3. If it is an extensionless request, route it to the equivalent PHP file
23
+ if ($uri !== '/' && !pathinfo($uri, PATHINFO_EXTENSION)) {
24
+ $php_file = $uri . '.php';
25
+ if (file_exists($doc_root . $php_file)) {
26
+ require $doc_root . $php_file;
27
+ return true;
28
+ }
29
+ }
30
+
31
+ // 4. Let PHP's core handle index.php fallback for directories or 404s
23
32
  return false;
package/scripts/dev.js CHANGED
@@ -23,6 +23,18 @@ async function startDev() {
23
23
  fwRoot = config.framework;
24
24
  }
25
25
  }
26
+
27
+ // Run build-components.js BEFORE starting servers to ensure
28
+ // active-themes.php, _uikit_dynamic.scss, _slider_dynamic.js etc. exist on first request
29
+ const buildComponentsPath = path.join(fwRoot, 'scripts/build-components.js');
30
+ console.log('[Squeditor] 🔧 Building dynamic components...');
31
+ const { execSync } = require('child_process');
32
+ try {
33
+ execSync(`node "${buildComponentsPath}"`, { stdio: 'inherit', cwd: projectRoot });
34
+ } catch (e) {
35
+ console.error('[Squeditor] ❌ Failed to build dynamic components:', e.message);
36
+ }
37
+
26
38
  const devRouterPath = path.join(fwRoot, 'scripts/dev-router.php');
27
39
 
28
40
  // Start PHP Server
@@ -35,7 +47,6 @@ async function startDev() {
35
47
  env: { ...process.env, SQUEDITOR_PHP_PORT: phpPort, SQUEDITOR_VITE_PORT: vitePort }
36
48
  });
37
49
 
38
- // Start Vite
39
50
  const vite = spawn('npx', [
40
51
  'vite',
41
52
  '--port', vitePort.toString(),
@@ -45,6 +56,24 @@ async function startDev() {
45
56
  env: { ...process.env, SQUEDITOR_PHP_PORT: phpPort, SQUEDITOR_VITE_PORT: vitePort }
46
57
  });
47
58
 
59
+ // Watch squeditor.config.js for changes and rebuild dynamic components
60
+ if (fs.existsSync(configPath)) {
61
+ let rebuildTimeout;
62
+ fs.watch(configPath, (eventType) => {
63
+ if (eventType === 'change') {
64
+ // Debounce to prevent multiple triggers from IDE saves
65
+ clearTimeout(rebuildTimeout);
66
+ rebuildTimeout = setTimeout(() => {
67
+ console.log(`\n[Squeditor] 🔄 Config changed. Rebuilding dynamic components...`);
68
+ const buildScript = spawn('node', [path.join(fwRoot, 'scripts/build-components.js')], { stdio: 'inherit' });
69
+ buildScript.on('close', (code) => {
70
+ if (code === 0) console.log(`[Squeditor] ✨ Rebuild complete! (Vite will hot-reload automatically)`);
71
+ });
72
+ }, 300);
73
+ }
74
+ });
75
+ }
76
+
48
77
  process.on('SIGINT', () => {
49
78
  php.kill();
50
79
  vite.kill();
@@ -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
+ };