wp-advads 1.0.33 → 1.0.35

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/.github/workflows/test.yml +17 -0
  2. package/CHANGELOG.md +23 -1
  3. package/LICENSE +15 -0
  4. package/README.md +86 -66
  5. package/package.json +9 -7
  6. package/src/app.js +17 -11
  7. package/src/commands/build.js +72 -59
  8. package/src/commands/class.js +34 -40
  9. package/src/commands/css.js +13 -19
  10. package/src/commands/init.js +4 -1
  11. package/src/commands/javascript.js +13 -19
  12. package/src/commands/release/change-version.js +14 -17
  13. package/src/commands/release/changelog.js +23 -31
  14. package/src/commands/release/git.js +25 -11
  15. package/src/commands/release/github.js +63 -12
  16. package/src/commands/release/semver.js +22 -25
  17. package/src/commands/release/translations.js +17 -23
  18. package/src/commands/release.js +3 -4
  19. package/src/commands/updates.js +20 -17
  20. package/src/commands/views.js +16 -18
  21. package/src/commands/wp-glotpress.js +125 -126
  22. package/src/commands/wp-pot.js +17 -9
  23. package/src/utilities/filesystem.js +57 -36
  24. package/src/utilities/flow.js +21 -0
  25. package/src/utilities/formatting.js +13 -4
  26. package/src/utilities/index.js +1 -1
  27. package/src/utilities/misc.js +1 -6
  28. package/src/utilities/settings.js +34 -14
  29. package/src/utilities/shell.js +60 -8
  30. package/template/files/class-singleton.php +5 -0
  31. package/template/files/javascript.js +0 -2
  32. package/template/files/view.php +2 -0
  33. package/test/build.test.js +168 -0
  34. package/test/changelog-locales.test.js +49 -0
  35. package/test/class.test.js +47 -0
  36. package/test/compile-templates.test.js +70 -0
  37. package/test/deps-hygiene.test.js +46 -0
  38. package/test/engines.test.js +11 -0
  39. package/test/get-latest-tag.test.js +51 -0
  40. package/test/git-precheck.test.js +44 -0
  41. package/test/github-final.test.js +93 -0
  42. package/test/glotpress-download.test.js +176 -0
  43. package/test/nested-path.test.js +39 -0
  44. package/test/no-fs-extra.test.js +11 -0
  45. package/test/package-identity.test.js +19 -0
  46. package/test/path-confine.test.js +42 -0
  47. package/test/pot-docs.test.js +27 -0
  48. package/test/pot-headers.test.js +49 -0
  49. package/test/project-root.test.js +26 -0
  50. package/test/release-waterfall.test.js +62 -0
  51. package/test/semver.test.js +25 -0
  52. package/test/settings-clone.test.js +34 -0
  53. package/test/settings-path.test.js +31 -0
  54. package/test/shell.test.js +100 -0
  55. package/test/singleton.test.js +14 -0
  56. package/test/stable-tag.test.js +13 -0
  57. package/test/stub-security.test.js +37 -0
  58. package/test/template-folder.test.js +19 -0
  59. package/test/translations.test.js +51 -0
  60. package/test/update-file-content.test.js +38 -0
  61. package/test/updates.test.js +39 -0
  62. package/test/version.test.js +22 -0
  63. package/test/view-escape.test.js +17 -0
  64. package/test/view.test.js +33 -0
  65. package/test/write-force.test.js +47 -0
  66. package/src/utilities/semver.js +0 -45
  67. package/template/.github/pull_request_template.md +0 -52
  68. package/template/configs/.editorconfig +0 -27
  69. package/template/configs/.eslintignore +0 -13
  70. package/template/configs/.eslintrc.js +0 -13
  71. package/template/configs/.phpcs.xml.dist +0 -87
  72. package/template/configs/.prettierignore +0 -49
  73. package/template/configs/.prettierrc.js +0 -3
  74. package/template/configs/.stylelintrc +0 -44
  75. package/template/configs/composer.json +0 -51
  76. package/template/configs/gitattributes +0 -34
  77. package/template/configs/gitignore +0 -43
  78. package/template/configs/package.json +0 -66
  79. package/template/configs/postcss.config.js +0 -6
  80. package/template/configs/tailwind.config.js +0 -8
  81. package/template/configs/webpack.mix.js +0 -65
  82. package/template/configs/webpack.mix.local.js +0 -3
  83. package/template/includes/index.php +0 -2
  84. package/template/index.js +0 -0
  85. package/template/index.php +0 -4
  86. package/template/plugin/assets/scss/app.scss +0 -0
  87. package/template/plugin/assets/src/app.js +0 -0
  88. package/template/tools/laravel-mix/wp-pot.js +0 -57
  89. package/template/tools/wp-glotpress.js +0 -162
