@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,96 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { posix } from 'node:path';
3
+ import { ValidationError } from "../errors.js";
4
+ import { compareText } from "./catalog.js";
5
+ import { translationKey } from "./po.js";
6
+ function normalizedPath(path) {
7
+ if (!path ||
8
+ path.includes('\\') ||
9
+ path.includes('\0') ||
10
+ posix.isAbsolute(path) ||
11
+ /^[A-Za-z]:/.test(path) ||
12
+ path
13
+ .split('/')
14
+ .some((component) => ['', '.', '..'].includes(component))) {
15
+ throw new ValidationError(`Script translation paths must be normalized package-relative paths: ${path}.`);
16
+ }
17
+ return path;
18
+ }
19
+ /**
20
+ * Compile deterministic Jed catalogs using explicit source/build mapping or unbundled identity paths.
21
+ * @param catalog
22
+ * @param mapping
23
+ */
24
+ export function compileScriptTranslations(catalog, mapping) {
25
+ if (!/^[A-Za-z0-9][A-Za-z0-9_.-]*$/.test(catalog.domain)) {
26
+ throw new ValidationError('JSON compilation requires a safe text domain filename component.');
27
+ }
28
+ if (mapping) {
29
+ for (const [source, scripts] of Object.entries(mapping)) {
30
+ normalizedPath(source);
31
+ if (!Array.isArray(scripts) ||
32
+ scripts.some((script) => 'string' !== typeof script)) {
33
+ throw new ValidationError('Script maps must contain arrays of runtime script paths.');
34
+ }
35
+ scripts.forEach(normalizedPath);
36
+ }
37
+ }
38
+ const groups = new Map();
39
+ for (const message of catalog.messages) {
40
+ for (const reference of message.references) {
41
+ const source = normalizedPath(reference.file);
42
+ let scripts = [];
43
+ if (mapping) {
44
+ scripts = Object.hasOwn(mapping, source)
45
+ ? mapping[source] ?? []
46
+ : [];
47
+ }
48
+ else if (/\.(?:js|mjs|cjs)$/.test(source)) {
49
+ scripts = [source];
50
+ }
51
+ else if (/\.(?:jsx|tsx?|mts|cts)$/.test(source)) {
52
+ throw new ValidationError(`A source-to-runtime script map is required for ${source}.`);
53
+ }
54
+ for (const script of scripts) {
55
+ // WordPress hashes the unminified script path when the registered URL ends in .min.js.
56
+ const runtime = normalizedPath(script).replace(/\.min\.js$/, '.js');
57
+ if (!/\.(?:js|mjs|cjs)$/.test(runtime)) {
58
+ throw new ValidationError(`Runtime translation target must be a JavaScript file: ${runtime}.`);
59
+ }
60
+ const entries = groups.get(runtime) ??
61
+ new Map();
62
+ entries.set(translationKey(message), message);
63
+ groups.set(runtime, entries);
64
+ }
65
+ }
66
+ }
67
+ return [...groups.entries()]
68
+ .sort(([a], [b]) => compareText(a, b))
69
+ .map(([source, entries]) => {
70
+ const translations = Object.create(null);
71
+ translations[''] = {
72
+ domain: catalog.domain,
73
+ lang: catalog.locale,
74
+ 'plural-forms': catalog.pluralForms,
75
+ };
76
+ for (const [key, message] of [...entries].sort(([a], [b]) => compareText(a, b))) {
77
+ translations[key] = [...message.translations];
78
+ }
79
+ const data = {
80
+ 'translation-revision-date': catalog.headers['po-revision-date'] ?? '',
81
+ generator: '@wpelevator/wp-release',
82
+ domain: catalog.domain,
83
+ locale_data: { messages: translations },
84
+ source,
85
+ };
86
+ return {
87
+ source,
88
+ file: `${'default' === catalog.domain
89
+ ? catalog.locale
90
+ : `${catalog.domain}-${catalog.locale}`}-${createHash('md5')
91
+ .update(source)
92
+ .digest('hex')}.json`,
93
+ bytes: Buffer.from(JSON.stringify(data) + '\n'),
94
+ };
95
+ });
96
+ }
@@ -0,0 +1,6 @@
1
+ import type { TranslatedCatalog } from './po.ts';
2
+ /**
3
+ * WordPress 6.5+ translation-array format; catalog text is never interpolated as PHP code.
4
+ * @param catalog
5
+ */
6
+ export declare function compilePhpTranslations(catalog: TranslatedCatalog): Buffer;
@@ -0,0 +1,35 @@
1
+ import { compareText } from "./catalog.js";
2
+ import { translationKey } from "./po.js";
3
+ /**
4
+ * Single-quoted PHP data values; NUL separators use an explicit safe literal expression.
5
+ * @param value
6
+ */
7
+ function phpString(value) {
8
+ return value
9
+ .split('\0')
10
+ .map((part) => `'${part
11
+ .replaceAll('\\', '\\\\')
12
+ .replaceAll("'", "\\'")}'`)
13
+ .join(' . "\\0" . ');
14
+ }
15
+ /**
16
+ * WordPress 6.5+ translation-array format; catalog text is never interpolated as PHP code.
17
+ * @param catalog
18
+ */
19
+ export function compilePhpTranslations(catalog) {
20
+ const headers = Object.entries(catalog.headers)
21
+ .filter(([key]) => 'messages' !== key)
22
+ .sort(([a], [b]) => compareText(a, b));
23
+ const messages = catalog.messages.map((message) => `\t\t${phpString(translationKey(message))} => ${phpString(message.translations.join('\0'))},`);
24
+ const lines = [
25
+ '<?php',
26
+ 'return [',
27
+ ...headers.map(([key, value]) => `\t${phpString(key)} => ${phpString(value)},`),
28
+ "\t'messages' => [",
29
+ ...messages,
30
+ '\t],',
31
+ '];',
32
+ '',
33
+ ];
34
+ return Buffer.from(lines.join('\n'));
35
+ }
package/dist/index.d.ts CHANGED
@@ -59,3 +59,31 @@ export type { SummaryContext } from './summary.ts';
59
59
  export type { CheckoutPath, DeployResult } from './svn-deploy.ts';
