@wpelevator/wp-release 0.15.0 → 0.17.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.
Files changed (98) hide show
  1. package/README.md +366 -39
  2. package/dist/changeset-config.d.ts +8 -0
  3. package/dist/changeset-config.js +3 -0
  4. package/dist/changeset-release.d.ts +9 -3
  5. package/dist/changeset-release.js +23 -14
  6. package/dist/cli.d.ts +6 -0
  7. package/dist/cli.js +12 -0
  8. package/dist/commands/artifact-signing.d.ts +20 -0
  9. package/dist/commands/artifact-signing.js +42 -0
  10. package/dist/commands/build.d.ts +32 -0
  11. package/dist/commands/build.js +91 -0
  12. package/dist/commands/changeset-add.d.ts +55 -6
  13. package/dist/commands/changeset-add.js +12 -3
  14. package/dist/commands/changeset-args.js +2 -2
  15. package/dist/commands/changeset-project-args.d.ts +63 -0
  16. package/dist/commands/changeset-project-args.js +114 -0
  17. package/dist/commands/changeset-status.d.ts +50 -1
  18. package/dist/commands/changeset-status.js +4 -2
  19. package/dist/commands/changeset-tag.d.ts +55 -6
  20. package/dist/commands/changeset-tag.js +7 -4
  21. package/dist/commands/changeset-version.d.ts +55 -6
  22. package/dist/commands/changeset-version.js +4 -2
  23. package/dist/commands/checksums.d.ts +4 -1
  24. package/dist/commands/checksums.js +27 -6
  25. package/dist/commands/i18n-audit.d.ts +2 -0
  26. package/dist/commands/i18n-audit.js +2 -0
  27. package/dist/commands/i18n-compile.d.ts +6 -0
  28. package/dist/commands/i18n-compile.js +76 -0
  29. package/dist/commands/i18n-json.d.ts +2 -0
  30. package/dist/commands/i18n-json.js +2 -0
  31. package/dist/commands/i18n-mo.d.ts +2 -0
  32. package/dist/commands/i18n-mo.js +2 -0
  33. package/dist/commands/i18n-php.d.ts +2 -0
  34. package/dist/commands/i18n-php.js +2 -0
  35. package/dist/commands/i18n-pot.d.ts +2 -0
  36. package/dist/commands/i18n-pot.js +2 -0
  37. package/dist/commands/i18n.d.ts +12 -0
  38. package/dist/commands/i18n.js +174 -0
  39. package/dist/commands/package-source.d.ts +43 -0
  40. package/dist/commands/package-source.js +101 -0
  41. package/dist/commands/verify.d.ts +7 -1
  42. package/dist/commands/verify.js +53 -17
  43. package/dist/commands/zip.d.ts +20 -8
  44. package/dist/commands/zip.js +109 -124
  45. package/dist/i18n/atomic-output.d.ts +23 -0
  46. package/dist/i18n/atomic-output.js +90 -0
  47. package/dist/i18n/audit.d.ts +7 -0
  48. package/dist/i18n/audit.js +69 -0
  49. package/dist/i18n/catalog.d.ts +54 -0
  50. package/dist/i18n/catalog.js +86 -0
  51. package/dist/i18n/compile.d.ts +27 -0
  52. package/dist/i18n/compile.js +71 -0
  53. package/dist/i18n/extraction.d.ts +45 -0
  54. package/dist/i18n/extraction.js +105 -0
  55. package/dist/i18n/files.d.ts +7 -0
  56. package/dist/i18n/files.js +45 -0
  57. package/dist/i18n/generate.d.ts +49 -0
  58. package/dist/i18n/generate.js +240 -0
  59. package/dist/i18n/javascript.d.ts +7 -0
  60. package/dist/i18n/javascript.js +143 -0
  61. package/dist/i18n/metadata.d.ts +16 -0
  62. package/dist/i18n/metadata.js +117 -0
  63. package/dist/i18n/options.d.ts +52 -0
  64. package/dist/i18n/options.js +156 -0
  65. package/dist/i18n/packages.d.ts +24 -0
  66. package/dist/i18n/packages.js +86 -0
  67. package/dist/i18n/php.d.ts +9 -0
  68. package/dist/i18n/php.js +238 -0
  69. package/dist/i18n/placeholders.d.ts +9 -0
  70. package/dist/i18n/placeholders.js +13 -0
  71. package/dist/i18n/po.d.ts +24 -0
  72. package/dist/i18n/po.js +180 -0
  73. package/dist/i18n/pot.d.ts +16 -0
  74. package/dist/i18n/pot.js +119 -0
  75. package/dist/i18n/translation-json.d.ts +14 -0
  76. package/dist/i18n/translation-json.js +96 -0
  77. package/dist/i18n/translation-php.d.ts +6 -0
  78. package/dist/i18n/translation-php.js +35 -0
  79. package/dist/index.d.ts +28 -0
  80. package/dist/index.js +16 -0
  81. package/dist/integrity.d.ts +33 -0
  82. package/dist/integrity.js +115 -0
  83. package/dist/output-files.d.ts +7 -0
  84. package/dist/output-files.js +25 -0
  85. package/dist/package-build.d.ts +33 -0
  86. package/dist/package-build.js +112 -0
  87. package/dist/package-files.d.ts +25 -0
  88. package/dist/package-files.js +24 -0
  89. package/dist/package-zip.d.ts +14 -2
  90. package/dist/package-zip.js +129 -38
  91. package/dist/release-tags.js +6 -0
  92. package/dist/single-project.d.ts +32 -0
  93. package/dist/single-project.js +144 -0
  94. package/dist/summary.d.ts +9 -0
  95. package/dist/summary.js +25 -0
  96. package/dist/zip.d.ts +3 -0
  97. package/dist/zip.js +16 -6
  98. package/package.json +20 -3
