@mintlify/previewing 4.0.1461 → 4.0.1463

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 (55) hide show
  1. package/dist/__tests__/listener-graphql-packages.test.d.ts +1 -0
  2. package/dist/__tests__/listener-graphql-packages.test.js +68 -0
  3. package/dist/__tests__/listener-package-manifest.test.d.ts +1 -0
  4. package/dist/__tests__/listener-package-manifest.test.js +103 -0
  5. package/dist/__tests__/packages-builder.test.d.ts +1 -0
  6. package/dist/__tests__/packages-builder.test.js +88 -0
  7. package/dist/__tests__/packages-collect.test.d.ts +1 -0
  8. package/dist/__tests__/packages-collect.test.js +387 -0
  9. package/dist/__tests__/packages-dev-snippets.test.d.ts +1 -0
  10. package/dist/__tests__/packages-dev-snippets.test.js +457 -0
  11. package/dist/__tests__/packages-dev.test.d.ts +1 -0
  12. package/dist/__tests__/packages-dev.test.js +1304 -0
  13. package/dist/__tests__/packages-flag.test.d.ts +1 -0
  14. package/dist/__tests__/packages-flag.test.js +146 -0
  15. package/dist/__tests__/packages-imports.test.d.ts +1 -0
  16. package/dist/__tests__/packages-imports.test.js +68 -0
  17. package/dist/__tests__/packages-island-errors.test.d.ts +1 -0
  18. package/dist/__tests__/packages-island-errors.test.js +128 -0
  19. package/dist/__tests__/packages-serve.test.d.ts +1 -0
  20. package/dist/__tests__/packages-serve.test.js +73 -0
  21. package/dist/__tests__/packages-setup.test.d.ts +1 -0
  22. package/dist/__tests__/packages-setup.test.js +160 -0
  23. package/dist/index.d.ts +1 -0
  24. package/dist/index.js +1 -0
  25. package/dist/local-preview/index.js +6 -0
  26. package/dist/local-preview/listener/index.d.ts +1 -0
  27. package/dist/local-preview/listener/index.js +10 -1
  28. package/dist/local-preview/listener/regenerateAllSnippets.js +2 -0
  29. package/dist/local-preview/listener/resolve-imports-from-import-closure.js +3 -0
  30. package/dist/local-preview/listener/resolveAllImports.js +2 -0
  31. package/dist/local-preview/packages/dev.d.ts +88 -0
  32. package/dist/local-preview/packages/dev.js +478 -0
  33. package/dist/local-preview/packages/index.d.ts +8 -0
  34. package/dist/local-preview/packages/index.js +89 -0
  35. package/dist/local-preview/packages/serve.d.ts +8 -0
  36. package/dist/local-preview/packages/serve.js +41 -0
  37. package/dist/local-preview/packages/state.d.ts +6 -0
  38. package/dist/local-preview/packages/state.js +6 -0
  39. package/dist/local-preview/run.js +8 -0
  40. package/dist/logging-state.d.ts +1 -0
  41. package/dist/logging-state.js +5 -0
  42. package/dist/packages/builder.d.ts +6 -0
  43. package/dist/packages/builder.js +17 -0
  44. package/dist/packages/collect.d.ts +61 -0
  45. package/dist/packages/collect.js +240 -0
  46. package/dist/packages/errno.d.ts +1 -0
  47. package/dist/packages/errno.js +3 -0
  48. package/dist/packages/flag.d.ts +1 -0
  49. package/dist/packages/flag.js +1 -0
  50. package/dist/packages/index.d.ts +3 -0
  51. package/dist/packages/index.js +3 -0
  52. package/dist/packages/setup.d.ts +18 -0
  53. package/dist/packages/setup.js +81 -0
  54. package/dist/tsconfig.tsbuildinfo +1 -1
  55. package/package.json +6 -3
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,146 @@
1
+ import { prebuild } from '@mintlify/prebuild';
2
+ import { mockProcessExit } from 'vitest-mock-process';
3
+ import { configurePreviewPath } from '../constants.js';
4
+ import { dev, isNativePackagesEnabled } from '../index.js';
5
+ import listener from '../local-preview/listener/index.js';
6
+ import { packageRoute, setPackagesRebuiltListener, startPackagesDev, } from '../local-preview/packages/index.js';
7
+ const captured = vi.hoisted(() => ({ app: undefined }));
8
+ vi.mock('http', async (importOriginal) => {
9
+ const actual = await importOriginal();
10
+ const createServer = (app) => {
11
+ captured.app = app;
12
+ return { listen: vi.fn() };
13
+ };
14
+ return { ...actual, default: { ...actual, createServer }, createServer };
15
+ });
16
+ vi.mock('socket.io', () => ({
17
+ Server: vi.fn(() => ({ emit: vi.fn(), on: vi.fn() })),
18
+ }));
19
+ vi.mock('fs-extra', () => {
20
+ const mocks = {
21
+ ensureDir: vi.fn().mockResolvedValue(undefined),
22
+ pathExists: vi.fn().mockResolvedValue(true),
23
+ readFileSync: vi.fn().mockReturnValue('0.0.100'),
24
+ emptyDirSync: vi.fn(),
25
+ outputFileSync: vi.fn(),
26
+ };
27
+ return { ...mocks, default: mocks };
28
+ });
29
+ vi.mock('@mintlify/prebuild', () => ({
30
+ prebuild: vi.fn().mockResolvedValue(undefined),
31
+ getFileListSync: vi.fn().mockReturnValue([]),
32
+ }));
33
+ vi.mock('is-online', () => ({ default: vi.fn().mockResolvedValue(true) }));
34
+ vi.mock('../local-preview/update.js', () => ({
35
+ silentUpdateClient: vi.fn().mockResolvedValue({ needsUpdate: false, error: undefined }),
36
+ }));
37
+ vi.mock('../local-preview/preview-lock.js', () => ({
38
+ acquirePreviewLock: vi.fn().mockReturnValue(vi.fn()),
39
+ }));
40
+ vi.mock('../util.js', () => ({
41
+ maybeFixMissingWindowsEnvVar: vi.fn(),
42
+ seedPreviewFromShared: vi.fn(),
43
+ }));
44
+ vi.mock('../local-preview/run.js', () => ({ run: vi.fn().mockResolvedValue(undefined) }));
45
+ vi.mock('../local-preview/setupNext.js', () => ({
46
+ setupNext: vi.fn(async () => (_req, res) => {
47
+ res.end('next');
48
+ }),
49
+ }));
50
+ vi.mock('../local-preview/listener/index.js', () => ({
51
+ default: vi.fn(),
52
+ initializeImportCache: vi.fn(),
53
+ initializeFrontmatterHashCache: vi.fn(),
54
+ initializeDocsConfigHashCache: vi.fn(),
55
+ }));
56
+ vi.mock('../local-preview/listener/referencedFiles.js', () => ({
57
+ refreshTrackedReferencedFiles: vi.fn(),
58
+ }));
59
+ vi.mock('../local-preview/on-demand-page.js', () => ({
60
+ ensurePageCompiled: vi.fn().mockResolvedValue(undefined),
61
+ getFailedCompileEvents: vi.fn().mockReturnValue([]),
62
+ markOnDemandPagesReady: vi.fn(),
63
+ setCompileListener: vi.fn(),
64
+ }));
65
+ vi.mock('../local-preview/packages/index.js', () => ({
66
+ startPackagesDev: vi.fn().mockResolvedValue(undefined),
67
+ setPackagesRebuiltListener: vi.fn(),
68
+ packageRoute: {
69
+ prefix: '/_mintlify/pkg',
70
+ handler: vi.fn((_req, res) => res.end('pkg')),
71
+ },
72
+ }));
73
+ const originalChdir = process.chdir;
74
+ const processExitMock = mockProcessExit();
75
+ const argv = { _: [], $0: '', packageName: 'mint', cliVersion: '1.0.0' };
76
+ const setFlag = (value) => vi.stubEnv('MINTLIFY_NATIVE_NPM_PACKAGES', value);
77
+ const startRun = async () => {
78
+ const { run } = await vi.importActual('../local-preview/run.js');
79
+ await run({ ...argv, needsUpdate: false });
80
+ };
81
+ const get = async (path) => {
82
+ const { createServer } = await vi.importActual('http');
83
+ const server = createServer(captured.app);
84
+ await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
85
+ try {
86
+ const { port } = server.address();
87
+ return await (await fetch(`http://127.0.0.1:${port}${path}`)).text();
88
+ }
89
+ finally {
90
+ await new Promise((resolve) => server.close(resolve));
91
+ }
92
+ };
93
+ beforeEach(() => {
94
+ vi.clearAllMocks();
95
+ process.chdir = vi.fn();
96
+ captured.app = undefined;
97
+ });
98
+ afterEach(() => {
99
+ process.chdir = originalChdir;
100
+ vi.unstubAllEnvs();
101
+ });
102
+ describe('isNativePackagesEnabled', () => {
103
+ it('is on only when MINTLIFY_NATIVE_NPM_PACKAGES is 1', () => {
104
+ setFlag(undefined);
105
+ expect(isNativePackagesEnabled()).toBe(false);
106
+ setFlag('true');
107
+ expect(isNativePackagesEnabled()).toBe(false);
108
+ setFlag('0');
109
+ expect(isNativePackagesEnabled()).toBe(false);
110
+ setFlag('1');
111
+ expect(isNativePackagesEnabled()).toBe(true);
112
+ });
113
+ });
114
+ describe('mint dev with native npm packages off', () => {
115
+ beforeEach(() => setFlag(undefined));
116
+ it('skips the packages runtime and tells prebuild to ignore packages', async () => {
117
+ await dev(argv);
118
+ expect(processExitMock).not.toHaveBeenCalled();
119
+ expect(startPackagesDev).not.toHaveBeenCalled();
120
+ expect(prebuild).toHaveBeenCalledWith(expect.any(String), expect.objectContaining({ nativePackages: false }));
121
+ });
122
+ it('lets /_mintlify/pkg requests fall through to the next handler', async () => {
123
+ configurePreviewPath(3000);
124
+ await startRun();
125
+ expect(await get('/_mintlify/pkg/x/client/a.js')).toBe('next');
126
+ expect(packageRoute.handler).not.toHaveBeenCalled();
127
+ expect(setPackagesRebuiltListener).not.toHaveBeenCalled();
128
+ expect(listener).toHaveBeenCalledWith(expect.any(Function), expect.objectContaining({ nativePackages: false }), expect.any(Promise));
129
+ });
130
+ });
131
+ describe('mint dev with MINTLIFY_NATIVE_NPM_PACKAGES=1', () => {
132
+ beforeEach(() => setFlag('1'));
133
+ it('starts the packages runtime and tells prebuild to read packages', async () => {
134
+ await dev(argv);
135
+ expect(processExitMock).not.toHaveBeenCalled();
136
+ expect(startPackagesDev).toHaveBeenCalledOnce();
137
+ expect(prebuild).toHaveBeenCalledWith(expect.any(String), expect.objectContaining({ nativePackages: true }));
138
+ });
139
+ it('serves /_mintlify/pkg from the package route', async () => {
140
+ configurePreviewPath(3000);
141
+ await startRun();
142
+ expect(await get('/_mintlify/pkg/x/client/a.js')).toBe('pkg');
143
+ expect(setPackagesRebuiltListener).toHaveBeenCalledWith(expect.any(Function));
144
+ expect(listener).toHaveBeenCalledWith(expect.any(Function), expect.objectContaining({ nativePackages: true }), expect.any(Promise));
145
+ });
146
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,68 @@
1
+ import { findAndRemoveImports, getAST, stringifyTree } from '@mintlify/common';
2
+ import fse from 'fs-extra';
3
+ import { mkdtemp, rm } from 'fs/promises';
4
+ import { tmpdir } from 'os';
5
+ import { join } from 'path';
6
+ const testPaths = vi.hoisted(() => ({ cmd: '', public: '' }));
7
+ vi.mock('../constants.js', () => ({
8
+ get CMD_EXEC_PATH() {
9
+ return testPaths.cmd;
10
+ },
11
+ get NEXT_PUBLIC_PATH() {
12
+ return testPaths.public;
13
+ },
14
+ }));
15
+ vi.mock('../local-preview/listener/utils.js', () => ({
16
+ getCurrentVariables: vi.fn(async () => ({})),
17
+ handleParseError: vi.fn(),
18
+ }));
19
+ import { resolvePageImports } from '../local-preview/listener/resolve-page-imports.js';
20
+ import { setPackageImportOptions } from '../local-preview/packages/state.js';
21
+ const PAGE = `import { LineChart } from "recharts";
22
+ import { Toggle } from "/snippets/toggle.jsx";
23
+ import { Note } from "/snippets/note.mdx";
24
+
25
+ <LineChart width={400} height={200} />
26
+
27
+ <Toggle />
28
+
29
+ <Note />
30
+ `;
31
+ const resolve = async () => {
32
+ const importsResponse = await findAndRemoveImports(getAST(PAGE, '/page.mdx'));
33
+ return stringifyTree(await resolvePageImports({ ...importsResponse, filename: 'page.mdx' }));
34
+ };
35
+ describe('page imports in mint dev with npm packages', () => {
36
+ let root;
37
+ beforeEach(async () => {
38
+ root = await mkdtemp(join(tmpdir(), 'packages-imports-'));
39
+ testPaths.cmd = join(root, 'docs');
40
+ testPaths.public = join(root, 'public');
41
+ await fse.ensureDir(testPaths.public);
42
+ await fse.outputFile(join(testPaths.cmd, 'snippets/toggle.jsx'), 'import { Switch } from "@acme/ui";\nexport const Toggle = () => <Switch />;\n');
43
+ await fse.outputFile(join(testPaths.cmd, 'snippets/note.mdx'), 'export const Note = () => <p>Hi</p>;\n');
44
+ vi.spyOn(console, 'log').mockImplementation(() => { });
45
+ });
46
+ afterEach(async () => {
47
+ setPackageImportOptions(undefined);
48
+ vi.mocked(console.log).mockRestore();
49
+ await rm(root, { recursive: true, force: true });
50
+ });
51
+ it('strips package imports when packages are off', async () => {
52
+ const output = await resolve();
53
+ expect(output).not.toContain('import');
54
+ expect(output).toContain('export const Toggle');
55
+ expect(output).toContain('export const Note');
56
+ });
57
+ it('keeps package and package snippet imports when packages are on', async () => {
58
+ setPackageImportOptions({
59
+ packageNames: new Set(['recharts', '@acme/ui']),
60
+ packageSnippets: new Set(['/snippets/toggle.jsx']),
61
+ });
62
+ const output = await resolve();
63
+ expect(output).toContain('import {LineChart} from "recharts";');
64
+ expect(output).toContain('import {Toggle} from "/snippets/toggle.jsx";');
65
+ expect(output).not.toContain('export const Toggle');
66
+ expect(output).toContain('export const Note');
67
+ });
68
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,128 @@
1
+ import { getFileCategory } from '@mintlify/common';
2
+ import fse from 'fs-extra';
3
+ import { join } from 'path';
4
+ vi.mock('../constants.js', async (importOriginal) => {
5
+ const actual = await importOriginal();
6
+ const { mkdtempSync } = await import('fs');
7
+ const { tmpdir } = await import('os');
8
+ const path = await import('path');
9
+ const root = mkdtempSync(path.join(tmpdir(), 'packages-island-errors-'));
10
+ return {
11
+ ...actual,
12
+ CMD_EXEC_PATH: path.join(root, 'docs'),
13
+ PREVIEW_PATH: path.join(root, 'preview'),
14
+ NEXT_PROPS_PATH: path.join(root, 'props'),
15
+ };
16
+ });
17
+ vi.mock('../local-preview/listener/regenerateAllSnippets.js', () => ({
18
+ regenerateAllSnippets: vi.fn().mockResolvedValue([]),
19
+ }));
20
+ vi.mock('@mintlify/package-builder', async (importOriginal) => {
21
+ const actual = await importOriginal();
22
+ const build = async () => ({
23
+ ok: true,
24
+ manifest: {
25
+ version: 1,
26
+ builderVersion: 'test',
27
+ buildId: 'b',
28
+ depsHash: 'd',
29
+ islandsHash: 'i',
30
+ react: { version: '18.3.1', domVersion: '18.3.1' },
31
+ packages: {},
32
+ css: [],
33
+ islands: {},
34
+ files: [],
35
+ diagnostics: [],
36
+ },
37
+ localFiles: [],
38
+ missingLocalFiles: [],
39
+ stats: {},
40
+ });
41
+ return { ...actual, build };
42
+ });
43
+ import { CMD_EXEC_PATH } from '../constants.js';
44
+ import { notifyPackageInputChange, startPackagesDev } from '../local-preview/packages/index.js';
45
+ import { clearErrorLogs, getCurrentErrorLogs, setLoggingCallbacks } from '../logging-state.js';
46
+ const BROKEN = 'import { LineChart } from "recharts";\n\n<LineChart onClick={() => {}} />\n';
47
+ const FIXED = 'import { LineChart } from "recharts";\n\n<LineChart width={400} />\n';
48
+ const errorMessages = () => getCurrentErrorLogs().map((log) => log.props.message);
49
+ const islandErrorCount = () => errorMessages().filter((message) => /^npm packages: bad\.mdx:3 /.test(message)).length;
50
+ const packageErrors = () => errorMessages().filter((message) => message.startsWith('npm packages:'));
51
+ const save = async (filename, content) => {
52
+ clearErrorLogs();
53
+ if (content !== undefined)
54
+ await fse.outputFile(join(CMD_EXEC_PATH, filename), content);
55
+ await notifyPackageInputChange(filename, getFileCategory(filename));
56
+ };
57
+ describe('npm package island errors in the mint dev log', () => {
58
+ beforeAll(() => {
59
+ setLoggingCallbacks(() => { }, () => { }, () => { });
60
+ });
61
+ beforeEach(async () => {
62
+ await fse.emptyDir(CMD_EXEC_PATH);
63
+ await fse.outputJson(join(CMD_EXEC_PATH, 'docs.json'), { name: 'Docs', packages: {} });
64
+ await fse.outputJson(join(CMD_EXEC_PATH, 'package.json'), {
65
+ dependencies: { recharts: '2.15.4' },
66
+ });
67
+ await fse.outputFile(join(CMD_EXEC_PATH, 'bad.mdx'), BROKEN);
68
+ await fse.outputFile(join(CMD_EXEC_PATH, 'other.mdx'), '# Other\n');
69
+ await fse.outputFile(join(CMD_EXEC_PATH, 'styles.css'), 'a{}');
70
+ clearErrorLogs();
71
+ await startPackagesDev();
72
+ expect(islandErrorCount()).toBe(1);
73
+ });
74
+ afterEach(async () => {
75
+ await fse.remove(join(CMD_EXEC_PATH, 'docs.json'));
76
+ await startPackagesDev();
77
+ clearErrorLogs();
78
+ });
79
+ it('keeps them after another page is saved, without repeating them', async () => {
80
+ await save('other.mdx', '# Other edited\n');
81
+ expect(islandErrorCount()).toBe(1);
82
+ await notifyPackageInputChange('other.mdx', 'page');
83
+ expect(islandErrorCount()).toBe(1);
84
+ await save('other.mdx', '# Other edited again\n');
85
+ expect(islandErrorCount()).toBe(1);
86
+ });
87
+ it('keeps them after a file that is not a page is saved', async () => {
88
+ await save('styles.css', 'b{}');
89
+ expect(islandErrorCount()).toBe(1);
90
+ });
91
+ it('keeps them next to the config error while docs.json does not parse', async () => {
92
+ await save('docs.json', '{ "name": "Docs", "packages": {');
93
+ expect(errorMessages()).toContainEqual(expect.stringMatching(/^npm packages: docs\.json could not be loaded/));
94
+ expect(islandErrorCount()).toBe(1);
95
+ });
96
+ it('drops them once the island is fixed and shows them again when it breaks the same way', async () => {
97
+ await save('bad.mdx', FIXED);
98
+ expect(errorMessages()).toEqual([]);
99
+ await save('bad.mdx', BROKEN);
100
+ expect(islandErrorCount()).toBe(1);
101
+ });
102
+ it('drops them when the island is fixed while another page saves at the same time', async () => {
103
+ clearErrorLogs();
104
+ await fse.outputFile(join(CMD_EXEC_PATH, 'bad.mdx'), FIXED);
105
+ await fse.outputFile(join(CMD_EXEC_PATH, 'other.mdx'), '# Other edited\n');
106
+ await Promise.all([
107
+ notifyPackageInputChange('other.mdx', 'page'),
108
+ notifyPackageInputChange('bad.mdx', 'page'),
109
+ ]);
110
+ expect(errorMessages()).toEqual([]);
111
+ });
112
+ it('drops them when the island is fixed while a stylesheet saves at the same time', async () => {
113
+ clearErrorLogs();
114
+ await fse.outputFile(join(CMD_EXEC_PATH, 'bad.mdx'), FIXED);
115
+ await Promise.all([
116
+ notifyPackageInputChange('bad.mdx', 'page'),
117
+ notifyPackageInputChange('styles.css', getFileCategory('styles.css')),
118
+ ]);
119
+ expect(errorMessages()).toEqual([]);
120
+ });
121
+ it('replaces them when a broken page is deleted', async () => {
122
+ await save('worse.mdx', BROKEN);
123
+ expect(packageErrors()).toHaveLength(2);
124
+ await fse.remove(join(CMD_EXEC_PATH, 'bad.mdx'));
125
+ await notifyPackageInputChange('bad.mdx', 'page');
126
+ expect(packageErrors()).toEqual([expect.stringMatching(/^npm packages: worse\.mdx:3 /)]);
127
+ });
128
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,73 @@
1
+ import express from 'express';
2
+ import fse from 'fs-extra';
3
+ import { mkdtemp, rm } from 'fs/promises';
4
+ import { createServer } from 'http';
5
+ import { tmpdir } from 'os';
6
+ import { join } from 'path';
7
+ import { createPackageRoute, EMPTY_ISLAND_MODULE, PACKAGE_ROUTE_PREFIX, } from '../local-preview/packages/serve.js';
8
+ const BUILD = 'a'.repeat(32);
9
+ describe('package route', () => {
10
+ let root;
11
+ let server;
12
+ let base;
13
+ let state;
14
+ beforeEach(async () => {
15
+ root = await mkdtemp(join(tmpdir(), 'packages-serve-'));
16
+ const buildsDir = join(root, '.mintlify', 'packages');
17
+ await fse.outputFile(join(buildsDir, BUILD, 'client/islands/i0123456789abcdef.js'), 'export {}');
18
+ await fse.outputFile(join(buildsDir, BUILD, 'client/chunk-AB12.js'), 'export const a = 1;');
19
+ await fse.outputFile(join(buildsDir, BUILD, 'manifest.json'), '{"islands":{}}');
20
+ await fse.outputFile(join(buildsDir, BUILD, 'server/islands/x.js'), 'secret');
21
+ await fse.outputFile(join(root, 'outside.js'), 'outside');
22
+ state = { enabled: true, buildsDir };
23
+ const app = express();
24
+ app.use(PACKAGE_ROUTE_PREFIX, createPackageRoute(() => state));
25
+ app.use((_req, res) => {
26
+ res.status(418).send('next');
27
+ });
28
+ server = createServer(app);
29
+ await new Promise((resolve) => server.listen(0, resolve));
30
+ base = `http://127.0.0.1:${server.address().port}${PACKAGE_ROUTE_PREFIX}`;
31
+ });
32
+ afterEach(async () => {
33
+ await new Promise((resolve) => server.close(resolve));
34
+ await rm(root, { recursive: true, force: true });
35
+ });
36
+ it('serves client files from a build under a dot folder', async () => {
37
+ const res = await fetch(`${base}/${BUILD}/client/islands/i0123456789abcdef.js`);
38
+ expect(res.status).toBe(200);
39
+ expect(res.headers.get('content-type')).toMatch(/javascript/);
40
+ expect(res.headers.get('x-content-type-options')).toBe('nosniff');
41
+ expect(res.headers.get('cache-control')).toBe('no-cache');
42
+ expect(await res.text()).toBe('export {}');
43
+ expect((await fetch(`${base}/${BUILD}/client/chunk-AB12.js`)).status).toBe(200);
44
+ });
45
+ it('never serves the manifest, server bundles or paths outside client', async () => {
46
+ for (const path of [
47
+ `${BUILD}/manifest.json`,
48
+ `${BUILD}/server/islands/x.js`,
49
+ `${BUILD}/client/../manifest.json`,
50
+ `${BUILD}/client/%2e%2e/manifest.json`,
51
+ `${BUILD}/client/..%2f..%2f..%2f..%2foutside.js`,
52
+ `..%2f..%2foutside.js`,
53
+ ]) {
54
+ const res = await fetch(`${base}/${path}`);
55
+ expect(res.status, path).toBe(404);
56
+ expect(await res.text(), path).not.toMatch(/secret|outside|islands/);
57
+ }
58
+ });
59
+ it('answers a missing island entry with a module that renders nothing', async () => {
60
+ const res = await fetch(`${base}/pending/client/islands/i0000000000000000.js`);
61
+ expect(res.status).toBe(200);
62
+ expect(res.headers.get('content-type')).toMatch(/javascript/);
63
+ expect(res.headers.get('cache-control')).toBe('no-store');
64
+ expect(await res.text()).toBe(EMPTY_ISLAND_MODULE);
65
+ const mod = await import(`data:text/javascript,${encodeURIComponent(EMPTY_ISLAND_MODULE)}`);
66
+ expect(mod).toMatchObject({ hydrate: expect.any(Function) });
67
+ });
68
+ it('falls through to Next when packages are off', async () => {
69
+ state = { ...state, enabled: false };
70
+ const res = await fetch(`${base}/${BUILD}/client/islands/i0123456789abcdef.js`);
71
+ expect(res.status).toBe(418);
72
+ });
73
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,160 @@
1
+ import { getMdx } from '@mintlify/common/server-mdx';
2
+ import fse from 'fs-extra';
3
+ import { mkdtemp, rm } from 'fs/promises';
4
+ import { tmpdir } from 'os';
5
+ import { join } from 'path';
6
+ import { collectPackageIslands } from '../packages/collect.js';
7
+ import { DocsJsonError, InvalidPackagesError, readPackagesSetup } from '../packages/setup.js';
8
+ const PAGE = 'import { Button } from "@acme/ui";\n\n<Button label="{{label}}" />\n';
9
+ describe('package setup config references', () => {
10
+ let docsRoot;
11
+ beforeEach(async () => {
12
+ docsRoot = await mkdtemp(join(tmpdir(), 'packages-setup-'));
13
+ await fse.outputJson(join(docsRoot, 'package.json'), {
14
+ dependencies: { '@acme/ui': '1.0.0' },
15
+ });
16
+ });
17
+ afterEach(async () => {
18
+ await rm(docsRoot, { recursive: true, force: true });
19
+ });
20
+ it('collects the same island ids that compilation uses with referenced variables', async () => {
21
+ const variables = { label: 'Synthetic label' };
22
+ await fse.outputJson(join(docsRoot, 'docs.json'), {
23
+ packages: {},
24
+ variables: { $ref: './variables.json' },
25
+ });
26
+ await fse.outputJson(join(docsRoot, 'variables.json'), variables);
27
+ await fse.outputFile(join(docsRoot, 'page.mdx'), PAGE);
28
+ const setup = await readPackagesSetup(docsRoot);
29
+ const packageNames = setup?.packageNames ?? new Set();
30
+ const collected = await collectPackageIslands({
31
+ docsRoot,
32
+ packageNames,
33
+ variables: setup?.variables,
34
+ });
35
+ const compiled = await getMdx({
36
+ path: 'page',
37
+ content: PAGE,
38
+ metadata: {},
39
+ snippets: [],
40
+ subdomain: 'local',
41
+ codeStyling: undefined,
42
+ tailwindSelectors: [],
43
+ variables,
44
+ packageBuild: {
45
+ baseUrl: '/_mintlify/pkg/synthetic',
46
+ islands: Object.fromEntries(collected.islands.map(({ id }) => [id, { prefix: `mi-${id}-`, html: '<button />' }])),
47
+ packageNames: [...packageNames],
48
+ packageSnippets: [...collected.packageSnippets],
49
+ },
50
+ });
51
+ expect(setup?.variables).toEqual(variables);
52
+ expect(collected.errors).toEqual([]);
53
+ expect(collected.islands).toHaveLength(1);
54
+ expect(compiled.parseFailed).toBeUndefined();
55
+ expect(Object.keys(compiled.mdxExtracts.islands ?? {})).toEqual(collected.pages['page.mdx']?.ids);
56
+ });
57
+ it('reads referenced provider, css and ssr settings with sibling overrides', async () => {
58
+ await fse.outputJson(join(docsRoot, 'docs.json'), {
59
+ packages: { $ref: './packages.json', ssr: false },
60
+ });
61
+ await fse.outputJson(join(docsRoot, 'packages.json'), {
62
+ provider: '/snippets\\provider.jsx',
63
+ css: ['@acme/ui/styles.css'],
64
+ ssr: true,
65
+ });
66
+ expect((await readPackagesSetup(docsRoot))?.packages).toEqual({
67
+ provider: '/snippets/provider.jsx',
68
+ css: ['@acme/ui/styles.css'],
69
+ ssr: false,
70
+ });
71
+ });
72
+ it('finds packages and variables in a referenced root config', async () => {
73
+ await fse.outputJson(join(docsRoot, 'docs.json'), { $ref: './config.json' });
74
+ await fse.outputJson(join(docsRoot, 'config.json'), {
75
+ packages: { ssr: false },
76
+ variables: { label: 'Synthetic label' },
77
+ });
78
+ const setup = await readPackagesSetup(docsRoot);
79
+ expect(setup?.packages).toEqual({ ssr: false });
80
+ expect(setup?.variables).toEqual({ label: 'Synthetic label' });
81
+ });
82
+ it.each([
83
+ { file: 'missing.json', content: undefined, message: /does not exist/ },
84
+ { file: 'invalid.json', content: '{', message: /contains invalid JSON/ },
85
+ ])('reports a broken $ref to $file as a docs config error', async ({ file, content, message }) => {
86
+ await fse.outputJson(join(docsRoot, 'docs.json'), {
87
+ packages: { $ref: `./${file}` },
88
+ });
89
+ if (content !== undefined)
90
+ await fse.outputFile(join(docsRoot, file), content);
91
+ await expect(readPackagesSetup(docsRoot)).rejects.toThrow(DocsJsonError);
92
+ await expect(readPackagesSetup(docsRoot)).rejects.toThrow(message);
93
+ });
94
+ it('keeps packages disabled when docs.json is missing', async () => {
95
+ expect(await readPackagesSetup(docsRoot)).toBeUndefined();
96
+ });
97
+ it('keeps packages disabled without a packages key', async () => {
98
+ await fse.outputJson(join(docsRoot, 'docs.json'), { name: 'Docs' });
99
+ expect(await readPackagesSetup(docsRoot)).toBeUndefined();
100
+ });
101
+ it.each([
102
+ ['', 'docs.json could not be loaded: Unexpected end of JSON input'],
103
+ ['["packages"]', 'docs.json is not a JSON object'],
104
+ ])('rejects docs.json %j', async (raw, message) => {
105
+ await fse.outputFile(join(docsRoot, 'docs.json'), raw);
106
+ await expect(readPackagesSetup(docsRoot)).rejects.toEqual(new DocsJsonError([message]));
107
+ });
108
+ it.each([
109
+ ['', 'package.json is not valid JSON: Unexpected end of JSON input'],
110
+ ['["recharts"]', 'package.json is not a JSON object'],
111
+ ['null', 'package.json is not a JSON object'],
112
+ ])('reports package.json %j without package names', async (raw, message) => {
113
+ await fse.outputJson(join(docsRoot, 'docs.json'), { name: 'Docs', packages: {} });
114
+ await fse.outputFile(join(docsRoot, 'package.json'), raw);
115
+ const setup = await readPackagesSetup(docsRoot);
116
+ expect(setup?.errors).toEqual([message]);
117
+ expect(setup?.packageJsonError).toBe(message);
118
+ expect(setup?.packageNames).toEqual(new Set());
119
+ });
120
+ it.each([
121
+ ['a string', 'nope', ['docs.json packages: Expected object, received string']],
122
+ [
123
+ 'an http registry',
124
+ { registries: { '@acme': 'http://npm.acme.com/' } },
125
+ [
126
+ 'docs.json packages.registries.@acme: Registry URLs must start with `https://` and then the host',
127
+ ],
128
+ ],
129
+ [
130
+ 'a provider outside the docs',
131
+ { provider: '../provider.jsx', isolation: 'none' },
132
+ [
133
+ 'docs.json packages.provider: Provider path cannot contain `..` segments',
134
+ "docs.json packages.isolation: Invalid enum value. Expected 'page' | 'frame', received 'none'",
135
+ ],
136
+ ],
137
+ ])('rejects %s for packages with the schema errors and the variables', async (_, packages, errors) => {
138
+ await fse.outputJson(join(docsRoot, 'docs.json'), {
139
+ name: 'Docs',
140
+ packages,
141
+ variables: { label: 'Synthetic label' },
142
+ });
143
+ const error = await readPackagesSetup(docsRoot).catch((caught) => caught);
144
+ expect(error).toBeInstanceOf(InvalidPackagesError);
145
+ if (!(error instanceof InvalidPackagesError))
146
+ return;
147
+ expect(error.problems).toEqual(errors);
148
+ expect(error.variables).toEqual({ label: 'Synthetic label' });
149
+ });
150
+ it('reports errors in a snippet a page imports', async () => {
151
+ await fse.outputJson(join(docsRoot, 'docs.json'), { name: 'Docs', packages: {} });
152
+ await fse.outputFile(join(docsRoot, 'charts.mdx'), 'import { Chart } from "/snippets/chart.jsx";\n\n<Chart />\n');
153
+ await fse.outputFile(join(docsRoot, 'snippets/chart.jsx'), 'import { Button } from "@acme/ui";\n\nexport const Chart = () => <Button\n');
154
+ const collected = await collectPackageIslands({
155
+ docsRoot,
156
+ packageNames: new Set(['@acme/ui']),
157
+ });
158
+ expect(collected.errors.some(({ file }) => file === 'snippets/chart.jsx')).toBe(true);
159
+ });
160
+ });
package/dist/index.d.ts CHANGED
@@ -6,4 +6,5 @@ import validateBuild from './local-preview/validate-build.js';
6
6
  import { getClientVersion } from './util.js';
7
7
  export * from './logs.js';
8
8
  export * from './logging-state.js';
9
+ export * from './packages/index.js';
9
10
  export { dev, exportSite, validateBuild, getClientVersion, getLatestClientVersion, downloadTargetMint, getVersionMap, LOCAL_LINKED_CLI_VERSION, };
package/dist/index.js CHANGED
@@ -6,4 +6,5 @@ import validateBuild from './local-preview/validate-build.js';
6
6
  import { getClientVersion } from './util.js';
7
7
  export * from './logs.js';
8
8
  export * from './logging-state.js';
9
+ export * from './packages/index.js';
9
10
  export { dev, exportSite, validateBuild, getClientVersion, getLatestClientVersion, downloadTargetMint, getVersionMap, LOCAL_LINKED_CLI_VERSION, };
@@ -6,8 +6,10 @@ import pathUtil from 'path';
6
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 { isNativePackagesEnabled } from '../packages/flag.js';
9
10
  import { seedPreviewFromShared } from '../util.js';
10
11
  import { timeDev, timeDevSync } from './dev-timing.js';
12
+ import { startPackagesDev } from './packages/index.js';
11
13
  import { acquirePreviewLock } from './preview-lock.js';
12
14
  import { run } from './run.js';
13
15
  import { silentUpdateClient } from './update.js';
@@ -88,6 +90,7 @@ const startDev = async (argv) => {
88
90
  timeDevSync('switch to local client directory', () => {
89
91
  process.chdir(CLIENT_PATH);
90
92
  });
93
+ const nativePackages = isNativePackagesEnabled();
91
94
  let fileImportsMap;
92
95
  try {
93
96
  const result = await timeDev('prebuild content', () => prebuild(CMD_EXEC_PATH, {
@@ -97,6 +100,7 @@ const startDev = async (argv) => {
97
100
  disablePrefetch,
98
101
  allowSourceRefs: true,
99
102
  lazyPages: true,
103
+ nativePackages,
100
104
  }));
101
105
  fileImportsMap = result?.fileImportsMap;
102
106
  }
@@ -107,6 +111,8 @@ const startDev = async (argv) => {
107
111
  await new Promise((resolve) => setTimeout(resolve, 50));
108
112
  process.exit(1);
109
113
  }
114
+ if (nativePackages)
115
+ await timeDev('build npm packages', () => startPackagesDev());
110
116
  await timeDev('start preview runtime', () => run({ ...argv, needsUpdate, fileImportsMap }));
111
117
  };
112
118
  const dev = async (argv) => {
@@ -6,6 +6,7 @@ type LocalPreviewOptions = {
6
6
  disablePrefetch?: boolean;
7
7
  allowSourceRefs?: boolean;
8
8
  lazyPages?: boolean;
9
+ nativePackages?: boolean;
9
10
  };
10
11
  declare const listener: (triggerRefresh: () => void, options?: LocalPreviewOptions, gate?: Promise<void>) => void;
11
12
  declare const initializeFrontmatterHashCache: () => Promise<void>;