@@ -1,7 +1,6 @@
1
1
  /**
2
2
  * External dependencies
3
3
  */
4
- import chalk from 'chalk';
5
4
  import logSymbols from 'log-symbols';
6
5
  import template from 'lodash/template.js';
7
6
 
@@ -9,12 +8,13 @@ import template from 'lodash/template.js';
9
8
  * Node dependencies
10
9
  */
11
10
  import path from 'path';
12
- import fs from 'fs-extra';
11
+ import fs from 'node:fs';
12
+ import { fileURLToPath } from 'node:url';
13
13
 
14
14
  /**
15
15
  * Internal Dependencies
16
16
  */
17
- import { msgSuccessOnSameLine, onSameLine, write } from './formatting.js'
17
+ import { msgErrorTitle, msgSuccessOnSameLine, onSameLine, write } from './formatting.js'
18
18
 
19
19
  let projectRoot = null
20
20
 
@@ -32,32 +32,26 @@ export function getProjectRoot(startDir = process.cwd()) {
32
32
  return projectRoot;
33
33
  }
34
34
 
35
- projectRoot = startDir;
35
+ let dir = startDir;
36
36
 
37
- while (projectRoot !== path.parse(projectRoot).root) {
38
- const possibleRootFile = path.join(projectRoot, 'wp.advads');
39
- if (fs.existsSync(possibleRootFile)) {
37
+ while (dir !== path.parse(dir).root) {
38
+ if (fs.existsSync(path.join(dir, 'wp.advads'))) {
39
+ projectRoot = dir;
40
40
  return projectRoot;
41
41
  }
42
42
 
43
- // Move up one directory level
44
- projectRoot = path.dirname(projectRoot);
45
- }
43
+ dir = path.dirname(dir);
44
+ }
46
45
 
47
- throw new Error('Project root not found. Make sure there is a wp.advads file in the root directory.');
46
+ projectRoot = null;
47
+ throw new Error('Project root not found. Make sure there is a wp.advads file in the root directory.');
48
48
  }
49
49
 
50
50
  /**
51
- * Retrieves the path to the template folder.
52
- *
53
- * This function uses `createRequire` to resolve the path to the `../../template` directory
54
- * relative to the current module's URL and removes the `/index.js` part from the resolved path.
55
- *
56
51
  * @returns {string} The path to the template folder.
57
52
  */
58
53
  export function getTemplateFolder() {
59
- const dirname = path.dirname(import.meta.url.replace('file://', ''))
60
- return path.join(dirname, '../../template')
54
+ return path.join( path.dirname( fileURLToPath( import.meta.url ) ), '../../template' )
61
55
  }
62
56
 
63
57
  /**
@@ -87,21 +81,22 @@ export function compileTemplate( filename, data ) {
87
81
  }
88
82
 
89
83
  /**
90
- * Deletes a specified file if it exists.
84
+ * Compile a template and write it, or print the shared scaffold failure.
91
85
  *
92
- * @param {string} name - The name of the file to be deleted.
93
- * @param {string} file - The path to the file to be deleted.
94
- *
95
- * @returns {void}
86
+ * @param {string} folder
87
+ * @param {string} filename
88
+ * @param {string} templateFile Path under template/
89
+ * @param {Object} data
90
+ * @param {boolean} force
96
91
  */
