@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.
- package/dist/__tests__/listener-graphql-packages.test.js +68 -0
- package/dist/__tests__/listener-package-manifest.test.d.ts +1 -0
- package/dist/__tests__/listener-package-manifest.test.js +103 -0
- package/dist/__tests__/packages-builder.test.d.ts +1 -0
- package/dist/__tests__/packages-builder.test.js +88 -0
- package/dist/__tests__/packages-collect.test.d.ts +1 -0
- package/dist/__tests__/packages-collect.test.js +387 -0
- package/dist/__tests__/packages-dev-snippets.test.d.ts +1 -0
- package/dist/__tests__/packages-dev-snippets.test.js +478 -0
- package/dist/__tests__/packages-dev.test.d.ts +1 -0
- package/dist/__tests__/packages-dev.test.js +1304 -0
- package/dist/__tests__/packages-flag.test.d.ts +1 -0
- package/dist/__tests__/packages-flag.test.js +146 -0
- package/dist/__tests__/packages-imports.test.d.ts +1 -0
- package/dist/__tests__/packages-imports.test.js +68 -0
- package/dist/__tests__/packages-island-errors.test.d.ts +1 -0
- package/dist/__tests__/packages-island-errors.test.js +128 -0
- package/dist/__tests__/packages-serve.test.d.ts +1 -0
- package/dist/__tests__/packages-serve.test.js +73 -0
- package/dist/__tests__/packages-setup.test.js +65 -7
- package/dist/local-preview/index.js +6 -0
- package/dist/local-preview/listener/index.d.ts +1 -0
- package/dist/local-preview/listener/index.js +10 -1
- package/dist/local-preview/listener/regenerateAllSnippets.js +2 -0
- package/dist/local-preview/listener/resolve-imports-from-import-closure.js +3 -0
- package/dist/local-preview/listener/resolveAllImports.js +2 -0
- package/dist/local-preview/packages/dev.d.ts +88 -0
- package/dist/local-preview/packages/dev.js +478 -0
- package/dist/local-preview/packages/index.d.ts +8 -0
- package/dist/local-preview/packages/index.js +89 -0
- package/dist/local-preview/packages/serve.d.ts +8 -0
- package/dist/local-preview/packages/serve.js +41 -0
- package/dist/local-preview/packages/state.d.ts +6 -0
- package/dist/local-preview/packages/state.js +6 -0
- package/dist/local-preview/run.js +8 -0
- package/dist/logging-state.d.ts +1 -0
- package/dist/logging-state.js +5 -0
- package/dist/packages/builder.d.ts +2 -0
- package/dist/packages/builder.js +1 -0
- package/dist/packages/index.d.ts +0 -1
- package/dist/packages/index.js +0 -1
- package/dist/packages/setup.d.ts +2 -0
- package/dist/packages/setup.js +5 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +4 -4
- package/dist/__tests__/packages-check.test.js +0 -254
- package/dist/packages/check.d.ts +0 -25
- package/dist/packages/check.js +0 -132
- /package/dist/__tests__/{packages-check.test.d.ts → listener-graphql-packages.test.d.ts} +0 -0
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { mkdir, rm } from 'fs/promises';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { CMD_EXEC_PATH } from '../constants.js';
|
|
4
|
+
import listener from '../local-preview/listener/index.js';
|
|
5
|
+
import { notifyPackageInputChange } from '../local-preview/packages/index.js';
|
|
6
|
+
const watcher = vi.hoisted(() => ({
|
|
7
|
+
handlers: {},
|
|
8
|
+
}));
|
|
9
|
+
vi.mock('../constants.js', async (importOriginal) => {
|
|
10
|
+
const actual = await importOriginal();
|
|
11
|
+
const { mkdtempSync } = await import('fs');
|
|
12
|
+
const { tmpdir } = await import('os');
|
|
13
|
+
const { join } = await import('path');
|
|
14
|
+
const root = mkdtempSync(join(tmpdir(), 'listener-graphql-'));
|
|
15
|
+
return {
|
|
16
|
+
...actual,
|
|
17
|
+
CMD_EXEC_PATH: join(root, 'docs'),
|
|
18
|
+
CLIENT_PATH: join(root, 'client'),
|
|
19
|
+
NEXT_PROPS_PATH: join(root, 'client', 'src', '_props'),
|
|
20
|
+
NEXT_PUBLIC_PATH: join(root, 'client', 'public'),
|
|
21
|
+
};
|
|
22
|
+
});
|
|
23
|
+
vi.mock('chokidar', () => {
|
|
24
|
+
const watch = () => {
|
|
25
|
+
const instance = {
|
|
26
|
+
on: (event, handler) => {
|
|
27
|
+
watcher.handlers[event] = handler;
|
|
28
|
+
return instance;
|
|
29
|
+
},
|
|
30
|
+
close: async () => { },
|
|
31
|
+
};
|
|
32
|
+
return instance;
|
|
33
|
+
};
|
|
34
|
+
return { default: { watch }, watch };
|
|
35
|
+
});
|
|
36
|
+
vi.mock('@mintlify/prebuild', async (importOriginal) => {
|
|
37
|
+
const actual = await importOriginal();
|
|
38
|
+
return {
|
|
39
|
+
...actual,
|
|
40
|
+
getMintIgnore: vi.fn().mockResolvedValue([]),
|
|
41
|
+
prebuild: vi.fn().mockResolvedValue(undefined),
|
|
42
|
+
};
|
|
43
|
+
});
|
|
44
|
+
vi.mock('../local-preview/listener/referencedFiles.js', async (importOriginal) => {
|
|
45
|
+
const actual = await importOriginal();
|
|
46
|
+
return {
|
|
47
|
+
...actual,
|
|
48
|
+
shouldRegenerateGraphqlSource: (filename) => filename === 'schema.graphql',
|
|
49
|
+
};
|
|
50
|
+
});
|
|
51
|
+
vi.mock('../local-preview/packages/index.js', () => ({
|
|
52
|
+
notifyPackageInputChange: vi.fn(),
|
|
53
|
+
writePackageBuildProps: vi.fn(),
|
|
54
|
+
}));
|
|
55
|
+
beforeAll(async () => {
|
|
56
|
+
await mkdir(CMD_EXEC_PATH, { recursive: true });
|
|
57
|
+
});
|
|
58
|
+
afterAll(async () => {
|
|
59
|
+
await rm(join(CMD_EXEC_PATH, '..'), { recursive: true, force: true });
|
|
60
|
+
});
|
|
61
|
+
it('tells the packages runtime about a saved GraphQL source', async () => {
|
|
62
|
+
const triggerRefresh = vi.fn();
|
|
63
|
+
listener(triggerRefresh, {});
|
|
64
|
+
await vi.waitFor(() => expect(watcher.handlers.change).toBeDefined());
|
|
65
|
+
await watcher.handlers.change?.('schema.graphql');
|
|
66
|
+
expect(triggerRefresh).toHaveBeenCalledOnce();
|
|
67
|
+
expect(notifyPackageInputChange).toHaveBeenCalledWith('schema.graphql', null);
|
|
68
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { validate } from '@mintlify/common';
|
|
2
|
+
import { mkdir, rm, writeFile } from 'fs/promises';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
import { CMD_EXEC_PATH, NEXT_PROPS_PATH } from '../constants.js';
|
|
5
|
+
import listener from '../local-preview/listener/index.js';
|
|
6
|
+
import { notifyPackageInputChange } from '../local-preview/packages/index.js';
|
|
7
|
+
import * as logs from '../logging-state.js';
|
|
8
|
+
const watcher = vi.hoisted(() => ({
|
|
9
|
+
handlers: {},
|
|
10
|
+
}));
|
|
11
|
+
vi.mock('../constants.js', async (importOriginal) => {
|
|
12
|
+
const actual = await importOriginal();
|
|
13
|
+
const { mkdtempSync } = await import('fs');
|
|
14
|
+
const { tmpdir } = await import('os');
|
|
15
|
+
const { join } = await import('path');
|
|
16
|
+
const root = mkdtempSync(join(tmpdir(), 'listener-manifest-'));
|
|
17
|
+
return {
|
|
18
|
+
...actual,
|
|
19
|
+
CMD_EXEC_PATH: join(root, 'docs'),
|
|
20
|
+
CLIENT_PATH: join(root, 'client'),
|
|
21
|
+
NEXT_PROPS_PATH: join(root, 'client', 'src', '_props'),
|
|
22
|
+
NEXT_PUBLIC_PATH: join(root, 'client', 'public'),
|
|
23
|
+
};
|
|
24
|
+
});
|
|
25
|
+
vi.mock('chokidar', () => {
|
|
26
|
+
const watch = () => {
|
|
27
|
+
const instance = {
|
|
28
|
+
on: (event, handler) => {
|
|
29
|
+
watcher.handlers[event] = handler;
|
|
30
|
+
return instance;
|
|
31
|
+
},
|
|
32
|
+
close: async () => { },
|
|
33
|
+
};
|
|
34
|
+
return instance;
|
|
35
|
+
};
|
|
36
|
+
return { default: { watch }, watch };
|
|
37
|
+
});
|
|
38
|
+
vi.mock('@mintlify/common', async (importOriginal) => {
|
|
39
|
+
const actual = await importOriginal();
|
|
40
|
+
return { ...actual, validate: vi.fn().mockRejectedValue(new Error('not an OpenAPI spec')) };
|
|
41
|
+
});
|
|
42
|
+
vi.mock('@mintlify/prebuild', async (importOriginal) => {
|
|
43
|
+
const actual = await importOriginal();
|
|
44
|
+
return {
|
|
45
|
+
...actual,
|
|
46
|
+
getMintIgnore: vi.fn().mockResolvedValue([]),
|
|
47
|
+
discoverOverlayFiles: vi.fn().mockResolvedValue([]),
|
|
48
|
+
};
|
|
49
|
+
});
|
|
50
|
+
vi.mock('../local-preview/packages/index.js', () => ({
|
|
51
|
+
notifyPackageInputChange: vi.fn(),
|
|
52
|
+
writePackageBuildProps: vi.fn(),
|
|
53
|
+
}));
|
|
54
|
+
const addChangeLogSpy = vi.spyOn(logs, 'addChangeLog');
|
|
55
|
+
const triggerRefresh = vi.fn();
|
|
56
|
+
const change = async (filename) => {
|
|
57
|
+
listener(triggerRefresh, {});
|
|
58
|
+
await vi.waitFor(() => expect(watcher.handlers.change).toBeDefined());
|
|
59
|
+
await watcher.handlers.change?.(filename);
|
|
60
|
+
};
|
|
61
|
+
const edited = (filename) => expect(addChangeLogSpy).toHaveBeenCalledWith(expect.objectContaining({ props: expect.objectContaining({ filename }) }));
|
|
62
|
+
const manifest = { name: 'docs', dependencies: { recharts: '2.15.0' } };
|
|
63
|
+
beforeAll(async () => {
|
|
64
|
+
await mkdir(join(CMD_EXEC_PATH, 'snippets'), { recursive: true });
|
|
65
|
+
await mkdir(NEXT_PROPS_PATH, { recursive: true });
|
|
66
|
+
await writeFile(join(CMD_EXEC_PATH, 'package.json'), JSON.stringify(manifest));
|
|
67
|
+
await writeFile(join(CMD_EXEC_PATH, 'snippets', 'package.json'), JSON.stringify(manifest));
|
|
68
|
+
await writeFile(join(NEXT_PROPS_PATH, 'customLanguages.json'), '[]');
|
|
69
|
+
});
|
|
70
|
+
afterAll(async () => {
|
|
71
|
+
await rm(join(CMD_EXEC_PATH, '..'), { recursive: true, force: true });
|
|
72
|
+
});
|
|
73
|
+
beforeEach(() => {
|
|
74
|
+
vi.clearAllMocks();
|
|
75
|
+
watcher.handlers = {};
|
|
76
|
+
});
|
|
77
|
+
afterEach(() => {
|
|
78
|
+
vi.unstubAllEnvs();
|
|
79
|
+
});
|
|
80
|
+
describe('listener package manifest changes', () => {
|
|
81
|
+
it('treats a root package.json as a potential OpenAPI spec when the flag is off', async () => {
|
|
82
|
+
vi.stubEnv('MINTLIFY_NATIVE_NPM_PACKAGES', undefined);
|
|
83
|
+
await change('package.json');
|
|
84
|
+
expect(notifyPackageInputChange).toHaveBeenCalledWith('package.json', 'potentialJsonOpenApiSpec');
|
|
85
|
+
expect(validate).toHaveBeenCalledWith(manifest);
|
|
86
|
+
expect(triggerRefresh).toHaveBeenCalledOnce();
|
|
87
|
+
edited('package.json');
|
|
88
|
+
});
|
|
89
|
+
it('leaves a root package.json to the packages runtime when the flag is on', async () => {
|
|
90
|
+
vi.stubEnv('MINTLIFY_NATIVE_NPM_PACKAGES', '1');
|
|
91
|
+
await change('package.json');
|
|
92
|
+
expect(notifyPackageInputChange).toHaveBeenCalledWith('package.json', 'potentialJsonOpenApiSpec');
|
|
93
|
+
expect(validate).not.toHaveBeenCalled();
|
|
94
|
+
expect(triggerRefresh).not.toHaveBeenCalled();
|
|
95
|
+
edited('package.json');
|
|
96
|
+
});
|
|
97
|
+
it('still parses a nested package.json when the flag is on', async () => {
|
|
98
|
+
vi.stubEnv('MINTLIFY_NATIVE_NPM_PACKAGES', '1');
|
|
99
|
+
await change(join('snippets', 'package.json'));
|
|
100
|
+
expect(validate).toHaveBeenCalledWith(manifest);
|
|
101
|
+
expect(triggerRefresh).toHaveBeenCalledOnce();
|
|
102
|
+
});
|
|
103
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import fse from 'fs-extra';
|
|
2
|
+
import { mkdtemp, rm } from 'fs/promises';
|
|
3
|
+
import { tmpdir } from 'os';
|
|
4
|
+
import { join } from 'path';
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
6
|
+
const notFound = (code, message = "Cannot find package '@mintlify/package-builder' imported from /cli/dist/index.js") => Object.assign(new Error(message), { code });
|
|
7
|
+
vi.mock('../packages/builder.js', async (importOriginal) => {
|
|
8
|
+
const actual = await importOriginal();
|
|
9
|
+
return {
|
|
10
|
+
...actual,
|
|
11
|
+
loadPackageBuilder: () => actual.loadPackageBuilder(() => Promise.reject(notFound('ERR_MODULE_NOT_FOUND'))),
|
|
12
|
+
};
|
|
13
|
+
});
|
|
14
|
+
import { PackagesDev } from '../local-preview/packages/dev.js';
|
|
15
|
+
import { PACKAGE_BUILDER_MISSING } from '../packages/builder.js';
|
|
16
|
+
const actualLoad = async () => (await vi.importActual('../packages/builder.js'))
|
|
17
|
+
.loadPackageBuilder;
|
|
18
|
+
const PAGE = `import { LineChart } from "recharts";
|
|
19
|
+
|
|
20
|
+
<LineChart width={400} height={200} data={[{ v: 1 }]} />
|
|
21
|
+
`;
|
|
22
|
+
describe('loadPackageBuilder', () => {
|
|
23
|
+
it.each(['ERR_MODULE_NOT_FOUND', 'MODULE_NOT_FOUND'])('explains how to get npm packages back on %s', async (code) => {
|
|
24
|
+
const load = await actualLoad();
|
|
25
|
+
await expect(load(() => Promise.reject(notFound(code)))).rejects.toThrow(PACKAGE_BUILDER_MISSING);
|
|
26
|
+
});
|
|
27
|
+
it('passes other errors through', async () => {
|
|
28
|
+
const error = Object.assign(new Error('EACCES: permission denied'), { code: 'EACCES' });
|
|
29
|
+
const load = await actualLoad();
|
|
30
|
+
await expect(load(() => Promise.reject(error))).rejects.toBe(error);
|
|
31
|
+
});
|
|
32
|
+
it('passes through a missing dependency of an installed builder', async () => {
|
|
33
|
+
const error = notFound('ERR_MODULE_NOT_FOUND', "Cannot find package 'entities' imported from /cli/node_modules/@mintlify/package-builder/dist/sanitize.js");
|
|
34
|
+
const load = await actualLoad();
|
|
35
|
+
await expect(load(() => Promise.reject(error))).rejects.toBe(error);
|
|
36
|
+
});
|
|
37
|
+
it('loads the builder when it is installed', async () => {
|
|
38
|
+
const load = await actualLoad();
|
|
39
|
+
expect(typeof (await load()).build).toBe('function');
|
|
40
|
+
});
|
|
41
|
+
it('pins the builder version from this workspace so releases publish them together', async () => {
|
|
42
|
+
const packageJson = (path) => fse.readJson(fileURLToPath(new URL(`../../../${path}/package.json`, import.meta.url)));
|
|
43
|
+
const previewing = await packageJson('previewing');
|
|
44
|
+
const builder = await packageJson('package-builder');
|
|
45
|
+
expect(previewing.optionalDependencies['@mintlify/package-builder']).toBe(builder.version);
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
describe('without @mintlify/package-builder installed', () => {
|
|
49
|
+
let root;
|
|
50
|
+
let docsRoot;
|
|
51
|
+
beforeEach(async () => {
|
|
52
|
+
root = await mkdtemp(join(tmpdir(), 'packages-builder-'));
|
|
53
|
+
docsRoot = join(root, 'docs');
|
|
54
|
+
await fse.outputJson(join(docsRoot, 'docs.json'), { name: 'Docs', packages: {} });
|
|
55
|
+
await fse.outputJson(join(docsRoot, 'package.json'), { dependencies: { recharts: '2.15.4' } });
|
|
56
|
+
await fse.outputFile(join(docsRoot, 'charts.mdx'), PAGE);
|
|
57
|
+
});
|
|
58
|
+
afterEach(async () => {
|
|
59
|
+
await rm(root, { recursive: true, force: true });
|
|
60
|
+
});
|
|
61
|
+
it('reports the reinstall hint in mint dev and shows it on package components', async () => {
|
|
62
|
+
const failed = vi.fn();
|
|
63
|
+
const dev = new PackagesDev({
|
|
64
|
+
docsRoot,
|
|
65
|
+
buildsDir: join(root, 'builds'),
|
|
66
|
+
propsPath: join(root, 'props'),
|
|
67
|
+
onRebuilt: () => { },
|
|
68
|
+
reporter: {
|
|
69
|
+
building: () => { },
|
|
70
|
+
built: () => { },
|
|
71
|
+
failed,
|
|
72
|
+
missingNodeModules: () => { },
|
|
73
|
+
islandErrors: () => { },
|
|
74
|
+
configError: () => { },
|
|
75
|
+
},
|
|
76
|
+
debounceMs: 0,
|
|
77
|
+
pollMs: 10,
|
|
78
|
+
});
|
|
79
|
+
try {
|
|
80
|
+
await dev.init();
|
|
81
|
+
expect(failed).toHaveBeenCalledWith([PACKAGE_BUILDER_MISSING], []);
|
|
82
|
+
expect(dev.getStatus()).toEqual({ state: 'failed', message: PACKAGE_BUILDER_MISSING });
|
|
83
|
+
}
|
|
84
|
+
finally {
|
|
85
|
+
await dev.close();
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
import { findAndRemoveImports, getAST, parseFrontmatter, stringifyTree } from '@mintlify/common';
|
|
2
|
+
import { getMdx } from '@mintlify/common/server-mdx';
|
|
3
|
+
import { createPage, preparseMdxTree } from '@mintlify/prebuild';
|
|
4
|
+
import fse from 'fs-extra';
|
|
5
|
+
import { mkdtemp, readdir, rm } from 'fs/promises';
|
|
6
|
+
import { tmpdir } from 'os';
|
|
7
|
+
import { join, sep } from 'path';
|
|
8
|
+
const testPaths = vi.hoisted(() => ({ cmd: '', public: '' }));
|
|
9
|
+
vi.mock('../constants.js', () => ({
|
|
10
|
+
get CMD_EXEC_PATH() {
|
|
11
|
+
return testPaths.cmd;
|
|
12
|
+
},
|
|
13
|
+
get NEXT_PUBLIC_PATH() {
|
|
14
|
+
return testPaths.public;
|
|
15
|
+
},
|
|
16
|
+
}));
|
|
17
|
+
vi.mock('@mintlify/common', async (importOriginal) => {
|
|
18
|
+
const actual = await importOriginal();
|
|
19
|
+
return { ...actual, getAST: vi.fn(actual.getAST) };
|
|
20
|
+
});
|
|
21
|
+
vi.mock('../local-preview/listener/utils.js', () => ({
|
|
22
|
+
getCurrentVariables: vi.fn(async () => ({ brand: 'Acme' })),
|
|
23
|
+
handleParseError: vi.fn(),
|
|
24
|
+
}));
|
|
25
|
+
import { resolvePageImports } from '../local-preview/listener/resolve-page-imports.js';
|
|
26
|
+
import { setPackageImportOptions } from '../local-preview/packages/state.js';
|
|
27
|
+
import { collectPackageIslands, PackageIslandCollector } from '../packages/collect.js';
|
|
28
|
+
import { readPackagesSetup } from '../packages/setup.js';
|
|
29
|
+
const CHARTS = `---
|
|
30
|
+
title: Charts
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
import { LineChart, Line } from "recharts";
|
|
34
|
+
import { RevenueChart } from "/snippets/revenue.jsx";
|
|
35
|
+
|
|
36
|
+
export const data = [{ v: 1 }, { v: 3 }];
|
|
37
|
+
|
|
38
|
+
# {{brand}} charts
|
|
39
|
+
|
|
40
|
+
<LineChart width={400} height={200} data={data}>
|
|
41
|
+
<Line dataKey="v" isAnimationActive={false} />
|
|
42
|
+
</LineChart>
|
|
43
|
+
|
|
44
|
+
<Note>
|
|
45
|
+
<RevenueChart currency="USD" />
|
|
46
|
+
</Note>
|
|
47
|
+
`;
|
|
48
|
+
const BROKEN = `import { LineChart } from "recharts";
|
|
49
|
+
|
|
50
|
+
Intro.
|
|
51
|
+
|
|
52
|
+
<LineChart width={400} height={200} onClick={() => {}} />
|
|
53
|
+
`;
|
|
54
|
+
const UNDECLARED = `import { debounce } from "lodash";
|
|
55
|
+
|
|
56
|
+
Nothing here.
|
|
57
|
+
`;
|
|
58
|
+
const REVENUE = `import { BarChart, Bar } from "recharts";
|
|
59
|
+
import { formatMoney } from "./format.js";
|
|
60
|
+
|
|
61
|
+
export const RevenueChart = ({ currency }) => (
|
|
62
|
+
<BarChart width={300} height={150} data={[{ v: 1 }]}>
|
|
63
|
+
<Bar dataKey="v" name={formatMoney(1, currency)} />
|
|
64
|
+
</BarChart>
|
|
65
|
+
);
|
|
66
|
+
`;
|
|
67
|
+
const BAR_SNIPPET = `import { BarChart, Bar } from "recharts";
|
|
68
|
+
|
|
69
|
+
<BarChart width={300} height={150} data={[{ v: 1 }]}>
|
|
70
|
+
<Bar dataKey="v" name="{{brand}} revenue" />
|
|
71
|
+
</BarChart>
|
|
72
|
+
`;
|
|
73
|
+
const LINE_SNIPPET = `<LineChart width={400} height={200} data={[{ v: 2 }]}>
|
|
74
|
+
<Line dataKey="v" isAnimationActive={false} />
|
|
75
|
+
</LineChart>
|
|
76
|
+
`;
|
|
77
|
+
const writeDocs = async (root) => {
|
|
78
|
+
await fse.outputJson(join(root, 'docs.json'), {
|
|
79
|
+
name: 'Docs',
|
|
80
|
+
packages: { provider: '/snippets\\provider.jsx' },
|
|
81
|
+
variables: { brand: 'Acme' },
|
|
82
|
+
});
|
|
83
|
+
await fse.outputJson(join(root, 'package.json'), {
|
|
84
|
+
dependencies: { react: '18.3.1', 'react-dom': '18.3.1', recharts: '2.15.4' },
|
|
85
|
+
});
|
|
86
|
+
await fse.outputFile(join(root, 'charts.mdx'), CHARTS);
|
|
87
|
+
await fse.outputFile(join(root, 'guides/broken.mdx'), BROKEN);
|
|
88
|
+
await fse.outputFile(join(root, 'undeclared.mdx'), UNDECLARED);
|
|
89
|
+
await fse.outputFile(join(root, 'plain.mdx'), '# Plain\n');
|
|
90
|
+
await fse.outputFile(join(root, 'snippets/revenue.jsx'), REVENUE);
|
|
91
|
+
await fse.outputFile(join(root, 'snippets/format.js'), 'export const formatMoney = (v) => `$${v}`;\n');
|
|
92
|
+
await fse.outputFile(join(root, 'snippets/provider.jsx'), 'export const Provider = ({ children }) => children;\n');
|
|
93
|
+
};
|
|
94
|
+
const localSnippets = async () => {
|
|
95
|
+
const dir = join(testPaths.cmd, '_snippets');
|
|
96
|
+
if (!(await fse.pathExists(dir)))
|
|
97
|
+
return [];
|
|
98
|
+
const names = (await readdir(dir, { recursive: true })).filter((name) => name.endsWith('.mdx'));
|
|
99
|
+
return Promise.all(names.map(async (name) => ({
|
|
100
|
+
snippetFileLocation: name.split(sep).join('/'),
|
|
101
|
+
content: await fse.readFile(join(dir, name), 'utf8'),
|
|
102
|
+
})));
|
|
103
|
+
};
|
|
104
|
+
const compileLikeMintDev = async (filename, packageBuild) => {
|
|
105
|
+
const sourcePath = join(testPaths.cmd, filename);
|
|
106
|
+
let content = await fse.readFile(sourcePath, 'utf8');
|
|
107
|
+
const tree = await preparseMdxTree(content, testPaths.cmd, sourcePath);
|
|
108
|
+
const importsResponse = await findAndRemoveImports(tree);
|
|
109
|
+
content = stringifyTree(await resolvePageImports({ ...importsResponse, filename }));
|
|
110
|
+
const { pageContent } = await createPage(filename, content, testPaths.cmd, [], []);
|
|
111
|
+
return getMdx({
|
|
112
|
+
path: filename.replace(/\.mdx$/, ''),
|
|
113
|
+
content: parseFrontmatter(pageContent).body,
|
|
114
|
+
metadata: {},
|
|
115
|
+
snippets: await localSnippets(),
|
|
116
|
+
subdomain: 'local',
|
|
117
|
+
codeStyling: undefined,
|
|
118
|
+
tailwindSelectors: [],
|
|
119
|
+
variables: { brand: 'Acme' },
|
|
120
|
+
packageBuild,
|
|
121
|
+
});
|
|
122
|
+
};
|
|
123
|
+
describe('collectPackageIslands', () => {
|
|
124
|
+
let root;
|
|
125
|
+
beforeEach(async () => {
|
|
126
|
+
root = await mkdtemp(join(tmpdir(), 'packages-collect-'));
|
|
127
|
+
testPaths.cmd = join(root, 'docs');
|
|
128
|
+
testPaths.public = join(root, 'public');
|
|
129
|
+
await fse.ensureDir(testPaths.public);
|
|
130
|
+
await writeDocs(testPaths.cmd);
|
|
131
|
+
});
|
|
132
|
+
afterEach(async () => {
|
|
133
|
+
setPackageImportOptions(undefined);
|
|
134
|
+
await rm(root, { recursive: true, force: true });
|
|
135
|
+
});
|
|
136
|
+
it('reads the packages setup and normalizes the provider path', async () => {
|
|
137
|
+
const setup = await readPackagesSetup(testPaths.cmd);
|
|
138
|
+
expect(setup?.packages.provider).toBe('/snippets/provider.jsx');
|
|
139
|
+
expect(setup?.packageNames).toEqual(new Set(['react', 'react-dom', 'recharts']));
|
|
140
|
+
expect(setup?.variables).toEqual({ brand: 'Acme' });
|
|
141
|
+
expect(setup?.errors).toEqual([]);
|
|
142
|
+
});
|
|
143
|
+
it('returns undefined without a packages key', async () => {
|
|
144
|
+
await fse.outputJson(join(testPaths.cmd, 'docs.json'), { name: 'Docs' });
|
|
145
|
+
expect(await readPackagesSetup(testPaths.cmd)).toBeUndefined();
|
|
146
|
+
});
|
|
147
|
+
it('returns undefined without a docs.json and rejects one that does not parse', async () => {
|
|
148
|
+
await fse.outputFile(join(testPaths.cmd, 'docs.json'), '{ "packages": {}, }');
|
|
149
|
+
await expect(readPackagesSetup(testPaths.cmd)).rejects.toThrow(/^docs\.json could not be loaded: \S/);
|
|
150
|
+
await fse.outputJson(join(testPaths.cmd, 'docs.json'), ['packages']);
|
|
151
|
+
await expect(readPackagesSetup(testPaths.cmd)).rejects.toThrow('docs.json is not a JSON object');
|
|
152
|
+
await fse.remove(join(testPaths.cmd, 'docs.json'));
|
|
153
|
+
expect(await readPackagesSetup(testPaths.cmd)).toBeUndefined();
|
|
154
|
+
});
|
|
155
|
+
it('parses only the pages that can hold package components', async () => {
|
|
156
|
+
vi.mocked(getAST).mockClear();
|
|
157
|
+
const collected = await collectPackageIslands({
|
|
158
|
+
docsRoot: testPaths.cmd,
|
|
159
|
+
packageNames: new Set(['recharts']),
|
|
160
|
+
variables: { brand: 'Acme' },
|
|
161
|
+
});
|
|
162
|
+
expect(collected.pages['plain.mdx']).toEqual({ ids: [], errors: [] });
|
|
163
|
+
const parsed = vi.mocked(getAST).mock.calls.map(([, file]) => file);
|
|
164
|
+
expect(parsed).toContain('/charts.mdx');
|
|
165
|
+
expect(parsed).not.toContain('/plain.mdx');
|
|
166
|
+
});
|
|
167
|
+
it('collects inline and snippet islands, errors and warnings across pages', async () => {
|
|
168
|
+
const setup = await readPackagesSetup(testPaths.cmd);
|
|
169
|
+
const collected = await collectPackageIslands({
|
|
170
|
+
docsRoot: testPaths.cmd,
|
|
171
|
+
packageNames: setup?.packageNames ?? new Set(),
|
|
172
|
+
variables: setup?.variables,
|
|
173
|
+
provider: setup?.packages.provider,
|
|
174
|
+
});
|
|
175
|
+
expect(collected.packageSnippets).toEqual(new Set(['/snippets/revenue.jsx']));
|
|
176
|
+
expect(Object.keys(collected.snippetFiles).sort()).toEqual([
|
|
177
|
+
'/snippets/provider.jsx',
|
|
178
|
+
'/snippets/revenue.jsx',
|
|
179
|
+
]);
|
|
180
|
+
expect(collected.snippetFiles['/snippets/revenue.jsx']).toBe(REVENUE);
|
|
181
|
+
expect(collected.islands.map((island) => island.kind)).toEqual(['inline', 'snippet']);
|
|
182
|
+
expect(collected.pages['charts.mdx']?.ids).toEqual(collected.islands.map((i) => i.id));
|
|
183
|
+
expect(collected.pages['plain.mdx']?.ids).toEqual([]);
|
|
184
|
+
expect(collected.errors).toEqual([
|
|
185
|
+
expect.objectContaining({ file: 'guides/broken.mdx', line: 5 }),
|
|
186
|
+
{
|
|
187
|
+
file: 'undeclared.mdx',
|
|
188
|
+
line: 1,
|
|
189
|
+
message: `imports "lodash", which isn't listed in package.json dependencies.`,
|
|
190
|
+
},
|
|
191
|
+
]);
|
|
192
|
+
expect(collected.warnings).toEqual([
|
|
193
|
+
expect.objectContaining({
|
|
194
|
+
file: 'undeclared.mdx',
|
|
195
|
+
message: expect.stringContaining('lodash'),
|
|
196
|
+
}),
|
|
197
|
+
]);
|
|
198
|
+
});
|
|
199
|
+
it('resolves relatively imported snippets before the snippets that import them', async () => {
|
|
200
|
+
await fse.outputFile(join(testPaths.cmd, 'snippets/a-outer.mdx'), 'import Inner from "./b-inner.mdx";\n\n<Inner />\n');
|
|
201
|
+
await fse.outputFile(join(testPaths.cmd, 'snippets/b-inner.mdx'), 'import { RevenueChart } from "./revenue.jsx";\n\n<RevenueChart currency="EUR" />\n');
|
|
202
|
+
await fse.outputFile(join(testPaths.cmd, 'nested.mdx'), 'import Outer from "/snippets/a-outer.mdx";\n\n<Outer />\n');
|
|
203
|
+
const setup = await readPackagesSetup(testPaths.cmd);
|
|
204
|
+
const collected = await collectPackageIslands({
|
|
205
|
+
docsRoot: testPaths.cmd,
|
|
206
|
+
packageNames: setup?.packageNames ?? new Set(),
|
|
207
|
+
variables: setup?.variables,
|
|
208
|
+
});
|
|
209
|
+
const [id] = collected.pages['nested.mdx']?.ids ?? [];
|
|
210
|
+
expect(collected.islands.find((island) => island.id === id)?.kind).toBe('snippet');
|
|
211
|
+
expect(collected.warnings).not.toContainEqual(expect.objectContaining({ file: 'snippets/a-outer.mdx' }));
|
|
212
|
+
});
|
|
213
|
+
it('produces the island ids that mint dev compiles for the page', async () => {
|
|
214
|
+
const setup = await readPackagesSetup(testPaths.cmd);
|
|
215
|
+
const collected = await collectPackageIslands({
|
|
216
|
+
docsRoot: testPaths.cmd,
|
|
217
|
+
packageNames: setup?.packageNames ?? new Set(),
|
|
218
|
+
variables: setup?.variables,
|
|
219
|
+
});
|
|
220
|
+
setPackageImportOptions({
|
|
221
|
+
packageNames: setup?.packageNames ?? new Set(),
|
|
222
|
+
packageSnippets: collected.packageSnippets,
|
|
223
|
+
});
|
|
224
|
+
const islands = Object.fromEntries(collected.islands.map((island) => [
|
|
225
|
+
island.id,
|
|
226
|
+
{ prefix: `mi-${island.id}-`, html: '<svg></svg>' },
|
|
227
|
+
]));
|
|
228
|
+
const compiled = await compileLikeMintDev('charts.mdx', {
|
|
229
|
+
baseUrl: '/_mintlify/pkg/abc',
|
|
230
|
+
islands,
|
|
231
|
+
packageNames: [...(setup?.packageNames ?? [])],
|
|
232
|
+
packageSnippets: [...collected.packageSnippets],
|
|
233
|
+
});
|
|
234
|
+
expect(Object.keys(compiled.mdxExtracts.islands ?? {})).toEqual(collected.pages['charts.mdx']?.ids);
|
|
235
|
+
expect(compiled.mdxExtracts.packageBaseUrl).toBe('/_mintlify/pkg/abc');
|
|
236
|
+
expect(compiled.mdxSource.compiledSource).not.toMatch(/LineChart|RevenueChart/);
|
|
237
|
+
});
|
|
238
|
+
it('extracts the package components that <Snippet file> injects with the ids mint dev compiles', async () => {
|
|
239
|
+
await fse.outputFile(join(testPaths.cmd, '_snippets/bar.mdx'), BAR_SNIPPET);
|
|
240
|
+
await fse.outputFile(join(testPaths.cmd, '_snippets/line.mdx'), LINE_SNIPPET);
|
|
241
|
+
await fse.outputFile(join(testPaths.cmd, 'revenue.mdx'), '# Revenue\n\n<Snippet file="bar.mdx" />\n');
|
|
242
|
+
await fse.outputFile(join(testPaths.cmd, 'trend.mdx'), 'import { LineChart, Line } from "recharts";\n\n<Snippet file="line.mdx" />\n');
|
|
243
|
+
const setup = await readPackagesSetup(testPaths.cmd);
|
|
244
|
+
const collector = new PackageIslandCollector({
|
|
245
|
+
docsRoot: testPaths.cmd,
|
|
246
|
+
packageNames: setup?.packageNames ?? new Set(),
|
|
247
|
+
variables: setup?.variables,
|
|
248
|
+
});
|
|
249
|
+
const collected = await collector.collectAll();
|
|
250
|
+
setPackageImportOptions({
|
|
251
|
+
packageNames: setup?.packageNames ?? new Set(),
|
|
252
|
+
packageSnippets: collected.packageSnippets,
|
|
253
|
+
});
|
|
254
|
+
const islands = Object.fromEntries(collected.islands.map((island) => [
|
|
255
|
+
island.id,
|
|
256
|
+
{ prefix: `mi-${island.id}-`, html: '<svg></svg>' },
|
|
257
|
+
]));
|
|
258
|
+
const packageBuild = {
|
|
259
|
+
baseUrl: '/_mintlify/pkg/abc',
|
|
260
|
+
islands,
|
|
261
|
+
packageNames: [...(setup?.packageNames ?? [])],
|
|
262
|
+
packageSnippets: [...collected.packageSnippets],
|
|
263
|
+
};
|
|
264
|
+
expect(collected.pages['revenue.mdx']).toEqual({ ids: [expect.any(String)], errors: [] });
|
|
265
|
+
expect(collected.pages['trend.mdx']).toEqual({ ids: [expect.any(String)], errors: [] });
|
|
266
|
+
const [barId] = collected.pages['revenue.mdx']?.ids ?? [];
|
|
267
|
+
expect(collected.islands.find((island) => island.id === barId)?.module).toContain('Acme revenue');
|
|
268
|
+
for (const page of ['revenue.mdx', 'trend.mdx']) {
|
|
269
|
+
const compiled = await compileLikeMintDev(page, packageBuild);
|
|
270
|
+
expect(compiled.parseFailed).toBeUndefined();
|
|
271
|
+
expect(Object.keys(compiled.mdxExtracts.islands ?? {})).toEqual(collected.pages[page]?.ids);
|
|
272
|
+
expect(compiled.mdxSource.compiledSource).not.toMatch(/BarChart|LineChart/);
|
|
273
|
+
}
|
|
274
|
+
expect((await collector.updatePage('revenue.mdx')).pages['revenue.mdx']?.ids).toEqual([barId]);
|
|
275
|
+
});
|
|
276
|
+
it('resolves <Snippet file> imports against the snippet file with the ids mint dev compiles', async () => {
|
|
277
|
+
const component = (name) => `import { BarChart, Bar } from "recharts";\n\nexport const ${name} = () => (\n <BarChart width={300} height={150} data={[{ v: 1 }]}>\n <Bar dataKey="v" name="${name}" />\n </BarChart>\n);\n`;
|
|
278
|
+
await fse.outputFile(join(testPaths.cmd, 'snippets/chart.jsx'), component('Chart'));
|
|
279
|
+
await fse.outputFile(join(testPaths.cmd, 'snippets/inner.jsx'), component('Inner'));
|
|
280
|
+
await fse.outputFile(join(testPaths.cmd, 'snippets/wrong.jsx'), component('Wrong'));
|
|
281
|
+
await fse.outputFile(join(testPaths.cmd, '_snippets/charts/x.mdx'), 'import { Chart } from "../../snippets/chart.jsx";\n\n<Chart />\n\n<Snippet file="charts/inner/y.mdx" />\n');
|
|
282
|
+
await fse.outputFile(join(testPaths.cmd, '_snippets/charts/inner/y.mdx'), 'import { Inner } from "../../../snippets/inner.jsx";\n\n<Inner />\n');
|
|
283
|
+
await fse.outputFile(join(testPaths.cmd, '_snippets/charts/w.mdx'), 'import { Wrong } from "../snippets/wrong.jsx";\n\n<Wrong />\n');
|
|
284
|
+
await fse.outputFile(join(testPaths.cmd, 'guides/deep/nested/page.mdx'), '# Nested\n\n<Snippet file="charts/x.mdx" />\n');
|
|
285
|
+
await fse.outputFile(join(testPaths.cmd, 'guides/wrong.mdx'), '# Wrong\n\n<Snippet file="charts/w.mdx" />\n');
|
|
286
|
+
const setup = await readPackagesSetup(testPaths.cmd);
|
|
287
|
+
const collected = await collectPackageIslands({
|
|
288
|
+
docsRoot: testPaths.cmd,
|
|
289
|
+
packageNames: setup?.packageNames ?? new Set(),
|
|
290
|
+
variables: setup?.variables,
|
|
291
|
+
});
|
|
292
|
+
setPackageImportOptions({
|
|
293
|
+
packageNames: setup?.packageNames ?? new Set(),
|
|
294
|
+
packageSnippets: collected.packageSnippets,
|
|
295
|
+
});
|
|
296
|
+
const packageBuild = {
|
|
297
|
+
baseUrl: '/_mintlify/pkg/abc',
|
|
298
|
+
islands: Object.fromEntries(collected.islands.map((island) => [
|
|
299
|
+
island.id,
|
|
300
|
+
{ prefix: `mi-${island.id}-`, html: '<svg></svg>' },
|
|
301
|
+
])),
|
|
302
|
+
packageNames: [...(setup?.packageNames ?? [])],
|
|
303
|
+
packageSnippets: [...collected.packageSnippets],
|
|
304
|
+
};
|
|
305
|
+
const modules = (page) => (collected.pages[page]?.ids ?? []).map((id) => collected.islands.find((island) => island.id === id)?.module);
|
|
306
|
+
expect(modules('guides/deep/nested/page.mdx')).toEqual([
|
|
307
|
+
expect.stringContaining('/snippets/chart.jsx'),
|
|
308
|
+
expect.stringContaining('/snippets/inner.jsx'),
|
|
309
|
+
]);
|
|
310
|
+
expect(collected.pages['guides/wrong.mdx']).toEqual({ ids: [], errors: [] });
|
|
311
|
+
for (const page of ['guides/deep/nested/page.mdx', 'guides/wrong.mdx']) {
|
|
312
|
+
const compiled = await compileLikeMintDev(page, packageBuild);
|
|
313
|
+
expect(compiled.parseFailed).toBeUndefined();
|
|
314
|
+
expect(Object.keys(compiled.mdxExtracts.islands ?? {})).toEqual(collected.pages[page]?.ids);
|
|
315
|
+
}
|
|
316
|
+
});
|
|
317
|
+
it('reports a <Snippet file> cycle on the page instead of expanding it forever', async () => {
|
|
318
|
+
await fse.outputFile(join(testPaths.cmd, '_snippets/note.mdx'), 'Note\n');
|
|
319
|
+
await fse.outputFile(join(testPaths.cmd, '_snippets/a.mdx'), 'A\n\n<Snippet file="b.mdx" />\n');
|
|
320
|
+
await fse.outputFile(join(testPaths.cmd, '_snippets/b.mdx'), 'B\n\n<Snippet file="a.mdx" />\n');
|
|
321
|
+
await fse.outputFile(join(testPaths.cmd, 'loop.mdx'), '# Loop\n\n<Snippet file="a.mdx" />\n');
|
|
322
|
+
const setup = await readPackagesSetup(testPaths.cmd);
|
|
323
|
+
const collected = await collectPackageIslands({
|
|
324
|
+
docsRoot: testPaths.cmd,
|
|
325
|
+
packageNames: setup?.packageNames ?? new Set(),
|
|
326
|
+
variables: setup?.variables,
|
|
327
|
+
});
|
|
328
|
+
expect(collected.pages['loop.mdx']).toEqual({
|
|
329
|
+
ids: [],
|
|
330
|
+
errors: [
|
|
331
|
+
{
|
|
332
|
+
file: 'loop.mdx',
|
|
333
|
+
message: '<Snippet file="a.mdx"> includes itself (a.mdx -> b.mdx -> a.mdx).',
|
|
334
|
+
},
|
|
335
|
+
],
|
|
336
|
+
});
|
|
337
|
+
expect(collected.pages['charts.mdx']?.errors).toEqual([]);
|
|
338
|
+
});
|
|
339
|
+
it('falls back to the snippet trees of a collector for the same docs root only', async () => {
|
|
340
|
+
const setup = await readPackagesSetup(testPaths.cmd);
|
|
341
|
+
const input = { packageNames: setup?.packageNames ?? new Set() };
|
|
342
|
+
const previous = new PackageIslandCollector({ ...input, docsRoot: testPaths.cmd });
|
|
343
|
+
expect((await previous.collectAll()).packageSnippets).toContain('/snippets/revenue.jsx');
|
|
344
|
+
const otherRoot = join(root, 'other');
|
|
345
|
+
await fse.copy(testPaths.cmd, otherRoot);
|
|
346
|
+
for (const docsRoot of [testPaths.cmd, otherRoot]) {
|
|
347
|
+
await fse.outputFile(join(docsRoot, 'snippets/revenue.jsx'), REVENUE.replace('</BarChart>', ''));
|
|
348
|
+
}
|
|
349
|
+
const same = new PackageIslandCollector({ ...input, docsRoot: testPaths.cmd });
|
|
350
|
+
same.inheritSnippetTrees(previous);
|
|
351
|
+
const other = new PackageIslandCollector({ ...input, docsRoot: otherRoot });
|
|
352
|
+
other.inheritSnippetTrees(previous);
|
|
353
|
+
expect((await same.collectAll()).packageSnippets).toContain('/snippets/revenue.jsx');
|
|
354
|
+
expect((await other.collectAll()).packageSnippets).not.toContain('/snippets/revenue.jsx');
|
|
355
|
+
});
|
|
356
|
+
it('inherits snippet trees once the previous collector finishes its queued updates', async () => {
|
|
357
|
+
const setup = await readPackagesSetup(testPaths.cmd);
|
|
358
|
+
const input = { docsRoot: testPaths.cmd, packageNames: setup?.packageNames ?? new Set() };
|
|
359
|
+
const revenue = join(testPaths.cmd, 'snippets/revenue.jsx');
|
|
360
|
+
const previous = new PackageIslandCollector(input);
|
|
361
|
+
expect((await previous.collectAll()).packageSnippets).toContain('/snippets/revenue.jsx');
|
|
362
|
+
await fse.remove(revenue);
|
|
363
|
+
const deleted = previous.collectAll();
|
|
364
|
+
const next = new PackageIslandCollector(input);
|
|
365
|
+
next.inheritSnippetTrees(previous);
|
|
366
|
+
expect((await deleted).packageSnippets).not.toContain('/snippets/revenue.jsx');
|
|
367
|
+
await fse.outputFile(revenue, REVENUE.replace('</BarChart>', ''));
|
|
368
|
+
expect((await next.collectAll()).packageSnippets).not.toContain('/snippets/revenue.jsx');
|
|
369
|
+
});
|
|
370
|
+
it('updates one page without re-reading the rest', async () => {
|
|
371
|
+
const setup = await readPackagesSetup(testPaths.cmd);
|
|
372
|
+
const collector = new PackageIslandCollector({
|
|
373
|
+
docsRoot: testPaths.cmd,
|
|
374
|
+
packageNames: setup?.packageNames ?? new Set(),
|
|
375
|
+
});
|
|
376
|
+
const before = await collector.collectAll();
|
|
377
|
+
await fse.outputFile(join(testPaths.cmd, 'charts.mdx'), CHARTS.replace('<Line dataKey="v"', '<Line dataKey="v" strokeWidth={3}'));
|
|
378
|
+
await fse.outputFile(join(testPaths.cmd, 'plain.mdx'), BROKEN);
|
|
379
|
+
const after = await collector.updatePage('charts.mdx');
|
|
380
|
+
expect(after.pages['charts.mdx']?.ids[0]).not.toBe(before.pages['charts.mdx']?.ids[0]);
|
|
381
|
+
expect(after.pages['charts.mdx']?.ids[1]).toBe(before.pages['charts.mdx']?.ids[1]);
|
|
382
|
+
expect(after.pages['plain.mdx']?.ids).toEqual([]);
|
|
383
|
+
expect(after.islands).toHaveLength(2);
|
|
384
|
+
await fse.remove(join(testPaths.cmd, 'charts.mdx'));
|
|
385
|
+
expect((await collector.updatePage('charts.mdx')).islands).toEqual([]);
|
|
386
|
+
});
|
|
387
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|