sdc-build-wp 4.7.1 → 4.9.0

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 CHANGED
@@ -8,13 +8,16 @@ sdc-build-wp --watch --builds=style,scripts # comma-seperated list of components
8
8
  sdc-build-wp --help
9
9
  ```
10
10
 
11
- ## Develop
11
+ ## Caching
12
12
 
13
- Develop locally with the following command from within the test project directory:
13
+ sdc-build-wp includes intelligent build caching to speed up subsequent builds by only rebuilding files that have changed or whose dependencies have changed.
14
14
 
15
+ ```sh
16
+ sdc-build-wp --no-cache # Disable caching for this build
17
+ sdc-build-wp --clear-cache # Clear all cached data
15
18
  ```
16
- node ~/sites/sdc/sdc-build-wp/index.js --watch
17
- ```
19
+
20
+ ## Watch
18
21
 
19
22
  While watch is enabled, use the following keyboard commands to control the build process:
20
23
 
@@ -23,3 +26,11 @@ While watch is enabled, use the following keyboard commands to control the build
23
26
  [p] Pause/Resume
24
27
  [q] Quit
25
28
  ````
29
+
30
+ ## Develop
31
+
32
+ Develop locally with the following command from within the test project directory:
33
+
34
+ ```
35
+ node ~/sites/sdc/sdc-build-wp/index.js --watch
36
+ ```
package/index.js CHANGED
@@ -1,130 +1,11 @@
1
1
  #!/usr/bin/env node
2
- import parseArgs from 'minimist';
3
- import path from 'path';
4
- import { fileURLToPath } from 'url';
5
- import { promises as fs } from 'fs';
6
- import project from './lib/project.js';
7
- import log from './lib/logging.js';
8
- import * as utils from './lib/utils.js';
9
- import * as LibComponents from './lib/components/index.js';
10
-
11
- project.components = Object.fromEntries(Object.entries(LibComponents).map(([name, Class]) => [name, new Class()]));
12
-
13
- const argv = parseArgs(process.argv.slice(2));
14
-
15
- if (argv.help || argv.h) {
16
- console.log(`
17
- Usage: sdc-build-wp [options] [arguments]
18
-
19
- Options:
20
- -h, --help Show help message and exit
21
- -v, --version Version
22
- -w, --watch Build and watch
23
- -b, --builds BUILDS Build with specific components
24
-
25
- Components:
26
-
27
- ${Object.entries(project.components).map(([key, component]) => {
28
- return `${key}\t\t${component.description}\r\n`;
29
- }).join('')}
30
- Examples:
31
-
32
- sdc-build-wp
33
- sdc-build-wp --watch
34
- sdc-build-wp --watch --builds=style,scripts
35
-
36
- While watch is enabled, use the following keyboard commands to control the build process:
37
-
38
- [r] Restart
39
- [p] Pause/Resume
40
- [q] Quit
41
- `);
42
-
43
- process.exit(0);
44
- } else if (argv.version || argv.v) {
45
- console.log(JSON.parse(await fs.readFile(path.join(path.dirname(fileURLToPath(import.meta.url)), 'package.json'))).version);
46
- process.exit(0);
47
- }
48
-
49
- project.builds = argv.builds ? (Array.isArray(argv.builds) ? argv.builds : argv.builds.split(',')) : Object.keys(project.components);
2
+ import { default as project, init, keypressListen } from './lib/project.js';
3
+ import build from './lib/build.js';
50
4
 
51
5
  (async () => {
52
- keypressListen();
53
- await runBuild();
54
- })();
55
-
56
- process.on('SIGINT', function () {
57
- console.log(`\r`);
58
- utils.stopActiveComponents();
59
- project.isRunning = false;
60
- utils.clearScreen();
61
- log('info', `Exited sdc-build-wp`);
62
- if (process.stdin.isTTY) {
63
- process.stdin.setRawMode(false);
64
- process.stdin.pause();
65
- }
66
- process.exit(0);
67
- });
68
-
69
- function keypressListen() {
70
- if (!process.stdin.isTTY) { return; }
71
-
72
- process.stdin.setRawMode(true);
73
- process.stdin.resume();
74
- process.stdin.setEncoding('utf8');
75
-
76
- process.stdin.on('data', (key) => {
77
- switch (key) {
78
- case '\u0003': // Ctrl+C
79
- case 'q':
80
- process.emit('SIGINT');
81
- return;
82
- case 'p':
83
- project.isRunning = !project.isRunning;
84
- utils.clearScreen();
85
- if (project.isRunning) {
86
- log('success', 'Resumed build process');
87
- } else {
88
- log('warn', 'Paused build process');
89
- }
90
- break;
91
- case 'r':
92
- log('info', 'Restarted build process');
93
- utils.stopActiveComponents();
94
- setTimeout(() => {
95
- utils.clearScreen();
96
- runBuild();
97
- }, 100);
98
- break;
99
- }
100
- });
101
- }
102
-
103
- async function runBuild() {
104
- project.isRunning = true;
105
- if (argv.watch && project.builds.includes('server')) {
106
- project.builds.splice(project.builds.indexOf('server'), 1);
107
- project.builds.unshift('server');
108
- project.components.server.serve(false);
6
+ await init();
7
+ if (project.argv.watch) {
8
+ keypressListen();
109
9
  }
110
-
111
- let initialBuildTimerStart = performance.now();
112
- log('info', `Started initial build [${project.builds.join(', ')}]`);
113
- let promisesBuilds = [];
114
- for (let build of project.builds) {
115
- promisesBuilds.push(project.components[build].init());
116
- }
117
- await Promise.all(promisesBuilds);
118
- utils.clearScreen();
119
- log('info', `Finished initial build in ${Math.round((performance.now() - initialBuildTimerStart) / 1000)} seconds`);
120
-
121
- if (argv.watch && project.builds.includes('server')) {
122
- project.builds.splice(project.builds.indexOf('server'), 1);
123
- project.builds.push('server');
124
- log('info', `Started watching [${project.builds.join(', ')}]`);
125
- log('info', `[r] to restart, [p] to pause/resume, [q] to quit`);
126
- for (let build of project.builds) {
127
- await project.components[build].watch();
128
- }
129
- }
130
- }
10
+ await build(project.argv.watch);
11
+ })();
package/lib/build.js ADDED
@@ -0,0 +1,64 @@
1
+ import { default as project } from './project.js';
2
+ import * as utils from './utils.js';
3
+ import log from './logging.js';
4
+
5
+ export default async function(watch = false) {
6
+ if (project.components.cache && project.builds.includes('cache')) {
7
+ try {
8
+ await project.components.cache.init();
9
+ } catch (error) {
10
+ log('warn', `Failed to initialize cache: ${error.message}`);
11
+ }
12
+ }
13
+
14
+ if (watch && project.builds.includes('server')) {
15
+ project.builds.splice(project.builds.indexOf('server'), 1);
16
+ project.builds.unshift('server');
17
+ project.components.server.serve(false);
18
+ }
19
+
20
+ let initialBuildTimerStart = performance.now();
21
+ log('info', `Started initial build [${project.builds.join(', ')}]`);
22
+
23
+ let promisesBuilds = [];
24
+ for (let build of project.builds) {
25
+ if (build === 'cache') { continue; }
26
+
27
+ promisesBuilds.push(
28
+ project.components[build].init().catch(error => {
29
+ log('error', `Failed to initialize ${build} component: ${error.message}`);
30
+ return { failed: true, component: build, error };
31
+ })
32
+ );
33
+ }
34
+
35
+ const results = await Promise.all(promisesBuilds);
36
+
37
+ const failedComponents = results.filter(result => result && result.failed);
38
+ if (failedComponents.length > 0) {
39
+ log('warn', `Continuing without failed components: ${failedComponents.map(f => f.component).join(', ')}`);
40
+ project.builds = project.builds.filter(build => !failedComponents.some(f => f.component === build));
41
+ }
42
+
43
+ utils.clearScreen();
44
+ log('info', `Finished initial build in ${Math.round((performance.now() - initialBuildTimerStart) / 1000)} seconds`);
45
+
46
+ if (watch && project.builds.includes('server')) {
47
+ project.isRunning = true;
48
+ project.builds.splice(project.builds.indexOf('server'), 1);
49
+ project.builds.push('server');
50
+ log('info', `Started watching [${project.builds.join(', ')}]`);
51
+ log('info', `[r] to restart, [p] to pause/resume, [q] to quit`);
52
+
53
+ for (let build of project.builds) {
54
+ try {
55
+ await project.components[build].watch();
56
+ } catch (error) {
57
+ log('error', `Failed to start watcher for ${build}: ${error.message}`);
58
+ log('warn', `Continuing without ${build} watcher`);
59
+ }
60
+ }
61
+ } else {
62
+ process.emit('SIGINT');
63
+ }
64
+ }
@@ -19,6 +19,7 @@ class BaseComponent {
19
19
  this.glob = glob;
20
20
  this.files = [];
21
21
  this.globs = [];
22
+ this.useCache = true;
22
23
  }
