sdc-build-wp 4.9.0 → 4.9.2

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/.phpcs.xml CHANGED
@@ -1,35 +1,60 @@
1
1
  <?xml version="1.0"?>
2
2
  <ruleset name="SDCBuildWPStandard">
3
3
  <description>sdc-build-wp coding standard</description>
4
-
4
+ <arg name="error-severity" value="1"/>
5
+ <arg name="warning-severity" value="1"/>
6
+ <arg name="extensions" value="php"/>
7
+ <arg name="colors"/>
8
+ <arg name="parallel" value="8"/>
5
9
  <rule ref="Generic.Arrays.ArrayIndent">
6
10
  <properties>
7
11
  <property name="indent" value="2" />
8
12
  </properties>
9
13
  </rule>
14
+ <rule ref="Generic.Arrays.DisallowLongArraySyntax"/>
15
+ <rule ref="Generic.WhiteSpace.LanguageConstructSpacing"/>
16
+ <rule ref="Generic.WhiteSpace.ScopeIndent">
17
+ <properties>
18
+ <property name="exact" value="true" />
19
+ <property name="indent" value="2"/>
20
+ </properties>
21
+ </rule>
22
+ <rule ref="Generic.WhiteSpace.ArbitraryParenthesesSpacing"/>
23
+ <rule ref="Generic.WhiteSpace.DisallowTabIndent"/>
10
24
  <rule ref="Generic.CodeAnalysis.EmptyPHPStatement"/>
25
+ <rule ref="Generic.CodeAnalysis.EmptyStatement"/>
11
26
  <rule ref="Generic.CodeAnalysis.UnusedFunctionParameter"/>
27
+ <rule ref="Generic.CodeAnalysis.UselessOverridingMethod"/>
12
28
  <rule ref="Generic.Formatting.SpaceAfterCast"/>
13
29
  <rule ref="Generic.Formatting.SpaceAfterNot">
14
- <properties>
15
- <property name="spacing" value="0" />
16
- </properties>
30
+ <properties>
31
+ <property name="spacing" value="0" />
32
+ </properties>
17
33
  </rule>
34
+ <rule ref="Generic.Formatting.DisallowMultipleStatements"/>
18
35
  <rule ref="Generic.Functions.OpeningFunctionBraceKernighanRitchie">
19
- <properties>
36
+ <properties>
20
37
  <property name="checkFunctions" value="false" />
21
38
  <property name="checkClosures" value="true" />
22
- </properties>
39
+ </properties>
23
40
  </rule>
41
+ <rule ref="Generic.Functions.FunctionCallArgumentSpacing"/>
24
42
  <rule ref="Generic.NamingConventions.UpperCaseConstantName"/>
25
- <rule ref="Generic.WhiteSpace.ArbitraryParenthesesSpacing"/>
43
+ <rule ref="Generic.NamingConventions.CamelCapsFunctionName"/>
26
44
  <rule ref="Generic.PHP.DisallowAlternativePHPTags"/>
27
45
  <rule ref="Generic.PHP.DisallowShortOpenTag"/>
28
- <rule ref="Generic.WhiteSpace.LanguageConstructSpacing"/>
29
- <rule ref="Generic.WhiteSpace.ScopeIndent">
46
+ <rule ref="Generic.PHP.LowerCaseConstant"/>
47
+ <rule ref="Generic.PHP.LowerCaseKeyword"/>
48
+ <rule ref="Generic.PHP.LowerCaseType"/>
49
+ <rule ref="Generic.PHP.DeprecatedFunctions"/>
50
+ <rule ref="Generic.PHP.ForbiddenFunctions">
30
51
  <properties>
31
- <property name="exact" value="true" />
32
- <property name="indent" value="2"/>
52
+ <property name="forbiddenFunctions" type="array">
53
+ <element key="eval" value="null"/>
54
+ <element key="create_function" value="null"/>
55
+ <element key="var_dump" value="null"/>
56
+ <element key="print_r" value="null"/>
57
+ </property>
33
58
  </properties>
34
59
  </rule>
