@life-and-dev/mdsite 0.5.3 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/README.md +29 -37
  2. package/dist/commands/clean.d.ts +1 -0
  3. package/dist/commands/clean.js +70 -0
  4. package/dist/commands/clean.js.map +1 -0
  5. package/dist/commands/commands.test.js +157 -75
  6. package/dist/commands/commands.test.js.map +1 -1
  7. package/dist/commands/generate.js +5 -4
  8. package/dist/commands/generate.js.map +1 -1
  9. package/dist/commands/init.js +5 -64
  10. package/dist/commands/init.js.map +1 -1
  11. package/dist/commands/prepare.js +2 -14
  12. package/dist/commands/prepare.js.map +1 -1
  13. package/dist/commands/prepare.test.js +26 -24
  14. package/dist/commands/prepare.test.js.map +1 -1
  15. package/dist/commands/preview.js +21 -21
  16. package/dist/commands/preview.js.map +1 -1
  17. package/dist/commands/start.js +13 -11
  18. package/dist/commands/start.js.map +1 -1
  19. package/dist/commands/stop.js +7 -4
  20. package/dist/commands/stop.js.map +1 -1
  21. package/dist/commands/workflows.test.js +42 -56
  22. package/dist/commands/workflows.test.js.map +1 -1
  23. package/dist/config/default-mdsite-config.js +7 -8
  24. package/dist/config/default-mdsite-config.js.map +1 -1
  25. package/dist/config/default-mdsite-config.test.js +7 -8
  26. package/dist/config/default-mdsite-config.test.js.map +1 -1
  27. package/dist/config/mdsite-config.d.ts +46 -10
  28. package/dist/config/mdsite-config.js +46 -24
  29. package/dist/config/mdsite-config.js.map +1 -1
  30. package/dist/config/mdsite-config.test.js +55 -50
  31. package/dist/config/mdsite-config.test.js.map +1 -1
  32. package/dist/index.js +8 -2
  33. package/dist/index.js.map +1 -1
  34. package/dist/index.test.js +13 -0
  35. package/dist/index.test.js.map +1 -1
  36. package/dist/process/child-process.d.ts +4 -0
  37. package/dist/process/child-process.js +33 -1
  38. package/dist/process/child-process.js.map +1 -1
  39. package/dist/process/child-process.test.js +41 -5
  40. package/dist/process/child-process.test.js.map +1 -1
  41. package/dist/process/runtime-state.d.ts +13 -5
  42. package/dist/process/runtime-state.js +25 -13
  43. package/dist/process/runtime-state.js.map +1 -1
  44. package/dist/process/runtime-state.test.js +10 -10
  45. package/dist/process/runtime-state.test.js.map +1 -1
  46. package/dist/renderer/mdsite-nuxt.d.ts +28 -3
  47. package/dist/renderer/mdsite-nuxt.js +32 -27
  48. package/dist/renderer/mdsite-nuxt.js.map +1 -1
  49. package/dist/renderer/mdsite-nuxt.test.js +40 -39
  50. package/dist/renderer/mdsite-nuxt.test.js.map +1 -1
  51. package/mdsite-nuxt/app/components/AppFooter.vue +84 -22
  52. package/mdsite-nuxt/app/composables/useFooter.test.ts +54 -0
  53. package/mdsite-nuxt/app/composables/useFooter.ts +48 -31
  54. package/mdsite-nuxt/app/composables/useSiteConfig.test.ts +13 -87
  55. package/mdsite-nuxt/app/composables/useSiteConfig.ts +7 -26
  56. package/mdsite-nuxt/app/composables/useSourceEdit.test.ts +103 -0
  57. package/mdsite-nuxt/app/composables/useSourceEdit.ts +39 -51
  58. package/mdsite-nuxt/app/layouts/default.vue +10 -3
  59. package/mdsite-nuxt/nuxt.config.ts +22 -15
  60. package/mdsite-nuxt/scripts/generate-favicons.test.ts +3 -3
  61. package/mdsite-nuxt/scripts/generate-favicons.ts +4 -4
  62. package/mdsite-nuxt/scripts/generate-indices.test.ts +71 -10
  63. package/mdsite-nuxt/scripts/generate-indices.ts +161 -27
  64. package/mdsite-nuxt/scripts/renderer-hooks.test.ts +0 -91
  65. package/mdsite-nuxt/scripts/renderer-hooks.ts +1 -50
  66. package/mdsite-nuxt/scripts/start.test.ts +0 -1
  67. package/mdsite-nuxt/scripts/start.ts +0 -1
  68. package/mdsite-nuxt/utils/mdsite-config.ts +86 -41
  69. package/package.json +1 -1
  70. package/mdsite-nuxt/example.config.yml +0 -67