23
24
 
24
25
  async init() {
@@ -34,9 +35,41 @@ class BaseComponent {
34
35
  verb: 'Built',
35
36
  itemLabel: null,
36
37
  timerStart: this.timer,
37
- timerEnd: performance.now()
38
+ timerEnd: performance.now(),
39
+ cached: false
38
40
  }, options);
39
- this.log('success', `${options.verb}${options.itemLabel ? ` ${options.itemLabel}` : ''} in ${Math.round(options.timerEnd - options.timerStart)}ms`);
41
+
42
+ const cacheIndicator = options.cached ? ' (cached)' : '';
43
+ this.log('success', `${options.verb}${options.itemLabel ? ` ${options.itemLabel}` : ''}${cacheIndicator} in ${Math.round(options.timerEnd - options.timerStart)}ms`);
44
+ }
45
+ async shouldSkipBuild(inputFile, outputFile, dependencies = []) {
46
+ if (!this.useCache || !this.project.components.cache || !this.project.components.cache.manifest?.entries) {
47
+ return false;
48
+ }
49
+
50
+ const needsRebuild = await this.project.components.cache.needsRebuild(
51
+ inputFile,
52
+ outputFile,
53
+ dependencies
54
+ );
55
+
56
+ return !needsRebuild;
57
+ }
58
+
59
+ async updateBuildCache(inputFile, outputFile, dependencies = []) {
60
+ if (!this.useCache || !this.project.components.cache || !this.project.components.cache.manifest?.entries) {
61
+ return;
62
+ }
63
+
64
+ await this.project.components.cache.updateCache(inputFile, outputFile, dependencies);
65
+ }
66
+
67
+ clearHashCache(filePaths) {
68
+ if (!this.useCache || !this.project.components.cache || !this.project.components.cache.manifest?.entries) {
69
+ return;
70
+ }
71
+
72
+ this.project.components.cache.clearHashCache(filePaths);
40
73
  }
41
74
 
42
75
  async watch() {
@@ -59,10 +59,21 @@ export default class BlocksComponent extends BaseComponent {
59
59
  `--webpack-copy-php`,
60
60
  `--config=${this.path.resolve(this.path.dirname(fileURLToPath(import.meta.url)), '../../webpack.config.js')}`,
61
61
  ];
62
- let execPromise = promisify(exec);
63
- const { stdout, stderr } = await execPromise(cmds.join(' '));
62
+ const execPromise = promisify(exec);
63
+ const timeoutMS = 40000;
64
+ const buildPromise = execPromise(cmds.join(' '), {
65
+ maxBuffer: 1024 * 1024 * 10,
66
+ cwd: this.project.path
67
+ });
68
+ const timeoutPromise = new Promise((_, reject) => {
69
+ setTimeout(() => reject(new Error(`Build timeout after ${timeoutMS / 1000} seconds`)), timeoutMS);
70
+ });
71
+ const { stdout, stderr } = await Promise.race([buildPromise, timeoutPromise]);
72
+ if (stderr && stderr.trim()) {
73
+ this.log('warn', `Build warnings for ${entryLabel}: ${stderr.trim()}`);
74
+ }
64
75
  } catch (error) {
65
- console.log(error.stdout || error);
76
+ console.error(error.stdout || error.stderr || error.message);
66
77
  this.log('error', `Failed building ${entryLabel} block - See above error.`);
67
78
  return false;
68
79
  }
@@ -84,19 +95,43 @@ export default class BlocksComponent extends BaseComponent {
84
95
  }
85
96
 
