Files
web/scripts/copyFiles.ts
Joakim Jäderberg 48324ef935 Merged in feature/copy-static-files-via-build-scripts (pull request #2798)
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
2025-09-16 10:59:33 +00:00

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);