60
60
  export type { GitCommandInput, GitCommandOutput, GitRunner, RepositoryLocation, } from './git.ts';
61
61
  export type { GitDeployInput, GitDeployOptions, GitDeployResult, } from './git-deploy.ts';
62
+ export type { SingleProjectOptions } from './single-project.ts';
63
+ export { buildPackage } from './package-build.ts';
64
+ export type { BuildPackageInput, BuildPackageResult } from './package-build.ts';
65
+ export type { BuildResult } from './commands/build.ts';
66
+ export { wordpressCodec, sha256sumCodec, resolveManifestCodec, readIntegrity, } from './integrity.ts';
67
+ export type { ManifestCodec, IntegrityConfig } from './integrity.ts';
68
+ export { aggregateCatalog, hasFatalDiagnostics } from './i18n/catalog.ts';
69
+ export type { Catalog, CatalogMessage, ExtractedMessage, SourceReference, I18nDiagnostic, ExtractionResult, CatalogResult, } from './i18n/catalog.ts';
70
+ export { parsePhpTranslations } from './i18n/php.ts';
71
+ export { parseJavascriptTranslations } from './i18n/javascript.ts';
72
+ export { parseMetadataTranslations, extractHeaderTranslations, } from './i18n/metadata.ts';
73
+ export type { ParseSourceInput } from './i18n/extraction.ts';
74
+ export { auditCatalog } from './i18n/audit.ts';
75
+ export { serializePot } from './i18n/pot.ts';
76
+ export { discoverI18nFiles } from './i18n/files.ts';
77
+ export { resolveI18nOptions, DEFAULT_I18N_INCLUDE, DEFAULT_I18N_EXCLUDE, } from './i18n/options.ts';
78
+ export type { I18nHeaders, I18nOptions } from './i18n/options.ts';
79
+ export { extractCatalog, generatePackagePot, resolvePotPath, validatePotDestinations, } from './i18n/generate.ts';
80
+ export type { ExtractCatalogInput, ExtractCatalogResult, PackagePotInput, PackagePotResult, } from './i18n/generate.ts';
81
+ export { resolveI18nPackages } from './i18n/packages.ts';
82
+ export type { I18nPackageInput, I18nPackage } from './i18n/packages.ts';
83
+ export { parseTranslatedPo, compileMo } from './i18n/po.ts';
84
+ export type { TranslatedCatalog } from './i18n/po.ts';
85
+ export { compileScriptTranslations } from './i18n/translation-json.ts';
86
+ export type { ScriptTranslationMap, ScriptTranslationOutput, } from './i18n/translation-json.ts';
87
+ export { compilePhpTranslations } from './i18n/translation-php.ts';
88
+ export { compileTranslations } from './i18n/compile.ts';
89
+ export type { CompileTranslationsInput, CompileTranslationsResult, } from './i18n/compile.ts';
package/dist/index.js CHANGED
@@ -27,3 +27,19 @@ export { GitDeploy, getDefaultToken, getGitAuthEnv, isValidRefName, resolveGitDe
27
27
  export { getGitDeployInputFromArgs } from "./commands/deploy-git.js";
28
28
  export { getDeployPackages, getGitDeploys, getPackageFiles, getSvnDeploys, } from "./package-deploy.js";
29
29
  export { zipPackage } from "./package-zip.js";
30
+ export { buildPackage } from "./package-build.js";
31
+ export { wordpressCodec, sha256sumCodec, resolveManifestCodec, readIntegrity, } from "./integrity.js";
32
+ export { aggregateCatalog, hasFatalDiagnostics } from "./i18n/catalog.js";
33
+ export { parsePhpTranslations } from "./i18n/php.js";
34
+ export { parseJavascriptTranslations } from "./i18n/javascript.js";
35
+ export { parseMetadataTranslations, extractHeaderTranslations, } from "./i18n/metadata.js";
36
+ export { auditCatalog } from "./i18n/audit.js";
37
+ export { serializePot } from "./i18n/pot.js";
38
+ export { discoverI18nFiles } from "./i18n/files.js";
39
+ export { resolveI18nOptions, DEFAULT_I18N_INCLUDE, DEFAULT_I18N_EXCLUDE, } from "./i18n/options.js";
40
+ export { extractCatalog, generatePackagePot, resolvePotPath, validatePotDestinations, } from "./i18n/generate.js";
41
+ export { resolveI18nPackages } from "./i18n/packages.js";
42
+ export { parseTranslatedPo, compileMo } from "./i18n/po.js";
43
+ export { compileScriptTranslations } from "./i18n/translation-json.js";
44
+ export { compilePhpTranslations } from "./i18n/translation-php.js";
45
+ export { compileTranslations } from "./i18n/compile.js";
@@ -0,0 +1,33 @@
1
+ import type { ChecksumsManifest, ChecksumAlgorithm } from './checksums.ts';
2
+ import type { SignatureFormat } from './signing.ts';
3
+ /** Custom codecs receive the validated WordPress-compatible integrity model. */
4
+ export interface ManifestCodec {
5
+ id: string;
6
+ filename: string;
7
+ encode: (manifest: ChecksumsManifest) => Uint8Array | Promise<Uint8Array>;
8
+ decode: (bytes: Uint8Array) => ChecksumsManifest | Promise<ChecksumsManifest>;
9
+ }
10
+ export interface IntegrityConfig {
11
+ codec?: 'wordpress-json' | 'sha256sum' | ManifestCodec;
12
+ algorithms?: ChecksumAlgorithm[];
13
+ file?: string;
14
+ embed?: boolean;
15
+ sign?: false | {
16
+ formats?: SignatureFormat[];
17
+ };
18
+ }
19
+ export declare const wordpressCodec: ManifestCodec;
20
+ export declare const sha256sumCodec: ManifestCodec;
21
+ export declare function resolveManifestCodec(value?: IntegrityConfig['codec']): ManifestCodec;
22
+ /**
23
+ * Relative artifact paths cannot escape their destination or use Windows paths.
24
+ * @param file
25
+ */
26
+ export declare function checkChecksumsFile(file: string): string;
27
+ export declare function readIntegrity(path: string, codec?: IntegrityConfig['codec']): Promise<ChecksumsManifest>;
28
+ /**
29
+ * Encode once and ensure the format preserves the selected payload hashes.
30
+ * @param codec
31
+ * @param manifest
32
+ */
33
+ export declare function encodeIntegrity(codec: ManifestCodec, manifest: ChecksumsManifest): Promise<Buffer>;
@@ -0,0 +1,115 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { basename } from 'node:path';
3
+ import { parseChecksums, readChecksums } from "./checksums.js";
4
+ import { ValidationError } from "./errors.js";
5
+ export const wordpressCodec = {
6
+ id: 'wordpress-json',
7
+ filename: 'checksums.json',
8
+ encode: (manifest) => Buffer.from(JSON.stringify(manifest, null, '\t') + '\n'),
9
+ decode: (bytes) => parseChecksums(Buffer.from(bytes).toString('utf8')),
10
+ };
11
+ export const sha256sumCodec = {
12
+ id: 'sha256sum',
13
+ filename: 'SHA256SUMS',
14
+ encode(manifest) {
15
+ const lines = Object.entries(manifest.files)
16
+ .sort()
17
+ .map(([path, hashes]) => {
18
+ if ('string' !== typeof hashes.sha256) {
19
+ throw new ValidationError('sha256sum requires one SHA-256 digest per file.');
20
+ }
21
+ const escaped = /[\\\n\r]/.test(path);
22
+ const name = path
23
+ .replaceAll('\\', '\\\\')
24
+ .replaceAll('\n', '\\n')
25
+ .replaceAll('\r', '\\r');
26
+ return `${escaped ? '\\' : ''}${hashes.sha256} ${name}`;
27
+ });
28
+ return Buffer.from(lines.join('\n') + '\n');
29
+ },
30
+ decode(bytes) {
31
+ const files = Object.create(null);
32
+ for (const line of Buffer.from(bytes)
33
+ .toString('utf8')
34
+ .split('\n')
35
+ .filter(Boolean)) {
36
+ const match = /^(\\?)([a-fA-F0-9]{64}) [ *](.+)$/.exec(line);
37
+ if (!match) {
38
+ throw new ValidationError('Invalid SHA256SUMS entry.');
39
+ }
40
+ const path = match[1]
41
+ ? (match[3] ?? '').replace(/\\([\\nr])/g, (_, char) => ({ n: '\n', r: '\r', '\\': '\\' })[char] ?? '')
42
+ : match[3] ?? '';
43
+ if (Object.hasOwn(files, path)) {
44
+ throw new ValidationError(`Duplicate checksum path: ${path}`);
45
+ }
46
+ files[path] = { sha256: (match[2] ?? '').toLowerCase() };
47
+ }
48
+ return parseChecksums(JSON.stringify({ files }));
49
+ },
50
+ };
51
+ export function resolveManifestCodec(value = 'wordpress-json') {
52
+ if (typeof value === 'string') {
53
+ if (!['wordpress-json', 'sha256sum'].includes(value)) {
54
+ throw new ValidationError(`Unknown checksums format: ${value}.`);
55
+ }
56
+ return value === 'sha256sum' ? sha256sumCodec : wordpressCodec;
57
+ }
58
+ if (!value ||
59
+ typeof value.encode !== 'function' ||
60
+ typeof value.decode !== 'function' ||
61
+ !value.id ||
62
+ !value.filename) {
63
+ throw new ValidationError('A checksums codec needs id, filename, encode and decode.');
64
+ }
65
+ return value;
66
+ }
67
+ /**
68
+ * Relative artifact paths cannot escape their destination or use Windows paths.
69
+ * @param file
70
+ */
71
+ export function checkChecksumsFile(file) {
72
+ if (!file ||
73
+ /[\\\x00-\x1f:]/.test(file) ||
74
+ file
75
+ .split('/')
76
+ .some((part) => !part || part === '.' || part === '..')) {
77
+ throw new ValidationError('--checksums-file must be a relative file path with forward slashes and no dot segments.');
78
+ }
79
+ return file;
80
+ }
81
+ export async function readIntegrity(path, codec) {
82
+ const selected = resolveManifestCodec(codec ??
83
+ (basename(path) === 'SHA256SUMS'
84
+ ? 'sha256sum'
85
+ : 'wordpress-json'));
86
+ const manifest = selected === wordpressCodec
87
+ ? await readChecksums(path)
88
+ : await selected.decode(await readFile(path));
89
+ return parseChecksums(JSON.stringify(manifest), path);
90
+ }
91
+ /**
92
+ * Encode once and ensure the format preserves the selected payload hashes.
93
+ * @param codec
94
+ * @param manifest
95
+ */
96
+ export async function encodeIntegrity(codec, manifest) {
97
+ const bytes = Buffer.from(await codec.encode(manifest));
98
+ const decoded = parseChecksums(JSON.stringify(await codec.decode(bytes)));
99
+ const expected = Object.keys(manifest.files);
100
+ const actual = Object.keys(decoded.files);
101
+ const preserved = expected.length === actual.length &&
102
+ actual.every((path) => {
103
+ const hashes = decoded.files[path];
104
+ return (Boolean(manifest.files[path]) &&
105
+ Object.entries(hashes ?? {}).every(([algorithm, digest]) => digest ===
106
+ manifest.files[path]?.[algorithm]));
107
+ });
108
+ if (!preserved ||
109
+ ['plugin', 'theme', 'version'].some((key) => decoded[key] !== undefined &&
110
+ decoded[key] !==
111
+ manifest[key])) {
112
+ throw new ValidationError('The checksums codec must preserve payload paths, digests and any represented package identity.');
113
+ }
114
+ return bytes;
115
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Validate generated file destinations before writing release artifacts.
3
+ *
4
+ * @param paths Output file paths.
5
+ * @param force Whether existing regular files can be overwritten.
6
+ */
7
+ export declare function checkOutputFiles(paths: string[], force: boolean): void;
@@ -0,0 +1,25 @@
1
+ import { lstatSync } from 'node:fs';
2
+ import { ValidationError } from "./errors.js";
3
+ import { isErrorCode } from "./file-selection.js";
4
+ /**
5
+ * Validate generated file destinations before writing release artifacts.
6
+ *
7
+ * @param paths Output file paths.
8
+ * @param force Whether existing regular files can be overwritten.
9
+ */
10
+ export function checkOutputFiles(paths, force) {
11
+ for (const path of paths) {
12
+ let existing;
13
+ try {
14
+ existing = lstatSync(path);
15
+ }
16
+ catch (error) {
17
+ if (!isErrorCode(error, 'ENOENT')) {
18
+ throw error;
19
+ }
20
+ }
21
+ if (existing && (!force || !existing.isFile())) {
22
+ throw new ValidationError(`${path} already exists, pass --force to overwrite a regular file.`);
23
+ }
24
+ }
25
+ }
@@ -0,0 +1,33 @@
1
+ import type { PackageType } from './checksums.ts';
2
+ import type { FileSelection } from './file-selection.ts';
3
+ export interface BuildPackageInput {
4
+ sourceDir: string;
5
+ selection: FileSelection;
6
+ outputDir: string;
7
+ force?: boolean;
8
+ /** Write a checksums manifest alongside the build directory. */
9
+ checksums?: boolean;
10
+ /** Slug and version for the manifest. */
11
+ slug?: string;
12
+ version?: string | null;
13
+ type?: PackageType;
14
+ }
15
+ export interface BuildPackageResult {
16
+ outputDir: string;
17
+ files: number;
18
+ checksumsPath?: string;
19
+ }
20
+ /**
21
+ * Get the manifest path alongside a build directory.
22
+ *
23
+ * @param outputDir Build directory path.
24
+ * @return Absolute manifest path.
25
+ */
26
+ export declare function getBuildChecksumsPath(outputDir: string): string;
27
+ /**
28
+ * Copy the release selection into a directory, replacing an existing build only with force.
29
+ *
30
+ * @param input Source selection, destination and overwrite option.
31
+ * @return Destination and number of copied files.
32
+ */
33
+ export declare function buildPackage(input: BuildPackageInput): Promise<BuildPackageResult>;
@@ -0,0 +1,112 @@
1
+ import { CHECKSUMS_EXTENSION, createChecksums, writeChecksums, } from "./checksums.js";
2
+ import { checkOutputFiles } from "./output-files.js";
3
+ import { copyFile, lstat, mkdir, mkdtemp, realpath, rename, rm, } from 'node:fs/promises';
4
+ import { basename, dirname, isAbsolute, join, relative, resolve, } from 'node:path';
5
+ import { ValidationError } from "./errors.js";
6
+ import { isErrorCode } from "./file-selection.js";
7
+ import { getPackageFilesToRelease } from "./package-files.js";
8
+ /**
9
+ * Get the manifest path alongside a build directory.
10
+ *
11
+ * @param outputDir Build directory path.
12
+ * @return Absolute manifest path.
13
+ */
14
+ export function getBuildChecksumsPath(outputDir) {
15
+ return resolve(outputDir) + CHECKSUMS_EXTENSION;
16
+ }
17
+ /**
18
+ * Resolve existing ancestors too, so symlinked parents cannot overlap the source.
19
+ *
20
+ * @param path Directory path.
21
+ * @return Canonical path, including any missing child directories.
22
+ */
23
+ async function resolveDirectoryPath(path) {
24
+ try {
25
+ return await realpath(path);
26
+ }
27
+ catch (error) {
28
+ if (isErrorCode(error, 'ENOENT')) {
29
+ return join(await resolveDirectoryPath(dirname(path)), basename(path));
30
+ }
31
+ throw error;
32
+ }
33
+ }
34
+ /**
35
+ * Copy the release selection into a directory, replacing an existing build only with force.
36
+ *
37
+ * @param input Source selection, destination and overwrite option.
38
+ * @return Destination and number of copied files.
39
+ */
40
+ export async function buildPackage(input) {
41
+ const sourceDir = await realpath(input.sourceDir);
42
+ const outputDir = resolve(input.outputDir);
43
+ const canonicalOutput = await resolveDirectoryPath(outputDir);
44
+ const sourceFromOutput = relative(canonicalOutput, sourceDir);
45
+ const outputContainsSource = !sourceFromOutput ||
46
+ (!isAbsolute(sourceFromOutput) &&
47
+ sourceFromOutput !== '..' &&
48
+ !sourceFromOutput.startsWith('../'));
49
+ if (outputContainsSource) {
50
+ throw new ValidationError('The build output must not be the source directory or an ancestor of it.');
51
+ }
52
+ const existing = await lstat(outputDir).catch((error) => {
53
+ if (isErrorCode(error, 'ENOENT')) {
54
+ return null;
55
+ }
56
+ throw error;
57
+ });
58
+ if (existing && (!input.force || !existing.isDirectory())) {
59
+ throw new ValidationError(`${outputDir} already exists; use --force to replace an existing directory.`);
60
+ }
61
+ const checksumsPath = input.checksums
62
+ ? getBuildChecksumsPath(outputDir)
63
+ : null;
64
+ if (checksumsPath) {
65
+ checkOutputFiles([checksumsPath], input.force ?? false);
66
+ }
67
+ const { files, rules } = await getPackageFilesToRelease({
68
+ sourceDir,
69
+ selection: input.selection,
70
+ outputs: [
71
+ canonicalOutput,
72
+ ...(checksumsPath
73
+ ? [canonicalOutput + CHECKSUMS_EXTENSION]
74
+ : []),
75
+ ],
76
+ excludeSignatures: !!checksumsPath,
77
+ operation: 'build',
78
+ });
79
+ await mkdir(dirname(outputDir), { recursive: true });
80
+ const staging = await mkdtemp(join(dirname(outputDir), '.wp-release-build-'));
81
+ try {
82
+ for (const file of files) {
83
+ const target = join(staging, file);
84
+ await mkdir(dirname(target), { recursive: true });
85
+ await copyFile(join(sourceDir, file), target);
86
+ }
87
+ const manifest = checksumsPath
88
+ ? await createChecksums({
89
+ sourceDir: staging,
90
+ rules,
91
+ slug: input.slug ?? basename(sourceDir),
92
+ version: input.version,
93
+ type: input.type,
94
+ })
95
+ : null;
96
+ if (existing) {
97
+ await rm(outputDir, { recursive: true });
98
+ }
99
+ await rename(staging, outputDir);
100
+ if (checksumsPath && manifest) {
101
+ await writeChecksums(checksumsPath, manifest, input.force);
102
+ }
103
+ }
104
+ finally {
105
+ await rm(staging, { recursive: true, force: true });
106
+ }
107
+ return {
108
+ outputDir,
109
+ files: files.length,
110
+ ...(checksumsPath ? { checksumsPath } : {}),
111
+ };
112
+ }
@@ -0,0 +1,25 @@
1
+ import type { FileRules, FileSelection } from './file-selection.ts';
2
+ interface PackageFilesInput {
3
+ /** Directory whose release files are selected. */
4
+ sourceDir: string;
5
+ /** Resolved ignore file and exclusion options. */
6
+ selection: FileSelection;
7
+ /** Absolute output paths to leave out of the release. */
8
+ outputs: string[];
9
+ /** Also exclude signatures of the output files. */
10
+ excludeSignatures?: boolean;
11
+ /** Operation name used in an empty-selection error. */
12
+ operation: 'build' | 'archive';
13
+ }
14
+ /**
15
+ * Prepare the release file selection shared by directory builds and ZIPs.
16
+ *
17
+ * @param input Source, file selection and output exclusions.
18
+ * @return Sorted file paths and the rules used to select them.
19
+ * @throws {ValidationError} When no release files are selected.
20
+ */
21
+ export declare function getPackageFilesToRelease(input: PackageFilesInput): Promise<{
22
+ files: string[];
23
+ rules: FileRules;
24
+ }>;
25
+ export {};
@@ -0,0 +1,24 @@
1
+ import { ValidationError } from "./errors.js";
2
+ import { ignoreOwnFile, ignoreOwnSignature, loadFileRules, selectFiles, } from "./file-selection.js";
3
+ /**
4
+ * Prepare the release file selection shared by directory builds and ZIPs.
5
+ *
6
+ * @param input Source, file selection and output exclusions.
7
+ * @return Sorted file paths and the rules used to select them.
8
+ * @throws {ValidationError} When no release files are selected.
9
+ */
10
+ export async function getPackageFilesToRelease(input) {
11
+ const { sourceDir, selection, outputs, excludeSignatures, operation } = input;
12
+ const rules = await loadFileRules(selection);
13
+ outputs.forEach((path) => {
14
+ ignoreOwnFile(rules, sourceDir, path);
15
+ if (excludeSignatures) {
16
+ ignoreOwnSignature(rules, sourceDir, path);
17
+ }
18
+ });
19
+ const files = await selectFiles(sourceDir, rules);
20
+ if (!files.length) {
21
+ throw new ValidationError(`No files to ${operation} in ${sourceDir}.`);
22
+ }
23
+ return { files, rules };
24
+ }
@@ -1,3 +1,5 @@
1
+ import type { IntegrityConfig } from './integrity.ts';
2
+ import type { SignFilesResult, SignatureFormat } from './signing.ts';
1
3
  import type { PackageType } from './checksums.ts';
2
4
  import type { FileSelection } from './file-selection.ts';
3
5
  export interface ZipPackageInput {
@@ -15,8 +17,13 @@ export interface ZipPackageInput {
15
17
  version?: string | null;
16
18
  /** Package type for the checksums manifest. */
17
19
  type?: PackageType;
18
- /** Also write the checksums manifest next to the archive. */
20
+ /** Generate a checksums manifest beside the archive or embedded in it. */
19
21
  checksums?: boolean;
22
+ checksumsFile?: string;
23
+ embedChecksums?: boolean;
24
+ integrity?: IntegrityConfig;
25
+ /** Sign the manifest only, before embedding it. */
26
+ signChecksums?: SignatureFormat[];
20
27
  /** Overwrite existing output files. */
21
28
  force?: boolean;
22
29
  }
@@ -24,8 +31,13 @@ export interface ZipPackageInput {
24
31
  export interface ZipPackageResult {
25
32
  /** Absolute path of the ZIP. */
26
33
  zipPath: string;
27
- /** Absolute path of the checksums manifest, set with checksums. */
34
+ /** Absolute path of the external manifest; absent when disabled or embedded. */
28
35
  checksumsPath?: string;
36
+ /** Relative path inside the package, only in embedded mode. */
37
+ checksumsEmbedded?: string;
38
+ /** Manifest signatures, external paths or package-relative embedded paths. */
39
+ checksumsSignatures?: string[];
40
+ checksumsSigning?: SignFilesResult;
29
41
  /** Number of archived files. */
30
42
  files: number;
31
43
  }