@wpelevator/wp-release 0.0.0-stage → 0.1.1

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 (49) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +316 -2
  3. package/dist/bin/wp-release.d.ts +2 -0
  4. package/dist/bin/wp-release.js +3 -0
  5. package/dist/checksums.d.ts +138 -0
  6. package/dist/checksums.js +216 -0
  7. package/dist/cli.d.ts +42 -0
  8. package/dist/cli.js +163 -0
  9. package/dist/commands/args.d.ts +81 -0
  10. package/dist/commands/args.js +116 -0
  11. package/dist/commands/checksums.d.ts +34 -0
  12. package/dist/commands/checksums.js +76 -0
  13. package/dist/commands/command.d.ts +63 -0
  14. package/dist/commands/command.js +33 -0
  15. package/dist/commands/deploy-svn.d.ts +74 -0
  16. package/dist/commands/deploy-svn.js +103 -0
  17. package/dist/commands/verify.d.ts +27 -0
  18. package/dist/commands/verify.js +60 -0
  19. package/dist/commands/version-check.d.ts +38 -0
  20. package/dist/commands/version-check.js +80 -0
  21. package/dist/commands/zip.d.ts +41 -0
  22. package/dist/commands/zip.js +138 -0
  23. package/dist/deploy-options.d.ts +136 -0
  24. package/dist/deploy-options.js +168 -0
  25. package/dist/errors.d.ts +16 -0
  26. package/dist/errors.js +16 -0
  27. package/dist/file-selection.d.ts +78 -0
  28. package/dist/file-selection.js +133 -0
  29. package/dist/filename.d.ts +17 -0
  30. package/dist/filename.js +23 -0
  31. package/dist/index.d.ts +30 -0
  32. package/dist/index.js +15 -0
  33. package/dist/logger.d.ts +42 -0
  34. package/dist/logger.js +56 -0
  35. package/dist/package-meta.d.ts +112 -0
  36. package/dist/package-meta.js +222 -0
  37. package/dist/svn-deploy.d.ts +208 -0
  38. package/dist/svn-deploy.js +485 -0
  39. package/dist/svn.d.ts +209 -0
  40. package/dist/svn.js +298 -0
  41. package/dist/sync.d.ts +32 -0
  42. package/dist/sync.js +90 -0
  43. package/dist/unzip.d.ts +45 -0
  44. package/dist/unzip.js +121 -0
  45. package/dist/version-check.d.ts +75 -0
  46. package/dist/version-check.js +193 -0
  47. package/dist/zip.d.ts +32 -0
  48. package/dist/zip.js +53 -0
  49. package/package.json +61 -5
