@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.
- package/README.md +22 -31
- package/package.json +1 -1
- package/php/functions.php +45 -0
- package/project-template/package.json +3 -1
- package/project-template/squeditor.config.js +21 -9
- package/project-template/src/assets/js/_slider_dynamic.js +2 -0
- package/project-template/src/assets/js/gsap-init.js +28 -1
- package/project-template/src/assets/js/main.js +1 -0
- package/project-template/src/assets/js/modules/splide-init.js +207 -0
- package/project-template/src/assets/js/modules/swiper-init.js +216 -0
- package/project-template/src/assets/js/uikit-components.js +27 -21
- package/project-template/src/assets/scss/_base.scss +0 -9
- package/project-template/src/assets/scss/_components.scss +107 -2
- package/project-template/src/assets/scss/_swiper.scss +30 -0
- package/project-template/src/assets/scss/main.scss +2 -1
- package/project-template/src/assets/scss/themes/_two.scss +95 -0
- package/project-template/src/assets/static/images/og-default.png +0 -0
- package/project-template/src/assets/static/images/placeholder.png +0 -0
- package/project-template/src/index.php +5 -5
- package/project-template/src/init.php +38 -2
- package/project-template/src/page-templates/head.php +9 -1
- package/project-template/src/slider-test.php +87 -0
- package/project-template/src/template-parts/header.php +11 -11
- package/project-template/tailwind.config.js +29 -1
- package/scripts/build-components.js +78 -1
- package/scripts/copy-static.js +26 -0
- package/scripts/dev-router.php +10 -1
- package/scripts/dev.js +30 -1
- package/scripts/package-customer.js +52 -21
- package/scripts/package-dist.js +1 -1
- package/scripts/scaffold.js +4 -3
- package/scripts/snapshot.js +134 -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
|
|
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
|
+
);
|
package/scripts/copy-static.js
CHANGED
|
@@ -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();
|
package/scripts/dev-router.php
CHANGED
|
@@ -19,5 +19,14 @@ if (preg_match('/\.html$/', $uri)) {
|
|
|
19
19
|
}
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
// 3.
|
|
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, '
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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, '
|
|
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
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
|
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
|
|
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,
|
|
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
|
|
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 {
|
package/scripts/package-dist.js
CHANGED
|
@@ -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] ✅
|
|
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
|
}
|
package/scripts/scaffold.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
package/scripts/snapshot.js
CHANGED
|
@@ -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 {
|
|
11
|
+
const { outputDir, rewriteExtension } = config.snapshot;
|
|
10
12
|
const distDir = path.join(projectRoot, outputDir);
|
|
11
13
|
|
|
12
|
-
|
|
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
|
-
//
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
//
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
70
|
+
try {
|
|
71
|
+
// Wait for PHP server to be ready instead of a fixed timeout
|
|
72
|
+
await waitForServer(snapshotBaseUrl);
|
|
32
73
|
|
|
33
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
41
|
-
let savePath = pagePath
|
|
42
|
-
if (savePath === ''
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
57
|
-
console.
|
|
58
|
-
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
+
};
|