@@ -3,7 +3,6 @@ vi.mock('node:fs/promises', () => ({
3
3
  access: vi.fn(),
4
4
  copyFile: vi.fn(),
5
5
  cp: vi.fn(),
6
- mkdir: vi.fn(),
7
6
  readFile: vi.fn(),
8
7
  rm: vi.fn(),
9
8
  writeFile: vi.fn()
@@ -24,6 +23,7 @@ vi.mock('../process/runtime-state.js', () => ({
24
23
  vi.mock('../process/child-process.js', () => ({
25
24
  openUrlInBrowser: vi.fn(),
26
25
  stopProcess: vi.fn(),
26
+ waitForRendererPort: vi.fn(),
27
27
  waitForTcpPort: vi.fn()
28
28
  }));
29
29
  vi.mock('../renderer/mdsite-nuxt.js', () => ({
@@ -31,7 +31,6 @@ vi.mock('../renderer/mdsite-nuxt.js', () => ({
31
31
  ensureRendererDependencies: vi.fn(),
32
32
  generateRenderer: vi.fn(),
33
33
  getBundledRendererDir: vi.fn(),
34
- getRendererGeneratedOutputPath: vi.fn(),
35
34
  hasPreviewArtifacts: vi.fn(),
36
35
  prepareRenderer: vi.fn(),
37
36
  previewRendererForeground: vi.fn(),
@@ -40,11 +39,12 @@ vi.mock('../renderer/mdsite-nuxt.js', () => ({
40
39
  startRendererInBackground: vi.fn()
41
40
  }));
42
41
  import path from 'node:path';
43
- import { access, copyFile, cp, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
42
+ import { access, copyFile, cp, readFile, rm, writeFile } from 'node:fs/promises';
44
43
  import { buildDefaultMdsiteConfig, loadMdsiteConfig, resolveContentOutputPath, serializeMdsiteConfig } from '../config/mdsite-config.js';
45
- import { openUrlInBrowser, stopProcess, waitForTcpPort } from '../process/child-process.js';
44
+ import { openUrlInBrowser, stopProcess, waitForRendererPort, waitForTcpPort } from '../process/child-process.js';
46
45
  import { clearRuntimeState, getRuntimeLogPath, isProcessRunning, readRuntimeState, writeRuntimeState } from '../process/runtime-state.js';
47
- import { ensurePreviewArtifacts, ensureRendererDependencies, generateRenderer, getBundledRendererDir, getRendererGeneratedOutputPath, hasPreviewArtifacts, prepareRenderer, previewRendererForeground, previewRendererInBackground, startRendererForeground, startRendererInBackground } from '../renderer/mdsite-nuxt.js';
46
+ import { ensurePreviewArtifacts, ensureRendererDependencies, generateRenderer, getBundledRendererDir, hasPreviewArtifacts, prepareRenderer, previewRendererForeground, previewRendererInBackground, startRendererForeground, startRendererInBackground } from '../renderer/mdsite-nuxt.js';
47
+ import { runCleanCommand } from './clean.js';
48
48
  import { runGenerateCommand } from './generate.js';
49
49
  import { runInitCommand } from './init.js';
50
50
  import { runPreviewCommand } from './preview.js';
@@ -55,7 +55,6 @@ const cpMock = vi.mocked(cp);
55
55
  const rmMock = vi.mocked(rm);
56
56
  const writeFileMock = vi.mocked(writeFile);
57
57
  const copyFileMock = vi.mocked(copyFile);
58
- const mkdirMock = vi.mocked(mkdir);
59
58
  const readFileMock = vi.mocked(readFile);
60
59
  const getBundledRendererDirMock = vi.mocked(getBundledRendererDir);
61
60
  const buildDefaultConfigMock = vi.mocked(buildDefaultMdsiteConfig);
@@ -71,7 +70,6 @@ const ensurePreviewArtifactsMock = vi.mocked(ensurePreviewArtifacts);
71
70
  const hasPreviewArtifactsMock = vi.mocked(hasPreviewArtifacts);
72
71
  const ensureRendererDependenciesMock = vi.mocked(ensureRendererDependencies);
73
72
  const generateRendererMock = vi.mocked(generateRenderer);
74
- const getRendererGeneratedOutputPathMock = vi.mocked(getRendererGeneratedOutputPath);
75
73
  const prepareRendererMock = vi.mocked(prepareRenderer);
76
74
  const previewRendererForegroundMock = vi.mocked(previewRendererForeground);
77
75
  const previewRendererInBackgroundMock = vi.mocked(previewRendererInBackground);
@@ -80,14 +78,13 @@ const startRendererInBackgroundMock = vi.mocked(startRendererInBackground);
80
78
  const stopProcessMock = vi.mocked(stopProcess);
81
79
  const openUrlInBrowserMock = vi.mocked(openUrlInBrowser);
82
80
  const waitForTcpPortMock = vi.mocked(waitForTcpPort);
81
+ const waitForRendererPortMock = vi.mocked(waitForRendererPort);
83
82
  const loadedConfig = {
84
83
  config: {
85
- favicon: '',
86
- features: { bibleTooltips: true, sourceEdit: true },
84
+ features: { bibleTooltips: true, sourceEdit: '', footer: [] },
87
85
  menu: [],
88
- footer: [],
89
- server: { output: '.output', path: '.renderer', repo: 'repo', gitBranch: 'main' },
90
- site: { canonical: '', name: 'Docs' },
86
+ paths: { input: '', build: '.renderer', output: '.output' },
87
+ site: { canonical: '', favicon: '', name: 'Docs' },
91
88
  themes: { light: { colors: {} }, dark: { colors: {} } }
92
89
  },
93
90
  configDir: '/content',
@@ -100,15 +97,20 @@ describe('command helpers', () => {
100
97
  vi.useRealTimers();
101
98
  loadConfigMock.mockResolvedValue(loadedConfig);
102
99
  hasPreviewArtifactsMock.mockResolvedValue(true);
103
- prepareRendererMock.mockResolvedValue({ rendererDir: '/renderer', rendererEnv: { TEST: '1' } });
100
+ prepareRendererMock.mockResolvedValue({
101
+ rendererDir: '/renderer',
102
+ rendererEnv: { TEST: '1' },
103
+ rendererOutputDir: '/renderer/.output'
104
+ });
104
105
  getRuntimeLogPathMock.mockImplementation((configDir, config, kind) => {
105
- return `${configDir}/${config.server.path}/${kind}.log`;
106
+ const basename = kind === 'start' ? 'live' : 'static';
107
+ return `${configDir}/${config.paths.build}/${basename}.log`;
106
108
  });
107
109
  resolveOutputMock.mockReturnValue('/content/.output/public');
108
- getRendererGeneratedOutputPathMock.mockReturnValue('/renderer/.output/public');
109
110
  getBundledRendererDirMock.mockReturnValue('/home/gizbar/git/mdsite/mdsite-nuxt');
110
111
  openUrlInBrowserMock.mockResolvedValue(true);
111
112
  waitForTcpPortMock.mockResolvedValue(true);
113
+ waitForRendererPortMock.mockImplementation(async (_logPath, fallbackPort) => fallbackPort);
112
114
  });
113
115
  it('runInitCommand creates every mdsite file when the content dir is empty', async () => {
114
116
  const existing = new Set();
@@ -119,27 +121,16 @@ describe('command helpers', () => {
119
121
  });
120
122
  buildDefaultConfigMock.mockResolvedValue(loadedConfig.config);
121
123
  serializeConfigMock.mockReturnValue('serialized-config');
122
- readFileMock.mockResolvedValueOnce(JSON.stringify({ name: 'mdsite-nuxt-renderer', version: '0.1.0', description: 'old', scripts: { dev: 'x' } }));
123
- readFileMock.mockResolvedValueOnce(JSON.stringify({ name: 'mdsite-nuxt-renderer', version: '0.1.0', lockfileVersion: 3, packages: { '': { name: 'mdsite-nuxt-renderer', version: '0.1.0' } } }));
124
- await expect(runInitCommand('/content')).resolves.toBe('Created mdsite.yml, .nvmrc, .renderer/package.json, .renderer/package-lock.json in /content.');
124
+ await expect(runInitCommand('/content')).resolves.toBe('Created mdsite.yml, .nvmrc in /content.');
125
125
  expect(buildDefaultConfigMock).toHaveBeenCalledWith('/content');
126
126
  expect(loadConfigMock).not.toHaveBeenCalled();
127
127
  expect(writeFileMock).toHaveBeenCalledWith('/content/mdsite.yml', 'serialized-config', 'utf8');
128
128
  expect(writeFileMock).toHaveBeenCalledWith('/content/.nvmrc', '24\n', 'utf8');
129
- expect(mkdirMock).toHaveBeenCalledWith(path.join('/content', '.renderer'), { recursive: true });
130
- expect(readFileMock).toHaveBeenCalledWith(path.join('/home/gizbar/git/mdsite/mdsite-nuxt', 'package.json'), 'utf8');
131
- expect(readFileMock).toHaveBeenCalledWith(path.join('/home/gizbar/git/mdsite/mdsite-nuxt', 'package-lock.json'), 'utf8');
132
- const writtenPkg = writeFileMock.mock.calls.find(([p]) => p === path.join('/content', '.renderer', 'package.json'));
133
- expect(writtenPkg).toBeDefined();
134
- expect(writtenPkg[1]).toBe(`${JSON.stringify({ name: 'content', version: '0.1.0', description: 'Docs', scripts: { dev: 'x' } }, null, 2)}\n`);
135
- const writtenLock = writeFileMock.mock.calls.find(([p]) => p === path.join('/content', '.renderer', 'package-lock.json'));
136
- expect(writtenLock).toBeDefined();
137
- expect(writtenLock[1]).toBe(`${JSON.stringify({ name: 'content', version: '0.1.0', lockfileVersion: 3, packages: { '': { name: 'content', version: '0.1.0' } } }, null, 2)}\n`);
138
129
  expect(copyFileMock).not.toHaveBeenCalled();
139
130
  expect(writeFileMock).toHaveBeenCalledWith('/content/.gitignore', expect.stringContaining('.renderer/*'), 'utf8');
140
- expect(writeFileMock).toHaveBeenCalledWith('/content/.gitignore', expect.stringContaining('!.renderer/package.json'), 'utf8');
141
- expect(writeFileMock).toHaveBeenCalledWith('/content/.gitignore', expect.stringContaining('!.renderer/package-lock.json'), 'utf8');
142
131
  expect(writeFileMock).toHaveBeenCalledWith('/content/.gitignore', expect.stringContaining('.output/'), 'utf8');
132
+ expect(writeFileMock).not.toHaveBeenCalledWith('/content/.gitignore', expect.stringContaining('!.renderer/package.json'), 'utf8');
133
+ expect(writeFileMock).not.toHaveBeenCalledWith('/content/.gitignore', expect.stringContaining('!.renderer/package-lock.json'), 'utf8');
143
134
  });
144
135
  it('runInitCommand preserves an existing .nvmrc and only creates the rest', async () => {
145
136
  const existing = new Set(['/content/.nvmrc']);
@@ -150,33 +141,23 @@ describe('command helpers', () => {
150
141
  });
151
142
  buildDefaultConfigMock.mockResolvedValue(loadedConfig.config);
152
143
  serializeConfigMock.mockReturnValue('serialized-config');
153
- readFileMock.mockResolvedValueOnce(JSON.stringify({ name: 'mdsite-nuxt-renderer', version: '0.1.0', description: 'old', scripts: { dev: 'x' } }));
154
- readFileMock.mockResolvedValueOnce(JSON.stringify({ name: 'mdsite-nuxt-renderer', version: '0.1.0', lockfileVersion: 3, packages: { '': { name: 'mdsite-nuxt-renderer', version: '0.1.0' } } }));
155
- await expect(runInitCommand('/content')).resolves.toBe('Created mdsite.yml, .renderer/package.json, .renderer/package-lock.json in /content.');
144
+ await expect(runInitCommand('/content')).resolves.toBe('Created mdsite.yml in /content.');
156
145
  expect(writeFileMock).not.toHaveBeenCalledWith('/content/.nvmrc', expect.anything(), expect.anything());
157
146
  expect(copyFileMock).not.toHaveBeenCalled();
158
147
  });
159
148
  it('runInitCommand loads an existing mdsite.yml to repair missing files without overwriting it', async () => {
160
149
  const existing = new Set([
161
- '/content/mdsite.yml',
162
- '/content/.nvmrc',
163
- path.join('/content', '.renderer', 'package.json')
150
+ '/content/mdsite.yml'
164
151
  ]);
165
152
  accessMock.mockImplementation(async (p) => {
166
153
  if (typeof p === 'string' && existing.has(p))
167
154
  return;
168
155
  throw new Error('missing');
169
156
  });
170
- readFileMock.mockResolvedValueOnce(JSON.stringify({ name: 'mdsite-nuxt-renderer', version: '0.1.0', lockfileVersion: 3, packages: { '': { name: 'mdsite-nuxt-renderer', version: '0.1.0' } } }));
171
- await expect(runInitCommand('/content')).resolves.toBe('Created .renderer/package-lock.json in /content.');
157
+ await expect(runInitCommand('/content')).resolves.toBe('Created .nvmrc in /content.');
172
158
  expect(loadConfigMock).toHaveBeenCalledWith('/content');
173
159
  expect(buildDefaultConfigMock).not.toHaveBeenCalled();
174
160
  expect(writeFileMock).not.toHaveBeenCalledWith('/content/mdsite.yml', expect.anything(), expect.anything());
175
- const writeFileCallsForPkg = writeFileMock.mock.calls.filter(([p]) => typeof p === 'string' && p === path.join('/content', '.renderer', 'package.json'));
176
- expect(writeFileCallsForPkg).toHaveLength(0);
177
- const writtenLock = writeFileMock.mock.calls.find(([p]) => p === path.join('/content', '.renderer', 'package-lock.json'));
178
- expect(writtenLock).toBeDefined();
179
- expect(writtenLock[1]).toBe(`${JSON.stringify({ name: 'content', version: '0.1.0', lockfileVersion: 3, packages: { '': { name: 'content', version: '0.1.0' } } }, null, 2)}\n`);
180
161
  expect(copyFileMock).not.toHaveBeenCalled();
181
162
  });
182
163
  it('runInitCommand reports nothing to create when every file already exists', async () => {
@@ -218,7 +199,7 @@ describe('command helpers', () => {
218
199
  readRuntimeStateMock.mockResolvedValueOnce({ kind: 'start', pid: 44 });
219
200
  isProcessRunningMock.mockReturnValueOnce(false);
220
201
  startRendererInBackgroundMock.mockResolvedValueOnce(777);
221
- await expect(runStartCommand('/content', { detached: true })).resolves.toBe('mdsite live running in background (PID 777). Log: /content/.renderer/start.log');
202
+ await expect(runStartCommand('/content', { detached: true })).resolves.toBe('mdsite live running in background (PID 777). Log: /content/.renderer/live.log');
222
203
  expect(ensureRendererDependenciesMock).toHaveBeenCalledWith('/renderer');
223
204
  expect(waitForTcpPortMock).toHaveBeenCalledWith('localhost', 3000);
224
205
  expect(waitForTcpPortMock.mock.invocationCallOrder[0]).toBeGreaterThan(writeRuntimeStateMock.mock.invocationCallOrder[0] ?? 0);
@@ -236,18 +217,30 @@ describe('command helpers', () => {
236
217
  readRuntimeStateMock.mockResolvedValueOnce(null);
237
218
  prepareRendererMock.mockResolvedValueOnce({
238
219
  rendererDir: '/renderer',
239
- rendererEnv: { HOST: '127.0.0.1', PORT: '4173', NUXT_HOST: 'start.local', NUXT_PORT: '4321' }
220
+ rendererEnv: { HOST: '127.0.0.1', PORT: '4173', NUXT_HOST: 'start.local', NUXT_PORT: '4321' },
221
+ rendererOutputDir: '/renderer/.output'
240
222
  });
241
223
  startRendererInBackgroundMock.mockResolvedValueOnce(778);
242
- await expect(runStartCommand('/content', { detached: true })).resolves.toBe('mdsite live running in background (PID 778). Log: /content/.renderer/start.log');
224
+ await expect(runStartCommand('/content', { detached: true })).resolves.toBe('mdsite live running in background (PID 778). Log: /content/.renderer/live.log');
225
+ expect(waitForRendererPortMock).toHaveBeenCalledWith('/content/.renderer/live.log', 4321);
243
226
  expect(waitForTcpPortMock).toHaveBeenCalledWith('start.local', 4321);
244
227
  expect(openUrlInBrowserMock).toHaveBeenCalledWith('http://start.local:4321');
245
228
  });
229
+ it('runStartCommand opens the actual port when Nuxt falls back to the next free one in detached mode', async () => {
230
+ readRuntimeStateMock.mockResolvedValueOnce(null);
231
+ startRendererInBackgroundMock.mockResolvedValueOnce(7781);
232
+ waitForRendererPortMock.mockResolvedValueOnce(3001);
233
+ await expect(runStartCommand('/content', { detached: true })).resolves.toBe('mdsite live running in background (PID 7781). Log: /content/.renderer/live.log');
234
+ expect(waitForRendererPortMock).toHaveBeenCalledWith('/content/.renderer/live.log', 3000);
235
+ expect(waitForRendererPortMock.mock.invocationCallOrder[0]).toBeGreaterThan(writeRuntimeStateMock.mock.invocationCallOrder[0] ?? 0);
236
+ expect(waitForTcpPortMock).toHaveBeenCalledWith('localhost', 3001);
237
+ expect(openUrlInBrowserMock).toHaveBeenCalledWith('http://localhost:3001');
238
+ });
246
239
  it('runStartCommand does not open the browser when detached start readiness times out', async () => {
247
240
  readRuntimeStateMock.mockResolvedValueOnce(null);
248
241
  startRendererInBackgroundMock.mockResolvedValueOnce(779);
249
242
  waitForTcpPortMock.mockResolvedValueOnce(false);
250
- await expect(runStartCommand('/content', { detached: true })).resolves.toBe('mdsite live running in background (PID 779). Log: /content/.renderer/start.log');
243
+ await expect(runStartCommand('/content', { detached: true })).resolves.toBe('mdsite live running in background (PID 779). Log: /content/.renderer/live.log');
251
244
  expect(waitForTcpPortMock).toHaveBeenCalledWith('localhost', 3000);
252
245
  expect(openUrlInBrowserMock).not.toHaveBeenCalled();
253
246
  });
@@ -255,7 +248,7 @@ describe('command helpers', () => {
255
248
  readRuntimeStateMock.mockResolvedValueOnce(null);
256
249
  startRendererInBackgroundMock.mockResolvedValueOnce(780);
257
250
  waitForTcpPortMock.mockRejectedValueOnce(new Error('connect failed'));
258
- await expect(runStartCommand('/content', { detached: true })).resolves.toBe('mdsite live running in background (PID 780). Log: /content/.renderer/start.log');
251
+ await expect(runStartCommand('/content', { detached: true })).resolves.toBe('mdsite live running in background (PID 780). Log: /content/.renderer/live.log');
259
252
  expect(openUrlInBrowserMock).not.toHaveBeenCalled();
260
253
  });
261
254
  it('runStartCommand exposes the foreground renderer on the network when host is set', async () => {
@@ -270,12 +263,12 @@ describe('command helpers', () => {
270
263
  it('runStartCommand binds the detached renderer to a custom host when host is set', async () => {
271
264
  readRuntimeStateMock.mockResolvedValueOnce(null);
272
265
  startRendererInBackgroundMock.mockResolvedValueOnce(781);
273
- await expect(runStartCommand('/content', { detached: true, host: '0.0.0.0' })).resolves.toBe('mdsite live running in background (PID 781). Log: /content/.renderer/start.log');
266
+ await expect(runStartCommand('/content', { detached: true, host: '0.0.0.0' })).resolves.toBe('mdsite live running in background (PID 781). Log: /content/.renderer/live.log');
274
267
  expect(startRendererInBackgroundMock).toHaveBeenCalledWith('/renderer', expect.objectContaining({
275
268
  NUXT_HOST: '0.0.0.0',
276
269
  HOST: '0.0.0.0',
277
270
  NITRO_HOST: '0.0.0.0'
278
- }), '/content/.renderer/start.log');
271
+ }), '/content/.renderer/live.log');
279
272
  expect(waitForTcpPortMock).toHaveBeenCalledWith('0.0.0.0', 3000);
280
273
  expect(openUrlInBrowserMock).toHaveBeenCalledWith('http://0.0.0.0:3000');
281
274
  });
@@ -288,7 +281,7 @@ describe('command helpers', () => {
288
281
  expect(waitForTcpPortMock).not.toHaveBeenCalled();
289
282
  expect(openUrlInBrowserMock).not.toHaveBeenCalled();
290
283
  expect(ensureRendererDependenciesMock).toHaveBeenCalledWith('/renderer');
291
- expect(ensurePreviewArtifactsMock).toHaveBeenCalledWith('/renderer');
284
+ expect(ensurePreviewArtifactsMock).toHaveBeenCalledWith('/renderer/.output');
292
285
  expect(previewRendererForegroundMock).toHaveBeenCalledWith('/renderer', expect.objectContaining({
293
286
  TEST: '1',
294
287
  NUXT_HOST: 'localhost',
@@ -309,8 +302,8 @@ describe('command helpers', () => {
309
302
  vi.setSystemTime(new Date('2026-04-10T13:00:00.000Z'));
310
303
  readRuntimeStateMock.mockResolvedValueOnce(null);
311
304
  previewRendererInBackgroundMock.mockResolvedValueOnce(888);
312
- await expect(runPreviewCommand('/content', { detached: true })).resolves.toBe('mdsite static running in background (PID 888). URL: http://localhost:3000 Log: /content/.renderer/preview.log');
313
- expect(ensurePreviewArtifactsMock).toHaveBeenCalledWith('/renderer');
305
+ await expect(runPreviewCommand('/content', { detached: true })).resolves.toBe('mdsite static running in background (PID 888). URL: http://localhost:3000 Log: /content/.renderer/static.log');
306
+ expect(ensurePreviewArtifactsMock).toHaveBeenCalledWith('/renderer/.output');
314
307
  expect(ensurePreviewArtifactsMock.mock.invocationCallOrder[0]).toBeGreaterThan(ensureRendererDependenciesMock.mock.invocationCallOrder[0] ?? 0);
315
308
  expect(previewRendererInBackgroundMock).toHaveBeenCalledWith('/renderer', expect.objectContaining({
316
309
  TEST: '1',
@@ -320,7 +313,7 @@ describe('command helpers', () => {
320
313
  PORT: '3000',
321
314
  NITRO_HOST: 'localhost',
322
315
  NITRO_PORT: '3000'
323
- }), '/content/.renderer/preview.log');
316
+ }), '/content/.renderer/static.log');
324
317
  expect(waitForTcpPortMock).toHaveBeenCalledWith('localhost', 3000);
325
318
  expect(waitForTcpPortMock.mock.invocationCallOrder[0]).toBeGreaterThan(writeRuntimeStateMock.mock.invocationCallOrder[0] ?? 0);
326
319
  expect(openUrlInBrowserMock.mock.invocationCallOrder[0]).toBeGreaterThan(waitForTcpPortMock.mock.invocationCallOrder[0] ?? 0);
@@ -336,10 +329,11 @@ describe('command helpers', () => {
336
329
  readRuntimeStateMock.mockResolvedValueOnce(null);
337
330
  prepareRendererMock.mockResolvedValueOnce({
338
331
  rendererDir: '/renderer',
339
- rendererEnv: { HOST: '127.0.0.1', PORT: '4173', NUXT_HOST: 'preview.local', NUXT_PORT: '4321' }
332
+ rendererEnv: { HOST: '127.0.0.1', PORT: '4173', NUXT_HOST: 'preview.local', NUXT_PORT: '4321' },
333
+ rendererOutputDir: '/renderer/.output'
340
334
  });
341
335
  previewRendererInBackgroundMock.mockResolvedValueOnce(999);
342
- await expect(runPreviewCommand('/content', { detached: true })).resolves.toBe('mdsite static running in background (PID 999). URL: http://preview.local:4321 Log: /content/.renderer/preview.log');
336
+ await expect(runPreviewCommand('/content', { detached: true })).resolves.toBe('mdsite static running in background (PID 999). URL: http://preview.local:4321 Log: /content/.renderer/static.log');
343
337
  expect(previewRendererInBackgroundMock).toHaveBeenCalledWith('/renderer', expect.objectContaining({
344
338
  HOST: 'preview.local',
345
339
  PORT: '4321',
@@ -347,18 +341,30 @@ describe('command helpers', () => {
347
341
  NUXT_PORT: '4321',
348
342
  NITRO_HOST: 'preview.local',
349
343
  NITRO_PORT: '4321'
350
- }), '/content/.renderer/preview.log');
344
+ }), '/content/.renderer/static.log');
345
+ expect(waitForRendererPortMock).toHaveBeenCalledWith('/content/.renderer/static.log', 4321);
351
346
  expect(waitForTcpPortMock).toHaveBeenCalledWith('preview.local', 4321);
352
347
  expect(openUrlInBrowserMock).toHaveBeenCalledWith('http://preview.local:4321');
353
348
  });
349
+ it('runPreviewCommand opens the actual port when Nitro falls back to the next free one in detached mode', async () => {
350
+ readRuntimeStateMock.mockResolvedValueOnce(null);
351
+ previewRendererInBackgroundMock.mockResolvedValueOnce(9991);
352
+ waitForRendererPortMock.mockResolvedValueOnce(3001);
353
+ await expect(runPreviewCommand('/content', { detached: true })).resolves.toBe('mdsite static running in background (PID 9991). URL: http://localhost:3001 Log: /content/.renderer/static.log');
354
+ expect(waitForRendererPortMock).toHaveBeenCalledWith('/content/.renderer/static.log', 3000);
355
+ expect(waitForRendererPortMock.mock.invocationCallOrder[0]).toBeGreaterThan(writeRuntimeStateMock.mock.invocationCallOrder[0] ?? 0);
356
+ expect(waitForTcpPortMock).toHaveBeenCalledWith('localhost', 3001);
357
+ expect(openUrlInBrowserMock).toHaveBeenCalledWith('http://localhost:3001');
358
+ });
354
359
  it('runPreviewCommand falls back to HOST and PORT when NUXT preview values are unset in detached mode', async () => {
355
360
  readRuntimeStateMock.mockResolvedValueOnce(null);
356
361
  prepareRendererMock.mockResolvedValueOnce({
357
362
  rendererDir: '/renderer',
358
- rendererEnv: { HOST: '127.0.0.1', PORT: '4173' }
363
+ rendererEnv: { HOST: '127.0.0.1', PORT: '4173' },
364
+ rendererOutputDir: '/renderer/.output'
359
365
  });
360
366
  previewRendererInBackgroundMock.mockResolvedValueOnce(1000);
361
- await expect(runPreviewCommand('/content', { detached: true })).resolves.toBe('mdsite static running in background (PID 1000). URL: http://127.0.0.1:4173 Log: /content/.renderer/preview.log');
367
+ await expect(runPreviewCommand('/content', { detached: true })).resolves.toBe('mdsite static running in background (PID 1000). URL: http://127.0.0.1:4173 Log: /content/.renderer/static.log');
362
368
  expect(previewRendererInBackgroundMock).toHaveBeenCalledWith('/renderer', expect.objectContaining({
363
369
  HOST: '127.0.0.1',
364
370
  PORT: '4173',
@@ -366,7 +372,7 @@ describe('command helpers', () => {
366
372
  NUXT_PORT: '4173',
367
373
  NITRO_HOST: '127.0.0.1',
368
374
  NITRO_PORT: '4173'
369
- }), '/content/.renderer/preview.log');
375
+ }), '/content/.renderer/static.log');
370
376
  expect(waitForTcpPortMock).toHaveBeenCalledWith('127.0.0.1', 4173);
371
377
  expect(openUrlInBrowserMock).toHaveBeenCalledWith('http://127.0.0.1:4173');
372
378
  });
@@ -382,16 +388,17 @@ describe('command helpers', () => {
382
388
  readRuntimeStateMock.mockResolvedValueOnce(null);
383
389
  prepareRendererMock.mockResolvedValueOnce({
384
390
  rendererDir: '/renderer',
385
- rendererEnv: { HOST: '127.0.0.1', PORT: '4173', NUXT_HOST: 'preview.local', NUXT_PORT: '4321' }
391
+ rendererEnv: { HOST: '127.0.0.1', PORT: '4173', NUXT_HOST: 'preview.local', NUXT_PORT: '4321' },
392
+ rendererOutputDir: '/renderer/.output'
386
393
  });
387
394
  previewRendererInBackgroundMock.mockResolvedValueOnce(1003);
388
- await expect(runPreviewCommand('/content', { detached: true, host: '0.0.0.0' })).resolves.toBe('mdsite static running in background (PID 1003). URL: http://0.0.0.0:4321 Log: /content/.renderer/preview.log');
395
+ await expect(runPreviewCommand('/content', { detached: true, host: '0.0.0.0' })).resolves.toBe('mdsite static running in background (PID 1003). URL: http://0.0.0.0:4321 Log: /content/.renderer/static.log');
389
396
  expect(previewRendererInBackgroundMock).toHaveBeenCalledWith('/renderer', expect.objectContaining({
390
397
  NUXT_HOST: '0.0.0.0',
391
398
  HOST: '0.0.0.0',
392
399
  NITRO_HOST: '0.0.0.0',
393
400
  NUXT_PORT: '4321'
394
- }), '/content/.renderer/preview.log');
401
+ }), '/content/.renderer/static.log');
395
402
  expect(waitForTcpPortMock).toHaveBeenCalledWith('0.0.0.0', 4321);
396
403
  expect(openUrlInBrowserMock).toHaveBeenCalledWith('http://0.0.0.0:4321');
397
404
  });
@@ -399,7 +406,7 @@ describe('command helpers', () => {
399
406
  readRuntimeStateMock.mockResolvedValueOnce(null);
400
407
  previewRendererInBackgroundMock.mockResolvedValueOnce(1001);
401
408
  waitForTcpPortMock.mockResolvedValueOnce(false);
402
- await expect(runPreviewCommand('/content', { detached: true })).resolves.toBe('mdsite static running in background (PID 1001). URL: http://localhost:3000 Log: /content/.renderer/preview.log');
409
+ await expect(runPreviewCommand('/content', { detached: true })).resolves.toBe('mdsite static running in background (PID 1001). URL: http://localhost:3000 Log: /content/.renderer/static.log');
403
410
  expect(waitForTcpPortMock).toHaveBeenCalledWith('localhost', 3000);
404
411
  expect(openUrlInBrowserMock).not.toHaveBeenCalled();
405
412
  });
@@ -407,7 +414,7 @@ describe('command helpers', () => {
407
414
  readRuntimeStateMock.mockResolvedValueOnce(null);
408
415
  previewRendererInBackgroundMock.mockResolvedValueOnce(1002);
409
416
  waitForTcpPortMock.mockRejectedValueOnce(new Error('connect failed'));
410
- await expect(runPreviewCommand('/content', { detached: true })).resolves.toBe('mdsite static running in background (PID 1002). URL: http://localhost:3000 Log: /content/.renderer/preview.log');
417
+ await expect(runPreviewCommand('/content', { detached: true })).resolves.toBe('mdsite static running in background (PID 1002). URL: http://localhost:3000 Log: /content/.renderer/static.log');
411
418
  expect(openUrlInBrowserMock).not.toHaveBeenCalled();
412
419
  });
413
420
  it('runStartCommand auto-runs mdsite init when mdsite.yml is missing', async () => {
@@ -416,7 +423,7 @@ describe('command helpers', () => {
416
423
  });
417
424
  buildDefaultConfigMock.mockResolvedValue(loadedConfig.config);
418
425
  serializeConfigMock.mockReturnValue('serialized-config');
419
- readFileMock.mockResolvedValueOnce('{}').mockResolvedValueOnce('{}');
426
+ readFileMock.mockResolvedValueOnce('{}');
420
427
  const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
421
428
  await expect(runStartCommand('/content')).resolves.toBeUndefined();
422
429
  expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('No mdsite.yml found'));
@@ -431,7 +438,7 @@ describe('command helpers', () => {
431
438
  });
432
439
  buildDefaultConfigMock.mockResolvedValue(loadedConfig.config);
433
440
  serializeConfigMock.mockReturnValue('serialized-config');
434
- readFileMock.mockResolvedValueOnce('{}').mockResolvedValueOnce('{}');
441
+ readFileMock.mockResolvedValueOnce('{}');
435
442
  const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
436
443
  await expect(runPreviewCommand('/content')).resolves.toBeUndefined();
437
444
  expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('No mdsite.yml found'));
@@ -447,13 +454,13 @@ describe('command helpers', () => {
447
454
  });
448
455
  buildDefaultConfigMock.mockResolvedValue(loadedConfig.config);
449
456
  serializeConfigMock.mockReturnValue('serialized-config');
450
- readFileMock.mockResolvedValueOnce('{}').mockResolvedValueOnce('{}');
457
+ readFileMock.mockResolvedValueOnce('{}');
451
458
  previewRendererInBackgroundMock.mockResolvedValueOnce(5555);
452
459
  const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
453
- await expect(runPreviewCommand('/content', { detached: true })).resolves.toBe('mdsite static running in background (PID 5555). URL: http://localhost:3000 Log: /content/.renderer/preview.log');
460
+ await expect(runPreviewCommand('/content', { detached: true })).resolves.toBe('mdsite static running in background (PID 5555). URL: http://localhost:3000 Log: /content/.renderer/static.log');
454
461
  expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('No mdsite.yml found'));
455
462
  expect(writeFileMock).toHaveBeenCalledWith('/content/mdsite.yml', 'serialized-config', 'utf8');
456
- expect(previewRendererInBackgroundMock).toHaveBeenCalledWith('/renderer', expect.objectContaining({ TEST: '1' }), '/content/.renderer/preview.log');
463
+ expect(previewRendererInBackgroundMock).toHaveBeenCalledWith('/renderer', expect.objectContaining({ TEST: '1' }), '/content/.renderer/static.log');
457
464
  consoleSpy.mockRestore();
458
465
  });
459
466
  it('runGenerateCommand auto-runs mdsite init when mdsite.yml is missing', async () => {
@@ -462,7 +469,7 @@ describe('command helpers', () => {
462
469
  });
463
470
  buildDefaultConfigMock.mockResolvedValue(loadedConfig.config);
464
471
  serializeConfigMock.mockReturnValue('serialized-config');
465
- readFileMock.mockResolvedValueOnce('{}').mockResolvedValueOnce('{}');
472
+ readFileMock.mockResolvedValueOnce('{}');
466
473
  const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
467
474
  await expect(runGenerateCommand('/content')).resolves.toBe('Generated site synced to /content/.output/public');
468
475
  expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('No mdsite.yml found'));
@@ -475,9 +482,9 @@ describe('command helpers', () => {
475
482
  hasPreviewArtifactsMock.mockResolvedValueOnce(false);
476
483
  const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
477
484
  await expect(runPreviewCommand('/content')).resolves.toBeUndefined();
478
- expect(hasPreviewArtifactsMock).toHaveBeenCalledWith('/renderer');
485
+ expect(hasPreviewArtifactsMock).toHaveBeenCalledWith('/renderer/.output');
479
486
  expect(generateRendererMock).toHaveBeenCalledWith('/renderer', { TEST: '1' });
480
- expect(ensurePreviewArtifactsMock).toHaveBeenCalledWith('/renderer');
487
+ expect(ensurePreviewArtifactsMock).toHaveBeenCalledWith('/renderer/.output');
481
488
  expect(previewRendererForegroundMock).toHaveBeenCalledWith('/renderer', expect.objectContaining({ TEST: '1' }));
482
489
  expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('No generated output found'));
483
490
  consoleSpy.mockRestore();
@@ -487,10 +494,10 @@ describe('command helpers', () => {
487
494
  hasPreviewArtifactsMock.mockResolvedValueOnce(false);
488
495
  previewRendererInBackgroundMock.mockResolvedValueOnce(1234);
489
496
  const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
490
- await expect(runPreviewCommand('/content', { detached: true })).resolves.toBe('mdsite static running in background (PID 1234). URL: http://localhost:3000 Log: /content/.renderer/preview.log');
491
- expect(hasPreviewArtifactsMock).toHaveBeenCalledWith('/renderer');
497
+ await expect(runPreviewCommand('/content', { detached: true })).resolves.toBe('mdsite static running in background (PID 1234). URL: http://localhost:3000 Log: /content/.renderer/static.log');
498
+ expect(hasPreviewArtifactsMock).toHaveBeenCalledWith('/renderer/.output');
492
499
  expect(generateRendererMock).toHaveBeenCalledWith('/renderer', { TEST: '1' });
493
- expect(previewRendererInBackgroundMock).toHaveBeenCalledWith('/renderer', expect.objectContaining({ TEST: '1' }), '/content/.renderer/preview.log');
500
+ expect(previewRendererInBackgroundMock).toHaveBeenCalledWith('/renderer', expect.objectContaining({ TEST: '1' }), '/content/.renderer/static.log');
494
501
  consoleSpy.mockRestore();
495
502
  });
496
503
  it('runGenerateCommand syncs renderer output to the configured destination', async () => {
@@ -514,5 +521,80 @@ describe('command helpers', () => {
514
521
  expect(clearRuntimeStateMock).toHaveBeenNthCalledWith(1, '/content', loadedConfig.config, 'start');
515
522
  expect(clearRuntimeStateMock).toHaveBeenNthCalledWith(2, '/content', loadedConfig.config, 'preview');
516
523
  });
524
+ it('runCleanCommand removes both configured working dirs and reports both removals', async () => {
525
+ // `.renderer` and `.output` are reported as existing; everything else throws on access().
526
+ const existing = new Set(['/content/.renderer', '/content/.output']);
527
+ accessMock.mockImplementation(async (p) => {
528
+ if (typeof p === 'string' && existing.has(p))
529
+ return;
530
+ throw new Error('missing');
531
+ });
532
+ // No active tracked start or preview processes.
533
+ readRuntimeStateMock.mockResolvedValueOnce(null).mockResolvedValueOnce(null);
534
+ await expect(runCleanCommand('/content')).resolves.toBe('Removed .renderer and .output from /content.');
535
+ expect(rmMock).toHaveBeenCalledWith('/content/.renderer', { recursive: true, force: true });
536
+ expect(rmMock).toHaveBeenCalledWith('/content/.output', { recursive: true, force: true });
537
+ });
538
+ it('runCleanCommand reports the single existing directory when only one of the two is present', async () => {
539
+ const existing = new Set(['/content/.output']);
540
+ accessMock.mockImplementation(async (p) => {
541
+ if (typeof p === 'string' && existing.has(p))
542
+ return;
543
+ throw new Error('missing');
544
+ });
545
+ readRuntimeStateMock.mockResolvedValueOnce(null).mockResolvedValueOnce(null);
546
+ await expect(runCleanCommand('/content')).resolves.toBe('Removed .output from /content.');
547
+ expect(rmMock).toHaveBeenCalledTimes(2);
548
+ expect(rmMock).toHaveBeenCalledWith('/content/.renderer', { recursive: true, force: true });
549
+ expect(rmMock).toHaveBeenCalledWith('/content/.output', { recursive: true, force: true });
550
+ });
551
+ it('runCleanCommand is a no-op when neither configured working dir exists', async () => {
552
+ const existing = new Set();
553
+ accessMock.mockImplementation(async (p) => {
554
+ if (typeof p === 'string' && existing.has(p))
555
+ return;
556
+ throw new Error('missing');
557
+ });
558
+ readRuntimeStateMock.mockResolvedValueOnce(null).mockResolvedValueOnce(null);
559
+ await expect(runCleanCommand('/content')).resolves.toBe('Nothing to clean in /content.');
560
+ // rm() is still called on both paths so force:true silently ignores missing dirs.
561
+ expect(rmMock).toHaveBeenCalledTimes(2);
562
+ });
563
+ it('runCleanCommand refuses to delete the working dirs while a tracked start process is alive', async () => {
564
+ readRuntimeStateMock
565
+ .mockResolvedValueOnce({ kind: 'start', pid: 44 })
566
+ .mockResolvedValueOnce(null);
567
+ isProcessRunningMock.mockReturnValueOnce(true);
568
+ await expect(runCleanCommand('/content')).rejects.toThrow('mdsite live is running with PID 44. Run `mdsite stop` before `mdsite clean`.');
569
+ expect(rmMock).not.toHaveBeenCalled();
570
+ });
571
+ it('runCleanCommand refuses to delete the working dirs while a tracked preview process is alive', async () => {
572
+ readRuntimeStateMock
573
+ .mockResolvedValueOnce(null)
574
+ .mockResolvedValueOnce({ kind: 'preview', pid: 55 });
575
+ isProcessRunningMock.mockReturnValueOnce(true);
576
+ await expect(runCleanCommand('/content')).rejects.toThrow('mdsite static is running with PID 55. Run `mdsite stop` before `mdsite clean`.');
577
+ expect(rmMock).not.toHaveBeenCalled();
578
+ });
579
+ it('runCleanCommand ignores stale tracked state whose PID is no longer alive', async () => {
580
+ const existing = new Set(['/content/.renderer', '/content/.output']);
581
+ accessMock.mockImplementation(async (p) => {
582
+ if (typeof p === 'string' && existing.has(p))
583
+ return;
584
+ throw new Error('missing');
585
+ });
586
+ readRuntimeStateMock
587
+ .mockResolvedValueOnce({ kind: 'start', pid: 44 })
588
+ .mockResolvedValueOnce({ kind: 'preview', pid: 55 });
589
+ isProcessRunningMock.mockReturnValue(false);
590
+ await expect(runCleanCommand('/content')).resolves.toBe('Removed .renderer and .output from /content.');
591
+ expect(rmMock).toHaveBeenCalledWith('/content/.renderer', { recursive: true, force: true });
592
+ expect(rmMock).toHaveBeenCalledWith('/content/.output', { recursive: true, force: true });
593
+ });
594
+ it('runCleanCommand surfaces the missing-config error from loadMdsiteConfig', async () => {
595
+ loadConfigMock.mockRejectedValueOnce(new Error('Missing mdsite.yml in /content. Run `mdsite init` first.'));
596
+ await expect(runCleanCommand('/content')).rejects.toThrow('Missing mdsite.yml in /content. Run `mdsite init` first.');
597
+ expect(rmMock).not.toHaveBeenCalled();
598
+ });
517
599
  });
518
600
  //# sourceMappingURL=commands.test.js.map