110 lines
3.0 KiB
JavaScript
110 lines
3.0 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import { readdir, stat, readFile } from 'node:fs/promises';
|
|
import {
|
|
dirname,
|
|
extname,
|
|
isAbsolute,
|
|
relative,
|
|
resolve,
|
|
sep,
|
|
} from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
import test from 'node:test';
|
|
|
|
const WEB_ROOT = fileURLToPath(new URL('../web/', import.meta.url));
|
|
|
|
const STATIC_IMPORT_PATTERN = /(?:^|[;\r\n])\s*(?:import|export)\s+(?:[\s\S]*?\sfrom\s+)?["']([^"']+)["']/g;
|
|
const DYNAMIC_IMPORT_PATTERN = /\bimport\s*\(\s*["']([^"']+)["']\s*\)/g;
|
|
|
|
async function collectJavaScriptFiles(directory) {
|
|
const entries = await readdir(directory, { withFileTypes: true });
|
|
const files = [];
|
|
|
|
for (const entry of entries) {
|
|
const entryPath = resolve(directory, entry.name);
|
|
if (entry.isDirectory()) {
|
|
files.push(...await collectJavaScriptFiles(entryPath));
|
|
} else if (entry.isFile() && extname(entry.name).toLowerCase() === '.js') {
|
|
files.push(entryPath);
|
|
}
|
|
}
|
|
|
|
return files;
|
|
}
|
|
|
|
function extractModuleSpecifiers(source) {
|
|
const specifiers = new Set();
|
|
|
|
for (const pattern of [STATIC_IMPORT_PATTERN, DYNAMIC_IMPORT_PATTERN]) {
|
|
pattern.lastIndex = 0;
|
|
for (const match of source.matchAll(pattern)) {
|
|
specifiers.add(match[1]);
|
|
}
|
|
}
|
|
|
|
return [...specifiers];
|
|
}
|
|
|
|
function isInsideWebRoot(targetPath) {
|
|
const pathFromWebRoot = relative(WEB_ROOT, targetPath);
|
|
return Boolean(
|
|
pathFromWebRoot === ''
|
|
|| (!pathFromWebRoot.startsWith(`..${sep}`)
|
|
&& pathFromWebRoot !== '..'
|
|
&& !isAbsolute(pathFromWebRoot))
|
|
);
|
|
}
|
|
|
|
async function resolveLocalModule(importerPath, specifier) {
|
|
const targetPath = resolve(dirname(importerPath), specifier);
|
|
if (!isInsideWebRoot(targetPath)) {
|
|
// Imports outside web/ are ComfyUI-provided modules such as scripts/app.js.
|
|
return null;
|
|
}
|
|
|
|
const candidates = [
|
|
targetPath,
|
|
`${targetPath}.js`,
|
|
resolve(targetPath, 'index.js'),
|
|
];
|
|
|
|
for (const candidate of candidates) {
|
|
try {
|
|
if ((await stat(candidate)).isFile()) return candidate;
|
|
} catch (error) {
|
|
if (error?.code !== 'ENOENT') throw error;
|
|
}
|
|
}
|
|
|
|
return targetPath;
|
|
}
|
|
|
|
test('all local frontend module imports resolve to files', async () => {
|
|
const sourceFiles = await collectJavaScriptFiles(WEB_ROOT);
|
|
const missingImports = [];
|
|
|
|
for (const importerPath of sourceFiles) {
|
|
const source = await readFile(importerPath, 'utf8');
|
|
for (const specifier of extractModuleSpecifiers(source)) {
|
|
if (!specifier.startsWith('.')) continue;
|
|
|
|
const resolvedPath = await resolveLocalModule(importerPath, specifier);
|
|
if (!resolvedPath) continue;
|
|
|
|
try {
|
|
if ((await stat(resolvedPath)).isFile()) continue;
|
|
} catch (error) {
|
|
if (error?.code !== 'ENOENT') throw error;
|
|
}
|
|
|
|
missingImports.push({
|
|
importer: relative(WEB_ROOT, importerPath),
|
|
specifier,
|
|
expected: relative(WEB_ROOT, resolvedPath),
|
|
});
|
|
}
|
|
}
|
|
|
|
assert.deepEqual(missingImports, [], 'local frontend imports must point to existing files');
|
|
});
|