@mintlify/previewing 4.0.1321 → 4.0.1323

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.
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,30 @@
1
+ import path from 'path';
2
+ import * as constants from '../constants.js';
3
+ describe('configurePreviewPath', () => {
4
+ afterEach(() => {
5
+ constants.configureSharedPreviewPath();
6
+ });
7
+ it('keeps non-dev commands outside port workspaces', () => {
8
+ constants.configureSharedPreviewPath();
9
+ expect(constants.PREVIEW_PATH).toBe(path.join(constants.DOT_MINTLIFY, 'previews', 'shared'));
10
+ });
11
+ it('isolates preview files by port', () => {
12
+ constants.configurePreviewPath(3000);
13
+ const defaultPreviewPath = constants.PREVIEW_PATH;
14
+ constants.configurePreviewPath(3001);
15
+ expect(constants.PREVIEW_PATH).not.toBe(defaultPreviewPath);
16
+ expect(constants.PREVIEW_PATH).toBe(path.join(constants.DOT_MINTLIFY, 'previews', '3001'));
17
+ expect(constants.MINT_PATH).toBe(path.join(constants.PREVIEW_PATH, 'mint'));
18
+ expect(constants.CLIENT_PATH).toBe(path.join(constants.MINT_PATH, 'apps', 'client'));
19
+ expect(constants.TAR_PATH).toBe(path.join(constants.PREVIEW_PATH, 'mint.tar.gz'));
20
+ });
21
+ it('returns to the same files for a reused port', () => {
22
+ constants.configurePreviewPath(4000);
23
+ const firstPath = constants.PREVIEW_PATH;
24
+ constants.configurePreviewPath('4000');
25
+ expect(constants.PREVIEW_PATH).toBe(firstPath);
26
+ });
27
+ it.each([0, 65536, 'not-a-port'])('rejects an invalid port: %s', (port) => {
28
+ expect(() => constants.configurePreviewPath(port)).toThrow(`invalid preview port: ${port}`);
29
+ });
30
+ });
@@ -6,6 +6,7 @@ import { dev } from '../index.js';
6
6
  import { run } from '../local-preview/run.js';
7
7
  import { silentUpdateClient } from '../local-preview/update.js';
8
8
  import * as logs from '../logging-state.js';
9
+ import { seedPreviewFromShared } from '../util.js';
9
10
  const originalChdir = process.chdir;
