mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-12 14:16:28 +03:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
93dce3d62e |
@@ -1,37 +0,0 @@
|
||||
/**
|
||||
* 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,7 +27,6 @@ import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { resolveLiveConfigPath } from './lib/impeccable-paths.mjs';
|
||||
import { livePathGlobToRegex } from './lib/live-path-globs.mjs';
|
||||
import {
|
||||
describeInjectArtifacts,
|
||||
frameworkIgnorePatterns,
|
||||
@@ -365,7 +364,7 @@ export function resolveFiles(rootDir, config) {
|
||||
const patterns = config.files;
|
||||
const userExcludes = Array.isArray(config.exclude) ? config.exclude : [];
|
||||
const allExcludes = [...HARD_EXCLUDES, ...userExcludes];
|
||||
const excludeRegexes = allExcludes.map(livePathGlobToRegex);
|
||||
const excludeRegexes = allExcludes.map(globToRegex);
|
||||
|
||||
const isExcluded = (relPath) => excludeRegexes.some((re) => re.test(relPath));
|
||||
const isGlob = (s) => /[*?[]/.test(s);
|
||||
@@ -402,6 +401,47 @@ export function resolveFiles(rootDir, config) {
|
||||
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
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
+33
-2
@@ -24,7 +24,6 @@ import { fileURLToPath } from 'node:url';
|
||||
import { resolveTargetSelection } from './context.mjs';
|
||||
import { resolveFiles } from './live-inject.mjs';
|
||||
import { readLiveServerInfo } from './lib/impeccable-paths.mjs';
|
||||
import { livePathGlobToRegex } from './lib/live-path-globs.mjs';
|
||||
import { resolveSurfaceBrief } from './lib/surface-briefs.mjs';
|
||||
import { resolveLiveTarget } from './live-target.mjs';
|
||||
import { bootInstructions } from './live/instructions.mjs';
|
||||
@@ -241,7 +240,7 @@ function scanForDrift(rootDir, resolvedFiles, config) {
|
||||
// Files matching the user's `exclude` globs are intentional omissions,
|
||||
// not drift. Compile them to regexes so the orphan list stays signal.
|
||||
const userExcludeRegexes = (Array.isArray(config.exclude) ? config.exclude : [])
|
||||
.map(livePathGlobToRegex);
|
||||
.map((p) => globToRegex(p));
|
||||
const isUserExcluded = (rel) => userExcludeRegexes.some((re) => re.test(rel));
|
||||
|
||||
const orphans = [];
|
||||
@@ -279,6 +278,38 @@ 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
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -11,6 +11,8 @@ import crypto from 'node:crypto';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
import { firstExistingFile, hasAnyDependency } from './frameworks/detect-utils.mjs';
|
||||
|
||||
export const SVELTE_LIVE_ROOT_COMPONENT = 'src/lib/impeccable/ImpeccableLiveRoot.svelte';
|
||||
export const SVELTE_LAYOUT_MARKER_OPEN = '<!-- impeccable-live-svelte-start -->';
|
||||
export const SVELTE_LAYOUT_MARKER_CLOSE = '<!-- impeccable-live-svelte-end -->';
|
||||
@@ -45,11 +47,17 @@ export function detectSvelteKitProject(cwd = process.cwd(), config = null) {
|
||||
&& fileIncludes(path.join(cwd, appHtml), '%sveltekit.head%');
|
||||
if (!hasTemplateMarkers) return null;
|
||||
|
||||
const hasSvelteConfig = fs.existsSync(path.join(cwd, 'svelte.config.js'))
|
||||
|| fs.existsSync(path.join(cwd, 'svelte.config.mjs'))
|
||||
|| fs.existsSync(path.join(cwd, 'svelte.config.cjs'))
|
||||
|| fs.existsSync(path.join(cwd, 'svelte.config.ts'));
|
||||
const hasKitPackage = packageHasSvelteKit(cwd);
|
||||
const hasSvelteConfig = Boolean(firstExistingFile(cwd, [
|
||||
'svelte.config.js',
|
||||
'svelte.config.mjs',
|
||||
'svelte.config.cjs',
|
||||
'svelte.config.ts',
|
||||
]));
|
||||
const hasKitPackage = hasAnyDependency(cwd, [
|
||||
'@sveltejs/kit',
|
||||
'@sveltejs/vite-plugin-svelte',
|
||||
'svelte',
|
||||
]);
|
||||
if (!hasSvelteConfig && !hasKitPackage) return null;
|
||||
|
||||
return {
|
||||
@@ -260,36 +268,16 @@ function findSvelteKitAppHtml(cwd, config) {
|
||||
}
|
||||
|
||||
function findSvelteKitLayout(cwd) {
|
||||
const candidates = [
|
||||
return firstExistingFile(cwd, [
|
||||
'src/routes/+layout.svelte',
|
||||
'src/routes/(app)/+layout.svelte',
|
||||
];
|
||||
for (const rel of candidates) {
|
||||
if (fs.existsSync(path.join(cwd, rel))) return rel;
|
||||
}
|
||||
return 'src/routes/+layout.svelte';
|
||||
]) || 'src/routes/+layout.svelte';
|
||||
}
|
||||
|
||||
function defaultSvelteLayout() {
|
||||
return `<script>\n let { children } = $props();\n</script>\n\n{@render children?.()}\n`;
|
||||
}
|
||||
|
||||
function packageHasSvelteKit(cwd) {
|
||||
const file = path.join(cwd, 'package.json');
|
||||
if (!fs.existsSync(file)) return false;
|
||||
try {
|
||||
const pkg = JSON.parse(fs.readFileSync(file, 'utf-8'));
|
||||
const deps = {
|
||||
...(pkg.dependencies || {}),
|
||||
...(pkg.devDependencies || {}),
|
||||
...(pkg.peerDependencies || {}),
|
||||
};
|
||||
return Boolean(deps['@sveltejs/kit'] || deps['@sveltejs/vite-plugin-svelte'] || deps.svelte);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function fileIncludes(file, text) {
|
||||
try {
|
||||
return fs.readFileSync(file, 'utf-8').includes(text);
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
import { firstExistingFile, hasAnyDependency } from './frameworks/detect-utils.mjs';
|
||||
import { buildLiveScriptSrc } from './frameworks/script-src.mjs';
|
||||
|
||||
export const TANSTACK_MARKER_OPEN = '{/* impeccable-live-tanstack-start */}';
|
||||
@@ -42,8 +44,8 @@ const START_PACKAGES = [
|
||||
];
|
||||
|
||||
export function detectTanStackStartProject(cwd = process.cwd()) {
|
||||
if (!packageHasTanStackStart(cwd)) return null;
|
||||
const rootRoute = findRootRouteFile(cwd);
|
||||
if (!hasAnyDependency(cwd, START_PACKAGES)) return null;
|
||||
const rootRoute = firstExistingFile(cwd, ROOT_ROUTE_CANDIDATES);
|
||||
if (!rootRoute) return null;
|
||||
|
||||
const ext = path.extname(rootRoute);
|
||||
@@ -218,29 +220,6 @@ function isManagedComponent(content) {
|
||||
return String(content || '').includes('impeccable-live-tanstack');
|
||||
}
|
||||
|
||||
function findRootRouteFile(cwd) {
|
||||
for (const rel of ROOT_ROUTE_CANDIDATES) {
|
||||
if (fs.existsSync(path.join(cwd, rel))) return rel;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function packageHasTanStackStart(cwd) {
|
||||
const file = path.join(cwd, 'package.json');
|
||||
if (!fs.existsSync(file)) return false;
|
||||
try {
|
||||
const pkg = JSON.parse(fs.readFileSync(file, 'utf-8'));
|
||||
const deps = {
|
||||
...(pkg.dependencies || {}),
|
||||
...(pkg.devDependencies || {}),
|
||||
...(pkg.peerDependencies || {}),
|
||||
};
|
||||
return START_PACKAGES.some((name) => Boolean(deps[name]));
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function relativeImportSpecifier(fromFile, toFile) {
|
||||
const rel = path.posix.relative(
|
||||
path.posix.dirname(fromFile.split(path.sep).join('/')),
|
||||
|
||||
@@ -10,38 +10,10 @@ import { dirname, join, relative, resolve } from 'node:path';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import { livePathGlobToRegex } from '../skill/scripts/lib/live-path-globs.mjs';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
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) {
|
||||
try {
|
||||
const out = execFileSync('node', [INJECT, ...args], {
|
||||
|
||||
Reference in New Issue
Block a user