97
- export function deleteFile(name, file) {
98
- if ( ! fs.existsSync(file) ) {
99
- console.log( `${logSymbols.error} ${chalk.red(`${name} file not found!`)}` )
100
- return
101
- }
102
-
103
- fs.rmSync(file)
104
- console.log( `${logSymbols.success} ${chalk.dim(`${name} deleted!`)}` )
92
+ export function writeFromTemplate( folder, filename, templateFile, data, force = false ) {
93
+ try {
94
+ const content = compileTemplate( getTemplateFile( templateFile ), data )
95
+ writeFile( folder, filename, content, force )
96
+ } catch ( err ) {
97
+ msgErrorTitle( 'We failed!!!' )
98
+ throw err
99
+ }
105
100
  }
106
101
 
107
102
  /**
@@ -120,7 +115,7 @@ export function updateFileContent(fileName, messages, next, callback) {
120
115
  fs.readFile(fileName, (err, buffer) => {
121
116
  if ( null !== err ) {
122
117
  onSameLine(`${logSymbols.error} ${messages.failed}`)
123
- return next()
118
+ return next(err)
124
119
  }
125
120
 
126
121
  const content = callback(buffer.toString())
@@ -128,7 +123,7 @@ export function updateFileContent(fileName, messages, next, callback) {
128
123
  fs.writeFile(fileName, content, (err) => {
129
124
  if ( null !== err ) {
130
125
  onSameLine(`${logSymbols.error} ${messages.failed}`)
131
- return next()
126
+ return next(err)
132
127
  }
133
128
 
134
129
  onSameLine(`${logSymbols.success} ${messages.updated}`)
@@ -137,12 +132,38 @@ export function updateFileContent(fileName, messages, next, callback) {
137
132
  })
138
133
  }
139
134
 
140
- export function writeFile(folder, filename, content) {
135
+ function assertInsideProject( target ) {
136
+ if ( String( target ).includes( '\0' ) ) {
137
+ throw new Error( 'Invalid path' )
138
+ }
139
+
140
+ const root = path.resolve( getProjectRoot() )
141
+ const resolved = path.resolve( target )
142
+ const relative = path.relative( root, resolved )
143
+
144
+ if ( relative.startsWith( `..${path.sep}` ) || relative === '..' || path.isAbsolute( relative ) ) {
145
+ throw new Error( 'Path escapes project root' )
146
+ }
147
+ }
148
+
149
+ export function writeFile(folder, filename, content, force = false) {
150
+ if ( String( filename ).includes( '\0' ) ) {
151
+ throw new Error( 'Invalid path' )
152
+ }
153
+
154
+ assertInsideProject( folder )
155
+ assertInsideProject( path.join( folder, filename ) )
156
+
157
+ const target = path.join( folder, filename )
158
+ if ( ! force && fs.existsSync( target ) ) {
159
+ throw new Error( `File exists: ${target}` )
160
+ }
161
+
141
162
  write('Creating directories!!');
142
- fs.ensureDirSync(folder);
163
+ fs.mkdirSync(folder, { recursive: true });
143
164
  msgSuccessOnSameLine('Directories created successfully');
144
165
 
145
166
  write('Creating file!!');
146
- fs.writeFileSync(path.join(folder, filename), content);
167
+ fs.writeFileSync(target, content);
147
168
  msgSuccessOnSameLine('File created successfully');
148
169
  }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Run callback-style steps in order. `next(err)` if any step fails.
3
+ *
4
+ * @param {Function[]} steps Each step is `(done) => void`
5
+ * @param {Function} next
6
+ */
7
+ export function runSteps( steps, next ) {
8
+ ;( async () => {
9
+ for ( const step of steps ) {
10
+ await new Promise( ( resolve, reject ) => {
11
+ step( ( err ) => {
12
+ if ( err ) {
13
+ reject( err )
14
+ } else {
15
+ resolve()
16
+ }
17
+ } )
18
+ } )
19
+ }
20
+ } )().then( () => next() ).catch( ( err ) => next( err ) )
21
+ }
@@ -28,6 +28,10 @@ export function onNewLine(text) {
28
28
  console.log(text)
29
29
  }
30
30
 
31
+ function canRewriteStdout() {
32
+ return Boolean( process.stdout.isTTY && ! process.env.NODE_TEST_CONTEXT )
33
+ }
34
+
31
35
  /**
32
36
  * Print text on same line by clearing buffer.
33
37
  *
@@ -36,9 +40,9 @@ export function onNewLine(text) {
36
40
  * @returns {void}
37
41
  */
38
42
  export function onSameLine(text) {
39
- if (process.stdout.clearLine) {
40
- process.stdout.clearLine(); // Clear the current line
41
- process.stdout.cursorTo(0); // Move the cursor to the start of the line
43
+ if ( canRewriteStdout() && process.stdout.clearLine ) {
44
+ process.stdout.clearLine();
45
+ process.stdout.cursorTo(0);
42
46
  }
43
47
 
44
48
  console.log(text)
@@ -118,5 +122,10 @@ export function msgErrorTitle(msg) {
118
122
  * @returns {void}
119
123
  */
120
124
  export function write(text) {
121
- process.stdout.write(text)
125
+ if ( canRewriteStdout() ) {
126
+ process.stdout.write(text)
127
+ return
128
+ }
129
+
130
+ console.log(text)
122
131
  }
@@ -1,6 +1,6 @@
1
1
  export * from './filesystem.js'
2
+ export * from './flow.js'
2
3
  export * from './formatting.js'
3
- export * from './semver.js'
4
4
  export * from './settings.js'
5
5
  export * from './shell.js'
6
6
  export * from './misc.js'
@@ -1,12 +1,7 @@
1
- /**
2
- * External Dependencies
3
- */
4
- import capitalize from 'lodash/capitalize.js';
5
-
6
1
  export function filenameToHeading(filename) {
7
2
  filename = filename.replace('.php', '');
8
3
  filename = filename.replace('.js', '');
9
4
  filename = filename.replace(/-/g, ' ');
10
5
 
11
- return capitalize(filename);
6
+ return filename.charAt(0).toUpperCase() + filename.slice(1).toLowerCase();
12
7
  }
@@ -1,9 +1,3 @@
1
- /**
2
- * External dependencies
3
- */
4
- import get from 'lodash/get.js'
5
- import set from 'lodash/set.js'
6
-
7
1
  /**
8
2
  * Node dependencies
9
3
  */
@@ -18,6 +12,14 @@ import { write } from './formatting.js'
18
12
 
19
13
  let settings = null
20
14
 
15
+ function configPath() {
16
+ try {
17
+ return path.join( getProjectRoot(), 'wp.advads' )
18
+ } catch {
19
+ return path.join( process.cwd(), 'wp.advads' )
20
+ }
21
+ }
22
+
21
23
  /**
22
24
  * Checks if the configuration file exists at the specified path.
23
25
  *
@@ -25,8 +27,7 @@ let settings = null
25
27
  */
26
28
  export function configFileExists() {
27
29
  try {
28
- const configPath = path.join(getProjectRoot(), 'wp.advads')
29
- return fs.existsSync(configPath);
30
+ return fs.existsSync( configPath() );
30
31
  } catch (err) {
31
32
  return false;
32
33
  }
@@ -39,8 +40,7 @@ export function configFileExists() {
39
40
  */
40
41
  export function readConfigFile() {
41
42
  try {
42
- const configPath = path.join(getProjectRoot(), 'wp.advads')
43
- const data = fs.readFileSync(configPath, 'utf-8');
43
+ const data = fs.readFileSync( configPath(), 'utf-8' );
44
44
  return JSON.parse(data);
45
45
  } catch (err) {
46
46
  return {};
@@ -55,9 +55,8 @@ export function readConfigFile() {
55
55
  export function saveConfig(data) {
56
56
  write('Saving config file...');
57
57
  try {
58
- const savePath = path.join(process.cwd(), 'wp.advads')
59
58
  const jsonData = JSON.stringify(data, null, 2);
60
- fs.writeFileSync(savePath, jsonData);
59
+ fs.writeFileSync( configPath(), jsonData );
61
60
  settings = data;
62
61
  } catch (err) {
63
62
  write('Error writing config file:', err);
@@ -82,7 +81,7 @@ export function getSetting(key = 'all', defaultVal = '') {
82
81
  return settings;
83
82
  }
84
83
 
85
- return get( settings, key, defaultVal );
84
+ return getPath( settings, key, defaultVal );
86
85
  }
87
86
 
88
87
  export function updateSetting(key, value) {
@@ -90,6 +89,27 @@ export function updateSetting(key, value) {
90
89
  settings = readConfigFile();
91
90
  }
92
91
 
93
- settings = set(settings, key, value);
92
+ settings = setPath(settings, key, value);
94
93
  saveConfig(settings);
95
94
  }
95
+
96
+ function getPath( object, key, defaultVal ) {
97
+ const value = String( key ).split( '.' ).reduce( ( cur, part ) => cur?.[ part ], object )
98
+ return value === undefined ? defaultVal : value
99
+ }
100
+
101
+ function setPath( object, key, value ) {
102
+ const parts = String( key ).split( '.' )
103
+ let cur = object
104
+
105
+ for ( let i = 0; i < parts.length - 1; i++ ) {
106
+ const part = parts[ i ]
107
+ if ( cur[ part ] == null || typeof cur[ part ] !== 'object' ) {
108
+ cur[ part ] = {}
109
+ }
110
+ cur = cur[ part ]
111
+ }
112
+
113
+ cur[ parts[ parts.length - 1 ] ] = value
114
+ return object
115
+ }
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Node dependencies
3
3
  */
4
- import { exec, spawn } from 'node:child_process'
4
+ import { spawn } from 'node:child_process'
5
5
 
6
6
  /**
7
7
  * Internal Dependencies
@@ -19,10 +19,20 @@ import { onSameLine } from './formatting.js'
19
19
  */
20
20
  export function runCommand( command, args, callback ) {
21
21
  const commandSpawn = spawn( command, args, {
22
- shell: true,
23
- stdio: [ 'inherit' ]
22
+ shell: false,
23
+ stdio: [ 'inherit', 'pipe', 'pipe' ]
24
24
  } )
25
25
 
26
+ let finished = false
27
+ const done = ( err ) => {
28
+ if ( finished ) {
29
+ return
30
+ }
31
+
32
+ finished = true
33
+ callback( err )
34
+ }
35
+
26
36
  commandSpawn.stdout.on( 'data', (data) => {
27
37
  onSameLine(data.toString())
28
38
  })
@@ -31,23 +41,65 @@ export function runCommand( command, args, callback ) {
31
41
  onSameLine(data.toString().replace('\n','').replace('\r','').trim())
32
42
  })
33
43
 
44
+ commandSpawn.on( 'error', done )
45
+
34
46
  commandSpawn.on( 'close', (code) => {
35
- callback()
47
+ if ( code !== 0 ) {
48
+ done( new Error( `Command failed with exit code ${code}` ) )
49
+ return
50
+ }
51
+
52
+ done()
36
53
  })
37
54
  }
38
55
 
39
56
  /**
40
57
  * Execute a command in the terminal.
41
58
  *
42
- * @param {string} command Command to execute
59
+ * @param {string|string[]} command Command to execute, or argv array
43
60
  * @param {Function} callback Callback function
61
+ * @param {Object} options cwd/env for spawn
44
62
  *
45
63
  * @returns {void}
46
64
  */
47
65
  export function execCommand( command, callback, options = {} ) {
48
- if ('string' !== typeof command) {
49
- command = command.join(' ')
66
+ const spawnOpts = {
67
+ cwd: options.cwd,
68
+ env: options.env,
69
+ stdio: [ 'ignore', 'pipe', 'pipe' ],
50
70
  }
51
71
 
52
- exec( command, options, (error, stdout, stderr) => callback(stdout, error, stderr) )
72
+ const child = Array.isArray( command )
73
+ ? spawn( command[0], command.slice( 1 ), { ...spawnOpts, shell: false } )
74
+ : spawn( command, { ...spawnOpts, shell: true } )
75
+
76
+ let stdout = ''
77
+ let stderr = ''
78
+ let finished = false
79
+ const done = ( error ) => {
80
+ if ( finished ) {
81
+ return
82
+ }
83
+
84
+ finished = true
85
+ callback( stdout, error, stderr )
86
+ }
87
+
88
+ child.stdout.on( 'data', ( data ) => {
89
+ const text = data.toString()
90
+ stdout += text
91
+ onSameLine( text )
92
+ } )
93
+
94
+ child.stderr.on( 'data', ( data ) => {
95
+ const text = data.toString()
96
+ stderr += text
97
+ onSameLine( text.replace( '\n', '' ).replace( '\r', '' ).trim() )
98
+ } )
99
+
100
+ child.on( 'error', done )
101
+
102
+ child.on( 'close', ( code ) => {
103
+ done( code ? new Error( `Command failed with exit code ${code}` ) : null )
104
+ } )
53
105
  }
@@ -33,4 +33,9 @@ class {{className}} {
33
33
 
34
34
  return $instance;
35
35
  }
36
+
37
+ /**
38
+ * Constructor.
39
+ */
40
+ private function __construct() {}
36
41
  }
@@ -6,8 +6,6 @@
6
6
  * @author {{author.name}} <{{author.email}}>
7
7
  * @description {{description}}
8
8
  */
9
- import jquery from 'jquery';
10
-
11
9
  export default function() {
12
10
  // Code goes here...
13
11
  }
@@ -9,3 +9,5 @@
9
9
  */
10
10
 
11
11
  defined( 'ABSPATH' ) || exit;
12
+
13
+ // Escape output: esc_html(), esc_attr(), esc_url(). Do not echo untrusted data raw.
@@ -0,0 +1,168 @@
1
+ /**
2
+ * TASK-002 / TASK-020: build waterfall must stop on composer / resource / rmSync failure.
3
+ * Resource build uses package.json scripts.build when present.
4
+ * Run with --experimental-test-module-mocks
5
+ */
6
+ import assert from 'node:assert/strict'
7
+ import { mkdtemp, rm, writeFile } from 'node:fs/promises'
8
+ import { tmpdir } from 'node:os'
9
+ import { join } from 'node:path'
10
+ import { mock, test, before, beforeEach } from 'node:test'
11
+ import fs from 'node:fs'
12
+
13
+ const execCalls = []
14
+ let execImpl = ( cmd, cb ) => cb( '', null, '' )
15
+ let potCalled = false
16
+
17
+ mock.module( import.meta.resolve( '../src/utilities/shell.js' ), {
18
+ exports: {
19
+ execCommand( cmd, cb, options = {} ) {
20
+ execCalls.push( { cmd, options } )
21
+ execImpl( cmd, cb, options )
22
+ },
23
+ runCommand() {},
24
+ },
25
+ } )
26
+
27
+ mock.module( import.meta.resolve( '../src/commands/wp-pot.js' ), {
28
+ exports: {
29
+ default( next ) {
30
+ potCalled = true
31
+ next()
32
+ },
33
+ },
34
+ } )
35
+
36
+ const { default: wpBuild } = await import( '../src/commands/build.js' )
37
+ const { pluginData } = await import( '../src/commands/release.js' )
38
+
39
+ function runBuild() {
40
+ return new Promise( ( resolve ) => {
41
+ wpBuild( ( err ) => resolve( err ) )
42
+ } )
43
+ }
44
+
45
+ async function withPackage( pkg, run ) {
46
+ const dir = await mkdtemp( join( tmpdir(), 'wp-advads-build-' ) )
47
+ await writeFile( join( dir, 'package.json' ), JSON.stringify( pkg ) )
48
+ const cwd = process.cwd()
49
+ process.chdir( dir )
50
+ try {
51
+ return await run()
52
+ } finally {
53
+ process.chdir( cwd )
54
+ await rm( dir, { recursive: true, force: true } )
55
+ }
56
+ }
57
+
58
+ before( () => {
59
+ mock.method( fs, 'rmSync', () => {} )
60
+ } )
61
+
62
+ beforeEach( () => {
63
+ execCalls.length = 0
64
+ potCalled = false
65
+ execImpl = ( cmd, cb ) => cb( '', null, '' )
66
+ fs.rmSync.mock.mockImplementation( () => {} )
67
+ pluginData.commitMessages = []
68
+ } )
69
+
70
+ test( 'composer install error stops the build before dump and pot', async () => {
71
+ execImpl = ( cmd, cb ) => {
72
+ if ( String( cmd ).includes( 'composer install' ) ) {
73
+ cb( '', new Error( 'install failed' ), 'install exploded' )
74
+ return
75
+ }
76
+ cb( '', null, '' )
77
+ }
78
+
79
+ const err = await runBuild()
80
+
81
+ assert.ok( err, 'expected the build callback to receive an error' )
82
+ assert.equal( potCalled, false )
83
+ assert.equal(
84
+ execCalls.some( ( call ) => call.cmd === 'composer dump' ),
85
+ false
86
+ )
87
+ } )
88
+
89
+ test( 'npm run build error stops the build before pot', async () => {
90
+ await withPackage( { scripts: { build: 'mix --production' } }, async () => {
91
+ execImpl = ( cmd, cb ) => {
92
+ if ( String( cmd ).includes( 'npm run build' ) ) {
93
+ cb( '', new Error( 'build failed' ), 'mix exploded' )
94
+ return
95
+ }
96
+ cb( '', null, '' )
97
+ }
98
+
99
+ const err = await runBuild()
100
+
101
+ assert.ok( err )
102
+ assert.equal( potCalled, false )
103
+ } )
104
+ } )
105
+
106
+ test( 'resource build runs npm run build instead of wp-scripts', async () => {
107
+ await withPackage( { scripts: { build: 'mix --production' } }, async () => {
108
+ await runBuild()
109
+
110
+ assert.equal(
111
+ execCalls.some( ( call ) => String( call.cmd ).includes( 'wp-scripts' ) ),
112
+ false
113
+ )
114
+ assert.equal(
115
+ execCalls.some( ( call ) => call.cmd === 'npm run build' ),
116
+ true
117
+ )
118
+ } )
119
+ } )
120
+
121
+ test( 'resource build is skipped when package.json has no build script', async () => {
122
+ await withPackage( { scripts: { start: 'node x' } }, async () => {
123
+ await runBuild()
124
+
125
+ assert.equal(
126
+ execCalls.some( ( call ) => String( call.cmd ).includes( 'wp-scripts' ) ),
127
+ false
128
+ )
129
+ assert.equal(
130
+ execCalls.some( ( call ) => String( call.cmd ).includes( 'npm run build' ) ),
131
+ false
132
+ )
133
+ assert.equal( potCalled, true )
134
+ } )
135
+ } )
136
+
137
+ test( 'composer dump receives COMPOSER_VENDOR_DIR packages', async () => {
138
+ await runBuild()
139
+
140
+ const dump = execCalls.find( ( call ) => call.cmd === 'composer dump' )
141
+ assert.ok( dump, 'expected composer dump to run' )
142
+ assert.equal( dump.options.env.COMPOSER_VENDOR_DIR, 'packages' )
143
+ } )
144
+
145
+ test( 'rmSync failure stops the build before composer', async () => {
146
+ fs.rmSync.mock.mockImplementation( () => {
147
+ throw new Error( 'EACCES' )
148
+ } )
149
+
150
+ const err = await runBuild()
151
+
152
+ assert.ok( err )
153
+ assert.equal( execCalls.length, 0 )
154
+ assert.equal( potCalled, false )
155
+ } )
156
+
157
+ test( 'standalone build does not mutate pluginData.commitMessages', async () => {
158
+ await runBuild()
159
+ assert.equal( pluginData.commitMessages.length, 0 )
160
+ } )
161
+
162
+ test( 'build appends messages to an optional sink', async () => {
163
+ const sink = []
164
+ await new Promise( ( resolve ) => {
165
+ wpBuild( () => resolve(), sink )
166
+ } )
167
+ assert.ok( sink.includes( 'update 3rd party dependencies' ) )
168
+ } )
@@ -0,0 +1,49 @@
1
+ /**
2
+ * TASK-012: changelog lists git files from glotpress.destination, not pluginData.
3
+ * Run with --test-isolation=none --experimental-test-module-mocks
4
+ */
5
+ import assert from 'node:assert/strict'
6
+ import { mkdir } from 'node:fs/promises'
7
+ import { mkdtemp, rm } from 'node:fs/promises'
8
+ import { tmpdir } from 'node:os'
9
+ import { join } from 'node:path'
10
+ import { mock, test } from 'node:test'
11
+
12
+ const execCalls = []
13
+
14
+ mock.module( import.meta.resolve( '../src/utilities/shell.js' ), {
15
+ exports: {
16
+ execCommand( cmd, cb ) {
17
+ execCalls.push( cmd )
18
+ cb( 'languages/myplugin-de_DE.po\n', null, '' )
19
+ },
20
+ runCommand() {},
21
+ },
22
+ } )
23
+
24
+ const { pluginData } = await import( '../src/commands/release.js' )
25
+ const { default: updateChangelog } = await import( '../src/commands/release/changelog.js' )
26
+
27
+ test( 'updateLocalesChangelog uses glotpress.destination instead of unset pluginData.glotpress', async () => {
28
+ const dir = await mkdtemp( join( tmpdir(), 'wp-advads-changelog-' ) )
29
+ const cwd = process.cwd()
30
+ process.chdir( dir )
31
+ await mkdir( join( dir, '.changelog' ) )
32
+
33
+ pluginData.semver = { preRelease: true, getNextVersionString: () => '1.2.3' }
34
+ pluginData.locales = { 'languages/myplugin-de_DE.po': 'German' }
35
+ delete pluginData.glotpress
36
+ execCalls.length = 0
37
+
38
+ try {
39
+ const err = await new Promise( ( resolve ) => {
40
+ updateChangelog( ( error ) => resolve( error ) )
41
+ } )
42
+
43
+ assert.equal( err, undefined )
44
+ assert.deepEqual( execCalls[ 0 ], [ 'git', 'ls-files', '--', './languages/' ] )
45
+ } finally {
46
+ process.chdir( cwd )
47
+ await rm( dir, { recursive: true, force: true } )
48
+ }
49
+ } )