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
@@ -0,0 +1,93 @@
1
+ /**
2
+ * TASK-004: githubFinalTasks must add an explicit file list, run hooks, and not push on failure.
3
+ */
4
+ import assert from 'node:assert/strict'
5
+ import { mkdtemp, rm, writeFile, mkdir } from 'node:fs/promises'
6
+ import { tmpdir } from 'node:os'
7
+ import { join } from 'node:path'
8
+ import { mock, test, beforeEach } from 'node:test'
9
+
10
+ const execCalls = []
11
+ let execImpl = ( cmd, cb ) => cb( '', null, '' )
12
+
13
+ mock.module( import.meta.resolve( '../src/utilities/shell.js' ), {
14
+ exports: {
15
+ execCommand( cmd, cb, options = {} ) {
16
+ execCalls.push( typeof cmd === 'string' ? cmd : cmd.join( ' ' ) )
17
+ execImpl( cmd, cb, options )
18
+ },
19
+ runCommand() {},
20
+ },
21
+ } )
22
+
23
+ const { pluginData } = await import( '../src/commands/release.js' )
24
+ const { githubFinalTasks } = await import( '../src/commands/release/github.js' )
25
+
26
+ function runFinal() {
27
+ return new Promise( ( resolve ) => {
28
+ githubFinalTasks( ( err ) => resolve( err ) )
29
+ } )
30
+ }
31
+
32
+ beforeEach( () => {
33
+ execCalls.length = 0
34
+ execImpl = ( cmd, cb ) => cb( '', null, '' )
35
+ pluginData.semver = { getNextVersionString: () => '1.2.3' }
36
+ pluginData.commitMessages = [ 'update readme.txt' ]
37
+ } )
38
+
39
+ async function withReleaseFiles( run ) {
40
+ const dir = await mkdtemp( join( tmpdir(), 'wp-advads-' ) )
41
+ const cwd = process.cwd()
42
+ await writeFile( join( dir, 'wp.advads' ), '{}' )
43
+ await writeFile( join( dir, 'changelog.txt' ), '== Changelog ==\n' )
44
+ await writeFile( join( dir, 'readme.txt' ), 'Stable tag: 1.0.0\n' )
45
+ await writeFile( join( dir, 'advanced-ads.php' ), '<?php\n' )
46
+ await mkdir( join( dir, 'languages' ) )
47
+ process.chdir( dir )
48
+ try {
49
+ return await run()
50
+ } finally {
51
+ process.chdir( cwd )
52
+ await rm( dir, { recursive: true, force: true } )
53
+ }
54
+ }
55
+
56
+ test( 'does not git add the whole working tree', async () => {
57
+ await withReleaseFiles( async () => {
58
+ await runFinal()
59
+ assert.equal(
60
+ execCalls.some( ( cmd ) => /(^| )git add \.( |$)/.test( cmd ) ),
61
+ false
62
+ )
63
+ } )
64
+ } )
65
+
66
+ test( 'commit runs hooks instead of --no-verify', async () => {
67
+ await withReleaseFiles( async () => {
68
+ await runFinal()
69
+ const commit = execCalls.find( ( cmd ) => cmd.includes( 'git commit' ) )
70
+ assert.ok( commit, 'expected a git commit' )
71
+ assert.equal( commit.includes( '--no-verify' ), false )
72
+ } )
73
+ } )
74
+
75
+ test( 'does not push when commit fails', async () => {
76
+ await withReleaseFiles( async () => {
77
+ execImpl = ( cmd, cb ) => {
78
+ const line = typeof cmd === 'string' ? cmd : cmd.join( ' ' )
79
+ if ( line.includes( 'git commit' ) ) {
80
+ cb( '', new Error( 'hook failed' ), 'phpcs' )
81
+ return
82
+ }
83
+ cb( '', null, '' )
84
+ }
85
+
86
+ const err = await runFinal()
87
+ assert.ok( err )
88
+ assert.equal(
89
+ execCalls.some( ( cmd ) => cmd.includes( 'git push' ) ),
90
+ false
91
+ )
92
+ } )
93
+ } )
@@ -0,0 +1,176 @@
1
+ /**
2
+ * TASK-011: GlotPress downloader returns { files, locales } in-process.
3
+ * Run with --test-isolation=none
4
+ */
5
+ import assert from 'node:assert/strict'
6
+ import { existsSync, readFileSync } from 'node:fs'
7
+ import { mkdtemp, rm } from 'node:fs/promises'
8
+ import { tmpdir } from 'node:os'
9
+ import { join } from 'node:path'
10
+ import { test } from 'node:test'
11
+ import { saveConfig } from '../src/utilities/settings.js'
12
+ import { downloadTranslations } from '../src/commands/wp-glotpress.js'
13
+
14
+ test( 'glotpress downloader is a function, not a class', () => {
15
+ const src = readFileSync( new URL( '../src/commands/wp-glotpress.js', import.meta.url ), 'utf8' )
16
+ assert.doesNotMatch( src, /class GlotPressDownloader/ )
17
+ } )
18
+
19
+ test( 'downloadTranslations returns files and locale names without scraping CLI stdout', async () => {
20
+ const dest = await mkdtemp( join( tmpdir(), 'wp-advads-glotpress-' ) )
21
+ const cwd = process.cwd()
22
+ process.chdir( dest )
23
+ saveConfig( {
24
+ glotpress: { project: 'my-plugin', destination: dest },
25
+ wpPot: { domain: 'myplugin' },
26
+ } )
27
+
28
+ const originalFetch = globalThis.fetch
29
+ globalThis.fetch = async ( url ) => {
30
+ const href = String( url )
31
+ if ( href.endsWith( '/api/projects/my-plugin' ) ) {
32
+ return {
33
+ ok: true,
34
+ json: async () => ( {
35
+ translation_sets: [
36
+ {
37
+ wp_locale: 'de_DE',
38
+ slug: 'default',
39
+ name: 'German',
40
+ locale: 'de',
41
+ current_count: 4,
42
+ },
43
+ ],
44
+ } ),
45
+ }
46
+ }
47
+ return { ok: true, text: async () => 'translation-bytes' }
48
+ }
49
+
50
+ try {
51
+ const result = await downloadTranslations()
52
+
53
+ assert.ok( result )
54
+ assert.ok( Array.isArray( result.files ) )
55
+ assert.ok( result.files.some( ( file ) => file.endsWith( 'myplugin-de_DE.po' ) ) )
56
+ assert.ok( result.files.some( ( file ) => file.endsWith( 'myplugin-de_DE.mo' ) ) )
57
+ assert.equal(
58
+ Object.values( result.locales ).includes( 'German' ),
59
+ true
60
+ )
61
+ } finally {
62
+ globalThis.fetch = originalFetch
63
+ process.chdir( cwd )
64
+ await rm( dest, { recursive: true, force: true } )
65
+ }
66
+ } )
67
+
68
+ async function downloadWithSets( dest, sets, fileResponse ) {
69
+ const cwd = process.cwd()
70
+ process.chdir( dest )
71
+ saveConfig( {
72
+ glotpress: { project: 'my-plugin', destination: dest },
73
+ wpPot: { domain: 'myplugin' },
74
+ } )
75
+
76
+ const originalFetch = globalThis.fetch
77
+ globalThis.fetch = async ( url ) => {
78
+ const href = String( url )
79
+ if ( href.endsWith( '/api/projects/my-plugin' ) ) {
80
+ return {
81
+ ok: true,
82
+ json: async () => ( { translation_sets: sets } ),
83
+ }
84
+ }
85
+ return fileResponse
86
+ }
87
+
88
+ try {
89
+ return await downloadTranslations()
90
+ } finally {
91
+ globalThis.fetch = originalFetch
92
+ process.chdir( cwd )
93
+ }
94
+ }
95
+
96
+ test( 'downloadTranslations skips locale ids that are not alphanumeric', async () => {
97
+ const dest = await mkdtemp( join( tmpdir(), 'wp-advads-glotpress-' ) )
98
+ try {
99
+ await downloadWithSets( dest, [
100
+ {
101
+ wp_locale: '../evil',
102
+ slug: 'default',
103
+ name: 'Evil',
104
+ locale: 'de',
105
+ current_count: 4,
106
+ },
107
+ ], { ok: true, text: async () => 'translation-bytes' } )
108
+
109
+ assert.equal( existsSync( join( dest, '..', 'evil.po' ) ), false )
110
+ assert.equal( existsSync( join( dest, '..', 'evil.mo' ) ), false )
111
+ } finally {
112
+ await rm( dest, { recursive: true, force: true } )
113
+ }
114
+ } )
115
+
116
+ test( 'downloadTranslations does not write HTML error bodies as po/mo files', async () => {
117
+ const dest = await mkdtemp( join( tmpdir(), 'wp-advads-glotpress-' ) )
118
+ try {
119
+ const result = await downloadWithSets( dest, [
120
+ {
121
+ wp_locale: 'de_DE',
122
+ slug: 'default',
123
+ name: 'German',
124
+ locale: 'de',
125
+ current_count: 4,
126
+ },
127
+ ], { ok: false, text: async () => '<html>nope</html>' } )
128
+
129
+ assert.equal( existsSync( join( dest, 'myplugin-de_DE.po' ) ), false )
130
+ assert.equal( existsSync( join( dest, 'myplugin-de_DE.mo' ) ), false )
131
+ assert.deepEqual( result.files, [] )
132
+ } finally {
133
+ await rm( dest, { recursive: true, force: true } )
134
+ }
135
+ } )
136
+
137
+ test( 'downloadTranslations creates nested destination directories', async () => {
138
+ const dest = await mkdtemp( join( tmpdir(), 'wp-advads-glotpress-' ) )
139
+ const nested = join( dest, 'languages', 'i18n' )
140
+ const cwd = process.cwd()
141
+ process.chdir( dest )
142
+ saveConfig( {
143
+ glotpress: { project: 'my-plugin', destination: nested },
144
+ wpPot: { domain: 'myplugin' },
145
+ } )
146
+
147
+ const originalFetch = globalThis.fetch
148
+ globalThis.fetch = async ( url ) => {
149
+ if ( String( url ).endsWith( '/api/projects/my-plugin' ) ) {
150
+ return {
151
+ ok: true,
152
+ json: async () => ( {
153
+ translation_sets: [
154
+ {
155
+ wp_locale: 'de_DE',
156
+ slug: 'default',
157
+ name: 'German',
158
+ locale: 'de',
159
+ current_count: 4,
160
+ },
161
+ ],
162
+ } ),
163
+ }
164
+ }
165
+ return { ok: true, text: async () => 'translation-bytes' }
166
+ }
167
+
168
+ try {
169
+ await downloadTranslations()
170
+ assert.equal( existsSync( join( nested, 'myplugin-de_DE.po' ) ), true )
171
+ } finally {
172
+ globalThis.fetch = originalFetch
173
+ process.chdir( cwd )
174
+ await rm( dest, { recursive: true, force: true } )
175
+ }
176
+ } )
@@ -0,0 +1,39 @@
1
+ /**
2
+ * TASK-032: js, css, and view nested names accept / and \\.
3
+ */
4
+ import assert from 'node:assert/strict'
5
+ import { existsSync } from 'node:fs'
6
+ import { mkdtemp, rm, writeFile } from 'node:fs/promises'
7
+ import { tmpdir } from 'node:os'
8
+ import { join } from 'node:path'
9
+ import { test } from 'node:test'
10
+ import css from '../src/commands/css.js'
11
+ import javascript from '../src/commands/javascript.js'
12
+ import views from '../src/commands/views.js'
13
+
14
+ test( 'js css and view nested paths accept slash and backslash', async () => {
15
+ const dir = await mkdtemp( join( tmpdir(), 'wp-advads-nested-' ) )
16
+ const cwd = process.cwd()
17
+ process.chdir( dir )
18
+
19
+ try {
20
+ await writeFile( join( dir, 'wp.advads' ), JSON.stringify( {
21
+ paths: { views: 'views', javascript: 'assets/src', css: 'assets/css' },
22
+ wp: { version: '1.0.0' },
23
+ misc: { package: 'AdvancedAds' },
24
+ author: { name: 'Test', email: 'test@example.com' },
25
+ } ) )
26
+
27
+ javascript( 'Admin/App', {} )
28
+ assert.equal( existsSync( join( dir, 'assets', 'src', 'admin', 'app.js' ) ), true )
29
+
30
+ css( 'Admin/App', {} )
31
+ assert.equal( existsSync( join( dir, 'assets', 'css', 'admin', 'app.css' ) ), true )
32
+
33
+ views( 'Admin\\Box' )
34
+ assert.equal( existsSync( join( dir, 'views', 'admin', 'box.php' ) ), true )
35
+ } finally {
36
+ process.chdir( cwd )
37
+ await rm( dir, { recursive: true, force: true } )
38
+ }
39
+ } )
@@ -0,0 +1,11 @@
1
+ /**
2
+ * TASK-040: drop fs-extra; stdlib mkdir/write is enough.
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 does not depend on fs-extra', () => {
9
+ const pkg = JSON.parse( readFileSync( new URL( '../package.json', import.meta.url ), 'utf8' ) )
10
+ assert.equal( pkg.dependencies['fs-extra'], undefined )
11
+ } )
@@ -0,0 +1,19 @@
1
+ /**
2
+ * TASK-057: package description grammar/capitalization; no StandardJS claim without tooling.
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 description uses WordPress and correct article', () => {
9
+ const { description } = JSON.parse( readFileSync( new URL( '../package.json', import.meta.url ), 'utf8' ) )
10
+ assert.doesNotMatch( description, /wordPress/ )
11
+ assert.doesNotMatch( description, /Create a Advanced/ )
12
+ assert.doesNotMatch( description, /eco system/ )
13
+ assert.match( description, /WordPress/ )
14
+ } )
15
+
16
+ test( 'README does not claim StandardJS', () => {
17
+ const readme = readFileSync( new URL( '../README.md', import.meta.url ), 'utf8' )
18
+ assert.doesNotMatch( readme, /StandardJS/i )
19
+ } )
@@ -0,0 +1,42 @@
1
+ /**
2
+ * TASK-013: scaffold writes must stay inside the project root.
3
+ * Run with --test-isolation=none
4
+ */
5
+ import assert from 'node:assert/strict'
6
+ import { existsSync } from 'node:fs'
7
+ import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'
8
+ import { tmpdir } from 'node:os'
9
+ import { join } from 'node:path'
10
+ import { test } from 'node:test'
11
+ import views from '../src/commands/views.js'
12
+ import { writeFile as writeScaffold } from '../src/utilities/filesystem.js'
13
+
14
+ test( 'scaffold writes stay inside the project root', async () => {
15
+ const dir = await mkdtemp( join( tmpdir(), 'wp-advads-path-' ) )
16
+ const project = join( dir, 'project' )
17
+ const outside = join( dir, 'outside' )
18
+ await mkdir( project )
19
+ await mkdir( outside )
20
+ await writeFile( join( project, 'wp.advads' ), JSON.stringify( {
21
+ paths: { views: 'views' },
22
+ wp: { version: '1.0.0' },
23
+ misc: { package: 'AdvancedAds' },
24
+ author: { name: 'Test', email: 'test@example.com' },
25
+ } ) )
26
+
27
+ const cwd = process.cwd()
28
+ process.chdir( project )
29
+
30
+ try {
31
+ assert.throws( () => views( '../../outside/pwned' ) )
32
+ assert.equal( existsSync( join( outside, 'pwned.php' ) ), false )
33
+
34
+ assert.throws( () => writeScaffold( join( project, 'views' ), 'foo\0.php', 'nope' ) )
35
+
36
+ assert.throws( () => writeScaffold( join( project, 'views' ), join( outside, 'pwned.php' ), 'nope' ) )
37
+ assert.equal( existsSync( join( outside, 'pwned.php' ) ), false )
38
+ } finally {
39
+ process.chdir( cwd )
40
+ await rm( dir, { recursive: true, force: true } )
41
+ }
42
+ } )
@@ -0,0 +1,27 @@
1
+ /**
2
+ * TASK-030: pot creates a .pot; docs and CLI copy must not claim po/mo.
3
+ */
4
+ import assert from 'node:assert/strict'
5
+ import { readFileSync } from 'node:fs'
6
+ import { test } from 'node:test'
7
+
8
+ test( 'pot command and docs describe a .pot, not po/mo files', () => {
9
+ const pot = readFileSync( new URL( '../src/commands/wp-pot.js', import.meta.url ), 'utf8' )
10
+ assert.match( pot, /make-pot/ )
11
+ assert.doesNotMatch( pot, /make-mo|make-json/ )
12
+ assert.doesNotMatch( pot, /PO and MO/ )
13
+
14
+ const app = readFileSync( new URL( '../src/app.js', import.meta.url ), 'utf8' )
15
+ const potCmd = app.split( ".command('pot')" )[1].split( '.command(' )[0]
16
+ assert.match( potCmd, /\.pot/ )
17
+ assert.doesNotMatch( potCmd, /\bpo\b|\bmo\b/i )
18
+
19
+ const readme = readFileSync( new URL( '../README.md', import.meta.url ), 'utf8' )
20
+ const potSection = readme.split( '#### `pot`' )[1].split( '####' )[0]
21
+ assert.match( potSection, /\.pot/ )
22
+ assert.doesNotMatch( potSection, /\.po\b|\.mo\b/ )
23
+
24
+ const changelog = readFileSync( new URL( '../CHANGELOG.md', import.meta.url ), 'utf8' )
25
+ assert.match( changelog, /`pot` command to generate `\.pot`/ )
26
+ assert.doesNotMatch( changelog, /`pot` command to generate `\.po` and `\.mo`/ )
27
+ } )
@@ -0,0 +1,49 @@
1
+ /**
2
+ * TASK-038: pot headers come from wp.advads author, not a hardcoded name.
3
+ * Run with --experimental-test-module-mocks
4
+ */
5
+ import assert from 'node:assert/strict'
6
+ import { mkdtemp, rm } from 'node:fs/promises'
7
+ import { tmpdir } from 'node:os'
8
+ import { join } from 'node:path'
9
+ import { mock, test } from 'node:test'
10
+
11
+ const potArgs = []
12
+
13
+ mock.module( import.meta.resolve( '../src/utilities/shell.js' ), {
14
+ exports: {
15
+ execCommand() {},
16
+ runCommand( cmd, args, cb ) {
17
+ potArgs.push( { cmd, args } )
18
+ cb()
19
+ },
20
+ },
21
+ } )
22
+
23
+ const { default: wpPot } = await import( '../src/commands/wp-pot.js' )
24
+ const { saveConfig } = await import( '../src/utilities/settings.js' )
25
+
26
+ test( 'pot Last-Translator and Language-Team use author from config', async () => {
27
+ const dir = await mkdtemp( join( tmpdir(), 'wp-advads-pot-' ) )
28
+ const cwd = process.cwd()
29
+ process.chdir( dir )
30
+ potArgs.length = 0
31
+
32
+ try {
33
+ saveConfig( {
34
+ author: { name: 'Jane Doe', email: 'jane@example.com' },
35
+ wpPot: { output: '/languages/', file: 'plugin.pot', domain: 'plugin' },
36
+ } )
37
+
38
+ await new Promise( ( resolve ) => wpPot( resolve ) )
39
+
40
+ const headersArg = potArgs[0].args.find( ( arg ) => arg.startsWith( '--headers=' ) )
41
+ const headers = JSON.parse( headersArg.slice( '--headers='.length ) )
42
+ assert.equal( headers['Last-Translator'], 'Jane Doe <jane@example.com>' )
43
+ assert.equal( headers['Language-Team'], 'Jane Doe <jane@example.com>' )
44
+ assert.doesNotMatch( headersArg, /Shakeeb Ahmed/ )
45
+ } finally {
46
+ process.chdir( cwd )
47
+ await rm( dir, { recursive: true, force: true } )
48
+ }
49
+ } )
@@ -0,0 +1,26 @@
1
+ /**
2
+ * TASK-015: a failed getProjectRoot walk must not cache the filesystem root.
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 { test } from 'node:test'
9
+ import { getProjectRoot } from '../src/utilities/filesystem.js'
10
+
11
+ test( 'failed getProjectRoot does not make a later lookup return the filesystem root', async () => {
12
+ const empty = await mkdtemp( join( tmpdir(), 'wp-advads-empty-' ) )
13
+ const project = await mkdtemp( join( tmpdir(), 'wp-advads-root-' ) )
14
+ await writeFile( join( project, 'wp.advads' ), '{}' )
15
+
16
+ try {
17
+ assert.throws(
18
+ () => getProjectRoot( empty ),
19
+ /Project root not found/
20
+ )
21
+ assert.equal( getProjectRoot( project ), project )
22
+ } finally {
23
+ await rm( empty, { recursive: true, force: true } )
24
+ await rm( project, { recursive: true, force: true } )
25
+ }
26
+ } )
@@ -0,0 +1,62 @@
1
+ /**
2
+ * TASK-003: release changelog and version waterfalls must pass errors to next.
3
+ */
4
+ import assert from 'node:assert/strict'
5
+ import { mkdtemp, rm } 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
+ mock.module( import.meta.resolve( '../src/utilities/shell.js' ), {
11
+ exports: {
12
+ execCommand( cmd, cb ) {
13
+ cb( '', null, '' )
14
+ },
15
+ runCommand() {},
16
+ },
17
+ } )
18
+
19
+ const { pluginData } = await import( '../src/commands/release.js' )
20
+ const { default: updateChangelog } = await import( '../src/commands/release/changelog.js' )
21
+ const { default: updateVersionNumber } = await import( '../src/commands/release/change-version.js' )
22
+
23
+ async function inTempDir( run ) {
24
+ const dir = await mkdtemp( join( tmpdir(), 'wp-advads-' ) )
25
+ const cwd = process.cwd()
26
+ process.chdir( dir )
27
+ try {
28
+ return await run()
29
+ } finally {
30
+ process.chdir( cwd )
31
+ await rm( dir, { recursive: true, force: true } )
32
+ }
33
+ }
34
+
35
+ test( 'updateChangelog passes a missing .changelog directory to next', async () => {
36
+ await inTempDir( async () => {
37
+ pluginData.semver = { preRelease: true }
38
+ pluginData.glotpress = { destination: './languages/' }
39
+ pluginData.locales = {}
40
+
41
+ const err = await new Promise( ( resolve ) => {
42
+ updateChangelog( ( error ) => resolve( error ) )
43
+ } )
44
+
45
+ assert.ok( err, 'expected changelog failure to reach next' )
46
+ } )
47
+ } )
48
+
49
+ test( 'updateVersionNumber passes missing PHP files to next', async () => {
50
+ await inTempDir( async () => {
51
+ pluginData.semver = {
52
+ preRelease: true,
53
+ getNextVersionString: () => '1.2.3',
54
+ }
55
+
56
+ const err = await new Promise( ( resolve ) => {
57
+ updateVersionNumber( ( error ) => resolve( error ) )
58
+ } )
59
+
60
+ assert.ok( err, 'expected version-update failure to reach next' )
61
+ } )
62
+ } )
@@ -0,0 +1,25 @@
1
+ /**
2
+ * TASK-009: SemVer parse, pre-count, and JS isset replacement.
3
+ */
4
+ import assert from 'node:assert/strict'
5
+ import { test } from 'node:test'
6
+ import { SemVer } from '../src/commands/release/semver.js'
7
+
8
+ test( 'same-line alpha pre-release increments the count past 2', () => {
9
+ const semver = new SemVer( '1.2.0-alpha.2', 'patch', 'alpha' )
10
+ assert.equal( semver.getNextVersionString(), '1.2.0-alpha.3' )
11
+ } )
12
+
13
+ test( 'same-line alpha.1 becomes alpha.2', () => {
14
+ const semver = new SemVer( '1.2.0-alpha.1', 'patch', 'alpha' )
15
+ assert.equal( semver.getNextVersionString(), '1.2.0-alpha.2' )
16
+ } )
17
+
18
+ test( 'invalid version throws instead of crashing on .groups', () => {
19
+ assert.throws( () => new SemVer( 'not-a-version', 'patch', false ), /failed/ )
20
+ } )
21
+
22
+ test( 'switching alpha to beta on the same numbers does not throw', () => {
23
+ const semver = new SemVer( '1.2.1-alpha.1', 'patch', 'beta' )
24
+ assert.equal( semver.getNextVersionString(), '1.2.1-beta.1' )
25
+ } )
@@ -0,0 +1,34 @@
1
+ /**
2
+ * TASK-018: scaffold commands must not mutate the cached getSetting() object.
3
+ * Run with --test-isolation=none
4
+ */
5
+ import assert from 'node:assert/strict'
6
+ import { mkdtemp, rm, writeFile } from 'node:fs/promises'
7
+ import { tmpdir } from 'node:os'
8
+ import { join } from 'node:path'
9
+ import { test } from 'node:test'
10
+ import { getSetting } from '../src/utilities/index.js'
11
+ import views from '../src/commands/views.js'
12
+
13
+ test( 'view command does not persist heading onto the cached settings', async () => {
14
+ const dir = await mkdtemp( join( tmpdir(), 'wp-advads-clone-' ) )
15
+ const cwd = process.cwd()
16
+ process.chdir( dir )
17
+
18
+ try {
19
+ await writeFile( join( dir, 'wp.advads' ), JSON.stringify( {
20
+ paths: { views: 'views' },
21
+ wp: { version: '1.0.0' },
22
+ misc: { package: 'AdvancedAds' },
23
+ author: { name: 'Test', email: 'test@example.com' },
24
+ } ) )
25
+
26
+ views( 'sidebar', 'Sidebar Heading' )
27
+
28
+ assert.equal( getSetting().heading, undefined )
29
+ assert.equal( getSetting().description, undefined )
30
+ } finally {
31
+ process.chdir( cwd )
32
+ await rm( dir, { recursive: true, force: true } )
33
+ }
34
+ } )
@@ -0,0 +1,31 @@
1
+ /**
2
+ * TASK-016: saveConfig and readConfigFile must use the same wp.advads path.
3
+ */
4
+ import assert from 'node:assert/strict'
5
+ import { existsSync } from 'node:fs'
6
+ import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
7
+ import { tmpdir } from 'node:os'
8
+ import { join } from 'node:path'
9
+ import { test } from 'node:test'
10
+ import { saveConfig, readConfigFile } from '../src/utilities/settings.js'
11
+
12
+ test( 'saveConfig from a subdirectory writes the discovered wp.advads', async () => {
13
+ const project = await mkdtemp( join( tmpdir(), 'wp-advads-cfg-' ) )
14
+ const sub = join( project, 'includes' )
15
+ await mkdir( sub )
16
+ await writeFile( join( project, 'wp.advads' ), JSON.stringify( { keep: true } ) )
17
+
18
+ const cwd = process.cwd()
19
+ process.chdir( sub )
20
+
21
+ try {
22
+ saveConfig( { keep: false } )
23
+ assert.equal( existsSync( join( sub, 'wp.advads' ) ), false )
24
+ const saved = JSON.parse( await readFile( join( project, 'wp.advads' ), 'utf8' ) )
25
+ assert.equal( saved.keep, false )
26
+ assert.equal( readConfigFile().keep, false )
27
+ } finally {
28
+ process.chdir( cwd )
29
+ await rm( project, { recursive: true, force: true } )
30
+ }
31
+ } )