@mintlify/previewing 4.0.1462 → 4.0.1464

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 (49) hide show
  1. package/dist/__tests__/listener-graphql-packages.test.js +68 -0
  2. package/dist/__tests__/listener-package-manifest.test.d.ts +1 -0
  3. package/dist/__tests__/listener-package-manifest.test.js +103 -0
  4. package/dist/__tests__/packages-builder.test.d.ts +1 -0
  5. package/dist/__tests__/packages-builder.test.js +88 -0
  6. package/dist/__tests__/packages-collect.test.d.ts +1 -0
  7. package/dist/__tests__/packages-collect.test.js +387 -0
  8. package/dist/__tests__/packages-dev-snippets.test.d.ts +1 -0
  9. package/dist/__tests__/packages-dev-snippets.test.js +478 -0
  10. package/dist/__tests__/packages-dev.test.d.ts +1 -0
  11. package/dist/__tests__/packages-dev.test.js +1304 -0
  12. package/dist/__tests__/packages-flag.test.d.ts +1 -0
  13. package/dist/__tests__/packages-flag.test.js +146 -0
  14. package/dist/__tests__/packages-imports.test.d.ts +1 -0
  15. package/dist/__tests__/packages-imports.test.js +68 -0
  16. package/dist/__tests__/packages-island-errors.test.d.ts +1 -0
  17. package/dist/__tests__/packages-island-errors.test.js +128 -0
  18. package/dist/__tests__/packages-serve.test.d.ts +1 -0
  19. package/dist/__tests__/packages-serve.test.js +73 -0
  20. package/dist/__tests__/packages-setup.test.js +65 -7
  21. package/dist/local-preview/index.js +6 -0
  22. package/dist/local-preview/listener/index.d.ts +1 -0
  23. package/dist/local-preview/listener/index.js +10 -1
  24. package/dist/local-preview/listener/regenerateAllSnippets.js +2 -0
  25. package/dist/local-preview/listener/resolve-imports-from-import-closure.js +3 -0
  26. package/dist/local-preview/listener/resolveAllImports.js +2 -0
  27. package/dist/local-preview/packages/dev.d.ts +88 -0
  28. package/dist/local-preview/packages/dev.js +478 -0
  29. package/dist/local-preview/packages/index.d.ts +8 -0
  30. package/dist/local-preview/packages/index.js +89 -0
  31. package/dist/local-preview/packages/serve.d.ts +8 -0
  32. package/dist/local-preview/packages/serve.js +41 -0
  33. package/dist/local-preview/packages/state.d.ts +6 -0
  34. package/dist/local-preview/packages/state.js +6 -0
  35. package/dist/local-preview/run.js +8 -0
  36. package/dist/logging-state.d.ts +1 -0
  37. package/dist/logging-state.js +5 -0
  38. package/dist/packages/builder.d.ts +2 -0
  39. package/dist/packages/builder.js +1 -0
  40. package/dist/packages/index.d.ts +0 -1
  41. package/dist/packages/index.js +0 -1
  42. package/dist/packages/setup.d.ts +2 -0
  43. package/dist/packages/setup.js +5 -1
  44. package/dist/tsconfig.tsbuildinfo +1 -1
  45. package/package.json +4 -4
  46. package/dist/__tests__/packages-check.test.js +0 -254
  47. package/dist/packages/check.d.ts +0 -25
  48. package/dist/packages/check.js +0 -132
  49. /package/dist/__tests__/{packages-check.test.d.ts → listener-graphql-packages.test.d.ts} +0 -0
@@ -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
+ });
@@ -3,9 +3,8 @@ import fse from 'fs-extra';
3
3
  import { mkdtemp, rm } from 'fs/promises';
4
4
  import { tmpdir } from 'os';
5
5
  import { join } from 'path';
6
- import { checkPackages } from '../packages/check.js';
7
6
  import { collectPackageIslands } from '../packages/collect.js';
8
- import { DocsJsonError, readPackagesSetup } from '../packages/setup.js';
7
+ import { DocsJsonError, InvalidPackagesError, readPackagesSetup } from '../packages/setup.js';
9
8
  const PAGE = 'import { Button } from "@acme/ui";\n\n<Button label="{{label}}" />\n';
