@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.
- package/dist/__tests__/constants.test.d.ts +1 -0
- package/dist/__tests__/constants.test.js +30 -0
- package/dist/__tests__/dev.test.js +12 -0
- package/dist/__tests__/downloadTargetMint.test.js +14 -14
- package/dist/__tests__/preview-lock.test.d.ts +1 -0
- package/dist/__tests__/preview-lock.test.js +30 -0
- package/dist/__tests__/seedPreviewFromShared.test.d.ts +1 -0
- package/dist/__tests__/seedPreviewFromShared.test.js +69 -0
- package/dist/constants.d.ts +13 -10
- package/dist/constants.js +38 -11
- package/dist/local-preview/client.js +7 -7
- package/dist/local-preview/export.js +3 -2
- package/dist/local-preview/index.js +20 -3
- package/dist/local-preview/preview-lock.d.ts +1 -0
- package/dist/local-preview/preview-lock.js +76 -0
- package/dist/local-preview/update.js +9 -7
- package/dist/local-preview/validate-build.js +3 -2
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/util.d.ts +5 -0
- package/dist/util.js +27 -4
- package/package.json +5 -5
|
@@ -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.
|
|
68
|
-
expect(moveSyncMock).toHaveBeenCalledWith(constants.
|
|
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.
|
|
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.
|
|
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.
|
|
93
|
-
expect(moveSyncMock).toHaveBeenCalledWith(constants.
|
|
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.
|
|
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.
|
|
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.
|
|
124
|
-
expect(moveSyncMock).toHaveBeenCalledWith(constants.
|
|
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.
|
|
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.
|
|
151
|
-
expect(moveSyncMock).toHaveBeenCalledWith(constants.
|
|
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.
|
|
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
|
+
});
|
package/dist/constants.d.ts
CHANGED
|
@@ -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
|
|
5
|
-
export declare
|
|
6
|
-
export declare
|
|
7
|
-
export declare
|
|
8
|
-
export declare
|
|
9
|
-
export declare
|
|
10
|
-
export declare
|
|
11
|
-
export declare
|
|
12
|
-
export declare
|
|
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
|
|
9
|
-
export
|
|
10
|
-
export
|
|
11
|
-
export
|
|
12
|
-
|
|
13
|
-
export
|
|
14
|
-
export
|
|
15
|
-
export
|
|
16
|
-
export
|
|
17
|
-
export
|
|
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 {
|
|
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(
|
|
24
|
-
fse.moveSync(
|
|
23
|
+
if (fse.existsSync(PREVIEW_PATH)) {
|
|
24
|
+
fse.moveSync(PREVIEW_PATH, PREVIEW_LAST_PATH, { overwrite: true });
|
|
25
25
|
}
|
|
26
|
-
fse.ensureDirSync(
|
|
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:
|
|
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(
|
|
64
|
-
fse.removeSync(
|
|
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,
|
|
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(
|
|
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,
|
|
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
|
|
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
|
|
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,
|
|
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
|
|
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(
|
|
54
|
-
await fse.move(
|
|
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(
|
|
79
|
-
await fse.remove(
|
|
80
|
+
if (await fse.pathExists(mintBackupPath)) {
|
|
81
|
+
await fse.remove(mintBackupPath);
|
|
80
82
|
}
|
|
81
|
-
await fse.move(MINT_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,
|
|
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
|
|
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
|
});
|