@rynt/extension-build 0.9.0 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/extension-dev-bridge.d.ts +17 -0
- package/dist/extension-dev-bridge.d.ts.map +1 -0
- package/dist/extension-dev-bridge.js +107 -0
- package/dist/extension-icon.d.ts +22 -0
- package/dist/extension-icon.d.ts.map +1 -0
- package/dist/extension-icon.js +93 -0
- package/dist/extension-manifest.d.ts +7 -0
- package/dist/extension-manifest.d.ts.map +1 -0
- package/dist/extension-manifest.js +129 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +223 -0
- package/package.json +11 -2
- package/scripts/write-host-shims.mjs +0 -68
- package/src/extension-dev-bridge.ts +0 -132
- package/src/extension-icon.ts +0 -120
- package/src/extension-manifest.ts +0 -187
- package/src/index.ts +0 -320
- package/tsconfig.json +0 -16
package/package.json
CHANGED
|
@@ -1,14 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rynt/extension-build",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.1",
|
|
4
4
|
"description": "Vite preset, host-shims and .ryntextension packer for Rynt Launcher extensions",
|
|
5
5
|
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
6
8
|
"exports": {
|
|
7
9
|
".": {
|
|
8
10
|
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js",
|
|
9
12
|
"default": "./dist/index.js"
|
|
10
13
|
}
|
|
11
14
|
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist",
|
|
17
|
+
"shims",
|
|
18
|
+
"bin"
|
|
19
|
+
],
|
|
12
20
|
"bin": {
|
|
13
21
|
"rynt-extension-pack": "./bin/pack.mjs"
|
|
14
22
|
},
|
|
@@ -40,7 +48,8 @@
|
|
|
40
48
|
"scripts": {
|
|
41
49
|
"gen-host-shims": "node ./scripts/write-host-shims.mjs",
|
|
42
50
|
"build": "tsc -p tsconfig.json",
|
|
43
|
-
"prepare": "pnpm run build"
|
|
51
|
+
"prepare": "pnpm run build",
|
|
52
|
+
"prepublishOnly": "pnpm run build"
|
|
44
53
|
},
|
|
45
54
|
"publishConfig": {
|
|
46
55
|
"access": "public"
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Генерирует `shims/*.host-shim.js`: реэкспорт с `globalThis.__RYNT_EXTENSION_HOST_MODULES__`.
|
|
3
|
-
*
|
|
4
|
-
* Запуск: `pnpm --filter @rynt/extension-build run gen-host-shims`
|
|
5
|
-
*/
|
|
6
|
-
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
7
|
-
import { dirname, join } from 'node:path';
|
|
8
|
-
import { fileURLToPath } from 'node:url';
|
|
9
|
-
|
|
10
|
-
import * as tiptapCore from '@tiptap/core';
|
|
11
|
-
import * as vue from 'vue';
|
|
12
|
-
import * as vueRouter from 'vue-router';
|
|
13
|
-
|
|
14
|
-
/** @type {readonly string[]} */
|
|
15
|
-
export const RYNT_EXTENSION_HOST_TIPTAP_SPECIFIERS = ['@tiptap/core'];
|
|
16
|
-
|
|
17
|
-
const root = dirname(fileURLToPath(import.meta.url));
|
|
18
|
-
const shimsDir = join(root, '..', 'shims');
|
|
19
|
-
mkdirSync(shimsDir, { recursive: true });
|
|
20
|
-
|
|
21
|
-
const banner = `/* eslint-disable -- generated by write-host-shims.mjs */\n`;
|
|
22
|
-
|
|
23
|
-
const header = `${banner}
|
|
24
|
-
const HOST = '__RYNT_EXTENSION_HOST_MODULES__';
|
|
25
|
-
|
|
26
|
-
function registry() {
|
|
27
|
-
const r = globalThis[HOST];
|
|
28
|
-
if (!r) {
|
|
29
|
-
throw new Error(
|
|
30
|
-
'[rynt/extension-build] Missing globalThis.' +
|
|
31
|
-
HOST +
|
|
32
|
-
' — call registerRyntExtensionHostModules() in the launcher before loading extensions.',
|
|
33
|
-
);
|
|
34
|
-
}
|
|
35
|
-
return r;
|
|
36
|
-
}
|
|
37
|
-
`;
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* @param {string} fileBase
|
|
41
|
-
* @param {string} slot
|
|
42
|
-
* @param {Record<string, unknown>} mod
|
|
43
|
-
*/
|
|
44
|
-
function writeNamespaceShim(fileBase, slot, mod) {
|
|
45
|
-
const keys = Object.keys(mod).filter(
|
|
46
|
-
(k) => k !== '__esModule' && k !== 'default',
|
|
47
|
-
);
|
|
48
|
-
const lines = [
|
|
49
|
-
header,
|
|
50
|
-
`const $ = registry()['${slot}'];`,
|
|
51
|
-
`if (!$) {`,
|
|
52
|
-
` throw new Error('[rynt/extension-build] Host did not register module ${slot}');`,
|
|
53
|
-
`}`,
|
|
54
|
-
'',
|
|
55
|
-
...keys.map((k) => `export const ${k} = $.${k};`),
|
|
56
|
-
'',
|
|
57
|
-
];
|
|
58
|
-
writeFileSync(join(shimsDir, `${fileBase}.host-shim.js`), lines.join('\n'));
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
writeNamespaceShim('vue', 'vue', vue);
|
|
62
|
-
writeNamespaceShim('vue-router', 'vue-router', vueRouter);
|
|
63
|
-
writeNamespaceShim('tiptap-core', '@tiptap/core', tiptapCore);
|
|
64
|
-
|
|
65
|
-
console.info(
|
|
66
|
-
'[write-host-shims] wrote vue, vue-router, @tiptap/core →',
|
|
67
|
-
shimsDir,
|
|
68
|
-
);
|
|
@@ -1,132 +0,0 @@
|
|
|
1
|
-
import path from 'node:path';
|
|
2
|
-
import { readFileSync } from 'node:fs';
|
|
3
|
-
|
|
4
|
-
import type { Plugin } from 'vite';
|
|
5
|
-
|
|
6
|
-
import { mergeExtensionManifestForDist } from './extension-manifest.js';
|
|
7
|
-
|
|
8
|
-
/** URL dev-hub лаунчера по умолчанию (см. extension-dev-hub.ts). */
|
|
9
|
-
export const RYNT_LAUNCHER_DEV_HUB_DEFAULT = 'http://127.0.0.1:39217';
|
|
10
|
-
|
|
11
|
-
function devHubBaseUrl(): string {
|
|
12
|
-
return (
|
|
13
|
-
process.env.RYNT_LAUNCHER_DEV_HUB?.trim().replace(/\/+$/u, '') ||
|
|
14
|
-
RYNT_LAUNCHER_DEV_HUB_DEFAULT
|
|
15
|
-
);
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function isDevBridgeActive(): boolean {
|
|
19
|
-
if (process.env.RYNT_DEV_BRIDGE === '0') return false;
|
|
20
|
-
return process.env.RYNT_DEV === '1' || process.env.RYNT_DEV === 'true';
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
async function postJson(url: string, body: Record<string, unknown>): Promise<void> {
|
|
24
|
-
const res = await fetch(url, {
|
|
25
|
-
method: 'POST',
|
|
26
|
-
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
|
27
|
-
body: JSON.stringify(body),
|
|
28
|
-
});
|
|
29
|
-
if (!res.ok) {
|
|
30
|
-
const text = await res.text().catch(() => '');
|
|
31
|
-
throw new Error(`HTTP ${res.status}${text ? `: ${text}` : ''}`);
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function readManifestFromDist(distDir: string): Record<string, unknown> | null {
|
|
36
|
-
try {
|
|
37
|
-
const manifestPath = path.join(distDir, 'manifest.json');
|
|
38
|
-
const raw = readFileSync(manifestPath, 'utf-8');
|
|
39
|
-
const parsed: unknown = JSON.parse(raw);
|
|
40
|
-
if (parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
41
|
-
return parsed as Record<string, unknown>;
|
|
42
|
-
}
|
|
43
|
-
} catch {
|
|
44
|
-
/* ignore */
|
|
45
|
-
}
|
|
46
|
-
return null;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
let unregisterHookInstalled = false;
|
|
50
|
-
let lastRegisteredId: string | null = null;
|
|
51
|
-
|
|
52
|
-
function installUnregisterHook(extensionId: string): void {
|
|
53
|
-
lastRegisteredId = extensionId;
|
|
54
|
-
if (unregisterHookInstalled) return;
|
|
55
|
-
unregisterHookInstalled = true;
|
|
56
|
-
|
|
57
|
-
const unregister = () => {
|
|
58
|
-
if (!lastRegisteredId) return;
|
|
59
|
-
const id = lastRegisteredId;
|
|
60
|
-
void postJson(`${devHubBaseUrl()}/api/v1/dev/extensions/unregister`, { id }).catch(
|
|
61
|
-
() => {
|
|
62
|
-
/* hub может быть уже остановлен */
|
|
63
|
-
},
|
|
64
|
-
);
|
|
65
|
-
};
|
|
66
|
-
|
|
67
|
-
process.once('SIGINT', () => {
|
|
68
|
-
unregister();
|
|
69
|
-
process.exit(0);
|
|
70
|
-
});
|
|
71
|
-
process.once('SIGTERM', unregister);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* При `RYNT_DEV=1` + `vite build --watch` регистрирует dist/ в dev-hub лаунчера
|
|
76
|
-
* после каждой сборки и снимает регистрацию при завершении процесса.
|
|
77
|
-
*
|
|
78
|
-
* Должен быть объявлен **до** `ryntExtensionEmitDistManifestPlugin`: Rollup вызывает
|
|
79
|
-
* `closeBundle` в обратном порядке, manifest пишется первым, регистрация — второй.
|
|
80
|
-
*/
|
|
81
|
-
export function ryntExtensionDevBridgePlugin(options: {
|
|
82
|
-
root: string;
|
|
83
|
-
manifestFromVite?: Record<string, unknown>;
|
|
84
|
-
}): Plugin {
|
|
85
|
-
const distDir = path.join(options.root, 'dist');
|
|
86
|
-
let watchMode = false;
|
|
87
|
-
|
|
88
|
-
return {
|
|
89
|
-
name: 'rynt-extension-dev-bridge',
|
|
90
|
-
apply: 'build',
|
|
91
|
-
|
|
92
|
-
config(config) {
|
|
93
|
-
watchMode = Boolean(config.build?.watch);
|
|
94
|
-
},
|
|
95
|
-
|
|
96
|
-
async closeBundle() {
|
|
97
|
-
if (!isDevBridgeActive() && !watchMode) return;
|
|
98
|
-
|
|
99
|
-
const manifest =
|
|
100
|
-
readManifestFromDist(distDir) ??
|
|
101
|
-
(options.manifestFromVite
|
|
102
|
-
? mergeExtensionManifestForDist(options.root, options.manifestFromVite)
|
|
103
|
-
: null);
|
|
104
|
-
if (!manifest) {
|
|
105
|
-
console.warn(
|
|
106
|
-
'[rynt/extension-build] dev-bridge: dist/manifest.json не найден, пропуск',
|
|
107
|
-
);
|
|
108
|
-
return;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
const rootDir = path.resolve(distDir);
|
|
112
|
-
const hub = devHubBaseUrl();
|
|
113
|
-
|
|
114
|
-
try {
|
|
115
|
-
await postJson(`${hub}/api/v1/dev/extensions/register`, {
|
|
116
|
-
manifest,
|
|
117
|
-
rootDir,
|
|
118
|
-
});
|
|
119
|
-
const id = typeof manifest.id === 'string' ? manifest.id : '';
|
|
120
|
-
if (id) installUnregisterHook(id);
|
|
121
|
-
console.info(`[rynt/extension-build] dev-bridge: зарегистрировано ${id} → ${hub}`);
|
|
122
|
-
} catch (e) {
|
|
123
|
-
const msg = e instanceof Error ? e.message : String(e);
|
|
124
|
-
console.warn(
|
|
125
|
-
`[rynt/extension-build] dev-bridge: не удалось связаться с лаунчером (${hub}): ${msg}`,
|
|
126
|
-
);
|
|
127
|
-
}
|
|
128
|
-
},
|
|
129
|
-
};
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
export { isDevBridgeActive };
|
package/src/extension-icon.ts
DELETED
|
@@ -1,120 +0,0 @@
|
|
|
1
|
-
import { copyFileSync, existsSync, mkdirSync } from 'node:fs';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
|
|
4
|
-
import { Jimp } from 'jimp';
|
|
5
|
-
|
|
6
|
-
/** Максимальная сторона иконки в px после сборки. */
|
|
7
|
-
export const EXTENSION_ICON_MAX_SIZE = 512;
|
|
8
|
-
|
|
9
|
-
/** Имя файла иконки в `dist/` (растровая). */
|
|
10
|
-
export const EXTENSION_ICON_DIST_PNG = 'icon.png';
|
|
11
|
-
|
|
12
|
-
const RASTER_EXTENSIONS = new Set([
|
|
13
|
-
'.png',
|
|
14
|
-
'.jpg',
|
|
15
|
-
'.jpeg',
|
|
16
|
-
'.webp',
|
|
17
|
-
'.bmp',
|
|
18
|
-
'.gif',
|
|
19
|
-
'.tiff',
|
|
20
|
-
'.tif',
|
|
21
|
-
]);
|
|
22
|
-
|
|
23
|
-
export function isRemoteOrDataIcon(value: string): boolean {
|
|
24
|
-
return /^(https?:|data:)/i.test(value.trim());
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export function isBundledExtensionIconRef(value: string): boolean {
|
|
28
|
-
const normalized = value.trim().replace(/\\/g, '/');
|
|
29
|
-
if (normalized.includes('/')) return false;
|
|
30
|
-
const base = normalized.toLowerCase();
|
|
31
|
-
return (
|
|
32
|
-
base === EXTENSION_ICON_DIST_PNG
|
|
33
|
-
|| base === 'icon.jpg'
|
|
34
|
-
|| base === 'icon.jpeg'
|
|
35
|
-
|| base === 'icon.webp'
|
|
36
|
-
|| base === 'icon.svg'
|
|
37
|
-
);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/** Локальный путь к исходнику иконки (относительно корня расширения). */
|
|
41
|
-
export function isLocalIconSource(value: string): boolean {
|
|
42
|
-
const trimmed = value.trim();
|
|
43
|
-
if (!trimmed) return false;
|
|
44
|
-
if (isRemoteOrDataIcon(trimmed)) return false;
|
|
45
|
-
if (isBundledExtensionIconRef(trimmed)) return false;
|
|
46
|
-
return true;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Берёт исходник по пути из vite-манифеста, кладёт в `dist/` и возвращает относительное имя
|
|
51
|
-
* для `manifest.icon` (например `icon.png`).
|
|
52
|
-
*
|
|
53
|
-
* Растровые форматы масштабируются до {@link EXTENSION_ICON_MAX_SIZE}px по длинной стороне.
|
|
54
|
-
* SVG копируется как есть (вектор не растеризуем — без native/wasm deps).
|
|
55
|
-
*/
|
|
56
|
-
export async function processExtensionIconForDist(
|
|
57
|
-
root: string,
|
|
58
|
-
iconSource: string,
|
|
59
|
-
distDir: string,
|
|
60
|
-
): Promise<string> {
|
|
61
|
-
const trimmed = iconSource.trim();
|
|
62
|
-
if (!isLocalIconSource(trimmed)) {
|
|
63
|
-
throw new Error(
|
|
64
|
-
`[rynt/extension-build] icon «${iconSource}» — не локальный путь к файлу`,
|
|
65
|
-
);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
const absSource = path.resolve(root, trimmed);
|
|
69
|
-
if (!existsSync(absSource)) {
|
|
70
|
-
throw new Error(`[rynt/extension-build] icon file not found: ${absSource}`);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
mkdirSync(distDir, { recursive: true });
|
|
74
|
-
const ext = path.extname(absSource).toLowerCase();
|
|
75
|
-
|
|
76
|
-
if (ext === '.svg') {
|
|
77
|
-
const destName = 'icon.svg';
|
|
78
|
-
copyFileSync(absSource, path.join(distDir, destName));
|
|
79
|
-
return destName;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
if (!RASTER_EXTENSIONS.has(ext)) {
|
|
83
|
-
throw new Error(
|
|
84
|
-
`[rynt/extension-build] unsupported icon format «${ext}» (${absSource}); ` +
|
|
85
|
-
'use PNG, JPEG, WebP, GIF, BMP, TIFF or SVG',
|
|
86
|
-
);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
const image = await Jimp.read(absSource);
|
|
90
|
-
if (
|
|
91
|
-
image.width > EXTENSION_ICON_MAX_SIZE
|
|
92
|
-
|| image.height > EXTENSION_ICON_MAX_SIZE
|
|
93
|
-
) {
|
|
94
|
-
image.scaleToFit({ w: EXTENSION_ICON_MAX_SIZE, h: EXTENSION_ICON_MAX_SIZE });
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
const destPath = path.join(distDir, EXTENSION_ICON_DIST_PNG);
|
|
98
|
-
await image.write(destPath as `${string}.${string}`);
|
|
99
|
-
return EXTENSION_ICON_DIST_PNG;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* Если в манифесте указан локальный путь — обрабатывает иконку и подменяет поле на имя файла в dist.
|
|
104
|
-
* URL / data: URI остаются без изменений.
|
|
105
|
-
*/
|
|
106
|
-
export async function applyExtensionIconToManifest(
|
|
107
|
-
root: string,
|
|
108
|
-
manifest: Record<string, unknown>,
|
|
109
|
-
): Promise<void> {
|
|
110
|
-
const raw = manifest.icon;
|
|
111
|
-
if (typeof raw !== 'string' || !raw.trim()) return;
|
|
112
|
-
|
|
113
|
-
if (isRemoteOrDataIcon(raw) || isBundledExtensionIconRef(raw)) {
|
|
114
|
-
return;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
const distDir = path.join(root, 'dist');
|
|
118
|
-
const distIcon = await processExtensionIconForDist(root, raw, distDir);
|
|
119
|
-
manifest.icon = distIcon;
|
|
120
|
-
}
|
|
@@ -1,187 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
existsSync,
|
|
3
|
-
mkdirSync,
|
|
4
|
-
readFileSync,
|
|
5
|
-
writeFileSync,
|
|
6
|
-
} from 'node:fs';
|
|
7
|
-
import { createRequire } from 'node:module';
|
|
8
|
-
import path from 'node:path';
|
|
9
|
-
|
|
10
|
-
import type { Plugin } from 'vite';
|
|
11
|
-
|
|
12
|
-
import { applyExtensionIconToManifest } from './extension-icon.js';
|
|
13
|
-
|
|
14
|
-
const require = createRequire(import.meta.url);
|
|
15
|
-
|
|
16
|
-
/** Валидация манифеста через SDK (devDependency extension-build, не бандлится в расширение). */
|
|
17
|
-
async function loadManifestValidators(): Promise<{
|
|
18
|
-
normalizeExtensionApiInManifest: (
|
|
19
|
-
manifest: { extensionApi?: string; id?: string },
|
|
20
|
-
options?: { extensionId?: string },
|
|
21
|
-
) => string | null;
|
|
22
|
-
applyValidatedRyntContributesToManifest: (manifest: {
|
|
23
|
-
id: string;
|
|
24
|
-
contributes?: Record<string, unknown>;
|
|
25
|
-
}) => string | null;
|
|
26
|
-
}> {
|
|
27
|
-
const { createJiti } = await import('jiti');
|
|
28
|
-
const jiti = createJiti(import.meta.url, { interopDefault: true });
|
|
29
|
-
const extEntry = require.resolve('@rynt/sdk/extension');
|
|
30
|
-
const srcRoot = path.join(path.dirname(extEntry), '..');
|
|
31
|
-
const versionMod = jiti(path.join(srcRoot, 'extensions/version.ts')) as {
|
|
32
|
-
normalizeExtensionApiInManifest: (
|
|
33
|
-
manifest: { extensionApi?: string; id?: string },
|
|
34
|
-
options?: { extensionId?: string },
|
|
35
|
-
) => string | null;
|
|
36
|
-
};
|
|
37
|
-
const ryntMod = jiti(
|
|
38
|
-
path.join(srcRoot, 'extensions/registries/manifest-rynt.ts'),
|
|
39
|
-
) as {
|
|
40
|
-
applyValidatedRyntContributesToManifest: (manifest: {
|
|
41
|
-
id: string;
|
|
42
|
-
contributes?: Record<string, unknown>;
|
|
43
|
-
}) => string | null;
|
|
44
|
-
};
|
|
45
|
-
return {
|
|
46
|
-
normalizeExtensionApiInManifest: versionMod.normalizeExtensionApiInManifest,
|
|
47
|
-
applyValidatedRyntContributesToManifest:
|
|
48
|
-
ryntMod.applyValidatedRyntContributesToManifest,
|
|
49
|
-
};
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function readJsonObject(filePath: string): Record<string, unknown> {
|
|
53
|
-
if (!existsSync(filePath)) {
|
|
54
|
-
return {};
|
|
55
|
-
}
|
|
56
|
-
try {
|
|
57
|
-
const raw = readFileSync(filePath, 'utf-8');
|
|
58
|
-
const parsed: unknown = JSON.parse(raw);
|
|
59
|
-
return typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)
|
|
60
|
-
? (parsed as Record<string, unknown>)
|
|
61
|
-
: {};
|
|
62
|
-
} catch (e) {
|
|
63
|
-
console.warn(`[rynt/extension-build] Failed to read JSON at ${filePath}:`, e);
|
|
64
|
-
return {};
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function versionFromPackageJson(pkg: Record<string, unknown>): string | null {
|
|
69
|
-
const v = pkg.version;
|
|
70
|
-
if (v == null) return null;
|
|
71
|
-
const s = String(v).trim();
|
|
72
|
-
return s.length > 0 ? s : null;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function manifestVersionString(value: unknown): string | null {
|
|
76
|
-
if (typeof value === 'string') {
|
|
77
|
-
const t = value.trim();
|
|
78
|
-
return t.length > 0 ? t : null;
|
|
79
|
-
}
|
|
80
|
-
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
81
|
-
return String(value);
|
|
82
|
-
}
|
|
83
|
-
return null;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
export function mergeExtensionManifestForDist(
|
|
87
|
-
root: string,
|
|
88
|
-
manifestFromVite: Record<string, unknown>,
|
|
89
|
-
): Record<string, unknown> {
|
|
90
|
-
const pkg = readJsonObject(path.join(root, 'package.json'));
|
|
91
|
-
const merged: Record<string, unknown> = { ...manifestFromVite };
|
|
92
|
-
|
|
93
|
-
if (typeof merged.name !== 'string' || !merged.name) {
|
|
94
|
-
if (typeof pkg.name === 'string' && pkg.name) merged.name = pkg.name;
|
|
95
|
-
}
|
|
96
|
-
const fromManifest = manifestVersionString(merged.version);
|
|
97
|
-
const fromPkg = versionFromPackageJson(pkg);
|
|
98
|
-
if (fromManifest) {
|
|
99
|
-
merged.version = fromManifest;
|
|
100
|
-
} else if (fromPkg) {
|
|
101
|
-
merged.version = fromPkg;
|
|
102
|
-
} else {
|
|
103
|
-
merged.version = '0.0.0';
|
|
104
|
-
console.warn(
|
|
105
|
-
'[rynt/extension-build] В manifest и package.json нет version — подставлено 0.0.0.',
|
|
106
|
-
);
|
|
107
|
-
}
|
|
108
|
-
if (merged.description === undefined || merged.description === null) {
|
|
109
|
-
if (typeof pkg.description === 'string' && pkg.description) {
|
|
110
|
-
merged.description = pkg.description;
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
if (typeof merged.id !== 'string' || !merged.id) {
|
|
114
|
-
if (typeof pkg.name === 'string' && pkg.name) merged.id = pkg.name;
|
|
115
|
-
}
|
|
116
|
-
if (typeof merged.main !== 'string' || !merged.main) {
|
|
117
|
-
merged.main = 'index.js';
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
return merged;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
export function ryntExtensionEmitDistManifestPlugin(options: {
|
|
124
|
-
root: string;
|
|
125
|
-
manifestFromVite: Record<string, unknown>;
|
|
126
|
-
}): Plugin {
|
|
127
|
-
const { root, manifestFromVite } = options;
|
|
128
|
-
return {
|
|
129
|
-
name: 'rynt-extension-emit-dist-manifest',
|
|
130
|
-
apply: 'build',
|
|
131
|
-
async closeBundle() {
|
|
132
|
-
const merged = mergeExtensionManifestForDist(root, manifestFromVite);
|
|
133
|
-
if (typeof merged.id !== 'string' || !merged.id) {
|
|
134
|
-
console.warn(
|
|
135
|
-
'[rynt/extension-build] manifest.id обязателен; skip writing dist/manifest.json',
|
|
136
|
-
);
|
|
137
|
-
return;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
try {
|
|
141
|
-
const { normalizeExtensionApiInManifest, applyValidatedRyntContributesToManifest } =
|
|
142
|
-
await loadManifestValidators();
|
|
143
|
-
|
|
144
|
-
const apiErr = normalizeExtensionApiInManifest(
|
|
145
|
-
merged as { extensionApi?: string; id?: string },
|
|
146
|
-
{ extensionId: merged.id },
|
|
147
|
-
);
|
|
148
|
-
if (apiErr) {
|
|
149
|
-
console.warn(
|
|
150
|
-
`[rynt/extension-build] extensionApi для «${merged.id}»: ${apiErr}`,
|
|
151
|
-
);
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
const contribErr = applyValidatedRyntContributesToManifest({
|
|
155
|
-
id: merged.id,
|
|
156
|
-
contributes:
|
|
157
|
-
merged.contributes &&
|
|
158
|
-
typeof merged.contributes === 'object' &&
|
|
159
|
-
!Array.isArray(merged.contributes)
|
|
160
|
-
? (merged.contributes as Record<string, unknown>)
|
|
161
|
-
: undefined,
|
|
162
|
-
});
|
|
163
|
-
if (contribErr) {
|
|
164
|
-
console.warn(
|
|
165
|
-
`[rynt/extension-build] contributes.rynt для «${merged.id}»: ${contribErr}`,
|
|
166
|
-
);
|
|
167
|
-
}
|
|
168
|
-
} catch (e) {
|
|
169
|
-
console.warn(
|
|
170
|
-
'[rynt/extension-build] Пропуск валидации manifest через SDK:',
|
|
171
|
-
e,
|
|
172
|
-
);
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
try {
|
|
176
|
-
await applyExtensionIconToManifest(root, merged);
|
|
177
|
-
} catch (e) {
|
|
178
|
-
const msg = e instanceof Error ? e.message : String(e);
|
|
179
|
-
throw new Error(msg);
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
const outPath = path.join(root, 'dist', 'manifest.json');
|
|
183
|
-
mkdirSync(path.dirname(outPath), { recursive: true });
|
|
184
|
-
writeFileSync(outPath, `${JSON.stringify(merged, null, 2)}\n`, 'utf-8');
|
|
185
|
-
},
|
|
186
|
-
};
|
|
187
|
-
}
|