mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-21 06:26:26 +00:00
fix(plugin-sdk): make require('trek-plugin-sdk') actually resolve everywhere
A freshly scaffolded plugin could not load anywhere: the npm package is
ESM-only (no require condition in its exports map), so the scaffold's
require('trek-plugin-sdk') threw ERR_PACKAGE_PATH_NOT_EXPORTED under
`trek-plugin dev` - and the runtime injection the wiki promised for the
plugin child never existed, so a packed plugin (node_modules stripped)
crashed with MODULE_NOT_FOUND after a real install.
- plugin child: inject a frozen {definePlugin, PLUGIN_API_VERSION} shim
for require('trek-plugin-sdk'); subpaths fail with a pointed error
- trek-plugin dev: inject the exact same shim, so a fresh scaffold runs
with zero npm install and dev parity with production holds
- npm package: ship a real CommonJS build (dist/cjs + require export
conditions) so the installed package also requires cleanly on Node 18+
- create: scaffold a package.json (type commonjs, SDK as devDependency,
npx scripts); print resolvable `npx trek-plugin-sdk ...` hints
- wiki: package.json in the scaffold tree + publishing checklist, and
document the zero-install dev flow
This commit is contained in:
@@ -7,6 +7,17 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import readline from 'node:readline';
|
||||
import { createRequire } from 'node:module';
|
||||
|
||||
/** This package's own version, for the scaffold's devDependency range. */
|
||||
function sdkVersionRange(): string {
|
||||
try {
|
||||
const pkg = createRequire(import.meta.url)('../../package.json') as { version?: string };
|
||||
return pkg.version ? `^${pkg.version}` : '^1';
|
||||
} catch {
|
||||
return '^1';
|
||||
}
|
||||
}
|
||||
|
||||
export interface ScaffoldOptions {
|
||||
author?: string;
|
||||
@@ -42,6 +53,17 @@ export function scaffold(name: string, type: string, targetDir: string, opts: Sc
|
||||
fs.writeFileSync(path.join(root, 'trek-plugin.json'), JSON.stringify(manifest, null, 2) + '\n');
|
||||
fs.writeFileSync(path.join(root, 'server', 'index.js'), SERVER_JS);
|
||||
fs.writeFileSync(path.join(root, 'README.md'), readme(name));
|
||||
// `type: commonjs` pins how the entry is parsed everywhere (dev, tests, TREK);
|
||||
// the SDK is a devDependency ONLY (types + mock host) — at runtime both the
|
||||
// dev server and TREK inject it, so it is never vendored into the artifact.
|
||||
fs.writeFileSync(path.join(root, 'package.json'), JSON.stringify({
|
||||
name,
|
||||
version: '1.0.0',
|
||||
private: true,
|
||||
type: 'commonjs',
|
||||
scripts: { dev: 'npx -y trek-plugin-sdk dev', pack: 'npx -y trek-plugin-sdk pack' },
|
||||
devDependencies: { 'trek-plugin-sdk': sdkVersionRange() },
|
||||
}, null, 2) + '\n');
|
||||
if (type !== 'integration') {
|
||||
fs.mkdirSync(path.join(root, 'client'), { recursive: true });
|
||||
fs.writeFileSync(path.join(root, 'client', 'index.html'), CLIENT_HTML);
|
||||
@@ -162,7 +184,7 @@ if (process.argv[1] && process.argv[1].endsWith('create.js')) {
|
||||
}
|
||||
try {
|
||||
scaffold(name, type, process.cwd());
|
||||
console.log(`Created ${name}/ — fill in the README, build server/index.js, then \`npx trek-plugin validate ${name}\`.`);
|
||||
console.log(`Created ${name}/ — fill in the README, build server/index.js, then \`npx trek-plugin-sdk validate ${name}\`.`);
|
||||
} catch (e) {
|
||||
console.error(e instanceof Error ? e.message : e);
|
||||
process.exit(1);
|
||||
|
||||
@@ -13,6 +13,7 @@ import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import http from 'node:http';
|
||||
import { createRequire } from 'node:module';
|
||||
import * as sdk from '../index.js';
|
||||
|
||||
interface Fixtures {
|
||||
config?: Record<string, unknown>;
|
||||
@@ -92,6 +93,34 @@ function createDevContext(id: string, grants: Set<string>, fx: Fixtures, db: Plu
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Serve `require('trek-plugin-sdk')` from THIS package, exactly like the TREK
|
||||
* child process does at runtime — so `dev` works on a fresh scaffold with no
|
||||
* npm install, and what loads here is what loads in production. That parity is
|
||||
* the point: the injected surface is the SAME minimal frozen shim the prod
|
||||
* child serves (definePlugin + PLUGIN_API_VERSION), and subpaths fail with the
|
||||
* same pointed error. validateManifest/createMockHost stay available where they
|
||||
* belong — in your tests, which load the installed package without this patch.
|
||||
*/
|
||||
let sdkInjected = false;
|
||||
export function installSdkInjection(): void {
|
||||
if (sdkInjected) return;
|
||||
const nodeModule = createRequire(import.meta.url)('node:module') as {
|
||||
_load: (request: string, parent: unknown, isMain: boolean) => unknown;
|
||||
};
|
||||
const realLoad = nodeModule._load;
|
||||
if (typeof realLoad !== 'function') return;
|
||||
sdkInjected = true;
|
||||
const shim = Object.freeze({ definePlugin: sdk.definePlugin, PLUGIN_API_VERSION: sdk.PLUGIN_API_VERSION });
|
||||
nodeModule._load = function (request: string, parent: unknown, isMain: boolean): unknown {
|
||||
if (request === 'trek-plugin-sdk') return shim;
|
||||
if (request.startsWith('trek-plugin-sdk/')) {
|
||||
throw new Error(`${request} is a build/test-time module — only 'trek-plugin-sdk' itself is injected inside TREK`);
|
||||
}
|
||||
return realLoad.call(this, request, parent, isMain);
|
||||
};
|
||||
}
|
||||
|
||||
function loadFixtures(dir: string): Fixtures {
|
||||
const p = path.join(dir, 'dev-fixtures.json');
|
||||
if (fs.existsSync(p)) { try { return JSON.parse(fs.readFileSync(p, 'utf8')); } catch { console.warn('warning: dev-fixtures.json is not valid JSON — ignoring'); } }
|
||||
@@ -120,6 +149,7 @@ export async function runDev(dir: string, opts: { port?: number } = {}): Promise
|
||||
const fx = loadFixtures(abs);
|
||||
const { db, note: dbNote, close: closeDb } = createDevDb(path.join(abs, '.trek-dev', 'db.sqlite'));
|
||||
const broadcasts: unknown[] = [];
|
||||
installSdkInjection();
|
||||
const req = createRequire(path.join(abs, 'server', 'index.js'));
|
||||
|
||||
let plugin: PluginLike = {};
|
||||
|
||||
@@ -67,14 +67,14 @@ async function main(): Promise<void> {
|
||||
const name = pos[0];
|
||||
if (!name || flags.interactive) {
|
||||
const created = await interactiveScaffold(process.cwd(), name);
|
||||
console.log(`\nCreated ${created}/ — build server/index.js, add docs/screenshot.png, then \`trek-plugin dev ${created}\`.`);
|
||||
console.log(`\nCreated ${created}/ — build server/index.js, add docs/screenshot.png, then \`npx trek-plugin-sdk dev ${created}\`.`);
|
||||
return;
|
||||
}
|
||||
scaffold(name, flags.type || 'integration', process.cwd(), {
|
||||
author: flags.author, description: flags.description,
|
||||
permissions: flags.permissions ? flags.permissions.split(/[\s,]+/).filter(Boolean) : undefined,
|
||||
});
|
||||
console.log(`Created ${name}/ — build server/index.js, add docs/screenshot.png, then \`trek-plugin dev ${name}\`.`);
|
||||
console.log(`Created ${name}/ — build server/index.js, add docs/screenshot.png, then \`npx trek-plugin-sdk dev ${name}\`.`);
|
||||
} else if (cmd === 'dev') {
|
||||
await runDev(pos[0] || '.', { port: flags.port ? Number(flags.port) : undefined });
|
||||
} else if (cmd === 'validate') {
|
||||
@@ -90,17 +90,17 @@ async function main(): Promise<void> {
|
||||
console.log(`Packed ${r.files.length} files -> ${path.relative(process.cwd(), r.artifact) || r.artifact}`);
|
||||
for (const f of r.files) console.log(' ' + f);
|
||||
console.log(`\nsha256: ${r.sha256}\nsize: ${r.size}`);
|
||||
console.log('\nUpload this plugin.zip to your release, then run `trek-plugin entry` to generate the registry entry.');
|
||||
console.log('\nUpload this plugin.zip to your release, then run `npx trek-plugin-sdk entry` to generate the registry entry.');
|
||||
}
|
||||
} else if (cmd === 'keygen') {
|
||||
const keyPath = flags.key || defaultKeyPath();
|
||||
const { publicKey } = generateKeypair(keyPath);
|
||||
console.log(`Signing key written to ${keyPath} (keep it safe + BACK IT UP — losing it means you can't ship signed updates).`);
|
||||
console.log(`\nauthorPublicKey (goes in your registry entry): ${publicKey}`);
|
||||
console.log('\nSign releases with `trek-plugin release --sign` (or `entry --sign`).');
|
||||
console.log('\nSign releases with `npx trek-plugin-sdk release --sign` (or `entry --sign`).');
|
||||
} else if (cmd === 'sign') {
|
||||
const zip = pos[0] || 'plugin.zip';
|
||||
if (!fs.existsSync(zip)) fail(`artifact not found: ${zip} — run \`trek-plugin pack\` first`);
|
||||
if (!fs.existsSync(zip)) fail(`artifact not found: ${zip} — run \`npx trek-plugin-sdk pack\` first`);
|
||||
const key = loadPrivateKey(flags.key || defaultKeyPath());
|
||||
const buf = fs.readFileSync(zip);
|
||||
console.log(`signature: ${signArtifact(buf, key)}`);
|
||||
@@ -162,7 +162,7 @@ async function main(): Promise<void> {
|
||||
console.error(`Creating GitHub release ${flags.tag} on ${flags.repo}…`);
|
||||
execFileSync('gh', ['release', 'create', flags.tag, packed.artifact, '--repo', flags.repo, '--title', flags.tag, '--notes', flags.notes || `Release ${flags.tag}`], { stdio: 'inherit' });
|
||||
const entry = buildEntry({ dir, repo: flags.repo, tag: flags.tag, zipPath: packed.artifact, commit: flags.commit, mergePath: flags.merge, signKeyPath: signKey(), now: new Date().toISOString() });
|
||||
console.error('\nRegistry entry (add as registry/plugins/' + entry.id + '.json in a TREK-Plugins PR, or run `trek-plugin submit`):\n');
|
||||
console.error('\nRegistry entry (add as registry/plugins/' + entry.id + '.json in a TREK-Plugins PR, or run `npx trek-plugin-sdk submit`):\n');
|
||||
process.stdout.write(JSON.stringify(entry, null, 2) + '\n');
|
||||
} else {
|
||||
console.error('usage: trek-plugin <create|dev|validate|pack|keygen|sign|entry|preflight|submit|release|publish> [...]');
|
||||
|
||||
Reference in New Issue
Block a user