feat: esm directory import (#21428)
This commit is contained in:
parent
d38290b985
commit
a3f06e485c
|
|
@ -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;
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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/,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in New Issue