UNPKG

@controlplane/cli

Version:

Control Plane Corporation CLI

239 lines 9.93 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.RemoteIgnorePolicy = void 0; exports.readIgnoreFileText = readIgnoreFileText; const ignore = require("ignore"); const fs = require("fs"); const path = require("path"); const dockerignore_1 = require("@balena/dockerignore"); const config_1 = require("../config"); const errors_1 = require("../errors"); // ANCHOR - Constants // The build cannot run without these, so no rule may exclude them (docker's own rule). const FORCE_INCLUDED = new Set(['Dockerfile', '.dockerignore']); // ANCHOR - RemoteIgnorePolicy /** * Decides which folder entries enter the build context. Exactly one rule system * governs a folder: a .dockerignore selects docker semantics and .gitignore files are * not consulted; otherwise .gitignore files select git semantics (nested files * honored); otherwise only the baseline applies. Matching is delegated to the * reference engines (@balena/dockerignore and ignore), both validated against real * `docker build` and `git check-ignore` behavior by the conformance corpus. */ class RemoteIgnorePolicy { constructor(mode, dockerMatcher, gitScopes, hasUserNegations) { this.mode = mode; this.dockerMatcher = dockerMatcher; this.gitScopes = gitScopes; this.hasUserNegations = hasUserNegations; } // Public Static Methods // /** * Builds the policy for a folder from its root ignore files. A .dockerignore * (even an empty one) selects docker mode; otherwise a .gitignore selects git * mode; otherwise only the baseline applies. * * @param {string} root - The absolute folder root. * @returns {RemoteIgnorePolicy} The compiled policy. */ static fromDir(root) { const dockerignorePath = path.join(root, '.dockerignore'); if (fs.existsSync(dockerignorePath)) { return RemoteIgnorePolicy.fromRules({ kind: 'docker', content: readIgnoreFile(dockerignorePath), source: '.dockerignore' }); } const gitignorePath = path.join(root, '.gitignore'); if (fs.existsSync(gitignorePath)) { return RemoteIgnorePolicy.fromRules({ kind: 'git', scopes: [{ dir: '', content: readIgnoreFile(gitignorePath), source: '.gitignore' }], }); } return RemoteIgnorePolicy.fromRules({ kind: 'none' }); } /** * Builds the policy from explicit rules. The conformance corpus drives this * directly; fromDir delegates here. * * @param {IgnoreRules} rules - The rule system and its file contents. * @returns {RemoteIgnorePolicy} The compiled policy. */ static fromRules(rules) { var _a; if (rules.kind === 'docker') { assertRuleComplexity(rules.content, rules.source); const baseline = config_1.BASELINE_EXCLUDES.map((name) => `**/${name}`); const matcher = (0, dockerignore_1.default)({ ignorecase: false }).add(baseline).add(rules.content); return new RemoteIgnorePolicy('docker', matcher, [], hasNegations(rules.content)); } if (rules.kind === 'git') { const root = rules.scopes.find((scope) => scope.dir === ''); if (root) { assertRuleComplexity(root.content, root.source); } const rootScope = { dir: '', matcher: ignore() .add(config_1.BASELINE_EXCLUDES) .add((_a = root === null || root === void 0 ? void 0 : root.content) !== null && _a !== void 0 ? _a : ''), }; const policy = new RemoteIgnorePolicy('git', null, [rootScope], false); for (const scope of rules.scopes) { if (scope.dir !== '') { policy.addGitScope(scope.dir, scope.content, scope.source); } } return policy; } return new RemoteIgnorePolicy('none', null, [{ dir: '', matcher: ignore().add(config_1.BASELINE_EXCLUDES) }], false); } // Public Methods // /** * Adds a nested .gitignore discovered during the walk. Its rules govern only paths * beneath its directory and override shallower scopes there, as git does. Only * meaningful in git mode; other modes never consult nested files. * * @param {string} dir - The scope directory, POSIX-relative to the folder root. * @param {string} content - The raw .gitignore content. * @param {string} source - The file's path for error messages. * @returns {void} */ addGitScope(dir, content, source) { if (this.mode !== 'git') { return; } assertRuleComplexity(content, source); this.gitScopes.push({ dir, matcher: ignore().add(content) }); } /** * Reports whether a folder entry is excluded from the build context. The path is * POSIX-relative to the folder root, never absolute and never containing "..". * * @param {string} relPath - The entry's POSIX-relative path. * @param {boolean} isDir - Whether the entry is a directory. * @returns {boolean} True when the entry must not enter the context. */ excludes(relPath, isDir) { if (FORCE_INCLUDED.has(relPath)) { return false; } if (this.mode === 'docker') { return this.dockerMatcher.ignores(relPath); } // Deeper scopes override shallower verdicts, matching git's nearest-file-wins rule. let verdict = false; for (const scope of this.gitScopes) { const scoped = relativeToScope(relPath, scope.dir); if (scoped === null) { continue; } const result = scope.matcher.test(isDir ? scoped + '/' : scoped); if (result.ignored) { verdict = true; } else if (result.unignored) { verdict = false; } } return verdict; } /** * Reports whether the walker must descend into excluded directories anyway. True * only in docker mode with user negations, where a `!` rule can re-include a path * beneath an excluded directory. Git semantics never re-include under an excluded * directory, so git-mode walks prune them outright. * * @returns {boolean} True when excluded directories must still be walked. */ mayReincludeBeneath() { return this.mode === 'docker' && this.hasUserNegations; } } exports.RemoteIgnorePolicy = RemoteIgnorePolicy; // ANCHOR - Exported Functions /** * Reads an ignore file as UTF-8, stripping a leading BOM. Filesystem failures * propagate raw, so each caller attaches the context its own error needs. * * @param {string} filePath - The ignore file path. * @returns {string} The file content. */ function readIgnoreFileText(filePath) { return fs.readFileSync(filePath, 'utf8').replace(/^\uFEFF/, ''); } // SECTION - Functions /** * Reads a root ignore file, wrapping filesystem failures. * * @param {string} filePath - The ignore file path. * @returns {string} The file content. */ function readIgnoreFile(filePath) { try { return readIgnoreFileText(filePath); } catch (e) { const message = e instanceof Error ? e.message : String(e); throw new errors_1.RemoteBuildError('context', `could not read ${path.basename(filePath)} (${message})`, 'Fix the file permissions and re-run.'); } } /** * Reports whether any rule line is a negation. * * @param {string} content - The raw ignore file content. * @returns {boolean} True when a `!` rule is present. */ function hasNegations(content) { return ruleLines(content).some((line) => line.startsWith('!')); } /** * Splits ignore file content into its rule lines, dropping blanks and comments. * * @param {string} content - The raw ignore file content. * @returns {string[]} The trimmed rule lines. */ function ruleLines(content) { return content .split('\n') .map((line) => line.trim()) .filter((line) => line !== '' && !line.startsWith('#')); } /** * Rejects rules that would catastrophically backtrack in the engines' compiled regexes. * Each maximal run of `*` is one backtracking token, and the gitignore engine treats a * `**` adjacent to a literal as an ordinary run rather than a structural token, so runs * per path segment — not single stars — are what the cap counts. * * @param {string} content - The raw ignore file content. * @param {string} source - The ignore file's path for the error message. * @returns {void} */ function assertRuleComplexity(content, source) { var _a; for (const line of ruleLines(content)) { const body = line.replace(/^!/, ''); for (const segment of body.split('/')) { const runs = ((_a = segment.match(/\*+/g)) !== null && _a !== void 0 ? _a : []).length; if (runs > config_1.MAX_WILDCARD_RUNS_PER_SEGMENT) { throw new errors_1.RemoteBuildError('context', `ignore pattern "${line}" in ${source} is too complex (more than ${config_1.MAX_WILDCARD_RUNS_PER_SEGMENT} wildcard groups in one path segment)`, 'Simplify the pattern and re-run the build.'); } } } } /** * Rebases a path onto a scope directory, or reports the path is outside the scope. * * @param {string} relPath - The POSIX-relative path from the folder root. * @param {string} scopeDir - The scope directory, '' for the root scope. * @returns {string | null} The path relative to the scope, or null when outside it. */ function relativeToScope(relPath, scopeDir) { if (scopeDir === '') { return relPath; } if (!relPath.startsWith(scopeDir + '/')) { return null; } return relPath.slice(scopeDir.length + 1); } // !SECTION //# sourceMappingURL=ignore.js.map