Files
2026-08-01 22:06:48 -03:00

162 lines
5.1 KiB
JavaScript

/**
* Nuxt registry entry, and the Nuxt adapter itself.
*
* A script element placed in app.vue is compiled as Vue-rendered DOM and is
* not executed. Nuxt instead auto-discovers client plugins. Keep the adapter
* generated, dev-only, and outside user-authored source: Live creates one
* marked .client.ts plugin on start and removes it on stop.
*/
import fs from 'node:fs';
import path from 'node:path';
import { buildLiveScriptSrc } from './script-src.mjs';
import { findConfigFile } from './detect-utils.mjs';
export const NUXT_PLUGIN_MARKER = 'impeccable-live-nuxt-plugin';
export const NUXT_PLUGIN_NAME = 'impeccable-live.client.ts';
const NUXT_CONFIG_RE = /^nuxt\.config\.(?:js|mjs|cjs|ts|mts|cts)$/;
export function detectNuxtProject(cwd = process.cwd()) {
const configFile = findConfigFile(cwd, NUXT_CONFIG_RE);
if (!configFile) return null;
const config = fs.readFileSync(path.join(cwd, configFile), 'utf-8');
const literalSrcDir = config.match(/\bsrcDir\s*:\s*(['"])([^'"]+)\1/);
let appDir = '';
if (literalSrcDir) {
const candidate = literalSrcDir[2]
.replace(/\\/g, '/')
.replace(/^\.\//, '')
.replace(/\/+$/, '');
const normalized = path.posix.normalize(candidate);
if (normalized !== '..' && !normalized.startsWith('../') && !path.isAbsolute(normalized)) {
appDir = normalized === '.' ? '' : normalized;
}
} else if (
fs.existsSync(path.join(cwd, 'app', 'app.vue'))
|| fs.existsSync(path.join(cwd, 'app', 'pages'))
) {
appDir = 'app';
}
const pluginFile = [appDir, 'plugins', NUXT_PLUGIN_NAME].filter(Boolean).join('/');
return { configFile, appDir, pluginFile };
}
export function buildNuxtPlugin(port, token) {
return `/* ${NUXT_PLUGIN_MARKER} */
const liveSrc = '${buildLiveScriptSrc(port, token)}';
const liveSelector = 'script[data-impeccable-live-nuxt]';
export default defineNuxtPlugin(() => {
if (!import.meta.dev || typeof document === 'undefined') return;
const expectedSrc = new URL(liveSrc, window.location.href).href;
let script = document.querySelector(liveSelector);
if (script?.src === expectedSrc) return;
script?.remove();
script = document.createElement('script');
script.src = liveSrc;
script.async = true;
script.dataset.impeccableLiveNuxt = '';
document.head.appendChild(script);
import.meta.hot?.dispose(() => {
if (script?.isConnected) script.remove();
});
});
/* /${NUXT_PLUGIN_MARKER} */
`;
}
export function applyNuxtLiveAdapter({ cwd = process.cwd(), port, token, project = detectNuxtProject(cwd) }) {
if (!project) return { error: 'nuxt_not_detected' };
const absFile = path.join(cwd, project.pluginFile);
const existing = fs.existsSync(absFile) ? fs.readFileSync(absFile, 'utf-8') : null;
if (existing !== null && !existing.includes(NUXT_PLUGIN_MARKER)) {
return {
file: project.pluginFile,
error: 'nuxt_plugin_conflict',
hint: `${project.pluginFile} already exists and is not managed by Impeccable Live`,
};
}
const content = buildNuxtPlugin(port, token);
fs.mkdirSync(path.dirname(absFile), { recursive: true });
if (content !== existing) fs.writeFileSync(absFile, content, 'utf-8');
return {
file: project.pluginFile,
inserted: true,
changed: content !== existing,
devOnly: true,
};
}
export function removeNuxtLiveAdapter({ cwd = process.cwd(), project = detectNuxtProject(cwd) }) {
if (!project) return { error: 'nuxt_not_detected' };
const absFile = path.join(cwd, project.pluginFile);
if (!fs.existsSync(absFile)) {
return { file: project.pluginFile, removed: false, note: 'no adapter present' };
}
const content = fs.readFileSync(absFile, 'utf-8');
if (!content.includes(NUXT_PLUGIN_MARKER)) {
return {
file: project.pluginFile,
removed: false,
error: 'nuxt_plugin_conflict',
hint: `${project.pluginFile} is not managed by Impeccable Live`,
};
}
fs.unlinkSync(absFile);
const pluginDir = path.dirname(absFile);
if (fs.readdirSync(pluginDir).length === 0) fs.rmdirSync(pluginDir);
return { file: project.pluginFile, removed: true };
}
export const nuxt = {
name: 'nuxt',
detect(cwd) {
return detectNuxtProject(cwd);
},
inject: {
kind: 'adapter',
apply({ cwd, port, token, project }) {
return applyNuxtLiveAdapter({ cwd, port, token, project });
},
remove({ cwd, project }) {
return removeNuxtLiveAdapter({ cwd, project });
},
// The plugin path depends on the resolved srcDir, so it cannot live in the
// static ignore list the way the SvelteKit paths do.
ignorePatterns(project) {
return project?.pluginFile ? [project.pluginFile] : [];
},
artifacts({ project }) {
if (!project?.pluginFile) return [];
return [{
kind: 'created',
path: project.pluginFile,
marker: NUXT_PLUGIN_MARKER,
// Mirrors removeNuxtLiveAdapter: the generated `plugins/` directory
// goes when it empties, its parent stays.
pruneTo: path.posix.dirname(path.posix.dirname(project.pluginFile)),
}];
},
},
source: {
extensions: ['.vue'],
preview: 'source',
styleMode: 'scoped',
commentSyntax: 'html',
},
};