wp-advads 1.0.34 → 1.0.36

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,139 +1,158 @@
1
- # wp-advads Scaffolding Tool
2
-
3
-
4
-
1
+ # wp-advads
5
2
 
6
3
  [![npm version](https://img.shields.io/npm/v/wp-advads.svg)](https://www.npmjs.com/package/wp-advads)
7
4
  [![License: ISC](https://img.shields.io/badge/License-ISC-blue.svg)](LICENSE)
8
5
 
9
- <p align="center">
10
- <img src="https://img.icons8.com/nolan/256/wordpress.png"/>
11
- </p>
6
+ CLI scaffolding for an **existing** Advanced Ads WordPress plugin. It writes `wp.advads` and file stubs (`class`, `view`, `updates`, `js`, `css`). It does not generate a plugin from scratch.
12
7
 
13
- ## 📃 Introduction
8
+ Requires **Node.js 18+**. Created by [Shakeeb Ahmed](https://github.com/meshakeeb).
14
9
 
15
- CLI tools to streamline development tasks for the Advanced Ads WordPress plugin.
16
- Created by [Shakeeb Ahmed](https://github.com/meshakeeb)
10
+ ## Installation
17
11
 
18
- ## 📦 Installation
19
- Use via npx:
20
12
  ```bash
21
13
  npx wp-advads
22
14
  ```
23
15
 
24
- ## 🚀 Usage
25
- Run the CLI using:
26
16
  ```bash
27
17
  npx wp-advads <command> [options]
28
18
  ```
29
19
 
30
- ## 🔧 Commands
20
+ ## Config
21
+
22
+ Commands (except `init`) need a `wp.advads` file. The CLI uses the nearest ancestor that already has one. `init` writes `wp.advads` in the current directory.
23
+
24
+ If the file is missing, the CLI exits and tells you to run `wp-advads init`. It does not prompt.
25
+
26
+ ## Commands
31
27
 
32
28
  #### `init`
33
- Initializes a new project configuration file.
29
+
30
+ Creates `wp.advads` in the current directory.
31
+
34
32
  ```bash
35
33
  npx wp-advads init
36
34
  ```
37
35
 
38
- `wp.advads` lives in the nearest ancestor directory that already has that file. `init` creates it in the current directory.
36
+ #### `build`
37
+
38
+ Installs PHP dependencies into `packages/` (`COMPOSER_VENDOR_DIR=packages`), dumps the autoloader, runs `npm run build` when `package.json` has a `scripts.build`, then generates a `.pot`.
39
+
40
+ ```bash
41
+ npx wp-advads build
42
+ ```
43
+
44
+ Standalone `build` does not touch release git messages. During `release`, commit bullets are passed in explicitly.
39
45
 
40
46
  #### `release`
41
- Creates a new release for the plugin.
47
+
48
+ Bumps the version, changelog, translations, build, and git (explicit file list, hooks run). Stops on the first failed step.
49
+
42
50
  ```bash
43
51
  npx wp-advads release
44
52
  ```
45
53
 
46
-
47
54
  #### `class <classname> [options]`
48
- Creates a new PHP class file with optional templates.
55
+
56
+ Writes a PHP class under `paths.php`. Combine `--heading` with a template flag (`-i` / `-g` / `-r` / `-s`).
57
+
49
58
  ```bash
50
59
  npx wp-advads class MyClass --heading "Class Heading" --description "Description" -s
51
60
  ```
52
61
 
53
- ##### Required Argument:
54
- - `<classname>` – The name of the class.
55
-
56
- ##### Optional Argument:
62
+ - `<classname>` – Class name (namespaces with `\`, e.g. `Foo\Bar`)
57
63
  - `--heading <heading>` – File heading
58
64
  - `--description <description>` – File description
59
- - `-i` – Initializer interface
60
- - `-g` – Integration interface
61
- - `-r` – REST interface
62
- - `-s` – Singleton class
63
-
65
+ - `-i` – Initializer
66
+ - `-g` – Integration
67
+ - `-r` – REST
68
+ - `-s` – Singleton (`__construct` is private)
69
+ - `--force` – Overwrite if the file exists
64
70
 
65
71
  #### `view <viewname> [heading]`
66
- Creates a new view file (e.g. template partial).
72
+
73
+ Writes a PHP view under `paths.views`. Nested names accept `/` or `\`.
74
+
67
75
  ```bash
68
76
  npx wp-advads view sidebar "Sidebar View"
77
+ npx wp-advads view admin/settings
69
78
  ```
70
79
 
71
- ##### Required Argument:
72
- - `<viewname>` – Name of the view file.
80
+ - `<viewname>` – View file name
81
+ - `[heading]` – Heading comment
82
+ - `--force` – Overwrite if the file exists
73
83
 
74
- ##### Optional Argument:
75
- - `[heading]` – A heading comment added to the view file.
84
+ #### `updates <version>`
76
85
 
86
+ Writes an upgrade file under `paths.updates`. `misc.prefix` from `wp.advads` is required (empty prefix is an error).
77
87
 
78
- #### `updates <version>`
79
- Creates update logic for the given plugin version.
80
88
  ```bash
81
89
  npx wp-advads updates 1.2.3
82
90
  ```
83
91
 
84
- ##### Required Argument:
85
- - `<version>` – Target version number (e.g. `1.2.0`)
92
+ - `<version>` – Target version (e.g. `1.2.3`)
93
+ - `--force` – Overwrite if the file exists
86
94
 
87
95
  #### `js <filename> [options]`
88
- Creates a new JavaScript file.
96
+
97
+ Writes a JavaScript file under `paths.javascript`. Nested names accept `/` or `\`.
98
+
89
99
  ```bash
90
100
  npx wp-advads js script -h "Main JS" -d "Handles the frontend logic"
91
101
  ```
92
102
 
93
- ##### Required Argument:
94
- - `<filename>` – Name of the JavaScript file (without extension).
103
+ - `<filename>` – Name without extension
104
+ - `-h <heading>` – Heading comment
105
+ - `-d <description>` – Description comment
106
+ - `--force` – Overwrite if the file exists
95
107
 
96
- ##### Optional Argument:
97
- - `-h <heading>` – Heading comment for the file.
98
- - `-d <description>` – Description comment for the file.
108
+ #### `css <filename> [options]`
99
109
 
110
+ Writes a CSS file under `paths.css`. Nested names accept `/` or `\`.
100
111
 
101
- #### `css <filename> [options]`
102
- Creates a new CSS file.
103
112
  ```bash
104
113
  npx wp-advads css styles -h "Main CSS" -d "Plugin styles"
105
114
  ```
106
115
 
107
- ##### Required Argument:
108
- - `<filename>` – Name of the CSS file (without extension).
109
-
110
- ##### Optional Argument:
111
- - `-h <heading>` – Heading comment for the file.
112
- - `-d <description>` – Description comment for the file.
116
+ - `<filename>` – Name without extension
117
+ - `-h <heading>` – Heading comment
118
+ - `-d <description>` – Description comment
119
+ - `--force` – Overwrite if the file exists
113
120
 
114
121
  #### `pot`
115
- Generates a `.pot` translation template.
122
+
123
+ Creates a `.pot` translation template (`wp i18n make-pot`).
124
+
116
125
  ```bash
117
126
  npx wp-advads pot
118
127
  ```
119
128
 
129
+ POT headers (`Last-Translator`, `Language-Team`) use `author.name` and `author.email` from `wp.advads`.
130
+
120
131
  #### `translations`
121
- Downloads translation files from GlotPress.
132
+
133
+ Downloads `.po` / `.mo` files from GlotPress into `glotpress.destination` (default `./languages/`). Locale ids must be `[a-zA-Z0-9_]+`; non-OK HTTP responses are skipped.
134
+
122
135
  ```bash
123
136
  npx wp-advads translations
124
137
  ```
125
138
 
126
- ### ⚠️ Notes
127
- A config file is required for all commands **except** init.
139
+ ## Notes
140
+
141
+ `class`, `view`, `updates`, `js`, and `css` will not overwrite an existing file unless you pass `--force`. Writes cannot escape the project root.
142
+
143
+ ## Tests
144
+
145
+ ```bash
146
+ npm test
147
+ ```
128
148
 
129
- If one is missing, you'll be prompted to create it with `wp-advads init`.
149
+ That runs `node --experimental-test-module-mocks --test`. Node 18+ is required (`engines.node`).
130
150
 
131
- `class`, `view`, `updates`, `js`, and `css` will not overwrite an existing file unless you pass `--force`.
151
+ ## Author
132
152
 
133
- ### 👤 Author
134
- Shakeeb Ahmed
135
- GitHub: [@meshakeeb](http://github.com/meshakeeb)
153
+ Shakeeb Ahmed
154
+ GitHub: [@meshakeeb](https://github.com/meshakeeb)
136
155
 
137
- ### 📖 Changelog
156
+ ## Changelog
138
157
 
139
- [See the changelog file](./CHANGELOG.md)
158
+ [CHANGELOG.md](./CHANGELOG.md)
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "wp-advads",
4
- "version": "1.0.34",
4
+ "version": "1.0.36",
5
5
  "description": "Create an Advanced Ads WordPress plugin ecosystem.",
6
6
  "author": {
7
7
  "name": "Shakeeb Ahmed",
package/src/app.js CHANGED
@@ -5,7 +5,7 @@
5
5
  */
6
6
  import chalk from 'chalk'
7
7
  import { program } from 'commander'
8
- import { readFileSync } from 'node:fs'
8
+ import { existsSync, readFileSync } from 'node:fs'
9
9
  import { dirname, join } from 'node:path'
10
10
  import { fileURLToPath } from 'node:url'
11
11
 
@@ -15,9 +15,11 @@ import { fileURLToPath } from 'node:url'
15
15
  import * as commands from './commands/index.js'
16
16
  import { configFileExists, onNewLine } from './utilities/index.js'
17
17
 
18
+ const pkgRoot = join( dirname( fileURLToPath( import.meta.url ) ), '..' )
18
19
  const { version } = JSON.parse(
19
- readFileSync( join( dirname( fileURLToPath( import.meta.url ) ), '../package.json' ), 'utf8' )
20
+ readFileSync( join( pkgRoot, 'package.json' ), 'utf8' )
20
21
  )
22
+ const fromLocal = existsSync( join( pkgRoot, '.git' ) )
21
23
 
22
24
  /**
23
25
  * App
@@ -27,8 +29,9 @@ const app = async () => {
27
29
  [
28
30
  chalk.hex('#FADC00').inverse.bold('Advanced Ads WordPress Plugin Toolkit'),
29
31
  chalk.white( 'v' + version ),
30
- chalk.dim( 'by Shakeeb Ahmed' )
31
- ].join(" ")
32
+ chalk.dim( 'by Shakeeb Ahmed' ),
33
+ fromLocal ? chalk.cyan( 'from local' ) : '',
34
+ ].filter( Boolean ).join(" ")
32
35
  );
33
36
 
34
37
  program.configureHelp({
@@ -27,8 +27,11 @@ export default function wpBuild(nextProcess = null, commitMessages = null) {
27
27
  .catch( ( err ) => {
28
28
  console.log('');
29
29
  console.log( `${logSymbols.error} ${chalk.bold.red(`We failed somewhere! Work hard mate...`)}` )
30
+ if ( err && err !== true ) {
31
+ console.log( err instanceof Error ? ( err.stack || err.message ) : String( err ) )
32
+ }
30
33
  if (typeof nextProcess === 'function') {
31
- nextProcess(true);
34
+ nextProcess(err || true);
32
35
  }
33
36
  } )
34
37
  }
@@ -52,11 +55,13 @@ function run( commitMessages ) {
52
55
 
53
56
  await runExec( 'composer install -o -a -n --no-dev --no-scripts', composerEnv )
54
57
  onSameLine(`${logSymbols.success} Installed PHP dependencies`);
55
- process.stderr.write('Composer dumping....')
58
+ process.stdout.write('Composer dumping...')
56
59
 
57
60
  await runExec( 'composer dump', composerEnv )
58
61
  onSameLine(`${logSymbols.success} Composer updated`)
59
- commitMessages?.push( 'update 3rd party dependencies' )
62
+ if ( Array.isArray( commitMessages ) ) {
63
+ commitMessages.push( 'update 3rd party dependencies' )
64
+ }
60
65
 
61
66
  process.stdout.write('Building plugin resources...')
62
67
  let buildScript
@@ -71,7 +76,9 @@ function run( commitMessages ) {
71
76
  } else {
72
77
  await runExec( 'npm run build' )
73
78
  onSameLine(`${logSymbols.success} Built plugin resources`);
74
- commitMessages?.push( 'Build plugin resources' )
79
+ if ( Array.isArray( commitMessages ) ) {
80
+ commitMessages.push( 'Build plugin resources' )
81
+ }
75
82
  }
76
83
 
77
84
  console.log('')
@@ -85,7 +92,9 @@ function runExec( cmd, options ) {
85
92
  return new Promise( ( resolve, reject ) => {
86
93
  execCommand( cmd, ( stdout, error, stderr ) => {
87
94
  if ( error ) {
88
- onSameLine(`${logSymbols.error} ${stderr || error.message}`)
95
+ const detail = [ stderr && stderr.trim(), error.message ].filter( Boolean ).join( '\n' )
96
+ onSameLine(`${logSymbols.error} ${detail}`)
97
+ error.message = detail
89
98
  reject( error )
90
99
  return
91
100
  }
@@ -22,34 +22,46 @@ const LOCALES_CHANGELOG = {
22
22
  *
23
23
  * @param {Array} list List of languages downloaded
24
24
  *
25
- * @returns {Array}
25
+ * @returns {string}
26
26
  */
27
27
  function naturalLanguageJoin(list) {
28
- const last = list.pop()
28
+ if ( list.length === 0 ) {
29
+ return ''
30
+ }
29
31
 
30
- if ( list.length > 0 ) {
31
- return `${list.join(', ')} and ${last}`
32
+ if ( list.length === 1 ) {
33
+ return list[0]
32
34
  }
33
35
 
34
- return last
36
+ return `${list.slice( 0, -1 ).join(', ')} and ${list[ list.length - 1 ]}`
37
+ }
38
+
39
+ function normalizeGitPath( file ) {
40
+ return String( file || '' )
41
+ .replace( /\\/g, '/' )
42
+ .replace( /^\.\//, '' )
43
+ .replace( /\/+/g, '/' )
35
44
  }
36
45
 
37
46
  /**
38
47
  * Loop through downloaded locales and categorize them as added or updated
39
48
  *
40
- * @param {Array} locales Locales downloaded from GlotPress
49
+ * @param {Object} locales Locales downloaded from GlotPress
41
50
  * @param {Array} files Files currently in branch
42
51
  *
43
- * @returns {Array}
52
+ * @returns {Object}
44
53
  */
45
54
  function loopLocales( locales, files ) {
46
55
  const changes = { updated: [], added: [] }
47
- const localeFiles = Object.keys(locales)
56
+ const tracked = new Set( files.map( normalizeGitPath ).filter( Boolean ) )
48
57
 
49
- localeFiles.map( (locale) => {
50
- const action = files.includes(locale) ? 'updated' : 'added'
51
- changes[ action ].push(locales[ locale ])
52
- })
58
+ for ( const [ file, name ] of Object.entries( locales || {} ) ) {
59
+ const action = tracked.has( normalizeGitPath( file ) ) ? 'updated' : 'added'
60
+ const label = name || file.split( '/' ).pop()
61
+ if ( label ) {
62
+ changes[ action ].push( label )
63
+ }
64
+ }
53
65
 
54
66
  changes.added = [... new Set(changes.added)].sort()
55
67
  changes.updated = [... new Set(changes.updated)].sort()
@@ -70,7 +82,12 @@ function writeLocalesChangelog(type, changes) {
70
82
  return
71
83
  }
72
84
 
73
- const message = LOCALES_CHANGELOG[type].message.replace('%s', naturalLanguageJoin(changes))
85
+ const message = naturalLanguageJoin(changes)
86
+ if ( ! message ) {
87
+ return
88
+ }
89
+
90
+ const line = LOCALES_CHANGELOG[type].message.replace('%s', message)
74
91
 
75
92
  if (!fs.existsSync('./.changelog')) {
76
93
  fs.mkdirSync('./.changelog');
@@ -78,7 +95,7 @@ function writeLocalesChangelog(type, changes) {
78
95
 
79
96
  fs.writeFileSync(
80
97
  `./.changelog/${LOCALES_CHANGELOG[type].file}`,
81
- message
98
+ line
82
99
  )
83
100
  }
84
101
 
@@ -91,7 +108,7 @@ function updateLocalesChangelog(next) {
91
108
  write('Generating translations changelog...')
92
109
 
93
110
  execCommand( [ 'git', 'ls-files', '--', destination ], function(result) {
94
- const files = result.split('\n').slice(0,-1)
111
+ const files = String( result || '' ).split('\n').map( ( line ) => line.trim() ).filter( Boolean )
95
112
  const changes = loopLocales(pluginData.locales, files)
96
113
 
97
114
  writeLocalesChangelog('added', changes.added)
@@ -3,7 +3,7 @@
3
3
  */
4
4
  import fs from 'fs';
5
5
  import chalk from 'chalk';
6
- import { resolve } from 'path';
6
+ import { join, relative, resolve } from 'path';
7
7
  import { heading, msgError, getSetting, write, msgSuccessOnSameLine, onSameLine, msgErrorOnSameLine } from "../utilities/index.js";
8
8
 
9
9
  const BASE_URL = 'https://translate.wpadvancedads.com/api/projects';
@@ -75,15 +75,15 @@ async function downloadAll( ctx ) {
75
75
  console.log( '' );
76
76
  console.log(
77
77
  chalk.yellow.bold( 'Updating Language:' ) +
78
- ` ${chalk.italic( set.name )}`
78
+ ` ${chalk.italic( set.name || locale )}`
79
79
  );
80
80
 
81
81
  for ( const format of [ 'mo', 'po' ] ) {
82
82
  const target = await downloadFile( ctx, locale, set, format );
83
83
  if ( target ) {
84
- const file = target.replace( /^\.\//, '' );
84
+ const file = relative( process.cwd(), resolve( target ) ).replace( /\\/g, '/' );
85
85
  files.push( file );
86
- names[ file ] = set.name;
86
+ names[ file ] = set.name || locale;
87
87
  }
88
88
  }
89
89
  }
@@ -142,7 +142,7 @@ async function downloadFile(ctx, locale, data, format) {
142
142
  chalk.bold(`Downloading: ${ctx.potPrefix}-${locale}.${format}`)
143
143
  );
144
144
 
145
- const target = `${ctx.destination}/${ctx.potPrefix}-${locale}.${format}`;
145
+ const target = join( ctx.destination, `${ctx.potPrefix}-${locale}.${format}` );
146
146
  const endpoint = `${BASE_URL}/${ctx.project}/${data.locale}/${data.slug}/export-translations?format=${format}`;
147
147
 
148
148
  const response = await fetch(endpoint, {
@@ -1,10 +1,10 @@
1
1
  /**
2
2
  * Internal Dependencies
3
3
  */
4
- import { heading, msgSuccess, runCommand, getSetting } from "../utilities/index.js"
4
+ import { write, msgSuccessOnSameLine, msgErrorOnSameLine, runCommand, getSetting } from "../utilities/index.js"
5
5
 
6
6
  export default (next = null) => {
7
- heading('Generating POT file...')
7
+ write('Generating POT file...')
8
8
 
9
9
  const settings = getSetting()
10
10
 
@@ -38,15 +38,35 @@ export default (next = null) => {
38
38
  `--domain=${domain}`,
39
39
  `--exclude=${exclude.join(',')}`,
40
40
  `--headers=${JSON.stringify(headers)}`,
41
- ], ( error ) => {
41
+ ], ( error, stdout, stderr ) => {
42
42
  if ( error ) {
43
+ msgErrorOnSameLine(error.message)
43
44
  if ( next ) {
44
45
  next( error )
45
46
  }
46
47
  return
47
48
  }
48
49
 
49
- msgSuccess('POT file generated successfully');
50
+ msgSuccessOnSameLine('POT file generated successfully');
51
+ printPotWarnings( stderr, stdout )
50
52
  if (next) next();
51
53
  } );
52
54
  }
55
+
56
+ function printPotWarnings( stderr, stdout ) {
57
+ const warnings = [ stderr, stdout ]
58
+ .filter( Boolean )
59
+ .join( '\n' )
60
+ .split( /\r?\n/ )
61
+ .map( ( line ) => line.trim() )
62
+ .filter( ( line ) => /warning:/i.test( line ) )
63
+
64
+ if ( ! warnings.length ) {
65
+ return
66
+ }
67
+
68
+ console.log( '' )
69
+ for ( const line of warnings ) {
70
+ console.log( line )
71
+ }
72
+ }
@@ -3,11 +3,6 @@
3
3
  */
4
4
  import { spawn } from 'node:child_process'
5
5
 
6
- /**
7
- * Internal Dependencies
8
- */
9
- import { onSameLine } from './formatting.js'
10
-
11
6
  /**
12
7
  * Run a command in the terminal.
13
8
  *
@@ -18,39 +13,12 @@ import { onSameLine } from './formatting.js'
18
13
  * @returns {void}
19
14
  */
20
15
  export function runCommand( command, args, callback ) {
21
- const commandSpawn = spawn( command, args, {
22
- shell: false,
23
- stdio: [ 'inherit', 'pipe', 'pipe' ]
24
- } )
25
-
26
- let finished = false
27
- const done = ( err ) => {
28
- if ( finished ) {
29
- return
30
- }
31
-
32
- finished = true
33
- callback( err )
34
- }
35
-
36
- commandSpawn.stdout.on( 'data', (data) => {
37
- onSameLine(data.toString())
38
- })
39
-
40
- commandSpawn.stderr.on( 'data', (data) => {
41
- onSameLine(data.toString().replace('\n','').replace('\r','').trim())
42
- })
43
-
44
- commandSpawn.on( 'error', done )
45
-
46
- commandSpawn.on( 'close', (code) => {
47
- if ( code !== 0 ) {
48
- done( new Error( `Command failed with exit code ${code}` ) )
49
- return
50
- }
51
-
52
- done()
53
- })
16
+ execCommand( [ command, ...args ], ( stdout, error, stderr ) => {
17
+ if ( error ) {
18
+ error.message = [ stderr && stderr.trim(), error.message ].filter( Boolean ).join( '\n' )
19
+ }
20
+ callback( error, stdout, stderr )
21
+ } )
54
22
  }
55
23
 
56
24
  /**
@@ -86,15 +54,11 @@ export function execCommand( command, callback, options = {} ) {
86
54
  }
87
55
 
88
56
  child.stdout.on( 'data', ( data ) => {
89
- const text = data.toString()
90
- stdout += text
91
- onSameLine( text )
57
+ stdout += data.toString()
92
58
  } )
93
59
 
94
60
  child.stderr.on( 'data', ( data ) => {
95
- const text = data.toString()
96
- stderr += text
97
- onSameLine( text.replace( '\n', '' ).replace( '\r', '' ).trim() )
61
+ stderr += data.toString()
98
62
  } )
99
63
 
100
64
  child.on( 'error', done )
@@ -13,6 +13,7 @@ import fs from 'node:fs'
13
13
  const execCalls = []
14
14
  let execImpl = ( cmd, cb ) => cb( '', null, '' )
15
15
  let potCalled = false
16
+ let potImpl = ( next ) => next()
16
17
 
17
18
  mock.module( import.meta.resolve( '../src/utilities/shell.js' ), {
18
19
  exports: {
@@ -28,7 +29,7 @@ mock.module( import.meta.resolve( '../src/commands/wp-pot.js' ), {
28
29
  exports: {
29
30
  default( next ) {
30
31
  potCalled = true
31
- next()
32
+ potImpl( next )
32
33
  },
33
34
  },
34
35
  } )
@@ -63,6 +64,7 @@ beforeEach( () => {
63
64
  execCalls.length = 0
64
65
  potCalled = false
65
66
  execImpl = ( cmd, cb ) => cb( '', null, '' )
67
+ potImpl = ( next ) => next()
66
68
  fs.rmSync.mock.mockImplementation( () => {} )
67
69
  pluginData.commitMessages = []
68
70
  } )
@@ -166,3 +168,24 @@ test( 'build appends messages to an optional sink', async () => {
166
168
  } )
167
169
  assert.ok( sink.includes( 'update 3rd party dependencies' ) )
168
170
  } )
171
+
172
+ test( 'build ignores a non-array commitMessages sink', async () => {
173
+ await new Promise( ( resolve, reject ) => {
174
+ wpBuild( ( err ) => err ? reject( err ) : resolve(), { name: () => 'build' } )
175
+ } )
176
+ } )
177
+
178
+ test( 'build failure prints the rejected error', async () => {
179
+ const lines = []
180
+ mock.method( console, 'log', ( ...args ) => {
181
+ lines.push( args.map( String ).join( ' ' ) )
182
+ } )
183
+ potImpl = ( next ) => next( new Error( 'make-pot failed: wp: command not found' ) )
184
+
185
+ await runBuild()
186
+
187
+ assert.ok(
188
+ lines.some( ( line ) => line.includes( 'make-pot failed: wp: command not found' ) ),
189
+ `expected the pot error in logs, got:\n${lines.join( '\n' )}`
190
+ )
191
+ } )
@@ -3,6 +3,7 @@
3
3
  * Run with --test-isolation=none --experimental-test-module-mocks
4
4
  */
5
5
  import assert from 'node:assert/strict'
6
+ import { readFileSync } from 'node:fs'
6
7
  import { mkdir } from 'node:fs/promises'
7
8
  import { mkdtemp, rm } from 'node:fs/promises'
8
9
  import { tmpdir } from 'node:os'
@@ -10,12 +11,13 @@ import { join } from 'node:path'
10
11
  import { mock, test } from 'node:test'
11
12
 
12
13
  const execCalls = []
14
+ let gitFiles = 'languages/myplugin-de_DE.po\n'
13
15
 
14
16
  mock.module( import.meta.resolve( '../src/utilities/shell.js' ), {
15
17
  exports: {
16
18
  execCommand( cmd, cb ) {
17
19
  execCalls.push( cmd )
18
- cb( 'languages/myplugin-de_DE.po\n', null, '' )
20
+ cb( gitFiles, null, '' )
19
21
  },
20
22
  runCommand() {},
21
23
  },
@@ -24,26 +26,68 @@ mock.module( import.meta.resolve( '../src/utilities/shell.js' ), {
24
26
  const { pluginData } = await import( '../src/commands/release.js' )
25
27
  const { default: updateChangelog } = await import( '../src/commands/release/changelog.js' )
26
28
 
27
- test( 'updateLocalesChangelog uses glotpress.destination instead of unset pluginData.glotpress', async () => {
29
+ async function withChangelog( run ) {
28
30
  const dir = await mkdtemp( join( tmpdir(), 'wp-advads-changelog-' ) )
29
31
  const cwd = process.cwd()
30
32
  process.chdir( dir )
31
33
  await mkdir( join( dir, '.changelog' ) )
32
-
33
34
  pluginData.semver = { preRelease: true, getNextVersionString: () => '1.2.3' }
34
- pluginData.locales = { 'languages/myplugin-de_DE.po': 'German' }
35
35
  delete pluginData.glotpress
36
36
  execCalls.length = 0
37
37
 
38
38
  try {
39
+ return await run( dir )
40
+ } finally {
41
+ process.chdir( cwd )
42
+ await rm( dir, { recursive: true, force: true } )
43
+ }
44
+ }
45
+
46
+ function changelog( dir, file ) {
47
+ return readFileSync( join( dir, '.changelog', file ), 'utf8' )
48
+ }
49
+
50
+ test( 'updateLocalesChangelog lists updated languages from glotpress.destination', async () => {
51
+ gitFiles = [
52
+ 'languages/myplugin-de_DE.po',
53
+ 'languages/myplugin-de_DE.mo',
54
+ 'languages/myplugin-fr_FR.po',
55
+ ].join( '\n' ) + '\n'
56
+ pluginData.locales = {
57
+ 'languages/myplugin-de_DE.po': 'German',
58
+ 'languages/myplugin-de_DE.mo': 'German',
59
+ 'languages/myplugin-fr_FR.po': 'French (France)',
60
+ 'languages/myplugin-it_IT.po': 'Italian',
61
+ }
62
+
63
+ await withChangelog( async ( dir ) => {
39
64
  const err = await new Promise( ( resolve ) => {
40
65
  updateChangelog( ( error ) => resolve( error ) )
41
66
  } )
42
67
 
43
68
  assert.equal( err, undefined )
44
69
  assert.deepEqual( execCalls[ 0 ], [ 'git', 'ls-files', '--', './languages/' ] )
45
- } finally {
46
- process.chdir( cwd )
47
- await rm( dir, { recursive: true, force: true } )
48
- }
70
+ assert.match( changelog( dir, 'improvement-102' ), /German/ )
71
+ assert.match( changelog( dir, 'improvement-102' ), /French \(France\)/ )
72
+ assert.doesNotMatch( changelog( dir, 'improvement-102' ), /undefined/ )
73
+ assert.match( changelog( dir, 'improvement-101' ), /Italian/ )
74
+ assert.doesNotMatch( changelog( dir, 'improvement-101' ), /undefined/ )
75
+ } )
76
+
77
+ gitFiles = 'languages/myplugin-de_DE.po\n'
78
+ pluginData.locales = { 'languages//myplugin-de_DE.po': 'German' }
79
+
80
+ await withChangelog( async ( dir ) => {
81
+ await new Promise( ( resolve ) => updateChangelog( resolve ) )
82
+ assert.match( changelog( dir, 'improvement-102' ), /update German translations/ )
83
+ } )
84
+
85
+ pluginData.locales = { 'languages/myplugin-de_DE.po': undefined }
86
+
87
+ await withChangelog( async ( dir ) => {
88
+ await new Promise( ( resolve ) => updateChangelog( resolve ) )
89
+ const updated = changelog( dir, 'improvement-102' )
90
+ assert.doesNotMatch( updated, /undefined/ )
91
+ assert.match( updated, /myplugin-de_DE\.po/ )
92
+ } )
49
93
  } )
@@ -9,13 +9,14 @@ import { join } from 'node:path'
9
9
  import { mock, test } from 'node:test'
10
10
 
11
11
  const potArgs = []
12
+ let potStderr = ''
12
13
 
13
14
  mock.module( import.meta.resolve( '../src/utilities/shell.js' ), {
14
15
  exports: {
15
16
  execCommand() {},
16
17
  runCommand( cmd, args, cb ) {
17
18
  potArgs.push( { cmd, args } )
18
- cb()
19
+ cb( null, '', potStderr )
19
20
  },
20
21
  },
21
22
  } )
@@ -42,6 +43,24 @@ test( 'pot Last-Translator and Language-Team use author from config', async () =
42
43
  assert.equal( headers['Last-Translator'], 'Jane Doe <jane@example.com>' )
43
44
  assert.equal( headers['Language-Team'], 'Jane Doe <jane@example.com>' )
44
45
  assert.doesNotMatch( headersArg, /Shakeeb Ahmed/ )
46
+
47
+ potStderr = 'Warning: The string "Hello" is missing a translator comment.'
48
+ const lines = []
49
+ const originalLog = console.log
50
+ console.log = ( msg ) => {
51
+ lines.push( String( msg ) )
52
+ }
53
+ try {
54
+ await new Promise( ( resolve ) => wpPot( resolve ) )
55
+ } finally {
56
+ console.log = originalLog
57
+ potStderr = ''
58
+ }
59
+
60
+ assert.ok(
61
+ lines.some( ( line ) => line.includes( 'Warning: The string "Hello" is missing a translator comment.' ) ),
62
+ `expected make-pot warnings in logs, got:\n${lines.join( '\n' )}`
63
+ )
45
64
  } finally {
46
65
  process.chdir( cwd )
47
66
  await rm( dir, { recursive: true, force: true } )
@@ -30,7 +30,7 @@ test( 'runCommand calls back with an error on non-zero exit', async () => {
30
30
  assert.ok( error, 'expected an error on exit 1' )
31
31
  } )
32
32
 
33
- test( 'execCommand prints stderr when the command fails', async () => {
33
+ test( 'runCommand does not print stdout or stderr when the command succeeds', async () => {
34
34
  const lines = []
35
35
  const originalLog = console.log
36
36
  console.log = ( msg ) => {
@@ -38,14 +38,46 @@ test( 'execCommand prints stderr when the command fails', async () => {
38
38
  }
39
39
 
40
40
  try {
41
- await execOnce( 'node -e "process.stderr.write(\'boom-fail\'); process.exit(1)"' )
41
+ const error = await runOnce( 'node', [
42
+ '-e',
43
+ 'process.stdout.write("make-pot progress"); process.stderr.write("scanning files")',
44
+ ] )
45
+ assert.ok( ! error )
46
+ } finally {
47
+ console.log = originalLog
48
+ }
49
+
50
+ assert.equal(
51
+ lines.some( ( line ) => line.includes( 'make-pot progress' ) || line.includes( 'scanning files' ) ),
52
+ false
53
+ )
54
+ } )
55
+
56
+ test( 'execCommand returns stderr when the command fails', async () => {
57
+ const { error, stderr } = await execOnce( 'node -e "process.stderr.write(\'boom-fail\'); process.exit(1)"' )
58
+ assert.ok( error )
59
+ assert.match( stderr, /boom-fail/ )
60
+ } )
61
+
62
+ test( 'execCommand does not print stdout or stderr when the command succeeds', async () => {
63
+ const lines = []
64
+ const originalLog = console.log
65
+ console.log = ( msg ) => {
66
+ lines.push( String( msg ) )
67
+ }
68
+
69
+ try {
70
+ const { error } = await execOnce(
71
+ 'node -e "process.stdout.write(\'Installing dependencies from lock file\'); process.stderr.write(\'Extracting archive\')"'
72
+ )
73
+ assert.equal( error, null )
42
74
  } finally {
43
75
  console.log = originalLog
44
76
  }
45
77
 
46
- assert.ok(
47
- lines.some( ( line ) => line.includes( 'boom-fail' ) ),
48
- 'expected stderr to be printed on failure'
78
+ assert.equal(
79
+ lines.some( ( line ) => line.includes( 'Installing dependencies from lock file' ) || line.includes( 'Extracting archive' ) ),
80
+ false
49
81
  )
50
82
  } )
51
83
 
@@ -20,3 +20,11 @@ test( 'wp-advads --version matches package.json and is not hardcoded 2.0.0', asy
20
20
  assert.match( stdout, new RegExp( `\\b${version}\\b` ) )
21
21
  assert.doesNotMatch( stdout, /v2\.0\.0/ )
22
22
  } )
23
+
24
+ test( 'banner says from local when the CLI is a git checkout', async () => {
25
+ const { stdout } = await execFileAsync( process.execPath, [ 'src/app.js', '--version' ], {
26
+ cwd: root,
27
+ } )
28
+
29
+ assert.match( stdout, /from local/ )
30
+ } )