wp-advads 1.0.31 → 1.0.34

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 +8 -7
  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 -13
  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,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
+ } )
@@ -0,0 +1,47 @@
1
+ /**
2
+ * TASK-008: class -i must still win when --heading is also set.
3
+ */
4
+ import assert from 'node:assert/strict'
5
+ import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
6
+ import { tmpdir } from 'node:os'
7
+ import { join } from 'node:path'
8
+ import { mock, test } from 'node:test'
9
+
10
+ const runCalls = []
11
+
12
+ mock.module( import.meta.resolve( '../src/utilities/shell.js' ), {
13
+ exports: {
14
+ execCommand() {},
15
+ runCommand( ...args ) {
16
+ runCalls.push( args )
17
+ },
18
+ },
19
+ } )
20
+
21
+ const { default: classFile } = await import( '../src/commands/class.js' )
22
+
23
+ test( 'class command uses the initializer template when heading and -i are both set', async () => {
24
+ const dir = await mkdtemp( join( tmpdir(), 'wp-advads-class-' ) )
25
+ const cwd = process.cwd()
26
+ process.chdir( dir )
27
+
28
+ try {
29
+ await writeFile( join( dir, 'wp.advads' ), JSON.stringify( {
30
+ paths: { php: 'includes' },
31
+ wp: { version: '1.0.0' },
32
+ misc: { package: 'AdvancedAds' },
33
+ author: { name: 'Test', email: 'test@example.com' },
34
+ } ) )
35
+
36
+ classFile( 'Foo_Bar', { heading: 'Class Heading', i: true } )
37
+
38
+ const php = await readFile( join( dir, 'includes', 'class-foo-bar.php' ), 'utf8' )
39
+ assert.match( php, /Initializer_Interface/ )
40
+ assert.match( php, /Class Heading/ )
41
+ assert.doesNotMatch( php, /\{\{/ )
42
+ assert.equal( runCalls.length, 0 )
43
+ } finally {
44
+ process.chdir( cwd )
45
+ await rm( dir, { recursive: true, force: true } )
46
+ }
47
+ } )
@@ -0,0 +1,70 @@
1
+ /**
2
+ * TASK-033 / TASK-058: stubs compile; view/update use init config plus command fields.
3
+ */
4
+ import assert from 'node:assert/strict'
5
+ import { readdirSync } from 'node:fs'
6
+ import { join } from 'node:path'
7
+ import { test } from 'node:test'
8
+ import { compileTemplate, getTemplateFolder } from '../src/utilities/filesystem.js'
9
+
10
+ const fixture = {
11
+ heading: 'Heading',
12
+ description: 'Description',
13
+ className: 'Foo_Bar',
14
+ namespace: '',
15
+ prefix: 'advads',
16
+ version: '1_2_3',
17
+ wp: { version: '1.0.0' },
18
+ misc: { package: 'AdvancedAds', prefix: 'advads' },
19
+ author: { name: 'Test', email: 'test@example.com' },
20
+ }
21
+
22
+ const initShape = {
23
+ wp: { version: '1.0.0', description: 'Plugin description' },
24
+ misc: { package: 'AdvancedAds', prefix: 'advads' },
25
+ author: { name: 'Test', email: 'test@example.com' },
26
+ }
27
+
28
+ test( 'every template/files stub compiles without leftover placeholders', () => {
29
+ const dir = join( getTemplateFolder(), 'files' )
30
+ const files = readdirSync( dir ).filter( ( file ) => ! file.startsWith( '.' ) )
31
+
32
+ assert.ok( files.includes( 'view.php' ) )
33
+ assert.ok( files.includes( 'update.php' ) )
34
+
35
+ for ( const file of files ) {
36
+ const out = compileTemplate( join( dir, file ), fixture )
37
+ assert.doesNotMatch( out, /\{\{/, file )
38
+ }
39
+ } )
40
+
41
+ test( 'view.php does not compile from init config without a description field', () => {
42
+ assert.throws(
43
+ () => compileTemplate( join( getTemplateFolder(), 'files/view.php' ), {
44
+ ...initShape,
45
+ heading: 'Sidebar template file',
46
+ } ),
47
+ /description/i
48
+ )
49
+ } )
50
+
51
+ test( 'view and update compile from init config plus command fields', () => {
52
+ const dir = join( getTemplateFolder(), 'files' )
53
+
54
+ const view = compileTemplate( join( dir, 'view.php' ), {
55
+ ...initShape,
56
+ heading: 'Sidebar template file',
57
+ description: 'Sidebar template file',
58
+ } )
59
+ assert.doesNotMatch( view, /\{\{/ )
60
+
61
+ const update = compileTemplate( join( dir, 'update.php' ), {
62
+ ...initShape,
63
+ heading: 'Update routine for version 1.2.3',
64
+ description: 'Update routine for version 1.2.3',
65
+ prefix: initShape.misc.prefix,
66
+ version: '1_2_3',
67
+ } )
68
+ assert.doesNotMatch( update, /\{\{/ )
69
+ assert.match( update, /function advads_upgrade_1_2_3/ )
70
+ } )
@@ -0,0 +1,46 @@
1
+ /**
2
+ * TASK-043 / 044 / 048 / 049 / 050: ignore junk, no jquery stub, lodash only for template, no async pkg, no deleteFile.
3
+ */
4
+ import assert from 'node:assert/strict'
5
+ import { readFileSync } from 'node:fs'
6
+ import { test } from 'node:test'
7
+
8
+ test( 'gitignore ignores .DS_Store', () => {
9
+ const gitignore = readFileSync( new URL( '../.gitignore', import.meta.url ), 'utf8' )
10
+ assert.match( gitignore, /^\.DS_Store$/m )
11
+ } )
12
+
13
+ test( 'javascript stub does not import jquery', () => {
14
+ const js = readFileSync( new URL( '../template/files/javascript.js', import.meta.url ), 'utf8' )
15
+ assert.doesNotMatch( js, /jquery/i )
16
+ } )
17
+
18
+ test( 'lodash is only used for template compile', () => {
19
+ const pkg = JSON.parse( readFileSync( new URL( '../package.json', import.meta.url ), 'utf8' ) )
20
+ assert.ok( pkg.dependencies.lodash )
21
+ assert.doesNotMatch( readFileSync( new URL( '../src/utilities/settings.js', import.meta.url ), 'utf8' ), /lodash/ )
22
+ assert.doesNotMatch( readFileSync( new URL( '../src/utilities/misc.js', import.meta.url ), 'utf8' ), /lodash/ )
23
+ assert.doesNotMatch( readFileSync( new URL( '../src/commands/init.js', import.meta.url ), 'utf8' ), /lodash/ )
24
+ assert.match( readFileSync( new URL( '../src/utilities/filesystem.js', import.meta.url ), 'utf8' ), /lodash\/template/ )
25
+ } )
26
+
27
+ test( 'package.json does not depend on async', () => {
28
+ const pkg = JSON.parse( readFileSync( new URL( '../package.json', import.meta.url ), 'utf8' ) )
29
+ assert.equal( pkg.dependencies.async, undefined )
30
+ } )
31
+
32
+ test( 'filesystem does not export deleteFile', () => {
33
+ const src = readFileSync( new URL( '../src/utilities/filesystem.js', import.meta.url ), 'utf8' )
34
+ assert.doesNotMatch( src, /deleteFile/ )
35
+ } )
36
+
37
+ test( 'scaffold commands share writeFromTemplate', () => {
38
+ assert.match( readFileSync( new URL( '../src/utilities/filesystem.js', import.meta.url ), 'utf8' ), /export function writeFromTemplate/ )
39
+ for ( const file of [ 'css.js', 'javascript.js', 'views.js', 'class.js', 'updates.js' ] ) {
40
+ assert.match(
41
+ readFileSync( new URL( `../src/commands/${file}`, import.meta.url ), 'utf8' ),
42
+ /writeFromTemplate/,
43
+ file
44
+ )
45
+ }
46
+ } )
@@ -0,0 +1,11 @@
1
+ /**
2
+ * TASK-042: npm should warn on Node older than 18.
3
+ */
4
+ import assert from 'node:assert/strict'
5
+ import { readFileSync } from 'node:fs'
6
+ import { test } from 'node:test'
7
+
8
+ test( 'package.json engines.node is >=18', () => {
9
+ const pkg = JSON.parse( readFileSync( new URL( '../package.json', import.meta.url ), 'utf8' ) )
10
+ assert.equal( pkg.engines?.node, '>=18' )
11
+ } )
@@ -0,0 +1,51 @@
1
+ /**
2
+ * TASK-054: getLatestTag must treat execCommand error; only "No names found" falls back.
3
+ */
4
+ import assert from 'node:assert/strict'
5
+ import { mkdtemp, rm, writeFile } from 'node:fs/promises'
6
+ import { tmpdir } from 'node:os'
7
+ import { join } from 'node:path'
8
+ import { mock, test } from 'node:test'
9
+
10
+ let execImpl = ( cmd, cb ) => cb( '', null, '' )
11
+
12
+ mock.module( import.meta.resolve( '../src/utilities/shell.js' ), {
13
+ exports: {
14
+ execCommand( cmd, cb ) {
15
+ execImpl( cmd, cb )
16
+ },
17
+ runCommand() {},
18
+ },
19
+ } )
20
+
21
+ const { getLatestTag } = await import( '../src/commands/release/git.js' )
22
+
23
+ test( 'getLatestTag rejects when git describe errors', async () => {
24
+ execImpl = ( cmd, cb ) => cb( '', new Error( 'failed' ), 'fatal: not a git repository' )
25
+
26
+ await assert.rejects( getLatestTag() )
27
+ } )
28
+
29
+ test( 'getLatestTag uses the described tag when git succeeds', async () => {
30
+ execImpl = ( cmd, cb ) => cb( 'v1.2.3\n', null, '' )
31
+
32
+ assert.equal( await getLatestTag(), '1.2.3' )
33
+ } )
34
+
35
+ test( 'getLatestTag falls back to wp.version when there are no tags', async () => {
36
+ execImpl = ( cmd, cb ) => {
37
+ cb( '', new Error( 'failed' ), 'fatal: No names found, cannot describe anything.' )
38
+ }
39
+
40
+ const dir = await mkdtemp( join( tmpdir(), 'wp-advads-tag-' ) )
41
+ const cwd = process.cwd()
42
+ await writeFile( join( dir, 'wp.advads' ), JSON.stringify( { wp: { version: '3.4.5' } } ) )
43
+ process.chdir( dir )
44
+
45
+ try {
46
+ assert.equal( await getLatestTag(), '3.4.5' )
47
+ } finally {
48
+ process.chdir( cwd )
49
+ await rm( dir, { recursive: true, force: true } )
50
+ }
51
+ } )
@@ -0,0 +1,44 @@
1
+ /**
2
+ * TASK-055: hasPermission / gitFetch must treat error first and not crash on non-string stderr.
3
+ */
4
+ import assert from 'node:assert/strict'
5
+ import { mock, test } from 'node:test'
6
+
7
+ let execImpl = ( cmd, cb ) => cb( '', null, '' )
8
+
9
+ mock.module( import.meta.resolve( '../src/utilities/shell.js' ), {
10
+ exports: {
11
+ execCommand( cmd, cb ) {
12
+ execImpl( cmd, cb )
13
+ },
14
+ runCommand() {},
15
+ },
16
+ } )
17
+
18
+ const { hasPermission, gitFetch } = await import( '../src/commands/release/git.js' )
19
+
20
+ function run( fn ) {
21
+ return new Promise( ( resolve, reject ) => {
22
+ try {
23
+ fn( ( err ) => resolve( err ) )
24
+ } catch ( err ) {
25
+ reject( err )
26
+ }
27
+ } )
28
+ }
29
+
30
+ test( 'hasPermission fails closed when error is set and stderr is missing', async () => {
31
+ execImpl = ( cmd, cb ) => cb( '', new Error( 'spawn failed' ), undefined )
32
+
33
+ const err = await run( hasPermission )
34
+
35
+ assert.ok( err )
36
+ } )
37
+
38
+ test( 'gitFetch fails closed when error is set and stderr is empty', async () => {
39
+ execImpl = ( cmd, cb ) => cb( '', new Error( 'spawn failed' ), '' )
40
+
41
+ const err = await run( gitFetch )
42
+
43
+ assert.ok( err )
44
+ } )