@wpelevator/wp-release 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
package/dist/svn.d.ts ADDED
@@ -0,0 +1,209 @@
1
+ import type { Logger } from './logger.ts';
2
+ export interface SvnCommandInput {
3
+ /** Arguments after "svn", passed without a shell. */
4
+ args: string[];
5
+ /** Working directory of the command. */
6
+ cwd?: string;
7
+ /** Text written to the standard input, like the password. */
8
+ input?: string;
9
+ }
10
+ export interface SvnCommandOutput {
11
+ /** Standard output. */
12
+ stdout: string;
13
+ /** Standard error. */
14
+ stderr: string;
15
+ }
16
+ /**
17
+ * Run an svn command. Rejects with an SvnError when it exits with an error.
18
+ */
19
+ export type SvnRunner = (input: SvnCommandInput) => Promise<SvnCommandOutput>;
20
+ export type SvnDepth = 'empty' | 'files' | 'immediates' | 'infinity';
21
+ /**
22
+ * Status of a working copy path, from the first two columns of svn status.
23
+ */
24
+ export type SvnStatusType = 'added' | 'modified' | 'deleted' | 'replaced' | 'missing' | 'unversioned' | 'conflicted' | 'obstructed';
25
+ export interface SvnStatusEntry {
26
+ /** Status of the path. */
27
+ status: SvnStatusType;
28
+ /** Path relative to the status path, with forward slashes. */
29
+ path: string;
30
+ }
31
+ export interface SvnOptions {
32
+ /** Command runner, createSvnRunner() by default. */
33
+ runner?: SvnRunner;
34
+ /** Username for commits. */
35
+ username?: string | null;
36
+ /** Password for commits, passed on the standard input. */
37
+ password?: string | null;
38
+ /** Logger for the commands, at debug level. */
39
+ logger?: Logger;
40
+ }
41
+ export interface SvnCheckoutInput {
42
+ /** Repository URL. */
43
+ url: string;
44
+ /** Working copy path. */
45
+ path: string;
46
+ /** Checkout depth, immediates by default for a sparse checkout. */
47
+ depth?: SvnDepth;
48
+ }
49
+ export interface SvnUpdateInput {
50
+ /** Working copy path. */
51
+ path: string;
52
+ /** New sticky depth of the path. */
53
+ depth: SvnDepth;
54
+ }
55
+ export interface SvnPathsInput {
56
+ /** Working copy paths. */
57
+ paths: string[];
58
+ /** Working directory that relative paths resolve against. */
59
+ cwd?: string;
60
+ }
61
+ export interface SvnCopyInput {
62
+ /** Source path or URL. */
63
+ from: string;
64
+ /** Target path or URL. */
65
+ to: string;
66
+ /** Working directory that relative paths resolve against. */
67
+ cwd?: string;
68
+ }
69
+ export interface SvnPropsetInput extends SvnPathsInput {
70
+ /** Property name, like svn:mime-type. */
71
+ name: string;
72
+ /** Property value. */
73
+ value: string;
74
+ }
75
+ export interface SvnCommitInput {
76
+ /** Working copy path. */
77
+ path: string;
78
+ /** Commit message. */
79
+ message: string;
80
+ }
81
+ /**
82
+ * Failed svn command, with exit code 3 for authentication and permission
83
+ * errors and 1 otherwise.
84
+ */
85
+ export declare class SvnError extends Error {
86
+ name: string;
87
+ exitCode: number;
88
+ stderr: string;
89
+ /**
90
+ * @param message Error message.
91
+ * @param stderr Standard error of the command.
92
+ */
93
+ constructor(message: string, stderr?: string);
94
+ }
95
+ /**
96
+ * Escape a path for svn, which reads anything after the last "@" as a peg
97
+ * revision, so that files like icon@2x.png work.
98
+ *
99
+ * @param path Working copy path.
100
+ * @return Path with a trailing "@" when it contains one.
101
+ */
102
+ export declare function toSvnPath(path: string): string;
103
+ /**
104
+ * Create a runner that spawns the svn binary with an argument array, never
105
+ * through a shell. Messages are in English so that they can be parsed.
106
+ *
107
+ * @param binary Path or name of the svn binary.
108
+ * @return Command runner.
109
+ */
110
+ export declare function createSvnRunner(binary?: string): SvnRunner;
111
+ /**
112
+ * Parse the output of svn status.
113
+ *
114
+ * @param stdout Output of svn status.
115
+ * @return Changed paths with their status, in output order.
116
+ */
117
+ export declare function parseStatus(stdout: string): SvnStatusEntry[];
118
+ /**
119
+ * Minimal Subversion client around the svn command line.
120
+ *
121
+ * Every command runs non-interactively without caching credentials. The
122
+ * password is only sent with commits, on the standard input, so it never
123
+ * appears in the process list or the logs.
124
+ */
125
+ export declare class Svn {
126
+ readonly runner: SvnRunner;
127
+ readonly username: string | null;
128
+ readonly password: string | null;
129
+ readonly logger: Logger | null;
130
+ /**
131
+ * @param options Runner, credentials and logger.
132
+ */
133
+ constructor(options?: SvnOptions);
134
+ /**
135
+ * Run an svn subcommand with the global options.
136
+ *
137
+ * @param input Arguments, working directory and standard input.
138
+ * @return Command output.
139
+ */
140
+ run(input: SvnCommandInput): Promise<SvnCommandOutput>;
141
+ /**
142
+ * Check out a repository, by default with only its top-level directories
143
+ * so that update() can add the ones that are needed.
144
+ *
145
+ * @param input URL, working copy path and depth.
146
+ */
147
+ checkout(input: SvnCheckoutInput): Promise<void>;
148
+ /**
149
+ * Update a path of a sparse working copy to a new depth.
150
+ *
151
+ * @param input Working copy path and depth.
152
+ */
153
+ update(input: SvnUpdateInput): Promise<void>;
154
+ /**
155
+ * List the entries of a repository directory.
156
+ *
157
+ * @param url Repository or working copy path.
158
+ * @return Entry names, directories with a trailing slash.
159
+ */
160
+ list(url: string): Promise<string[]>;
161
+ /**
162
+ * Get the changed, unversioned and missing paths of a working copy,
163
+ * including the files that svn ignores by default.
164
+ *
165
+ * @param path Working copy path.
166
+ * @return Changed paths relative to the working copy.
167
+ */
168
+ status(path: string): Promise<SvnStatusEntry[]>;
169
+ /**
170
+ * Schedule unversioned paths for addition, including the files that svn
171
+ * ignores by default and missing parent directories.
172
+ *
173
+ * @param input Paths and working directory.
174
+ */
175
+ add(input: SvnPathsInput): Promise<void>;
176
+ /**
177
+ * Schedule paths for deletion, also when they're already missing.
178
+ *
179
+ * @param input Paths and working directory.
180
+ */
181
+ remove(input: SvnPathsInput): Promise<void>;
182
+ /**
183
+ * Copy a path with its history.
184
+ *
185
+ * @param input Source, target and working directory.
186
+ */
187
+ copy(input: SvnCopyInput): Promise<void>;
188
+ /**
189
+ * Set a property on paths.
190
+ *
191
+ * @param input Property name and value, paths and working directory.
192
+ */
193
+ propset(input: SvnPropsetInput): Promise<void>;
194
+ /**
195
+ * Commit a working copy with the credentials.
196
+ *
197
+ * @param input Working copy path and message.
198
+ * @return Committed revision, or null when there was nothing to commit.
199
+ */
200
+ commit(input: SvnCommitInput): Promise<number | null>;
201
+ /**
202
+ * Run a subcommand for paths, in batches so that the argument list stays
203
+ * below the operating system limit.
204
+ *
205
+ * @param args Subcommand and options.
206
+ * @param input Paths and working directory.
207
+ */
208
+ runForPaths(args: string[], input: SvnPathsInput): Promise<void>;
209
+ }
package/dist/svn.js ADDED
@@ -0,0 +1,298 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { CredentialsError } from "./errors.js";
3
+ /**
4
+ * Failed svn command, with exit code 3 for authentication and permission
5
+ * errors and 1 otherwise.
6
+ */
7
+ export class SvnError extends Error {
8
+ name = 'SvnError';
9
+ exitCode;
10
+ stderr;
11
+ /**
12
+ * @param message Error message.
13
+ * @param stderr Standard error of the command.
14
+ */
15
+ constructor(message, stderr = '') {
16
+ super(message);
17
+ this.stderr = stderr;
18
+ this.exitCode = isAuthError(stderr) ? CredentialsError.EXIT_CODE : 1;
19
+ }
20
+ }
21
+ /**
22
+ * Error codes and messages of failed authentication or missing commit access.
23
+ */
24
+ const AUTH_ERROR_PATTERN = /E170001|E170013|E175013|E215004|E220003|Authentication failed|Authorization failed|403 Forbidden|401 Unauthorized|Access to .* forbidden/i;
25
+ function isAuthError(stderr) {
26
+ return AUTH_ERROR_PATTERN.test(stderr);
27
+ }
28
+ /**
29
+ * Escape a path for svn, which reads anything after the last "@" as a peg
30
+ * revision, so that files like icon@2x.png work.
31
+ *
32
+ * @param path Working copy path.
33
+ * @return Path with a trailing "@" when it contains one.
34
+ */
35
+ export function toSvnPath(path) {
36
+ return path.includes('@') ? `${path}@` : path;
37
+ }
38
+ /**
39
+ * Create a runner that spawns the svn binary with an argument array, never
40
+ * through a shell. Messages are in English so that they can be parsed.
41
+ *
42
+ * @param binary Path or name of the svn binary.
43
+ * @return Command runner.
44
+ */
45
+ export function createSvnRunner(binary = 'svn') {
46
+ return ({ args, cwd, input }) => new Promise((resolve, reject) => {
47
+ const child = spawn(binary, args, {
48
+ cwd,
49
+ env: { ...process.env, LC_ALL: 'C' },
50
+ stdio: ['pipe', 'pipe', 'pipe'],
51
+ });
52
+ const output = { stdout: '', stderr: '' };
53
+ child.stdout.setEncoding('utf8');
54
+ child.stderr.setEncoding('utf8');
55
+ child.stdout.on('data', (data) => (output.stdout += data));
56
+ child.stderr.on('data', (data) => (output.stderr += data));
57
+ child.on('error', (error) => reject(new SvnError(`Can't run ${binary}: ${error.message}. Install the Subversion command line client.`)));
58
+ child.on('close', (code) => {
59
+ if (0 === code) {
60
+ resolve(output);
61
+ }
62
+ else {
63
+ reject(new SvnError(`svn ${args[0]} failed: ${output.stderr.trim()}`, output.stderr));
64
+ }
65
+ });
66
+ child.stdin.end(input ?? '');
67
+ });
68
+ }
69
+ /**
70
+ * Parse the output of svn status.
71
+ *
72
+ * @param stdout Output of svn status.
73
+ * @return Changed paths with their status, in output order.
74
+ */
75
+ export function parseStatus(stdout) {
76
+ return stdout
77
+ .split('\n')
78
+ .map((line) => line.replace(/\r$/, ''))
79
+ .map((line) => ({
80
+ status: getStatusType(line.slice(0, 2)),
81
+ path: line.slice(8).split('\\').join('/'),
82
+ }))
83
+ .filter((entry) => null !== entry.status && '' !== entry.path);
84
+ }
85
+ const STATUS_TYPES = {
86
+ A: 'added',
87
+ M: 'modified',
88
+ D: 'deleted',
89
+ R: 'replaced',
90
+ '!': 'missing',
91
+ '?': 'unversioned',
92
+ I: 'unversioned',
93
+ C: 'conflicted',
94
+ '~': 'obstructed',
95
+ };
96
+ /**
97
+ * Map the item and property columns of a status line to a status type.
98
+ *
99
+ * @param columns First two characters of the status line.
100
+ * @return Status type, or null for unchanged paths and other lines.
101
+ */
102
+ function getStatusType(columns) {
103
+ const [item = ' ', property = ' '] = columns;
104
+ if (Object.hasOwn(STATUS_TYPES, item)) {
105
+ return STATUS_TYPES[item] ?? null;
106
+ }
107
+ if (' ' === item && 'M' === property) {
108
+ return 'modified';
109
+ }
110
+ if (' ' === item && 'C' === property) {
111
+ return 'conflicted';
112
+ }
113
+ return null;
114
+ }
115
+ /**
116
+ * Minimal Subversion client around the svn command line.
117
+ *
118
+ * Every command runs non-interactively without caching credentials. The
119
+ * password is only sent with commits, on the standard input, so it never
120
+ * appears in the process list or the logs.
121
+ */
122
+ export class Svn {
123
+ runner;
124
+ username;
125
+ password;
126
+ logger;
127
+ /**
128
+ * @param options Runner, credentials and logger.
129
+ */
130
+ constructor(options = {}) {
131
+ this.runner = options.runner ?? createSvnRunner();
132
+ this.username = options.username ?? null;
133
+ this.password = options.password ?? null;
134
+ this.logger = options.logger ?? null;
135
+ }
136
+ /**
137
+ * Run an svn subcommand with the global options.
138
+ *
139
+ * @param input Arguments, working directory and standard input.
140
+ * @return Command output.
141
+ */
142
+ async run(input) {
143
+ const [subcommand = '', ...rest] = input.args;
144
+ const args = [
145
+ subcommand,
146
+ '--non-interactive',
147
+ '--no-auth-cache',
148
+ ...rest,
149
+ ];
150
+ this.logger?.debug(`svn ${args.join(' ')}`);
151
+ return this.runner({ ...input, args });
152
+ }
153
+ /**
154
+ * Check out a repository, by default with only its top-level directories
155
+ * so that update() can add the ones that are needed.
156
+ *
157
+ * @param input URL, working copy path and depth.
158
+ */
159
+ async checkout(input) {
160
+ const { url, path, depth = 'immediates' } = input;
161
+ await this.run({
162
+ args: [
163
+ 'checkout',
164
+ '--depth',
165
+ depth,
166
+ '--',
167
+ url,
168
+ toSvnPath(path),
169
+ ],
170
+ });
171
+ }
172
+ /**
173
+ * Update a path of a sparse working copy to a new depth.
174
+ *
175
+ * @param input Working copy path and depth.
176
+ */
177
+ async update(input) {
178
+ await this.run({
179
+ args: [
180
+ 'update',
181
+ '--set-depth',
182
+ input.depth,
183
+ '--',
184
+ toSvnPath(input.path),
185
+ ],
186
+ });
187
+ }
188
+ /**
189
+ * List the entries of a repository directory.
190
+ *
191
+ * @param url Repository or working copy path.
192
+ * @return Entry names, directories with a trailing slash.
193
+ */
194
+ async list(url) {
195
+ const { stdout } = await this.run({ args: ['list', '--', url] });
196
+ return stdout.split('\n').filter(Boolean);
197
+ }
198
+ /**
199
+ * Get the changed, unversioned and missing paths of a working copy,
200
+ * including the files that svn ignores by default.
201
+ *
202
+ * @param path Working copy path.
203
+ * @return Changed paths relative to the working copy.
204
+ */
205
+ async status(path) {
206
+ const { stdout } = await this.run({
207
+ args: ['status', '--no-ignore', '--ignore-externals'],
208
+ cwd: path,
209
+ });
210
+ return parseStatus(stdout);
211
+ }
212
+ /**
213
+ * Schedule unversioned paths for addition, including the files that svn
214
+ * ignores by default and missing parent directories.
215
+ *
216
+ * @param input Paths and working directory.
217
+ */
218
+ async add(input) {
219
+ await this.runForPaths(['add', '--no-ignore', '--parents'], input);
220
+ }
221
+ /**
222
+ * Schedule paths for deletion, also when they're already missing.
223
+ *
224
+ * @param input Paths and working directory.
225
+ */
226
+ async remove(input) {
227
+ await this.runForPaths(['delete', '--force'], input);
228
+ }
229
+ /**
230
+ * Copy a path with its history.
231
+ *
232
+ * @param input Source, target and working directory.
233
+ */
234
+ async copy(input) {
235
+ await this.run({
236
+ args: [
237
+ 'copy',
238
+ '--',
239
+ toSvnPath(input.from),
240
+ toSvnPath(input.to),
241
+ ],
242
+ cwd: input.cwd,
243
+ });
244
+ }
245
+ /**
246
+ * Set a property on paths.
247
+ *
248
+ * @param input Property name and value, paths and working directory.
249
+ */
250
+ async propset(input) {
251
+ await this.runForPaths(['propset', input.name, input.value], input);
252
+ }
253
+ /**
254
+ * Commit a working copy with the credentials.
255
+ *
256
+ * @param input Working copy path and message.
257
+ * @return Committed revision, or null when there was nothing to commit.
258
+ */
259
+ async commit(input) {
260
+ const auth = this.username ? ['--username', this.username] : [];
261
+ const password = this.password ? ['--password-from-stdin'] : [];
262
+ const { stdout } = await this.run({
263
+ args: [
264
+ 'commit',
265
+ '--message',
266
+ input.message,
267
+ ...auth,
268
+ ...password,
269
+ ],
270
+ cwd: input.path,
271
+ input: this.password ? `${this.password}\n` : undefined,
272
+ });
273
+ const revision = /Committed revision (\d+)\./.exec(stdout)?.[1];
274
+ return revision ? Number(revision) : null;
275
+ }
276
+ /**
277
+ * Run a subcommand for paths, in batches so that the argument list stays
278
+ * below the operating system limit.
279
+ *
280
+ * @param args Subcommand and options.
281
+ * @param input Paths and working directory.
282
+ */
283
+ async runForPaths(args, input) {
284
+ const batchSize = 500;
285
+ for (let start = 0; start < input.paths.length; start += batchSize) {
286
+ await this.run({
287
+ args: [
288
+ ...args,
289
+ '--',
290
+ ...input.paths
291
+ .slice(start, start + batchSize)
292
+ .map(toSvnPath),
293
+ ],
294
+ cwd: input.cwd,
295
+ });
296
+ }
297
+ }
298
+ }
package/dist/sync.d.ts ADDED
@@ -0,0 +1,32 @@
1
+ export interface SyncDirectoryInput {
2
+ /** Directory with the files to copy. */
3
+ sourceDir: string;
4
+ /** Relative file paths to copy, from selectFiles(). */
5
+ files: string[];
6
+ /** Directory to make identical to the selected files. */
7
+ targetDir: string;
8
+ }
9
+ export interface SyncResult {
10
+ /** Files that were new or changed and got copied. */
11
+ copied: string[];
12
+ /** Files that were deleted because they're no longer selected. */
13
+ deleted: string[];
14
+ }
15
+ /**
16
+ * Copy a file unless the target already has the same contents.
17
+ *
18
+ * @param source Source file path.
19
+ * @param target Target file path.
20
+ * @return Whether the file was copied.
21
+ */
22
+ export declare function copyFileIfChanged(source: string, target: string): Promise<boolean>;
23
+ /**
24
+ * Make a directory contain exactly the selected files, like rsync with
25
+ * --delete: delete files that aren't selected and the directories they
26
+ * leave empty, then copy the files that are new or changed. Files that
27
+ * didn't change are left alone, so that svn sees only real changes.
28
+ *
29
+ * @param input Source directory, selected files and target directory.
30
+ * @return Copied and deleted files, relative to the target directory.
31
+ */
32
+ export declare function syncDirectory(input: SyncDirectoryInput): Promise<SyncResult>;
package/dist/sync.js ADDED
@@ -0,0 +1,90 @@
1
+ import { copyFile, mkdir, readdir, readFile, rm, rmdir, stat, } from 'node:fs/promises';
2
+ import { dirname, join } from 'node:path';
3
+ import ignore from 'ignore';
4
+ import { isErrorCode, selectFiles } from "./file-selection.js";
5
+ /**
6
+ * SVN metadata directory, which a sync never copies, deletes or lists.
7
+ */
8
+ const SVN_DIRECTORY = '.svn';
9
+ /**
10
+ * Check if two files have the same contents.
11
+ *
12
+ * @param source Source file path.
13
+ * @param target Target file path.
14
+ * @return Whether the target exists with the same contents.
15
+ */
16
+ async function isSameFile(source, target) {
17
+ const targetStats = await stat(target).catch((error) => {
18
+ if (isErrorCode(error, 'ENOENT')) {
19
+ return null;
20
+ }
21
+ throw error;
22
+ });
23
+ const sourceStats = await stat(source);
24
+ if (!targetStats?.isFile() || targetStats.size !== sourceStats.size) {
25
+ return false;
26
+ }
27
+ const [sourceContents, targetContents] = await Promise.all([
28
+ readFile(source),
29
+ readFile(target),
30
+ ]);
31
+ return sourceContents.equals(targetContents);
32
+ }
33
+ /**
34
+ * Copy a file unless the target already has the same contents.
35
+ *
36
+ * @param source Source file path.
37
+ * @param target Target file path.
38
+ * @return Whether the file was copied.
39
+ */
40
+ export async function copyFileIfChanged(source, target) {
41
+ if (await isSameFile(source, target)) {
42
+ return false;
43
+ }
44
+ await mkdir(dirname(target), { recursive: true });
45
+ await copyFile(source, target);
46
+ return true;
47
+ }
48
+ /**
49
+ * Remove the empty directories below a directory, deepest first.
50
+ *
51
+ * @param dir Directory to clean up.
52
+ * @param root Whether this is the top directory, which is kept.
53
+ */
54
+ async function removeEmptyDirectories(dir, root = true) {
55
+ const entries = await readdir(dir, { withFileTypes: true });
56
+ const subdirs = entries.filter((entry) => entry.isDirectory() && SVN_DIRECTORY !== entry.name);
57
+ for (const subdir of subdirs) {
58
+ await removeEmptyDirectories(join(dir, subdir.name), false);
59
+ }
60
+ if (!root && 0 === (await readdir(dir)).length) {
61
+ await rmdir(dir);
62
+ }
63
+ }
64
+ /**
65
+ * Make a directory contain exactly the selected files, like rsync with
66
+ * --delete: delete files that aren't selected and the directories they
67
+ * leave empty, then copy the files that are new or changed. Files that
68
+ * didn't change are left alone, so that svn sees only real changes.
69
+ *
70
+ * @param input Source directory, selected files and target directory.
71
+ * @return Copied and deleted files, relative to the target directory.
72
+ */
73
+ export async function syncDirectory(input) {
74
+ const { sourceDir, files, targetDir } = input;
75
+ const selected = new Set(files);
76
+ await mkdir(targetDir, { recursive: true });
77
+ const existing = await selectFiles(targetDir, ignore().add(`${SVN_DIRECTORY}/`));
78
+ const deleted = existing.filter((file) => !selected.has(file));
79
+ for (const file of deleted) {
80
+ await rm(join(targetDir, file));
81
+ }
82
+ await removeEmptyDirectories(targetDir);
83
+ const copied = [];
84
+ for (const file of files) {
85
+ if (await copyFileIfChanged(join(sourceDir, file), join(targetDir, file))) {
86
+ copied.push(file);
87
+ }
88
+ }
89
+ return { copied, deleted };
90
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Archive entry type, from the Unix mode in the entry attributes.
3
+ */
4
+ export type ZipEntryType = 'file' | 'directory' | 'symlink';
5
+ export interface ZipEntryInfo {
6
+ /** Entry path inside the archive. */
7
+ path: string;
8
+ /** Entry type. */
9
+ type: ZipEntryType;
10
+ }
11
+ export interface ExtractZipInput {
12
+ /** Archive path. */
13
+ zipPath: string;
14
+ /** Directory to extract into. */
15
+ targetDir: string;
16
+ /** Root folder that every entry must be in, stripped when extracting. */
17
+ rootFolder?: string | null;
18
+ }
19
+ /**
20
+ * List the entries of an archive without extracting it.
21
+ *
22
+ * @param zipPath Archive path.
23
+ * @return Entries in archive order.
24
+ */
25
+ export declare function readZipEntries(zipPath: string): Promise<ZipEntryInfo[]>;
26
+ /**
27
+ * Collect the problems that make an archive unsafe or unexpected to extract:
28
+ * absolute paths, ".." segments, backslashes, symbolic links and entries
29
+ * outside the root folder.
30
+ *
31
+ * @param entries Entries from readZipEntries().
32
+ * @param rootFolder Root folder that every entry must be in, or null.
33
+ * @return Error messages, empty when the archive can be extracted.
34
+ */
35
+ export declare function getZipEntryErrors(entries: ZipEntryInfo[], rootFolder?: string | null): string[];
36
+ /**
37
+ * Extract an archive after checking every entry with getZipEntryErrors(),
38
+ * so that nothing is written outside the target directory. The root folder,
39
+ * when set, is stripped from the paths.
40
+ *
41
+ * @param input Archive, target directory and root folder.
42
+ * @return Extracted file paths relative to the target directory, sorted.
43
+ * @throws {ValidationError} If the archive has unsafe or unexpected entries.
44
+ */
45
+ export declare function extractZip(input: ExtractZipInput): Promise<string[]>;