mirror of
https://github.com/JimLiu/baoyu-skills.git
synced 2026-07-12 05:51:44 +08:00
refactor(shared-skill-packages): add files allowlist support and filter test/changelog files
This commit is contained in:
@@ -10,8 +10,12 @@ const PACKAGE_DEPENDENCY_SECTIONS = [
|
||||
"devDependencies",
|
||||
];
|
||||
|
||||
const SKIPPED_DIRS = new Set([".git", ".clawhub", ".clawdhub", "node_modules"]);
|
||||
const SKIPPED_DIRS = new Set([".git", ".changeset", ".clawhub", ".clawdhub", "node_modules"]);
|
||||
const SKIPPED_FILES = new Set([".DS_Store"]);
|
||||
const TEST_DIR_NAMES = new Set(["__tests__", "test", "tests"]);
|
||||
const TEST_FILE_PATTERN = /\.(test|spec)\.[^.]+$/;
|
||||
const CHANGELOG_FILE_PATTERN = /^CHANGELOG(?:\..+)?\.md$/i;
|
||||
const UNSUPPORTED_FILES_GLOB_PATTERN = /[*?[\]{}!]/;
|
||||
|
||||
export async function syncSharedSkillPackages(repoRoot, options = {}) {
|
||||
const root = path.resolve(repoRoot);
|
||||
@@ -131,23 +135,7 @@ async function syncPackageTree({ sourceDir, targetDir, workspacePackages }) {
|
||||
const sourcePackageJsonPath = path.join(sourceDir, "package.json");
|
||||
const packageJson = JSON.parse(await fs.readFile(sourcePackageJsonPath, "utf8"));
|
||||
const localDeps = collectLocalDependencies(packageJson, workspacePackages);
|
||||
|
||||
const entries = await fs.readdir(sourceDir, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
if (SKIPPED_DIRS.has(entry.name) || SKIPPED_FILES.has(entry.name)) continue;
|
||||
|
||||
const sourcePath = path.join(sourceDir, entry.name);
|
||||
const targetPath = path.join(targetDir, entry.name);
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
await copyDirectory(sourcePath, targetPath);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!entry.isFile() || entry.name === "package.json") continue;
|
||||
await fs.mkdir(path.dirname(targetPath), { recursive: true });
|
||||
await fs.copyFile(sourcePath, targetPath);
|
||||
}
|
||||
await copyPackageContents({ sourceDir, targetDir, packageJson });
|
||||
|
||||
for (const name of localDeps) {
|
||||
const nestedSourceDir = workspacePackages.get(name);
|
||||
@@ -167,7 +155,7 @@ async function copyDirectory(sourceDir, targetDir) {
|
||||
await fs.mkdir(targetDir, { recursive: true });
|
||||
const entries = await fs.readdir(sourceDir, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
if (SKIPPED_DIRS.has(entry.name) || SKIPPED_FILES.has(entry.name)) continue;
|
||||
if (shouldSkipEntry(entry)) continue;
|
||||
|
||||
const sourcePath = path.join(sourceDir, entry.name);
|
||||
const targetPath = path.join(targetDir, entry.name);
|
||||
@@ -183,6 +171,102 @@ async function copyDirectory(sourceDir, targetDir) {
|
||||
}
|
||||
}
|
||||
|
||||
async function copyPackageContents({ sourceDir, targetDir, packageJson }) {
|
||||
const includedPaths = resolveIncludedPackagePaths(packageJson);
|
||||
if (!includedPaths) {
|
||||
const entries = await fs.readdir(sourceDir, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
if (shouldSkipEntry(entry)) continue;
|
||||
|
||||
const sourcePath = path.join(sourceDir, entry.name);
|
||||
const targetPath = path.join(targetDir, entry.name);
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
await copyDirectory(sourcePath, targetPath);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!entry.isFile() || entry.name === "package.json") continue;
|
||||
await fs.mkdir(path.dirname(targetPath), { recursive: true });
|
||||
await fs.copyFile(sourcePath, targetPath);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
for (const relativePath of includedPaths) {
|
||||
const sourcePath = path.join(sourceDir, relativePath);
|
||||
const targetPath = path.join(targetDir, relativePath);
|
||||
await copyPath(sourcePath, targetPath);
|
||||
}
|
||||
}
|
||||
|
||||
async function copyPath(sourcePath, targetPath) {
|
||||
let stat;
|
||||
try {
|
||||
stat = await fs.lstat(sourcePath);
|
||||
} catch (error) {
|
||||
if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") return;
|
||||
throw error;
|
||||
}
|
||||
|
||||
const name = path.basename(sourcePath);
|
||||
if (stat.isDirectory()) {
|
||||
if (shouldSkipName(name, { isDirectory: true })) return;
|
||||
await copyDirectory(sourcePath, targetPath);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!stat.isFile()) return;
|
||||
if (name === "package.json" || shouldSkipName(name, { isFile: true })) return;
|
||||
await fs.mkdir(path.dirname(targetPath), { recursive: true });
|
||||
await fs.copyFile(sourcePath, targetPath);
|
||||
}
|
||||
|
||||
function resolveIncludedPackagePaths(packageJson) {
|
||||
if (!Array.isArray(packageJson.files)) return null;
|
||||
|
||||
const includedPaths = [];
|
||||
for (const entry of packageJson.files) {
|
||||
if (typeof entry !== "string") continue;
|
||||
|
||||
const normalized = normalizeIncludedPath(entry);
|
||||
if (!normalized || normalized === "package.json") continue;
|
||||
includedPaths.push(normalized);
|
||||
}
|
||||
|
||||
return [...new Set(includedPaths)];
|
||||
}
|
||||
|
||||
function normalizeIncludedPath(entry) {
|
||||
const trimmed = entry.trim();
|
||||
if (!trimmed) return null;
|
||||
if (UNSUPPORTED_FILES_GLOB_PATTERN.test(trimmed)) {
|
||||
throw new Error(`Unsupported package.json files entry: ${entry}`);
|
||||
}
|
||||
|
||||
const normalized = path.posix.normalize(trimmed.replace(/\\/g, "/")).replace(/^(\.\/)+/, "");
|
||||
if (!normalized || normalized === ".") return null;
|
||||
if (path.posix.isAbsolute(normalized) || normalized.startsWith("../")) {
|
||||
throw new Error(`Package file entry must stay within the package root: ${entry}`);
|
||||
}
|
||||
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function shouldSkipEntry(entry) {
|
||||
return shouldSkipName(entry.name, {
|
||||
isDirectory: entry.isDirectory(),
|
||||
isFile: entry.isFile(),
|
||||
});
|
||||
}
|
||||
|
||||
function shouldSkipName(name, { isDirectory = false, isFile = false } = {}) {
|
||||
if (SKIPPED_DIRS.has(name) || SKIPPED_FILES.has(name)) return true;
|
||||
if (isDirectory && TEST_DIR_NAMES.has(name)) return true;
|
||||
if (isFile && (TEST_FILE_PATTERN.test(name) || CHANGELOG_FILE_PATTERN.test(name))) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
async function discoverWorkspacePackages(repoRoot) {
|
||||
const packagesRoot = path.join(repoRoot, "packages");
|
||||
const map = new Map();
|
||||
|
||||
@@ -31,6 +31,26 @@ test("syncSharedSkillPackages vendors workspace packages into skill scripts", as
|
||||
path.join(root, "packages", "baoyu-md", "src", "index.ts"),
|
||||
"export const markdown = true;\n",
|
||||
);
|
||||
await writeFile(
|
||||
path.join(root, "packages", "baoyu-md", "src", "index.test.ts"),
|
||||
"test('ignored', () => {});\n",
|
||||
);
|
||||
await writeFile(
|
||||
path.join(root, "packages", "baoyu-md", "src", "__tests__", "helper.ts"),
|
||||
"export const helper = true;\n",
|
||||
);
|
||||
await writeFile(
|
||||
path.join(root, "packages", "baoyu-md", "tests", "setup.ts"),
|
||||
"export const setup = true;\n",
|
||||
);
|
||||
await writeFile(
|
||||
path.join(root, "packages", "baoyu-md", ".changeset", "demo.md"),
|
||||
"---\n",
|
||||
);
|
||||
await writeFile(
|
||||
path.join(root, "packages", "baoyu-md", "CHANGELOG.md"),
|
||||
"# changelog\n",
|
||||
);
|
||||
|
||||
const consumerDir = path.join(root, "skills", "demo-skill", "scripts");
|
||||
await writeJson(path.join(consumerDir, "package.json"), {
|
||||
@@ -67,4 +87,97 @@ test("syncSharedSkillPackages vendors workspace packages into skill scripts", as
|
||||
"utf8",
|
||||
);
|
||||
assert.match(vendoredFile, /markdown = true/);
|
||||
|
||||
await assert.rejects(
|
||||
fs.readFile(path.join(consumerDir, "vendor", "baoyu-md", "src", "index.test.ts"), "utf8"),
|
||||
{ code: "ENOENT" },
|
||||
);
|
||||
await assert.rejects(
|
||||
fs.readFile(path.join(consumerDir, "vendor", "baoyu-md", "src", "__tests__", "helper.ts"), "utf8"),
|
||||
{ code: "ENOENT" },
|
||||
);
|
||||
await assert.rejects(
|
||||
fs.readFile(path.join(consumerDir, "vendor", "baoyu-md", "tests", "setup.ts"), "utf8"),
|
||||
{ code: "ENOENT" },
|
||||
);
|
||||
await assert.rejects(
|
||||
fs.readFile(path.join(consumerDir, "vendor", "baoyu-md", ".changeset", "demo.md"), "utf8"),
|
||||
{ code: "ENOENT" },
|
||||
);
|
||||
await assert.rejects(
|
||||
fs.readFile(path.join(consumerDir, "vendor", "baoyu-md", "CHANGELOG.md"), "utf8"),
|
||||
{ code: "ENOENT" },
|
||||
);
|
||||
});
|
||||
|
||||
test("syncSharedSkillPackages respects package.json files allowlists", async (t) => {
|
||||
const root = await makeTempDir("baoyu-sync-files-");
|
||||
t.after(() => fs.rm(root, { recursive: true, force: true }));
|
||||
|
||||
await writeJson(path.join(root, "packages", "demo-pkg", "package.json"), {
|
||||
name: "demo-pkg",
|
||||
version: "1.0.0",
|
||||
files: ["README.md", "src", "CHANGELOG.md", ".changeset"],
|
||||
});
|
||||
await writeFile(
|
||||
path.join(root, "packages", "demo-pkg", "README.md"),
|
||||
"# Demo\n",
|
||||
);
|
||||
await writeFile(
|
||||
path.join(root, "packages", "demo-pkg", "src", "index.ts"),
|
||||
"export const demo = true;\n",
|
||||
);
|
||||
await writeFile(
|
||||
path.join(root, "packages", "demo-pkg", "src", "index.test.ts"),
|
||||
"test('ignored', () => {});\n",
|
||||
);
|
||||
await writeFile(
|
||||
path.join(root, "packages", "demo-pkg", "docs", "private.md"),
|
||||
"private\n",
|
||||
);
|
||||
await writeFile(
|
||||
path.join(root, "packages", "demo-pkg", "CHANGELOG.md"),
|
||||
"# changelog\n",
|
||||
);
|
||||
await writeFile(
|
||||
path.join(root, "packages", "demo-pkg", ".changeset", "demo.md"),
|
||||
"---\n",
|
||||
);
|
||||
|
||||
const consumerDir = path.join(root, "skills", "demo-skill", "scripts");
|
||||
await writeJson(path.join(consumerDir, "package.json"), {
|
||||
name: "demo-skill-scripts",
|
||||
version: "1.0.0",
|
||||
dependencies: {
|
||||
"demo-pkg": "^1.0.0",
|
||||
},
|
||||
});
|
||||
|
||||
await syncSharedSkillPackages(root, { install: false });
|
||||
|
||||
const readme = await fs.readFile(path.join(consumerDir, "vendor", "demo-pkg", "README.md"), "utf8");
|
||||
assert.match(readme, /Demo/);
|
||||
|
||||
const vendoredSource = await fs.readFile(
|
||||
path.join(consumerDir, "vendor", "demo-pkg", "src", "index.ts"),
|
||||
"utf8",
|
||||
);
|
||||
assert.match(vendoredSource, /demo = true/);
|
||||
|
||||
await assert.rejects(
|
||||
fs.readFile(path.join(consumerDir, "vendor", "demo-pkg", "docs", "private.md"), "utf8"),
|
||||
{ code: "ENOENT" },
|
||||
);
|
||||
await assert.rejects(
|
||||
fs.readFile(path.join(consumerDir, "vendor", "demo-pkg", "CHANGELOG.md"), "utf8"),
|
||||
{ code: "ENOENT" },
|
||||
);
|
||||
await assert.rejects(
|
||||
fs.readFile(path.join(consumerDir, "vendor", "demo-pkg", ".changeset", "demo.md"), "utf8"),
|
||||
{ code: "ENOENT" },
|
||||
);
|
||||
await assert.rejects(
|
||||
fs.readFile(path.join(consumerDir, "vendor", "demo-pkg", "src", "index.test.ts"), "utf8"),
|
||||
{ code: "ENOENT" },
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user