@@ -0,0 +1,17 @@
1
+ export declare const FILENAME_FORMAT = "{slug}.{version}";
2
+ export interface FilenameValues {
3
+ /** Plugin or theme slug. */
4
+ slug: string;
5
+ /** Version, if known. */
6
+ version?: string | null;
7
+ }
8
+ /**
9
+ * Format the release filename (without an extension) from placeholders. The
10
+ * default format falls back to just the slug when the version is unknown.
11
+ *
12
+ * @param format Format with {slug} and {version} placeholders.
13
+ * @param values Placeholder values.
14
+ * @return Filename without an extension.
15
+ * @throws {ValidationError} If the format needs a version that is unknown.
16
+ */
17
+ export declare function formatFilename(format: string, values: FilenameValues): string;
@@ -0,0 +1,23 @@
1
+ import { ValidationError } from "./errors.js";
2
+ export const FILENAME_FORMAT = '{slug}.{version}';
3
+ /**
4
+ * Format the release filename (without an extension) from placeholders. The
5
+ * default format falls back to just the slug when the version is unknown.
6
+ *
7
+ * @param format Format with {slug} and {version} placeholders.
8
+ * @param values Placeholder values.
9
+ * @return Filename without an extension.
10
+ * @throws {ValidationError} If the format needs a version that is unknown.
11
+ */
12
+ export function formatFilename(format, values) {
13
+ const { slug, version = null } = values;
14
+ if (!version && FILENAME_FORMAT === format) {
15
+ return slug;
16
+ }
17
+ if (!version && format.includes('{version}')) {
18
+ throw new ValidationError(`The filename format "${format}" needs a version, pass --version.`);
19
+ }
20
+ return format
21
+ .replaceAll('{slug}', slug)
22
+ .replaceAll('{version}', version ?? '');
23
+ }
@@ -0,0 +1,30 @@
1
+ export { run, commands, getHelp, getCommandHelp } from './cli.ts';
2
+ export { getDeployInputFromArgs } from './commands/deploy-svn.ts';
3
+ export { ValidationError, CredentialsError } from './errors.ts';
4
+ export { Logger } from './logger.ts';
5
+ export { SVN_URL, isPrerelease, normalizeTag, isTagRef, isValidSlug, isValidVersion, getDeployOptionErrors, getDeployOptionWarnings, resolveDeployOptions, } from './deploy-options.ts';
6
+ export { DISTIGNORE_FILENAME, parseExclude, resolveFileSelection, loadFileRules, selectFiles, } from './file-selection.ts';
7
+ export { FILENAME_FORMAT, formatFilename } from './filename.ts';
8
+ export { CHECKSUMS_EXTENSION, CHECKSUM_ALGORITHMS, hashFile, hashFiles, createChecksums, writeChecksums, readChecksums, parseChecksums, compareChecksums, verifyChecksums, } from './checksums.ts';
9
+ export { HEADER_BYTES, PLUGIN_HEADERS, THEME_HEADERS, README_HEADERS, README_FILENAMES, parseFileHeaders, parseReadmeHeaders, readFileStart, findMainPluginFile, readPackageMeta, } from './package-meta.ts';
10
+ export { ZIP_MTIME, ZIP_FILE_MODE, createZip } from './zip.ts';
11
+ export { readZipEntries, getZipEntryErrors, extractZip } from './unzip.ts';
12
+ export { GIT_TAG_FORMAT, getGitTagVersion, findConstantValues, findVersionMismatches, } from './version-check.ts';
13
+ export { Svn, SvnError, createSvnRunner, parseStatus, toSvnPath, } from './svn.ts';
14
+ export { syncDirectory, copyFileIfChanged } from './sync.ts';
15
+ export { ASSET_MIME_TYPES, SvnDeploy } from './svn-deploy.ts';
16
+ export type { CliIo } from './cli.ts';
17
+ export type { Command, CommandContext, CommandOutcome, ParsedValues, } from './commands/command.ts';
18
+ export type { DeployArgsInput } from './commands/deploy-svn.ts';
19
+ export type { LoggerOptions, OutputStream } from './logger.ts';
20
+ export type { DeployInput, DeployOptions } from './deploy-options.ts';
21
+ export type { FileRules, FileSelection, FileSelectionInput, } from './file-selection.ts';
22
+ export type { FilenameValues } from './filename.ts';
23
+ export type { ChecksumAlgorithm, ChecksumsComparison, ChecksumsManifest, CreateChecksumsInput, ExpectedHashes, FileHashes, PackageType, VerifyChecksumsInput, } from './checksums.ts';
24
+ export type { FindMainPluginFileInput, HeaderValues, PackageMeta, ParseFileHeadersInput, ReadPackageMetaInput, ReadmeMeta, } from './package-meta.ts';
25
+ export type { CreateZipInput } from './zip.ts';
26
+ export type { ExtractZipInput, ZipEntryInfo, ZipEntryType } from './unzip.ts';
27
+ export type { FindVersionMismatchesInput, GitTagInput, VersionCheckResult, VersionMismatch, } from './version-check.ts';
28
+ export type { SvnCheckoutInput, SvnCommandInput, SvnCommandOutput, SvnCommitInput, SvnCopyInput, SvnDepth, SvnOptions, SvnPathsInput, SvnPropsetInput, SvnRunner, SvnStatusEntry, SvnStatusType, SvnUpdateInput, } from './svn.ts';
29
+ export type { SyncDirectoryInput, SyncResult } from './sync.ts';
30
+ export type { CheckoutPath, DeployResult } from './svn-deploy.ts';
package/dist/index.js ADDED
@@ -0,0 +1,15 @@
1
+ export { run, commands, getHelp, getCommandHelp } from "./cli.js";
2
+ export { getDeployInputFromArgs } from "./commands/deploy-svn.js";
3
+ export { ValidationError, CredentialsError } from "./errors.js";
4
+ export { Logger } from "./logger.js";
5
+ export { SVN_URL, isPrerelease, normalizeTag, isTagRef, isValidSlug, isValidVersion, getDeployOptionErrors, getDeployOptionWarnings, resolveDeployOptions, } from "./deploy-options.js";
6
+ export { DISTIGNORE_FILENAME, parseExclude, resolveFileSelection, loadFileRules, selectFiles, } from "./file-selection.js";
7
+ export { FILENAME_FORMAT, formatFilename } from "./filename.js";
8
+ export { CHECKSUMS_EXTENSION, CHECKSUM_ALGORITHMS, hashFile, hashFiles, createChecksums, writeChecksums, readChecksums, parseChecksums, compareChecksums, verifyChecksums, } from "./checksums.js";
9
+ export { HEADER_BYTES, PLUGIN_HEADERS, THEME_HEADERS, README_HEADERS, README_FILENAMES, parseFileHeaders, parseReadmeHeaders, readFileStart, findMainPluginFile, readPackageMeta, } from "./package-meta.js";
10
+ export { ZIP_MTIME, ZIP_FILE_MODE, createZip } from "./zip.js";
11
+ export { readZipEntries, getZipEntryErrors, extractZip } from "./unzip.js";
12
+ export { GIT_TAG_FORMAT, getGitTagVersion, findConstantValues, findVersionMismatches, } from "./version-check.js";
13
+ export { Svn, SvnError, createSvnRunner, parseStatus, toSvnPath, } from "./svn.js";
14
+ export { syncDirectory, copyFileIfChanged } from "./sync.js";
15
+ export { ASSET_MIME_TYPES, SvnDeploy } from "./svn-deploy.js";
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Stream that output is written to, like process.stdout.
3
+ */
4
+ export interface OutputStream {
5
+ write: (text: string) => unknown;
6
+ }
7
+ export interface LoggerOptions {
8
+ /** Use GitHub Actions workflow commands. */
9
+ ci?: boolean;
10
+ /** Print errors only. */
11
+ quiet?: boolean;
12
+ /** Print debug messages. */
13
+ verbose?: boolean;
14
+ /** Stream for regular output. */
15
+ stdout?: OutputStream;
16
+ /** Stream for warnings and errors. */
17
+ stderr?: OutputStream;
18
+ }
19
+ /**
20
+ * Logger that prints plain text locally and GitHub Actions workflow commands in CI.
21
+ */
22
+ export declare class Logger {
23
+ readonly ci: boolean;
24
+ readonly quiet: boolean;
25
+ readonly verbose: boolean;
26
+ readonly stdout: OutputStream;
27
+ readonly stderr: OutputStream;
28
+ constructor({ ci, quiet, verbose, stdout, stderr, }?: LoggerOptions);
29
+ info(message: string): void;
30
+ debug(message: string): void;
31
+ notice(message: string): void;
32
+ warning(message: string): void;
33
+ error(message: string): void;
34
+ /**
35
+ * Run a callback inside a collapsible log group.
36
+ *
37
+ * @param title Group title.
38
+ * @param callback Callback to run inside the group.
39
+ * @return Callback result.
40
+ */
41
+ group<T>(title: string, callback: () => T | Promise<T>): Promise<T>;
42
+ }
package/dist/logger.js ADDED
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Logger that prints plain text locally and GitHub Actions workflow commands in CI.
3
+ */
4
+ export class Logger {
5
+ ci;
6
+ quiet;
7
+ verbose;
8
+ stdout;
9
+ stderr;
10
+ constructor({ ci = 'true' === process.env.GITHUB_ACTIONS, quiet = false, verbose = false, stdout = process.stdout, stderr = process.stderr, } = {}) {
11
+ this.ci = ci;
12
+ this.quiet = quiet;
13
+ this.verbose = verbose;
14
+ this.stdout = stdout;
15
+ this.stderr = stderr;
16
+ }
17
+ info(message) {
18
+ if (!this.quiet) {
19
+ this.stdout.write(`${message}\n`);
20
+ }
21
+ }
22
+ debug(message) {
23
+ if (this.verbose && !this.quiet) {
24
+ this.stdout.write(this.ci ? `::debug::${message}\n` : `${message}\n`);
25
+ }
26
+ }
27
+ notice(message) {
28
+ if (!this.quiet) {
29
+ this.stdout.write(this.ci ? `::notice::${message}\n` : `${message}\n`);
30
+ }
31
+ }
32
+ warning(message) {
33
+ this.stderr.write(this.ci ? `::warning::${message}\n` : `Warning: ${message}\n`);
34
+ }
35
+ error(message) {
36
+ this.stderr.write(this.ci ? `::error::${message}\n` : `Error: ${message}\n`);
37
+ }
38
+ /**
39
+ * Run a callback inside a collapsible log group.
40
+ *
41
+ * @param title Group title.
42
+ * @param callback Callback to run inside the group.
43
+ * @return Callback result.
44
+ */
45
+ async group(title, callback) {
46
+ this.info(this.ci ? `::group::${title}` : `== ${title}`);
47
+ try {
48
+ return await callback();
49
+ }
50
+ finally {
51
+ if (this.ci) {
52
+ this.info('::endgroup::');
53
+ }
54
+ }
55
+ }
56
+ }
@@ -0,0 +1,112 @@
1
+ import type { PackageType } from './checksums.ts';
2
+ /**
3
+ * Number of bytes that WordPress reads from a file to parse its headers.
4
+ */
5
+ export declare const HEADER_BYTES = 8192;
6
+ /**
7
+ * Headers of the main plugin file, as read by get_plugin_data().
8
+ */
9
+ export declare const PLUGIN_HEADERS: readonly ["Plugin Name", "Plugin URI", "Version", "Description", "Author", "Author URI", "Text Domain", "Domain Path", "Network", "Requires at least", "Requires PHP", "Requires Plugins", "Update URI", "License", "License URI"];
10
+ /**
11
+ * Headers of the theme style.css, as read by wp_get_theme().
12
+ */
13
+ export declare const THEME_HEADERS: readonly ["Theme Name", "Theme URI", "Description", "Author", "Author URI", "Version", "Template", "Status", "Tags", "Text Domain", "Domain Path", "Requires at least", "Tested up to", "Requires PHP", "License", "License URI"];
14
+ /**
15
+ * Headers at the top of readme.txt and readme.md, as read by the
16
+ * WordPress.org readme parser.
17
+ */
18
+ export declare const README_HEADERS: readonly ["Contributors", "Donate link", "Tags", "Requires at least", "Tested up to", "Requires PHP", "Stable tag", "License", "License URI"];
19
+ /**
20
+ * Readme file names that WordPress.org reads, in order of preference.
21
+ */
22
+ export declare const README_FILENAMES: string[];
23
+ /**
24
+ * Header values keyed by the header name as listed in PLUGIN_HEADERS,
25
+ * THEME_HEADERS or README_HEADERS. Missing and empty headers are left out.
26
+ */
27
+ export type HeaderValues = Record<string, string>;
28
+ export interface ParseFileHeadersInput {
29
+ /** File contents, usually the first HEADER_BYTES bytes. */
30
+ contents: string;
31
+ /** Header names to read. */
32
+ headers: readonly string[];
33
+ }
34
+ export interface ReadmeMeta {
35
+ /** Readme path relative to the package directory. */
36
+ file: string;
37
+ /** Readme headers like Stable tag and Tested up to. */
38
+ headers: HeaderValues;
39
+ }
40
+ /**
41
+ * Plugin or theme details read from its files.
42
+ */
43
+ export interface PackageMeta {
44
+ /** Package type. */
45
+ type: PackageType;
46
+ /** Main plugin file or style.css, relative to the package directory. */
47
+ mainFile: string;
48
+ /** Headers of the main file. */
49
+ headers: HeaderValues;
50
+ /** Version header, or null when it's missing. */
51
+ version: string | null;
52
+ /** Readme details, or null without a readme. */
53
+ readme: ReadmeMeta | null;
54
+ }
55
+ export interface FindMainPluginFileInput {
56
+ /** Plugin directory. */
57
+ dir: string;
58
+ /** Plugin slug, to prefer {slug}.php when several files have headers. */
59
+ slug?: string | null;
60
+ }
61
+ export interface ReadPackageMetaInput {
62
+ /** Plugin or theme directory. */
63
+ dir: string;
64
+ /** Plugin slug, to prefer {slug}.php when several files have headers. */
65
+ slug?: string | null;
66
+ /** Readme file name, defaults to the first of README_FILENAMES that exists. */
67
+ readme?: string | null;
68
+ }
69
+ /**
70
+ * Parse WordPress file headers like "Version: 1.2.0" from a PHP or CSS file
71
+ * comment, the same way as get_file_data() in WordPress: a header can be on
72
+ * any line, after comment characters, and the value ends at the line end or
73
+ * a closing comment.
74
+ *
75
+ * @param input File contents and the headers to read.
76
+ * @return Non-empty header values.
77
+ */
78
+ export declare function parseFileHeaders(input: ParseFileHeadersInput): HeaderValues;
79
+ /**
80
+ * Parse the headers of a readme.txt or readme.md, the "Key: value" lines
81
+ * between the title and the first section. Markdown emphasis around the
82
+ * names, like "**Stable tag:** 1.2.0", is ignored.
83
+ *
84
+ * @param contents Readme contents.
85
+ * @return Non-empty header values keyed by the names in README_HEADERS.
86
+ */
87
+ export declare function parseReadmeHeaders(contents: string): HeaderValues;
88
+ /**
89
+ * Read the start of a file, where WordPress looks for the headers.
90
+ *
91
+ * @param path File path.
92
+ * @return The first HEADER_BYTES bytes as text, or null when the file doesn't exist.
93
+ */
94
+ export declare function readFileStart(path: string): Promise<string | null>;
95
+ /**
96
+ * Find the main plugin file: a top-level .php file with a "Plugin Name"
97
+ * header in its first 8 KB, like WordPress does. When several files have the
98
+ * header, {slug}.php wins, then the first file by name.
99
+ *
100
+ * @param input Plugin directory and slug.
101
+ * @return File name, or null when there's no plugin file.
102
+ */
103
+ export declare function findMainPluginFile(input: FindMainPluginFileInput): Promise<string | null>;
104
+ /**
105
+ * Read the plugin or theme headers and readme of a directory. A directory
106
+ * with a main plugin file is a plugin, otherwise one with a "Theme Name" in
107
+ * style.css is a theme.
108
+ *
109
+ * @param input Directory, slug and readme name.
110
+ * @return Package details, or null when the directory has neither.
111
+ */
112
+ export declare function readPackageMeta(input: ReadPackageMetaInput): Promise<PackageMeta | null>;
@@ -0,0 +1,222 @@
1
+ import { open, readdir, readFile } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ import { isErrorCode } from "./file-selection.js";
4
+ /**
5
+ * Number of bytes that WordPress reads from a file to parse its headers.
6
+ */
7
+ export const HEADER_BYTES = 8192;
8
+ /**
9
+ * Headers of the main plugin file, as read by get_plugin_data().
10
+ */
11
+ export const PLUGIN_HEADERS = [
12
+ 'Plugin Name',
13
+ 'Plugin URI',
14
+ 'Version',
15
+ 'Description',
16
+ 'Author',
17
+ 'Author URI',
18
+ 'Text Domain',
19
+ 'Domain Path',
20
+ 'Network',
21
+ 'Requires at least',
22
+ 'Requires PHP',
23
+ 'Requires Plugins',
24
+ 'Update URI',
25
+ 'License',
26
+ 'License URI',
27
+ ];
28
+ /**
29
+ * Headers of the theme style.css, as read by wp_get_theme().
30
+ */
31
+ export const THEME_HEADERS = [
32
+ 'Theme Name',
33
+ 'Theme URI',
34
+ 'Description',
35
+ 'Author',
36
+ 'Author URI',
37
+ 'Version',
38
+ 'Template',
39
+ 'Status',
40
+ 'Tags',
41
+ 'Text Domain',
42
+ 'Domain Path',
43
+ 'Requires at least',
44
+ 'Tested up to',
45
+ 'Requires PHP',
46
+ 'License',
47
+ 'License URI',
48
+ ];
49
+ /**
50
+ * Headers at the top of readme.txt and readme.md, as read by the
51
+ * WordPress.org readme parser.
52
+ */
53
+ export const README_HEADERS = [
54
+ 'Contributors',
55
+ 'Donate link',
56
+ 'Tags',
57
+ 'Requires at least',
58
+ 'Tested up to',
59
+ 'Requires PHP',
60
+ 'Stable tag',
61
+ 'License',
62
+ 'License URI',
63
+ ];
64
+ /**
65
+ * Readme file names that WordPress.org reads, in order of preference.
66
+ */
67
+ export const README_FILENAMES = ['readme.txt', 'readme.md'];
68
+ /**
69
+ * Parse WordPress file headers like "Version: 1.2.0" from a PHP or CSS file
70
+ * comment, the same way as get_file_data() in WordPress: a header can be on
71
+ * any line, after comment characters, and the value ends at the line end or
72
+ * a closing comment.
73
+ *
74
+ * @param input File contents and the headers to read.
75
+ * @return Non-empty header values.
76
+ */
77
+ export function parseFileHeaders(input) {
78
+ const contents = input.contents.replace(/\r\n?/g, '\n');
79
+ const entries = input.headers.map((header) => [
80
+ header,
81
+ matchFileHeader(contents, header),
82
+ ]);
83
+ return Object.fromEntries(entries.filter(([, value]) => value));
84
+ }
85
+ function matchFileHeader(contents, header) {
86
+ const pattern = new RegExp(`^(?:[ \\t]*<\\?php)?[ \\t/*#@]*${escapeRegExp(header)}:(.*)$`, 'mi');
87
+ const value = pattern.exec(contents)?.[1] ?? '';
88
+ return value.replace(/\s*(?:\*\/|\?>).*/, '').trim();
89
+ }
90
+ function escapeRegExp(text) {
91
+ return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
92
+ }
93
+ /**
94
+ * Parse the headers of a readme.txt or readme.md, the "Key: value" lines
95
+ * between the title and the first section. Markdown emphasis around the
96
+ * names, like "**Stable tag:** 1.2.0", is ignored.
97
+ *
98
+ * @param contents Readme contents.
99
+ * @return Non-empty header values keyed by the names in README_HEADERS.
100
+ */
101
+ export function parseReadmeHeaders(contents) {
102
+ const lines = contents.replace(/\r\n?/g, '\n').split('\n');
103
+ const firstSection = lines.findIndex((line, index) => index > 0 && /^\s*(==|##)/.test(line));
104
+ const headerLines = lines.slice(0, -1 === firstSection ? lines.length : firstSection);
105
+ const values = {};
106
+ headerLines.forEach((line) => {
107
+ const match = /^[\s*_-]*([^:*_]+?)[*_]*\s*:[*_]*\s*(.*?)\s*$/.exec(line);
108
+ const header = README_HEADERS.find((name) => name.toLowerCase() === match?.[1]?.toLowerCase());
109
+ if (header && match?.[2] && !Object.hasOwn(values, header)) {
110
+ values[header] = match[2];
111
+ }
112
+ });
113
+ return values;
114
+ }
115
+ /**
116
+ * Read the start of a file, where WordPress looks for the headers.
117
+ *
118
+ * @param path File path.
119
+ * @return The first HEADER_BYTES bytes as text, or null when the file doesn't exist.
120
+ */
121
+ export async function readFileStart(path) {
122
+ const handle = await open(path, 'r').catch((error) => {
123
+ if (isErrorCode(error, 'ENOENT')) {
124
+ return null;
125
+ }
126
+ throw error;
127
+ });
128
+ if (!handle) {
129
+ return null;
130
+ }
131
+ try {
132
+ const buffer = Buffer.alloc(HEADER_BYTES);
133
+ const { bytesRead } = await handle.read(buffer, 0, HEADER_BYTES, 0);
134
+ return buffer.subarray(0, bytesRead).toString('utf8');
135
+ }
136
+ finally {
137
+ await handle.close();
138
+ }
139
+ }
140
+ /**
141
+ * Find the main plugin file: a top-level .php file with a "Plugin Name"
142
+ * header in its first 8 KB, like WordPress does. When several files have the
143
+ * header, {slug}.php wins, then the first file by name.
144
+ *
145
+ * @param input Plugin directory and slug.
146
+ * @return File name, or null when there's no plugin file.
147
+ */
148
+ export async function findMainPluginFile(input) {
149
+ const { dir, slug } = input;
150
+ const entries = await readdir(dir, { withFileTypes: true });
151
+ const phpFiles = entries
152
+ .filter((entry) => entry.isFile() && entry.name.endsWith('.php'))
153
+ .map((entry) => entry.name)
154
+ .sort();
155
+ const candidates = await Promise.all(phpFiles.map(async (file) => {
156
+ const contents = await readFileStart(join(dir, file));
157
+ const headers = parseFileHeaders({
158
+ contents: contents ?? '',
159
+ headers: ['Plugin Name'],
160
+ });
161
+ return headers['Plugin Name'] ? file : null;
162
+ }));
163
+ const pluginFiles = candidates.filter((file) => null !== file);
164
+ return (pluginFiles.find((file) => `${slug}.php` === file) ??
165
+ pluginFiles.at(0) ??
166
+ null);
167
+ }
168
+ /**
169
+ * Read the plugin or theme headers and readme of a directory. A directory
170
+ * with a main plugin file is a plugin, otherwise one with a "Theme Name" in
171
+ * style.css is a theme.
172
+ *
173
+ * @param input Directory, slug and readme name.
174
+ * @return Package details, or null when the directory has neither.
175
+ */
176
+ export async function readPackageMeta(input) {
177
+ const { dir } = input;
178
+ const pluginFile = await findMainPluginFile(input);
179
+ const mainFile = pluginFile ?? 'style.css';
180
+ const type = pluginFile ? 'plugin' : 'theme';
181
+ const headers = parseFileHeaders({
182
+ contents: (await readFileStart(join(dir, mainFile))) ?? '',
183
+ headers: pluginFile ? PLUGIN_HEADERS : THEME_HEADERS,
184
+ });
185
+ if (pluginFile || headers['Theme Name']) {
186
+ return {
187
+ type,
188
+ mainFile,
189
+ headers,
190
+ version: headers.Version ?? null,
191
+ readme: await readReadme(dir, input.readme),
192
+ };
193
+ }
194
+ return null;
195
+ }
196
+ /**
197
+ * Read the readme headers.
198
+ *
199
+ * @param dir Package directory.
200
+ * @param name Readme file name, or the default names when not set.
201
+ * @return Readme details, or null when there's no readme.
202
+ */
203
+ async function readReadme(dir, name) {
204
+ const names = name ? [name] : README_FILENAMES;
205
+ const readmes = await Promise.all(names.map(async (file) => ({
206
+ file,
207
+ contents: await readFile(join(dir, file), 'utf8').catch((error) => {
208
+ if (isErrorCode(error, 'ENOENT')) {
209
+ return null;
210
+ }
211
+ throw error;
212
+ }),
213
+ })));
214
+ const readme = readmes.find(({ contents }) => null !== contents);
215
+ if (readme) {
216
+ return {
217
+ file: readme.file,
218
+ headers: parseReadmeHeaders(readme.contents ?? ''),
219
+ };
220
+ }
221
+ return null;
222
+ }