10
9
  describe('package setup config references', () => {
11
10
  let docsRoot;
@@ -90,13 +89,72 @@ describe('package setup config references', () => {
90
89
  if (content !== undefined)
91
90
  await fse.outputFile(join(docsRoot, file), content);
92
91
  await expect(readPackagesSetup(docsRoot)).rejects.toThrow(DocsJsonError);
93
- const build = vi.fn();
94
- const checked = await checkPackages({ docsRoot, build });
95
- expect(checked.ok).toBe(false);
96
- expect(checked.lines).toEqual([{ level: 'error', message: expect.stringMatching(message) }]);
97
- expect(build).not.toHaveBeenCalled();
92
+ await expect(readPackagesSetup(docsRoot)).rejects.toThrow(message);
98
93
  });
99
94
  it('keeps packages disabled when docs.json is missing', async () => {
100
95
  expect(await readPackagesSetup(docsRoot)).toBeUndefined();
101
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
+ });
102
160
  });
@@ -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>;
@@ -1,5 +1,5 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import { findAndRemoveImports, hasImports, getFileCategory, normalizeOverlayKey, toPosixPath, validate, stringifyTree, isMintIgnored, replaceVariables, } from '@mintlify/common';
2
+ import { findAndRemoveImports, hasImports, getFileCategory, isPackageManifestPath, normalizeOverlayKey, toPosixPath, validate, stringifyTree, isMintIgnored, replaceVariables, } from '@mintlify/common';
3
3
  import { createPage, discoverOverlayFiles, getFileListSync, getMintIgnore, MintConfigUpdater, DocsConfigUpdater, preparseMdxTree, prebuild, } from '@mintlify/prebuild';
4
4
  import { isOverlayDocument } from '@mintlify/validation';
5
5
  import Chalk from 'chalk';
@@ -12,7 +12,9 @@ import pathUtil from 'path';
12
12
  import { CMD_EXEC_PATH, NEXT_PROPS_PATH, NEXT_PUBLIC_PATH, CLIENT_PATH } from '../../constants.js';
13
13
  import { addChangeLog, addErrorLog, clearErrorLogs, getCurrentErrorLogs, } from '../../logging-state.js';
14
14
  import { AddedLog, DeletedLog, EditedLog, WarningLog, InfoLog, ErrorLog } from '../../logs.js';
15
+ import { isNativePackagesEnabled } from '../../packages/flag.js';
15
16
  import { reportPageCompileError, reportPageCompileSuccess } from '../on-demand-page.js';
17
+ import { notifyPackageInputChange, writePackageBuildProps } from '../packages/index.js';
16
18
  import { clearDocsConfigCaches, getDocsConfigApiHash, getDocsConfigNavigationHash, getVariablesHash, hasDocsConfigApiChanged, hasDocsConfigNavigationChanged, hasNavigationApiReference, hadDocsConfigApiReference, hasDocsConfigVariablesChanged, updateDocsConfigHashCache, } from './docs-config-cache.js';
17
19
  import { generateDependentSnippets } from './generateDependentSnippets.js';
18
20
  import { generatePagesWithImports } from './generatePagesWithImports.js';