@@ -0,0 +1,143 @@
1
+ import { parse } from '@babel/parser';
2
+ import traverseModule from '@babel/traverse';
3
+ import { extractCalls, syntaxFailure } from "./extraction.js";
4
+ // Babel traverse publishes CommonJS; Node's ESM default is its module object.
5
+ const traverse = traverseModule.default;
6
+ const signatures = {
7
+ __: { id: 0, domain: 1, parameters: [] },
8
+ _x: { id: 0, context: 1, domain: 2, parameters: [] },
9
+ _n: { id: 0, plural: 1, domain: 3, parameters: [] },
10
+ _nx: { id: 0, plural: 1, context: 3, domain: 4, parameters: [] },
11
+ };
12
+ function staticString(node) {
13
+ if ('StringLiteral' === node?.type) {
14
+ return node.value;
15
+ }
16
+ if ('TemplateLiteral' === node?.type && !node.expressions.length) {
17
+ return node.quasis
18
+ .map((part) => part.value.cooked ?? part.value.raw)
19
+ .join('');
20
+ }
21
+ if ('BinaryExpression' === node?.type && '+' === node.operator) {
22
+ const left = staticString(node.left);
23
+ const right = staticString(node.right);
24
+ return undefined !== left && undefined !== right
25
+ ? left + right
26
+ : undefined;
27
+ }
28
+ if (node &&
29
+ [
30
+ 'TSAsExpression',
31
+ 'TSSatisfiesExpression',
32
+ 'TSNonNullExpression',
33
+ 'ParenthesizedExpression',
34
+ ].includes(node.type)) {
35
+ return staticString(node.expression);
36
+ }
37
+ return undefined;
38
+ }
39
+ function importedName(path, name) {
40
+ const binding = path.scope.getBinding(name);
41
+ const declaration = binding?.path.parentPath;
42
+ if (binding?.constant &&
43
+ declaration?.isImportDeclaration() &&
44
+ 'type' !== declaration.node.importKind &&
45
+ '@wordpress/i18n' === declaration.node.source.value &&
46
+ binding.path.isImportSpecifier() &&
47
+ 'type' !== binding.path.node.importKind) {
48
+ const imported = binding.path.node.imported;
49
+ return 'Identifier' === imported.type ? imported.name : imported.value;
50
+ }
51
+ return undefined;
52
+ }
53
+ function memberName(node) {
54
+ return !node.computed && 'Identifier' === node.property.type
55
+ ? node.property.name
56
+ : undefined;
57
+ }
58
+ function functionName(path, legacyGlobals) {
59
+ const callee = path.node.callee;
60
+ if ('Identifier' === callee.type) {
61
+ return (importedName(path, callee.name) ??
62
+ (legacyGlobals && !path.scope.getBinding(callee.name)
63
+ ? callee.name
64
+ : undefined));
65
+ }
66
+ if ('MemberExpression' === callee.type) {
67
+ const object = callee.object;
68
+ if ('Identifier' === object.type) {
69
+ const binding = path.scope.getBinding(object.name);
70
+ const declaration = binding?.path.parentPath;
71
+ if (binding?.constant &&
72
+ binding.path.isImportNamespaceSpecifier() &&
73
+ declaration?.isImportDeclaration() &&
74
+ 'type' !== declaration.node.importKind &&
75
+ '@wordpress/i18n' === declaration.node.source.value) {
76
+ return memberName(callee);
77
+ }
78
+ }
79
+ if ('MemberExpression' === object.type &&
80
+ 'i18n' === memberName(object) &&
81
+ 'Identifier' === object.object.type &&
82
+ 'wp' === object.object.name &&
83
+ !path.scope.getBinding('wp')) {
84
+ return memberName(callee);
85
+ }
86
+ }
87
+ return undefined;
88
+ }
89
+ /**
90
+ * Parse JS/JSX/TS/TSX with lexical scope resolution and untouched source positions.
91
+ * @param input
92
+ */
93
+ export function parseJavascriptTranslations(input) {
94
+ try {
95
+ const typescript = /\.[cm]?tsx?$/.test(input.file);
96
+ const jsx = /\.(jsx|tsx|js|mjs|cjs)$/.test(input.file);
97
+ const ast = parse(input.contents, {
98
+ sourceType: 'unambiguous',
99
+ plugins: [
100
+ ...(typescript ? ['typescript'] : []),
101
+ ...(jsx ? ['jsx'] : []),
102
+ ],
103
+ });
104
+ const calls = [];
105
+ traverse(ast, {
106
+ CallExpression(path) {
107
+ const name = functionName(path, input.legacyGlobals ?? true);
108
+ const signature = name && Object.hasOwn(signatures, name)
109
+ ? signatures[name]
110
+ : undefined;
111
+ if (name && signature) {
112
+ const statement = path.getStatementParent();
113
+ const commentAnchors = [];
114
+ let ancestor = path;
115
+ while (ancestor && ancestor !== statement) {
116
+ commentAnchors.push(ancestor.node.start ?? 0);
117
+ ancestor = ancestor.parentPath;
118
+ }
119
+ commentAnchors.push(statement?.node.start ?? 0);
120
+ calls.push({
121
+ name,
122
+ signature,
123
+ commentAnchors,
124
+ arguments: path.node.arguments.map(staticString),
125
+ file: input.file,
126
+ line: path.node.loc?.start.line,
127
+ column: (path.node.loc?.start.column ?? 0) + 1,
128
+ start: path.node.start ?? 0,
129
+ statementStart: statement?.node.start ?? path.node.start ?? 0,
130
+ });
131
+ }
132
+ },
133
+ });
134
+ return extractCalls(input, calls, (ast.comments ?? []).map((comment) => ({
135
+ start: comment.start ?? 0,
136
+ end: comment.end ?? 0,
137
+ value: comment.value,
138
+ })));
139
+ }
140
+ catch (error) {
141
+ return syntaxFailure(input.file, error);
142
+ }
143
+ }
@@ -0,0 +1,16 @@
1
+ import type { PackageMeta } from '../package-meta.ts';
2
+ import type { ExtractedMessage, ExtractionResult } from './catalog.ts';
3
+ import type { ParseSourceInput } from './extraction.ts';
4
+ /**
5
+ * Extract only the actual WordPress main file's translatable header values.
6
+ * @param meta
7
+ * @param contents
8
+ * @param domain
9
+ */
10
+ export declare function extractHeaderTranslations(meta: PackageMeta, contents: string, domain: string): ExtractedMessage[];
11
+ /**
12
+ * Walk versioned WordPress metadata tables, including array and wildcard fields.
13
+ * @param input
14
+ * @param kind
15
+ */
16
+ export declare function parseMetadataTranslations(input: ParseSourceInput, kind: 'block' | 'theme'): ExtractionResult;
@@ -0,0 +1,117 @@
1
+ import { findFileHeaderLine } from "../package-meta.js";
2
+ import { syntaxFailure } from "./extraction.js";
3
+ const blockFields = {
4
+ title: 'block title',
5
+ description: 'block description',
6
+ keywords: ['block keyword'],
7
+ styles: [{ label: 'block style label' }],
8
+ variations: [
9
+ {
10
+ title: 'block variation title',
11
+ description: 'block variation description',
12
+ keywords: ['block variation keyword'],
13
+ },
14
+ ],
15
+ };
16
+ const presetFields = {
17
+ typography: {
18
+ fontSizes: [{ name: 'Font size name' }],
19
+ fontFamilies: [{ name: 'Font family name' }],
20
+ },
21
+ color: {
22
+ palette: [{ name: 'Color name' }],
23
+ gradients: [{ name: 'Gradient name' }],
24
+ duotone: [{ name: 'Duotone name' }],
25
+ },
26
+ spacing: { spacingSizes: [{ name: 'Space size name' }] },
27
+ dimensions: { aspectRatios: [{ name: 'Aspect ratio name' }] },
28
+ };
29
+ const themeFields = {
30
+ title: 'Style variation name',
31
+ description: 'Style variation description',
32
+ settings: {
33
+ ...presetFields,
34
+ shadow: { presets: [{ name: 'Shadow name' }] },
35
+ blocks: { '*': presetFields },
36
+ },
37
+ customTemplates: [{ title: 'Custom template name' }],
38
+ templateParts: [{ title: 'Template part name' }],
39
+ };
40
+ function message(id, domain, file, context, label, line) {
41
+ return {
42
+ id,
43
+ domain,
44
+ ...(context ? { context } : {}),
45
+ translations: [],
46
+ flags: [],
47
+ comments: label ? [label] : [],
48
+ references: [{ file, ...(line ? { line } : {}) }],
49
+ };
50
+ }
51
+ /**
52
+ * Extract only the actual WordPress main file's translatable header values.
53
+ * @param meta
54
+ * @param contents
55
+ * @param domain
56
+ */
57
+ export function extractHeaderTranslations(meta, contents, domain) {
58
+ const fields = [
59
+ 'plugin' === meta.type ? 'Plugin Name' : 'Theme Name',
60
+ 'plugin' === meta.type ? 'Plugin URI' : 'Theme URI',
61
+ 'Description',
62
+ 'Author',
63
+ 'Author URI',
64
+ ];
65
+ return fields
66
+ .filter((field) => meta.headers[field])
67
+ .map((field) => message(meta.headers[field] ?? '', domain, meta.mainFile, undefined, `${field} of the ${meta.type}`, findFileHeaderLine(contents, field) ?? undefined));
68
+ }
69
+ /**
70
+ * Walk versioned WordPress metadata tables, including array and wildcard fields.
71
+ * @param input
72
+ * @param kind
73
+ */
74
+ export function parseMetadataTranslations(input, kind) {
75
+ try {
76
+ const data = JSON.parse(input.contents);
77
+ if (!data || 'object' !== typeof data || Array.isArray(data)) {
78
+ throw new Error('WordPress metadata must be a JSON object.');
79
+ }
80
+ const domain = 'block' === kind
81
+ ? data.textdomain ??
82
+ input.domain
83
+ : input.domain;
84
+ if ('string' !== typeof domain) {
85
+ throw new Error('Block textdomain must be a string.');
86
+ }
87
+ const messages = [];
88
+ function visit(value, table) {
89
+ if ('string' === typeof table && 'string' === typeof value) {
90
+ messages.push(message(value, domain, input.file, table));
91
+ }
92
+ else if (Array.isArray(table) &&
93
+ Array.isArray(value) &&
94
+ table[0]) {
95
+ value.forEach((item) => visit(item, table[0]));
96
+ }
97
+ else if (table &&
98
+ 'object' === typeof table &&
99
+ !Array.isArray(table) &&
100
+ value &&
101
+ 'object' === typeof value &&
102
+ !Array.isArray(value)) {
103
+ for (const [key, child] of Object.entries(value)) {
104
+ const schema = table[key] ?? table['*'];
105
+ if (schema) {
106
+ visit(child, schema);
107
+ }
108
+ }
109
+ }
110
+ }
111
+ visit(data, 'block' === kind ? blockFields : themeFields);
112
+ return { messages, diagnostics: [] };
113
+ }
114
+ catch (error) {
115
+ return syntaxFailure(input.file, error);
116
+ }
117
+ }
@@ -0,0 +1,52 @@
1
+ /** Gettext header values by name; `false` removes a header set by the defaults or an earlier layer. */
2
+ export type I18nHeaders = Record<string, string | false>;
3
+ export interface I18nOptions {
4
+ enabled?: boolean;
5
+ domain?: string;
6
+ allDomains?: boolean;
7
+ include?: string[];
8
+ exclude?: string[];
9
+ /** Package-relative directory; Domain Path or languages by default. */
10
+ destination?: string;
11
+ headers?: I18nHeaders;
12
+ /** Static comment lines to prepend to the POT; no year is generated. */
13
+ fileComment?: string;
14
+ references?: boolean;
15
+ strict?: boolean;
16
+ legacyGlobals?: boolean;
17
+ suppress?: string[];
18
+ }
19
+ export declare const DEFAULT_I18N_INCLUDE: string[];
20
+ export declare const DEFAULT_I18N_EXCLUDE: string[];
21
+ /**
22
+ * Validate external config independently of extraction and distribution file settings.
23
+ * @param input
24
+ * @param label
25
+ */
26
+ export declare function validateI18nOptions(input: unknown, label?: string): asserts input is I18nOptions | undefined;
27
+ /**
28
+ * Validate static text to prepend as a POT file comment.
29
+ * @param fileComment
30
+ * @param label
31
+ */
32
+ export declare function validateI18nFileComment(fileComment: unknown, label?: string): asserts fileComment is string;
33
+ /**
34
+ * Validate gettext header names and values independently of extraction options.
35
+ *
36
+ * @param headers Header values, or `false` to remove a header.
37
+ * @param label Config path for error messages.
38
+ * @throws {ValidationError} For invalid names or values, or a removed Content-Type.
39
+ */
40
+ export declare function validateI18nHeaders(headers: unknown, label?: string): asserts headers is I18nHeaders;
41
+ /**
42
+ * Scalars and lists replace inherited values; header dictionaries merge by key.
43
+ * @param layers Extraction settings in increasing precedence.
44
+ */
45
+ export declare function resolveI18nOptions(...layers: Array<I18nOptions | undefined>): I18nOptions;
46
+ /**
47
+ * Merge gettext header names case-insensitively, preserving deliberate empty values and removals.
48
+ *
49
+ * @param layers Header values in increasing precedence.
50
+ * @return Merged headers, with `false` for removed headers so that later layers can still set them.
51
+ */
52
+ export declare function mergeI18nHeaders(...layers: Array<I18nHeaders | undefined>): I18nHeaders;
@@ -0,0 +1,156 @@
1
+ import { ValidationError } from "../errors.js";
2
+ export const DEFAULT_I18N_INCLUDE = [
3
+ '**/*.{php,js,jsx,ts,tsx,mjs,cjs,mts,cts}',
4
+ '**/block.json',
5
+ '**/theme.json',
6
+ 'styles/**/*.json',
7
+ ];
8
+ export const DEFAULT_I18N_EXCLUDE = [
9
+ '**/node_modules/**',
10
+ '**/vendor/**',
11
+ '**/.*/**',
12
+ '**/tests/**',
13
+ '**/test/**',
14
+ '**/__tests__/**',
15
+ '**/*.test.*',
16
+ '**/*.spec.*',
17
+ '**/*.min.*',
18
+ '**/*.{pot,po,mo}',
19
+ '**/*.l10n.php',
20
+ ];
21
+ /**
22
+ * Validate external config independently of extraction and distribution file settings.
23
+ * @param input
24
+ * @param label
25
+ */
26
+ export function validateI18nOptions(input, label = 'i18n') {
27
+ if (undefined === input) {
28
+ return;
29
+ }
30
+ if (!input || 'object' !== typeof input || Array.isArray(input)) {
31
+ throw new ValidationError(`${label} must be an object.`);
32
+ }
33
+ const options = input;
34
+ const allowed = [
35
+ 'enabled',
36
+ 'domain',
37
+ 'allDomains',
38
+ 'include',
39
+ 'exclude',
40
+ 'destination',
41
+ 'headers',
42
+ 'fileComment',
43
+ 'references',
44
+ 'strict',
45
+ 'legacyGlobals',
46
+ 'suppress',
47
+ ];
48
+ for (const key of Object.keys(options)) {
49
+ if (!allowed.includes(key)) {
50
+ throw new ValidationError(`${label}.${key} is not a supported extraction option.`);
51
+ }
52
+ }
53
+ for (const key of [
54
+ 'enabled',
55
+ 'allDomains',
56
+ 'references',
57
+ 'strict',
58
+ 'legacyGlobals',
59
+ ]) {
60
+ if (undefined !== options[key] &&
61
+ 'boolean' !== typeof options[key]) {
62
+ throw new ValidationError(`${label}.${key} must be a boolean.`);
63
+ }
64
+ }
65
+ for (const key of ['domain', 'destination']) {
66
+ if (undefined !== options[key] &&
67
+ ('string' !== typeof options[key] || !options[key]?.trim())) {
68
+ throw new ValidationError(`${label}.${key} must be a non-empty string.`);
69
+ }
70
+ }
71
+ if (undefined !== options.fileComment) {
72
+ validateI18nFileComment(options.fileComment, `${label}.fileComment`);
73
+ }
74
+ for (const key of ['include', 'exclude', 'suppress']) {
75
+ const value = options[key];
76
+ if (undefined !== value &&
77
+ (!Array.isArray(value) ||
78
+ value.some((entry) => 'string' !== typeof entry || !entry.trim()))) {
79
+ throw new ValidationError(`${label}.${key} must be a list of non-empty strings.`);
80
+ }
81
+ }
82
+ if (undefined !== options.headers) {
83
+ validateI18nHeaders(options.headers, `${label}.headers`);
84
+ }
85
+ }
86
+ /**
87
+ * Validate static text to prepend as a POT file comment.
88
+ * @param fileComment
89
+ * @param label
90
+ */
91
+ export function validateI18nFileComment(fileComment, label = 'i18n.fileComment') {
92
+ if ('string' !== typeof fileComment ||
93
+ !fileComment.trim() ||
94
+ fileComment.includes('\0')) {
95
+ throw new ValidationError(`${label} must be a non-empty string without NUL characters.`);
96
+ }
97
+ }
98
+ /**
99
+ * Validate gettext header names and values independently of extraction options.
100
+ *
101
+ * @param headers Header values, or `false` to remove a header.
102
+ * @param label Config path for error messages.
103
+ * @throws {ValidationError} For invalid names or values, or a removed Content-Type.
104
+ */
105
+ export function validateI18nHeaders(headers, label = 'i18n.headers') {
106
+ if (!headers ||
107
+ 'object' !== typeof headers ||
108
+ Array.isArray(headers) ||
109
+ Object.entries(headers).some(([key, value]) => !/^[A-Za-z][A-Za-z0-9-]*$/.test(key) ||
110
+ (false !== value &&
111
+ ('string' !== typeof value || /[\r\n\0]/.test(value))))) {
112
+ throw new ValidationError(`${label} must contain gettext header names with single-line string values, or false to remove a header.`);
113
+ }
114
+ if (Object.entries(headers).some(([key, value]) => false === value && 'content-type' === key.toLowerCase())) {
115
+ throw new ValidationError(`${label} can't remove Content-Type, which declares the UTF-8 charset of the catalog.`);
116
+ }
117
+ }
118
+ /**
119
+ * Scalars and lists replace inherited values; header dictionaries merge by key.
120
+ * @param layers Extraction settings in increasing precedence.
121
+ */
122
+ export function resolveI18nOptions(...layers) {
123
+ let options = {
124
+ enabled: true,
125
+ include: DEFAULT_I18N_INCLUDE,
126
+ exclude: DEFAULT_I18N_EXCLUDE,
127
+ references: true,
128
+ legacyGlobals: true,
129
+ };
130
+ for (const layer of layers) {
131
+ validateI18nOptions(layer);
132
+ if (layer) {
133
+ options = {
134
+ ...options,
135
+ ...layer,
136
+ headers: mergeI18nHeaders(options.headers, layer.headers),
137
+ };
138
+ }
139
+ }
140
+ return options;
141
+ }
142
+ /**
143
+ * Merge gettext header names case-insensitively, preserving deliberate empty values and removals.
144
+ *
145
+ * @param layers Header values in increasing precedence.
146
+ * @return Merged headers, with `false` for removed headers so that later layers can still set them.
147
+ */
148
+ export function mergeI18nHeaders(...layers) {
149
+ const headers = new Map();
150
+ for (const layer of layers) {
151
+ for (const [key, value] of Object.entries(layer ?? {})) {
152
+ headers.set(key.toLowerCase(), [key, value]);
153
+ }
154
+ }
155
+ return Object.fromEntries(headers.values());
156
+ }
@@ -0,0 +1,24 @@
1
+ import type { I18nOptions } from './options.ts';
2
+ export interface I18nPackageInput {
3
+ cwd: string;
4
+ configFile?: string;
5
+ package?: string;
6
+ all?: boolean;
7
+ sourceDir?: string;
8
+ /** An explicit absolute output path; only supported for one package. */
9
+ outputPath?: string;
10
+ options?: I18nOptions;
11
+ /** Audit selection need not validate output destinations. */
12
+ validateOutputs?: boolean;
13
+ }
14
+ export interface I18nPackage {
15
+ package?: string;
16
+ sourceDir: string;
17
+ outputPath: string;
18
+ options: I18nOptions;
19
+ }
20
+ /**
21
+ * The config adapter resolves package directories and options before invoking primitives.
22
+ * @param input
23
+ */
24
+ export declare function resolveI18nPackages(input: I18nPackageInput): Promise<I18nPackage[]>;
@@ -0,0 +1,86 @@
1
+ import { basename, resolve } from 'node:path';
2
+ import { loadChangesetConfig } from "../changeset-config.js";
3
+ import { discoverConfiguredPackages, findPackage, } from "../changeset-packages.js";
4
+ import { ReleaseRepository } from "../release-repository.js";
5
+ import { readPackageMeta } from "../package-meta.js";
6
+ import { ValidationError } from "../errors.js";
7
+ import { validateI18nContainment } from "./atomic-output.js";
8
+ import { resolveI18nOptions } from "./options.js";
9
+ import { resolvePotPath, validatePotDestinations } from "./generate.js";
10
+ /**
11
+ * The config adapter resolves package directories and options before invoking primitives.
12
+ * @param input
13
+ */
14
+ export async function resolveI18nPackages(input) {
15
+ if ((input.package && input.all) ||
16
+ ((input.package || input.all) && input.sourceDir) ||
17
+ (input.all && input.outputPath)) {
18
+ throw new ValidationError('Choose a source directory, --package, or --all. --output requires one package.');
19
+ }
20
+ if ('' === input.package?.trim()) {
21
+ throw new ValidationError('--package must not be empty.');
22
+ }
23
+ const packages = [];
24
+ if (input.package || input.all) {
25
+ const loaded = await loadChangesetConfig(input);
26
+ if (!loaded) {
27
+ throw new ValidationError('--package and --all require wp-release configuration.');
28
+ }
29
+ const repository = new ReleaseRepository({
30
+ cwd: input.cwd,
31
+ });
32
+ const configured = await discoverConfiguredPackages(repository, loaded.config);
33
+ let selected = configured;
34
+ if (input.package) {
35
+ const match = findPackage(configured, input.package);
36
+ if (!match) {
37
+ throw new ValidationError(`Unknown package: ${input.package}.`);
38
+ }
39
+ selected = [match];
40
+ }
41
+ for (const pkg of selected) {
42
+ const options = resolveI18nOptions(loaded.config.i18n, pkg.group.i18n, input.options);
43
+ const meta = await readPackageMeta({
44
+ dir: pkg.directory,
45
+ slug: basename(pkg.directory),
46
+ });
47
+ if (input.package && (!options.enabled || !meta)) {
48
+ throw new ValidationError(`Package ${pkg.name} is disabled for i18n or has no WordPress headers.`);
49
+ }
50
+ if (options.enabled && meta) {
51
+ packages.push({
52
+ package: pkg.name,
53
+ sourceDir: pkg.directory,
54
+ options,
55
+ outputPath: input.outputPath ??
56
+ resolvePotPath(pkg.directory, meta, options),
57
+ });
58
+ }
59
+ }
60
+ }
61
+ else {
62
+ const sourceDir = resolve(input.cwd, input.sourceDir ?? '.');
63
+ const meta = await readPackageMeta({
64
+ dir: sourceDir,
65
+ slug: basename(sourceDir),
66
+ });
67
+ if (!meta) {
68
+ throw new ValidationError(`No WordPress plugin or theme headers found in ${sourceDir}.`);
69
+ }
70
+ const options = resolveI18nOptions(input.options);
71
+ packages.push({
72
+ sourceDir,
73
+ options,
74
+ outputPath: input.outputPath ?? resolvePotPath(sourceDir, meta, options),
75
+ });
76
+ }
77
+ if (false !== input.validateOutputs) {
78
+ await validatePotDestinations(packages.map((pkg) => pkg.outputPath));
79
+ if (!input.outputPath) {
80
+ for (const pkg of packages) {
81
+ await validateI18nContainment(pkg.sourceDir, pkg.outputPath);
82
+ }
83
+ }
84
+ }
85
+ return packages;
86
+ }
@@ -0,0 +1,9 @@
1
+ import type { CallSignature, ParseSourceInput } from './extraction.ts';
2
+ import type { ExtractionResult } from './catalog.ts';
3
+ /** PHP argument names and positions follow the WordPress l10n.php signatures. */
4
+ export declare const PHP_TRANSLATION_FUNCTIONS: Record<string, CallSignature>;
5
+ /**
6
+ * Parse PHP without executing it; namespaced fallback calls follow known file-local bindings.
7
+ * @param input
8
+ */
9
+ export declare function parsePhpTranslations(input: ParseSourceInput): ExtractionResult;