2026-08-06 09f01778d5a034e2af50ae05da8908f7b6a871c8
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
/**
 * 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',
  },
};