diff --git a/src/modules/qqbot/plugin-platform/infrastructure/integration/package/plugin-package-path-policy.service.ts b/src/modules/qqbot/plugin-platform/infrastructure/integration/package/plugin-package-path-policy.service.ts index 6c4bb6d..d0dc11f 100644 --- a/src/modules/qqbot/plugin-platform/infrastructure/integration/package/plugin-package-path-policy.service.ts +++ b/src/modules/qqbot/plugin-platform/infrastructure/integration/package/plugin-package-path-policy.service.ts @@ -2,13 +2,10 @@ import { Inject, Injectable, Optional } from '@nestjs/common'; import { existsSync, statSync } from 'node:fs'; import { isAbsolute, relative, resolve, sep } from 'node:path'; -const DEFAULT_BUILTIN_PACKAGE_ROOT = resolve( - process.cwd(), - 'src', - 'modules', - 'qqbot', - 'plugins', -); +const DEFAULT_BUILTIN_PACKAGE_ROOT_SEGMENTS = [ + ['src', 'modules', 'qqbot', 'plugins'], + ['dist', 'modules', 'qqbot', 'plugins'], +]; export const QQBOT_PLUGIN_PACKAGE_CONTROLLED_ROOTS = Symbol( 'QQBOT_PLUGIN_PACKAGE_CONTROLLED_ROOTS', ); @@ -30,7 +27,9 @@ export class QqbotPluginPackagePathPolicyService { controlledRoots?: string[], ) { this.controlledRoots = ( - controlledRoots?.length ? controlledRoots : [DEFAULT_BUILTIN_PACKAGE_ROOT] + controlledRoots?.length + ? controlledRoots + : resolveDefaultBuiltinPackageRoots() ).map((root) => resolve(root)); } @@ -58,7 +57,7 @@ export class QqbotPluginPackagePathPolicyService { throw new Error('Plugin entry must stay inside the package root'); } - return entryFile; + return this.resolveCompiledEntryFile(entryFile); } /** @@ -95,4 +94,33 @@ export class QqbotPluginPackagePathPolicyService { isAbsolute(relation) ); } + + /** + * Resolves a manifest entry to the file Node can import in the current runtime. + * @param entryFile - Policy-checked entry path declared by `plugin.json`. + * @returns The declared file when it exists, otherwise the compiled `.js` sibling for TypeScript entries. + */ + private resolveCompiledEntryFile(entryFile: string): string { + if (existsSync(entryFile)) return entryFile; + + if (entryFile.endsWith('.ts')) { + const compiledEntryFile = `${entryFile.slice(0, -3)}.js`; + if (existsSync(compiledEntryFile)) { + return compiledEntryFile; + } + } + + return entryFile; + } +} + +/** + * Resolves the default built-in package root for source development or production `dist` output. + * @returns A single preferred controlled root so package discovery does not duplicate source and dist manifests. + */ +function resolveDefaultBuiltinPackageRoots(): string[] { + const candidates = DEFAULT_BUILTIN_PACKAGE_ROOT_SEGMENTS.map((segments) => + resolve(process.cwd(), ...segments), + ); + return [candidates.find((candidate) => existsSync(candidate)) || candidates[0]]; } diff --git a/test/modules/qqbot/plugin-platform/plugin-package-source.spec.ts b/test/modules/qqbot/plugin-platform/plugin-package-source.spec.ts index 33452ba..fa78f88 100644 --- a/test/modules/qqbot/plugin-platform/plugin-package-source.spec.ts +++ b/test/modules/qqbot/plugin-platform/plugin-package-source.spec.ts @@ -136,6 +136,74 @@ describe('QqbotPluginPackageSourceService', () => { ); }); + it('resolves TypeScript manifest entries to compiled JavaScript files in dist packages', async () => { + const packageRoot = join(tempRoot, 'compiled-entry'); + mkdirSync(join(packageRoot, 'src'), { recursive: true }); + writeFileSync( + join(packageRoot, 'plugin.json'), + JSON.stringify({ + key: 'compiled-entry', + name: 'Compiled Entry', + version: '1.0.0', + entry: 'src/index.ts', + runtime: { + workerType: 'thread', + timeoutMs: 5000, + memoryMb: 128, + maxConcurrency: 1, + }, + operations: [], + }), + 'utf8', + ); + writeFileSync( + join(packageRoot, 'src', 'index.js'), + 'module.exports = { createPlugin() {} };', + 'utf8', + ); + + const source = new QqbotPluginPackageSourceService( + new QqbotPluginPackagePathPolicyService([tempRoot]), + ); + + await expect(source.discoverPackages()).resolves.toEqual([ + expect.objectContaining({ + entry: 'src/index.ts', + entryFile: join(packageRoot, 'src', 'index.js'), + pluginKey: 'compiled-entry', + }), + ]); + }); + + it('discovers the default built-in plugin root from production dist output', async () => { + const previousCwd = process.cwd(); + const productionRoot = join(tempRoot, 'production-app'); + const distPluginRoot = join( + productionRoot, + 'dist', + 'modules', + 'qqbot', + 'plugins', + ); + mkdirSync(distPluginRoot, { recursive: true }); + + try { + process.chdir(productionRoot); + jest.resetModules(); + const { + QqbotPluginPackagePathPolicyService: RuntimePathPolicyService, + } = await import( + '../../../../src/modules/qqbot/plugin-platform/infrastructure/integration/package/plugin-package-path-policy.service' + ); + const pathPolicy = new RuntimePathPolicyService(); + + expect(pathPolicy.listExistingRoots()).toEqual([distPluginRoot]); + } finally { + process.chdir(previousCwd); + jest.resetModules(); + } + }); + it('does not keep platform-side manifest transfer shims', () => { const source = readFileSync( join(