@artemsemkin/wp-headers 1.1.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Artem Semkin
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,26 @@
1
+ # @artemsemkin/wp-headers
2
+
3
+ Generate and patch WordPress file headers from `package.json` data.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install @artemsemkin/wp-headers
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```ts
14
+ import { processMapping } from '@artemsemkin/wp-headers'
15
+
16
+ processMapping({
17
+ type: 'plugin',
18
+ slug: 'my-plugin',
19
+ entityDir: '/path/to/plugin',
20
+ tgmBasePath: '/path/to/theme/src/php',
21
+ })
22
+ ```
23
+
24
+ This reads `package.json` from `entityDir`, generates the appropriate WordPress headers, and writes them to the target files (`style.css`, plugin PHP, `readme.txt`).
25
+
26
+ Header field data comes from `pkg.wp.theme` or `pkg.wp.plugin` in your `package.json`.
package/dist/core.d.ts ADDED
@@ -0,0 +1,8 @@
1
+ /** Serialize fields into a `/* ... *​/` block comment */
2
+ export declare function buildComment(fields: Record<string, string>): string;
3
+ /** Serialize a titled readme header block: `=== Title ===\n\nKey: Value\n\n` */
4
+ export declare function buildReadmeBlock(title: string, fields: Record<string, string>): string;
5
+ /** Replace the first `/* ... *​/` block comment in content. Returns `null` if not found. */
6
+ export declare function replaceComment(content: string, newComment: string): string | null;
7
+ /** Replace the `=== ... ===` header block in content. Returns `null` if not found. */
8
+ export declare function replaceReadmeBlock(content: string, newBlock: string): string | null;
package/dist/core.js ADDED
@@ -0,0 +1,55 @@
1
+ /** Serialize fields into a `/* ... *​/` block comment */
2
+ export function buildComment(fields) {
3
+ const lines = ['/*'];
4
+ for (const [key, value] of Object.entries(fields)) {
5
+ lines.push(`${key}: ${value}`);
6
+ }
7
+ lines.push('*/');
8
+ return lines.join('\n') + '\n';
9
+ }
10
+ /** Serialize a titled readme header block: `=== Title ===\n\nKey: Value\n\n` */
11
+ export function buildReadmeBlock(title, fields) {
12
+ const lines = [`=== ${title} ===`, ''];
13
+ for (const [key, value] of Object.entries(fields)) {
14
+ lines.push(`${key}: ${value}`);
15
+ }
16
+ lines.push('');
17
+ return lines.join('\n') + '\n';
18
+ }
19
+ /** Replace the first `/* ... *​/` block comment in content. Returns `null` if not found. */
20
+ export function replaceComment(content, newComment) {
21
+ const startIdx = content.indexOf('/*');
22
+ if (startIdx === -1) {
23
+ return null;
24
+ }
25
+ const endIdx = content.indexOf('*/', startIdx);
26
+ if (endIdx === -1) {
27
+ return null;
28
+ }
29
+ const after = content.slice(endIdx + 2).replace(/^\n*/, '');
30
+ return content.slice(0, startIdx) + newComment + '\n' + after;
31
+ }
32
+ /** Replace the `=== ... ===` header block in content. Returns `null` if not found. */
33
+ export function replaceReadmeBlock(content, newBlock) {
34
+ content = content.replace(/\r\n/g, '\n');
35
+ const lines = content.split('\n');
36
+ const titleIdx = lines.findIndex((l) => /^===\s+.+\s+===$/.test(l));
37
+ if (titleIdx === -1) {
38
+ return null;
39
+ }
40
+ let endIdx = titleIdx + 1;
41
+ while (endIdx < lines.length) {
42
+ const line = lines[endIdx];
43
+ if (line.trim() === '') {
44
+ endIdx++;
45
+ continue;
46
+ }
47
+ if (/^[A-Za-z][A-Za-z ]*:/.test(line)) {
48
+ endIdx++;
49
+ continue;
50
+ }
51
+ break;
52
+ }
53
+ const afterHeader = lines.slice(endIdx).join('\n');
54
+ return newBlock + afterHeader;
55
+ }
@@ -0,0 +1,9 @@
1
+ export { buildComment, buildReadmeBlock, replaceComment, replaceReadmeBlock } from './core.js';
2
+ export type { ThemeStyleConfig, ThemeReadmeConfig } from './wp-theme.js';
3
+ export { wpThemeStyle, wpThemeReadme, themeStyleFromPkg, themeReadmeFromPkg } from './wp-theme.js';
4
+ export type { PluginHeaderConfig, PluginReadmeConfig } from './wp-plugin.js';
5
+ export { wpPluginHeader, wpPluginReadme, pluginHeaderFromPkg, pluginReadmeFromPkg } from './wp-plugin.js';
6
+ export { titleCase, deriveName } from './wp-helpers.js';
7
+ export { patchTgmVersion } from './patch-tgm.js';
8
+ export type { HeaderMapping } from './process.js';
9
+ export { readText, processMapping } from './process.js';
package/dist/index.js ADDED
@@ -0,0 +1,9 @@
1
+ // Core
2
+ export { buildComment, buildReadmeBlock, replaceComment, replaceReadmeBlock } from './core.js';
3
+ export { wpThemeStyle, wpThemeReadme, themeStyleFromPkg, themeReadmeFromPkg } from './wp-theme.js';
4
+ export { wpPluginHeader, wpPluginReadme, pluginHeaderFromPkg, pluginReadmeFromPkg } from './wp-plugin.js';
5
+ // Helpers
6
+ export { titleCase, deriveName } from './wp-helpers.js';
7
+ // TGM
8
+ export { patchTgmVersion } from './patch-tgm.js';
9
+ export { readText, processMapping } from './process.js';
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Patch the `version` field for a specific plugin slug inside a TGM-style PHP array.
3
+ *
4
+ * Handles both `array()` and `[]` syntax, aligned `=>` whitespace,
5
+ * mixed quote styles, nested function calls, and arbitrary field order.
6
+ *
7
+ * Returns the patched content, or the original content unchanged if:
8
+ * - The slug is not found
9
+ * - No version field exists in the slug's array block
10
+ * - The version already matches
11
+ */
12
+ export declare function patchTgmVersion(content: string, slug: string, version: string): string;
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Patch the `version` field for a specific plugin slug inside a TGM-style PHP array.
3
+ *
4
+ * Handles both `array()` and `[]` syntax, aligned `=>` whitespace,
5
+ * mixed quote styles, nested function calls, and arbitrary field order.
6
+ *
7
+ * Returns the patched content, or the original content unchanged if:
8
+ * - The slug is not found
9
+ * - No version field exists in the slug's array block
10
+ * - The version already matches
11
+ */
12
+ export function patchTgmVersion(content, slug, version) {
13
+ // A. Find slug via regex — handles mixed quotes + flexible whitespace
14
+ const escapedSlug = slug.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
15
+ const slugPattern = new RegExp(`['"]slug['"]\\s*=>\\s*['"]${escapedSlug}['"]`);
16
+ const slugMatch = slugPattern.exec(content);
17
+ if (!slugMatch) {
18
+ return content;
19
+ }
20
+ const slugPos = slugMatch.index;
21
+ // B. Walk backwards with dual depth tracking to find enclosing array opener
22
+ let parenDepth = 0;
23
+ let bracketDepth = 0;
24
+ let arrayStart = -1;
25
+ for (let i = slugPos - 1; i >= 0; i--) {
26
+ const ch = content[i];
27
+ if (ch === ')') {
28
+ parenDepth++;
29
+ }
30
+ else if (ch === '(') {
31
+ if (parenDepth === 0) {
32
+ // Check if preceded by "array"
33
+ const before = content.slice(Math.max(0, i - 5), i);
34
+ if (before === 'array') {
35
+ arrayStart = i - 5;
36
+ }
37
+ else {
38
+ arrayStart = i;
39
+ }
40
+ break;
41
+ }
42
+ parenDepth--;
43
+ }
44
+ else if (ch === ']') {
45
+ bracketDepth++;
46
+ }
47
+ else if (ch === '[') {
48
+ if (bracketDepth === 0) {
49
+ arrayStart = i;
50
+ break;
51
+ }
52
+ bracketDepth--;
53
+ }
54
+ }
55
+ if (arrayStart === -1) {
56
+ return content;
57
+ }
58
+ // C. Walk forwards with depth tracking to find matching closer
59
+ // Determine what kind of opener we have
60
+ const openerChar = content[arrayStart] === '[' ? '[' : '(';
61
+ const closerChar = openerChar === '[' ? ']' : ')';
62
+ // Find the actual opening bracket/paren position
63
+ let openPos = arrayStart;
64
+ if (openerChar === '(') {
65
+ openPos = content.indexOf('(', arrayStart);
66
+ /* v8 ignore next 3 */
67
+ if (openPos === -1) {
68
+ return content;
69
+ }
70
+ }
71
+ let depth = 0;
72
+ let arrayEnd = -1;
73
+ for (let i = openPos; i < content.length; i++) {
74
+ const ch = content[i];
75
+ if (ch === openerChar) {
76
+ depth++;
77
+ }
78
+ else if (ch === closerChar) {
79
+ depth--;
80
+ if (depth === 0) {
81
+ arrayEnd = i;
82
+ break;
83
+ }
84
+ }
85
+ }
86
+ if (arrayEnd === -1) {
87
+ return content;
88
+ }
89
+ // D. Replace version within the block
90
+ const block = content.slice(arrayStart, arrayEnd + 1);
91
+ const versionPattern = /(['"])version\1(\s*=>\s*)(['"])[^'"]*\3/;
92
+ const newBlock = block.replace(versionPattern, (_, q1, arrow, q3) => {
93
+ return `${q1}version${q1}${arrow}${q3}${version}${q3}`;
94
+ });
95
+ if (newBlock === block) {
96
+ return content;
97
+ }
98
+ return content.slice(0, arrayStart) + newBlock + content.slice(arrayEnd + 1);
99
+ }
@@ -0,0 +1,12 @@
1
+ /** Read a UTF-8 file, stripping BOM if present */
2
+ export declare function readText(filePath: string): string;
3
+ export interface HeaderMapping {
4
+ type: 'theme' | 'plugin';
5
+ slug: string;
6
+ entityDir: string;
7
+ /** Base path to resolve TGM file from (usually the theme's PHP source dir) */
8
+ tgmBasePath: string;
9
+ /** Subdirectory within entityDir containing PHP source files */
10
+ phpSrc?: string;
11
+ }
12
+ export declare function processMapping(mapping: HeaderMapping): void;
@@ -0,0 +1,78 @@
1
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { resolve } from 'node:path';
3
+ import { buildComment, buildReadmeBlock, replaceComment, replaceReadmeBlock } from './core.js';
4
+ import { patchTgmVersion } from './patch-tgm.js';
5
+ import { themeStyleFromPkg, themeReadmeFromPkg, wpThemeStyle, wpThemeReadme } from './wp-theme.js';
6
+ import { pluginHeaderFromPkg, pluginReadmeFromPkg, wpPluginHeader, wpPluginReadme } from './wp-plugin.js';
7
+ /** Read a UTF-8 file, stripping BOM if present */
8
+ export function readText(filePath) {
9
+ return readFileSync(filePath, 'utf-8').replace(/^\uFEFF/, '');
10
+ }
11
+ export function processMapping(mapping) {
12
+ const phpSrc = mapping.phpSrc ?? 'src/php';
13
+ const pkgPath = resolve(mapping.entityDir, 'package.json');
14
+ if (!existsSync(pkgPath)) {
15
+ return;
16
+ }
17
+ const pkg = JSON.parse(readText(pkgPath));
18
+ const wp = pkg['wp'] ?? {};
19
+ if (mapping.type === 'theme') {
20
+ if (!wp['theme']) {
21
+ return;
22
+ }
23
+ // style.css
24
+ const styleConfig = themeStyleFromPkg(pkg, mapping.slug);
25
+ writeFileSync(resolve(mapping.entityDir, phpSrc, 'style.css'), buildComment(wpThemeStyle(styleConfig)));
26
+ // readme.txt
27
+ const themeReadmePath = resolve(mapping.entityDir, phpSrc, 'readme.txt');
28
+ if (existsSync(themeReadmePath)) {
29
+ const readmeContent = readText(themeReadmePath);
30
+ const readmeConfig = themeReadmeFromPkg(pkg, mapping.slug);
31
+ const block = buildReadmeBlock(readmeConfig.name, wpThemeReadme(readmeConfig));
32
+ writeFileSync(themeReadmePath, replaceReadmeBlock(readmeContent, block) ?? block + readmeContent);
33
+ }
34
+ }
35
+ else {
36
+ if (!wp['plugin']) {
37
+ return;
38
+ }
39
+ const version = pkg['version'] ?? '1.0.0';
40
+ // Plugin PHP header
41
+ const pluginPhpPath = resolve(mapping.entityDir, phpSrc, `${mapping.slug}.php`);
42
+ if (existsSync(pluginPhpPath)) {
43
+ const content = readText(pluginPhpPath);
44
+ const headerConfig = pluginHeaderFromPkg(pkg, mapping.slug);
45
+ const comment = buildComment(wpPluginHeader(headerConfig));
46
+ const replaced = replaceComment(content, comment);
47
+ if (replaced !== null) {
48
+ const normalized = replaced.replace(/^\s*<\?(?:php|PHP)?\s*/, '');
49
+ writeFileSync(pluginPhpPath, `<?php\n${normalized.trimStart()}`);
50
+ }
51
+ else {
52
+ const stripped = content.replace(/^\s*<\?(?:php|PHP)?\s*/, '');
53
+ writeFileSync(pluginPhpPath, `<?php\n${comment}\n${stripped.trimStart()}`);
54
+ }
55
+ }
56
+ // readme.txt
57
+ const pluginReadmePath = resolve(mapping.entityDir, phpSrc, 'readme.txt');
58
+ if (existsSync(pluginReadmePath)) {
59
+ const readmeContent = readText(pluginReadmePath);
60
+ const readmeConfig = pluginReadmeFromPkg(pkg, mapping.slug);
61
+ const block = buildReadmeBlock(readmeConfig.name, wpPluginReadme(readmeConfig));
62
+ writeFileSync(pluginReadmePath, replaceReadmeBlock(readmeContent, block) ?? block + readmeContent);
63
+ }
64
+ // TGM version patching
65
+ const pluginWp = wp['plugin'];
66
+ const loadPluginsFile = pluginWp['loadPluginsFile'];
67
+ if (loadPluginsFile && mapping.tgmBasePath) {
68
+ const tgmFilePath = resolve(mapping.tgmBasePath, loadPluginsFile);
69
+ if (existsSync(tgmFilePath)) {
70
+ const tgmContent = readText(tgmFilePath);
71
+ const patched = patchTgmVersion(tgmContent, mapping.slug, version);
72
+ if (patched !== tgmContent) {
73
+ writeFileSync(tgmFilePath, patched);
74
+ }
75
+ }
76
+ }
77
+ }
78
+ }
@@ -0,0 +1,7 @@
1
+ /** Convert a hyphenated string to Title Case */
2
+ export declare function titleCase(str: string): string;
3
+ /**
4
+ * Derive a display name from pkg.name, stripping `@scope/` prefix and type suffix,
5
+ * then title-casing. Falls back to `titleCase(slug)`.
6
+ */
7
+ export declare function deriveName(pkgName: string | undefined, slug: string, suffix: string): string;
@@ -0,0 +1,15 @@
1
+ /** Convert a hyphenated string to Title Case */
2
+ export function titleCase(str) {
3
+ return str
4
+ .split('-')
5
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
6
+ .join(' ');
7
+ }
8
+ /**
9
+ * Derive a display name from pkg.name, stripping `@scope/` prefix and type suffix,
10
+ * then title-casing. Falls back to `titleCase(slug)`.
11
+ */
12
+ export function deriveName(pkgName, slug, suffix) {
13
+ const name = pkgName ?? '';
14
+ return titleCase(name.replace(/^@[\w-]+\//, '').replace(new RegExp(`${suffix}$`), '')) || titleCase(slug);
15
+ }
@@ -0,0 +1,64 @@
1
+ export interface PluginHeaderConfig {
2
+ /** Plugin display name — always emitted */
3
+ name: string;
4
+ /** Plugin version — always emitted */
5
+ version: string;
6
+ /** Text domain — always emitted */
7
+ textDomain: string;
8
+ /** Plugin URI */
9
+ pluginUri?: string;
10
+ /** Short description */
11
+ description?: string;
12
+ /** Author name */
13
+ author?: string;
14
+ /** Author URI */
15
+ authorUri?: string;
16
+ /** License name */
17
+ license?: string;
18
+ /** License URI */
19
+ licenseUri?: string;
20
+ /** Minimum WordPress version */
21
+ requiresAtLeast?: string;
22
+ /** Minimum PHP version */
23
+ requiresPHP?: string;
24
+ /** Tested up to WordPress version */
25
+ testedUpTo?: string;
26
+ /** Update URI */
27
+ updateUri?: string;
28
+ /** Domain path for translations */
29
+ domainPath?: string;
30
+ /** Whether the plugin can only be activated network-wide (multisite) */
31
+ network?: string;
32
+ /** Comma-separated list of required plugin slugs */
33
+ requiresPlugins?: string;
34
+ }
35
+ export interface PluginReadmeConfig {
36
+ /** Plugin display name — used in `=== Title ===` line */
37
+ name: string;
38
+ /** Stable tag (version) */
39
+ stableTag: string;
40
+ /** WordPress.org username(s) */
41
+ contributors?: string;
42
+ /** Donate link URL */
43
+ donateLink?: string;
44
+ /** Comma-separated tag list */
45
+ tags?: string;
46
+ /** Minimum WordPress version */
47
+ requiresAtLeast?: string;
48
+ /** Tested up to WordPress version */
49
+ testedUpTo?: string;
50
+ /** Minimum PHP version */
51
+ requiresPHP?: string;
52
+ /** License name */
53
+ license?: string;
54
+ /** License URI */
55
+ licenseUri?: string;
56
+ }
57
+ /** Convert a PluginHeaderConfig to ordered WP fields */
58
+ export declare function wpPluginHeader(config: PluginHeaderConfig): Record<string, string>;
59
+ /** Convert a PluginReadmeConfig to ordered WP readme fields */
60
+ export declare function wpPluginReadme(config: PluginReadmeConfig): Record<string, string>;
61
+ /** Extract PluginHeaderConfig from a package.json object */
62
+ export declare function pluginHeaderFromPkg(pkg: Record<string, unknown>, slug: string): PluginHeaderConfig;
63
+ /** Extract PluginReadmeConfig from a package.json object */
64
+ export declare function pluginReadmeFromPkg(pkg: Record<string, unknown>, slug: string): PluginReadmeConfig;
@@ -0,0 +1,123 @@
1
+ import { titleCase } from './wp-helpers.js';
2
+ /** Convert a PluginHeaderConfig to ordered WP fields */
3
+ export function wpPluginHeader(config) {
4
+ const fields = {};
5
+ fields['Plugin Name'] = config.name;
6
+ if (config.pluginUri) {
7
+ fields['Plugin URI'] = config.pluginUri;
8
+ }
9
+ if (config.description) {
10
+ fields['Description'] = config.description;
11
+ }
12
+ fields['Version'] = config.version;
13
+ if (config.author) {
14
+ fields['Author'] = config.author;
15
+ }
16
+ if (config.authorUri) {
17
+ fields['Author URI'] = config.authorUri;
18
+ }
19
+ if (config.license) {
20
+ fields['License'] = config.license;
21
+ }
22
+ if (config.licenseUri) {
23
+ fields['License URI'] = config.licenseUri;
24
+ }
25
+ if (config.requiresAtLeast) {
26
+ fields['Requires at least'] = config.requiresAtLeast;
27
+ }
28
+ if (config.requiresPHP) {
29
+ fields['Requires PHP'] = config.requiresPHP;
30
+ }
31
+ if (config.testedUpTo) {
32
+ fields['Tested up to'] = config.testedUpTo;
33
+ }
34
+ if (config.updateUri) {
35
+ fields['Update URI'] = config.updateUri;
36
+ }
37
+ fields['Text Domain'] = config.textDomain;
38
+ if (config.domainPath) {
39
+ fields['Domain Path'] = config.domainPath;
40
+ }
41
+ if (config.network) {
42
+ fields['Network'] = config.network;
43
+ }
44
+ if (config.requiresPlugins) {
45
+ fields['Requires Plugins'] = config.requiresPlugins;
46
+ }
47
+ return fields;
48
+ }
49
+ /** Convert a PluginReadmeConfig to ordered WP readme fields */
50
+ export function wpPluginReadme(config) {
51
+ const fields = {};
52
+ if (config.contributors) {
53
+ fields['Contributors'] = config.contributors;
54
+ }
55
+ if (config.donateLink) {
56
+ fields['Donate link'] = config.donateLink;
57
+ }
58
+ if (config.tags) {
59
+ fields['Tags'] = config.tags;
60
+ }
61
+ if (config.requiresAtLeast) {
62
+ fields['Requires at least'] = config.requiresAtLeast;
63
+ }
64
+ if (config.testedUpTo) {
65
+ fields['Tested up to'] = config.testedUpTo;
66
+ }
67
+ if (config.requiresPHP) {
68
+ fields['Requires PHP'] = config.requiresPHP;
69
+ }
70
+ fields['Stable tag'] = config.stableTag;
71
+ if (config.license) {
72
+ fields['License'] = config.license;
73
+ }
74
+ if (config.licenseUri) {
75
+ fields['License URI'] = config.licenseUri;
76
+ }
77
+ return fields;
78
+ }
79
+ /** Extract PluginHeaderConfig from a package.json object */
80
+ export function pluginHeaderFromPkg(pkg, slug) {
81
+ const wp = pkg['wp'] ?? {};
82
+ const plugin = (wp['plugin'] ?? {});
83
+ const version = pkg['version'] ?? '1.0.0';
84
+ const name = plugin['name'] ?? titleCase(slug);
85
+ return {
86
+ name,
87
+ version,
88
+ textDomain: plugin['textDomain'] ?? slug,
89
+ pluginUri: plugin['pluginUri'],
90
+ description: plugin['description'],
91
+ author: plugin['author'],
92
+ authorUri: plugin['authorUri'],
93
+ license: plugin['license'],
94
+ licenseUri: plugin['licenseUri'],
95
+ requiresAtLeast: plugin['requiresAtLeast'],
96
+ requiresPHP: plugin['requiresPHP'],
97
+ testedUpTo: plugin['testedUpTo'],
98
+ updateUri: plugin['updateUri'],
99
+ domainPath: plugin['domainPath'],
100
+ network: plugin['network'],
101
+ requiresPlugins: plugin['requiresPlugins'],
102
+ };
103
+ }
104
+ /** Extract PluginReadmeConfig from a package.json object */
105
+ export function pluginReadmeFromPkg(pkg, slug) {
106
+ const wp = pkg['wp'] ?? {};
107
+ const plugin = (wp['plugin'] ?? {});
108
+ const version = pkg['version'] ?? '1.0.0';
109
+ const name = plugin['name'] ?? titleCase(slug);
110
+ const contributors = plugin['contributors'] ?? plugin['author'];
111
+ return {
112
+ name,
113
+ stableTag: version,
114
+ contributors,
115
+ donateLink: plugin['donateLink'],
116
+ tags: plugin['tags'],
117
+ requiresAtLeast: plugin['requiresAtLeast'],
118
+ testedUpTo: plugin['testedUpTo'],
119
+ requiresPHP: plugin['requiresPHP'],
120
+ license: plugin['license'],
121
+ licenseUri: plugin['licenseUri'],
122
+ };
123
+ }
@@ -0,0 +1,60 @@
1
+ export interface ThemeStyleConfig {
2
+ /** Theme display name — always emitted */
3
+ name: string;
4
+ /** Theme version — always emitted */
5
+ version: string;
6
+ /** Text domain — always emitted */
7
+ textDomain: string;
8
+ /** Theme URI */
9
+ uri?: string;
10
+ /** Short description */
11
+ description?: string;
12
+ /** Author name */
13
+ author?: string;
14
+ /** Author URI */
15
+ authorUri?: string;
16
+ /** License name */
17
+ license?: string;
18
+ /** License URI */
19
+ licenseUri?: string;
20
+ /** Minimum WordPress version */
21
+ requiresAtLeast?: string;
22
+ /** Minimum PHP version */
23
+ requiresPHP?: string;
24
+ /** Tested up to WordPress version */
25
+ testedUpTo?: string;
26
+ /** Comma-separated tag list */
27
+ tags?: string;
28
+ /** Parent theme slug (for child themes) */
29
+ template?: string;
30
+ /** Domain path for translations */
31
+ domainPath?: string;
32
+ /** Update URI */
33
+ updateUri?: string;
34
+ }
35
+ export interface ThemeReadmeConfig {
36
+ /** Theme display name — used in `=== Title ===` line */
37
+ name: string;
38
+ /** Stable tag (version) */
39
+ stableTag: string;
40
+ /** WordPress.org username(s) */
41
+ contributors?: string;
42
+ /** Minimum WordPress version */
43
+ requiresAtLeast?: string;
44
+ /** Tested up to WordPress version */
45
+ testedUpTo?: string;
46
+ /** Minimum PHP version */
47
+ requiresPHP?: string;
48
+ /** License name */
49
+ license?: string;
50
+ /** License URI */
51
+ licenseUri?: string;
52
+ }
53
+ /** Convert a ThemeStyleConfig to ordered WP fields */
54
+ export declare function wpThemeStyle(config: ThemeStyleConfig): Record<string, string>;
55
+ /** Convert a ThemeReadmeConfig to ordered WP readme fields */
56
+ export declare function wpThemeReadme(config: ThemeReadmeConfig): Record<string, string>;
57
+ /** Extract ThemeStyleConfig from a package.json object */
58
+ export declare function themeStyleFromPkg(pkg: Record<string, unknown>, slug: string): ThemeStyleConfig;
59
+ /** Extract ThemeReadmeConfig from a package.json object */
60
+ export declare function themeReadmeFromPkg(pkg: Record<string, unknown>, slug: string): ThemeReadmeConfig;
@@ -0,0 +1,115 @@
1
+ import { deriveName } from './wp-helpers.js';
2
+ /** Convert a ThemeStyleConfig to ordered WP fields */
3
+ export function wpThemeStyle(config) {
4
+ const fields = {};
5
+ fields['Theme Name'] = config.name;
6
+ if (config.uri) {
7
+ fields['Theme URI'] = config.uri;
8
+ }
9
+ if (config.description) {
10
+ fields['Description'] = config.description;
11
+ }
12
+ fields['Version'] = config.version;
13
+ if (config.author) {
14
+ fields['Author'] = config.author;
15
+ }
16
+ if (config.authorUri) {
17
+ fields['Author URI'] = config.authorUri;
18
+ }
19
+ if (config.license) {
20
+ fields['License'] = config.license;
21
+ }
22
+ if (config.licenseUri) {
23
+ fields['License URI'] = config.licenseUri;
24
+ }
25
+ if (config.requiresAtLeast) {
26
+ fields['Requires at least'] = config.requiresAtLeast;
27
+ }
28
+ if (config.requiresPHP) {
29
+ fields['Requires PHP'] = config.requiresPHP;
30
+ }
31
+ if (config.testedUpTo) {
32
+ fields['Tested up to'] = config.testedUpTo;
33
+ }
34
+ if (config.tags) {
35
+ fields['Tags'] = config.tags;
36
+ }
37
+ if (config.template) {
38
+ fields['Template'] = config.template;
39
+ }
40
+ fields['Text Domain'] = config.textDomain;
41
+ if (config.domainPath) {
42
+ fields['Domain Path'] = config.domainPath;
43
+ }
44
+ if (config.updateUri) {
45
+ fields['Update URI'] = config.updateUri;
46
+ }
47
+ return fields;
48
+ }
49
+ /** Convert a ThemeReadmeConfig to ordered WP readme fields */
50
+ export function wpThemeReadme(config) {
51
+ const fields = {};
52
+ if (config.contributors) {
53
+ fields['Contributors'] = config.contributors;
54
+ }
55
+ if (config.requiresAtLeast) {
56
+ fields['Requires at least'] = config.requiresAtLeast;
57
+ }
58
+ if (config.testedUpTo) {
59
+ fields['Tested up to'] = config.testedUpTo;
60
+ }
61
+ if (config.requiresPHP) {
62
+ fields['Requires PHP'] = config.requiresPHP;
63
+ }
64
+ fields['Stable tag'] = config.stableTag;
65
+ if (config.license) {
66
+ fields['License'] = config.license;
67
+ }
68
+ if (config.licenseUri) {
69
+ fields['License URI'] = config.licenseUri;
70
+ }
71
+ return fields;
72
+ }
73
+ /** Extract ThemeStyleConfig from a package.json object */
74
+ export function themeStyleFromPkg(pkg, slug) {
75
+ const wp = pkg['wp'] ?? {};
76
+ const theme = (wp['theme'] ?? {});
77
+ const version = pkg['version'] ?? '1.0.0';
78
+ const name = deriveName(pkg['name'], slug, '-theme');
79
+ return {
80
+ name,
81
+ version,
82
+ textDomain: theme['textDomain'] ?? slug,
83
+ uri: theme['uri'],
84
+ description: theme['description'],
85
+ author: theme['author'],
86
+ authorUri: theme['authorUri'],
87
+ license: theme['license'],
88
+ licenseUri: theme['licenseUri'],
89
+ requiresAtLeast: theme['requiresAtLeast'],
90
+ requiresPHP: theme['requiresPHP'],
91
+ testedUpTo: theme['testedUpTo'],
92
+ tags: theme['tags'],
93
+ template: theme['template'],
94
+ domainPath: theme['domainPath'],
95
+ updateUri: theme['updateUri'],
96
+ };
97
+ }
98
+ /** Extract ThemeReadmeConfig from a package.json object */
99
+ export function themeReadmeFromPkg(pkg, slug) {
100
+ const wp = pkg['wp'] ?? {};
101
+ const theme = (wp['theme'] ?? {});
102
+ const version = pkg['version'] ?? '1.0.0';
103
+ const name = deriveName(pkg['name'], slug, '-theme');
104
+ const contributors = theme['contributors'] ?? theme['author'];
105
+ return {
106
+ name,
107
+ stableTag: version,
108
+ contributors,
109
+ requiresAtLeast: theme['requiresAtLeast'],
110
+ testedUpTo: theme['testedUpTo'],
111
+ requiresPHP: theme['requiresPHP'],
112
+ license: theme['license'],
113
+ licenseUri: theme['licenseUri'],
114
+ };
115
+ }
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "@artemsemkin/wp-headers",
3
+ "version": "1.1.0",
4
+ "description": "Generate and patch WordPress file headers (style.css, plugin PHP, readme.txt, TGM)",
5
+ "license": "MIT",
6
+ "author": "Artem Semkin",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/artkrsk/wp-headers.git"
10
+ },
11
+ "homepage": "https://github.com/artkrsk/wp-headers#readme",
12
+ "bugs": {
13
+ "url": "https://github.com/artkrsk/wp-headers/issues"
14
+ },
15
+ "keywords": [
16
+ "wordpress",
17
+ "headers",
18
+ "wp-headers",
19
+ "style-css",
20
+ "readme-txt",
21
+ "plugin-header"
22
+ ],
23
+ "type": "module",
24
+ "exports": {
25
+ ".": {
26
+ "types": "./dist/index.d.ts",
27
+ "default": "./dist/index.js"
28
+ }
29
+ },
30
+ "files": [
31
+ "dist"
32
+ ],
33
+ "publishConfig": {
34
+ "access": "public"
35
+ },
36
+ "scripts": {
37
+ "build": "tsc -p tsconfig.build.json",
38
+ "prepublishOnly": "pnpm build",
39
+ "test": "vitest run",
40
+ "test:watch": "vitest",
41
+ "test:coverage": "vitest run --coverage"
42
+ },
43
+ "devDependencies": {
44
+ "@types/node": "^25.3.0",
45
+ "@vitest/coverage-v8": "3.2.4",
46
+ "typescript": "^5.7.0",
47
+ "vitest": "^3.0.0"
48
+ }
49
+ }