86
97
  watch() {
87
- for (let block of this.globs) {
88
- this.chokidar.watch(`${block}/src`, {
89
- ...this.project.chokidarOpts
90
- }).on('all', (event, path) => {
91
- if (!this.project.isRunning) { return; }
92
- if (!['unlink', 'unlinkDir'].includes(event)) {
98
+ const watchPaths = this.globs.map(block => `${block}/src`);
99
+ const buildQueue = new Set();
100
+ const debounceTimers = new Map();
101
+ const DEBOUNCE_DELAY = 500;
102
+
103
+ this.watcher = this.chokidar.watch(watchPaths, {
104
+ ...this.project.chokidarOpts
105
+ }).on('all', async (event, path) => {
106
+ if (!this.project.isRunning) { return; }
107
+ if (['unlink', 'unlinkDir'].includes(event)) { return; }
108
+
109
+ const block = this.globs.find(blockPath => path.startsWith(`${blockPath}/src`));
110
+ if (!block) { return; }
111
+ if (debounceTimers.has(block)) {
112
+ clearTimeout(debounceTimers.get(block));
113
+ }
114
+ debounceTimers.set(block, setTimeout(async () => {
115
+ if (buildQueue.has(block)) { return; }
116
+ try {
117
+ buildQueue.add(block);
118
+ this.project.components.server.server.notify('Building...', 10000);
93
119
  if (path.endsWith('.js')) {
94
- this.project.components.scripts.lint(path);
120
+ if (!this.project.components.scripts.isBuilding) {
121
+ this.project.components.scripts.lint(path).catch(lintError => {
122
+ this.log('warn', `Linting failed for ${path}: ${lintError.message}`);
123
+ });
124
+ }
95
125
  }
96
- this.process(block);
126
+ await this.process(block);
127
+ } catch (error) {
128
+ this.log('error', `Failed to process block ${block}: ${error.message}`);
129
+ } finally {
130
+ buildQueue.delete(block);
131
+ debounceTimers.delete(block);
97
132
  }
98
- });
99
- }
133
+ }, DEBOUNCE_DELAY));
134
+ });
100
135
  }
101
136
 
102
137
  }
