-
Notifications
You must be signed in to change notification settings - Fork 20
Expand file tree
/
Copy pathpackage.mjs
More file actions
104 lines (91 loc) · 4.94 KB
/
Copy pathpackage.mjs
File metadata and controls
104 lines (91 loc) · 4.94 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
import { build } from 'esbuild';
import { readFileSync, existsSync } from 'node:fs';
import { rm, mkdir, writeFile } from 'node:fs/promises';
import { join } from 'node:path';
import { createHash } from 'node:crypto';
import { zipStore } from './scripts/zip-store.mjs';
import { configUiMember, collectMembers } from './scripts/entry-path.mjs';
const plugin = process.argv[2];
if (!plugin) {
console.error('Usage: node package.mjs <plugin-dir>');
process.exit(1);
}
const root = process.cwd();
const dir = join(root, plugin);
const fail = (msg) => {
console.error(`✗ ${plugin}: ${msg}`);
process.exit(1);
};
// ── Validate manifest ──────────────────────────────────────────────────────
if (!existsSync(join(dir, 'manifest.json'))) fail('no manifest.json');
const manifest = JSON.parse(readFileSync(join(dir, 'manifest.json'), 'utf8'));
const missing = ['id', 'name', 'version', 'type', 'main'].filter((f) => !manifest[f]);
if (missing.length) fail(`manifest.json missing required field(s): ${missing.join(', ')}`);
if (manifest.type !== 'extension') fail(`type must be "extension" to be installable (got "${manifest.type}")`);
// Warn on unsupported i18n locale codes
const SUPPORTED_LOCALES = ['en', 'es', 'fr', 'it', 'ar', 'he', 'te', 'zh-CN', 'zh-HK'];
if (manifest.i18n && typeof manifest.i18n === 'object') {
for (const code of Object.keys(manifest.i18n)) {
if (!SUPPORTED_LOCALES.includes(code)) console.warn(`⚠ ${plugin}: i18n locale "${code}" is not a dashboard-supported code`);
}
}
// version must match the top released CHANGELOG heading (single source of release truth)
if (!existsSync(join(dir, 'CHANGELOG.md'))) fail('missing CHANGELOG.md');
const top = readFileSync(join(dir, 'CHANGELOG.md'), 'utf8').match(/^##\s*\[(\d+\.\d+\.\d+)\]\s*[—–-]\s*\d{4}-\d{2}-\d{2}/m);
if (!top) fail('CHANGELOG.md has no released "## [x.y.z] — YYYY-MM-DD" heading');
if (top[1] !== manifest.version) fail(`version drift: manifest is ${manifest.version} but CHANGELOG top is ${top[1]}`);
// ── Build ───────────────────────────────────────────────────────────────────
await mkdir(join(dir, 'dist'), { recursive: true });
// A package.json in dist/ pins CommonJS so Node loads the CJS bundle even when the repo root is ESM.
await writeFile(join(dir, 'dist', 'package.json'), JSON.stringify({ type: 'commonjs' }));
await build({
entryPoints: [join(dir, 'index.ts')],
bundle: true,
platform: 'node',
format: 'cjs',
target: 'node22',
outfile: join(dir, 'dist', 'index.js'),
// Bake the manifest version into the bundle (single source of truth = manifest.json) so the plugin
// can report its own version at runtime — the sandbox does not pass `manifest` into ctx.
define: { __PLUGIN_VERSION__: JSON.stringify(manifest.version) },
});
// ── Package ───────────────────────────────────────────────────────────────────
const zipName = `${plugin}.zip`;
const zipPath = join(root, zipName);
await rm(zipPath, { force: true });
const entries = ['manifest.json', 'dist/index.js', 'dist/package.json'];
// A configUi plugin ships a static editor bundle the host serves in a sandboxed iframe — include it.
if (manifest.configUi?.entry) {
const ui = manifest.configUi.entry;
let member;
try {
member = configUiMember(ui);
} catch (e) {
fail(e.message);
}
if (!existsSync(join(dir, ui))) fail(`configUi.entry "${ui}" not found`);
entries.push(member);
}
let result;
try {
result = collectMembers(dir, entries);
} catch (e) {
fail(e.message);
}
// The archive members are chosen here, not read from the manifest, so a `main` pointing anywhere else
// produces a package every gate in this repo accepts and the host refuses: it rejects an archive whose
// `main` is not among its entries, at install, after the release is already tagged and published.
const mainInZip = join('.', manifest.main).split('\\').join('/');
if (!result.some((f) => f.name === mainInZip)) {
fail(`manifest main "${manifest.main}" is not in the package (members: ${result.map((f) => f.name).join(', ')})`);
}
// ── Report size + sha256 (release artifacts — surfaced here and in the GitHub Release) ──
// Size is checked before the write, not after: an oversized archive that fails the limit used to be
// left behind on disk anyway.
const zip = zipStore(result);
const kb = (zip.length / 1024).toFixed(1);
if (zip.length > 5 * 1024 * 1024) fail(`package is ${kb} KB, over the 5 MB install limit`);
await writeFile(zipPath, zip);
const sha256 = createHash('sha256').update(zip).digest('hex');
console.log(`✓ Packaged ${plugin} v${manifest.version} → ${zipName} (${kb} KB)`);
console.log(` sha256: ${sha256}`);