@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.
- package/LICENSE +21 -0
- package/README.md +316 -2
- package/dist/bin/wp-release.d.ts +2 -0
- package/dist/bin/wp-release.js +3 -0
- package/dist/checksums.d.ts +138 -0
- package/dist/checksums.js +216 -0
- package/dist/cli.d.ts +42 -0
- package/dist/cli.js +163 -0
- package/dist/commands/args.d.ts +81 -0
- package/dist/commands/args.js +116 -0
- package/dist/commands/checksums.d.ts +34 -0
- package/dist/commands/checksums.js +76 -0
- package/dist/commands/command.d.ts +63 -0
- package/dist/commands/command.js +33 -0
- package/dist/commands/deploy-svn.d.ts +74 -0
- package/dist/commands/deploy-svn.js +103 -0
- package/dist/commands/verify.d.ts +27 -0
- package/dist/commands/verify.js +60 -0
- package/dist/commands/version-check.d.ts +38 -0
- package/dist/commands/version-check.js +80 -0
- package/dist/commands/zip.d.ts +41 -0
- package/dist/commands/zip.js +138 -0
- package/dist/deploy-options.d.ts +136 -0
- package/dist/deploy-options.js +168 -0
- package/dist/errors.d.ts +16 -0
- package/dist/errors.js +16 -0
- package/dist/file-selection.d.ts +78 -0
- package/dist/file-selection.js +133 -0
- package/dist/filename.d.ts +17 -0
- package/dist/filename.js +23 -0
- package/dist/index.d.ts +30 -0
- package/dist/index.js +15 -0
- package/dist/logger.d.ts +42 -0
- package/dist/logger.js +56 -0
- package/dist/package-meta.d.ts +112 -0
- package/dist/package-meta.js +222 -0
- package/dist/svn-deploy.d.ts +208 -0
- package/dist/svn-deploy.js +485 -0
- package/dist/svn.d.ts +209 -0
- package/dist/svn.js +298 -0
- package/dist/sync.d.ts +32 -0
- package/dist/sync.js +90 -0
- package/dist/unzip.d.ts +45 -0
- package/dist/unzip.js +121 -0
- package/dist/version-check.d.ts +75 -0
- package/dist/version-check.js +193 -0
- package/dist/zip.d.ts +32 -0
- package/dist/zip.js +53 -0
- package/package.json +61 -5
package/dist/unzip.js
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { createWriteStream } from 'node:fs';
|
|
2
|
+
import { mkdir } from 'node:fs/promises';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
import { pipeline } from 'node:stream/promises';
|
|
5
|
+
import yauzl from 'yauzl';
|
|
6
|
+
import { ValidationError } from "./errors.js";
|
|
7
|
+
/**
|
|
8
|
+
* File type of a symbolic link in the upper bits of a Unix mode (S_IFLNK).
|
|
9
|
+
*/
|
|
10
|
+
const SYMLINK_FILE_TYPE = 0o12;
|
|
11
|
+
/**
|
|
12
|
+
* Open an archive with yauzl and call a callback for every entry, one at a
|
|
13
|
+
* time.
|
|
14
|
+
*
|
|
15
|
+
* @param zipPath Archive path.
|
|
16
|
+
* @param onEntry Callback for each entry, awaited before reading the next.
|
|
17
|
+
*/
|
|
18
|
+
async function readEntries(zipPath, onEntry) {
|
|
19
|
+
const zip = await new Promise((resolve, reject) => yauzl.open(zipPath, { lazyEntries: true, autoClose: false }, (error, file) => (error ? reject(error) : resolve(file)))).catch((error) => {
|
|
20
|
+
throw new ValidationError(`${zipPath} isn't a valid ZIP file: ${error.message}`);
|
|
21
|
+
});
|
|
22
|
+
try {
|
|
23
|
+
await new Promise((resolve, reject) => {
|
|
24
|
+
zip.on('entry', (entry) => onEntry(entry, zip).then(() => zip.readEntry(), reject));
|
|
25
|
+
zip.on('end', resolve);
|
|
26
|
+
zip.on('error', (error) => reject(new ValidationError(`Can't read ${zipPath}: ${error.message}`)));
|
|
27
|
+
zip.readEntry();
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
finally {
|
|
31
|
+
zip.close();
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Get the type of an archive entry. Directories end with a slash, and
|
|
36
|
+
* symbolic links have the link file type in the Unix mode.
|
|
37
|
+
*
|
|
38
|
+
* @param entry Archive entry.
|
|
39
|
+
* @return Entry type.
|
|
40
|
+
*/
|
|
41
|
+
function getEntryType(entry) {
|
|
42
|
+
// The Unix mode is in the upper 16 bits, and the file type above its lower 12 bits.
|
|
43
|
+
const mode = Math.floor(entry.externalFileAttributes / 0x10000);
|
|
44
|
+
if (SYMLINK_FILE_TYPE === Math.floor(mode / 0o10000) % 0o20) {
|
|
45
|
+
return 'symlink';
|
|
46
|
+
}
|
|
47
|
+
return entry.fileName.endsWith('/') ? 'directory' : 'file';
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* List the entries of an archive without extracting it.
|
|
51
|
+
*
|
|
52
|
+
* @param zipPath Archive path.
|
|
53
|
+
* @return Entries in archive order.
|
|
54
|
+
*/
|
|
55
|
+
export async function readZipEntries(zipPath) {
|
|
56
|
+
const entries = [];
|
|
57
|
+
await readEntries(zipPath, async (entry) => {
|
|
58
|
+
entries.push({ path: entry.fileName, type: getEntryType(entry) });
|
|
59
|
+
});
|
|
60
|
+
return entries;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Collect the problems that make an archive unsafe or unexpected to extract:
|
|
64
|
+
* absolute paths, ".." segments, backslashes, symbolic links and entries
|
|
65
|
+
* outside the root folder.
|
|
66
|
+
*
|
|
67
|
+
* @param entries Entries from readZipEntries().
|
|
68
|
+
* @param rootFolder Root folder that every entry must be in, or null.
|
|
69
|
+
* @return Error messages, empty when the archive can be extracted.
|
|
70
|
+
*/
|
|
71
|
+
export function getZipEntryErrors(entries, rootFolder = null) {
|
|
72
|
+
const errors = entries.map(({ path, type }) => {
|
|
73
|
+
if (path.startsWith('/') || /^[a-zA-Z]:/.test(path)) {
|
|
74
|
+
return `${path} is an absolute path.`;
|
|
75
|
+
}
|
|
76
|
+
const segments = path.split('/');
|
|
77
|
+
if (path.includes('\\') || segments.includes('..')) {
|
|
78
|
+
return `${path} is outside the archive root.`;
|
|
79
|
+
}
|
|
80
|
+
if ('symlink' === type) {
|
|
81
|
+
return `${path} is a symbolic link, which isn't supported.`;
|
|
82
|
+
}
|
|
83
|
+
if (rootFolder && segments[0] !== rootFolder) {
|
|
84
|
+
return `${path} isn't in the ${rootFolder}/ root folder.`;
|
|
85
|
+
}
|
|
86
|
+
return null;
|
|
87
|
+
});
|
|
88
|
+
return errors.filter((error) => null !== error);
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Extract an archive after checking every entry with getZipEntryErrors(),
|
|
92
|
+
* so that nothing is written outside the target directory. The root folder,
|
|
93
|
+
* when set, is stripped from the paths.
|
|
94
|
+
*
|
|
95
|
+
* @param input Archive, target directory and root folder.
|
|
96
|
+
* @return Extracted file paths relative to the target directory, sorted.
|
|
97
|
+
* @throws {ValidationError} If the archive has unsafe or unexpected entries.
|
|
98
|
+
*/
|
|
99
|
+
export async function extractZip(input) {
|
|
100
|
+
const { zipPath, targetDir, rootFolder = null } = input;
|
|
101
|
+
const errors = getZipEntryErrors(await readZipEntries(zipPath), rootFolder);
|
|
102
|
+
const files = [];
|
|
103
|
+
if (errors.length) {
|
|
104
|
+
throw new ValidationError(`Can't extract ${zipPath}:\n${errors.join('\n')}`);
|
|
105
|
+
}
|
|
106
|
+
await readEntries(zipPath, async (entry, zip) => {
|
|
107
|
+
const path = rootFolder
|
|
108
|
+
? entry.fileName.split('/').slice(1).join('/')
|
|
109
|
+
: entry.fileName;
|
|
110
|
+
if (path && 'file' === getEntryType(entry)) {
|
|
111
|
+
const target = join(targetDir, path);
|
|
112
|
+
await mkdir(dirname(target), { recursive: true });
|
|
113
|
+
await pipeline(await openReadStream(zip, entry), createWriteStream(target, { flags: 'wx' }));
|
|
114
|
+
files.push(path);
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
return files.sort();
|
|
118
|
+
}
|
|
119
|
+
function openReadStream(zip, entry) {
|
|
120
|
+
return new Promise((resolve, reject) => zip.openReadStream(entry, (error, stream) => error ? reject(error) : resolve(stream)));
|
|
121
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { PackageMeta } from './package-meta.ts';
|
|
2
|
+
export declare const GIT_TAG_FORMAT = "{version}";
|
|
3
|
+
/**
|
|
4
|
+
* A version that differs from the expected one, or a version that is missing.
|
|
5
|
+
*/
|
|
6
|
+
export interface VersionMismatch {
|
|
7
|
+
/** Where the version comes from, like "Stable tag" or "Git tag". */
|
|
8
|
+
source: string;
|
|
9
|
+
/** File that holds the version, if any. */
|
|
10
|
+
file: string | null;
|
|
11
|
+
/** Version found, or null when it's missing. */
|
|
12
|
+
actual: string | null;
|
|
13
|
+
/** Human-readable description. */
|
|
14
|
+
message: string;
|
|
15
|
+
}
|
|
16
|
+
export interface FindVersionMismatchesInput {
|
|
17
|
+
/** Plugin or theme directory. */
|
|
18
|
+
sourceDir: string;
|
|
19
|
+
/** Metadata from readPackageMeta(). */
|
|
20
|
+
meta: PackageMeta;
|
|
21
|
+
/** Plugin or theme slug, for {slug} in the git tag format. */
|
|
22
|
+
slug: string;
|
|
23
|
+
/** Expected version, defaults to the Version header. */
|
|
24
|
+
version?: string | null;
|
|
25
|
+
/** Git tag name or ref like refs/tags/v1.2.0, not compared when empty. */
|
|
26
|
+
gitTag?: string | null;
|
|
27
|
+
/** Git tag format with {version} and {slug}, defaults to {version}. */
|
|
28
|
+
gitTagFormat?: string;
|
|
29
|
+
/** PHP constants that must be set to the version. */
|
|
30
|
+
constants?: string[];
|
|
31
|
+
/** Compare the readme Stable tag of plugins, defaults to true. */
|
|
32
|
+
stableTag?: boolean;
|
|
33
|
+
}
|
|
34
|
+
export interface GitTagInput {
|
|
35
|
+
/** Git tag name or ref like refs/tags/v1.2.0. */
|
|
36
|
+
gitTag?: string;
|
|
37
|
+
/** Git tag format with {version} and {slug}, defaults to {version}. */
|
|
38
|
+
gitTagFormat?: string;
|
|
39
|
+
/** Plugin or theme slug. */
|
|
40
|
+
slug: string;
|
|
41
|
+
}
|
|
42
|
+
export interface VersionCheckResult {
|
|
43
|
+
/** Expected version, or null when it's unknown. */
|
|
44
|
+
version: string | null;
|
|
45
|
+
/** Every mismatch, empty when all versions agree. */
|
|
46
|
+
mismatches: VersionMismatch[];
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Get the version from a git tag with a format like v{version} or
|
|
50
|
+
* {slug}-{version}. A leading "v" is stripped from the version, like for
|
|
51
|
+
* deploys, so v1.2.0 matches the default {version} format.
|
|
52
|
+
*
|
|
53
|
+
* @param input Tag, format and slug.
|
|
54
|
+
* @return Version, or null when the tag doesn't match the format.
|
|
55
|
+
* @throws {ValidationError} If the format has no {version}.
|
|
56
|
+
*/
|
|
57
|
+
export declare function getGitTagVersion(input: GitTagInput): string | null;
|
|
58
|
+
/**
|
|
59
|
+
* Find the values of a PHP constant set with define( 'NAME', '1.2.0' ) or
|
|
60
|
+
* const NAME = '1.2.0'.
|
|
61
|
+
*
|
|
62
|
+
* @param contents PHP code.
|
|
63
|
+
* @param name Constant name.
|
|
64
|
+
* @return Values in the order they appear.
|
|
65
|
+
*/
|
|
66
|
+
export declare function findConstantValues(contents: string, name: string): string[];
|
|
67
|
+
/**
|
|
68
|
+
* Check that the Version header, readme Stable tag, PHP constants and git tag
|
|
69
|
+
* of a plugin or theme agree, and report every one that doesn't. Themes have
|
|
70
|
+
* no Stable tag, so it's only compared for plugins.
|
|
71
|
+
*
|
|
72
|
+
* @param input Package metadata and the versions to compare.
|
|
73
|
+
* @return Expected version and the mismatches.
|
|
74
|
+
*/
|
|
75
|
+
export declare function findVersionMismatches(input: FindVersionMismatchesInput): Promise<VersionCheckResult>;
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import ignore from 'ignore';
|
|
4
|
+
import { normalizeTag } from "./deploy-options.js";
|
|
5
|
+
import { ValidationError } from "./errors.js";
|
|
6
|
+
import { selectFiles } from "./file-selection.js";
|
|
7
|
+
export const GIT_TAG_FORMAT = '{version}';
|
|
8
|
+
const TAG_REF_PREFIX = 'refs/tags/';
|
|
9
|
+
/**
|
|
10
|
+
* Directories that aren't searched for PHP constants, since they hold
|
|
11
|
+
* dependencies with their own versions.
|
|
12
|
+
*/
|
|
13
|
+
const CONSTANT_SEARCH_IGNORE = ['node_modules/', 'vendor/', '.*'];
|
|
14
|
+
/**
|
|
15
|
+
* Get the version from a git tag with a format like v{version} or
|
|
16
|
+
* {slug}-{version}. A leading "v" is stripped from the version, like for
|
|
17
|
+
* deploys, so v1.2.0 matches the default {version} format.
|
|
18
|
+
*
|
|
19
|
+
* @param input Tag, format and slug.
|
|
20
|
+
* @return Version, or null when the tag doesn't match the format.
|
|
21
|
+
* @throws {ValidationError} If the format has no {version}.
|
|
22
|
+
*/
|
|
23
|
+
export function getGitTagVersion(input) {
|
|
24
|
+
const { gitTag = '', gitTagFormat = GIT_TAG_FORMAT, slug } = input;
|
|
25
|
+
const tag = (gitTag ?? '').replace(TAG_REF_PREFIX, '');
|
|
26
|
+
if (!gitTagFormat.includes('{version}')) {
|
|
27
|
+
throw new ValidationError(`The git tag format "${gitTagFormat}" needs a {version} placeholder.`);
|
|
28
|
+
}
|
|
29
|
+
const pattern = escapeRegExp(gitTagFormat)
|
|
30
|
+
.replaceAll('\\{slug\\}', escapeRegExp(slug))
|
|
31
|
+
.replace('\\{version\\}', '(.+)');
|
|
32
|
+
const version = new RegExp(`^${pattern}$`).exec(tag)?.[1];
|
|
33
|
+
return version ? normalizeTag(version) : null;
|
|
34
|
+
}
|
|
35
|
+
function escapeRegExp(text) {
|
|
36
|
+
return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Find the values of a PHP constant set with define( 'NAME', '1.2.0' ) or
|
|
40
|
+
* const NAME = '1.2.0'.
|
|
41
|
+
*
|
|
42
|
+
* @param contents PHP code.
|
|
43
|
+
* @param name Constant name.
|
|
44
|
+
* @return Values in the order they appear.
|
|
45
|
+
*/
|
|
46
|
+
export function findConstantValues(contents, name) {
|
|
47
|
+
const quotedName = `(['"])${escapeRegExp(name)}\\1`;
|
|
48
|
+
const patterns = [
|
|
49
|
+
new RegExp(`\\bdefine\\s*\\(\\s*${quotedName}\\s*,\\s*(['"])(.*?)\\2\\s*[,)]`, 'g'),
|
|
50
|
+
new RegExp(`\\bconst\\s+()${escapeRegExp(name)}\\s*=\\s*(['"])(.*?)\\2\\s*;`, 'g'),
|
|
51
|
+
];
|
|
52
|
+
return patterns
|
|
53
|
+
.flatMap((pattern) => [...contents.matchAll(pattern)])
|
|
54
|
+
.sort((a, b) => (a.index ?? 0) - (b.index ?? 0))
|
|
55
|
+
.map((match) => match[3] ?? '');
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Compare a version with the expected one.
|
|
59
|
+
*
|
|
60
|
+
* @param mismatch Mismatch details without the message.
|
|
61
|
+
* @param expected Expected version.
|
|
62
|
+
* @return The mismatch, or null when the version matches.
|
|
63
|
+
*/
|
|
64
|
+
function compareVersion(mismatch, expected) {
|
|
65
|
+
const { source, file, actual } = mismatch;
|
|
66
|
+
const location = file ? ` in ${file}` : '';
|
|
67
|
+
if (null === actual) {
|
|
68
|
+
return {
|
|
69
|
+
...mismatch,
|
|
70
|
+
message: `${source} is missing${location}, expected ${expected}.`,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
if (actual !== expected) {
|
|
74
|
+
return {
|
|
75
|
+
...mismatch,
|
|
76
|
+
message: `${source}${location} is ${actual}, expected ${expected}.`,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Compare the PHP constants with the expected version. Every PHP file except
|
|
83
|
+
* dependencies is searched, so that constants defined outside the main
|
|
84
|
+
* plugin file are found too.
|
|
85
|
+
*
|
|
86
|
+
* @param sourceDir Plugin or theme directory.
|
|
87
|
+
* @param constants Constant names.
|
|
88
|
+
* @param expected Expected version.
|
|
89
|
+
* @return Mismatches, including a missing entry for each constant that isn't defined.
|
|
90
|
+
*/
|
|
91
|
+
async function compareConstants(sourceDir, constants, expected) {
|
|
92
|
+
const files = constants.length
|
|
93
|
+
? await selectFiles(sourceDir, ignore().add(CONSTANT_SEARCH_IGNORE))
|
|
94
|
+
: [];
|
|
95
|
+
const sources = await Promise.all(files
|
|
96
|
+
.filter((file) => file.endsWith('.php'))
|
|
97
|
+
.map(async (file) => ({
|
|
98
|
+
file,
|
|
99
|
+
contents: await readFile(join(sourceDir, file), 'utf8'),
|
|
100
|
+
})));
|
|
101
|
+
return constants.flatMap((name) => {
|
|
102
|
+
const source = `Constant ${name}`;
|
|
103
|
+
const definitions = sources.flatMap(({ file, contents }) => findConstantValues(contents, name).map((actual) => ({
|
|
104
|
+
source,
|
|
105
|
+
file,
|
|
106
|
+
actual,
|
|
107
|
+
})));
|
|
108
|
+
if (definitions.length) {
|
|
109
|
+
return definitions.map((definition) => compareVersion(definition, expected));
|
|
110
|
+
}
|
|
111
|
+
return [
|
|
112
|
+
compareVersion({ source, file: null, actual: null }, expected),
|
|
113
|
+
];
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Compare the version in a git tag with the expected version.
|
|
118
|
+
*
|
|
119
|
+
* @param input Tag, format and slug.
|
|
120
|
+
* @param expected Expected version.
|
|
121
|
+
* @return The mismatch, or null when the tag matches.
|
|
122
|
+
*/
|
|
123
|
+
function compareGitTag(input, expected) {
|
|
124
|
+
const tag = input.gitTag.replace(TAG_REF_PREFIX, '');
|
|
125
|
+
const actual = getGitTagVersion(input);
|
|
126
|
+
if (null === actual) {
|
|
127
|
+
return {
|
|
128
|
+
source: 'Git tag',
|
|
129
|
+
file: null,
|
|
130
|
+
actual: tag,
|
|
131
|
+
message: `Git tag ${tag} doesn't match the format ${input.gitTagFormat}.`,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
return compareVersion({ source: `Git tag ${tag}`, file: null, actual }, expected);
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Check that the Version header, readme Stable tag, PHP constants and git tag
|
|
138
|
+
* of a plugin or theme agree, and report every one that doesn't. Themes have
|
|
139
|
+
* no Stable tag, so it's only compared for plugins.
|
|
140
|
+
*
|
|
141
|
+
* @param input Package metadata and the versions to compare.
|
|
142
|
+
* @return Expected version and the mismatches.
|
|
143
|
+
*/
|
|
144
|
+
export async function findVersionMismatches(input) {
|
|
145
|
+
const { sourceDir, meta, slug, gitTag = null, gitTagFormat = GIT_TAG_FORMAT, constants = [], stableTag = true, } = input;
|
|
146
|
+
const version = input.version ? normalizeTag(input.version) : null;
|
|
147
|
+
const expected = version ?? meta.version;
|
|
148
|
+
if (!expected) {
|
|
149
|
+
return {
|
|
150
|
+
version: null,
|
|
151
|
+
mismatches: [
|
|
152
|
+
{
|
|
153
|
+
source: 'Version header',
|
|
154
|
+
file: meta.mainFile,
|
|
155
|
+
actual: null,
|
|
156
|
+
message: `Version header is missing in ${meta.mainFile}.`,
|
|
157
|
+
},
|
|
158
|
+
],
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
const readme = meta.readme;
|
|
162
|
+
const checkStableTag = stableTag && 'plugin' === meta.type;
|
|
163
|
+
const mismatches = [
|
|
164
|
+
compareVersion({
|
|
165
|
+
source: 'Version header',
|
|
166
|
+
file: meta.mainFile,
|
|
167
|
+
actual: meta.version,
|
|
168
|
+
}, expected),
|
|
169
|
+
checkStableTag && !readme
|
|
170
|
+
? {
|
|
171
|
+
source: 'Stable tag',
|
|
172
|
+
file: null,
|
|
173
|
+
actual: null,
|
|
174
|
+
message: `Readme is missing, expected Stable tag ${expected}.`,
|
|
175
|
+
}
|
|
176
|
+
: null,
|
|
177
|
+
checkStableTag && readme
|
|
178
|
+
? compareVersion({
|
|
179
|
+
source: 'Stable tag',
|
|
180
|
+
file: readme.file,
|
|
181
|
+
actual: readme.headers['Stable tag'] ?? null,
|
|
182
|
+
}, expected)
|
|
183
|
+
: null,
|
|
184
|
+
...(await compareConstants(sourceDir, constants, expected)),
|
|
185
|
+
gitTag
|
|
186
|
+
? compareGitTag({ gitTag, gitTagFormat, slug }, expected)
|
|
187
|
+
: null,
|
|
188
|
+
];
|
|
189
|
+
return {
|
|
190
|
+
version: expected,
|
|
191
|
+
mismatches: mismatches.filter((mismatch) => null !== mismatch),
|
|
192
|
+
};
|
|
193
|
+
}
|
package/dist/zip.d.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Modification time of every archive entry, so that the same files always
|
|
3
|
+
* give the same archive. It's local midnight on the first day that ZIP
|
|
4
|
+
* timestamps support, since ZIP stores local time without a time zone.
|
|
5
|
+
*/
|
|
6
|
+
export declare const ZIP_MTIME: Date;
|
|
7
|
+
/**
|
|
8
|
+
* File mode of every archive entry, so that the archive doesn't depend on
|
|
9
|
+
* the umask or file system of the machine that builds it.
|
|
10
|
+
*/
|
|
11
|
+
export declare const ZIP_FILE_MODE = 33188;
|
|
12
|
+
export interface CreateZipInput {
|
|
13
|
+
/** Directory with the files. */
|
|
14
|
+
sourceDir: string;
|
|
15
|
+
/** Relative file paths with forward slashes, from selectFiles(). */
|
|
16
|
+
files: string[];
|
|
17
|
+
/** Archive path. */
|
|
18
|
+
zipPath: string;
|
|
19
|
+
/** Folder to put the files in inside the archive, or null for the root. */
|
|
20
|
+
rootFolder?: string | null;
|
|
21
|
+
/** Overwrite an existing archive. */
|
|
22
|
+
force?: boolean;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Write a ZIP archive of files, streaming each file into the archive. The
|
|
26
|
+
* entries are sorted and use a fixed timestamp and file mode, so that the
|
|
27
|
+
* same files always give the same archive.
|
|
28
|
+
*
|
|
29
|
+
* @param input Files, archive path and root folder.
|
|
30
|
+
* @throws {ValidationError} If the archive exists and force isn't set.
|
|
31
|
+
*/
|
|
32
|
+
export declare function createZip(input: CreateZipInput): Promise<void>;
|
package/dist/zip.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { createWriteStream } from 'node:fs';
|
|
2
|
+
import { rm } from 'node:fs/promises';
|
|
3
|
+
import { join, posix } from 'node:path';
|
|
4
|
+
import { pipeline } from 'node:stream/promises';
|
|
5
|
+
import yazl from 'yazl';
|
|
6
|
+
import { ValidationError } from "./errors.js";
|
|
7
|
+
import { isErrorCode } from "./file-selection.js";
|
|
8
|
+
/**
|
|
9
|
+
* Modification time of every archive entry, so that the same files always
|
|
10
|
+
* give the same archive. It's local midnight on the first day that ZIP
|
|
11
|
+
* timestamps support, since ZIP stores local time without a time zone.
|
|
12
|
+
*/
|
|
13
|
+
export const ZIP_MTIME = new Date(1980, 0, 1);
|
|
14
|
+
/**
|
|
15
|
+
* File mode of every archive entry, so that the archive doesn't depend on
|
|
16
|
+
* the umask or file system of the machine that builds it.
|
|
17
|
+
*/
|
|
18
|
+
export const ZIP_FILE_MODE = 0o100644;
|
|
19
|
+
/**
|
|
20
|
+
* Write a ZIP archive of files, streaming each file into the archive. The
|
|
21
|
+
* entries are sorted and use a fixed timestamp and file mode, so that the
|
|
22
|
+
* same files always give the same archive.
|
|
23
|
+
*
|
|
24
|
+
* @param input Files, archive path and root folder.
|
|
25
|
+
* @throws {ValidationError} If the archive exists and force isn't set.
|
|
26
|
+
*/
|
|
27
|
+
export async function createZip(input) {
|
|
28
|
+
const { sourceDir, files, zipPath, rootFolder = null, force = false, } = input;
|
|
29
|
+
if (rootFolder && !isValidRootFolder(rootFolder)) {
|
|
30
|
+
throw new ValidationError(`Invalid root folder "${rootFolder}", use a single folder name.`);
|
|
31
|
+
}
|
|
32
|
+
const zip = new yazl.ZipFile();
|
|
33
|
+
const output = createWriteStream(zipPath, { flags: force ? 'w' : 'wx' });
|
|
34
|
+
[...files].sort().forEach((file) => zip.addFile(join(sourceDir, file), rootFolder ? posix.join(rootFolder, file) : file, {
|
|
35
|
+
mtime: ZIP_MTIME,
|
|
36
|
+
mode: ZIP_FILE_MODE,
|
|
37
|
+
forceDosTimestamp: true,
|
|
38
|
+
}));
|
|
39
|
+
zip.end();
|
|
40
|
+
try {
|
|
41
|
+
await pipeline(zip.outputStream, output);
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
if (isErrorCode(error, 'EEXIST')) {
|
|
45
|
+
throw new ValidationError(`${zipPath} already exists, pass --force to overwrite it.`);
|
|
46
|
+
}
|
|
47
|
+
await rm(zipPath, { force: true });
|
|
48
|
+
throw error;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
function isValidRootFolder(name) {
|
|
52
|
+
return !/[\\/]/.test(name) && '.' !== name && '..' !== name;
|
|
53
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,62 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
2
|
+
"name": "@wpelevator/wp-release",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Package, checksum, verify and version-check WordPress plugins and themes, and deploy plugins to WordPress.org SVN",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/wpelevator/basement.git",
|
|
9
|
+
"directory": "packages/js/wp-release"
|
|
10
|
+
},
|
|
11
|
+
"type": "module",
|
|
12
|
+
"bin": {
|
|
13
|
+
"wp-release": "dist/bin/wp-release.js"
|
|
14
|
+
},
|
|
15
|
+
"exports": {
|
|
16
|
+
".": {
|
|
17
|
+
"wpelevator-source": "./src/index.ts",
|
|
18
|
+
"types": "./dist/index.d.ts",
|
|
19
|
+
"default": "./dist/index.js"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"dist"
|
|
24
|
+
],
|
|
25
|
+
"publishConfig": {
|
|
26
|
+
"access": "public"
|
|
27
|
+
},
|
|
28
|
+
"engines": {
|
|
29
|
+
"node": ">=22.12"
|
|
30
|
+
},
|
|
31
|
+
"keywords": [
|
|
32
|
+
"wordpress",
|
|
33
|
+
"plugin",
|
|
34
|
+
"theme",
|
|
35
|
+
"release",
|
|
36
|
+
"deploy",
|
|
37
|
+
"svn",
|
|
38
|
+
"zip"
|
|
39
|
+
],
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"ignore": "^7.0.12",
|
|
42
|
+
"yauzl": "^3.4.0",
|
|
43
|
+
"yazl": "^3.3.1"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"@types/node": "^22.20.5",
|
|
47
|
+
"@types/yauzl": "^3.4.0",
|
|
48
|
+
"@types/yazl": "^3.3.1",
|
|
49
|
+
"@wordpress/scripts": "^33.0",
|
|
50
|
+
"npm-run-all2": "^9.0",
|
|
51
|
+
"typescript": "^6.0.3"
|
|
52
|
+
},
|
|
53
|
+
"scripts": {
|
|
54
|
+
"build": "tsc -p tsconfig.build.json",
|
|
55
|
+
"prepack": "npm run build",
|
|
56
|
+
"lint": "npm-run-all lint:*",
|
|
57
|
+
"lint:js": "wp-scripts lint-js src",
|
|
58
|
+
"lint:types": "tsc -p tsconfig.json",
|
|
59
|
+
"format": "wp-scripts format src",
|
|
60
|
+
"test": "node --test --conditions=wpelevator-source"
|
|
61
|
+
}
|
|
62
|
+
}
|