@@ -0,0 +1,294 @@
1
+ import BaseComponent from './base.js';
2
+ import { promises as fs } from 'fs';
3
+ import { createHash } from 'crypto';
4
+ import path from 'path';
5
+
6
+ export default class CacheComponent extends BaseComponent {
7
+
8
+ constructor() {
9
+ super();
10
+ this.description = 'Build caching';
11
+ this.cacheDir = `${this.project.path}/.sdc-build-wp/cache`;
12
+ this.manifestPath = `${this.cacheDir}/manifest.json`;
13
+ this.manifest = {};
14
+ this.hashCache = new Map();
15
+ this.dependencyGraph = new Map();
16
+ }
17
+
18
+ async init() {
19
+ await fs.mkdir(this.cacheDir, { recursive: true });
20
+ await this.loadManifest();
21
+ await this.cleanStaleEntries();
22
+ await this.ensureGitignore();
23
+ }
24
+
25
+ async loadManifest() {
26
+ const packageVersion = await this.getPackageVersion();
27
+ try {
28
+ const manifestData = await fs.readFile(this.manifestPath, 'utf8');
29
+ this.manifest = JSON.parse(manifestData);
30
+ if (this.manifest.version !== packageVersion) {
31
+ throw new Error(`Manifest version mismatch: expected ${packageVersion}, found ${this.manifest.version}`);
32
+ }
33
+ } catch (error) {
34
+ this.manifest = {
35
+ version: packageVersion,
36
+ timestamp: Date.now(),
37
+ entries: {},
38
+ dependencies: {}
39
+ };
40
+ }
41
+ }
42
+
43
+ async saveManifest() {
44
+ this.manifest.timestamp = Date.now();
45
+ await fs.writeFile(this.manifestPath, JSON.stringify(this.manifest, null, 2));
46
+ }
47
+
48
+ async ensureGitignore() {
49
+ const gitignorePath = path.join(this.project.path, '.gitignore');
50
+
51
+ try {
52
+ let gitignoreContent = '';
53
+ let gitignoreExists = false;
54
+
55
+ try {
56
+ gitignoreContent = await fs.readFile(gitignorePath, 'utf8');
57
+ gitignoreExists = true;
58
+ } catch (error) {
59
+ // .gitignore doesn't exist, we'll create it
60
+ }
61
+
62
+ const lines = gitignoreContent.split('\n');
63
+ let hasSDCBuild = lines.some(line => line.trim() === '.sdc-build-wp/cache');
64
+ let needsUpdate = false;
65
+
66
+ if (!hasSDCBuild) {
67
+ if (gitignoreContent && !gitignoreContent.endsWith('\n')) {
68
+ gitignoreContent += '\n';
69
+ }
70
+ gitignoreContent += '.sdc-build-wp/cache\n';
71
+ needsUpdate = true;
72
+ this.log('info', 'Added .sdc-build-wp/cache to .gitignore');
73
+ }
74
+
75
+ if (needsUpdate || !gitignoreExists) {
76
+ await fs.writeFile(gitignorePath, gitignoreContent);
77
+ if (!gitignoreExists) {
78
+ this.log('info', 'Created .gitignore file');
79
+ }
80
+ }
81
+ } catch (error) {
82
+ this.log('warn', `Failed to update .gitignore: ${error.message}`);
83
+ }
84
+ }
85
+
86
+ async getPackageVersion() {
87
+ try {
88
+ return await this.utils.getThisPackageVersion() || '1.0.0';
89
+ } catch (error) {
90
+ this.log('warn', `Failed to read package.json version: ${error.message}`);
91
+ return '1.0.0';
92
+ }
93
+ }
94
+
95
+ async getFileHash(filePath) {
96
+
97
+ if (this.hashCache.has(filePath)) {
98
+ return this.hashCache.get(filePath);
99
+ }
100
+
101
+ try {
102
+ const content = await fs.readFile(filePath);
103
+ const hash = createHash('sha256').update(content).digest('hex');
104
+ this.hashCache.set(filePath, hash);
105
+ return hash;
106
+ } catch (error) {
107
+ return null;
108
+ }
109
+ }
110
+
111
+ async getFileStatsHash(filePath) {
112
+ try {
113
+ const stats = await fs.stat(filePath);
114
+ const hash = createHash('sha256')
115
+ .update(`${stats.mtime.getTime()}-${stats.size}`)
116
+ .digest('hex');
117
+ return hash;
118
+ } catch (error) {
119
+ return null;
120
+ }
121
+ }
122
+
123
+ async needsRebuild(inputFile, outputFile, dependencies = []) {
124
+ const cacheKey = this.getCacheKey(inputFile, outputFile);
125
+ const cachedEntry = this.manifest.entries[cacheKey];
126
+
127
+ if (!cachedEntry) {
128
+ return true;
129
+ }
130
+
131
+ try {
132
+ await fs.access(outputFile);
133
+ } catch (error) {
134
+ return true;
135
+ }
136
+
137
+ const currentInputHash = await this.getFileHash(inputFile);
138
+ if (currentInputHash !== cachedEntry.inputHash) {
139
+ return true;
140
+ }
141
+
142
+ for (const dep of dependencies) {
143
+ const currentDepHash = await this.getFileHash(dep);
144
+ const cachedDepHash = cachedEntry.dependencies?.[dep];
145
+
146
+ if (currentDepHash !== cachedDepHash) {
147
+ return true;
148
+ }
149
+ }
150
+
151
+ return false;
152
+ }
153
+
154
+ async updateCache(inputFile, outputFile, dependencies = []) {
155
+ const cacheKey = this.getCacheKey(inputFile, outputFile);
156
+ const inputHash = await this.getFileHash(inputFile);
157
+
158
+ const dependencyHashes = {};
159
+ for (const dep of dependencies) {
160
+ dependencyHashes[dep] = await this.getFileHash(dep);
161
+ }
162
+
163
+ this.manifest.entries[cacheKey] = {
164
+ inputFile,
165
+ outputFile,
166
+ inputHash,
167
+ dependencies: dependencyHashes,
168
+ timestamp: Date.now()
169
+ };
170
+
171
+ await this.saveManifest();
172
+ }
173
+
174
+ getCacheKey(inputFile, outputFile) {
175
+ const relativePath = path.relative(this.project.path, inputFile);
176
+ const relativeOutput = path.relative(this.project.path, outputFile);
177
+ return createHash('md5').update(`${relativePath}:${relativeOutput}`).digest('hex');
178
+ }
179
+
180
+ async invalidateFile(filePath) {
181
+ const toRemove = [];
182
+ const filesToClearFromHashCache = new Set([filePath]);
183
+
184
+ for (const [cacheKey, entry] of Object.entries(this.manifest.entries)) {
185
+ if (entry.inputFile === filePath || entry.dependencies?.[filePath]) {
186
+ toRemove.push(cacheKey);
187
+ filesToClearFromHashCache.add(entry.inputFile);
188
+ if (entry.dependencies) {
189
+ Object.keys(entry.dependencies).forEach(dep => filesToClearFromHashCache.add(dep));
190
+ }
191
+ }
192
+ }
193
+
194
+ for (const key of toRemove) {
195
+ delete this.manifest.entries[key];
196
+ }
197
+
198
+ if (toRemove.length > 0) {
199
+ await this.saveManifest();
200
+ }
201
+
202
+ filesToClearFromHashCache.forEach(file => this.hashCache.delete(file));
203
+ }
204
+
205
+ async cleanStaleEntries() {
206
+ const toRemove = [];
207
+ const maxAge = 7 * 24 * 60 * 60 * 1000; // 7 days
208
+ const now = Date.now();
209
+
210
+ for (const [cacheKey, entry] of Object.entries(this.manifest.entries)) {
211
+ if (now - entry.timestamp > maxAge) {
212
+ toRemove.push(cacheKey);
213
+ continue;
214
+ }
215
+ try {
216
+ await fs.access(entry.inputFile);
217
+ } catch (error) {
218
+ toRemove.push(cacheKey);
219
+ }
220
+ }
221
+
222
+ for (const key of toRemove) {
223
+ delete this.manifest.entries[key];
224
+ }
225
+
226
+ if (toRemove.length > 0) {
227
+ this.log('info', `Cleaned ${toRemove.length} stale cache entries`);
228
+ await this.saveManifest();
229
+ }
230
+ }
231
+
232
+ async clearCache() {
233
+ try {
234
+ await fs.rm(this.cacheDir, { recursive: true, force: true });
235
+ await fs.mkdir(this.cacheDir, { recursive: true });
236
+ const packageVersion = await this.getPackageVersion();
237
+ this.manifest = {
238
+ version: packageVersion,
239
+ timestamp: Date.now(),
240
+ entries: {},
241
+ dependencies: {}
242
+ };
243
+ this.hashCache.clear();
244
+ this.log('info', 'Cache cleared');
245
+ } catch (error) {
246
+ this.log('error', `Failed to clear cache: ${error.message}`);
247
+ }
248
+ }
249
+
250
+ getCacheInfo(inputFile, outputFile) {
251
+ const cacheKey = this.getCacheKey(inputFile, outputFile);
252
+ const entry = this.manifest.entries[cacheKey];
253
+ return {
254
+ cacheKey,
255
+ exists: !!entry,
256
+ entry: entry || null,
257
+ inMemoryCache: this.hashCache.has(inputFile)
258
+ };
259
+ }
260
+
261
+ clearHashCache(filePaths) {
262
+ if (Array.isArray(filePaths)) {
263
+ filePaths.forEach(filePath => this.hashCache.delete(filePath));
264
+ } else {
265
+ this.hashCache.delete(filePaths);
266
+ }
267
+ }
268
+
269
+ async build() {
270
+ //
271
+ }
272
+
273
+ async process() {
274
+ //
275
+ }
276
+
277
+ async watch() {
278
+ this.watcher = this.chokidar.watch([
279
+ `${this.project.path}/**/*`,
280
+ `!${this.project.path}/.sdc-build-wp/**/*`,
281
+ `!${this.project.paths.nodeModules}/**/*`,
282
+ `!${this.project.paths.composer.vendor}/**/*`,
283
+ `!${this.project.path}/.git/**/*`
284
+ ], {
285
+ ...this.project.chokidarOpts,
286
+ ignoreInitial: true
287
+ }).on('unlink', async (filePath) => {
288
+ await this.invalidateFile(filePath);
289
+ }).on('change', async (filePath) => {
290
+ this.hashCache.delete(filePath);
291
+ await this.invalidateFile(filePath);
292
+ });
293
+ }
294
+ }
@@ -44,9 +44,13 @@ export default class FontsComponent extends BaseComponent {
44
44
  watch() {
45
45
  this.watcher = this.chokidar.watch(this.globs, {
46
46
  ...this.project.chokidarOpts
47
- }).on('all', (event, path) => {
47
+ }).on('all', async (event, path) => {
48
48
  if (!this.project.isRunning) { return; }
49
- this.process();
49
+ try {
50
+ await this.process();
51
+ } catch (error) {
52
+ this.log('error', `Failed to process fonts: ${error.message}`);
53
+ }
50
54
  });
51
55
  }
52
56
 
@@ -79,9 +79,13 @@ export default class ImagesComponent extends BaseComponent {
79
79
  watch() {
80
80
  this.watcher = this.chokidar.watch(this.project.paths.images, {
81
81
  ...this.project.chokidarOpts
82
- }).on('all', (event, path) => {
82
+ }).on('all', async (event, path) => {
83
83
  if (!this.project.isRunning) { return; }
84
- this.process();
84
+ try {
85
+ await this.process();
86
+ } catch (error) {
87
+ this.log('error', `Failed to process images: ${error.message}`);
88
+ }
85
89
  });
86
90
  }
87
91
 
@@ -6,3 +6,4 @@ export { default as fonts } from './fonts.js';
6
6
  export { default as php } from './php.js';
7
7
  export { default as server } from './server.js';
8
8
  export { default as errors } from './errors.js';
9
+ export { default as cache } from './cache.js';
@@ -89,10 +89,14 @@ export default class PHPComponent extends BaseComponent {
89
89
  watch() {
90
90
  this.watcher = this.chokidar.watch(this.globs, {
91
91
  ...this.project.chokidarOpts
92
- }).on('all', (event, path) => {
92
+ }).on('all', async (event, path) => {
93
93
  if (!this.project.isRunning) { return; }
94
94
  if (!['unlink', 'unlinkDir'].includes(event)) {
95
- this.process(path);
95
+ try {
96
+ await this.process(path);
97
+ } catch (error) {
98
+ this.log('error', `Failed to process PHP file ${path}: ${error.message}`);
99
+ }
96
100
  }
97
101
  });
98
102
  }
@@ -8,6 +8,7 @@ export default class ScriptsComponent extends BaseComponent {
8
8
  constructor() {
9
9
  super();
10
10
  this.description = `Lint and process script files`;
11
+ this.isBuilding = false;
11
12
  }
12
13
 
13
14
  async init() {
@@ -24,6 +25,7 @@ export default class ScriptsComponent extends BaseComponent {
24
25
  entriesToLint: null
25
26
  }, options);
26
27
  let entryLabel = `/${this.project.paths.dist}/${this.project.paths.src.scripts}/${this.utils.entryBasename(entry).replace(/\.js$|\.jsx$|\.ts$|\.tsx$/g, '.min.js')}`;
28
+ let outFile = `${this.project.path}${entryLabel}`;
27
29
 
28
30
  this.start();
29
31
 
@@ -38,6 +40,18 @@ export default class ScriptsComponent extends BaseComponent {
38
40
  return false;
39
41
  }
40
42
 
43
+ const dependencies = this.utils.getAllJSDependencies(entry);
44
+
45
+ this.clearHashCache([entry, ...(options.entriesToLint || []), ...dependencies]);
46
+
47
+ if (await this.shouldSkipBuild(entry, outFile, dependencies)) {
48
+ this.end({
49
+ itemLabel: entryLabel,
50
+ cached: true
51
+ });
52
+ return true;
53
+ }
54
+
41
55
  try {
42
56
  const result = await esbuild.build({
43
57
  platform: 'node',
@@ -52,6 +66,8 @@ export default class ScriptsComponent extends BaseComponent {
52
66
  if (result.warnings.length > 0) {
53
67
  this.log('warn', result.warnings);
54
68
  }
69
+
70
+ await this.updateBuildCache(entry, outFile, dependencies);
55
71
  } catch (error) {
56
72
  console.error(error);
57
73
  this.log('error', `Failed building ${entryLabel} - See above error.`);
@@ -64,16 +80,25 @@ export default class ScriptsComponent extends BaseComponent {
64
80
  }
65
81
 
66
82
  async process() {
67
- const promisesScripts = this.files.map((group, index) => this.build(group.file, { entriesToLint: index == 0 ? this.globs : null }));
68
- await Promise.all(promisesScripts);
83
+ this.isBuilding = true;
84
+ try {
85
+ const promisesScripts = this.files.map((group, index) => this.build(group.file, { entriesToLint: index == 0 ? this.globs : null }));
86
+ await Promise.all(promisesScripts);
87
+ } finally {
88
+ this.isBuilding = false;
89
+ }
69
90
  }
70
91
 
71
92
  watch() {
72
93
  this.watcher = this.chokidar.watch(this.globs, {
73
94
  ...this.project.chokidarOpts
74
- }).on('all', (event, path) => {
95
+ }).on('all', async (event, path) => {
75
96
  if (!this.project.isRunning) { return; }
76
- this.process();
97
+ try {
98
+ await this.process();
99
+ } catch (error) {
100
+ this.log('error', `Failed to process scripts: ${error.message}`);
101
+ }
77
102
  });
78
103
  }
79
104
 
@@ -1,4 +1,7 @@
1
1
  import BaseComponent from './base.js';
2
+ import path from 'path';
3
+ import { fileURLToPath } from 'url';
4
+ import { readFile } from 'fs/promises';
2
5
  import { create } from 'browser-sync';
3
6
 
4
7
  export default class ServerComponent extends BaseComponent {
@@ -6,6 +9,7 @@ export default class ServerComponent extends BaseComponent {
6
9
  constructor() {
7
10
  super();
8
11
  this.description = `Run a dev proxy server for live reloading`;
12
+ this.sessions = {};
9
13
  this.server = create('SDC WP Build Server');
10
14
  this.watchedFiles = [
11
15
  `${this.project.paths.dist}/**/*`,
@@ -23,10 +27,12 @@ export default class ServerComponent extends BaseComponent {
23
27
  }
24
28
 
25
29
  async init() {
26
- //
30
+ this.project.pageScript = await readFile(path.join(path.dirname(fileURLToPath(import.meta.url)), '../page-script.js'), 'utf8');
27
31
  }
28
32
 
29
33
  serve(watch = false) {
34
+
35
+ let thisProject = this.project;
30
36
  let bsOptions = {
31
37
  logPrefix: '',
32
38
  logFileChanges: false,
@@ -58,36 +64,72 @@ export default class ServerComponent extends BaseComponent {
58
64
  bottom: '0',
59
65
  borderRadius: '5px 0px 0px'
60
66
  }
61
- }
62
- };
63
- if (this.project.package.sdc?.browsersync?.location == 'end') {
64
- bsOptions.snippetOptions = {
67
+ },
68
+ snippetOptions: {
65
69
  rule: {
66
- match: /<\/body>/i,
70
+ match: thisProject.package.sdc?.browsersync?.location == 'end' ? /<\/body>/ : /<body[^>]*>/,
67
71
  fn: function (snippet, match) {
68
- return snippet + match;
72
+ const customScript = `<script async>${thisProject.pageScript}</script>`;
73
+ const allScripts = snippet + customScript;
74
+ return thisProject.package.sdc?.browsersync?.location == 'end' ? allScripts + match : match + allScripts;
69
75
  }
70
76
  }
71
- };
72
- }
73
- this.server.init(bsOptions);
77
+ }
78
+ };
79
+
80
+ this.server.init(bsOptions, (err, bs) => {
81
+ if (err) {
82
+ this.log('error', `Failed to start BrowserSync server: ${err.message}`);
83
+ this.log('warn', 'Continuing without live reload server');
84
+ return;
85
+ }
86
+ try {
87
+ this.setupSocketHandlers(this);
88
+ } catch (setupError) {
89
+ this.log('error', `Failed to setup socket handlers: ${setupError.message}`);
90
+ }
91
+ });
74
92
  }
75
93
 
76
- async watch() {
77
- this.server.watch(this.watchedFiles, {
78
- ignored: this.ignoredFiles,
79
- ignoreInitial: true
80
- }, (event, file) => {
81
- if (!this.project.isRunning) { return; }
82
- if (['add', 'addDir', 'change'].includes(event)) {
83
- this.server.reload(file);
84
- if (file.split('.').pop() == 'css') {
85
- this.server.notify('Style injected', 500);
94
+ setupSocketHandlers(serverComponent) {
95
+ this.server.sockets.on('connection', (socket) => {
96
+ socket.on('sdc:scriptsOnPage', (data) => {
97
+ if (!serverComponent.sessions[data.sessionID]) {
98
+ serverComponent.sessions[data.sessionID] = {
99
+ scripts: []
100
+ };
86
101
  }
87
- } else if (['unlink', 'unlinkDir'].includes(event)) {
88
- this.server.reload();
89
- }
102
+ serverComponent.sessions[data.sessionID].scripts = data.data;
103
+ });
90
104
  });
91
105
  }
92
106
 
107
+ async watch() {
108
+ try {
109
+ this.server.watch(this.watchedFiles, {
110
+ ignored: this.ignoredFiles,
111
+ ignoreInitial: true
112
+ }, (event, file) => {
113
+ if (!this.project.isRunning) { return; }
114
+ try {
115
+ if (['add', 'addDir', 'change'].includes(event)) {
116
+ this.server.reload(file);
117
+ if (file.split('.').pop() == 'css') {
118
+ this.server.notify('Style updated', 500);
119
+ return;
120
+ }
121
+ } else if (['unlink', 'unlinkDir'].includes(event)) {
122
+ this.server.reload();
123
+ }
124
+ this.server.notify('Reloading...', 10000);
125
+ } catch (reloadError) {
126
+ this.log('warn', `Failed to reload ${file}: ${reloadError.message}`);
127
+ }
128
+ });
129
+ } catch (watchError) {
130
+ this.log('error', `Failed to start file watcher: ${watchError.message}`);
131
+ throw watchError;
132
+ }
133
+ }
134
+
93
135
  }
@@ -105,6 +105,7 @@ export default class StyleComponent extends BaseComponent {
105
105
  outFile = this.project.path + '/' + options.name + '.min.css';
106
106
  }
107
107
  let entryLabel = outFile.replace(this.project.path, '');
108
+
108
109
  this.start();
109
110
 
110
111
  try {
@@ -112,6 +113,7 @@ export default class StyleComponent extends BaseComponent {
112
113
  } catch(error) {
113
114
  await fs.mkdir(`${this.project.path}/${this.project.paths.dist}/${this.project.paths.src.style}`, { recursive: true });
114
115
  }
116
+
115
117
  try {
116
118
  const stylelinted = await stylelint.lint({
117
119
  configFile: this.path.resolve(this.path.dirname(fileURLToPath(import.meta.url)), '../../.stylelintrc.json'),
@@ -124,6 +126,18 @@ export default class StyleComponent extends BaseComponent {
124
126
  console.error(stylelinted.report);
125
127
  throw Error('Linting error');
126
128
  }
129
+
130
+ this.clearHashCache([entry, ...(options.entriesToLint || [])]);
131
+
132
+ const sassDependencies = this.utils.getImportedSASSFiles(entry);
133
+ if (await this.shouldSkipBuild(entry, outFile, sassDependencies)) {
134
+ this.end({
135
+ itemLabel: entryLabel,
136
+ cached: true
137
+ });
138
+ return true;
139
+ }
140
+
127
141
  const compileResult = await sass.compileAsync(entry, {
128
142
  style: 'compressed',
129
143
  sourceMap: true
@@ -142,6 +156,9 @@ export default class StyleComponent extends BaseComponent {
142
156
  if (process.env.NODE_ENV != 'production') {
143
157
  await fs.writeFile(`${outFile}.map`, postcssResult.map.toString());
144
158
  }
159
+
160
+ await this.updateBuildCache(entry, outFile, sassDependencies);
161
+
145
162
  thisClass.end({
146
163
  itemLabel: entryLabel
147
164
  });
@@ -180,17 +197,21 @@ export default class StyleComponent extends BaseComponent {
180
197
  this.globs
181
198
  ], {
182
199
  ...this.project.chokidarOpts
183
- }).on('all', (event, path) => {
200
+ }).on('all', async (event, path) => {
184
201
  if (!this.project.isRunning) { return; }
185
- let hasRanSingle = false;
186
- for (let group of this.files) {
187
- if (path == group.file || this.utils.getImportedSASSFiles(group.file).includes(path)) {
188
- this.process(group.file, { buildTheme: path == this.project.paths.theme.json });
189
- hasRanSingle = true;
202
+ try {
203
+ let hasRanSingle = false;
204
+ for (let group of this.files) {
205
+ if (path == group.file || this.utils.getImportedSASSFiles(group.file).includes(path)) {
206
+ await this.process(group.file, { buildTheme: path == this.project.paths.theme.json });
207
+ hasRanSingle = true;
208
+ }
190
209
  }
191
- }
192
- if (!hasRanSingle) {
193
- this.process(null, { buildTheme: path == this.project.paths.theme.json });
210
+ if (!hasRanSingle) {
211
+ await this.process(null, { buildTheme: path == this.project.paths.theme.json });
212
+ }
213
+ } catch (error) {
214
+ this.log('error', `Failed to process styles for ${path}: ${error.message}`);
194
215
  }
195
216
  });
196
217
  }
package/lib/help.js ADDED
@@ -0,0 +1,34 @@
1
+ import project from './project.js';
2
+
3
+ export default function() {
4
+ console.log(`
5
+ Usage: sdc-build-wp [options] [arguments]
6
+
7
+ Options:
8
+ -h, --help Show help message and exit
9
+ -v, --version Version
10
+ -w, --watch Build and watch
11
+ -b, --builds BUILDS Build with specific components
12
+ --no-cache Disable build caching
13
+ --clear-cache Clear build cache and exit
14
+
15
+ Components:
16
+
17
+ ${Object.entries(project.components).map(([key, component]) => {
18
+ return `${key}\t\t${component.description}\r\n`;
19
+ }).join('')}
20
+ Examples:
21
+
22
+ sdc-build-wp
23
+ sdc-build-wp --watch
24
+ sdc-build-wp --watch --builds=style,scripts
25
+ sdc-build-wp --no-cache
26
+ sdc-build-wp --clear-cache
27
+
28
+ While watch is enabled, use the following keyboard commands to control the build process:
29
+
30
+ [r] Restart
31
+ [p] Pause/Resume
32
+ [q] Quit
33
+ `);
34
+ }
package/lib/logging.js CHANGED
@@ -1,6 +1,7 @@
1
1
  // based heavily on Nick Salloum's 'node-pretty-log'
2
2
  // https://github.com/callmenick/node-pretty-log
3
3
  import chalk from 'chalk';
4
+ import { default as project } from './project.js';
4
5
 
5
6
  function getTime() {
6
7
  return new Date().toLocaleTimeString('en-US');
@@ -23,6 +24,9 @@ function log(type, ...messages) {
23
24
  chalk.bgRed.gray(getTime()),
24
25
  ...messages
25
26
  );
27
+ if (project.builds.includes('server') && project.isRunning) {
28
+ project.components.server.server.notify('ERROR', 2500);
29
+ }
26
30
  break;
27
31
  case 'warn':
28
32
  console.log.call(
@@ -0,0 +1,19 @@
1
+ let randomSessionID = Array.from({ length: 50 }, () => Math.random().toString(36)[2]).join('');
2
+
3
+ let socket = window.___browserSync___?.socket;
4
+
5
+ function getScriptsOnPage() {
6
+ socket.emit('sdc:scriptsOnPage', {
7
+ timestamp: Date.now(),
8
+ sessionID: randomSessionID,
9
+ data: Array.from(document.querySelectorAll('script') || []).map((script) => script.src || false).filter(script => script && script.length > 0)
10
+ });
11
+ }
12
+
13
+ const scriptsOnPageInterval = setInterval(() => {
14
+ socket = window.___browserSync___?.socket;
15
+ if (socket) {
16
+ getScriptsOnPage();
17
+ clearInterval(scriptsOnPageInterval);
18
+ }
19
+ }, 500);
package/lib/project.js CHANGED
@@ -1,6 +1,13 @@
1
+ import parseArgs from 'minimist';
1
2
  import { readFile } from 'fs/promises';
3
+ import build from './build.js';
4
+ import * as utils from './utils.js';
5
+ import log from './logging.js';
6
+ import * as LibComponents from './components/index.js';
7
+ import help from './help.js';
2
8
 
3
9
  let project = {
10
+ argv: null,
4
11
  isRunning: false,
5
12
  path: process.cwd(),
6
13
  package: JSON.parse(await readFile(new URL(process.cwd() + '/package.json', import.meta.url))),
@@ -51,7 +58,107 @@ project.chokidarOpts = {
51
58
  `${project.paths.composer.vendor}/**/*`,
52
59
  project.paths.theme.scss,
53
60
  `${project.path}/blocks/*/build/*.php`,
61
+ `${project.path}/.sdc-build-wp/**/*`,
54
62
  ]
55
63
  };
56
64
 
57
65
  export default project;
66
+
67
+ export async function init() {
68
+ project.argv = parseArgs(process.argv.slice(2));
69
+ project.components = Object.fromEntries(Object.entries(LibComponents).map(([name, Class]) => [name, new Class()]));
70
+
71
+ process.on('unhandledRejection', (reason, promise) => {
72
+ log('error', `Unhandled Promise Rejection: ${reason}`);
73
+ log('warn', 'Continuing build process despite error');
74
+ });
75
+
76
+ process.on('uncaughtException', (error) => {
77
+ log('error', `Uncaught Exception: ${error.message}`);
78
+ log('warn', 'Attempting graceful shutdown');
79
+ utils.stopActiveComponents();
80
+ process.exit(1);
81
+ });
82
+
83
+ if (project.argv.help || project.argv.h) {
84
+ help();
85
+ process.exit(0);
86
+ } else if (project.argv.version || project.argv.v) {
87
+ console.log(await utils.getThisPackageVersion());
88
+ process.exit(0);
89
+ } else if (project.argv['clear-cache']) {
90
+ if (project.components.cache) {
91
+ try {
92
+ await project.components.cache.init();
93
+ await project.components.cache.clearCache();
94
+ } catch (error) {
95
+ console.log(`Error clearing cache: ${error.message}`);
96
+ }
97
+ }
98
+ process.exit(0);
99
+ }
100
+
101
+ if (project.argv['no-cache']) {
102
+ Object.values(project.components).forEach(component => {
103
+ if (component.useCache !== undefined) {
104
+ component.useCache = false;
105
+ }
106
+ });
107
+ }
108
+
109
+ project.builds = project.argv.builds ? (Array.isArray(project.argv.builds) ? project.argv.builds : project.argv.builds.split(',')) : Object.keys(project.components).filter(component => component !== 'cache');
110
+
111
+ if (!project.argv['no-cache'] && !project.builds.includes('cache')) {
112
+ project.builds.unshift('cache');
113
+ }
114
+
115
+ process.on('SIGINT', function () {
116
+ console.log(`\r`);
117
+ if (project.isRunning) {
118
+ utils.stopActiveComponents();
119
+ project.isRunning = false;
120
+ utils.clearScreen();
121
+ }
122
+ log('info', `Exited sdc-build-wp`);
123
+ if (process.stdin.isTTY) {
124
+ process.stdin.setRawMode(false);
125
+ process.stdin.pause();
126
+ }
127
+ process.exit(0);
128
+ });
129
+
130
+ }
131
+
132
+ export function keypressListen() {
133
+ if (!process.stdin.isTTY) { return; }
134
+
135
+ process.stdin.setRawMode(true);
136
+ process.stdin.resume();
137
+ process.stdin.setEncoding('utf8');
138
+
139
+ process.stdin.on('data', (key) => {
140
+ switch (key) {
141
+ case '\u0003': // Ctrl+C
142
+ case 'q':
143
+ process.emit('SIGINT');
144
+ return;
145
+ case 'p':
146
+ project.isRunning = !project.isRunning;
147
+ utils.clearScreen();
148
+ if (project.isRunning) {
149
+ log('success', 'Resumed build process');
150
+ } else {
151
+ log('warn', 'Paused build process');
152
+ }
153
+ break;
154
+ case 'r':
155
+ log('info', 'Restarted build process');
156
+ utils.stopActiveComponents();
157
+ setTimeout(() => {
158
+ utils.clearScreen();
159
+ build(true);
160
+ }, 100);
161
+ break;
162
+ }
163
+ });
164
+ }
package/lib/utils.js CHANGED
@@ -1,8 +1,14 @@
1
+ import path from 'path';
1
2
  import fs from 'fs';
3
+ import * as fsPromises from 'fs/promises';
2
4
  import { readdir } from 'node:fs/promises';
3
- import path from 'path';
5
+ import { fileURLToPath } from 'url';
4
6
  import project from './project.js';
5
7
 
8
+ export async function getThisPackageVersion() {
9
+ return JSON.parse(await fsPromises.readFile(path.join(path.dirname(fileURLToPath(import.meta.url)), '../package.json'))).version
10
+ }
11
+
6
12
  export function clearScreen() {
7
13
  if (!process.stdout.isTTY) { return; }
8
14
  process.stdout.write('\x1B[2J\x1B[0f');
@@ -10,7 +16,23 @@ export function clearScreen() {
10
16
 
11
17
  export function stopActiveComponents() {
12
18
  if (project.components.server?.server) {
13
- project.components.server.server.exit();
19
+ try {
20
+ project.components.server.server.exit();
21
+ } catch (error) {
22
+ console.warn('Failed to stop server:', error.message);
23
+ }
24
+ }
25
+ Object.values(project.components).forEach(component => {
26
+ if (component.watcher) {
27
+ try {
28
+ component.watcher.close();
29
+ } catch (error) {
30
+ console.warn(`Failed to stop watcher for ${component.constructor.name}:`, error.message);
31
+ }
32
+ }
33
+ });
34
+ if (project.components.scripts) {
35
+ project.components.scripts.isBuilding = false;
14
36
  }
15
37
  }
16
38
 
@@ -53,6 +75,68 @@ export function getImportedSASSFiles(filePath) {
53
75
  return imports;
54
76
  }
55
77
 
78
+ export function getImportedJSFiles(filePath) {
79
+ try {
80
+ const content = fs.readFileSync(filePath, 'utf8');
81
+ const regex = /(?:import\s+[^'"]*\s+from\s+['"`]([^'">`]+)['"`]|import\s*\(\s*['"`]([^'">`]+)['"`]\s*\)|require\s*\(\s*['"`]([^'">`]+)['"`]\s*\))/g;
82
+ const imports = [];
83
+ let match;
84
+
85
+ while ((match = regex.exec(content)) !== null) {
86
+ const importPath = match[1] || match[2] || match[3];
87
+ if (importPath.startsWith('.') || importPath.startsWith('/')) {
88
+ let resolvedPath;
89
+ if (importPath.startsWith('.')) {
90
+ resolvedPath = path.resolve(path.dirname(filePath), importPath);
91
+ } else {
92
+ resolvedPath = path.resolve(project.path, importPath.substring(1));
93
+ }
94
+ const extensions = ['.js', '.jsx', '.ts', '.tsx', '.mjs'];
95
+ if (!path.extname(resolvedPath)) {
96
+ for (const ext of extensions) {
97
+ const pathWithExt = resolvedPath + ext;
98
+ if (fs.existsSync(pathWithExt)) {
99
+ imports.push(pathWithExt);
100
+ break;
101
+ }
102
+ }
103
+ const indexPath = path.join(resolvedPath, 'index');
104
+ for (const ext of extensions) {
105
+ const pathWithExt = indexPath + ext;
106
+ if (fs.existsSync(pathWithExt)) {
107
+ imports.push(pathWithExt);
108
+ break;
109
+ }
110
+ }
111
+ } else {
112
+ if (fs.existsSync(resolvedPath)) {
113
+ imports.push(resolvedPath);
114
+ }
115
+ }
116
+ }
117
+ }
118
+ return [...new Set(imports)].filter(importPath =>
119
+ importPath.startsWith(project.path) && fs.existsSync(importPath)
120
+ );
121
+ } catch (error) {
122
+ return [];
123
+ }
124
+ }
125
+
126
+ export function getAllJSDependencies(filePath, visited = new Set()) {
127
+ if (visited.has(filePath)) {
128
+ return [];
129
+ }
130
+ visited.add(filePath);
131
+ const directDeps = getImportedJSFiles(filePath);
132
+ const allDeps = [...directDeps];
133
+ for (const dep of directDeps) {
134
+ const nestedDeps = getAllJSDependencies(dep, visited);
135
+ allDeps.push(...nestedDeps);
136
+ }
137
+ return [...new Set(allDeps)];
138
+ }
139
+
56
140
  export function addEntriesByFiletypes(filetypes = []) {
57
141
  let finalFiles = [];
58
142
  for (const [name, files] of Object.entries(project.package.sdc.entries)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sdc-build-wp",
3
- "version": "4.7.1",
3
+ "version": "4.9.0",
4
4
  "description": "Custom WordPress build process.",
5
5
  "engines": {
6
6
  "node": ">=22"