35
60
  <rule ref="PEAR.Files.IncludingFile.BracketsNotRequired"/>
@@ -38,18 +63,20 @@
38
63
  <rule ref="PEAR.NamingConventions.ValidClassName"/>
39
64
  <rule ref="Squiz.ControlStructures.ForEachLoopDeclaration"/>
40
65
  <rule ref="Squiz.ControlStructures.ForLoopDeclaration"/>
66
+ <rule ref="Squiz.ControlStructures.ControlSignature"/>
41
67
  <rule ref="Squiz.Strings.ConcatenationSpacing">
42
- <properties>
43
- <property name="spacing" value="1" />
44
- </properties>
68
+ <properties>
69
+ <property name="spacing" value="1" />
70
+ </properties>
45
71
  </rule>
46
72
  <rule ref="Squiz.Strings.DoubleQuoteUsage.NotRequired"/>
47
73
  <rule ref="Squiz.WhiteSpace.FunctionSpacing">
48
- <properties>
49
- <property name="spacing" value="1" />
50
- </properties>
74
+ <properties>
75
+ <property name="spacing" value="1" />
76
+ </properties>
51
77
  </rule>
52
78
  <rule ref="Squiz.WhiteSpace.SemicolonSpacing"/>
53
79
  <rule ref="Squiz.WhiteSpace.SuperfluousWhitespace"/>
54
-
80
+ <rule ref="Squiz.WhiteSpace.LogicalOperatorSpacing"/>
81
+ <rule ref="Squiz.WhiteSpace.LanguageConstructSpacing"/>
55
82
  </ruleset>
package/README.md CHANGED
@@ -10,7 +10,7 @@ sdc-build-wp --help
10
10
 
11
11
  ## Caching
12
12
 
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.
13
+ Caching speeds up subsequent builds by only rebuilding files that have changed or whose dependencies have changed.
14
14
 
