import { path, FileSystem } from '@servicenow/sdk-build-core' import ignore, { type Ignore } from 'ignore' /** Name of the ignore file honored, relative to the project root. */ export const NOW_PACK_IGNORE_FILE = '.nowpackignore' /** * Builds an {@link Ignore} matcher from `.nowpackignore` file contents. * * The contents are parsed with the canonical `ignore` library, so full `.gitignore` semantics * are honored — anchoring, directory-only (`dir/`) patterns, negation (`!`), the parent-directory * re-inclusion rule, character ranges, and escaping. */ export function parseNowPackIgnore(content: string): Ignore { return ignore().add(content) } /** * Strips a single leading separator so an entry is safe to pass to the `ignore` matcher. * * This is deliberately NOT a general path-normalization helper — it does not resolve or * collapse `..` segments. `sourceFiles` are POSIX project-relative paths, so no backslash * conversion is needed either; only the leading separator is removed. * * This intentionally does not use `path.join`/`path.resolve`/`path.relative` from build-core: * those resolve (and therefore silently collapse) `..` segments, which would hide a source file * path that escapes the project root instead of letting the `ignore` matcher raise on it. */ function stripLeadingSeparatorForIgnoreMatch(relativePath: string): string { return relativePath.replace(/^[/\\]/, '') } /** * Builds a keep/drop predicate from the project's `.nowpackignore` file. * * The `.nowpackignore` file is read from `rootDir` and honored like a `.gitignore`: matching * paths are dropped, while `!`-prefixed patterns can re-include previously ignored paths. * * The `.nowpackignore` file itself is always dropped so it is never bundled back into the zip, * regardless of the rules it contains. * * @returns a predicate that returns `true` for files that should be kept. When no * `.nowpackignore` file exists (or it defines no rules), the predicate keeps every file * except the ignore file itself. * @throws {Error} if a `.nowpackignore` file is present but cannot be read, or if its contents * cannot be parsed into a matcher — both fail the build rather than silently applying no rules. */ export function createNowPackIgnoreFilter(fs: FileSystem, rootDir: string): (relativePath: string) => boolean { const ignorePath = path.join(rootDir, NOW_PACK_IGNORE_FILE) // The ignore file itself must never be bundled, even when it has no rules or cannot be read. const isIgnoreFile = (normalized: string): boolean => normalized === NOW_PACK_IGNORE_FILE if (!FileSystem.existsSync(fs, ignorePath)) { return (relativePath: string) => !isIgnoreFile(stripLeadingSeparatorForIgnoreMatch(relativePath)) } // A present-but-unreadable .nowpackignore fails hard rather than silently applying no // exclusions — quietly keeping every file could bundle sources the author meant to drop. const content = fs.readFileSync(ignorePath, { encoding: 'utf-8' }).toString() const matcher = parseNowPackIgnore(content) return (relativePath: string): boolean => { const normalized = stripLeadingSeparatorForIgnoreMatch(relativePath) // Empty paths are meaningless to `ignore` (and it throws on them); keep them. if (normalized.length === 0) { return true } if (isIgnoreFile(normalized)) { return false } return !matcher.ignores(normalized) } }