162 lines
5.1 KiB
JavaScript
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',
|
|
},
|
|
};
|