SW-3467 Copy static files via build scripts * add file copy script and add all fonts to design-system * add file copy script and add all fonts to design-system * add file copy script and add all fonts to design-system * remove fonts that will be copied via build scripts * wip * update paths to shared files * update material-symbol script * merge * fix missing shared segment for path in fonts.css Approved-by: Linus Flood
174 lines
5.2 KiB
TypeScript
174 lines
5.2 KiB
TypeScript
#!/usr/bin/env tsx
|
|
import { Command } from "commander";
|
|
import { promises as fs } from "node:fs";
|
|
import * as path from "node:path";
|
|
|
|
async function ensureDir(dir: string): Promise<void> {
|
|
await fs.mkdir(dir, { recursive: true });
|
|
}
|
|
|
|
async function copyDir(
|
|
src: string,
|
|
dest: string,
|
|
includeExts: string[] = [],
|
|
excludeExts: string[] = []
|
|
): Promise<void> {
|
|
await ensureDir(dest);
|
|
const entries = await fs.readdir(src, { withFileTypes: true });
|
|
for (const entry of entries) {
|
|
const srcPath = path.join(src, entry.name);
|
|
const destPath = path.join(dest, entry.name);
|
|
if (entry.isDirectory()) {
|
|
await copyDir(srcPath, destPath, includeExts, excludeExts);
|
|
} else if (entry.isFile()) {
|
|
if (!shouldCopyFile(entry.name, includeExts, excludeExts)) {
|
|
continue;
|
|
}
|
|
await fs.copyFile(srcPath, destPath);
|
|
}
|
|
}
|
|
}
|
|
|
|
function parseExtList(list?: string): string[] {
|
|
if (!list) return [];
|
|
return list
|
|
.split(",")
|
|
.map((s) => s.trim())
|
|
.filter(Boolean)
|
|
.map((e) =>
|
|
e.startsWith(".") ? e.toLowerCase() : `.${e.toLowerCase()}`
|
|
);
|
|
}
|
|
|
|
function isMultiPartPattern(pattern: string): boolean {
|
|
// true for patterns like '.d.ts' or '.spec.ts' (contains an extra dot after the leading one)
|
|
return pattern.slice(1).includes(".");
|
|
}
|
|
|
|
function matchesPattern(fileNameLower: string, pattern: string): boolean {
|
|
if (isMultiPartPattern(pattern)) {
|
|
// match by filename suffix for multi-segment patterns
|
|
return fileNameLower.endsWith(pattern);
|
|
}
|
|
// single-segment patterns: compare the file's last extension exactly (prevents '.ts' matching '.tsx')
|
|
return path.extname(fileNameLower) === pattern;
|
|
}
|
|
|
|
function shouldCopyFile(
|
|
fileName: string,
|
|
includePatterns: string[],
|
|
excludePatterns: string[]
|
|
): boolean {
|
|
const fileLower = fileName.toLowerCase();
|
|
|
|
// Exclude takes precedence
|
|
if (
|
|
excludePatterns.length > 0 &&
|
|
excludePatterns.some((p) => matchesPattern(fileLower, p))
|
|
) {
|
|
return false;
|
|
}
|
|
|
|
// If no include patterns provided, include everything not excluded
|
|
if (includePatterns.length === 0) return true;
|
|
|
|
// Otherwise include only if at least one include pattern matches
|
|
return includePatterns.some((p) => matchesPattern(fileLower, p));
|
|
}
|
|
|
|
async function run(
|
|
sourceArg: string,
|
|
destArg: string,
|
|
includeExts: string[] = [],
|
|
excludeExts: string[] = []
|
|
): Promise<{ src: string; dest: string }> {
|
|
const src = path.resolve(process.cwd(), sourceArg);
|
|
const dest = path.resolve(process.cwd(), destArg);
|
|
|
|
// Validate source exists
|
|
let stat: any;
|
|
try {
|
|
stat = await fs.stat(src);
|
|
} catch {
|
|
throw new Error(`Source path does not exist: ${src}`);
|
|
}
|
|
if (!stat.isDirectory()) {
|
|
throw new Error(`Source must be a directory: ${src}`);
|
|
}
|
|
|
|
// Prevent copying into source or a parent of source
|
|
const rel = path.relative(src, dest);
|
|
if (!rel || !rel.startsWith("..")) {
|
|
throw new Error(
|
|
"Destination must not be inside (or be) the source directory."
|
|
);
|
|
}
|
|
|
|
await ensureDir(dest);
|
|
await copyDir(src, dest, includeExts, excludeExts);
|
|
return { src, dest };
|
|
}
|
|
|
|
const program = new Command();
|
|
|
|
program
|
|
.name("copy-files")
|
|
.description(
|
|
"Recursively copy files from a source directory to a destination directory."
|
|
)
|
|
.argument("<source-path>", "Source directory")
|
|
.argument("<destination-path>", "Destination directory")
|
|
.option("-q, --quiet", "Suppress success output")
|
|
.option(
|
|
"--include-ext <exts>",
|
|
"Comma-separated list of file extensions to include, e.g. '.js,.css' or 'js,css'"
|
|
)
|
|
.option(
|
|
"--exclude-ext <exts>",
|
|
"Comma-separated list of file extensions to exclude, e.g. '.map,.tmp' or 'map,tmp'"
|
|
)
|
|
.action(
|
|
async (
|
|
sourcePath: string,
|
|
destinationPath: string,
|
|
options: {
|
|
quiet?: boolean;
|
|
includeExt?: string;
|
|
excludeExt?: string;
|
|
}
|
|
) => {
|
|
try {
|
|
const includeExts = parseExtList(options.includeExt);
|
|
const excludeExts = parseExtList(options.excludeExt);
|
|
const result = await run(
|
|
sourcePath,
|
|
destinationPath,
|
|
includeExts,
|
|
excludeExts
|
|
);
|
|
if (!options.quiet) {
|
|
console.log(
|
|
`Copied files from "${result.src}" to "${result.dest}".`
|
|
);
|
|
}
|
|
} catch (err) {
|
|
console.error(err instanceof Error ? err.message : String(err));
|
|
process.exit(1);
|
|
}
|
|
}
|
|
);
|
|
|
|
// Show examples in help
|
|
program.addHelpText(
|
|
"after",
|
|
`
|
|
Examples:
|
|
tsx scripts/copyFiles.ts static/fonts dist/fonts
|
|
tsx scripts/copyFiles.ts assets/images build/images
|
|
tsx scripts/copyFiles.ts src dist --include-ext .js,.css
|
|
tsx scripts/copyFiles.ts src dist --exclude-ext map,tmp
|
|
`
|
|
);
|
|
|
|
program.parseAsync(process.argv);
|