10
11
  vi.mock('fs-extra', () => {
11
12
  const mocks = {
@@ -23,6 +24,9 @@ vi.mock('fs-extra', () => {
23
24
  vi.mock('../local-preview/update.js', () => ({
24
25
  silentUpdateClient: vi.fn().mockResolvedValue({ needsUpdate: false, error: undefined }),
25
26
  }));
27
+ vi.mock('../local-preview/preview-lock.js', () => ({
28
+ acquirePreviewLock: vi.fn().mockReturnValue(vi.fn()),
29
+ }));
26
30
  vi.mock('is-online', () => ({
27
31
  default: vi.fn().mockResolvedValue(true),
28
32
  }));
@@ -35,11 +39,13 @@ vi.mock('../local-preview/run.js', () => ({
35
39
  vi.mock('../util.js', () => {
36
40
  return {
37
41
  maybeFixMissingWindowsEnvVar: vi.fn(),
42
+ seedPreviewFromShared: vi.fn(),
38
43
  };
39
44
  });
40
45
  const prebuildMock = vi.mocked(prebuild);
41
46
  const runMock = vi.mocked(run);
42
47
  const silentUpdateClientMock = vi.mocked(silentUpdateClient);
48
+ const seedPreviewFromSharedMock = vi.mocked(seedPreviewFromShared);
43
49
  const defaultYargs = {
44
50
  _: [],
45
51
  $0: '',
@@ -59,10 +65,16 @@ describe('dev', () => {
59
65
  it('happy path', async () => {
60
66
  await dev(defaultYargs);
61
67
  expect(addLogSpy).toHaveBeenCalledWith(expect.objectContaining({ props: { message: 'preparing local preview...' } }));
68
+ expect(seedPreviewFromSharedMock).toHaveBeenCalled();
62
69
  expect(silentUpdateClientMock).toHaveBeenCalled();
63
70
  expect(prebuildMock).toHaveBeenCalled();
64
71
  expect(runMock).toHaveBeenCalled();
65
72
  });
73
+ it('does not seed from shared when a specific client version is requested', async () => {
74
+ await dev({ ...defaultYargs, 'client-version': '1.2.3' });
75
+ expect(seedPreviewFromSharedMock).not.toHaveBeenCalled();
76
+ expect(silentUpdateClientMock).toHaveBeenCalled();
77
+ });
66
78
  it('prebuild fails', async () => {
67
79
  const errorText = 'Some OpenAPI or docs.json schema error';
68
80
  prebuildMock.mockRejectedValueOnce(new Error(errorText));
@@ -64,18 +64,18 @@ describe('downloadTargetMint', () => {
64
64
  existingVersion: versionString,
65
65
  });
66
66
  // Verify backup was created
67
- expect(existsSyncMock).toHaveBeenCalledWith(constants.DOT_MINTLIFY);
68
- expect(moveSyncMock).toHaveBeenCalledWith(constants.DOT_MINTLIFY, constants.DOT_MINTLIFY_LAST, {
67
+ expect(existsSyncMock).toHaveBeenCalledWith(constants.PREVIEW_PATH);
68
+ expect(moveSyncMock).toHaveBeenCalledWith(constants.PREVIEW_PATH, constants.PREVIEW_LAST_PATH, {
69
69
  overwrite: true,
70
70
  });
71
- expect(fse.ensureDirSync).toHaveBeenCalledWith(constants.DOT_MINTLIFY);
71
+ expect(fse.ensureDirSync).toHaveBeenCalledWith(constants.PREVIEW_PATH);
72
72
  // Verify download and extraction
73
73
  expect(getTarUrlMock).toHaveBeenCalledWith(targetMintVersion);
74
74
  expect(pipelineMock).toHaveBeenCalled();
75
75
  expect(tarMock.x).toHaveBeenCalled();
76
76
  // Verify cleanup
77
77
  expect(removeSyncMock).toHaveBeenCalledWith(constants.TAR_PATH);
78
- expect(removeSyncMock).toHaveBeenCalledWith(constants.DOT_MINTLIFY_LAST);
78
+ expect(removeSyncMock).toHaveBeenCalledWith(constants.PREVIEW_LAST_PATH);
79
79
  expect(writeFileSyncMock).toHaveBeenCalledWith(constants.VERSION_PATH, targetMintVersion);
80
80
  // Verify no restore was needed
81
81
  expect(restoreMintlifyLastMock).not.toHaveBeenCalled();
@@ -89,18 +89,18 @@ describe('downloadTargetMint', () => {
89
89
  existingVersion: versionString,
90
90
  });
91
91
  // Verify backup was created
92
- expect(existsSyncMock).toHaveBeenCalledWith(constants.DOT_MINTLIFY);
93
- expect(moveSyncMock).toHaveBeenCalledWith(constants.DOT_MINTLIFY, constants.DOT_MINTLIFY_LAST, {
92
+ expect(existsSyncMock).toHaveBeenCalledWith(constants.PREVIEW_PATH);
93
+ expect(moveSyncMock).toHaveBeenCalledWith(constants.PREVIEW_PATH, constants.PREVIEW_LAST_PATH, {
94
94
  overwrite: true,
95
95
  });
96
- expect(fse.ensureDirSync).toHaveBeenCalledWith(constants.DOT_MINTLIFY);
96
+ expect(fse.ensureDirSync).toHaveBeenCalledWith(constants.PREVIEW_PATH);
97
97
  // Verify download and extraction
98
98
  expect(getTarUrlMock).toHaveBeenCalledWith(clientVersion);
99
99
  expect(pipelineMock).toHaveBeenCalled();
100
100
  expect(tarMock.x).toHaveBeenCalled();
101
101
  // Verify cleanup
102
102
  expect(removeSyncMock).toHaveBeenCalledWith(constants.TAR_PATH);
103
- expect(removeSyncMock).toHaveBeenCalledWith(constants.DOT_MINTLIFY_LAST);
103
+ expect(removeSyncMock).toHaveBeenCalledWith(constants.PREVIEW_LAST_PATH);
104
104
  expect(writeFileSyncMock).toHaveBeenCalledWith(constants.VERSION_PATH, clientVersion);
105
105
  // Verify no restore was needed
106
106
  expect(restoreMintlifyLastMock).not.toHaveBeenCalled();
@@ -120,11 +120,11 @@ describe('downloadTargetMint', () => {
120
120
  existingVersion: versionString,
121
121
  });
122
122
  // Verify backup was created
123
- expect(existsSyncMock).toHaveBeenCalledWith(constants.DOT_MINTLIFY);
124
- expect(moveSyncMock).toHaveBeenCalledWith(constants.DOT_MINTLIFY, constants.DOT_MINTLIFY_LAST, {
123
+ expect(existsSyncMock).toHaveBeenCalledWith(constants.PREVIEW_PATH);
124
+ expect(moveSyncMock).toHaveBeenCalledWith(constants.PREVIEW_PATH, constants.PREVIEW_LAST_PATH, {
125
125
  overwrite: true,
126
126
  });
127
- expect(fse.ensureDirSync).toHaveBeenCalledWith(constants.DOT_MINTLIFY);
127
+ expect(fse.ensureDirSync).toHaveBeenCalledWith(constants.PREVIEW_PATH);
128
128
  // Verify use backup version
129
129
  expect(restoreMintlifyLastMock).toHaveBeenCalled();
130
130
  // Verify no extraction
@@ -147,11 +147,11 @@ describe('downloadTargetMint', () => {
147
147
  existingVersion: versionString,
148
148
  });
149
149
  // Verify backup was created
150
- expect(existsSyncMock).toHaveBeenCalledWith(constants.DOT_MINTLIFY);
151
- expect(moveSyncMock).toHaveBeenCalledWith(constants.DOT_MINTLIFY, constants.DOT_MINTLIFY_LAST, {
150
+ expect(existsSyncMock).toHaveBeenCalledWith(constants.PREVIEW_PATH);
151
+ expect(moveSyncMock).toHaveBeenCalledWith(constants.PREVIEW_PATH, constants.PREVIEW_LAST_PATH, {
152
152
  overwrite: true,
153
153
  });
154
- expect(fse.ensureDirSync).toHaveBeenCalledWith(constants.DOT_MINTLIFY);
154
+ expect(fse.ensureDirSync).toHaveBeenCalledWith(constants.PREVIEW_PATH);
155
155
  // Verify download success
156
156
  expect(pipelineMock).toHaveBeenCalled();
157
157
  // Verify use backup version
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,30 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { acquirePreviewLock } from '../local-preview/preview-lock.js';
5
+ describe('acquirePreviewLock', () => {
6
+ let locksDirectory;
7
+ beforeEach(() => {
8
+ locksDirectory = fs.mkdtempSync(path.join(os.tmpdir(), 'mint-preview-locks-'));
9
+ });
10
+ afterEach(() => {
11
+ fs.rmSync(locksDirectory, { recursive: true, force: true });
12
+ });
13
+ it('prevents two preview instances from mutating the same port workspace', () => {
14
+ const release = acquirePreviewLock(3000, locksDirectory);
15
+ expect(() => acquirePreviewLock(3000, locksDirectory)).toThrow('local preview on port 3000 is already starting or running');
16
+ release();
17
+ });
18
+ it('allows the workspace to be reused after release', () => {
19
+ acquirePreviewLock(3000, locksDirectory)();
20
+ const release = acquirePreviewLock(3000, locksDirectory);
21
+ expect(fs.existsSync(path.join(locksDirectory, '3000.lock'))).toBe(true);
22
+ release();
23
+ });
24
+ it('recovers a stale lock', () => {
25
+ fs.writeFileSync(path.join(locksDirectory, '3000.lock'), JSON.stringify({ pid: -1, token: 'stale' }));
26
+ const release = acquirePreviewLock(3000, locksDirectory);
27
+ expect(fs.existsSync(path.join(locksDirectory, '3000.lock'))).toBe(true);
28
+ release();
29
+ });
30
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,69 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { seedPreviewFromShared } from '../util.js';
5
+ describe('seedPreviewFromShared', () => {
6
+ let tempDir;
7
+ let sharedMintPath;
8
+ let mintPath;
9
+ let versionPath;
10
+ beforeEach(() => {
11
+ tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mint-seed-preview-'));
12
+ sharedMintPath = path.join(tempDir, 'shared', 'mint');
13
+ mintPath = path.join(tempDir, '3000', 'mint');
14
+ versionPath = path.join(mintPath, 'mint-version.txt');
15
+ });
16
+ afterEach(() => {
17
+ fs.rmSync(tempDir, { recursive: true, force: true });
18
+ });
19
+ const writeClient = (clientPath, version, extraFile) => {
20
+ fs.mkdirSync(clientPath, { recursive: true });
21
+ fs.writeFileSync(path.join(clientPath, 'mint-version.txt'), version);
22
+ if (extraFile) {
23
+ fs.writeFileSync(path.join(clientPath, extraFile), extraFile);
24
+ }
25
+ };
26
+ it('does nothing when the current workspace is the shared workspace', () => {
27
+ writeClient(sharedMintPath, '2.0.0');
28
+ seedPreviewFromShared({
29
+ sharedMintPath,
30
+ mintPath: sharedMintPath,
31
+ versionPath: path.join(sharedMintPath, 'mint-version.txt'),
32
+ });
33
+ expect(fs.readdirSync(path.join(tempDir, 'shared'))).toEqual(['mint']);
34
+ expect(fs.existsSync(mintPath)).toBe(false);
35
+ });
36
+ it('does nothing when the shared workspace has no client', () => {
37
+ seedPreviewFromShared({ sharedMintPath, mintPath, versionPath });
38
+ expect(fs.existsSync(mintPath)).toBe(false);
39
+ });
40
+ it('does nothing when the port workspace already has the shared version', () => {
41
+ writeClient(sharedMintPath, '2.0.0', 'shared-only.txt');
42
+ writeClient(mintPath, '2.0.0', 'port-only.txt');
43
+ seedPreviewFromShared({ sharedMintPath, mintPath, versionPath });
44
+ expect(fs.existsSync(path.join(mintPath, 'port-only.txt'))).toBe(true);
45
+ expect(fs.existsSync(path.join(mintPath, 'shared-only.txt'))).toBe(false);
46
+ });
47
+ it('copies the shared client when the port workspace is empty', () => {
48
+ writeClient(sharedMintPath, '2.0.0', 'client.txt');
49
+ seedPreviewFromShared({ sharedMintPath, mintPath, versionPath });
50
+ expect(fs.readFileSync(versionPath, 'utf8')).toBe('2.0.0');
51
+ expect(fs.readFileSync(path.join(mintPath, 'client.txt'), 'utf8')).toBe('client.txt');
52
+ });
53
+ it('replaces a stale port workspace with the shared client', () => {
54
+ writeClient(sharedMintPath, '2.0.0', 'new-client.txt');
55
+ writeClient(mintPath, '1.0.0', 'old-client.txt');
56
+ seedPreviewFromShared({ sharedMintPath, mintPath, versionPath });
57
+ expect(fs.readFileSync(versionPath, 'utf8')).toBe('2.0.0');
58
+ expect(fs.existsSync(path.join(mintPath, 'new-client.txt'))).toBe(true);
59
+ expect(fs.existsSync(path.join(mintPath, 'old-client.txt'))).toBe(false);
60
+ });
61
+ it('does not overwrite a newer port client with an older shared client', () => {
62
+ writeClient(sharedMintPath, '2.0.0', 'shared-only.txt');
63
+ writeClient(mintPath, '3.0.0', 'port-only.txt');
64
+ seedPreviewFromShared({ sharedMintPath, mintPath, versionPath });
65
+ expect(fs.readFileSync(versionPath, 'utf8')).toBe('3.0.0');
66
+ expect(fs.existsSync(path.join(mintPath, 'port-only.txt'))).toBe(true);
67
+ expect(fs.existsSync(path.join(mintPath, 'shared-only.txt'))).toBe(false);
68
+ });
69
+ });
@@ -1,19 +1,22 @@
1
1
  export declare const INSTALL_PATH: string;
2
2
  export declare const HOME_DIR: string;
3
3
  export declare const DOT_MINTLIFY: string;
4
- export declare const DOT_MINTLIFY_LAST: string;
5
- export declare const MINT_PATH: string;
6
- export declare const VERSION_PATH: string;
7
- export declare const CLIENT_PATH: string;
8
- export declare const NEXT_SIDE_EFFECT_PATH: string;
9
- export declare const NEXT_ROUTER_SERVER_PATH: string;
10
- export declare const NEXT_CONFIG_PATH: string;
11
- export declare const NEXT_PUBLIC_PATH: string;
12
- export declare const NEXT_PROPS_PATH: string;
4
+ export declare let PREVIEW_PATH: string;
5
+ export declare let PREVIEW_LAST_PATH: string;
6
+ export declare let MINT_PATH: string;
7
+ export declare let VERSION_PATH: string;
8
+ export declare let CLIENT_PATH: string;
9
+ export declare let NEXT_SIDE_EFFECT_PATH: string;
10
+ export declare let NEXT_ROUTER_SERVER_PATH: string;
11
+ export declare let NEXT_CONFIG_PATH: string;
12
+ export declare let NEXT_PUBLIC_PATH: string;
13
+ export declare let NEXT_PROPS_PATH: string;
14
+ export declare let TAR_PATH: string;
15
+ export declare const configurePreviewPath: (port?: string | number) => void;
16
+ export declare const configureSharedPreviewPath: () => void;
13
17
  export declare const MINT_CLIENT_RELEASES_CLOUDFRONT_URL = "https://releases.mintlify.com";
14
18
  export declare const TARGET_MINT_VERSION_URL = "https://releases.mintlify.com/mint-version.txt";
15
19
  export declare const MINT_VERSION_MAP_URL = "https://releases.mintlify.com/mint-version-map.json";
16
- export declare const TAR_PATH: string;
17
20
  export declare const CMD_EXEC_PATH: string;
18
21
  export declare const SUPPORTED_MEDIA_EXTENSIONS: string[];
19
22
  export declare const LOCAL_LINKED_CLI_VERSION = "linked to local package";
package/dist/constants.js CHANGED
@@ -5,20 +5,47 @@ import * as url from 'url';
5
5
  export const INSTALL_PATH = url.fileURLToPath(new URL('.', import.meta.url));
6
6
  export const HOME_DIR = os.homedir();
7
7
  export const DOT_MINTLIFY = path.join(HOME_DIR, '.mintlify');
8
- export const DOT_MINTLIFY_LAST = path.join(HOME_DIR, '.mintlify-last');
9
- export const MINT_PATH = path.join(DOT_MINTLIFY, 'mint');
10
- export const VERSION_PATH = path.join(MINT_PATH, 'mint-version.txt');
11
- export const CLIENT_PATH = path.join(MINT_PATH, 'apps', 'client');
12
- const NEXT_DIST_SERVER_PATH = path.join(MINT_PATH, 'node_modules', 'next', 'dist', 'server');
13
- export const NEXT_SIDE_EFFECT_PATH = path.join(NEXT_DIST_SERVER_PATH, 'next.js');
14
- export const NEXT_ROUTER_SERVER_PATH = path.join(NEXT_DIST_SERVER_PATH, 'lib', 'router-server.js');
15
- export const NEXT_CONFIG_PATH = path.join(CLIENT_PATH, '.next', 'required-server-files.json');
16
- export const NEXT_PUBLIC_PATH = path.join(CLIENT_PATH, 'public');
17
- export const NEXT_PROPS_PATH = path.join(CLIENT_PATH, 'src', '_props');
8
+ export let PREVIEW_PATH;
9
+ export let PREVIEW_LAST_PATH;
10
+ export let MINT_PATH;
11
+ export let VERSION_PATH;
12
+ export let CLIENT_PATH;
13
+ export let NEXT_SIDE_EFFECT_PATH;
14
+ export let NEXT_ROUTER_SERVER_PATH;
15
+ export let NEXT_CONFIG_PATH;
16
+ export let NEXT_PUBLIC_PATH;
17
+ export let NEXT_PROPS_PATH;
18
+ export let TAR_PATH;
19
+ const setPreviewPath = (name) => {
20
+ PREVIEW_PATH = path.join(DOT_MINTLIFY, 'previews', name);
21
+ PREVIEW_LAST_PATH = `${PREVIEW_PATH}-last`;
22
+ MINT_PATH = path.join(PREVIEW_PATH, 'mint');
23
+ VERSION_PATH = path.join(MINT_PATH, 'mint-version.txt');
24
+ CLIENT_PATH = path.join(MINT_PATH, 'apps', 'client');
25
+ const nextDistServerPath = path.join(MINT_PATH, 'node_modules', 'next', 'dist', 'server');
26
+ NEXT_SIDE_EFFECT_PATH = path.join(nextDistServerPath, 'next.js');
27
+ NEXT_ROUTER_SERVER_PATH = path.join(nextDistServerPath, 'lib', 'router-server.js');
28
+ NEXT_CONFIG_PATH = path.join(CLIENT_PATH, '.next', 'required-server-files.json');
29
+ NEXT_PUBLIC_PATH = path.join(CLIENT_PATH, 'public');
30
+ NEXT_PROPS_PATH = path.join(CLIENT_PATH, 'src', '_props');
31
+ TAR_PATH = path.join(PREVIEW_PATH, 'mint.tar.gz');
32
+ };
33
+ // A port can only belong to one local server at a time, so it gives each running
34
+ // preview an isolated workspace while still allowing warm starts to reuse files.
35
+ export const configurePreviewPath = (port = 3000) => {
36
+ const numericPort = Number(port);
37
+ if (!Number.isInteger(numericPort) || numericPort < 1 || numericPort > 65535) {
38
+ throw new Error(`invalid preview port: ${port}`);
39
+ }
40
+ setPreviewPath(String(numericPort));
41
+ };
42
+ export const configureSharedPreviewPath = () => {
43
+ setPreviewPath('shared');
44
+ };
45
+ configureSharedPreviewPath();
18
46
  export const MINT_CLIENT_RELEASES_CLOUDFRONT_URL = 'https://releases.mintlify.com';
19
47
  export const TARGET_MINT_VERSION_URL = `${MINT_CLIENT_RELEASES_CLOUDFRONT_URL}/mint-version.txt`;
20
48
  export const MINT_VERSION_MAP_URL = `${MINT_CLIENT_RELEASES_CLOUDFRONT_URL}/mint-version-map.json`;
21
- export const TAR_PATH = path.join(DOT_MINTLIFY, `mint.tar.gz`);
22
49
  // command execution location
23
50
  export const CMD_EXEC_PATH = process.cwd();
24
51
  export const SUPPORTED_MEDIA_EXTENSIONS = [
@@ -4,7 +4,7 @@ import isOnline from 'is-online';
4
4
  import yaml from 'js-yaml';
5
5
  import { pipeline } from 'node:stream/promises';
6
6
  import * as tar from 'tar';
7
- import { DOT_MINTLIFY, DOT_MINTLIFY_LAST, VERSION_PATH, TAR_PATH, TARGET_MINT_VERSION_URL, MINT_VERSION_MAP_URL, } from '../constants.js';
7
+ import { PREVIEW_PATH, PREVIEW_LAST_PATH, VERSION_PATH, TAR_PATH, TARGET_MINT_VERSION_URL, MINT_VERSION_MAP_URL, } from '../constants.js';
8
8
  import { restoreMintlifyLast, getTarUrl } from '../util.js';
9
9
  export const getLatestClientVersion = async () => {
10
10
  const hasInternet = await isOnline();
@@ -20,10 +20,10 @@ export const getLatestClientVersion = async () => {
20
20
  }
21
21
  };
22
22
  export const downloadTargetMint = async ({ targetVersion, existingVersion, }) => {
23
- if (fse.existsSync(DOT_MINTLIFY)) {
24
- fse.moveSync(DOT_MINTLIFY, DOT_MINTLIFY_LAST, { overwrite: true });
23
+ if (fse.existsSync(PREVIEW_PATH)) {
24
+ fse.moveSync(PREVIEW_PATH, PREVIEW_LAST_PATH, { overwrite: true });
25
25
  }
26
- fse.ensureDirSync(DOT_MINTLIFY);
26
+ fse.ensureDirSync(PREVIEW_PATH);
27
27
  const tarUrl = getTarUrl(targetVersion);
28
28
  let currentVersion = targetVersion.trim();
29
29
  try {
@@ -43,7 +43,7 @@ export const downloadTargetMint = async ({ targetVersion, existingVersion, }) =>
43
43
  tar.x({
44
44
  sync: true,
45
45
  file: TAR_PATH,
46
- cwd: DOT_MINTLIFY,
46
+ cwd: PREVIEW_PATH,
47
47
  onwarn: (_code, message) => {
48
48
  throw new Error(message);
49
49
  },
@@ -60,8 +60,8 @@ export const downloadTargetMint = async ({ targetVersion, existingVersion, }) =>
60
60
  }
61
61
  }
62
62
  fse.removeSync(TAR_PATH);
63
- if (fse.existsSync(DOT_MINTLIFY_LAST)) {
64
- fse.removeSync(DOT_MINTLIFY_LAST);
63
+ if (fse.existsSync(PREVIEW_LAST_PATH)) {
64
+ fse.removeSync(PREVIEW_LAST_PATH);
65
65
  }
66
66
  fse.writeFileSync(VERSION_PATH, currentVersion);
67
67
  };
@@ -9,7 +9,7 @@ import isOnline from 'is-online';
9
9
  import os from 'os';
10
10
  import path from 'path';
11
11
  import { fileURLToPath } from 'url';
12
- import { CLIENT_PATH, DOT_MINTLIFY, CMD_EXEC_PATH, VERSION_PATH, NEXT_PUBLIC_PATH, NEXT_PROPS_PATH, } from '../constants.js';
12
+ import { CLIENT_PATH, CMD_EXEC_PATH, VERSION_PATH, NEXT_PUBLIC_PATH, NEXT_PROPS_PATH, PREVIEW_PATH, configureSharedPreviewPath, } from '../constants.js';
13
13
  import { addLog, clearLogs } from '../logging-state.js';
14
14
  import { ErrorLog, SpinnerLog, SuccessLog } from '../logs.js';
15
15
  import { getGroupFilteredRoutes } from './getGroupFilteredRoutes.js';
@@ -194,13 +194,14 @@ async function generateExportArchive(routes, outputPath, groups) {
194
194
  }
195
195
  }
196
196
  export const exportSite = async (argv) => {
197
+ configureSharedPreviewPath();
197
198
  const hasInternet = await isOnline();
198
199
  const clientVersion = argv['client-version'];
199
200
  const groups = argv.groups;
200
201
  const cliVersion = argv.cliVersion;
201
202
  const disableOpenApi = argv.disableOpenapi;
202
203
  const outputPath = path.resolve(argv.output);
203
- await fse.ensureDir(DOT_MINTLIFY);
204
+ await fse.ensureDir(PREVIEW_PATH);
204
205
  const versionString = (await pathExists(VERSION_PATH))
205
206
  ? fse.readFileSync(VERSION_PATH, 'utf8')
206
207
  : null;
@@ -3,10 +3,12 @@ import { getFileListSync, prebuild } from '@mintlify/prebuild';
3
3
  import fse, { pathExists } from 'fs-extra';
4
4
  import isOnline from 'is-online';
5
5
  import pathUtil from 'path';
6
- import { CLIENT_PATH, DOT_MINTLIFY, CMD_EXEC_PATH, VERSION_PATH, NEXT_PUBLIC_PATH, NEXT_PROPS_PATH, } from '../constants.js';
6
+ import { CLIENT_PATH, CMD_EXEC_PATH, VERSION_PATH, NEXT_PUBLIC_PATH, NEXT_PROPS_PATH, PREVIEW_PATH, configurePreviewPath, } from '../constants.js';
7
7
  import { addLog, clearLogs } from '../logging-state.js';
8
8
  import { ErrorLog, SpinnerLog } from '../logs.js';
9
+ import { seedPreviewFromShared } from '../util.js';
9
10
  import { timeDev, timeDevSync } from './dev-timing.js';
11
+ import { acquirePreviewLock } from './preview-lock.js';
10
12
  import { run } from './run.js';
11
13
  import { silentUpdateClient } from './update.js';
12
14
  const PRUNE_EXEMPT_PREFIXES = ['favicons/', '.mint-content-source'];
@@ -27,7 +29,7 @@ const prunePublicOrphans = () => {
27
29
  }
28
30
  }
29
31
  };
30
- const dev = async (argv) => {
32
+ const startDev = async (argv) => {
31
33
  const hasInternet = await timeDev('check internet', () => isOnline());
32
34
  const localSchema = argv['local-schema'];
33
35
  const clientVersion = argv['client-version'];
@@ -37,7 +39,10 @@ const dev = async (argv) => {
37
39
  const cliVersion = argv.cliVersion;
38
40
  const disableOpenApi = argv.disableOpenapi;
39
41
  const disablePrefetch = argv.disablePrefetch;
40
- await timeDev('ensure .mintlify directory', () => fse.ensureDir(DOT_MINTLIFY));
42
+ await timeDev('ensure preview directory', () => fse.ensureDir(PREVIEW_PATH));
43
+ if (!localClientVersion && !clientVersion) {
44
+ timeDevSync('seed preview from shared workspace', () => seedPreviewFromShared());
45
+ }
41
46
  const versionString = await timeDev('read local client version', async () => {
42
47
  return (await pathExists(VERSION_PATH)) ? fse.readFileSync(VERSION_PATH, 'utf8') : null;
43
48
  });
@@ -104,4 +109,16 @@ const dev = async (argv) => {
104
109
  }
105
110
  await timeDev('start preview runtime', () => run({ ...argv, needsUpdate, fileImportsMap }));
106
111
  };
112
+ const dev = async (argv) => {
113
+ const port = Number(argv.port ?? 3000);
114
+ configurePreviewPath(port);
115
+ const releasePreviewLock = acquirePreviewLock(port);
116
+ try {
117
+ await startDev(argv);
118
+ }
119
+ catch (error) {
120
+ releasePreviewLock();
121
+ throw error;
122
+ }
123
+ };
107
124
  export default dev;
@@ -0,0 +1 @@
1
+ export declare const acquirePreviewLock: (port: number, locksDirectory?: string) => (() => void);
@@ -0,0 +1,76 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { DOT_MINTLIFY } from '../constants.js';
5
+ const isNodeError = (error, code) => error instanceof Error && 'code' in error && error.code === code;
6
+ const readLockOwner = (lockPath) => {
7
+ try {
8
+ const owner = JSON.parse(fs.readFileSync(lockPath, 'utf8'));
9
+ if (Number.isInteger(owner.pid) && owner.pid > 0 && typeof owner.token === 'string') {
10
+ return owner;
11
+ }
12
+ }
13
+ catch { }
14
+ return undefined;
15
+ };
16
+ const isProcessRunning = (pid) => {
17
+ try {
18
+ process.kill(pid, 0);
19
+ return true;
20
+ }
21
+ catch (error) {
22
+ return !isNodeError(error, 'ESRCH');
23
+ }
24
+ };
25
+ export const acquirePreviewLock = (port, locksDirectory = path.join(DOT_MINTLIFY, 'preview-locks')) => {
26
+ fs.mkdirSync(locksDirectory, { recursive: true });
27
+ const lockPath = path.join(locksDirectory, `${port}.lock`);
28
+ const token = `${process.pid}-${randomUUID()}`;
29
+ const candidatePath = path.join(locksDirectory, `.${port}-${token}.candidate`);
30
+ fs.writeFileSync(candidatePath, JSON.stringify({ pid: process.pid, token }), { flag: 'wx' });
31
+ let acquired = false;
32
+ try {
33
+ for (let attempt = 0; attempt < 5; attempt++) {
34
+ try {
35
+ fs.linkSync(candidatePath, lockPath);
36
+ acquired = true;
37
+ break;
38
+ }
39
+ catch (error) {
40
+ if (!isNodeError(error, 'EEXIST'))
41
+ throw error;
42
+ const owner = readLockOwner(lockPath);
43
+ if (owner && isProcessRunning(owner.pid)) {
44
+ throw new Error(`local preview on port ${port} is already starting or running`);
45
+ }
46
+ const stalePath = `${lockPath}.stale-${token}`;
47
+ try {
48
+ fs.renameSync(lockPath, stalePath);
49
+ fs.rmSync(stalePath, { force: true });
50
+ }
51
+ catch (renameError) {
52
+ if (!isNodeError(renameError, 'ENOENT'))
53
+ throw renameError;
54
+ }
55
+ }
56
+ }
57
+ }
58
+ finally {
59
+ fs.rmSync(candidatePath, { force: true });
60
+ }
61
+ if (!acquired) {
62
+ throw new Error(`could not acquire local preview workspace for port ${port}`);
63
+ }
64
+ let released = false;
65
+ const release = () => {
66
+ if (released)
67
+ return;
68
+ released = true;
69
+ process.off('exit', release);
70
+ if (readLockOwner(lockPath)?.token === token) {
71
+ fs.rmSync(lockPath, { force: true });
72
+ }
73
+ };
74
+ process.once('exit', release);
75
+ return release;
76
+ };
@@ -2,11 +2,11 @@ import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { spawn } from 'child_process';
3
3
  import fse from 'fs-extra';
4
4
  import path from 'path';
5
- import { LOCAL_LINKED_CLI_VERSION, MINT_PATH, DOT_MINTLIFY, CLIENT_PATH } from '../constants.js';
5
+ import { LOCAL_LINKED_CLI_VERSION, MINT_PATH, PREVIEW_PATH, CLIENT_PATH } from '../constants.js';
6
6
  import { clearLogs, addLog } from '../logging-state.js';
7
7
  import { SpinnerLog } from '../logs.js';
8
8
  import { getLatestClientVersion, tryDownloadTargetMint, getCompatibleClientVersion, } from './client.js';
9
- const MINT_BACKUP_PATH = path.join(DOT_MINTLIFY, 'mint-backup');
9
+ const getMintBackupPath = () => path.join(PREVIEW_PATH, 'mint-backup');
10
10
  const isCurrentlySymlinked = async () => {
11
11
  if (!(await fse.pathExists(MINT_PATH)))
12
12
  return false;
@@ -47,11 +47,12 @@ const buildClient = async () => {
47
47
  };
48
48
  const restoreFromBackup = async () => {
49
49
  try {
50
+ const mintBackupPath = getMintBackupPath();
50
51
  if (await isCurrentlySymlinked()) {
51
52
  await fse.remove(MINT_PATH);
52
53
  }
53
- if (await fse.pathExists(MINT_BACKUP_PATH)) {
54
- await fse.move(MINT_BACKUP_PATH, MINT_PATH);
54
+ if (await fse.pathExists(mintBackupPath)) {
55
+ await fse.move(mintBackupPath, MINT_PATH);
55
56
  }
56
57
  return undefined;
57
58
  }
@@ -61,6 +62,7 @@ const restoreFromBackup = async () => {
61
62
  };
62
63
  const symlinkLocalClient = async (targetPath) => {
63
64
  try {
65
+ const mintBackupPath = getMintBackupPath();
64
66
  const resolvedPath = path.resolve(targetPath);
65
67
  if (!(await fse.pathExists(resolvedPath))) {
66
68
  return `Path does not exist: ${resolvedPath}`;
@@ -75,10 +77,10 @@ const symlinkLocalClient = async (targetPath) => {
75
77
  await fse.remove(MINT_PATH);
76
78
  }
77
79
  else {
78
- if (await fse.pathExists(MINT_BACKUP_PATH)) {
79
- await fse.remove(MINT_BACKUP_PATH);
80
+ if (await fse.pathExists(mintBackupPath)) {
81
+ await fse.remove(mintBackupPath);
80
82
  }
81
- await fse.move(MINT_PATH, MINT_BACKUP_PATH);
83
+ await fse.move(MINT_PATH, mintBackupPath);
82
84
  }
83
85
  }
84
86
  await fse.ensureDir(path.dirname(MINT_PATH));
@@ -2,12 +2,13 @@ import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { prebuild } from '@mintlify/prebuild';
3
3
  import fse, { pathExists } from 'fs-extra';
4
4
  import isOnline from 'is-online';
5
- import { CLIENT_PATH, DOT_MINTLIFY, CMD_EXEC_PATH, VERSION_PATH, NEXT_PUBLIC_PATH, NEXT_PROPS_PATH, } from '../constants.js';
5
+ import { CLIENT_PATH, CMD_EXEC_PATH, VERSION_PATH, NEXT_PUBLIC_PATH, NEXT_PROPS_PATH, PREVIEW_PATH, configureSharedPreviewPath, } from '../constants.js';
6
6
  import { addLog, clearLogs } from '../logging-state.js';
7
7
  import { ErrorLog, SpinnerLog, SuccessLog } from '../logs.js';
8
8
  import { timeDev, timeDevSync } from './dev-timing.js';
9
9
  import { silentUpdateClient } from './update.js';
10
10
  const validateBuild = async (argv) => {
11
+ configureSharedPreviewPath();
11
12
  const hasInternet = await timeDev('check internet', () => isOnline());
12
13
  const localSchema = argv['local-schema'];
13
14
  const clientVersion = argv['client-version'];
@@ -15,7 +16,7 @@ const validateBuild = async (argv) => {
15
16
  const groups = argv.groups;
16
17
  const cliVersion = argv.cliVersion;
17
18
  const disableOpenApi = argv.disableOpenapi;
18
- await timeDev('ensure .mintlify directory', () => fse.ensureDir(DOT_MINTLIFY));
19
+ await timeDev('ensure preview directory', () => fse.ensureDir(PREVIEW_PATH));
19
20
  const versionString = await timeDev('read local client version', async () => {
20
21
  return (await pathExists(VERSION_PATH)) ? fse.readFileSync(VERSION_PATH, 'utf8') : null;
21
22
  });