@controlplane/cli
Version:
Control Plane Corporation CLI
239 lines • 9.93 kB
JavaScript
;
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