15
15
  ```sh
16
16
  sdc-build-wp --no-cache # Disable caching for this build
@@ -36,11 +36,12 @@ class BaseComponent {
36
36
  itemLabel: null,
37
37
  timerStart: this.timer,
38
38
  timerEnd: performance.now(),
39
+ skipTimer: false,
39
40
  cached: false
40
41
  }, options);
41
42
 
42
43
  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
+ this.log('success', `${options.verb}${options.itemLabel ? ` ${options.itemLabel}` : ''}${cacheIndicator}${options.skipTimer ? '' : ` in ${Math.round(options.timerEnd - options.timerStart)}ms`}`);
44
45
  }
45
46
  async shouldSkipBuild(inputFile, outputFile, dependencies = []) {
46
47
  if (!this.useCache || !this.project.components.cache || !this.project.components.cache.manifest?.entries) {
@@ -1,8 +1,9 @@
1
1
  import { fileURLToPath } from 'url';
2
2
  import BaseComponent from './base.js';
3
- import { stat } from 'fs/promises';
3
+ import { stat, readFile } from 'fs/promises';
4
4
  import { exec } from 'child_process';
5
5
  import { promisify } from 'util';
6
+ import { createHash } from 'crypto';
6
7
 
7
8
  export default class BlocksComponent extends BaseComponent {
8
9
 
@@ -23,6 +24,66 @@ export default class BlocksComponent extends BaseComponent {
23
24
  // }
24
25
  await this.process();
25
26
  }
27
+ async getBlockDependencies(blockPath) {
28
+ const dependencies = [];
29
+ const srcPath = `${blockPath}/src`;
30
+
31
+ try {
32
+ const srcFiles = await Array.fromAsync(
33
+ this.glob(`${srcPath}/**/*`)
34
+ );
35
+
36
+ dependencies.push(...srcFiles);
37
+
38
+ for (const file of srcFiles) {
39
+ if (/\.(js|jsx|ts|tsx)$/.test(file)) {
40
+ const jsDependencies = this.utils.getAllJSDependencies(file);
41
+ dependencies.push(...jsDependencies);
42
+ } else if (/\.(scss|sass)$/.test(file)) {
43
+ const scssDependencies = this.utils.getImportedSASSFiles(file);
44
+ dependencies.push(...scssDependencies);
45
+ }
46
+ }
47
+
48
+ const uniqueDependencies = [...new Set(dependencies)];
49
+ const existingDependencies = [];
50
+
51
+ for (const dep of uniqueDependencies) {
52
+ try {
53
+ await stat(dep);
54
+ existingDependencies.push(dep);
55
+ } catch (error) {
56
+ // File doesn't exist, skip it
57
+ }
58
+ }
59
+
60
+ return existingDependencies;
61
+ } catch (error) {
62
+ this.log('warn', `Failed to get dependencies for block ${blockPath}: ${error.message}`);
63
+ return [];
64
+ }
65
+ }
66
+
67
+ async getCurrentFileHash(filePath) {
68
+ try {
69
+ const content = await readFile(filePath);
70
+ return createHash('sha256').update(content).digest('hex');
71
+ } catch (error) {
72
+ return null;
73
+ }
74
+ }
75
+
76
+ async buildOutputExists(buildPath) {
77
+ try {
78
+ await stat(buildPath);
79
+ const buildFiles = await Array.fromAsync(
80
+ this.glob(`${buildPath}/**/*`)
81
+ );
82
+ return buildFiles.length > 0;
83
+ } catch (error) {
84
+ return false;
85
+ }
86
+ }
26
87
 
27
88
  async build(entry, options) {
28
89
  options = Object.assign({}, {}, options);
@@ -50,6 +111,26 @@ export default class BlocksComponent extends BaseComponent {
50
111
  this.log('error', `Failed building ${entry} blocks - no block.json found.`);
51
112
  return false;
52
113
  }
114
+
115
+ const dependencies = await this.getBlockDependencies(entry);
116
+ const buildOutputDir = `${entry}/build`;
117
+ const cacheOutputFile = `${buildOutputDir}/index.js`;
118
+
119
+ const shouldSkip = await this.shouldSkipBuild(workingBlockJson, cacheOutputFile, dependencies);
120
+ const buildExists = await this.buildOutputExists(buildOutputDir);
121
+
122
+ if (shouldSkip && buildExists) {
123
+ this.end({
124
+ itemLabel: entryLabel,
125
+ cached: true,
126
+ timerStart: timerStart,
127
+ timerEnd: performance.now()
128
+ });
129
+ return true;
130
+ }
131
+
132
+ this.clearHashCache([workingBlockJson, ...dependencies]);
133
+
53
134
  try {
54
135
  const cmds = [
55
136
  `${this.project.path}/node_modules/@wordpress/scripts/bin/wp-scripts.js`,
@@ -72,6 +153,8 @@ export default class BlocksComponent extends BaseComponent {
72
153
  if (stderr && stderr.trim()) {
73
154
  this.log('warn', `Build warnings for ${entryLabel}: ${stderr.trim()}`);
74
155
  }
156
+
157
+ await this.updateBuildCache(workingBlockJson, cacheOutputFile, dependencies);
75
158
  } catch (error) {
76
159
  console.error(error.stdout || error.stderr || error.message);
77
160
  this.log('error', `Failed building ${entryLabel} block - See above error.`);
@@ -100,37 +183,113 @@ export default class BlocksComponent extends BaseComponent {
100
183
  const debounceTimers = new Map();
101
184
  const DEBOUNCE_DELAY = 500;
102
185
 
103
- this.watcher = this.chokidar.watch(watchPaths, {
186
+ const dependencyMap = new Map();
187
+ const updateDependencyMap = async () => {
188
+ dependencyMap.clear();
189
+ for (const block of this.globs) {
190
+ try {
191
+ const dependencies = await this.getBlockDependencies(block);
192
+ dependencyMap.set(block, dependencies);
193
+ } catch (error) {
194
+ this.log('warn', `Failed to get dependencies for block ${block}: ${error.message}`);
195
+ dependencyMap.set(block, []);
196
+ }
197
+ }
198
+ };
199
+
200
+ updateDependencyMap();
201
+ const dependencyWatchPaths = [
202
+ `${this.project.path}/${this.project.paths.src.src}/**/*`,
203
+ `${this.project.path}/blocks/**/src/**/*`,
204
+ ...watchPaths
205
+ ];
206
+
207
+ this.watcher = this.chokidar.watch(dependencyWatchPaths, {
104
208
  ...this.project.chokidarOpts
105
209
  }).on('all', async (event, path) => {
106
210
  if (!this.project.isRunning) { return; }
107
211
  if (['unlink', 'unlinkDir'].includes(event)) { return; }
108
212
 
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));
213
+ const directBlock = this.globs.find(blockPath => path.startsWith(`${blockPath}/src`));
214
+
215
+ let contentChanged = false;
216
+ if (this.project.components.cache) {
217
+ const oldHash = this.project.components.cache.hashCache.get(path);
218
+ const newHash = await this.getCurrentFileHash(path);
219
+ if (oldHash !== newHash) {
220
+ contentChanged = true;
221
+ if (newHash) {
222
+ this.project.components.cache.hashCache.set(path, newHash);
223
+ }
224
+ }
225
+ } else {
226
+ contentChanged = true;
227
+ }
228
+ if (!contentChanged) {
229
+ this.end({
230
+ itemLabel: directBlock ? directBlock.replace(this.project.path, '') : 'a block',
231
+ cached: true,
232
+ skipTimer: true
233
+ });
234
+ return;
113
235
  }
114
- debounceTimers.set(block, setTimeout(async () => {
115
- if (buildQueue.has(block)) { return; }
236
+
237
+ const affectedBlocks = new Set();
238
+
239
+ if (directBlock) {
240
+ affectedBlocks.add(directBlock);
116
241
  try {
117
- buildQueue.add(block);
118
- this.project.components.server.server.notify('Building...', 10000);
119
- if (path.endsWith('.js')) {
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
- }
125
- }
126
- await this.process(block);
242
+ const dependencies = await this.getBlockDependencies(directBlock);
243
+ dependencyMap.set(directBlock, dependencies);
127
244
  } catch (error) {
128
- this.log('error', `Failed to process block ${block}: ${error.message}`);
129
- } finally {
130
- buildQueue.delete(block);
131
- debounceTimers.delete(block);
245
+ this.log('warn', `Failed to update dependencies for block ${directBlock}: ${error.message}`);
246
+ }
247
+ }
248
+ for (const [block, dependencies] of dependencyMap) {
249
+ if (dependencies.includes(path)) {
250
+ affectedBlocks.add(block);
251
+ }
252
+ }
253
+
254
+ if (affectedBlocks.size === 0 && !directBlock) {
255
+ await updateDependencyMap();
256
+ for (const [block, dependencies] of dependencyMap) {
257
+ if (dependencies.includes(path)) {
258
+ affectedBlocks.add(block);
259
+ }
132
260
  }
133
- }, DEBOUNCE_DELAY));
261
+ }
262
+
263
+ if (affectedBlocks.size > 0 && this.project.components.cache) {
264
+ this.project.components.cache.hashCache.delete(path);
265
+ await this.project.components.cache.invalidateFile(path);
266
+ }
267
+
268
+ for (const block of affectedBlocks) {
269
+ if (debounceTimers.has(block)) {
270
+ clearTimeout(debounceTimers.get(block));
271
+ }
272
+ debounceTimers.set(block, setTimeout(async () => {
273
+ if (buildQueue.has(block)) { return; }
274
+ try {
275
+ buildQueue.add(block);
276
+ this.project.components.server.server.notify('Building...', 10000);
277
+ if (path.endsWith('.js')) {
278
+ if (!this.project.components.scripts.isBuilding) {
279
+ this.project.components.scripts.lint(path).catch(lintError => {
280
+ this.log('warn', `Linting failed for ${path}: ${lintError.message}`);
281
+ });
282
+ }
283
+ }
284
+ await this.process(block);
285
+ } catch (error) {
286
+ this.log('error', `Failed to process block ${block}: ${error.message}`);
287
+ } finally {
288
+ buildQueue.delete(block);
289
+ debounceTimers.delete(block);
290
+ }
291
+ }, DEBOUNCE_DELAY));
292
+ }
134
293
  });
135
294
  }
136
295
 
@@ -18,6 +18,24 @@ export default class PHPComponent extends BaseComponent {
18
18
  // await this.process(null, { lintType: 'warn' }); // this errors "Fatal error: Allowed memory size"
19
19
  }
20
20
 
21
+ async checkSyntax(entry) {
22
+ try {
23
+ let execPromise = promisify(exec);
24
+ const { stdout, stderr } = await execPromise(`php -l "${entry}"`, {
25
+ cwd: this.path.resolve(this.path.dirname(fileURLToPath(import.meta.url)), '../../')
26
+ });
27
+ } catch (error) {
28
+ if (error.stderr.includes('command not found')) {
29
+ this.log('warn', 'PHP syntax checker not found. Skipping syntax check.');
30
+ return true;
31
+ }
32
+ console.error(error.stderr.replace(this.project.path, ''));
33
+ this.log('error', `Failed to validate ${entry.replace(this.project.path, '')} - See above error.`);
34
+ return false;
35
+ }
36
+ return true;
37
+ }
38
+
21
39
  async build(entry, options) {
22
40
  options = Object.assign({}, {
23
41
  lintType: 'fix'
@@ -25,25 +43,37 @@ export default class PHPComponent extends BaseComponent {
25
43
  let entryLabel = `all PHP files`;
26
44
 
27
45
  this.start();
28
- let workingLintBin = 'phpcbf';
29
- if (options.lintType == 'warn') {
30
- workingLintBin = 'phpcs';
31
- }
46
+
32
47
  let phpFiles = '.';
33
48
  let additionalFlags = '';
49
+ let filesToValidate = this.globs;
50
+
34
51
  if (entry) {
35
- phpFiles = entry;
36
52
  entryLabel = entry.replace(this.project.path, '');
53
+ filesToValidate = [entry];
54
+ phpFiles = entry;
37
55
  } else {
38
56
  additionalFlags += ' -d memory_limit=2G'; // FIXME: this doesn't solve error issue "Fatal error: Allowed memory size"
39
57
  }
58
+
59
+ let syntaxErrors = false;
60
+ for (const phpFile of filesToValidate) {
61
+ const syntaxValid = await this.checkSyntax(phpFile);
62
+ if (!syntaxValid) {
63
+ syntaxErrors = true;
64
+ }
65
+ }
66
+ if (syntaxErrors) {
67
+ return false;
68
+ }
69
+
70
+ let workingLintBin = 'phpcbf';
71
+ if (options.lintType == 'warn') {
72
+ workingLintBin = 'phpcs';
73
+ }
40
74
  try {
41
75
  const cmds = [
42
76
  `vendor/bin/${workingLintBin}`,
43
- `--parallel=5`,
44
- `--error-severity=1`,
45
- `--warning-severity=1`,
46
- `--colors`,
47
77
  `--basepath=${this.project.path}`,
48
78
  phpFiles,
49
79
  additionalFlags
@@ -66,10 +96,6 @@ export default class PHPComponent extends BaseComponent {
66
96
  console.error(error.stderr?.length ? error.stderr : error.stdout);
67
97
  this.log('error', `Failed linting ${entryLabel.replace(this.project.path, '')} - See above error.`);
68
98
  return false;
69
- } else {
70
- if (error.stdout?.length) {
71
- console.log(error.stdout);
72
- }
73
99
  }
74
100
  }
75
101
  if (this.project.components.server?.server) {
@@ -20,6 +20,7 @@ export default class ServerComponent extends BaseComponent {
20
20
  this.watchedFiles.push(`**/*.php`);
21
21
  }
22
22
  this.ignoredFiles = [
23
+ `.sdc-build-wp/cache/**`,
23
24
  `node_modules/**`,
24
25
  `vendor/**/*`,
25
26
  `**/*.map`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sdc-build-wp",
3
- "version": "4.9.0",
3
+ "version": "4.9.2",
4
4
  "description": "Custom WordPress build process.",
5
5
  "engines": {
6
6
  "node": ">=22"