mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-14 15:16:35 +03:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
53cb5c8cf7 |
@@ -0,0 +1,37 @@
|
|||||||
|
/**
|
||||||
|
* Convert a live-config glob pattern to a RegExp.
|
||||||
|
*
|
||||||
|
* Supports `**` across path segments, `*` within one segment, and `?` for one
|
||||||
|
* character. Callers normalize project-relative paths to forward slashes.
|
||||||
|
*/
|
||||||
|
export function livePathGlobToRegex(pattern) {
|
||||||
|
let re = '';
|
||||||
|
let i = 0;
|
||||||
|
while (i < pattern.length) {
|
||||||
|
const c = pattern[i];
|
||||||
|
if (c === '*') {
|
||||||
|
if (pattern[i + 1] === '*') {
|
||||||
|
if (pattern[i + 2] === '/') {
|
||||||
|
re += '(?:.*/)?';
|
||||||
|
i += 3;
|
||||||
|
} else {
|
||||||
|
re += '.*';
|
||||||
|
i += 2;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
re += '[^/]*';
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
} else if (c === '?') {
|
||||||
|
re += '[^/]';
|
||||||
|
i += 1;
|
||||||
|
} else if (/[.+^${}()|[\]\\]/.test(c)) {
|
||||||
|
re += `\\${c}`;
|
||||||
|
i += 1;
|
||||||
|
} else {
|
||||||
|
re += c;
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new RegExp(`^${re}$`);
|
||||||
|
}
|
||||||
@@ -27,6 +27,7 @@ import fs from 'node:fs';
|
|||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import { fileURLToPath } from 'node:url';
|
import { fileURLToPath } from 'node:url';
|
||||||
import { resolveLiveConfigPath } from './lib/impeccable-paths.mjs';
|
import { resolveLiveConfigPath } from './lib/impeccable-paths.mjs';
|
||||||
|
import { livePathGlobToRegex } from './lib/live-path-globs.mjs';
|
||||||
import {
|
import {
|
||||||
describeInjectArtifacts,
|
describeInjectArtifacts,
|
||||||
frameworkIgnorePatterns,
|
frameworkIgnorePatterns,
|
||||||
@@ -364,7 +365,7 @@ export function resolveFiles(rootDir, config) {
|
|||||||
const patterns = config.files;
|
const patterns = config.files;
|
||||||
const userExcludes = Array.isArray(config.exclude) ? config.exclude : [];
|
const userExcludes = Array.isArray(config.exclude) ? config.exclude : [];
|
||||||
const allExcludes = [...HARD_EXCLUDES, ...userExcludes];
|
const allExcludes = [...HARD_EXCLUDES, ...userExcludes];
|
||||||
const excludeRegexes = allExcludes.map(globToRegex);
|
const excludeRegexes = allExcludes.map(livePathGlobToRegex);
|
||||||
|
|
||||||
const isExcluded = (relPath) => excludeRegexes.some((re) => re.test(relPath));
|
const isExcluded = (relPath) => excludeRegexes.some((re) => re.test(relPath));
|
||||||
const isGlob = (s) => /[*?[]/.test(s);
|
const isGlob = (s) => /[*?[]/.test(s);
|
||||||
@@ -401,47 +402,6 @@ export function resolveFiles(rootDir, config) {
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert a glob pattern to a RegExp. Supports:
|
|
||||||
* ** → any number of path segments (including zero)
|
|
||||||
* * → any chars except `/`
|
|
||||||
* ? → any single char except `/`
|
|
||||||
* Paths are normalized to forward slashes before matching.
|
|
||||||
*/
|
|
||||||
function globToRegex(pattern) {
|
|
||||||
let re = '';
|
|
||||||
let i = 0;
|
|
||||||
while (i < pattern.length) {
|
|
||||||
const c = pattern[i];
|
|
||||||
if (c === '*') {
|
|
||||||
if (pattern[i + 1] === '*') {
|
|
||||||
// ** — any number of segments, including zero. Handle the common
|
|
||||||
// **/ and /** forms so `a/**/b` matches `a/b` as well as `a/x/y/b`.
|
|
||||||
if (pattern[i + 2] === '/') {
|
|
||||||
re += '(?:.*/)?';
|
|
||||||
i += 3;
|
|
||||||
} else {
|
|
||||||
re += '.*';
|
|
||||||
i += 2;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
re += '[^/]*';
|
|
||||||
i += 1;
|
|
||||||
}
|
|
||||||
} else if (c === '?') {
|
|
||||||
re += '[^/]';
|
|
||||||
i += 1;
|
|
||||||
} else if (/[.+^${}()|[\]\\]/.test(c)) {
|
|
||||||
re += '\\' + c;
|
|
||||||
i += 1;
|
|
||||||
} else {
|
|
||||||
re += c;
|
|
||||||
i += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return new RegExp('^' + re + '$');
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Core operations
|
// Core operations
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
+2
-33
@@ -24,6 +24,7 @@ import { fileURLToPath } from 'node:url';
|
|||||||
import { resolveTargetSelection } from './context.mjs';
|
import { resolveTargetSelection } from './context.mjs';
|
||||||
import { resolveFiles } from './live-inject.mjs';
|
import { resolveFiles } from './live-inject.mjs';
|
||||||
import { readLiveServerInfo } from './lib/impeccable-paths.mjs';
|
import { readLiveServerInfo } from './lib/impeccable-paths.mjs';
|
||||||
|
import { livePathGlobToRegex } from './lib/live-path-globs.mjs';
|
||||||
import { resolveSurfaceBrief } from './lib/surface-briefs.mjs';
|
import { resolveSurfaceBrief } from './lib/surface-briefs.mjs';
|
||||||
import { resolveLiveTarget } from './live-target.mjs';
|
import { resolveLiveTarget } from './live-target.mjs';
|
||||||
import { bootInstructions } from './live/instructions.mjs';
|
import { bootInstructions } from './live/instructions.mjs';
|
||||||
@@ -240,7 +241,7 @@ function scanForDrift(rootDir, resolvedFiles, config) {
|
|||||||
// Files matching the user's `exclude` globs are intentional omissions,
|
// Files matching the user's `exclude` globs are intentional omissions,
|
||||||
// not drift. Compile them to regexes so the orphan list stays signal.
|
// not drift. Compile them to regexes so the orphan list stays signal.
|
||||||
const userExcludeRegexes = (Array.isArray(config.exclude) ? config.exclude : [])
|
const userExcludeRegexes = (Array.isArray(config.exclude) ? config.exclude : [])
|
||||||
.map((p) => globToRegex(p));
|
.map(livePathGlobToRegex);
|
||||||
const isUserExcluded = (rel) => userExcludeRegexes.some((re) => re.test(rel));
|
const isUserExcluded = (rel) => userExcludeRegexes.some((re) => re.test(rel));
|
||||||
|
|
||||||
const orphans = [];
|
const orphans = [];
|
||||||
@@ -278,38 +279,6 @@ function scanForDrift(rootDir, resolvedFiles, config) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Same glob-to-regex mapping used by live-inject.mjs. Kept inline here
|
|
||||||
* to avoid a circular import (live-inject.mjs already imports nothing
|
|
||||||
* from live.mjs). The two must stay in sync.
|
|
||||||
*/
|
|
||||||
function globToRegex(pattern) {
|
|
||||||
let re = '';
|
|
||||||
let i = 0;
|
|
||||||
while (i < pattern.length) {
|
|
||||||
const c = pattern[i];
|
|
||||||
if (c === '*') {
|
|
||||||
if (pattern[i + 1] === '*') {
|
|
||||||
if (pattern[i + 2] === '/') { re += '(?:.*/)?'; i += 3; }
|
|
||||||
else { re += '.*'; i += 2; }
|
|
||||||
} else {
|
|
||||||
re += '[^/]*';
|
|
||||||
i += 1;
|
|
||||||
}
|
|
||||||
} else if (c === '?') {
|
|
||||||
re += '[^/]';
|
|
||||||
i += 1;
|
|
||||||
} else if (/[.+^${}()|[\]\\]/.test(c)) {
|
|
||||||
re += '\\' + c;
|
|
||||||
i += 1;
|
|
||||||
} else {
|
|
||||||
re += c;
|
|
||||||
i += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return new RegExp('^' + re + '$');
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Helpers
|
// Helpers
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -10,10 +10,38 @@ import { dirname, join, relative, resolve } from 'node:path';
|
|||||||
import { tmpdir } from 'node:os';
|
import { tmpdir } from 'node:os';
|
||||||
import { fileURLToPath } from 'node:url';
|
import { fileURLToPath } from 'node:url';
|
||||||
import { execFileSync } from 'node:child_process';
|
import { execFileSync } from 'node:child_process';
|
||||||
|
import { livePathGlobToRegex } from '../skill/scripts/lib/live-path-globs.mjs';
|
||||||
|
|
||||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||||
const INJECT = resolve(__dirname, '..', 'skill/scripts/live-inject.mjs');
|
const INJECT = resolve(__dirname, '..', 'skill/scripts/live-inject.mjs');
|
||||||
|
|
||||||
|
describe('live path globs', () => {
|
||||||
|
it('matches recursive segments, including zero segments', () => {
|
||||||
|
const anywhere = livePathGlobToRegex('**/index.html');
|
||||||
|
assert.equal(anywhere.test('index.html'), true);
|
||||||
|
assert.equal(anywhere.test('public/index.html'), true);
|
||||||
|
assert.equal(anywhere.test('apps/web/public/index.html'), true);
|
||||||
|
|
||||||
|
const underPublic = livePathGlobToRegex('public/**/*.html');
|
||||||
|
assert.equal(underPublic.test('public/index.html'), true);
|
||||||
|
assert.equal(underPublic.test('public/docs/index.html'), true);
|
||||||
|
assert.equal(underPublic.test('src/index.html'), false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps single-star and question-mark matches inside one segment', () => {
|
||||||
|
const pattern = livePathGlobToRegex('pages/*/item?.html');
|
||||||
|
assert.equal(pattern.test('pages/docs/item1.html'), true);
|
||||||
|
assert.equal(pattern.test('pages/docs/deep/item1.html'), false);
|
||||||
|
assert.equal(pattern.test('pages/docs/item12.html'), false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('treats regular-expression punctuation as literal path text', () => {
|
||||||
|
const pattern = livePathGlobToRegex('pages/[draft]/item+.html');
|
||||||
|
assert.equal(pattern.test('pages/[draft]/item+.html'), true);
|
||||||
|
assert.equal(pattern.test('pages/d/itemm.html'), false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
function runInject(cwd, configPath, args) {
|
function runInject(cwd, configPath, args) {
|
||||||
try {
|
try {
|
||||||
const out = execFileSync('node', [INJECT, ...args], {
|
const out = execFileSync('node', [INJECT, ...args], {
|
||||||
|
|||||||
Reference in New Issue
Block a user