Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
142 lines
3.9 KiB
JavaScript
142 lines
3.9 KiB
JavaScript
import { readFile, readdir, stat } from "node:fs/promises";
|
|
import {
|
|
dirname,
|
|
extname,
|
|
join,
|
|
normalize,
|
|
relative,
|
|
resolve
|
|
} from "node:path";
|
|
|
|
const relativeImportPattern =
|
|
/(?:import|export)\s+(?:type\s+)?(?:[^'";]*?\s+from\s+)?["'](\.[^"']+)["']|import\s*\(\s*["'](\.[^"']+)["']\s*\)/g;
|
|
const moduleRelativeAssetPattern =
|
|
/new URL\(\s*["'](\.[^"']+)["']\s*,\s*import\.meta\.url\s*\)/g;
|
|
const javascriptExtensions = new Set([".cjs", ".js", ".mjs"]);
|
|
|
|
const extensionCandidates = [".ts", ".tsx", ".js", ".mjs", ".cjs", ".json"];
|
|
|
|
export async function missingPackedRelativeImports(packageRoot, entryPath) {
|
|
const normalizedRoot = resolve(packageRoot);
|
|
const entry = resolve(normalizedRoot, entryPath);
|
|
const pending = [entry];
|
|
const visited = new Set();
|
|
const missing = new Set();
|
|
|
|
while (pending.length > 0) {
|
|
const sourcePath = pending.pop();
|
|
if (!sourcePath || visited.has(sourcePath)) {
|
|
continue;
|
|
}
|
|
visited.add(sourcePath);
|
|
if (!(await isFile(sourcePath))) {
|
|
missing.add(asPackagePath(normalizedRoot, sourcePath));
|
|
continue;
|
|
}
|
|
|
|
const source = await readFile(sourcePath, "utf8");
|
|
for (const specifier of relativeImportSpecifiers(source)) {
|
|
const importedPath = await resolveRelativeImport(sourcePath, specifier);
|
|
if (!importedPath || !isInside(normalizedRoot, importedPath)) {
|
|
missing.add(
|
|
`${asPackagePath(normalizedRoot, sourcePath)} -> ${specifier}`
|
|
);
|
|
continue;
|
|
}
|
|
pending.push(importedPath);
|
|
}
|
|
}
|
|
|
|
return [...missing].sort();
|
|
}
|
|
|
|
export async function missingPackedModuleRelativeAssets(packageRoot) {
|
|
const normalizedRoot = resolve(packageRoot);
|
|
const files = await listFiles(normalizedRoot);
|
|
const missing = [];
|
|
|
|
for (const file of files) {
|
|
if (!javascriptExtensions.has(extname(file))) {
|
|
continue;
|
|
}
|
|
|
|
const source = await readFile(file, "utf8");
|
|
moduleRelativeAssetPattern.lastIndex = 0;
|
|
for (const match of source.matchAll(moduleRelativeAssetPattern)) {
|
|
const specifier = match[1];
|
|
const cleanSpecifier = specifier.split(/[?#]/, 1)[0];
|
|
const assetPath = resolve(dirname(file), cleanSpecifier);
|
|
if (!(await isFile(assetPath)) || !isInside(normalizedRoot, assetPath)) {
|
|
missing.push(`${asPackagePath(normalizedRoot, file)} -> ${specifier}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
return missing.sort();
|
|
}
|
|
|
|
function relativeImportSpecifiers(source) {
|
|
const specifiers = [];
|
|
relativeImportPattern.lastIndex = 0;
|
|
for (const match of source.matchAll(relativeImportPattern)) {
|
|
specifiers.push(match[1] ?? match[2]);
|
|
}
|
|
return specifiers;
|
|
}
|
|
|
|
async function resolveRelativeImport(sourcePath, specifier) {
|
|
const cleanSpecifier = specifier.split(/[?#]/, 1)[0];
|
|
const basePath = resolve(dirname(sourcePath), cleanSpecifier);
|
|
const candidates = extname(basePath)
|
|
? [basePath]
|
|
: [
|
|
basePath,
|
|
...extensionCandidates.map((extension) => `${basePath}${extension}`),
|
|
...extensionCandidates.map((extension) =>
|
|
join(basePath, `index${extension}`)
|
|
)
|
|
];
|
|
for (const candidate of candidates) {
|
|
if (await isFile(candidate)) {
|
|
return candidate;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
async function isFile(path) {
|
|
try {
|
|
return (await stat(path)).isFile();
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
async function listFiles(directory) {
|
|
const entries = await readdir(directory, { withFileTypes: true });
|
|
const files = [];
|
|
|
|
for (const entry of entries) {
|
|
const path = join(directory, entry.name);
|
|
if (entry.isDirectory()) {
|
|
files.push(...(await listFiles(path)));
|
|
} else if (entry.isFile()) {
|
|
files.push(path);
|
|
}
|
|
}
|
|
|
|
return files;
|
|
}
|
|
|
|
function isInside(root, path) {
|
|
const child = relative(root, path);
|
|
return (
|
|
child !== ".." &&
|
|
!child.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`)
|
|
);
|
|
}
|
|
|
|
function asPackagePath(root, path) {
|
|
return normalize(relative(root, path)).replaceAll("\\", "/");
|
|
}
|