mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-15 07:36:50 +03:00
Fix Live accept for Elixir templates in lib/ (#374)
* Fix Live accept for Elixir templates in lib/ Wrap and accept search the repo for impeccable variant markers. That search skipped .ex files and the lib/ tree, so Phoenix LiveView markup inside ~H""" blocks never matched and browser Accept returned "Session markers not found". Extend the same EXTENSIONS and searchDirs in live-accept.mjs and live-wrap.mjs. Add a regression test that accepts from lib/my_app_web/components/layouts.ex. * Live: give the source search one owner for template extensions The #374 fix had to patch the same hardcoded EXTENSIONS array in two files because live-wrap.mjs and live-accept.mjs each carried their own copy of the project source walk. The copies had already drifted: same extension list twice, same searchDirs twice, and one realpathSync guarded by try/catch while the other was not. Meanwhile hook-lib.mjs had solved this properly for the design hook in #316/#347 with a configurable `detector.extensions` and suffix matching that handles .blade.php and .html.erb. Live never read it, so a project that taught the hook about .heex still got 'Session markers not found' on Accept. - lib/template-extensions.mjs is the single owner. It holds Live's built-in markup list, the suffix matcher, and the detector.extensions config reader. hook-lib.mjs now imports its normalize/merge/match helpers from here instead of duplicating them, and re-exports matchConfiguredExtension for its existing callers. - Live resolves built-ins PLUS detector.extensions, so teaching the hook about a server template teaches wrap and accept at the same time. - live/source-search.mjs holds the walk both scripts share. Callers pass the one thing that actually differs (skipDirs, fileFilter). Unifying gives live-wrap the guarded realpathSync, so a dangling symlink in the tree no longer throws out of the whole wrap, and makes it skip .impeccable artifacts the way accept already did. - Extensions are matched on filename suffix rather than path.extname, so root.html.heex and show.html.erb resolve. - Drop .exs. Those are Elixir scripts (mix.exs, config/*.exs), never markup, and including them only lets a wrap query match build config. - Fill the Elixir gap in the manual-edit paths, which kept their own allowlists and would have left Live half-working for Phoenix: live-commit-manual-edits.mjs and live-manual-edit-evidence.mjs. Verified the round trip by hand against a Phoenix layout: wrap injects markers into a ~H""" block in lib/**/*.ex, accept carbonizes the chosen variant back out. AI assistance: written with Claude Code. Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Nils Kanevad <heliumbrain@users.noreply.github.com> Co-authored-by: Paul Bakaus <paul.bakaus@gmail.com> Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
co-authored by
Nils Kanevad
Paul Bakaus
Claude
parent
e6f3ce6d9a
commit
5d719a279a
+10
-43
@@ -42,6 +42,16 @@ import path from 'node:path';
|
||||
import { pathToFileURL, fileURLToPath } from 'node:url';
|
||||
import { extractPlatform, loadContext } from './context.mjs';
|
||||
import { IMPECCABLE_COMMAND } from './lib/provider.mjs';
|
||||
// `detector.extensions` (issue #316) is shared with Live's source search, which
|
||||
// needs the same answer for `.heex` / `.blade.php` when it hunts for session
|
||||
// markers. lib/template-extensions.mjs owns the shape; re-exported here because
|
||||
// hook-lib has been the import site for matchConfiguredExtension since #347.
|
||||
import {
|
||||
matchConfiguredExtension,
|
||||
mergeExtensions,
|
||||
} from './lib/template-extensions.mjs';
|
||||
|
||||
export { matchConfiguredExtension };
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
@@ -263,49 +273,6 @@ function applyDetectorConfigSource(config, raw) {
|
||||
return config;
|
||||
}
|
||||
|
||||
// Extra scanned extensions from `detector.extensions` config. Entries are
|
||||
// `{ ext, engine }` (engine 'html' | 'text', default 'html' — the common case
|
||||
// for server-side templates) or bare strings as shorthand. Extensions are
|
||||
// matched against the end of the filename, not path.extname, so double
|
||||
// extensions like `.blade.php` and `.html.erb` work (issue #316).
|
||||
function normalizeExtensionEntries(entries) {
|
||||
if (!Array.isArray(entries)) return [];
|
||||
const out = [];
|
||||
for (const entry of entries) {
|
||||
const raw = typeof entry === 'string' ? entry : entry?.ext;
|
||||
if (typeof raw !== 'string') continue;
|
||||
let ext = raw.trim().toLowerCase();
|
||||
if (!ext) continue;
|
||||
if (!ext.startsWith('.')) ext = `.${ext}`;
|
||||
const engine = (!(typeof entry === 'string') && entry?.engine === 'text') ? 'text' : 'html';
|
||||
out.push({ ext, engine });
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function mergeExtensions(existing, incoming) {
|
||||
const map = new Map();
|
||||
for (const entry of normalizeExtensionEntries(existing)) map.set(entry.ext, entry);
|
||||
for (const entry of normalizeExtensionEntries(incoming)) map.set(entry.ext, entry);
|
||||
return Array.from(map.values());
|
||||
}
|
||||
|
||||
export function matchConfiguredExtension(filePath, extensions) {
|
||||
if (!Array.isArray(extensions) || extensions.length === 0) return null;
|
||||
const name = path.basename(String(filePath || '')).toLowerCase();
|
||||
if (!name) return null;
|
||||
// The longest matching suffix wins, so `.blade.php` beats a broader `.php`
|
||||
// entry regardless of config order.
|
||||
let best = null;
|
||||
for (const entry of normalizeExtensionEntries(extensions)) {
|
||||
if (name.length > entry.ext.length && name.endsWith(entry.ext)
|
||||
&& (!best || entry.ext.length > best.ext.length)) {
|
||||
best = entry;
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
function applyConfigSource(config, raw) {
|
||||
if (!raw || typeof raw !== 'object') return config;
|
||||
if (Object.prototype.hasOwnProperty.call(raw, 'enabled')) {
|
||||
|
||||
Reference in New Issue
Block a user