@notegen/plugin-cli 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.
- package/LICENSE +21 -0
- package/README.md +349 -0
- package/dist/bin.d.ts +2 -0
- package/dist/bin.js +3 -0
- package/dist/cli.d.ts +15 -0
- package/dist/cli.js +398 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +13 -0
- package/dist/lib/archive.d.ts +17 -0
- package/dist/lib/archive.js +257 -0
- package/dist/lib/constants.d.ts +16 -0
- package/dist/lib/constants.js +16 -0
- package/dist/lib/diagnostics.d.ts +25 -0
- package/dist/lib/diagnostics.js +49 -0
- package/dist/lib/files.d.ts +30 -0
- package/dist/lib/files.js +396 -0
- package/dist/lib/integrity.d.ts +20 -0
- package/dist/lib/integrity.js +162 -0
- package/dist/lib/manifest.d.ts +13 -0
- package/dist/lib/manifest.js +645 -0
- package/dist/lib/package.d.ts +21 -0
- package/dist/lib/package.js +229 -0
- package/dist/lib/path-rules.d.ts +23 -0
- package/dist/lib/path-rules.js +153 -0
- package/dist/lib/project.d.ts +21 -0
- package/dist/lib/project.js +209 -0
- package/dist/lib/scaffold.d.ts +22 -0
- package/dist/lib/scaffold.js +230 -0
- package/dist/lib/signing.d.ts +26 -0
- package/dist/lib/signing.js +181 -0
- package/dist/lib/strict-json.d.ts +26 -0
- package/dist/lib/strict-json.js +210 -0
- package/dist/lib/tasks.d.ts +70 -0
- package/dist/lib/tasks.js +241 -0
- package/dist/lib/watch.d.ts +6 -0
- package/dist/lib/watch.js +63 -0
- package/package.json +64 -0
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import { Readable } from 'node:stream';
|
|
4
|
+
import { deflateRawSync } from 'node:zlib';
|
|
5
|
+
import * as yauzl from 'yauzl';
|
|
6
|
+
import * as yazl from 'yazl';
|
|
7
|
+
import { MAX_ARCHIVE_BYTES, MAX_ARCHIVE_ENTRIES, MAX_COMPRESSION_RATIO, MAX_ENTRY_BYTES, MAX_UNCOMPRESSED_BYTES, } from './constants.js';
|
|
8
|
+
import { diagnostic, DiagnosticError } from './diagnostics.js';
|
|
9
|
+
import { atomicWriteFile, assertRegularFile, fileSize } from './files.js';
|
|
10
|
+
import { assertUniquePackagePaths, validatePackagePath, } from './path-rules.js';
|
|
11
|
+
function archiveError(code, message, path) {
|
|
12
|
+
return new DiagnosticError(diagnostic({ code, message, path }));
|
|
13
|
+
}
|
|
14
|
+
const CRC32_TABLE = Uint32Array.from({ length: 256 }, (_, value) => {
|
|
15
|
+
let current = value;
|
|
16
|
+
for (let bit = 0; bit < 8; bit += 1) {
|
|
17
|
+
current = (current & 1) === 1 ? 0xedb88320 ^ (current >>> 1) : current >>> 1;
|
|
18
|
+
}
|
|
19
|
+
return current >>> 0;
|
|
20
|
+
});
|
|
21
|
+
function crc32(bytes) {
|
|
22
|
+
let value = 0xffffffff;
|
|
23
|
+
for (const byte of bytes) {
|
|
24
|
+
value = (CRC32_TABLE[(value ^ byte) & 0xff] ?? 0) ^ (value >>> 8);
|
|
25
|
+
}
|
|
26
|
+
return (value ^ 0xffffffff) >>> 0;
|
|
27
|
+
}
|
|
28
|
+
function extractedEntryCount(entries) {
|
|
29
|
+
const extracted = new Set();
|
|
30
|
+
for (const entry of entries) {
|
|
31
|
+
extracted.add(entry.path);
|
|
32
|
+
const segments = entry.path.split('/');
|
|
33
|
+
for (let index = 1; index < segments.length; index += 1) {
|
|
34
|
+
extracted.add(segments.slice(0, index).join('/'));
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return extracted.size;
|
|
38
|
+
}
|
|
39
|
+
async function openZip(bytes, path) {
|
|
40
|
+
return new Promise((resolve, reject) => {
|
|
41
|
+
yauzl.fromBuffer(bytes, {
|
|
42
|
+
autoClose: true,
|
|
43
|
+
decodeStrings: true,
|
|
44
|
+
lazyEntries: true,
|
|
45
|
+
strictFileNames: true,
|
|
46
|
+
validateEntrySizes: true,
|
|
47
|
+
}, (error, zipFile) => {
|
|
48
|
+
if (error || !zipFile) {
|
|
49
|
+
reject(archiveError('archive.invalid', error?.message ?? 'The ZIP archive could not be opened', path));
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
resolve(zipFile);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
function openEntryStream(zipFile, entry) {
|
|
57
|
+
return new Promise((resolve, reject) => {
|
|
58
|
+
zipFile.openReadStream(entry, (error, stream) => {
|
|
59
|
+
if (error || !stream) {
|
|
60
|
+
reject(archiveError('archive.entry_unreadable', error?.message ?? 'The ZIP entry could not be read', entry.fileName));
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
resolve(stream);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
async function readStreamLimited(stream, maximum, path) {
|
|
68
|
+
const chunks = [];
|
|
69
|
+
let total = 0;
|
|
70
|
+
for await (const value of stream) {
|
|
71
|
+
const chunk = typeof value === 'string' ? Buffer.from(value) : Buffer.from(value);
|
|
72
|
+
total += chunk.length;
|
|
73
|
+
if (total > maximum) {
|
|
74
|
+
stream.destroy();
|
|
75
|
+
throw archiveError('archive.entry_too_large', `Archive entry exceeds ${maximum} bytes`, path);
|
|
76
|
+
}
|
|
77
|
+
chunks.push(chunk);
|
|
78
|
+
}
|
|
79
|
+
return Buffer.concat(chunks, total);
|
|
80
|
+
}
|
|
81
|
+
function assertSafeZipMetadata(entry) {
|
|
82
|
+
if ((entry.generalPurposeBitFlag & 0x1) !== 0) {
|
|
83
|
+
throw archiveError('archive.encrypted', 'Encrypted ZIP entries are not supported', entry.fileName);
|
|
84
|
+
}
|
|
85
|
+
const directory = entry.fileName.endsWith('/');
|
|
86
|
+
if (entry.compressionMethod !== 0 && entry.compressionMethod !== 8) {
|
|
87
|
+
throw archiveError('archive.compression', 'Only stored and deflate ZIP entries are supported', entry.fileName);
|
|
88
|
+
}
|
|
89
|
+
if (entry.uncompressedSize > MAX_ENTRY_BYTES) {
|
|
90
|
+
throw archiveError('archive.entry_too_large', `Archive entry exceeds ${MAX_ENTRY_BYTES} bytes`, entry.fileName);
|
|
91
|
+
}
|
|
92
|
+
if (entry.uncompressedSize > 0
|
|
93
|
+
&& (entry.compressedSize === 0
|
|
94
|
+
|| (entry.uncompressedSize > 1024 * 1024
|
|
95
|
+
&& entry.uncompressedSize / entry.compressedSize > MAX_COMPRESSION_RATIO))) {
|
|
96
|
+
throw archiveError('archive.compression_ratio', `Archive entry exceeds the ${MAX_COMPRESSION_RATIO}:1 compression-ratio limit`, entry.fileName);
|
|
97
|
+
}
|
|
98
|
+
const hostSystem = (entry.versionMadeBy >>> 8) & 0xff;
|
|
99
|
+
if (hostSystem === 3) {
|
|
100
|
+
const mode = (entry.externalFileAttributes >>> 16) & 0xffff;
|
|
101
|
+
const fileType = mode & 0o170000;
|
|
102
|
+
if ((fileType !== 0 && fileType !== 0o040000 && fileType !== 0o100000)
|
|
103
|
+
|| (directory && fileType === 0o100000)
|
|
104
|
+
|| (!directory && fileType === 0o040000)) {
|
|
105
|
+
throw archiveError('archive.special_file', 'Symbolic links and special files are not allowed', entry.fileName);
|
|
106
|
+
}
|
|
107
|
+
if (!directory && (mode & 0o111) !== 0) {
|
|
108
|
+
throw archiveError('archive.executable', 'Executable file modes are not allowed', entry.fileName);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
else if (hostSystem === 0 && entry.externalFileAttributes !== 0) {
|
|
112
|
+
const attributesDescribeDirectory = (entry.externalFileAttributes & 0x10) !== 0;
|
|
113
|
+
if (attributesDescribeDirectory !== directory) {
|
|
114
|
+
throw archiveError('archive.file-type-mismatch', 'DOS ZIP attributes disagree with the entry path type', entry.fileName);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
if (directory && entry.uncompressedSize !== 0) {
|
|
118
|
+
throw archiveError('archive.directory_payload', 'ZIP directory entries must not contain payload bytes', entry.fileName);
|
|
119
|
+
}
|
|
120
|
+
return directory;
|
|
121
|
+
}
|
|
122
|
+
export async function readPackageArchive(path) {
|
|
123
|
+
await assertRegularFile(path, 'Plugin archive');
|
|
124
|
+
const size = await fileSize(path);
|
|
125
|
+
if (size > MAX_ARCHIVE_BYTES) {
|
|
126
|
+
throw archiveError('archive.too_large', `Plugin archive exceeds ${MAX_ARCHIVE_BYTES} bytes`, path);
|
|
127
|
+
}
|
|
128
|
+
const archiveBytes = await readFile(path);
|
|
129
|
+
if (archiveBytes.byteLength > MAX_ARCHIVE_BYTES) {
|
|
130
|
+
throw archiveError('archive.too_large', `Plugin archive exceeds ${MAX_ARCHIVE_BYTES} bytes`, path);
|
|
131
|
+
}
|
|
132
|
+
const sha256 = createHash('sha256').update(archiveBytes).digest('hex');
|
|
133
|
+
const zipFile = await openZip(archiveBytes, path);
|
|
134
|
+
if (zipFile.entryCount > MAX_ARCHIVE_ENTRIES) {
|
|
135
|
+
zipFile.close();
|
|
136
|
+
throw archiveError('archive.too_many_entries', `Plugin archive contains more than ${MAX_ARCHIVE_ENTRIES} entries`, path);
|
|
137
|
+
}
|
|
138
|
+
const files = new Map();
|
|
139
|
+
const paths = [];
|
|
140
|
+
let totalUncompressed = 0;
|
|
141
|
+
await new Promise((resolve, reject) => {
|
|
142
|
+
let settled = false;
|
|
143
|
+
const failArchive = (error) => {
|
|
144
|
+
if (settled)
|
|
145
|
+
return;
|
|
146
|
+
settled = true;
|
|
147
|
+
zipFile.close();
|
|
148
|
+
reject(error);
|
|
149
|
+
};
|
|
150
|
+
zipFile.once('error', (error) => {
|
|
151
|
+
failArchive(archiveError('archive.invalid', error.message, path));
|
|
152
|
+
});
|
|
153
|
+
zipFile.on('entry', (entry) => {
|
|
154
|
+
void (async () => {
|
|
155
|
+
const directory = assertSafeZipMetadata(entry);
|
|
156
|
+
const safePath = validatePackagePath(entry.fileName, {
|
|
157
|
+
directory,
|
|
158
|
+
label: 'ZIP entry path',
|
|
159
|
+
});
|
|
160
|
+
totalUncompressed += entry.uncompressedSize;
|
|
161
|
+
if (totalUncompressed > MAX_UNCOMPRESSED_BYTES) {
|
|
162
|
+
throw archiveError('archive.uncompressed_too_large', `Expanded plugin archive exceeds ${MAX_UNCOMPRESSED_BYTES} bytes`, path);
|
|
163
|
+
}
|
|
164
|
+
paths.push({ path: safePath, directory });
|
|
165
|
+
assertUniquePackagePaths(paths);
|
|
166
|
+
if (directory) {
|
|
167
|
+
zipFile.readEntry();
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
const stream = await openEntryStream(zipFile, entry);
|
|
171
|
+
const bytes = await readStreamLimited(stream, MAX_ENTRY_BYTES, safePath);
|
|
172
|
+
if (bytes.length !== entry.uncompressedSize) {
|
|
173
|
+
throw archiveError('archive.size_mismatch', 'ZIP entry size does not match its central-directory metadata', safePath);
|
|
174
|
+
}
|
|
175
|
+
if (crc32(bytes) !== (entry.crc32 >>> 0)) {
|
|
176
|
+
throw archiveError('archive.crc-mismatch', 'ZIP entry content does not match its CRC32 metadata', safePath);
|
|
177
|
+
}
|
|
178
|
+
files.set(safePath, bytes);
|
|
179
|
+
zipFile.readEntry();
|
|
180
|
+
})().catch(failArchive);
|
|
181
|
+
});
|
|
182
|
+
zipFile.once('end', () => {
|
|
183
|
+
if (settled)
|
|
184
|
+
return;
|
|
185
|
+
settled = true;
|
|
186
|
+
resolve();
|
|
187
|
+
});
|
|
188
|
+
zipFile.readEntry();
|
|
189
|
+
});
|
|
190
|
+
if (extractedEntryCount(paths) > MAX_ARCHIVE_ENTRIES) {
|
|
191
|
+
throw archiveError('archive.too_many_extracted_entries', `Extracted plugin archive would exceed ${MAX_ARCHIVE_ENTRIES} entries`, path);
|
|
192
|
+
}
|
|
193
|
+
return Object.freeze({ files, sha256, size: archiveBytes.byteLength });
|
|
194
|
+
}
|
|
195
|
+
async function outputStreamToBuffer(stream, maximum) {
|
|
196
|
+
const chunks = [];
|
|
197
|
+
let size = 0;
|
|
198
|
+
for await (const value of stream) {
|
|
199
|
+
const chunk = typeof value === 'string' ? Buffer.from(value) : Buffer.from(value);
|
|
200
|
+
size += chunk.length;
|
|
201
|
+
if (size > maximum) {
|
|
202
|
+
stream.destroy();
|
|
203
|
+
throw archiveError('archive.too_large', `Generated plugin archive exceeds ${maximum} bytes`);
|
|
204
|
+
}
|
|
205
|
+
chunks.push(chunk);
|
|
206
|
+
}
|
|
207
|
+
return Buffer.concat(chunks, size);
|
|
208
|
+
}
|
|
209
|
+
export async function writePackageArchive(input, outputPath, options = {}) {
|
|
210
|
+
const files = [...input].map((file) => ({
|
|
211
|
+
path: validatePackagePath(file.path),
|
|
212
|
+
bytes: Buffer.from(file.bytes),
|
|
213
|
+
}));
|
|
214
|
+
if (files.length === 0) {
|
|
215
|
+
throw archiveError('archive.empty', 'A plugin archive cannot be empty');
|
|
216
|
+
}
|
|
217
|
+
if (files.length > MAX_ARCHIVE_ENTRIES) {
|
|
218
|
+
throw archiveError('archive.too_many_entries', `A plugin archive cannot contain more than ${MAX_ARCHIVE_ENTRIES} entries`);
|
|
219
|
+
}
|
|
220
|
+
assertUniquePackagePaths(files.map((file) => ({ path: file.path })));
|
|
221
|
+
if (extractedEntryCount(files.map((file) => ({ path: file.path }))) > MAX_ARCHIVE_ENTRIES) {
|
|
222
|
+
throw archiveError('archive.too_many_extracted_entries', `A plugin archive cannot expand to more than ${MAX_ARCHIVE_ENTRIES} entries`);
|
|
223
|
+
}
|
|
224
|
+
let expandedSize = 0;
|
|
225
|
+
for (const file of files) {
|
|
226
|
+
if (file.bytes.length > MAX_ENTRY_BYTES) {
|
|
227
|
+
throw archiveError('archive.entry_too_large', `Package file exceeds ${MAX_ENTRY_BYTES} bytes`, file.path);
|
|
228
|
+
}
|
|
229
|
+
expandedSize += file.bytes.length;
|
|
230
|
+
}
|
|
231
|
+
if (expandedSize > MAX_UNCOMPRESSED_BYTES) {
|
|
232
|
+
throw archiveError('archive.uncompressed_too_large', `Package payload exceeds ${MAX_UNCOMPRESSED_BYTES} bytes`);
|
|
233
|
+
}
|
|
234
|
+
files.sort((left, right) => left.path < right.path ? -1 : left.path > right.path ? 1 : 0);
|
|
235
|
+
const zip = new yazl.ZipFile();
|
|
236
|
+
const timestamp = new Date('1980-01-01T00:00:00.000Z');
|
|
237
|
+
for (const file of files) {
|
|
238
|
+
const compressed = file.bytes.length <= 1024 * 1024
|
|
239
|
+
? true
|
|
240
|
+
: file.bytes.length <= deflateRawSync(file.bytes, { level: 9 }).byteLength * MAX_COMPRESSION_RATIO;
|
|
241
|
+
zip.addBuffer(file.bytes, file.path, {
|
|
242
|
+
compress: compressed,
|
|
243
|
+
compressionLevel: 9,
|
|
244
|
+
forceZip64Format: false,
|
|
245
|
+
mode: 0o100644,
|
|
246
|
+
mtime: timestamp,
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
zip.end();
|
|
250
|
+
const bytes = await outputStreamToBuffer(zip.outputStream, MAX_ARCHIVE_BYTES);
|
|
251
|
+
await atomicWriteFile(outputPath, bytes, { force: options.force, mode: 0o644 });
|
|
252
|
+
return Object.freeze({
|
|
253
|
+
path: outputPath,
|
|
254
|
+
sha256: createHash('sha256').update(bytes).digest('hex'),
|
|
255
|
+
size: bytes.length,
|
|
256
|
+
});
|
|
257
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export declare const PACKAGE_EXTENSION = ".notegen-plugin";
|
|
2
|
+
export declare const UNSIGNED_PACKAGE_EXTENSION = ".unsigned.notegen-plugin";
|
|
3
|
+
export declare const DEVELOPMENT_OUTPUT_DIRECTORY = ".notegen/package";
|
|
4
|
+
export declare const RELEASE_OUTPUT_DIRECTORY = ".notegen/releases";
|
|
5
|
+
export declare const MAX_ARCHIVE_BYTES: number;
|
|
6
|
+
export declare const MAX_UNCOMPRESSED_BYTES: number;
|
|
7
|
+
export declare const MAX_ENTRY_BYTES: number;
|
|
8
|
+
export declare const MAX_ENTRY_FILE_BYTES: number;
|
|
9
|
+
export declare const MAX_ARCHIVE_ENTRIES = 256;
|
|
10
|
+
export declare const MAX_COMPRESSION_RATIO = 100;
|
|
11
|
+
export declare const EXIT_SUCCESS = 0;
|
|
12
|
+
export declare const EXIT_PROJECT_FAILURE = 1;
|
|
13
|
+
export declare const EXIT_USAGE = 2;
|
|
14
|
+
export declare const EXIT_UNSAFE_REFUSAL = 3;
|
|
15
|
+
export declare const EXIT_UNEXPECTED = 70;
|
|
16
|
+
export declare const EXIT_INTERRUPTED = 130;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export const PACKAGE_EXTENSION = '.notegen-plugin';
|
|
2
|
+
export const UNSIGNED_PACKAGE_EXTENSION = '.unsigned.notegen-plugin';
|
|
3
|
+
export const DEVELOPMENT_OUTPUT_DIRECTORY = '.notegen/package';
|
|
4
|
+
export const RELEASE_OUTPUT_DIRECTORY = '.notegen/releases';
|
|
5
|
+
export const MAX_ARCHIVE_BYTES = 20 * 1024 * 1024;
|
|
6
|
+
export const MAX_UNCOMPRESSED_BYTES = 50 * 1024 * 1024;
|
|
7
|
+
export const MAX_ENTRY_BYTES = 10 * 1024 * 1024;
|
|
8
|
+
export const MAX_ENTRY_FILE_BYTES = 5 * 1024 * 1024;
|
|
9
|
+
export const MAX_ARCHIVE_ENTRIES = 256;
|
|
10
|
+
export const MAX_COMPRESSION_RATIO = 100;
|
|
11
|
+
export const EXIT_SUCCESS = 0;
|
|
12
|
+
export const EXIT_PROJECT_FAILURE = 1;
|
|
13
|
+
export const EXIT_USAGE = 2;
|
|
14
|
+
export const EXIT_UNSAFE_REFUSAL = 3;
|
|
15
|
+
export const EXIT_UNEXPECTED = 70;
|
|
16
|
+
export const EXIT_INTERRUPTED = 130;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export type DiagnosticSeverity = 'error' | 'warning';
|
|
2
|
+
export interface Diagnostic {
|
|
3
|
+
readonly severity: DiagnosticSeverity;
|
|
4
|
+
readonly code: string;
|
|
5
|
+
readonly message: string;
|
|
6
|
+
readonly path?: string;
|
|
7
|
+
readonly hint?: string;
|
|
8
|
+
}
|
|
9
|
+
export interface DiagnosticInput {
|
|
10
|
+
readonly severity?: DiagnosticSeverity;
|
|
11
|
+
readonly code: string;
|
|
12
|
+
readonly message: string;
|
|
13
|
+
readonly path?: string;
|
|
14
|
+
readonly hint?: string;
|
|
15
|
+
}
|
|
16
|
+
export declare function diagnostic(input: DiagnosticInput): Diagnostic;
|
|
17
|
+
export declare class DiagnosticError extends Error {
|
|
18
|
+
readonly diagnostics: readonly Diagnostic[];
|
|
19
|
+
constructor(diagnostics: Diagnostic | readonly Diagnostic[]);
|
|
20
|
+
}
|
|
21
|
+
export declare function fail(code: string, message: string, path?: string, hint?: string): never;
|
|
22
|
+
export declare function isDiagnosticError(value: unknown): value is DiagnosticError;
|
|
23
|
+
export declare function hasDiagnosticErrors(diagnostics: readonly Diagnostic[]): boolean;
|
|
24
|
+
export declare function formatDiagnostic(value: Diagnostic): string;
|
|
25
|
+
export declare function diagnosticsFromError(error: unknown): readonly Diagnostic[];
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
export function diagnostic(input) {
|
|
2
|
+
return Object.freeze({
|
|
3
|
+
severity: input.severity ?? 'error',
|
|
4
|
+
code: input.code,
|
|
5
|
+
message: input.message,
|
|
6
|
+
...(input.path === undefined ? {} : { path: input.path }),
|
|
7
|
+
...(input.hint === undefined ? {} : { hint: input.hint }),
|
|
8
|
+
});
|
|
9
|
+
}
|
|
10
|
+
export class DiagnosticError extends Error {
|
|
11
|
+
diagnostics;
|
|
12
|
+
constructor(diagnostics) {
|
|
13
|
+
const values = Array.isArray(diagnostics)
|
|
14
|
+
? diagnostics
|
|
15
|
+
: [diagnostics];
|
|
16
|
+
const first = values[0];
|
|
17
|
+
super(first?.message ?? 'NoteGen plugin validation failed');
|
|
18
|
+
this.name = 'DiagnosticError';
|
|
19
|
+
this.diagnostics = Object.freeze([...values]);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export function fail(code, message, path, hint) {
|
|
23
|
+
throw new DiagnosticError(diagnostic({ code, message, path, hint }));
|
|
24
|
+
}
|
|
25
|
+
export function isDiagnosticError(value) {
|
|
26
|
+
return value instanceof DiagnosticError;
|
|
27
|
+
}
|
|
28
|
+
export function hasDiagnosticErrors(diagnostics) {
|
|
29
|
+
return diagnostics.some((item) => item.severity === 'error');
|
|
30
|
+
}
|
|
31
|
+
export function formatDiagnostic(value) {
|
|
32
|
+
const location = value.path ? ` ${value.path}` : '';
|
|
33
|
+
const hint = value.hint ? `\n hint: ${value.hint}` : '';
|
|
34
|
+
return `${value.severity.toUpperCase()} [${value.code}]${location}: ${value.message}${hint}`;
|
|
35
|
+
}
|
|
36
|
+
export function diagnosticsFromError(error) {
|
|
37
|
+
if (isDiagnosticError(error))
|
|
38
|
+
return error.diagnostics;
|
|
39
|
+
if (error instanceof Error) {
|
|
40
|
+
return [diagnostic({
|
|
41
|
+
code: 'internal.unexpected',
|
|
42
|
+
message: error.message || error.name,
|
|
43
|
+
})];
|
|
44
|
+
}
|
|
45
|
+
return [diagnostic({
|
|
46
|
+
code: 'internal.unexpected',
|
|
47
|
+
message: typeof error === 'string' ? error : 'Unexpected failure',
|
|
48
|
+
})];
|
|
49
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export declare function pathExists(path: string): Promise<boolean>;
|
|
2
|
+
export declare function assertRegularFile(path: string, label?: string): Promise<void>;
|
|
3
|
+
export declare function assertDirectory(path: string, label?: string): Promise<void>;
|
|
4
|
+
export declare function assertInside(parent: string, candidate: string, label?: string): void;
|
|
5
|
+
export declare function assertNoSymlinkComponents(parent: string, candidate: string, label?: string): Promise<void>;
|
|
6
|
+
export declare function readUtf8File(path: string, label?: string): Promise<string>;
|
|
7
|
+
export declare function writeFileExclusive(path: string, contents: string | Uint8Array, mode?: number): Promise<void>;
|
|
8
|
+
export declare function atomicWriteFile(path: string, contents: string | Uint8Array, options?: {
|
|
9
|
+
readonly force?: boolean;
|
|
10
|
+
readonly mode?: number;
|
|
11
|
+
}): Promise<void>;
|
|
12
|
+
export interface AtomicFileWrite {
|
|
13
|
+
readonly path: string;
|
|
14
|
+
readonly contents: string | Uint8Array;
|
|
15
|
+
readonly mode?: number;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Publishes a related set of files as one recoverable transaction. Each target
|
|
19
|
+
* is linked from a fully written sibling file, and any replaced files are kept
|
|
20
|
+
* until every new target has been committed. If a later commit fails, already
|
|
21
|
+
* committed targets are removed and the previous set is restored.
|
|
22
|
+
*/
|
|
23
|
+
export declare function atomicWriteFiles(input: readonly AtomicFileWrite[], options?: {
|
|
24
|
+
readonly force?: boolean;
|
|
25
|
+
}): Promise<void>;
|
|
26
|
+
export declare function replaceDirectoryAtomically(destination: string, populate: (temporary: string) => Promise<void>): Promise<void>;
|
|
27
|
+
export declare function makeTemporaryDirectory(prefix: string): Promise<string>;
|
|
28
|
+
export declare function copyRegularFile(source: string, destination: string): Promise<void>;
|
|
29
|
+
export declare function canonicalPath(path: string): Promise<string>;
|
|
30
|
+
export declare function fileSize(path: string): Promise<number>;
|