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 +15 -4
- package/index.js +7 -126
- package/lib/build.js +64 -0
- package/lib/components/base.js +35 -2
- package/lib/components/blocks.js +48 -13
- package/lib/components/cache.js +294 -0
- package/lib/components/fonts.js +6 -2
- package/lib/components/images.js +6 -2
- package/lib/components/index.js +1 -0
- package/lib/components/php.js +6 -2
- package/lib/components/scripts.js +29 -4
- package/lib/components/server.js +65 -23
- package/lib/components/style.js +30 -9
- package/lib/help.js +34 -0
- package/lib/logging.js +4 -0
- package/lib/page-script.js +19 -0
- package/lib/project.js +107 -0
- package/lib/utils.js +86 -2
- package/package.json +1 -1
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
|
-
##
|
|
11
|
+
## Caching
|
|
12
12
|
|
|
13
|
-
|
|
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
|
-
|
|
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
|
|
3
|
-
import
|
|
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
|
-
|
|
53
|
-
|
|
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
|
-
|
|
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
|
+
}
|
package/lib/components/base.js
CHANGED
|
@@ -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
|
-
|
|
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() {
|
package/lib/components/blocks.js
CHANGED
|
@@ -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
|
-
|
|
63
|
-
const
|
|
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.
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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.
|
|
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
|
+
}
|
package/lib/components/fonts.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
package/lib/components/images.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
package/lib/components/index.js
CHANGED
package/lib/components/php.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
68
|
-
|
|
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
|
-
|
|
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
|
|
package/lib/components/server.js
CHANGED
|
@@ -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>/
|
|
70
|
+
match: thisProject.package.sdc?.browsersync?.location == 'end' ? /<\/body>/ : /<body[^>]*>/,
|
|
67
71
|
fn: function (snippet, match) {
|
|
68
|
-
|
|
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
|
-
|
|
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
|
-
|
|
77
|
-
this.server.
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
88
|
-
|
|
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
|
}
|
package/lib/components/style.js
CHANGED
|
@@ -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
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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
|
-
|
|
193
|
-
|
|
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
|
|
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
|
-
|
|
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)) {
|