@extenshi/cli 0.20.0 → 0.22.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/README.md +4 -0
- package/dist/amo-source-detect.d.ts +18 -0
- package/dist/amo-source-detect.d.ts.map +1 -0
- package/dist/amo-source-detect.js +41 -0
- package/dist/amo-source-detect.js.map +1 -0
- package/dist/amo-source.d.ts +41 -0
- package/dist/amo-source.d.ts.map +1 -0
- package/dist/amo-source.js +240 -0
- package/dist/amo-source.js.map +1 -0
- package/dist/amowebstore-md.d.ts +38 -0
- package/dist/amowebstore-md.d.ts.map +1 -0
- package/dist/amowebstore-md.js +118 -0
- package/dist/amowebstore-md.js.map +1 -0
- package/dist/auto-evidence.d.ts +100 -0
- package/dist/auto-evidence.d.ts.map +1 -0
- package/dist/auto-evidence.js +197 -0
- package/dist/auto-evidence.js.map +1 -0
- package/dist/cli.js +320 -8
- package/dist/cli.js.map +1 -1
- package/dist/edgewebstore-md.d.ts +26 -0
- package/dist/edgewebstore-md.d.ts.map +1 -0
- package/dist/edgewebstore-md.js +95 -0
- package/dist/edgewebstore-md.js.map +1 -0
- package/dist/evidence-ingest.d.ts +60 -0
- package/dist/evidence-ingest.d.ts.map +1 -0
- package/dist/evidence-ingest.js +141 -0
- package/dist/evidence-ingest.js.map +1 -0
- package/dist/expected-error.d.ts +9 -0
- package/dist/expected-error.d.ts.map +1 -1
- package/dist/expected-error.js +16 -1
- package/dist/expected-error.js.map +1 -1
- package/dist/firefox-rdp.d.ts +55 -0
- package/dist/firefox-rdp.d.ts.map +1 -0
- package/dist/firefox-rdp.js +203 -0
- package/dist/firefox-rdp.js.map +1 -0
- package/dist/gecko-id.d.ts +55 -0
- package/dist/gecko-id.d.ts.map +1 -0
- package/dist/gecko-id.js +132 -0
- package/dist/gecko-id.js.map +1 -0
- package/dist/generate-listing.d.ts +19 -8
- package/dist/generate-listing.d.ts.map +1 -1
- package/dist/generate-listing.js +66 -18
- package/dist/generate-listing.js.map +1 -1
- package/dist/icon.d.ts +18 -0
- package/dist/icon.d.ts.map +1 -1
- package/dist/icon.js +29 -20
- package/dist/icon.js.map +1 -1
- package/dist/json-ld.d.ts +89 -0
- package/dist/json-ld.d.ts.map +1 -0
- package/dist/json-ld.js +118 -0
- package/dist/json-ld.js.map +1 -0
- package/dist/landing-page.d.ts +123 -0
- package/dist/landing-page.d.ts.map +1 -0
- package/dist/landing-page.js +405 -0
- package/dist/landing-page.js.map +1 -0
- package/dist/media.d.ts +42 -0
- package/dist/media.d.ts.map +1 -0
- package/dist/media.js +96 -0
- package/dist/media.js.map +1 -0
- package/dist/package.d.ts +163 -0
- package/dist/package.d.ts.map +1 -0
- package/dist/package.js +500 -0
- package/dist/package.js.map +1 -0
- package/dist/page.d.ts +141 -0
- package/dist/page.d.ts.map +1 -0
- package/dist/page.js +227 -0
- package/dist/page.js.map +1 -0
- package/dist/pay-contract.d.ts +20 -13
- package/dist/pay-contract.d.ts.map +1 -1
- package/dist/pay-contract.js +20 -11
- package/dist/pay-contract.js.map +1 -1
- package/dist/pay-test-cycle.d.ts +40 -0
- package/dist/pay-test-cycle.d.ts.map +1 -0
- package/dist/pay-test-cycle.js +207 -0
- package/dist/pay-test-cycle.js.map +1 -0
- package/dist/pay.d.ts +1 -1
- package/dist/pay.d.ts.map +1 -1
- package/dist/pay.js +1 -0
- package/dist/pay.js.map +1 -1
- package/dist/project.d.ts +39 -1
- package/dist/project.d.ts.map +1 -1
- package/dist/project.js +231 -20
- package/dist/project.js.map +1 -1
- package/dist/publish.d.ts +33 -0
- package/dist/publish.d.ts.map +1 -1
- package/dist/publish.js +113 -2
- package/dist/publish.js.map +1 -1
- package/dist/review-checks.d.ts +13 -0
- package/dist/review-checks.d.ts.map +1 -1
- package/dist/review-checks.js.map +1 -1
- package/dist/review-risk.d.ts +10 -1
- package/dist/review-risk.d.ts.map +1 -1
- package/dist/review-risk.js +1 -0
- package/dist/review-risk.js.map +1 -1
- package/dist/scan.d.ts +3 -2
- package/dist/scan.d.ts.map +1 -1
- package/dist/scan.js +4 -2
- package/dist/scan.js.map +1 -1
- package/dist/screenshots-scenes.d.ts +1383 -0
- package/dist/screenshots-scenes.d.ts.map +1 -0
- package/dist/screenshots-scenes.js +276 -0
- package/dist/screenshots-scenes.js.map +1 -0
- package/dist/screenshots.d.ts +145 -0
- package/dist/screenshots.d.ts.map +1 -0
- package/dist/screenshots.js +557 -0
- package/dist/screenshots.js.map +1 -0
- package/dist/smoke.d.ts +81 -0
- package/dist/smoke.d.ts.map +1 -0
- package/dist/smoke.js +491 -0
- package/dist/smoke.js.map +1 -0
- package/dist/store-assets.d.ts +139 -0
- package/dist/store-assets.d.ts.map +1 -0
- package/dist/store-assets.js +582 -0
- package/dist/store-assets.js.map +1 -0
- package/dist/telemetry.d.ts.map +1 -1
- package/dist/telemetry.js +3 -0
- package/dist/telemetry.js.map +1 -1
- package/dist/zip.d.ts +57 -0
- package/dist/zip.d.ts.map +1 -0
- package/dist/zip.js +134 -0
- package/dist/zip.js.map +1 -0
- package/package.json +4 -3
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `extenshi package <dir> --browser chrome,edge,firefox` — one reproducible
|
|
3
|
+
* store package per browser from an unpacked build directory.
|
|
4
|
+
*
|
|
5
|
+
* REPRODUCIBLE means byte-identical for the same files on any machine, in any
|
|
6
|
+
* timezone, under any umask: Dojo release readiness binds every piece of
|
|
7
|
+
* evidence (tests, scan, installed smoke) to the package's SHA-256, so a CI
|
|
8
|
+
* build and a local build of one commit must agree to the byte. The archive is
|
|
9
|
+
* written by the SAME writer Dojo uses (`zip.ts`, a byte-identical copy of
|
|
10
|
+
* dojo/src/lib/tools/zip.ts) with the same choices as Dojo's extension archive:
|
|
11
|
+
* - entries sorted by path, manifest.json at the root, no directory entries;
|
|
12
|
+
* - every entry stamped 2000-01-01 00:00:00 (DOS fields carry no timezone, so
|
|
13
|
+
* the constant is built from LOCAL components — see REPRODUCIBLE_MTIME);
|
|
14
|
+
* - STORE (no compression): deflate output depends on the zlib build, which
|
|
15
|
+
* differs between Node releases, and reproducibility across machines is the
|
|
16
|
+
* point. Store uploads accept uncompressed archives;
|
|
17
|
+
* - "version made by" MS-DOS with zero external attributes, so no Unix mode
|
|
18
|
+
* bits, owners or extra fields — nothing machine-specific survives.
|
|
19
|
+
*
|
|
20
|
+
* The user's source directory is never modified. `--fix-war` and `--gecko-id`
|
|
21
|
+
* rewrite manifest.json in the PACKAGED copy only, and every rewrite is listed
|
|
22
|
+
* in the output (`changes`).
|
|
23
|
+
*/
|
|
24
|
+
export declare const PACKAGE_BROWSERS: readonly ["chrome", "edge", "firefox"];
|
|
25
|
+
export type PackageBrowser = (typeof PACKAGE_BROWSERS)[number];
|
|
26
|
+
/** Written next to the zips; `release prepare`, `test smoke`, `evidence ingest` read it directly. */
|
|
27
|
+
export declare const PACKAGES_MANIFEST_FILE = "extenshi-packages.json";
|
|
28
|
+
export declare const PACKAGES_MANIFEST_SCHEMA_VERSION = 1;
|
|
29
|
+
export interface PackageFile {
|
|
30
|
+
/** Posix path relative to the package root. */
|
|
31
|
+
path: string;
|
|
32
|
+
bytes: Uint8Array;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* `--exclude` pattern → matcher, gitignore-style: `*` stays inside one path
|
|
36
|
+
* segment, `**` crosses segments, a pattern without `/` matches a basename at any
|
|
37
|
+
* depth, a pattern with `/` is anchored at the package root, and a pattern naming
|
|
38
|
+
* a directory excludes everything under it.
|
|
39
|
+
*/
|
|
40
|
+
export declare function excludeMatcher(patterns: readonly string[]): (relative: string) => boolean;
|
|
41
|
+
/**
|
|
42
|
+
* A `web_accessible_resources` entry as the BROWSER matches it: `*` matches any
|
|
43
|
+
* run of characters INCLUDING `/` (Chromium's MatchPattern and Firefox's
|
|
44
|
+
* MatchGlob both work this way), `?` one character. Leading `/` is ignored.
|
|
45
|
+
*/
|
|
46
|
+
export declare function warGlobToRegExp(glob: string): RegExp;
|
|
47
|
+
export interface CollectedFiles {
|
|
48
|
+
files: PackageFile[];
|
|
49
|
+
/** Relative paths left out by --exclude (dot-entries and node_modules are not listed). */
|
|
50
|
+
excluded: string[];
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Read every file under `root`, sorted, skipping dot-entries (`.git`, `.github`,
|
|
54
|
+
* `.extenshi`, `.DS_Store`, …), `node_modules` and `skipDirs` (the output
|
|
55
|
+
* directory when it sits inside the source). Symlinks are refused: following one
|
|
56
|
+
* would pack bytes from outside the directory, and a link itself is not
|
|
57
|
+
* something every store unpacks the same way.
|
|
58
|
+
*/
|
|
59
|
+
export declare function collectFiles(root: string, options?: {
|
|
60
|
+
exclude?: readonly string[];
|
|
61
|
+
skipDirs?: readonly string[];
|
|
62
|
+
}): CollectedFiles;
|
|
63
|
+
type Json = Record<string, any>;
|
|
64
|
+
/**
|
|
65
|
+
* Every file the manifest names, which the browser loads or refuses to install
|
|
66
|
+
* without: background, pages, content scripts, icons, locale, rulesets.
|
|
67
|
+
* `web_accessible_resources` is not here — its entries are patterns, handled by
|
|
68
|
+
* the WAR check.
|
|
69
|
+
*/
|
|
70
|
+
export declare function manifestReferences(manifest: Json): string[];
|
|
71
|
+
export interface WarWildcard {
|
|
72
|
+
/** Index into the manifest's web_accessible_resources array. */
|
|
73
|
+
entry: number;
|
|
74
|
+
pattern: string;
|
|
75
|
+
}
|
|
76
|
+
export declare function warWildcards(manifest: Json): WarWildcard[];
|
|
77
|
+
/**
|
|
78
|
+
* Replace every wildcard in `web_accessible_resources` with the explicit list of
|
|
79
|
+
* packaged files it matches (as the browser would match it). Exact entries stay.
|
|
80
|
+
* manifest.json itself is never exposed. Returns human-readable change notes.
|
|
81
|
+
*/
|
|
82
|
+
export declare function expandWarWildcards(manifest: Json, packagedPaths: readonly string[]): string[];
|
|
83
|
+
/** `browser_specific_settings.gecko.id`, or the MV2 `applications.gecko.id` spelling. */
|
|
84
|
+
export declare function manifestGeckoId(manifest: Json): string | undefined;
|
|
85
|
+
export interface PackOptions {
|
|
86
|
+
browser: PackageBrowser;
|
|
87
|
+
/** Explicit Firefox add-on ID written into the packaged manifest. */
|
|
88
|
+
geckoId?: string;
|
|
89
|
+
/** Expand wildcard web_accessible_resources in the packaged manifest. */
|
|
90
|
+
fixWar?: boolean;
|
|
91
|
+
/** Bound Dojo project, used only to suggest its derived add-on ID in errors. */
|
|
92
|
+
projectId?: string;
|
|
93
|
+
}
|
|
94
|
+
export interface PackResult {
|
|
95
|
+
browser: PackageBrowser;
|
|
96
|
+
bytes: Uint8Array;
|
|
97
|
+
sha256: string;
|
|
98
|
+
entries: string[];
|
|
99
|
+
manifest: Json;
|
|
100
|
+
/** Rewrites applied to the PACKAGED manifest.json (never the source). */
|
|
101
|
+
changes: string[];
|
|
102
|
+
warnings: string[];
|
|
103
|
+
}
|
|
104
|
+
export declare function packExtension(source: readonly PackageFile[], options: PackOptions): PackResult;
|
|
105
|
+
/** `dyslexia-font-reading-ruler-chrome-0.1.0.zip`: browser in the name so the wrong upload is hard. */
|
|
106
|
+
export declare function packageFileName(name: string, browser: PackageBrowser, version: string): string;
|
|
107
|
+
/** Resolve `__MSG_key__` against the default locale, for the file name only. */
|
|
108
|
+
export declare function displayName(manifest: Json, files: readonly PackageFile[]): string;
|
|
109
|
+
export interface PackageEntry {
|
|
110
|
+
browser: PackageBrowser;
|
|
111
|
+
/** Zip file name, relative to the packages manifest. */
|
|
112
|
+
file: string;
|
|
113
|
+
sha256: string;
|
|
114
|
+
bytes: number;
|
|
115
|
+
entries: number;
|
|
116
|
+
name: string;
|
|
117
|
+
version: string;
|
|
118
|
+
/** hashMetadata(packaged manifest) — equals the digest `release prepare` records. */
|
|
119
|
+
manifestDigest: string;
|
|
120
|
+
geckoId?: string;
|
|
121
|
+
/** Source directory as given, relative to where the command ran. */
|
|
122
|
+
source: string;
|
|
123
|
+
changes: string[];
|
|
124
|
+
warnings: string[];
|
|
125
|
+
}
|
|
126
|
+
export interface PackagesManifest {
|
|
127
|
+
schemaVersion: typeof PACKAGES_MANIFEST_SCHEMA_VERSION;
|
|
128
|
+
tool: string;
|
|
129
|
+
packages: PackageEntry[];
|
|
130
|
+
}
|
|
131
|
+
export declare function isPackagesManifest(value: unknown): value is PackagesManifest;
|
|
132
|
+
/**
|
|
133
|
+
* `<artifact>` for `release prepare`, `test smoke` and `evidence ingest`: a zip,
|
|
134
|
+
* or the packages manifest `extenshi package` wrote (then `browser` picks the
|
|
135
|
+
* entry). The zip's bytes must still hash to what the manifest recorded — a
|
|
136
|
+
* package rebuilt or edited since then is refused rather than silently used.
|
|
137
|
+
*/
|
|
138
|
+
export declare function resolvePackagedArtifact(file: string, browser?: string): {
|
|
139
|
+
artifactPath: string;
|
|
140
|
+
entry?: PackageEntry;
|
|
141
|
+
};
|
|
142
|
+
/**
|
|
143
|
+
* Browsers a zip was packaged for, from the packages manifest next to it. Chrome
|
|
144
|
+
* and Edge built from one directory are byte-identical, so one zip can be both.
|
|
145
|
+
*/
|
|
146
|
+
export declare function packagedBrowsersFor(artifactPath: string, sha256: string): PackageBrowser[];
|
|
147
|
+
export interface RunPackageOptions {
|
|
148
|
+
/** Default build directory for every browser without its own. */
|
|
149
|
+
dir?: string;
|
|
150
|
+
dirs?: Partial<Record<PackageBrowser, string>>;
|
|
151
|
+
browsers: PackageBrowser[];
|
|
152
|
+
outDir: string;
|
|
153
|
+
exclude?: string[];
|
|
154
|
+
geckoId?: string;
|
|
155
|
+
fixWar?: boolean;
|
|
156
|
+
json?: boolean;
|
|
157
|
+
cliVersion: string;
|
|
158
|
+
cwd?: string;
|
|
159
|
+
}
|
|
160
|
+
export declare function parsePackageBrowsers(value: string | undefined): PackageBrowser[];
|
|
161
|
+
export declare function runPackage(options: RunPackageOptions): PackagesManifest;
|
|
162
|
+
export {};
|
|
163
|
+
//# sourceMappingURL=package.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"package.d.ts","sourceRoot":"","sources":["../src/package.ts"],"names":[],"mappings":"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"}
|
package/dist/package.js
ADDED
|
@@ -0,0 +1,500 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `extenshi package <dir> --browser chrome,edge,firefox` — one reproducible
|
|
3
|
+
* store package per browser from an unpacked build directory.
|
|
4
|
+
*
|
|
5
|
+
* REPRODUCIBLE means byte-identical for the same files on any machine, in any
|
|
6
|
+
* timezone, under any umask: Dojo release readiness binds every piece of
|
|
7
|
+
* evidence (tests, scan, installed smoke) to the package's SHA-256, so a CI
|
|
8
|
+
* build and a local build of one commit must agree to the byte. The archive is
|
|
9
|
+
* written by the SAME writer Dojo uses (`zip.ts`, a byte-identical copy of
|
|
10
|
+
* dojo/src/lib/tools/zip.ts) with the same choices as Dojo's extension archive:
|
|
11
|
+
* - entries sorted by path, manifest.json at the root, no directory entries;
|
|
12
|
+
* - every entry stamped 2000-01-01 00:00:00 (DOS fields carry no timezone, so
|
|
13
|
+
* the constant is built from LOCAL components — see REPRODUCIBLE_MTIME);
|
|
14
|
+
* - STORE (no compression): deflate output depends on the zlib build, which
|
|
15
|
+
* differs between Node releases, and reproducibility across machines is the
|
|
16
|
+
* point. Store uploads accept uncompressed archives;
|
|
17
|
+
* - "version made by" MS-DOS with zero external attributes, so no Unix mode
|
|
18
|
+
* bits, owners or extra fields — nothing machine-specific survives.
|
|
19
|
+
*
|
|
20
|
+
* The user's source directory is never modified. `--fix-war` and `--gecko-id`
|
|
21
|
+
* rewrite manifest.json in the PACKAGED copy only, and every rewrite is listed
|
|
22
|
+
* in the output (`changes`).
|
|
23
|
+
*/
|
|
24
|
+
import { createHash } from 'node:crypto';
|
|
25
|
+
import fs from 'node:fs';
|
|
26
|
+
import path from 'node:path';
|
|
27
|
+
import { hashMetadata } from '@extenshi/contracts';
|
|
28
|
+
import { InvalidInputError } from './expected-error.js';
|
|
29
|
+
import { isPlaceholderGeckoId, isValidGeckoId, PLACEHOLDER_GECKO_ID, projectGeckoId } from './gecko-id.js';
|
|
30
|
+
import { ZipBuilder } from './zip.js';
|
|
31
|
+
export const PACKAGE_BROWSERS = ['chrome', 'edge', 'firefox'];
|
|
32
|
+
/** Written next to the zips; `release prepare`, `test smoke`, `evidence ingest` read it directly. */
|
|
33
|
+
export const PACKAGES_MANIFEST_FILE = 'extenshi-packages.json';
|
|
34
|
+
export const PACKAGES_MANIFEST_SCHEMA_VERSION = 1;
|
|
35
|
+
/**
|
|
36
|
+
* Same stamp as Dojo's extension archive (dojo/src/lib/tools/extension-zip.ts):
|
|
37
|
+
* 2000-01-01 00:00:00 LOCAL. A UTC instant would render as different DOS fields
|
|
38
|
+
* per timezone; local components always render as the same fields. 2000 rather
|
|
39
|
+
* than the DOS epoch because local midnight 1980-01-01 in a positive-offset zone
|
|
40
|
+
* falls below the earliest representable date.
|
|
41
|
+
*
|
|
42
|
+
* Built per archive rather than once at module load: the writer renders it with
|
|
43
|
+
* the CURRENT zone, so a value captured under another TZ (a test, a process that
|
|
44
|
+
* changes process.env.TZ) would no longer be local midnight — and no longer the
|
|
45
|
+
* same bytes.
|
|
46
|
+
*/
|
|
47
|
+
const reproducibleMtime = () => new Date(2000, 0, 1, 0, 0, 0, 0);
|
|
48
|
+
/** Classic ZIP (no ZIP64) limits. */
|
|
49
|
+
const MAX_ENTRIES = 0xffff;
|
|
50
|
+
const MAX_TOTAL_BYTES = 1024 * 1024 * 1024;
|
|
51
|
+
/** Never part of a store package, whatever the build directory holds. */
|
|
52
|
+
const ALWAYS_SKIPPED_DIRS = new Set(['node_modules']);
|
|
53
|
+
// ── Globs ───────────────────────────────────────────────────────────────────
|
|
54
|
+
const escapeRe = (s) => s.replace(/[.+^${}()|[\]\\]/g, '\\$&');
|
|
55
|
+
/**
|
|
56
|
+
* `--exclude` pattern → matcher, gitignore-style: `*` stays inside one path
|
|
57
|
+
* segment, `**` crosses segments, a pattern without `/` matches a basename at any
|
|
58
|
+
* depth, a pattern with `/` is anchored at the package root, and a pattern naming
|
|
59
|
+
* a directory excludes everything under it.
|
|
60
|
+
*/
|
|
61
|
+
export function excludeMatcher(patterns) {
|
|
62
|
+
const compiled = patterns
|
|
63
|
+
.flatMap((p) => p.split(','))
|
|
64
|
+
// `dir/**` is `dir`: the suffix below already covers everything under a match.
|
|
65
|
+
.map((p) => p
|
|
66
|
+
.trim()
|
|
67
|
+
.replace(/^\.\//, '')
|
|
68
|
+
.replace(/\/+$/, '')
|
|
69
|
+
.replace(/\/\*\*$/, ''))
|
|
70
|
+
.filter(Boolean)
|
|
71
|
+
.map((pattern) => {
|
|
72
|
+
const source = pattern
|
|
73
|
+
.split('**')
|
|
74
|
+
.map((part) => part
|
|
75
|
+
.split('*')
|
|
76
|
+
.map((chunk) => escapeRe(chunk).replace(/\?/g, '[^/]'))
|
|
77
|
+
.join('[^/]*'))
|
|
78
|
+
.join('.*');
|
|
79
|
+
const anchored = pattern.includes('/');
|
|
80
|
+
return anchored ? new RegExp(`^${source}(?:/.*)?$`) : new RegExp(`(?:^|/)${source}(?:/.*)?$`);
|
|
81
|
+
});
|
|
82
|
+
return (relative) => compiled.some((re) => re.test(relative));
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* A `web_accessible_resources` entry as the BROWSER matches it: `*` matches any
|
|
86
|
+
* run of characters INCLUDING `/` (Chromium's MatchPattern and Firefox's
|
|
87
|
+
* MatchGlob both work this way), `?` one character. Leading `/` is ignored.
|
|
88
|
+
*/
|
|
89
|
+
export function warGlobToRegExp(glob) {
|
|
90
|
+
const body = glob
|
|
91
|
+
.replace(/^\/+/, '')
|
|
92
|
+
.split('*')
|
|
93
|
+
.map((chunk) => escapeRe(chunk).replace(/\?/g, '.'))
|
|
94
|
+
.join('.*');
|
|
95
|
+
return new RegExp(`^${body}$`);
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Read every file under `root`, sorted, skipping dot-entries (`.git`, `.github`,
|
|
99
|
+
* `.extenshi`, `.DS_Store`, …), `node_modules` and `skipDirs` (the output
|
|
100
|
+
* directory when it sits inside the source). Symlinks are refused: following one
|
|
101
|
+
* would pack bytes from outside the directory, and a link itself is not
|
|
102
|
+
* something every store unpacks the same way.
|
|
103
|
+
*/
|
|
104
|
+
export function collectFiles(root, options = {}) {
|
|
105
|
+
const stat = fs.statSync(root, { throwIfNoEntry: false });
|
|
106
|
+
if (!stat?.isDirectory())
|
|
107
|
+
throw new InvalidInputError(`Build directory not found: ${root}`);
|
|
108
|
+
const excluded = excludeMatcher(options.exclude ?? []);
|
|
109
|
+
const skip = new Set((options.skipDirs ?? []).map((dir) => path.resolve(dir)));
|
|
110
|
+
const files = [];
|
|
111
|
+
const left = [];
|
|
112
|
+
let total = 0;
|
|
113
|
+
const walk = (dir, prefix) => {
|
|
114
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true }).sort((a, b) => (a.name < b.name ? -1 : 1));
|
|
115
|
+
for (const entry of entries) {
|
|
116
|
+
if (entry.name.startsWith('.') || ALWAYS_SKIPPED_DIRS.has(entry.name))
|
|
117
|
+
continue;
|
|
118
|
+
const full = path.join(dir, entry.name);
|
|
119
|
+
const relative = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
120
|
+
if (skip.has(path.resolve(full)))
|
|
121
|
+
continue;
|
|
122
|
+
if (excluded(relative)) {
|
|
123
|
+
left.push(relative);
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
if (entry.isSymbolicLink())
|
|
127
|
+
throw new InvalidInputError(`${relative} is a symbolic link. Packages contain files only — copy the target in, or exclude it.`);
|
|
128
|
+
if (entry.isDirectory())
|
|
129
|
+
walk(full, relative);
|
|
130
|
+
else if (entry.isFile()) {
|
|
131
|
+
const bytes = fs.readFileSync(full);
|
|
132
|
+
total += bytes.byteLength;
|
|
133
|
+
if (total > MAX_TOTAL_BYTES)
|
|
134
|
+
throw new InvalidInputError('The build directory exceeds 1 GiB.');
|
|
135
|
+
files.push({ path: relative, bytes: new Uint8Array(bytes) });
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
walk(root, '');
|
|
140
|
+
if (files.length > MAX_ENTRIES)
|
|
141
|
+
throw new InvalidInputError(`Too many files for a ZIP (${files.length}).`);
|
|
142
|
+
return { files, excluded: left.sort() };
|
|
143
|
+
}
|
|
144
|
+
const asObject = (v) => v && typeof v === 'object' && !Array.isArray(v) ? v : undefined;
|
|
145
|
+
const asStrings = (v) => Array.isArray(v) ? v.filter((x) => typeof x === 'string') : typeof v === 'string' ? [v] : [];
|
|
146
|
+
const normalizeRef = (p) => p.trim().replace(/^\.?\/+/, '');
|
|
147
|
+
/**
|
|
148
|
+
* Every file the manifest names, which the browser loads or refuses to install
|
|
149
|
+
* without: background, pages, content scripts, icons, locale, rulesets.
|
|
150
|
+
* `web_accessible_resources` is not here — its entries are patterns, handled by
|
|
151
|
+
* the WAR check.
|
|
152
|
+
*/
|
|
153
|
+
export function manifestReferences(manifest) {
|
|
154
|
+
const refs = [];
|
|
155
|
+
const bg = asObject(manifest.background);
|
|
156
|
+
refs.push(...asStrings(bg?.service_worker), ...asStrings(bg?.scripts), ...asStrings(bg?.page));
|
|
157
|
+
for (const key of ['action', 'browser_action', 'page_action']) {
|
|
158
|
+
const action = asObject(manifest[key]);
|
|
159
|
+
refs.push(...asStrings(action?.default_popup));
|
|
160
|
+
const icon = action?.default_icon;
|
|
161
|
+
refs.push(...(typeof icon === 'string' ? [icon] : Object.values(asObject(icon) ?? {}).flatMap(asStrings)));
|
|
162
|
+
}
|
|
163
|
+
refs.push(...asStrings(manifest.options_page), ...asStrings(asObject(manifest.options_ui)?.page));
|
|
164
|
+
refs.push(...asStrings(asObject(manifest.side_panel)?.default_path));
|
|
165
|
+
const sidebar = asObject(manifest.sidebar_action);
|
|
166
|
+
refs.push(...asStrings(sidebar?.default_panel));
|
|
167
|
+
refs.push(...asStrings(manifest.devtools_page));
|
|
168
|
+
refs.push(...Object.values(asObject(manifest.chrome_url_overrides) ?? {}).flatMap(asStrings));
|
|
169
|
+
for (const cs of Array.isArray(manifest.content_scripts) ? manifest.content_scripts : [])
|
|
170
|
+
refs.push(...asStrings(asObject(cs)?.js), ...asStrings(asObject(cs)?.css));
|
|
171
|
+
refs.push(...Object.values(asObject(manifest.icons) ?? {}).flatMap(asStrings));
|
|
172
|
+
if (typeof manifest.default_locale === 'string')
|
|
173
|
+
refs.push(`_locales/${manifest.default_locale}/messages.json`);
|
|
174
|
+
const dnr = asObject(manifest.declarative_net_request);
|
|
175
|
+
for (const rule of Array.isArray(dnr?.rule_resources) ? dnr.rule_resources : [])
|
|
176
|
+
refs.push(...asStrings(asObject(rule)?.path));
|
|
177
|
+
refs.push(...asStrings(asObject(manifest.sandbox)?.pages));
|
|
178
|
+
return [
|
|
179
|
+
...new Set(refs.map(normalizeRef).filter((p) => p && !/^[a-z]+:/i.test(p) && !p.startsWith('__MSG_'))),
|
|
180
|
+
].sort();
|
|
181
|
+
}
|
|
182
|
+
export function warWildcards(manifest) {
|
|
183
|
+
const war = Array.isArray(manifest.web_accessible_resources) ? manifest.web_accessible_resources : [];
|
|
184
|
+
const out = [];
|
|
185
|
+
war.forEach((item, entry) => {
|
|
186
|
+
// MV2: plain strings. MV3: { resources: [...], matches|extension_ids }.
|
|
187
|
+
const patterns = typeof item === 'string' ? [item] : asStrings(asObject(item)?.resources);
|
|
188
|
+
for (const pattern of patterns)
|
|
189
|
+
if (pattern.includes('*'))
|
|
190
|
+
out.push({ entry, pattern });
|
|
191
|
+
});
|
|
192
|
+
return out;
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Replace every wildcard in `web_accessible_resources` with the explicit list of
|
|
196
|
+
* packaged files it matches (as the browser would match it). Exact entries stay.
|
|
197
|
+
* manifest.json itself is never exposed. Returns human-readable change notes.
|
|
198
|
+
*/
|
|
199
|
+
export function expandWarWildcards(manifest, packagedPaths) {
|
|
200
|
+
const changes = [];
|
|
201
|
+
const candidates = packagedPaths.filter((p) => p !== 'manifest.json');
|
|
202
|
+
const expand = (patterns) => {
|
|
203
|
+
const out = new Set();
|
|
204
|
+
for (const pattern of patterns) {
|
|
205
|
+
if (!pattern.includes('*')) {
|
|
206
|
+
out.add(pattern);
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
const re = warGlobToRegExp(pattern);
|
|
210
|
+
const matched = candidates.filter((p) => re.test(p));
|
|
211
|
+
changes.push(matched.length
|
|
212
|
+
? `web_accessible_resources: "${pattern}" → ${matched.length} explicit file${matched.length === 1 ? '' : 's'}`
|
|
213
|
+
: `web_accessible_resources: "${pattern}" matched no packaged file and was removed`);
|
|
214
|
+
for (const p of matched)
|
|
215
|
+
out.add(p);
|
|
216
|
+
}
|
|
217
|
+
return [...out].sort();
|
|
218
|
+
};
|
|
219
|
+
const war = manifest.web_accessible_resources;
|
|
220
|
+
if (!Array.isArray(war))
|
|
221
|
+
return changes;
|
|
222
|
+
if (war.every((item) => typeof item === 'string')) {
|
|
223
|
+
manifest.web_accessible_resources = expand(war);
|
|
224
|
+
return changes;
|
|
225
|
+
}
|
|
226
|
+
manifest.web_accessible_resources = war
|
|
227
|
+
.map((item) => {
|
|
228
|
+
const obj = asObject(item);
|
|
229
|
+
if (!obj || !Array.isArray(obj.resources))
|
|
230
|
+
return item;
|
|
231
|
+
return { ...obj, resources: expand(asStrings(obj.resources)) };
|
|
232
|
+
})
|
|
233
|
+
.filter((item) => !(asObject(item) &&
|
|
234
|
+
Array.isArray(asObject(item)?.resources) &&
|
|
235
|
+
asObject(item)?.resources.length === 0));
|
|
236
|
+
return changes;
|
|
237
|
+
}
|
|
238
|
+
/** `browser_specific_settings.gecko.id`, or the MV2 `applications.gecko.id` spelling. */
|
|
239
|
+
export function manifestGeckoId(manifest) {
|
|
240
|
+
for (const key of ['browser_specific_settings', 'applications']) {
|
|
241
|
+
const id = asObject(asObject(manifest[key])?.gecko)?.id;
|
|
242
|
+
if (typeof id === 'string' && id.trim())
|
|
243
|
+
return id.trim();
|
|
244
|
+
}
|
|
245
|
+
return undefined;
|
|
246
|
+
}
|
|
247
|
+
function geckoIdHelp(projectId) {
|
|
248
|
+
const example = projectId ? projectGeckoId(projectId) : '{xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}';
|
|
249
|
+
return (`Set browser_specific_settings.gecko.id in manifest.json (email-style "name@yourdomain" or a GUID like "${example}"), ` +
|
|
250
|
+
`or pass --gecko-id <id> to write it into the packaged copy. AMO makes the ID permanent at the first upload, so pick it once.` +
|
|
251
|
+
(projectId
|
|
252
|
+
? ` The bound Dojo project's default is ${example} (the ID Dojo's scaffold writes) — pass --gecko-id "${example}" to use it.`
|
|
253
|
+
: ''));
|
|
254
|
+
}
|
|
255
|
+
export function packExtension(source, options) {
|
|
256
|
+
const byPath = new Map(source.map((f) => [f.path, f]));
|
|
257
|
+
const seen = new Map();
|
|
258
|
+
for (const f of source) {
|
|
259
|
+
const folded = f.path.toLowerCase();
|
|
260
|
+
const other = seen.get(folded);
|
|
261
|
+
// Case-only twins unpack as ONE file on macOS/Windows, so which one the store
|
|
262
|
+
// or a reviewer sees would depend on their machine.
|
|
263
|
+
if (other)
|
|
264
|
+
throw new InvalidInputError(`${other} and ${f.path} differ only in letter case.`);
|
|
265
|
+
seen.set(folded, f.path);
|
|
266
|
+
}
|
|
267
|
+
const manifestFile = byPath.get('manifest.json');
|
|
268
|
+
if (!manifestFile)
|
|
269
|
+
throw new InvalidInputError('No manifest.json at the root of the build directory — point at the unpacked build.');
|
|
270
|
+
let manifest;
|
|
271
|
+
try {
|
|
272
|
+
manifest = JSON.parse(new TextDecoder().decode(manifestFile.bytes).replace(/^/, ''));
|
|
273
|
+
}
|
|
274
|
+
catch (error) {
|
|
275
|
+
throw new InvalidInputError(`manifest.json is not valid JSON: ${error.message}`);
|
|
276
|
+
}
|
|
277
|
+
if (!asObject(manifest))
|
|
278
|
+
throw new InvalidInputError('manifest.json must be a JSON object.');
|
|
279
|
+
const missing = manifestReferences(manifest).filter((ref) => !byPath.has(ref));
|
|
280
|
+
if (missing.length)
|
|
281
|
+
throw new InvalidInputError(`manifest.json names file${missing.length === 1 ? '' : 's'} the package does not contain: ${missing.join(', ')}. ` +
|
|
282
|
+
'Browsers refuse to load such a package. Add the files, or check --exclude.');
|
|
283
|
+
const changes = [];
|
|
284
|
+
const warnings = [];
|
|
285
|
+
const wildcards = warWildcards(manifest);
|
|
286
|
+
if (wildcards.length && options.fixWar) {
|
|
287
|
+
changes.push(...expandWarWildcards(manifest, source.map((f) => f.path)));
|
|
288
|
+
}
|
|
289
|
+
else if (wildcards.length) {
|
|
290
|
+
warnings.push(`web_accessible_resources uses wildcard${wildcards.length === 1 ? '' : 's'} (${wildcards.map((w) => `"${w.pattern}"`).join(', ')}) — ` +
|
|
291
|
+
'every matching file becomes readable by web pages, and reviewers ask why. Pass --fix-war to list the files explicitly in the packaged copy.');
|
|
292
|
+
}
|
|
293
|
+
if (options.browser === 'firefox') {
|
|
294
|
+
const existing = manifestGeckoId(manifest);
|
|
295
|
+
if (options.geckoId !== undefined) {
|
|
296
|
+
const id = options.geckoId.trim();
|
|
297
|
+
if (!isValidGeckoId(id) || isPlaceholderGeckoId(id))
|
|
298
|
+
throw new InvalidInputError(`--gecko-id "${id}" is ${isValidGeckoId(id) ? 'a placeholder' : 'not a valid add-on ID'}. ${geckoIdHelp(options.projectId)}`);
|
|
299
|
+
if (id !== existing) {
|
|
300
|
+
const settings = asObject(manifest.browser_specific_settings) ?? {};
|
|
301
|
+
manifest.browser_specific_settings = {
|
|
302
|
+
...settings,
|
|
303
|
+
gecko: { ...(asObject(settings.gecko) ?? {}), id },
|
|
304
|
+
};
|
|
305
|
+
changes.push(`browser_specific_settings.gecko.id set to ${id}${existing ? ` (was ${existing})` : ''}`);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
else if (!existing) {
|
|
309
|
+
throw new InvalidInputError(`The Firefox package has no add-on ID. ${geckoIdHelp(options.projectId)}`);
|
|
310
|
+
}
|
|
311
|
+
else if (isPlaceholderGeckoId(existing) || !isValidGeckoId(existing)) {
|
|
312
|
+
throw new InvalidInputError(`The Firefox add-on ID "${existing}" is ${existing === PLACEHOLDER_GECKO_ID || isPlaceholderGeckoId(existing) ? 'a placeholder' : 'not a valid add-on ID'} — an AMO upload would own it forever. ${geckoIdHelp(options.projectId)}`);
|
|
313
|
+
}
|
|
314
|
+
const bg = asObject(manifest.background);
|
|
315
|
+
if (manifest.manifest_version === 3 && bg?.service_worker && !bg.scripts && !bg.page)
|
|
316
|
+
throw new InvalidInputError('Firefox does not run MV3 background service workers and refuses this manifest ("background.service_worker is currently disabled"). ' +
|
|
317
|
+
'Add "scripts": ["<your background file>"] next to service_worker in the Firefox build — Chromium ignores it.');
|
|
318
|
+
}
|
|
319
|
+
const files = changes.length
|
|
320
|
+
? source.map((f) => f.path === 'manifest.json'
|
|
321
|
+
? { path: f.path, bytes: new TextEncoder().encode(`${JSON.stringify(manifest, null, 2)}\n`) }
|
|
322
|
+
: f)
|
|
323
|
+
: [...source];
|
|
324
|
+
files.sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
|
|
325
|
+
const zip = new ZipBuilder({ mtime: reproducibleMtime() });
|
|
326
|
+
for (const f of files)
|
|
327
|
+
zip.addFile(f.path, f.bytes);
|
|
328
|
+
const bytes = zip.toBytes();
|
|
329
|
+
return {
|
|
330
|
+
browser: options.browser,
|
|
331
|
+
bytes,
|
|
332
|
+
sha256: createHash('sha256').update(bytes).digest('hex'),
|
|
333
|
+
entries: files.map((f) => f.path),
|
|
334
|
+
manifest,
|
|
335
|
+
changes,
|
|
336
|
+
warnings,
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
/** `dyslexia-font-reading-ruler-chrome-0.1.0.zip`: browser in the name so the wrong upload is hard. */
|
|
340
|
+
export function packageFileName(name, browser, version) {
|
|
341
|
+
const slug = name
|
|
342
|
+
.toLowerCase()
|
|
343
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
344
|
+
.replace(/^-+|-+$/g, '')
|
|
345
|
+
.slice(0, 60) || 'extension';
|
|
346
|
+
const safeVersion = version.replace(/[^0-9A-Za-z.-]/g, '') || '0.0.0';
|
|
347
|
+
return `${slug}-${browser}-${safeVersion}.zip`;
|
|
348
|
+
}
|
|
349
|
+
/** Resolve `__MSG_key__` against the default locale, for the file name only. */
|
|
350
|
+
export function displayName(manifest, files) {
|
|
351
|
+
const raw = typeof manifest.name === 'string' ? manifest.name : '';
|
|
352
|
+
const token = /^__MSG_(\w+)__$/.exec(raw)?.[1];
|
|
353
|
+
if (!token || typeof manifest.default_locale !== 'string')
|
|
354
|
+
return raw;
|
|
355
|
+
const file = files.find((f) => f.path === `_locales/${manifest.default_locale}/messages.json`);
|
|
356
|
+
try {
|
|
357
|
+
const messages = JSON.parse(new TextDecoder().decode(file?.bytes).replace(/^/, ''));
|
|
358
|
+
const hit = Object.entries(messages).find(([key]) => key.toLowerCase() === token.toLowerCase())?.[1];
|
|
359
|
+
return typeof hit?.message === 'string' ? hit.message : raw;
|
|
360
|
+
}
|
|
361
|
+
catch {
|
|
362
|
+
return raw;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
export function isPackagesManifest(value) {
|
|
366
|
+
const obj = asObject(value);
|
|
367
|
+
return obj?.schemaVersion === PACKAGES_MANIFEST_SCHEMA_VERSION && Array.isArray(obj.packages);
|
|
368
|
+
}
|
|
369
|
+
const sha256File = (file) => createHash('sha256').update(fs.readFileSync(file)).digest('hex');
|
|
370
|
+
/**
|
|
371
|
+
* `<artifact>` for `release prepare`, `test smoke` and `evidence ingest`: a zip,
|
|
372
|
+
* or the packages manifest `extenshi package` wrote (then `browser` picks the
|
|
373
|
+
* entry). The zip's bytes must still hash to what the manifest recorded — a
|
|
374
|
+
* package rebuilt or edited since then is refused rather than silently used.
|
|
375
|
+
*/
|
|
376
|
+
export function resolvePackagedArtifact(file, browser) {
|
|
377
|
+
if (!file.toLowerCase().endsWith('.json'))
|
|
378
|
+
return { artifactPath: file };
|
|
379
|
+
let parsed;
|
|
380
|
+
try {
|
|
381
|
+
parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
382
|
+
}
|
|
383
|
+
catch (error) {
|
|
384
|
+
throw new InvalidInputError(`Could not read ${file}: ${error.message}`);
|
|
385
|
+
}
|
|
386
|
+
if (!isPackagesManifest(parsed))
|
|
387
|
+
throw new InvalidInputError(`${file} is not an ${PACKAGES_MANIFEST_FILE} written by extenshi package.`);
|
|
388
|
+
const matches = browser ? parsed.packages.filter((p) => p.browser === browser) : parsed.packages;
|
|
389
|
+
if (matches.length !== 1)
|
|
390
|
+
throw new InvalidInputError(browser
|
|
391
|
+
? `${file} has no ${browser} package (it lists: ${parsed.packages.map((p) => p.browser).join(', ') || 'none'}).`
|
|
392
|
+
: `${file} lists ${parsed.packages.length} packages — pass --browser to pick one.`);
|
|
393
|
+
const entry = matches[0];
|
|
394
|
+
const artifactPath = path.resolve(path.dirname(file), entry.file);
|
|
395
|
+
if (!fs.existsSync(artifactPath))
|
|
396
|
+
throw new InvalidInputError(`Package not found: ${artifactPath}`);
|
|
397
|
+
if (sha256File(artifactPath) !== entry.sha256)
|
|
398
|
+
throw new InvalidInputError(`${entry.file} no longer matches the sha256 in ${path.basename(file)} — it changed after extenshi package. Re-run extenshi package.`);
|
|
399
|
+
return { artifactPath, entry };
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* Browsers a zip was packaged for, from the packages manifest next to it. Chrome
|
|
403
|
+
* and Edge built from one directory are byte-identical, so one zip can be both.
|
|
404
|
+
*/
|
|
405
|
+
export function packagedBrowsersFor(artifactPath, sha256) {
|
|
406
|
+
const manifestPath = path.join(path.dirname(artifactPath), PACKAGES_MANIFEST_FILE);
|
|
407
|
+
try {
|
|
408
|
+
const parsed = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
409
|
+
if (!isPackagesManifest(parsed))
|
|
410
|
+
return [];
|
|
411
|
+
return parsed.packages.filter((p) => p.sha256 === sha256).map((p) => p.browser);
|
|
412
|
+
}
|
|
413
|
+
catch {
|
|
414
|
+
return [];
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
export function parsePackageBrowsers(value) {
|
|
418
|
+
const list = (value ?? 'chrome')
|
|
419
|
+
.split(',')
|
|
420
|
+
.map((s) => s.trim().toLowerCase())
|
|
421
|
+
.filter(Boolean)
|
|
422
|
+
.map((s) => (s === 'chromium' ? 'chrome' : s));
|
|
423
|
+
const bad = list.filter((s) => !PACKAGE_BROWSERS.includes(s));
|
|
424
|
+
if (bad.length || !list.length)
|
|
425
|
+
throw new InvalidInputError(`--browser takes a comma-separated list of: ${PACKAGE_BROWSERS.join(', ')}`);
|
|
426
|
+
return [...new Set(list)];
|
|
427
|
+
}
|
|
428
|
+
function boundProjectId(cwd) {
|
|
429
|
+
try {
|
|
430
|
+
const binding = JSON.parse(fs.readFileSync(path.join(cwd, '.extenshi/project.json'), 'utf8'));
|
|
431
|
+
return typeof binding.projectId === 'string' ? binding.projectId : undefined;
|
|
432
|
+
}
|
|
433
|
+
catch {
|
|
434
|
+
return undefined;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
export function runPackage(options) {
|
|
438
|
+
const cwd = options.cwd ?? process.cwd();
|
|
439
|
+
const outDir = path.resolve(cwd, options.outDir);
|
|
440
|
+
const projectId = boundProjectId(cwd);
|
|
441
|
+
const results = [];
|
|
442
|
+
for (const browser of options.browsers) {
|
|
443
|
+
const dir = options.dirs?.[browser] ?? options.dir;
|
|
444
|
+
if (!dir)
|
|
445
|
+
throw new InvalidInputError(`No build directory for ${browser}: pass <dir> or --${browser}-dir <dir>.`);
|
|
446
|
+
const root = path.resolve(cwd, dir);
|
|
447
|
+
const { files } = collectFiles(root, { exclude: options.exclude, skipDirs: [outDir] });
|
|
448
|
+
const packed = packExtension(files, {
|
|
449
|
+
browser,
|
|
450
|
+
geckoId: options.geckoId,
|
|
451
|
+
fixWar: options.fixWar,
|
|
452
|
+
projectId,
|
|
453
|
+
});
|
|
454
|
+
const version = typeof packed.manifest.version === 'string' ? packed.manifest.version : '0.0.0';
|
|
455
|
+
const name = displayName(packed.manifest, files);
|
|
456
|
+
results.push({
|
|
457
|
+
bytes: packed.bytes,
|
|
458
|
+
entry: {
|
|
459
|
+
browser,
|
|
460
|
+
file: packageFileName(name || path.basename(root), browser, version),
|
|
461
|
+
sha256: packed.sha256,
|
|
462
|
+
bytes: packed.bytes.byteLength,
|
|
463
|
+
entries: packed.entries.length,
|
|
464
|
+
name,
|
|
465
|
+
version,
|
|
466
|
+
manifestDigest: hashMetadata(packed.manifest),
|
|
467
|
+
...(browser === 'firefox' ? { geckoId: manifestGeckoId(packed.manifest) } : {}),
|
|
468
|
+
source: path.relative(cwd, root).split(path.sep).join('/') || '.',
|
|
469
|
+
changes: packed.changes,
|
|
470
|
+
warnings: packed.warnings,
|
|
471
|
+
},
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
475
|
+
for (const { entry, bytes } of results)
|
|
476
|
+
fs.writeFileSync(path.join(outDir, entry.file), bytes);
|
|
477
|
+
const manifest = {
|
|
478
|
+
schemaVersion: PACKAGES_MANIFEST_SCHEMA_VERSION,
|
|
479
|
+
tool: `extenshi-cli/${options.cliVersion}`,
|
|
480
|
+
packages: results.map((r) => r.entry),
|
|
481
|
+
};
|
|
482
|
+
fs.writeFileSync(path.join(outDir, PACKAGES_MANIFEST_FILE), `${JSON.stringify(manifest, null, 2)}\n`);
|
|
483
|
+
if (options.json) {
|
|
484
|
+
process.stdout.write(`${JSON.stringify({ ...manifest, outDir }, null, 2)}\n`);
|
|
485
|
+
}
|
|
486
|
+
else {
|
|
487
|
+
// sha256sum format, so `shasum -a 256 -c` can check the files later.
|
|
488
|
+
for (const { entry } of results)
|
|
489
|
+
process.stdout.write(`${entry.sha256} ${path.join(outDir, entry.file)}\n`);
|
|
490
|
+
for (const { entry } of results) {
|
|
491
|
+
for (const change of entry.changes)
|
|
492
|
+
process.stderr.write(`${entry.browser}: changed in the package: ${change}\n`);
|
|
493
|
+
for (const warning of entry.warnings)
|
|
494
|
+
process.stderr.write(`${entry.browser}: warning: ${warning}\n`);
|
|
495
|
+
}
|
|
496
|
+
process.stderr.write(`Wrote ${path.join(outDir, PACKAGES_MANIFEST_FILE)}\n`);
|
|
497
|
+
}
|
|
498
|
+
return manifest;
|
|
499
|
+
}
|
|
500
|
+
//# sourceMappingURL=package.js.map
|