@@ -185,6 +187,7 @@ const onUnlinkEvent = async (filename, triggerRefresh, options) => {
185
187
  try {
186
188
  const importedFiles = getImportedFilesFromCache();
187
189
  const potentialCategory = getFileCategory(filename, { importedFiles });
190
+ await notifyPackageInputChange(filename, potentialCategory);
188
191
  frontmatterHashes.delete(filename);
189
192
  removePageMetadataCacheEntryForFile(filename);
190
193
  if (potentialCategory === 'page') {
@@ -274,6 +277,7 @@ const onUnlinkEvent = async (filename, triggerRefresh, options) => {
274
277
  await fse.emptyDir(NEXT_PUBLIC_PATH);
275
278
  await fse.emptyDir(NEXT_PROPS_PATH);
276
279
  const prebuildResult = await prebuild(CMD_EXEC_PATH, options);
280
+ await writePackageBuildProps();
277
281
  await initializeImportCache(CMD_EXEC_PATH, prebuildResult?.fileImportsMap);
278
282
  await initializeFrontmatterHashCache();
279
283
  await refreshTrackedReferencedFiles();
@@ -384,6 +388,7 @@ const onUpdateEvent = async (filename, triggerRefresh, options = {}) => {
384
388
  clearErrorLogs();
385
389
  const filePath = pathUtil.join(CMD_EXEC_PATH, filename);
386
390
  if (shouldRegenerateGraphqlSource(filename)) {
391
+ await notifyPackageInputChange(filename, getFileCategory(filename, { importedFiles: getImportedFilesFromCache() }));
387
392
  await rebuildGraphqlSchema(triggerRefresh, options);
388
393
  return null;
389
394
  }
@@ -391,6 +396,9 @@ const onUpdateEvent = async (filename, triggerRefresh, options = {}) => {
391
396
  await syncImportedFileLocations(importChanges);
392
397
  const importedFiles = getImportedFilesFromCache();
393
398
  const potentialCategory = getFileCategory(filename, { importedFiles });
399
+ await notifyPackageInputChange(filename, potentialCategory);
400
+ if (isNativePackagesEnabled() && isPackageManifestPath(filename))
401
+ return null;
394
402
  if (hasTrackedReferencedFile(filename)) {
395
403
  if (await fse.pathExists(pathUtil.join(CMD_EXEC_PATH, 'docs.json'))) {
396
404
  return onUpdateEvent('docs.json', triggerRefresh, options);
@@ -586,6 +594,7 @@ const onUpdateEvent = async (filename, triggerRefresh, options = {}) => {
586
594
  await fse.emptyDir(NEXT_PUBLIC_PATH);
587
595
  await fse.emptyDir(NEXT_PROPS_PATH);
588
596
  const prebuildResult = await prebuild(CMD_EXEC_PATH, options);
597
+ await writePackageBuildProps();
589
598
  await initializeImportCache(CMD_EXEC_PATH, prebuildResult?.fileImportsMap);
590
599
  await initializeFrontmatterHashCache();
591
600
  await refreshTrackedReferencedFiles();
@@ -4,6 +4,7 @@ import { outputFile } from 'fs-extra';
4
4
  import { readFile } from 'fs/promises';
5
5
  import { join } from 'path';
6
6
  import { CMD_EXEC_PATH, NEXT_PUBLIC_PATH } from '../../constants.js';
7
+ import { packageImportArgs } from '../packages/state.js';
7
8
  import { getImportedFilesFromCache } from './importCache.js';
8
9
  import { getCurrentVariables, getMintIgnoreGlobs, handleParseError } from './utils.js';
9
10
  const getV2SnippetFilenames = () => {
@@ -65,6 +66,7 @@ export const regenerateAllSnippets = async () => {
65
66
  const resolvedTree = await baseResolveAllImports({
66
67
  snippets: resolvedTrees,
67
68
  fileWithImports: importData,
69
+ ...packageImportArgs(),
68
70
  });
69
71
  await outputFile(join(NEXT_PUBLIC_PATH, targetFilename), stringifyTree(resolvedTree), {
70
72
  flag: 'w',
@@ -3,6 +3,7 @@ import { preparseMdxTree } from '@mintlify/prebuild';
3
3
  import fse from 'fs-extra';
4
4
  import { join } from 'path';
5
5
  import { CMD_EXEC_PATH, NEXT_PUBLIC_PATH } from '../../constants.js';
6
+ import { packageImportArgs } from '../packages/state.js';
6
7
  import { getCurrentVariables, handleParseError } from './utils.js';
7
8
  const getImportSourcePath = async (filename) => {
8
9
  const generatedPath = join(NEXT_PUBLIC_PATH, filename);
@@ -70,6 +71,7 @@ const resolveSnippetImports = async (snippetsByFilename) => {
70
71
  ? await baseResolveAllImports({
71
72
  snippets: resolvedSnippets,
72
73
  fileWithImports: snippet,
74
+ ...packageImportArgs(),
73
75
  })
74
76
  : snippet.tree;
75
77
  resolvedSnippets.push({ filename: snippet.filename, tree });
@@ -94,5 +96,6 @@ export const resolveImportsFromImportClosure = async (fileWithImports) => {
94
96
  return await baseResolveAllImports({
95
97
  snippets,
96
98
  fileWithImports,
99
+ ...packageImportArgs(),
97
100
  });
98
101
  };