feat: esm directory import (#21428)

This commit is contained in:
Tony 2026-03-18 15:15:28 +08:00 committed by GitHub
parent d38290b985
commit a3f06e485c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 106 additions and 29 deletions

View File

@ -124,4 +124,80 @@ describe('registry dynamic loading', () => {
process.env.NODE_ENV = originalEnv;
});
// https://github.com/DIYgod/RSSHub/pull/18002
it('prioritizes literal segments over parameter segments in route matching', async () => {
const originalEnv = process.env.NODE_ENV;
process.env.NODE_ENV = 'development';
const modules = {
'/specificity/param-route.ts': {
route: {
path: '/:category',
name: 'ParamRoute',
handler: () => ({
title: 'param',
link: 'https://example.com',
item: [],
allowEmpty: true,
}),
},
},
'/specificity/literal-route.ts': {
route: {
path: '/news/:channel',
name: 'LiteralRoute',
handler: () => ({
title: 'literal',
link: 'https://example.com',
item: [],
allowEmpty: true,
}),
},
},
'/specificity/news-route.ts': {
route: {
path: '/news',
name: 'NewsRoute',
handler: () => ({
title: 'news',
link: 'https://example.com',
item: [],
allowEmpty: true,
}),
},
},
};
const directoryImportMock = vi.fn(() => modules);
vi.doMock('@/utils/directory-import', () => ({
directoryImport: directoryImportMock,
}));
vi.resetModules();
const { default: registry } = await import('@/registry');
const app = new Hono();
app.use(async (ctx, next) => {
await next();
const data = ctx.get('data');
if (data) {
return ctx.json(data);
}
});
app.route('/', registry);
// /news/sports should match /news/:channel (literal "news" wins over :category)
const literalResponse = await app.request('/specificity/news/sports');
expect((await literalResponse.json()).title).toBe('literal');
// /news should match /news (literal wins over :category)
const newsResponse = await app.request('/specificity/news');
expect((await newsResponse.json()).title).toBe('news');
// /products should match /:category
const paramResponse = await app.request('/specificity/products');
expect((await paramResponse.json()).title).toBe('param');
process.env.NODE_ENV = originalEnv;
});
});

View File

@ -72,10 +72,10 @@ if (config.isPackage) {
}
break;
default:
modules = directoryImport({
modules = (await directoryImport({
targetDirectoryPath: path.join(__dirname, './routes'),
importPattern: /\.tsx?$/,
}) as typeof modules;
})) as typeof modules;
}
}
@ -100,6 +100,7 @@ if (Object.keys(modules).length) {
namespaces[namespace] = Object.assign(
{
routes: {},
apiRoutes: {},
},
namespaces[namespace],
content.namespace

View File

@ -23,21 +23,21 @@ describe('directory-import', () => {
}
});
it('imports valid files and skips invalid ones', () => {
it('imports valid files and skips invalid ones', async () => {
tempDir = createTempDir();
const rootModule = path.join(tempDir, 'valid.cjs');
const rootModule = path.join(tempDir, 'valid.mjs');
const jsonModule = path.join(tempDir, 'data.json');
const ignoredText = path.join(tempDir, 'note.txt');
const declaration = path.join(tempDir, 'types.d.ts');
const nestedModule = path.join(tempDir, 'sub', 'child.cjs');
const nestedModule = path.join(tempDir, 'sub', 'child.mjs');
writeFile(rootModule, "module.exports = { value: 'root' };");
writeFile(rootModule, "export const value = 'root';");
writeFile(jsonModule, JSON.stringify({ value: 'json' }));
writeFile(ignoredText, 'ignore');
writeFile(declaration, 'export {};');
writeFile(nestedModule, "module.exports = { value: 'child' };");
writeFile(nestedModule, "export const value = 'child';");
const modules = directoryImport({ targetDirectoryPath: tempDir });
const modules = await directoryImport({ targetDirectoryPath: tempDir });
const keyFor = (filePath: string) => filePath.slice(tempDir.length);
expect(modules).toHaveProperty(keyFor(rootModule));
@ -47,15 +47,15 @@ describe('directory-import', () => {
expect(modules).not.toHaveProperty(keyFor(declaration));
});
it('can skip subdirectories and apply patterns', () => {
it('can skip subdirectories and apply patterns', async () => {
tempDir = createTempDir();
const rootModule = path.join(tempDir, 'keep.cjs');
const nestedModule = path.join(tempDir, 'sub', 'skip.cjs');
const rootModule = path.join(tempDir, 'keep.mjs');
const nestedModule = path.join(tempDir, 'sub', 'skip.mjs');
writeFile(rootModule, "module.exports = { value: 'keep' };");
writeFile(nestedModule, "module.exports = { value: 'skip' };");
writeFile(rootModule, "export const value = 'keep';");
writeFile(nestedModule, "export const value = 'skip';");
const modules = directoryImport({
const modules = await directoryImport({
targetDirectoryPath: tempDir,
includeSubdirectories: false,
importPattern: /keep/,

View File

@ -1,8 +1,6 @@
import fs from 'node:fs';
import { createRequire } from 'node:module';
import path from 'node:path';
const require = createRequire(import.meta.url);
import { pathToFileURL } from 'node:url';
export type DirectoryImportOptions = {
targetDirectoryPath: string;
@ -33,23 +31,25 @@ const readDirectory = (targetDirectoryPath: string, includeSubdirectories: boole
return files;
};
export const directoryImport = ({ targetDirectoryPath, importPattern = /.*/, includeSubdirectories = true }: DirectoryImportOptions) => {
export const directoryImport = async ({ targetDirectoryPath, importPattern = /.*/, includeSubdirectories = true }: DirectoryImportOptions) => {
const modules: Record<string, unknown> = {};
const filesPaths = readDirectory(targetDirectoryPath, includeSubdirectories);
for (const filePath of filesPaths) {
const { ext: fileExtension } = path.parse(filePath);
const isValidModuleExtension = VALID_IMPORT_EXTENSIONS.has(fileExtension);
const isDeclarationFile = filePath.endsWith('.d.ts') || filePath.endsWith('.d.tsx');
const isValidFilePath = importPattern.test(filePath);
await Promise.all(
filesPaths.map(async (filePath) => {
const { ext: fileExtension } = path.parse(filePath);
const isValidModuleExtension = VALID_IMPORT_EXTENSIONS.has(fileExtension);
const isDeclarationFile = filePath.endsWith('.d.ts') || filePath.endsWith('.d.tsx');
const isValidFilePath = importPattern.test(filePath);
if (!isValidModuleExtension || isDeclarationFile || !isValidFilePath) {
continue;
}
if (!isValidModuleExtension || isDeclarationFile || !isValidFilePath) {
return;
}
const relativeModulePath = filePath.slice(targetDirectoryPath.length);
modules[relativeModulePath] = require(filePath);
}
const relativeModulePath = filePath.slice(targetDirectoryPath.length);
modules[relativeModulePath] = await import(pathToFileURL(filePath).href);
})
);
return modules;
};