@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.
- package/dist/__tests__/listener-graphql-packages.test.d.ts +1 -0
- 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 +457 -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.d.ts +1 -0
- package/dist/__tests__/packages-setup.test.js +160 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- 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 +6 -0
- package/dist/packages/builder.js +17 -0
- package/dist/packages/collect.d.ts +61 -0
- package/dist/packages/collect.js +240 -0
- package/dist/packages/errno.d.ts +1 -0
- package/dist/packages/errno.js +3 -0
- package/dist/packages/flag.d.ts +1 -0
- package/dist/packages/flag.js +1 -0
- package/dist/packages/index.d.ts +3 -0
- package/dist/packages/index.js +3 -0
- package/dist/packages/setup.d.ts +18 -0
- package/dist/packages/setup.js +81 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +6 -3
|
@@ -0,0 +1,1304 @@
|
|
|
1
|
+
import { getFileCategory, isMintIgnored } from '@mintlify/common';
|
|
2
|
+
import { collectLocalFiles, } from '@mintlify/package-builder';
|
|
3
|
+
import fse from 'fs-extra';
|
|
4
|
+
import { mkdtemp, rm } from 'fs/promises';
|
|
5
|
+
import { tmpdir } from 'os';
|
|
6
|
+
import { join } from 'path';
|
|
7
|
+
const heldRead = vi.hoisted(() => ({
|
|
8
|
+
current: undefined,
|
|
9
|
+
}));
|
|
10
|
+
vi.mock('fs/promises', async (importOriginal) => {
|
|
11
|
+
const actual = await importOriginal();
|
|
12
|
+
const readFile = actual.readFile;
|
|
13
|
+
return {
|
|
14
|
+
...actual,
|
|
15
|
+
readFile: async (...args) => {
|
|
16
|
+
const data = await readFile(...args);
|
|
17
|
+
const hold = heldRead.current;
|
|
18
|
+
if (hold && String(args[0]).endsWith(hold.file)) {
|
|
19
|
+
heldRead.current = undefined;
|
|
20
|
+
hold.reached = true;
|
|
21
|
+
await hold.until;
|
|
22
|
+
}
|
|
23
|
+
return data;
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
});
|
|
27
|
+
import { PACKAGE_BUILD_PROPS_FILE, PackagesDev, } from '../local-preview/packages/dev.js';
|
|
28
|
+
import { getPackageImportOptions, setPackageImportOptions, } from '../local-preview/packages/state.js';
|
|
29
|
+
import { collectPackageIslands } from '../packages/collect.js';
|
|
30
|
+
const page = (stroke) => `import { LineChart, Line } from "recharts";
|
|
31
|
+
|
|
32
|
+
<LineChart width={400} height={200} data={[{ v: 1 }]}>
|
|
33
|
+
<Line dataKey="v" strokeWidth={${stroke}} />
|
|
34
|
+
</LineChart>
|
|
35
|
+
`;
|
|
36
|
+
const fakeBuild = (mode) => async (input) => {
|
|
37
|
+
const missingLocalFiles = [];
|
|
38
|
+
const localFiles = Object.keys(collectLocalFiles({
|
|
39
|
+
docsRoot: input.docsRoot,
|
|
40
|
+
islands: input.islands,
|
|
41
|
+
snippetFiles: input.snippetFiles,
|
|
42
|
+
provider: input.packages.provider,
|
|
43
|
+
onMissing: (file) => missingLocalFiles.push(file),
|
|
44
|
+
})).sort();
|
|
45
|
+
await mode.building;
|
|
46
|
+
if (mode.throwing)
|
|
47
|
+
throw new Error('EACCES: permission denied');
|
|
48
|
+
const ids = input.islands.map((island) => island.id).sort();
|
|
49
|
+
const buildId = `b${ids.join('').slice(0, 30)}`;
|
|
50
|
+
const manifest = {
|
|
51
|
+
version: 1,
|
|
52
|
+
builderVersion: 'test',
|
|
53
|
+
buildId,
|
|
54
|
+
depsHash: 'd',
|
|
55
|
+
islandsHash: 'i',
|
|
56
|
+
react: { version: '18.3.1', domVersion: '18.3.1' },
|
|
57
|
+
packages: {},
|
|
58
|
+
css: [],
|
|
59
|
+
clientAccess: { version: 1, files: {}, globalFiles: [] },
|
|
60
|
+
islands: {},
|
|
61
|
+
files: [],
|
|
62
|
+
diagnostics: mode.missingNodeModules
|
|
63
|
+
? [{ level: 'error', code: 'install-missing-node-modules', message: 'No node_modules.' }]
|
|
64
|
+
: mode.failing
|
|
65
|
+
? [
|
|
66
|
+
{
|
|
67
|
+
level: 'error',
|
|
68
|
+
code: mode.failCode ?? 'bundle-failed',
|
|
69
|
+
message: 'Could not resolve "recharts".',
|
|
70
|
+
},
|
|
71
|
+
]
|
|
72
|
+
: [],
|
|
73
|
+
};
|
|
74
|
+
await fse.ensureDir(input.outDir);
|
|
75
|
+
if (!mode.missingNodeModules) {
|
|
76
|
+
for (const id of ids) {
|
|
77
|
+
await fse.outputFile(join(input.outDir, 'client/islands', `${id}.js`), 'export {}');
|
|
78
|
+
manifest.files.push({
|
|
79
|
+
path: `client/islands/${id}.js`,
|
|
80
|
+
bytes: 9,
|
|
81
|
+
gzipBytes: 9,
|
|
82
|
+
integrity: 'sha384-fixture',
|
|
83
|
+
});
|
|
84
|
+
manifest.clientAccess.files[`client/islands/${id}.js`] = [id];
|
|
85
|
+
if (mode.islandCss) {
|
|
86
|
+
await fse.outputFile(join(input.outDir, 'client/islands', `${id}.css`), 'a{}');
|
|
87
|
+
manifest.files.push({
|
|
88
|
+
path: `client/islands/${id}.css`,
|
|
89
|
+
bytes: 3,
|
|
90
|
+
gzipBytes: 3,
|
|
91
|
+
integrity: 'sha384-css',
|
|
92
|
+
});
|
|
93
|
+
manifest.clientAccess.files[`client/islands/${id}.css`] = [id];
|
|
94
|
+
}
|
|
95
|
+
manifest.islands[id] = {
|
|
96
|
+
entry: `client/islands/${id}.js`,
|
|
97
|
+
prefix: `mi-${id}-`,
|
|
98
|
+
html: `<svg data-id="${id}"></svg>`,
|
|
99
|
+
...(mode.islandCss && {
|
|
100
|
+
css: [{ path: `client/islands/${id}.css`, integrity: 'sha384-css', bytes: 3 }],
|
|
101
|
+
}),
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
await fse.outputFile(join(input.outDir, 'client/styles.css'), 'a{}');
|
|
105
|
+
manifest.css.push({ path: 'client/styles.css', integrity: 'sha384-x', bytes: 3 });
|
|
106
|
+
manifest.files.push({
|
|
107
|
+
path: 'client/styles.css',
|
|
108
|
+
integrity: 'sha384-x',
|
|
109
|
+
bytes: 3,
|
|
110
|
+
gzipBytes: 3,
|
|
111
|
+
});
|
|
112
|
+
manifest.clientAccess.files['client/styles.css'] = [];
|
|
113
|
+
manifest.clientAccess.globalFiles.push('client/styles.css');
|
|
114
|
+
}
|
|
115
|
+
return {
|
|
116
|
+
ok: !mode.missingNodeModules && !mode.failing,
|
|
117
|
+
manifest,
|
|
118
|
+
localFiles,
|
|
119
|
+
missingLocalFiles: missingLocalFiles.sort(),
|
|
120
|
+
stats: {
|
|
121
|
+
installMs: 0,
|
|
122
|
+
bundleMs: 0,
|
|
123
|
+
renderMs: 0,
|
|
124
|
+
totalMs: 0,
|
|
125
|
+
clientBytes: 0,
|
|
126
|
+
clientGzipBytes: 0,
|
|
127
|
+
serverBytes: 0,
|
|
128
|
+
islandClientGzipBytes: {},
|
|
129
|
+
reactDirs: { react: [], 'react-dom': [] },
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
};
|
|
133
|
+
const reporter = () => {
|
|
134
|
+
const calls = [];
|
|
135
|
+
return {
|
|
136
|
+
calls,
|
|
137
|
+
building: () => calls.push('building'),
|
|
138
|
+
built: () => calls.push('built'),
|
|
139
|
+
failed: (errors) => calls.push(`failed ${errors.join(',')}`),
|
|
140
|
+
missingNodeModules: () => calls.push('missing'),
|
|
141
|
+
islandErrors: (errors) => {
|
|
142
|
+
if (errors.length > 0)
|
|
143
|
+
calls.push(`islandErrors ${errors.join(',')}`);
|
|
144
|
+
},
|
|
145
|
+
configError: (message) => calls.push(`configError ${message}`),
|
|
146
|
+
};
|
|
147
|
+
};
|
|
148
|
+
const until = async (condition) => {
|
|
149
|
+
for (let i = 0; i < 200; i++) {
|
|
150
|
+
if (await condition())
|
|
151
|
+
return;
|
|
152
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
153
|
+
}
|
|
154
|
+
throw new Error('condition not met');
|
|
155
|
+
};
|
|
156
|
+
describe('PackagesDev', () => {
|
|
157
|
+
let root;
|
|
158
|
+
let docsRoot;
|
|
159
|
+
let propsPath;
|
|
160
|
+
let buildsDir;
|
|
161
|
+
let mode;
|
|
162
|
+
let rebuilt;
|
|
163
|
+
let dev;
|
|
164
|
+
let report;
|
|
165
|
+
const props = async () => fse.readJson(join(propsPath, PACKAGE_BUILD_PROPS_FILE));
|
|
166
|
+
const create = (overrides = {}) => new PackagesDev({
|
|
167
|
+
docsRoot,
|
|
168
|
+
buildsDir,
|
|
169
|
+
propsPath,
|
|
170
|
+
onRebuilt: () => {
|
|
171
|
+
rebuilt++;
|
|
172
|
+
},
|
|
173
|
+
reporter: report,
|
|
174
|
+
debounceMs: 0,
|
|
175
|
+
pollMs: 10,
|
|
176
|
+
build: fakeBuild(mode),
|
|
177
|
+
...overrides,
|
|
178
|
+
});
|
|
179
|
+
beforeEach(async () => {
|
|
180
|
+
root = await mkdtemp(join(tmpdir(), 'packages-dev-'));
|
|
181
|
+
docsRoot = join(root, 'docs');
|
|
182
|
+
propsPath = join(root, 'props');
|
|
183
|
+
buildsDir = join(root, 'builds');
|
|
184
|
+
mode = { missingNodeModules: false };
|
|
185
|
+
rebuilt = 0;
|
|
186
|
+
report = reporter();
|
|
187
|
+
await fse.outputJson(join(docsRoot, 'docs.json'), { name: 'Docs', packages: {} });
|
|
188
|
+
await fse.outputJson(join(docsRoot, 'package.json'), { dependencies: { recharts: '2.15.4' } });
|
|
189
|
+
await fse.outputFile(join(docsRoot, 'charts.mdx'), page(2));
|
|
190
|
+
dev = create();
|
|
191
|
+
});
|
|
192
|
+
afterEach(async () => {
|
|
193
|
+
await dev.close();
|
|
194
|
+
setPackageImportOptions(undefined);
|
|
195
|
+
await rm(root, { recursive: true, force: true });
|
|
196
|
+
});
|
|
197
|
+
it('builds on start and writes the build for the local compile', async () => {
|
|
198
|
+
await fse.outputFile(join(propsPath, 'charts.mdx'), 'compiled by prebuild');
|
|
199
|
+
await dev.init();
|
|
200
|
+
expect(await fse.pathExists(join(propsPath, 'charts.mdx'))).toBe(true);
|
|
201
|
+
const written = await props();
|
|
202
|
+
const [id] = Object.keys(written.islands);
|
|
203
|
+
expect(written.baseUrl).toMatch(/^\/_mintlify\/pkg\/bi[0-9a-f]+-1$/);
|
|
204
|
+
expect(written.islands[id ?? '']).toEqual({
|
|
205
|
+
prefix: `mi-${id}-`,
|
|
206
|
+
html: `<svg data-id="${id}"></svg>`,
|
|
207
|
+
});
|
|
208
|
+
expect(written.packageNames).toEqual(['recharts']);
|
|
209
|
+
expect(written.css.map((css) => css.path)).toEqual(['client/styles.css']);
|
|
210
|
+
expect(dev.getStatus()).toEqual({ state: 'ready' });
|
|
211
|
+
expect(getPackageImportOptions()?.packageNames).toEqual(new Set(['recharts']));
|
|
212
|
+
expect(await fse.readdir(buildsDir)).toEqual([written.baseUrl.split('/').pop()]);
|
|
213
|
+
expect(dev.routeState().enabled).toBe(true);
|
|
214
|
+
});
|
|
215
|
+
it('preserves global and island CSS references in local compile props', async () => {
|
|
216
|
+
mode.islandCss = true;
|
|
217
|
+
await dev.init();
|
|
218
|
+
const written = await props();
|
|
219
|
+
const [id] = Object.keys(written.islands);
|
|
220
|
+
expect(written.css).toEqual([{ path: 'client/styles.css', integrity: 'sha384-x', bytes: 3 }]);
|
|
221
|
+
expect(written.islands[id ?? '']?.css).toEqual([
|
|
222
|
+
{ path: `client/islands/${id}.css`, integrity: 'sha384-css', bytes: 3 },
|
|
223
|
+
]);
|
|
224
|
+
expect(written.baseUrl).not.toContain('/pages/');
|
|
225
|
+
});
|
|
226
|
+
it('adds a changed island right away and swaps in the rebuild', async () => {
|
|
227
|
+
await dev.init();
|
|
228
|
+
rebuilt = 0;
|
|
229
|
+
const before = await props();
|
|
230
|
+
await fse.outputFile(join(docsRoot, 'charts.mdx'), page(3));
|
|
231
|
+
const change = dev.onFileChange('charts.mdx', 'page');
|
|
232
|
+
await change;
|
|
233
|
+
const pending = await props();
|
|
234
|
+
const [newId] = Object.keys(pending.islands);
|
|
235
|
+
expect(Object.keys(before.islands)).not.toContain(newId);
|
|
236
|
+
expect(pending.baseUrl).toBe(before.baseUrl);
|
|
237
|
+
expect(pending.islands[newId ?? '']?.html).toBeUndefined();
|
|
238
|
+
expect(dev.getStatus()).toEqual({ state: 'building' });
|
|
239
|
+
await until(async () => (await props()).baseUrl !== before.baseUrl);
|
|
240
|
+
const after = await props();
|
|
241
|
+
expect(after.islands[newId ?? '']?.html).toContain(newId);
|
|
242
|
+
expect(rebuilt).toBe(1);
|
|
243
|
+
expect((await fse.readdir(buildsDir)).sort()).toEqual([before.baseUrl, after.baseUrl].map((url) => url.split('/').pop()).sort());
|
|
244
|
+
});
|
|
245
|
+
it('keeps listing the islands edits drop until the next build finishes', async () => {
|
|
246
|
+
await dev.init();
|
|
247
|
+
const [built] = Object.keys((await props()).islands);
|
|
248
|
+
let release = () => { };
|
|
249
|
+
mode.building = new Promise((resolve) => {
|
|
250
|
+
release = resolve;
|
|
251
|
+
});
|
|
252
|
+
await fse.outputFile(join(docsRoot, 'charts.mdx'), page(3));
|
|
253
|
+
await dev.onFileChange('charts.mdx', 'page');
|
|
254
|
+
const [added] = Object.keys((await props()).islands);
|
|
255
|
+
await fse.outputFile(join(docsRoot, 'charts.mdx'), page(4));
|
|
256
|
+
await dev.onFileChange('charts.mdx', 'page');
|
|
257
|
+
const pending = (await props()).islands;
|
|
258
|
+
release();
|
|
259
|
+
const [current] = Object.keys(pending);
|
|
260
|
+
expect(Object.keys(pending).sort()).toEqual([current, added, built].sort());
|
|
261
|
+
expect(pending[built ?? '']?.html).toBe(`<svg data-id="${built}"></svg>`);
|
|
262
|
+
expect(pending[added ?? '']?.html).toBeUndefined();
|
|
263
|
+
expect(pending[current ?? '']?.html).toBeUndefined();
|
|
264
|
+
const settled = {
|
|
265
|
+
[current ?? '']: { prefix: `mi-${current}-`, html: `<svg data-id="${current}"></svg>` },
|
|
266
|
+
};
|
|
267
|
+
await until(async () => JSON.stringify((await props()).islands) === JSON.stringify(settled));
|
|
268
|
+
});
|
|
269
|
+
it('rebuilds when a page edit is undone while its build runs', async () => {
|
|
270
|
+
await dev.init();
|
|
271
|
+
rebuilt = 0;
|
|
272
|
+
const before = (await props()).islands;
|
|
273
|
+
let release = () => { };
|
|
274
|
+
mode.building = new Promise((resolve) => {
|
|
275
|
+
release = resolve;
|
|
276
|
+
});
|
|
277
|
+
await fse.outputFile(join(docsRoot, 'charts.mdx'), page(3));
|
|
278
|
+
await dev.onFileChange('charts.mdx', 'page');
|
|
279
|
+
await until(() => report.calls.filter((call) => call === 'building').length === 2);
|
|
280
|
+
await fse.outputFile(join(docsRoot, 'charts.mdx'), page(2));
|
|
281
|
+
await dev.onFileChange('charts.mdx', 'page');
|
|
282
|
+
release();
|
|
283
|
+
await until(() => rebuilt === 2);
|
|
284
|
+
expect((await props()).islands).toEqual(before);
|
|
285
|
+
});
|
|
286
|
+
it('re-extracts the islands of a <Snippet file> when the snippet changes', async () => {
|
|
287
|
+
await fse.outputFile(join(docsRoot, 'charts.mdx'), '<Snippet file="chart.mdx" />\n');
|
|
288
|
+
await fse.outputFile(join(docsRoot, '_snippets/chart.mdx'), page(2));
|
|
289
|
+
await dev.init();
|
|
290
|
+
rebuilt = 0;
|
|
291
|
+
const before = Object.keys((await props()).islands);
|
|
292
|
+
expect(before).toHaveLength(1);
|
|
293
|
+
await fse.outputFile(join(docsRoot, '_snippets/chart.mdx'), page(3));
|
|
294
|
+
await dev.onFileChange('_snippets/chart.mdx', 'snippet');
|
|
295
|
+
await until(() => rebuilt === 1);
|
|
296
|
+
const after = Object.keys((await props()).islands);
|
|
297
|
+
expect(after).toHaveLength(1);
|
|
298
|
+
expect(after).not.toEqual(before);
|
|
299
|
+
});
|
|
300
|
+
it('retries replacing the props file while another process has it open', async () => {
|
|
301
|
+
const rename = fse.rename.bind(fse);
|
|
302
|
+
let thrown = 0;
|
|
303
|
+
const spy = vi.spyOn(fse, 'rename').mockImplementation(async (from, to) => {
|
|
304
|
+
if (String(to).endsWith(PACKAGE_BUILD_PROPS_FILE) && thrown < 2) {
|
|
305
|
+
thrown++;
|
|
306
|
+
throw Object.assign(new Error('EPERM: operation not permitted, rename'), { code: 'EPERM' });
|
|
307
|
+
}
|
|
308
|
+
return rename(from, to);
|
|
309
|
+
});
|
|
310
|
+
try {
|
|
311
|
+
await dev.init();
|
|
312
|
+
expect(thrown).toBe(2);
|
|
313
|
+
expect(dev.getStatus()).toEqual({ state: 'ready' });
|
|
314
|
+
expect((await props()).packageNames).toEqual(['recharts']);
|
|
315
|
+
}
|
|
316
|
+
finally {
|
|
317
|
+
spy.mockRestore();
|
|
318
|
+
}
|
|
319
|
+
});
|
|
320
|
+
it('serializes concurrent props writes', async () => {
|
|
321
|
+
await dev.init();
|
|
322
|
+
await Promise.all(Array.from({ length: 5 }, () => dev.writeProps()));
|
|
323
|
+
expect((await props()).packageNames).toEqual(['recharts']);
|
|
324
|
+
expect((await fse.readdir(propsPath)).filter((file) => file.endsWith('.tmp'))).toEqual([]);
|
|
325
|
+
});
|
|
326
|
+
it('never lets a slow props write overwrite a newer one', async () => {
|
|
327
|
+
await dev.init();
|
|
328
|
+
rebuilt = 0;
|
|
329
|
+
const before = await props();
|
|
330
|
+
const outputFile = fse.outputFile.bind(fse);
|
|
331
|
+
let delayed = false;
|
|
332
|
+
const spy = vi.spyOn(fse, 'outputFile').mockImplementation(async (file, data) => {
|
|
333
|
+
if (!delayed && String(file).includes(PACKAGE_BUILD_PROPS_FILE)) {
|
|
334
|
+
delayed = true;
|
|
335
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
336
|
+
}
|
|
337
|
+
return outputFile(file, data);
|
|
338
|
+
});
|
|
339
|
+
try {
|
|
340
|
+
const slow = dev.writeProps();
|
|
341
|
+
await fse.outputFile(join(docsRoot, 'charts.mdx'), page(3));
|
|
342
|
+
await Promise.all([slow, dev.onFileChange('charts.mdx', 'page')]);
|
|
343
|
+
await until(() => rebuilt === 1);
|
|
344
|
+
const after = await props();
|
|
345
|
+
expect(after.baseUrl).not.toBe(before.baseUrl);
|
|
346
|
+
expect(Object.keys(after.islands)).not.toEqual(Object.keys(before.islands));
|
|
347
|
+
expect(Object.values(after.islands).every((island) => island.html !== undefined)).toBe(true);
|
|
348
|
+
}
|
|
349
|
+
finally {
|
|
350
|
+
spy.mockRestore();
|
|
351
|
+
}
|
|
352
|
+
});
|
|
353
|
+
it('keeps a page edit that lands while every page is being re-collected', async () => {
|
|
354
|
+
await fse.outputFile(join(docsRoot, 'area.mdx'), page(5));
|
|
355
|
+
await dev.init();
|
|
356
|
+
let release = () => { };
|
|
357
|
+
const hold = {
|
|
358
|
+
file: join(docsRoot, 'charts.mdx'),
|
|
359
|
+
until: new Promise((resolve) => {
|
|
360
|
+
release = resolve;
|
|
361
|
+
}),
|
|
362
|
+
reached: false,
|
|
363
|
+
};
|
|
364
|
+
heldRead.current = hold;
|
|
365
|
+
await dev.onFileChange('.mintignore', 'mintIgnore');
|
|
366
|
+
await until(() => hold.reached);
|
|
367
|
+
await fse.outputFile(join(docsRoot, 'charts.mdx'), page(3));
|
|
368
|
+
const change = dev.onFileChange('charts.mdx', 'page');
|
|
369
|
+
await Promise.race([change, new Promise((resolve) => setTimeout(resolve, 200))]);
|
|
370
|
+
release();
|
|
371
|
+
await change;
|
|
372
|
+
const expected = await collectPackageIslands({
|
|
373
|
+
docsRoot,
|
|
374
|
+
packageNames: new Set(['recharts']),
|
|
375
|
+
});
|
|
376
|
+
const ids = JSON.stringify(expected.islands.map((island) => island.id).sort());
|
|
377
|
+
await until(async () => {
|
|
378
|
+
const after = await props();
|
|
379
|
+
return (JSON.stringify(Object.keys(after.islands).sort()) === ids &&
|
|
380
|
+
Object.values(after.islands).every((island) => island.html !== undefined));
|
|
381
|
+
});
|
|
382
|
+
});
|
|
383
|
+
it('keeps collecting when a page vanishes while every page is being re-collected', async () => {
|
|
384
|
+
await fse.outputFile(join(docsRoot, 'area.mdx'), page(5));
|
|
385
|
+
await dev.init();
|
|
386
|
+
rebuilt = 0;
|
|
387
|
+
let vanish = (_error) => { };
|
|
388
|
+
const hold = {
|
|
389
|
+
file: join(docsRoot, 'area.mdx'),
|
|
390
|
+
until: new Promise((_resolve, reject) => {
|
|
391
|
+
vanish = reject;
|
|
392
|
+
}),
|
|
393
|
+
reached: false,
|
|
394
|
+
};
|
|
395
|
+
heldRead.current = hold;
|
|
396
|
+
await fse.outputFile(join(docsRoot, 'added.mdx'), page(7));
|
|
397
|
+
const change = dev.onFileChange('added.mdx', 'page');
|
|
398
|
+
await until(() => hold.reached);
|
|
399
|
+
await fse.remove(join(docsRoot, 'area.mdx'));
|
|
400
|
+
vanish(Object.assign(new Error('ENOENT: no such file or directory'), { code: 'ENOENT' }));
|
|
401
|
+
await change;
|
|
402
|
+
await dev.onFileChange('area.mdx', 'page');
|
|
403
|
+
const expected = (await collectPackageIslands({ docsRoot, packageNames: new Set(['recharts']) })).islands
|
|
404
|
+
.map((island) => island.id)
|
|
405
|
+
.sort();
|
|
406
|
+
await until(async () => {
|
|
407
|
+
const after = await props();
|
|
408
|
+
return (JSON.stringify(Object.keys(after.islands).sort()) === JSON.stringify(expected) &&
|
|
409
|
+
Object.values(after.islands).every((island) => island.html !== undefined));
|
|
410
|
+
});
|
|
411
|
+
expect(report.calls.filter((call) => call.startsWith('failed'))).toEqual([]);
|
|
412
|
+
});
|
|
413
|
+
it('waits for the running build when closed', async () => {
|
|
414
|
+
await dev.init();
|
|
415
|
+
rebuilt = 0;
|
|
416
|
+
let release = () => { };
|
|
417
|
+
mode.building = new Promise((resolve) => {
|
|
418
|
+
release = resolve;
|
|
419
|
+
});
|
|
420
|
+
await fse.outputFile(join(docsRoot, 'charts.mdx'), page(3));
|
|
421
|
+
await dev.onFileChange('charts.mdx', 'page');
|
|
422
|
+
await until(() => report.calls.filter((call) => call === 'building').length === 2);
|
|
423
|
+
let closed = false;
|
|
424
|
+
const closing = dev.close().then(() => {
|
|
425
|
+
closed = true;
|
|
426
|
+
});
|
|
427
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
428
|
+
expect(closed).toBe(false);
|
|
429
|
+
release();
|
|
430
|
+
await closing;
|
|
431
|
+
expect(rebuilt).toBe(1);
|
|
432
|
+
});
|
|
433
|
+
it('skips the rebuild when a page edit keeps the same islands', async () => {
|
|
434
|
+
await dev.init();
|
|
435
|
+
rebuilt = 0;
|
|
436
|
+
await fse.outputFile(join(docsRoot, 'charts.mdx'), `${page(2)}\nMore prose.\n`);
|
|
437
|
+
await dev.onFileChange('charts.mdx', 'page');
|
|
438
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
439
|
+
expect(rebuilt).toBe(0);
|
|
440
|
+
});
|
|
441
|
+
it('ignores markdown that is neither a page nor a snippet', async () => {
|
|
442
|
+
await dev.init();
|
|
443
|
+
rebuilt = 0;
|
|
444
|
+
report.calls.length = 0;
|
|
445
|
+
for (const file of [
|
|
446
|
+
'contribute.md',
|
|
447
|
+
'guides/contribute.md',
|
|
448
|
+
'skill.md',
|
|
449
|
+
'.mintlify/skills/docs/skill.md',
|
|
450
|
+
]) {
|
|
451
|
+
expect(isMintIgnored(file)).toBe(false);
|
|
452
|
+
await fse.outputFile(join(docsRoot, file), page(3));
|
|
453
|
+
await dev.onFileChange(file, getFileCategory(file));
|
|
454
|
+
expect(dev.getStatus()).toEqual({ state: 'ready' });
|
|
455
|
+
}
|
|
456
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
457
|
+
expect(rebuilt).toBe(0);
|
|
458
|
+
expect(report.calls).toEqual([]);
|
|
459
|
+
await fse.outputFile(join(docsRoot, 'charts.mdx'), page(3));
|
|
460
|
+
await dev.onFileChange('charts.mdx', 'page');
|
|
461
|
+
await until(() => rebuilt === 1);
|
|
462
|
+
});
|
|
463
|
+
it('keeps serving pages without node_modules and rebuilds once they appear', async () => {
|
|
464
|
+
mode.missingNodeModules = true;
|
|
465
|
+
await dev.init();
|
|
466
|
+
const written = await props();
|
|
467
|
+
expect(written.baseUrl).toBe('/_mintlify/pkg/pending');
|
|
468
|
+
expect(Object.values(written.islands).every((island) => island.html === undefined)).toBe(true);
|
|
469
|
+
expect(dev.getStatus()).toEqual({ state: 'missing-node-modules' });
|
|
470
|
+
expect(report.calls).toContain('missing');
|
|
471
|
+
mode.missingNodeModules = false;
|
|
472
|
+
await fse.outputFile(join(docsRoot, 'node_modules/.package-lock.json'), '{}');
|
|
473
|
+
await until(async () => (await props()).baseUrl !== '/_mintlify/pkg/pending');
|
|
474
|
+
expect(dev.getStatus()).toEqual({ state: 'ready' });
|
|
475
|
+
});
|
|
476
|
+
it('rebuilds once an install without a known marker settles node_modules', async () => {
|
|
477
|
+
mode.missingNodeModules = true;
|
|
478
|
+
await dev.init();
|
|
479
|
+
expect(dev.getStatus()).toEqual({ state: 'missing-node-modules' });
|
|
480
|
+
mode.missingNodeModules = false;
|
|
481
|
+
mode.failing = true;
|
|
482
|
+
await fse.outputJson(join(docsRoot, 'node_modules/react/package.json'), { version: '18.3.1' });
|
|
483
|
+
await until(() => dev.getStatus().state === 'failed');
|
|
484
|
+
mode.failing = false;
|
|
485
|
+
await fse.outputJson(join(docsRoot, 'node_modules/recharts/package.json'), {
|
|
486
|
+
version: '2.15.4',
|
|
487
|
+
});
|
|
488
|
+
await until(() => dev.getStatus().state === 'ready');
|
|
489
|
+
expect((await props()).baseUrl).not.toBe('/_mintlify/pkg/pending');
|
|
490
|
+
});
|
|
491
|
+
it('waits for the installer marker instead of a half-written node_modules when a lockfile exists', async () => {
|
|
492
|
+
await fse.outputJson(join(docsRoot, 'package-lock.json'), { lockfileVersion: 3, packages: {} });
|
|
493
|
+
mode.missingNodeModules = true;
|
|
494
|
+
await dev.init();
|
|
495
|
+
mode.missingNodeModules = false;
|
|
496
|
+
await fse.outputJson(join(docsRoot, 'node_modules/recharts/package.json'), {
|
|
497
|
+
version: '2.15.4',
|
|
498
|
+
});
|
|
499
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
500
|
+
expect(dev.getStatus()).toEqual({ state: 'missing-node-modules' });
|
|
501
|
+
expect(report.calls.filter((call) => call === 'building')).toHaveLength(1);
|
|
502
|
+
await fse.outputFile(join(docsRoot, 'node_modules/.package-lock.json'), '{}');
|
|
503
|
+
await until(() => dev.getStatus().state === 'ready');
|
|
504
|
+
});
|
|
505
|
+
it('rebuilds a successful build with dependency drift when the install settles, then stops watching', async () => {
|
|
506
|
+
const marker = join(docsRoot, 'node_modules/.package-lock.json');
|
|
507
|
+
const installedVersion = join(docsRoot, 'node_modules/installed-version.txt');
|
|
508
|
+
await fse.outputJson(join(docsRoot, 'package-lock.json'), { lockfileVersion: 3, packages: {} });
|
|
509
|
+
await fse.outputFile(marker, '{}');
|
|
510
|
+
await fse.outputFile(installedVersion, '1.0.0');
|
|
511
|
+
const build = fakeBuild(mode);
|
|
512
|
+
await dev.close();
|
|
513
|
+
dev = create({
|
|
514
|
+
build: async (input) => {
|
|
515
|
+
const version = await fse.readFile(installedVersion, 'utf8');
|
|
516
|
+
const result = await build(input);
|
|
517
|
+
for (const island of Object.values(result.manifest.islands))
|
|
518
|
+
island.html = version;
|
|
519
|
+
if (version === '1.0.0') {
|
|
520
|
+
result.manifest.diagnostics.push({
|
|
521
|
+
level: 'warning',
|
|
522
|
+
code: 'local-node-modules-drift',
|
|
523
|
+
message: 'The installed dependency does not match the lockfile.',
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
return result;
|
|
527
|
+
},
|
|
528
|
+
});
|
|
529
|
+
await dev.init();
|
|
530
|
+
expect(dev.getStatus()).toEqual({ state: 'ready' });
|
|
531
|
+
expect(Object.values((await props()).islands).map((island) => island.html)).toEqual(['1.0.0']);
|
|
532
|
+
await fse.writeFile(installedVersion, '2.0.0');
|
|
533
|
+
await fse.writeFile(marker, '{"installed":true}');
|
|
534
|
+
await until(() => rebuilt === 2);
|
|
535
|
+
expect(Object.values((await props()).islands).map((island) => island.html)).toEqual(['2.0.0']);
|
|
536
|
+
expect(dev.getStatus()).toEqual({ state: 'ready' });
|
|
537
|
+
await fse.writeFile(marker, '{"installed":true,"saved":true}');
|
|
538
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
539
|
+
expect(rebuilt).toBe(2);
|
|
540
|
+
});
|
|
541
|
+
it('rebuilds local inputs changed while disabled before the running build finishes', async () => {
|
|
542
|
+
const helper = join(docsRoot, 'snippets/label.js');
|
|
543
|
+
const before = 'export default "before";\n';
|
|
544
|
+
const after = 'export default "after";\n';
|
|
545
|
+
await fse.outputFile(helper, before);
|
|
546
|
+
await fse.outputFile(join(docsRoot, 'snippets/card.jsx'), 'import { LineChart } from "recharts";\nimport label from "./label.js";\nexport const Card = () => <LineChart label={label} />;\n');
|
|
547
|
+
await fse.outputFile(join(docsRoot, 'charts.mdx'), 'import { Card } from "/snippets/card.jsx";\n\n<Card />\n');
|
|
548
|
+
const build = fakeBuild(mode);
|
|
549
|
+
let reached = () => { };
|
|
550
|
+
await dev.close();
|
|
551
|
+
dev = create({
|
|
552
|
+
build: async (input) => {
|
|
553
|
+
const content = await fse.readFile(helper, 'utf8');
|
|
554
|
+
reached();
|
|
555
|
+
const result = await build(input);
|
|
556
|
+
for (const island of Object.values(result.manifest.islands))
|
|
557
|
+
island.html = content;
|
|
558
|
+
return result;
|
|
559
|
+
},
|
|
560
|
+
});
|
|
561
|
+
await dev.init();
|
|
562
|
+
expect(Object.values((await props()).islands).map((island) => island.html)).toEqual([before]);
|
|
563
|
+
let release = () => { };
|
|
564
|
+
mode.building = new Promise((resolve) => {
|
|
565
|
+
release = resolve;
|
|
566
|
+
});
|
|
567
|
+
const started = new Promise((resolve) => {
|
|
568
|
+
reached = resolve;
|
|
569
|
+
});
|
|
570
|
+
try {
|
|
571
|
+
await dev.onFileChange('package.json', getFileCategory('package.json'));
|
|
572
|
+
await started;
|
|
573
|
+
await fse.outputJson(join(docsRoot, 'docs.json'), { name: 'Docs' });
|
|
574
|
+
await dev.onFileChange('docs.json', 'docsConfig');
|
|
575
|
+
await fse.writeFile(helper, after);
|
|
576
|
+
await dev.onFileChange('snippets/label.js', 'js');
|
|
577
|
+
await fse.outputJson(join(docsRoot, 'docs.json'), { name: 'Docs', packages: {} });
|
|
578
|
+
await dev.onFileChange('docs.json', 'docsConfig');
|
|
579
|
+
}
|
|
580
|
+
finally {
|
|
581
|
+
mode.building = undefined;
|
|
582
|
+
release();
|
|
583
|
+
}
|
|
584
|
+
await until(async () => Object.values((await props()).islands).some((island) => island.html === after));
|
|
585
|
+
expect(Object.values((await props()).islands).map((island) => island.html)).toEqual([after]);
|
|
586
|
+
expect(report.calls.filter((call) => call === 'building')).toHaveLength(3);
|
|
587
|
+
expect(report.calls.filter((call) => call === 'built')).toHaveLength(2);
|
|
588
|
+
expect(dev.getStatus()).toEqual({ state: 'ready' });
|
|
589
|
+
});
|
|
590
|
+
describe('after a build fails', () => {
|
|
591
|
+
const failOnce = () => {
|
|
592
|
+
const builds = fakeBuild(mode);
|
|
593
|
+
return create({
|
|
594
|
+
build: async (input) => {
|
|
595
|
+
try {
|
|
596
|
+
return await builds(input);
|
|
597
|
+
}
|
|
598
|
+
finally {
|
|
599
|
+
mode.failing = false;
|
|
600
|
+
mode.throwing = false;
|
|
601
|
+
}
|
|
602
|
+
},
|
|
603
|
+
});
|
|
604
|
+
};
|
|
605
|
+
const saveManifest = async () => {
|
|
606
|
+
await fse.outputJson(join(docsRoot, 'package.json'), {
|
|
607
|
+
dependencies: { recharts: '2.15.4', '@acme/ui': '1.0.0' },
|
|
608
|
+
});
|
|
609
|
+
await dev.onFileChange('package.json', getFileCategory('package.json'));
|
|
610
|
+
};
|
|
611
|
+
const marker = () => join(docsRoot, 'node_modules/.package-lock.json');
|
|
612
|
+
beforeEach(async () => {
|
|
613
|
+
await fse.outputJson(join(docsRoot, 'package-lock.json'), {
|
|
614
|
+
lockfileVersion: 3,
|
|
615
|
+
packages: {},
|
|
616
|
+
});
|
|
617
|
+
await fse.outputFile(marker(), '{}');
|
|
618
|
+
await dev.close();
|
|
619
|
+
dev = failOnce();
|
|
620
|
+
await dev.init();
|
|
621
|
+
expect(dev.getStatus()).toEqual({ state: 'ready' });
|
|
622
|
+
});
|
|
623
|
+
it.each([
|
|
624
|
+
['a bundle error', { failing: true, failCode: 'bundle-error' }],
|
|
625
|
+
['a missing react', { failing: true, failCode: 'react-missing' }],
|
|
626
|
+
['a thrown error', { throwing: true }],
|
|
627
|
+
])('retries a build that failed with %s only once the install settles', async (_, failure) => {
|
|
628
|
+
const builds = () => report.calls.filter((call) => call === 'building').length;
|
|
629
|
+
Object.assign(mode, failure);
|
|
630
|
+
report.calls.length = 0;
|
|
631
|
+
await saveManifest();
|
|
632
|
+
await until(() => dev.getStatus().state === 'failed');
|
|
633
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
634
|
+
expect(dev.getStatus().state).toBe('failed');
|
|
635
|
+
expect(builds()).toBe(1);
|
|
636
|
+
await fse.outputFile(marker(), '{"packages":{}}');
|
|
637
|
+
await until(() => dev.getStatus().state === 'ready');
|
|
638
|
+
expect(builds()).toBe(2);
|
|
639
|
+
});
|
|
640
|
+
it('retries a failed build when the install settles while it runs', async () => {
|
|
641
|
+
let release = () => { };
|
|
642
|
+
mode.building = new Promise((resolve) => {
|
|
643
|
+
release = resolve;
|
|
644
|
+
});
|
|
645
|
+
mode.failing = true;
|
|
646
|
+
report.calls.length = 0;
|
|
647
|
+
await saveManifest();
|
|
648
|
+
await until(() => report.calls.includes('building'));
|
|
649
|
+
await fse.outputFile(marker(), '{"packages":{}}');
|
|
650
|
+
mode.building = undefined;
|
|
651
|
+
release();
|
|
652
|
+
await until(() => dev.getStatus().state === 'ready');
|
|
653
|
+
expect(report.calls).toEqual([
|
|
654
|
+
'building',
|
|
655
|
+
'failed Could not resolve "recharts".',
|
|
656
|
+
'building',
|
|
657
|
+
'built',
|
|
658
|
+
]);
|
|
659
|
+
});
|
|
660
|
+
it('retries on the next page save even when its islands did not change', async () => {
|
|
661
|
+
mode.failing = true;
|
|
662
|
+
await saveManifest();
|
|
663
|
+
await until(() => dev.getStatus().state === 'failed');
|
|
664
|
+
rebuilt = 0;
|
|
665
|
+
await fse.outputFile(join(docsRoot, 'charts.mdx'), `${page(2)}\nMore prose.\n`);
|
|
666
|
+
await dev.onFileChange('charts.mdx', 'page');
|
|
667
|
+
await until(() => rebuilt === 1);
|
|
668
|
+
expect(dev.getStatus()).toEqual({ state: 'ready' });
|
|
669
|
+
});
|
|
670
|
+
});
|
|
671
|
+
describe('when adopting a finished build fails', () => {
|
|
672
|
+
const settle = () => new Promise((resolve) => setTimeout(resolve, 50));
|
|
673
|
+
const failOnce = (name, fails, message, before) => {
|
|
674
|
+
const original = fse[name].bind(fse);
|
|
675
|
+
let thrown = false;
|
|
676
|
+
return vi.spyOn(fse, name).mockImplementation((async (...args) => {
|
|
677
|
+
if (!thrown && fails(String(args[0]), args[1], before)) {
|
|
678
|
+
thrown = true;
|
|
679
|
+
throw new Error(message);
|
|
680
|
+
}
|
|
681
|
+
return original(...args);
|
|
682
|
+
}));
|
|
683
|
+
};
|
|
684
|
+
it.each([
|
|
685
|
+
['moving it into place', 'EXDEV: cross-device link not permitted', 'move', () => true],
|
|
686
|
+
[
|
|
687
|
+
'pruning old builds',
|
|
688
|
+
'EBUSY: resource busy or locked',
|
|
689
|
+
'remove',
|
|
690
|
+
(target) => target === join(buildsDir, 'stale'),
|
|
691
|
+
],
|
|
692
|
+
[
|
|
693
|
+
'writing package-build.json',
|
|
694
|
+
'ENOSPC: no space left on device',
|
|
695
|
+
'outputFile',
|
|
696
|
+
(target, data, before) => target.includes(PACKAGE_BUILD_PROPS_FILE) && !String(data).includes(before.baseUrl),
|
|
697
|
+
],
|
|
698
|
+
])('keeps the last build when %s fails, cleans up and retries on the next save', async (_, message, name, fails) => {
|
|
699
|
+
await dev.init();
|
|
700
|
+
const before = await props();
|
|
701
|
+
const builtId = before.baseUrl.split('/').pop();
|
|
702
|
+
await fse.ensureDir(join(buildsDir, 'stale'));
|
|
703
|
+
const spy = failOnce(name, fails, message, before);
|
|
704
|
+
try {
|
|
705
|
+
await dev.onFileChange('package.json', getFileCategory('package.json'));
|
|
706
|
+
await until(() => dev.getStatus().state === 'failed');
|
|
707
|
+
await settle();
|
|
708
|
+
}
|
|
709
|
+
finally {
|
|
710
|
+
spy.mockRestore();
|
|
711
|
+
}
|
|
712
|
+
expect(dev.getStatus()).toEqual({ state: 'failed', message });
|
|
713
|
+
expect(report.calls.filter((call) => call.startsWith('failed'))).toEqual([
|
|
714
|
+
`failed ${message}`,
|
|
715
|
+
]);
|
|
716
|
+
expect(await props()).toEqual(before);
|
|
717
|
+
expect((await fse.readdir(buildsDir)).filter((entry) => entry !== 'stale')).toEqual([
|
|
718
|
+
builtId,
|
|
719
|
+
]);
|
|
720
|
+
await fse.outputFile(join(docsRoot, 'charts.mdx'), `${page(2)}\nMore prose.\n`);
|
|
721
|
+
await dev.onFileChange('charts.mdx', 'page');
|
|
722
|
+
await until(() => dev.getStatus().state === 'ready');
|
|
723
|
+
const after = await props();
|
|
724
|
+
expect(after.baseUrl).not.toBe(before.baseUrl);
|
|
725
|
+
expect((await fse.readdir(buildsDir)).sort()).toEqual([builtId, after.baseUrl.split('/').pop()].sort());
|
|
726
|
+
});
|
|
727
|
+
});
|
|
728
|
+
it('stays off without a packages key and turns on when docs.json adds one', async () => {
|
|
729
|
+
const mtime = async (file) => (await fse.stat(join(propsPath, file))).mtimeMs;
|
|
730
|
+
await fse.outputJson(join(docsRoot, 'docs.json'), { name: 'Docs' });
|
|
731
|
+
await dev.init();
|
|
732
|
+
expect(dev.enabled).toBe(false);
|
|
733
|
+
expect(dev.routeState().enabled).toBe(false);
|
|
734
|
+
expect(getPackageImportOptions()).toBeUndefined();
|
|
735
|
+
expect(await fse.pathExists(join(propsPath, PACKAGE_BUILD_PROPS_FILE))).toBe(false);
|
|
736
|
+
expect(await fse.pathExists(buildsDir)).toBe(false);
|
|
737
|
+
await dev.onFileChange('charts.mdx', 'page');
|
|
738
|
+
expect(rebuilt).toBe(0);
|
|
739
|
+
await fse.outputJson(join(docsRoot, 'docs.json'), { name: 'Docs', packages: {} });
|
|
740
|
+
await fse.outputFile(join(propsPath, 'charts.mdx'), 'compiled without packages');
|
|
741
|
+
await fse.outputFile(join(propsPath, 'generated.mdx'), 'generated page');
|
|
742
|
+
await dev.onFileChange('docs.json', 'docsConfig');
|
|
743
|
+
expect(await mtime('charts.mdx')).toBe(0);
|
|
744
|
+
expect(await mtime('generated.mdx')).toBeGreaterThan(0);
|
|
745
|
+
await until(() => rebuilt === 1);
|
|
746
|
+
expect(dev.enabled).toBe(true);
|
|
747
|
+
expect((await props()).baseUrl).toMatch(/^\/_mintlify\/pkg\/b/);
|
|
748
|
+
await fse.outputFile(join(propsPath, 'charts.mdx'), 'stale compile');
|
|
749
|
+
await fse.outputJson(join(docsRoot, 'package.json'), {
|
|
750
|
+
dependencies: { recharts: '2.15.4', '@acme/ui': '1.0.0' },
|
|
751
|
+
});
|
|
752
|
+
await dev.onFileChange('package.json', getFileCategory('package.json'));
|
|
753
|
+
expect(await mtime('charts.mdx')).toBe(0);
|
|
754
|
+
expect(await mtime('generated.mdx')).toBeGreaterThan(0);
|
|
755
|
+
await until(() => rebuilt === 2);
|
|
756
|
+
});
|
|
757
|
+
describe('an invalid packages value', () => {
|
|
758
|
+
const settle = () => new Promise((resolve) => setTimeout(resolve, 50));
|
|
759
|
+
it('starts with packages off, shows the schema errors and turns on once it is valid', async () => {
|
|
760
|
+
await fse.outputJson(join(docsRoot, 'docs.json'), { name: 'Docs', packages: 'nope' });
|
|
761
|
+
await fse.outputJson(join(propsPath, PACKAGE_BUILD_PROPS_FILE), { baseUrl: '/stale' });
|
|
762
|
+
await dev.init();
|
|
763
|
+
expect(dev.enabled).toBe(false);
|
|
764
|
+
expect(dev.getStatus()).toEqual({ state: 'disabled' });
|
|
765
|
+
expect(getPackageImportOptions()).toBeUndefined();
|
|
766
|
+
expect(await fse.pathExists(join(propsPath, PACKAGE_BUILD_PROPS_FILE))).toBe(false);
|
|
767
|
+
expect(await fse.pathExists(buildsDir)).toBe(false);
|
|
768
|
+
expect(rebuilt).toBe(0);
|
|
769
|
+
expect(report.calls).toEqual([
|
|
770
|
+
'configError docs.json packages: Expected object, received string. Packages stay off until docs.json is fixed.',
|
|
771
|
+
]);
|
|
772
|
+
report.calls.length = 0;
|
|
773
|
+
await fse.outputJson(join(docsRoot, 'docs.json'), {
|
|
774
|
+
name: 'Docs',
|
|
775
|
+
packages: { provider: '../provider.jsx', isolation: 'none' },
|
|
776
|
+
});
|
|
777
|
+
await dev.onFileChange('docs.json', 'docsConfig');
|
|
778
|
+
await settle();
|
|
779
|
+
expect(dev.enabled).toBe(false);
|
|
780
|
+
expect(rebuilt).toBe(0);
|
|
781
|
+
expect(report.calls).toEqual([
|
|
782
|
+
'configError docs.json packages.provider: Provider path cannot contain `..` segments',
|
|
783
|
+
"configError docs.json packages.isolation: Invalid enum value. Expected 'page' | 'frame', received 'none'. Packages stay off until docs.json is fixed.",
|
|
784
|
+
]);
|
|
785
|
+
report.calls.length = 0;
|
|
786
|
+
await fse.outputJson(join(docsRoot, 'docs.json'), { name: 'Docs', packages: {} });
|
|
787
|
+
await dev.onFileChange('docs.json', 'docsConfig');
|
|
788
|
+
await until(() => rebuilt === 1);
|
|
789
|
+
expect(dev.enabled).toBe(true);
|
|
790
|
+
expect(dev.getStatus()).toEqual({ state: 'ready' });
|
|
791
|
+
expect((await props()).baseUrl).toMatch(/^\/_mintlify\/pkg\/b/);
|
|
792
|
+
expect(report.calls).toEqual(['building', 'built']);
|
|
793
|
+
});
|
|
794
|
+
it('keeps the last packages setup and shows the schema errors until it is valid again', async () => {
|
|
795
|
+
const regenerateSnippets = vi.fn(async () => { });
|
|
796
|
+
await dev.close();
|
|
797
|
+
dev = create({ regenerateSnippets });
|
|
798
|
+
await dev.init();
|
|
799
|
+
const built = await props();
|
|
800
|
+
const importOptions = getPackageImportOptions();
|
|
801
|
+
await fse.outputFile(join(propsPath, 'charts.mdx'), 'compiled with packages');
|
|
802
|
+
rebuilt = 0;
|
|
803
|
+
report.calls.length = 0;
|
|
804
|
+
await fse.outputJson(join(docsRoot, 'docs.json'), {
|
|
805
|
+
name: 'Docs',
|
|
806
|
+
packages: { css: ['./styles.css'] },
|
|
807
|
+
});
|
|
808
|
+
await fse.outputJson(join(docsRoot, 'package.json'), {
|
|
809
|
+
dependencies: { recharts: '2.15.4', '@acme/ui': '1.0.0' },
|
|
810
|
+
});
|
|
811
|
+
await dev.onFileChange('docs.json', 'docsConfig');
|
|
812
|
+
await settle();
|
|
813
|
+
expect(dev.enabled).toBe(true);
|
|
814
|
+
expect(dev.getStatus()).toEqual({ state: 'ready' });
|
|
815
|
+
expect(getPackageImportOptions()).toBe(importOptions);
|
|
816
|
+
expect(await props()).toEqual(built);
|
|
817
|
+
expect(await fse.pathExists(join(propsPath, 'charts.mdx'))).toBe(true);
|
|
818
|
+
expect(regenerateSnippets).not.toHaveBeenCalled();
|
|
819
|
+
expect(rebuilt).toBe(0);
|
|
820
|
+
expect(report.calls).toEqual([
|
|
821
|
+
'configError docs.json packages.css.0: CSS entries must be paths inside an installed package, such as `@acme/ui/dist/styles.css`. Keeping the last package setup until docs.json is fixed.',
|
|
822
|
+
]);
|
|
823
|
+
await fse.outputFile(join(docsRoot, 'charts.mdx'), page(3));
|
|
824
|
+
await dev.onFileChange('charts.mdx', 'page');
|
|
825
|
+
await until(() => rebuilt === 1);
|
|
826
|
+
expect((await props()).packageNames).toEqual(['recharts']);
|
|
827
|
+
await fse.outputJson(join(docsRoot, 'docs.json'), { name: 'Docs', packages: {} });
|
|
828
|
+
await dev.onFileChange('docs.json', 'docsConfig');
|
|
829
|
+
expect(regenerateSnippets).toHaveBeenCalledTimes(1);
|
|
830
|
+
await until(() => rebuilt === 2);
|
|
831
|
+
expect((await props()).packageNames).toEqual(['@acme/ui', 'recharts']);
|
|
832
|
+
});
|
|
833
|
+
});
|
|
834
|
+
it('collects with the new docs.json variables while keeping the last packages setup', async () => {
|
|
835
|
+
const chart = `import { LineChart, Line } from "recharts";
|
|
836
|
+
|
|
837
|
+
<LineChart width={400} height={200} data={[{ v: 1, w: 2 }]}>
|
|
838
|
+
<Line dataKey="{{key}}" />
|
|
839
|
+
</LineChart>
|
|
840
|
+
`;
|
|
841
|
+
await fse.outputJson(join(docsRoot, 'docs.json'), {
|
|
842
|
+
name: 'Docs',
|
|
843
|
+
packages: {},
|
|
844
|
+
variables: { key: 'v' },
|
|
845
|
+
});
|
|
846
|
+
await fse.outputFile(join(docsRoot, 'charts.mdx'), chart);
|
|
847
|
+
await dev.init();
|
|
848
|
+
rebuilt = 0;
|
|
849
|
+
report.calls.length = 0;
|
|
850
|
+
await fse.outputJson(join(docsRoot, 'docs.json'), {
|
|
851
|
+
name: 'Docs',
|
|
852
|
+
packages: { css: ['./styles.css'] },
|
|
853
|
+
variables: { key: 'w' },
|
|
854
|
+
});
|
|
855
|
+
await dev.onFileChange('docs.json', 'docsConfig');
|
|
856
|
+
await until(() => rebuilt === 1);
|
|
857
|
+
const expected = await collectPackageIslands({
|
|
858
|
+
docsRoot,
|
|
859
|
+
packageNames: new Set(['recharts']),
|
|
860
|
+
variables: { key: 'w' },
|
|
861
|
+
});
|
|
862
|
+
const [id] = expected.islands.map((island) => island.id);
|
|
863
|
+
expect(id).toBeDefined();
|
|
864
|
+
expect(Object.keys((await props()).islands)).toContain(id);
|
|
865
|
+
expect(report.calls[0]).toMatch(/Keeping the last package setup until docs\.json is fixed\.$/);
|
|
866
|
+
});
|
|
867
|
+
it('rebuilds when a root package manifest changes, going by its path', async () => {
|
|
868
|
+
await dev.init();
|
|
869
|
+
for (const file of ['package.json', 'package-lock.json', 'pnpm-lock.yaml']) {
|
|
870
|
+
rebuilt = 0;
|
|
871
|
+
await dev.onFileChange(file, getFileCategory(file));
|
|
872
|
+
await until(() => rebuilt === 1);
|
|
873
|
+
}
|
|
874
|
+
rebuilt = 0;
|
|
875
|
+
await dev.onFileChange('snippets/package.json', getFileCategory('snippets/package.json'));
|
|
876
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
877
|
+
expect(rebuilt).toBe(0);
|
|
878
|
+
});
|
|
879
|
+
describe('a docs.json that does not parse', () => {
|
|
880
|
+
const INVALID = '{ "name": "Docs", "packages": {}, }';
|
|
881
|
+
const parserMessage = (() => {
|
|
882
|
+
try {
|
|
883
|
+
JSON.parse(INVALID);
|
|
884
|
+
return '';
|
|
885
|
+
}
|
|
886
|
+
catch (error) {
|
|
887
|
+
return error.message;
|
|
888
|
+
}
|
|
889
|
+
})();
|
|
890
|
+
const settle = () => new Promise((resolve) => setTimeout(resolve, 50));
|
|
891
|
+
it('keeps the packages setup until it parses again', async () => {
|
|
892
|
+
const regenerateSnippets = vi.fn(async () => { });
|
|
893
|
+
await dev.close();
|
|
894
|
+
dev = create({ regenerateSnippets });
|
|
895
|
+
await dev.init();
|
|
896
|
+
const built = await props();
|
|
897
|
+
const importOptions = getPackageImportOptions();
|
|
898
|
+
await fse.outputFile(join(propsPath, 'charts.mdx'), 'compiled with packages');
|
|
899
|
+
rebuilt = 0;
|
|
900
|
+
report.calls.length = 0;
|
|
901
|
+
await fse.outputFile(join(docsRoot, 'docs.json'), INVALID);
|
|
902
|
+
await dev.onFileChange('docs.json', 'docsConfig');
|
|
903
|
+
await settle();
|
|
904
|
+
expect(dev.enabled).toBe(true);
|
|
905
|
+
expect(dev.routeState().enabled).toBe(true);
|
|
906
|
+
expect(dev.getStatus()).toEqual({ state: 'ready' });
|
|
907
|
+
expect(getPackageImportOptions()).toBe(importOptions);
|
|
908
|
+
expect(await props()).toEqual(built);
|
|
909
|
+
expect(await fse.pathExists(join(propsPath, 'charts.mdx'))).toBe(true);
|
|
910
|
+
expect(regenerateSnippets).not.toHaveBeenCalled();
|
|
911
|
+
expect(rebuilt).toBe(0);
|
|
912
|
+
expect(report.calls).toEqual([
|
|
913
|
+
expect.stringContaining(`configError docs.json could not be loaded: ${parserMessage}`),
|
|
914
|
+
]);
|
|
915
|
+
await fse.outputFile(join(docsRoot, 'charts.mdx'), page(3));
|
|
916
|
+
await dev.onFileChange('charts.mdx', 'page');
|
|
917
|
+
await until(() => rebuilt === 1);
|
|
918
|
+
expect(dev.getStatus()).toEqual({ state: 'ready' });
|
|
919
|
+
await fse.outputJson(join(docsRoot, 'docs.json'), { name: 'Docs', packages: {} });
|
|
920
|
+
await dev.onFileChange('docs.json', 'docsConfig');
|
|
921
|
+
await settle();
|
|
922
|
+
expect(regenerateSnippets).not.toHaveBeenCalled();
|
|
923
|
+
expect(rebuilt).toBe(1);
|
|
924
|
+
await fse.outputJson(join(docsRoot, 'package.json'), {
|
|
925
|
+
dependencies: { recharts: '2.15.4', '@acme/ui': '1.0.0' },
|
|
926
|
+
});
|
|
927
|
+
await dev.onFileChange('package.json', getFileCategory('package.json'));
|
|
928
|
+
expect(regenerateSnippets).toHaveBeenCalledTimes(1);
|
|
929
|
+
await until(() => rebuilt === 2);
|
|
930
|
+
expect((await props()).packageNames).toEqual(['@acme/ui', 'recharts']);
|
|
931
|
+
});
|
|
932
|
+
it('stays quiet for a project without packages', async () => {
|
|
933
|
+
await fse.outputJson(join(docsRoot, 'docs.json'), { name: 'Docs' });
|
|
934
|
+
await dev.init();
|
|
935
|
+
report.calls.length = 0;
|
|
936
|
+
for (const raw of ['', '{ "name": "Do', '\uFEFF{ "name": "Docs" }', '// c\n{}']) {
|
|
937
|
+
await fse.outputFile(join(docsRoot, 'docs.json'), raw);
|
|
938
|
+
await dev.onFileChange('docs.json', 'docsConfig');
|
|
939
|
+
}
|
|
940
|
+
await fse.remove(join(docsRoot, 'docs.json'));
|
|
941
|
+
await dev.onFileChange('docs.json', 'docsConfig');
|
|
942
|
+
await settle();
|
|
943
|
+
expect(dev.enabled).toBe(false);
|
|
944
|
+
expect(report.calls).toEqual([]);
|
|
945
|
+
expect(rebuilt).toBe(0);
|
|
946
|
+
});
|
|
947
|
+
it('keeps the setup through empty, truncated, BOM and commented saves, then turns off when packages goes away', async () => {
|
|
948
|
+
const regenerateSnippets = vi.fn(async () => { });
|
|
949
|
+
await dev.close();
|
|
950
|
+
dev = create({ regenerateSnippets });
|
|
951
|
+
await dev.init();
|
|
952
|
+
const built = await props();
|
|
953
|
+
rebuilt = 0;
|
|
954
|
+
report.calls.length = 0;
|
|
955
|
+
const saves = ['', '{ "name": "Do', '\uFEFF{ "name": "Docs", "packages": {} }', '// c\n{}'];
|
|
956
|
+
for (const raw of saves) {
|
|
957
|
+
await fse.outputFile(join(docsRoot, 'docs.json'), raw);
|
|
958
|
+
await dev.onFileChange('docs.json', 'docsConfig');
|
|
959
|
+
await fse.outputJson(join(docsRoot, 'docs.json'), { name: 'Docs', packages: {} });
|
|
960
|
+
await dev.onFileChange('docs.json', 'docsConfig');
|
|
961
|
+
}
|
|
962
|
+
await settle();
|
|
963
|
+
expect(dev.enabled).toBe(true);
|
|
964
|
+
expect(await props()).toEqual(built);
|
|
965
|
+
expect(regenerateSnippets).not.toHaveBeenCalled();
|
|
966
|
+
expect(rebuilt).toBe(0);
|
|
967
|
+
expect(report.calls).toHaveLength(saves.length);
|
|
968
|
+
expect(report.calls.every((call) => call.startsWith('configError docs.json'))).toBe(true);
|
|
969
|
+
await fse.outputJson(join(docsRoot, 'docs.json'), { name: 'Docs' });
|
|
970
|
+
await dev.onFileChange('docs.json', 'docsConfig');
|
|
971
|
+
expect(dev.enabled).toBe(false);
|
|
972
|
+
expect(regenerateSnippets).toHaveBeenCalledTimes(1);
|
|
973
|
+
expect(await fse.pathExists(join(propsPath, PACKAGE_BUILD_PROPS_FILE))).toBe(false);
|
|
974
|
+
});
|
|
975
|
+
it('turns packages off when docs.json is deleted', async () => {
|
|
976
|
+
await dev.init();
|
|
977
|
+
await fse.remove(join(docsRoot, 'docs.json'));
|
|
978
|
+
await dev.onFileChange('docs.json', 'docsConfig');
|
|
979
|
+
expect(dev.enabled).toBe(false);
|
|
980
|
+
expect(getPackageImportOptions()).toBeUndefined();
|
|
981
|
+
expect(await fse.pathExists(join(propsPath, PACKAGE_BUILD_PROPS_FILE))).toBe(false);
|
|
982
|
+
});
|
|
983
|
+
it('starts with packages off and turns them on once it parses', async () => {
|
|
984
|
+
await fse.outputFile(join(docsRoot, 'docs.json'), INVALID);
|
|
985
|
+
await fse.outputJson(join(propsPath, PACKAGE_BUILD_PROPS_FILE), { baseUrl: '/stale' });
|
|
986
|
+
await dev.init();
|
|
987
|
+
expect(dev.enabled).toBe(false);
|
|
988
|
+
expect(dev.getStatus()).toEqual({ state: 'disabled' });
|
|
989
|
+
expect(getPackageImportOptions()).toBeUndefined();
|
|
990
|
+
expect(await fse.pathExists(join(propsPath, PACKAGE_BUILD_PROPS_FILE))).toBe(false);
|
|
991
|
+
expect(await fse.pathExists(buildsDir)).toBe(false);
|
|
992
|
+
expect(report.calls).toEqual([]);
|
|
993
|
+
await fse.outputJson(join(docsRoot, 'docs.json'), { name: 'Docs', packages: {} });
|
|
994
|
+
await dev.onFileChange('docs.json', 'docsConfig');
|
|
995
|
+
await until(() => rebuilt === 1);
|
|
996
|
+
expect(dev.enabled).toBe(true);
|
|
997
|
+
expect(dev.getStatus()).toEqual({ state: 'ready' });
|
|
998
|
+
expect((await props()).baseUrl).toMatch(/^\/_mintlify\/pkg\/b/);
|
|
999
|
+
});
|
|
1000
|
+
});
|
|
1001
|
+
it('keeps the packages setup while package.json does not parse, and drops the names once it is deleted', async () => {
|
|
1002
|
+
const regenerateSnippets = vi.fn(async () => { });
|
|
1003
|
+
await dev.close();
|
|
1004
|
+
dev = create({ regenerateSnippets });
|
|
1005
|
+
await dev.init();
|
|
1006
|
+
const built = await props();
|
|
1007
|
+
const importOptions = getPackageImportOptions();
|
|
1008
|
+
await fse.outputFile(join(propsPath, 'charts.mdx'), 'compiled with packages');
|
|
1009
|
+
rebuilt = 0;
|
|
1010
|
+
report.calls.length = 0;
|
|
1011
|
+
const saves = ['', '{ "dependencies": { "recharts": ', '["recharts"]', 'null'];
|
|
1012
|
+
for (const raw of saves) {
|
|
1013
|
+
await fse.outputFile(join(docsRoot, 'package.json'), raw);
|
|
1014
|
+
await dev.onFileChange('package.json', getFileCategory('package.json'));
|
|
1015
|
+
}
|
|
1016
|
+
await fse.outputJson(join(docsRoot, 'docs.json'), { name: 'Docs', packages: {} });
|
|
1017
|
+
await dev.onFileChange('docs.json', 'docsConfig');
|
|
1018
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
1019
|
+
expect(dev.enabled).toBe(true);
|
|
1020
|
+
expect(dev.getStatus()).toEqual({ state: 'ready' });
|
|
1021
|
+
expect(getPackageImportOptions()).toBe(importOptions);
|
|
1022
|
+
expect(await props()).toEqual(built);
|
|
1023
|
+
expect(await fse.pathExists(join(propsPath, 'charts.mdx'))).toBe(true);
|
|
1024
|
+
expect(regenerateSnippets).not.toHaveBeenCalled();
|
|
1025
|
+
expect(rebuilt).toBe(0);
|
|
1026
|
+
const keeping = '. Keeping the last package setup until package.json is fixed.';
|
|
1027
|
+
expect(report.calls).toEqual([
|
|
1028
|
+
`configError package.json is not valid JSON: Unexpected end of JSON input${keeping}`,
|
|
1029
|
+
`configError package.json is not valid JSON: Unexpected end of JSON input${keeping}`,
|
|
1030
|
+
`configError package.json is not a JSON object${keeping}`,
|
|
1031
|
+
`configError package.json is not a JSON object${keeping}`,
|
|
1032
|
+
`configError package.json is not a JSON object${keeping}`,
|
|
1033
|
+
]);
|
|
1034
|
+
await fse.outputJson(join(docsRoot, 'package.json'), { dependencies: { recharts: '2.15.4' } });
|
|
1035
|
+
await dev.onFileChange('package.json', getFileCategory('package.json'));
|
|
1036
|
+
await until(() => rebuilt === 1);
|
|
1037
|
+
expect(regenerateSnippets).not.toHaveBeenCalled();
|
|
1038
|
+
expect((await props()).packageNames).toEqual(['recharts']);
|
|
1039
|
+
await fse.remove(join(docsRoot, 'package.json'));
|
|
1040
|
+
await dev.onFileChange('package.json', getFileCategory('package.json'));
|
|
1041
|
+
expect(regenerateSnippets).toHaveBeenCalledTimes(1);
|
|
1042
|
+
expect(getPackageImportOptions()?.packageNames).toEqual(new Set());
|
|
1043
|
+
expect((await props()).packageNames).toEqual([]);
|
|
1044
|
+
});
|
|
1045
|
+
describe('files the package build reads', () => {
|
|
1046
|
+
const CARD = `import { LineChart } from "recharts";
|
|
1047
|
+
import "./card.css";
|
|
1048
|
+
|
|
1049
|
+
export const Card = () => <LineChart width={400} height={200} data={[{ v: 1 }]} />;
|
|
1050
|
+
`;
|
|
1051
|
+
const settle = () => new Promise((resolve) => setTimeout(resolve, 50));
|
|
1052
|
+
const duringNextBuild = async (start, during) => {
|
|
1053
|
+
let release = () => { };
|
|
1054
|
+
mode.building = new Promise((resolve) => {
|
|
1055
|
+
release = resolve;
|
|
1056
|
+
});
|
|
1057
|
+
report.calls.length = 0;
|
|
1058
|
+
await start();
|
|
1059
|
+
await until(() => report.calls.includes('building'));
|
|
1060
|
+
mode.building = undefined;
|
|
1061
|
+
await during();
|
|
1062
|
+
release();
|
|
1063
|
+
};
|
|
1064
|
+
beforeEach(async () => {
|
|
1065
|
+
await fse.outputFile(join(docsRoot, 'snippets/card.jsx'), CARD);
|
|
1066
|
+
await fse.outputFile(join(docsRoot, 'cards.mdx'), 'import { Card } from "/snippets/card.jsx";\n\n<Card />\n');
|
|
1067
|
+
await fse.outputFile(join(docsRoot, 'style.css'), 'body { color: red; }\n');
|
|
1068
|
+
await fse.outputFile(join(docsRoot, 'snippets/unused.css'), '.unused { color: red; }\n');
|
|
1069
|
+
});
|
|
1070
|
+
it('rebuilds when CSS or an asset a package snippet pulls in changes, not for other CSS', async () => {
|
|
1071
|
+
await fse.outputFile(join(docsRoot, 'snippets/card.css'), '.card { background: url(./bg.png); }\n');
|
|
1072
|
+
await fse.outputFile(join(docsRoot, 'snippets/bg.png'), new Uint8Array([1, 2, 3]));
|
|
1073
|
+
await dev.init();
|
|
1074
|
+
rebuilt = 0;
|
|
1075
|
+
await fse.outputFile(join(docsRoot, 'style.css'), 'body { color: blue; }\n');
|
|
1076
|
+
await dev.onFileChange('style.css', 'css');
|
|
1077
|
+
await fse.outputFile(join(docsRoot, 'snippets/unused.css'), '.unused { color: blue; }\n');
|
|
1078
|
+
await dev.onFileChange('snippets/unused.css', 'css');
|
|
1079
|
+
await settle();
|
|
1080
|
+
expect(rebuilt).toBe(0);
|
|
1081
|
+
await fse.outputFile(join(docsRoot, 'snippets/card.css'), '.card { color: blue; background: url(./bg.png); }\n');
|
|
1082
|
+
await dev.onFileChange('snippets/card.css', 'css');
|
|
1083
|
+
await until(() => rebuilt === 1);
|
|
1084
|
+
await fse.outputFile(join(docsRoot, 'snippets/bg.png'), new Uint8Array([4, 5, 6]));
|
|
1085
|
+
await dev.onFileChange('snippets/bg.png', 'staticFile');
|
|
1086
|
+
await until(() => rebuilt === 2);
|
|
1087
|
+
});
|
|
1088
|
+
it('rebuilds once a missing file a package snippet imports is created, even outside snippets', async () => {
|
|
1089
|
+
await fse.outputFile(join(docsRoot, 'snippets/card.jsx'), CARD.replace('./card.css', '../styles/card.css'));
|
|
1090
|
+
mode.failing = true;
|
|
1091
|
+
await dev.init();
|
|
1092
|
+
expect(dev.getStatus().state).toBe('failed');
|
|
1093
|
+
rebuilt = 0;
|
|
1094
|
+
await dev.onFileChange('style.css', 'css');
|
|
1095
|
+
await dev.onFileChange('snippets/unused.css', 'css');
|
|
1096
|
+
await settle();
|
|
1097
|
+
expect(rebuilt).toBe(0);
|
|
1098
|
+
mode.failing = false;
|
|
1099
|
+
await fse.outputFile(join(docsRoot, 'styles/card.css'), '.card { color: blue; }\n');
|
|
1100
|
+
await dev.onFileChange('styles/card.css', 'css');
|
|
1101
|
+
await until(() => rebuilt === 1);
|
|
1102
|
+
expect(dev.getStatus()).toEqual({ state: 'ready' });
|
|
1103
|
+
});
|
|
1104
|
+
it('rebuilds when a file the running build reads for the first time changes mid-build', async () => {
|
|
1105
|
+
await fse.outputFile(join(docsRoot, 'styles/card.css'), '.card { color: red; }\n');
|
|
1106
|
+
await dev.init();
|
|
1107
|
+
rebuilt = 0;
|
|
1108
|
+
await duringNextBuild(async () => {
|
|
1109
|
+
await fse.outputFile(join(docsRoot, 'snippets/card.jsx'), CARD.replace('./card.css', '../styles/card.css'));
|
|
1110
|
+
await dev.onFileChange('snippets/card.jsx', 'snippet-v2');
|
|
1111
|
+
}, async () => {
|
|
1112
|
+
await fse.outputFile(join(docsRoot, 'styles/card.css'), '.card { color: blue; }\n');
|
|
1113
|
+
await dev.onFileChange('styles/card.css', 'css');
|
|
1114
|
+
});
|
|
1115
|
+
await until(() => rebuilt === 2);
|
|
1116
|
+
});
|
|
1117
|
+
it('does not rebuild again for an unrelated file that changes mid-build', async () => {
|
|
1118
|
+
await dev.init();
|
|
1119
|
+
rebuilt = 0;
|
|
1120
|
+
await duringNextBuild(() => dev.onFileChange('snippets/card.jsx', 'snippet-v2'), () => dev.onFileChange('snippets/unused.css', 'css'));
|
|
1121
|
+
await until(() => rebuilt >= 1);
|
|
1122
|
+
await settle();
|
|
1123
|
+
expect(rebuilt).toBe(1);
|
|
1124
|
+
});
|
|
1125
|
+
describe('after a build that threw', () => {
|
|
1126
|
+
const write = async ({ file, content, category }) => {
|
|
1127
|
+
await fse.outputFile(join(docsRoot, file), content);
|
|
1128
|
+
await dev.onFileChange(file, category);
|
|
1129
|
+
};
|
|
1130
|
+
const css = (content) => ({
|
|
1131
|
+
file: 'snippets/card.css',
|
|
1132
|
+
content,
|
|
1133
|
+
category: 'css',
|
|
1134
|
+
});
|
|
1135
|
+
beforeEach(async () => {
|
|
1136
|
+
await fse.outputFile(join(docsRoot, 'snippets/card.css'), '.card { color: red; }\n');
|
|
1137
|
+
});
|
|
1138
|
+
it.each([
|
|
1139
|
+
[
|
|
1140
|
+
'a stylesheet',
|
|
1141
|
+
{
|
|
1142
|
+
file: 'snippets/card.jsx',
|
|
1143
|
+
content: CARD.replace('./card.css', '../styles/new.css'),
|
|
1144
|
+
category: 'snippet-v2',
|
|
1145
|
+
},
|
|
1146
|
+
{ file: 'styles/new.css', content: '.card { color: blue; }\n', category: 'css' },
|
|
1147
|
+
],
|
|
1148
|
+
[
|
|
1149
|
+
'an image',
|
|
1150
|
+
css('.card { background: url(../images/hero.webp); }\n'),
|
|
1151
|
+
{ file: 'images/hero.webp', content: 'webp', category: 'staticFile' },
|
|
1152
|
+
],
|
|
1153
|
+
[
|
|
1154
|
+
'a font',
|
|
1155
|
+
css('@font-face { font-family: Inter; src: url(../fonts/inter.woff2); }\n'),
|
|
1156
|
+
{ file: 'fonts/inter.woff2', content: 'woff2', category: 'staticFile' },
|
|
1157
|
+
],
|
|
1158
|
+
[
|
|
1159
|
+
'an SVG',
|
|
1160
|
+
css('.card { mask: url(../images/icon.svg); }\n'),
|
|
1161
|
+
{ file: 'images/icon.svg', content: '<svg/>', category: 'staticFile' },
|
|
1162
|
+
],
|
|
1163
|
+
])('rebuilds for %s the throwing build was first to need', async (_, edit, created) => {
|
|
1164
|
+
await dev.init();
|
|
1165
|
+
rebuilt = 0;
|
|
1166
|
+
mode.throwing = true;
|
|
1167
|
+
await write(edit);
|
|
1168
|
+
await until(() => rebuilt === 1);
|
|
1169
|
+
expect(dev.getStatus().state).toBe('failed');
|
|
1170
|
+
mode.throwing = false;
|
|
1171
|
+
await write(created);
|
|
1172
|
+
await until(() => rebuilt === 2);
|
|
1173
|
+
expect(dev.getStatus()).toEqual({ state: 'ready' });
|
|
1174
|
+
});
|
|
1175
|
+
it('skips files no build can read, and goes back to the files read once a build reports', async () => {
|
|
1176
|
+
await fse.outputFile(join(docsRoot, 'images/logo.png'), 'png');
|
|
1177
|
+
await dev.init();
|
|
1178
|
+
rebuilt = 0;
|
|
1179
|
+
mode.throwing = true;
|
|
1180
|
+
await write(css('.card { color: blue; }\n'));
|
|
1181
|
+
await until(() => rebuilt === 1);
|
|
1182
|
+
mode.throwing = false;
|
|
1183
|
+
await write({ file: 'notes.txt', content: 'a', category: 'staticFile' });
|
|
1184
|
+
await write({ file: 'data/config.yaml', content: 'a: 1', category: 'staticFile' });
|
|
1185
|
+
await settle();
|
|
1186
|
+
expect(rebuilt).toBe(1);
|
|
1187
|
+
expect(dev.getStatus().state).toBe('failed');
|
|
1188
|
+
await write({ file: 'images/logo.png', content: 'png2', category: 'staticFile' });
|
|
1189
|
+
await until(() => rebuilt === 2);
|
|
1190
|
+
expect(dev.getStatus()).toEqual({ state: 'ready' });
|
|
1191
|
+
await write({ file: 'images/logo.png', content: 'png3', category: 'staticFile' });
|
|
1192
|
+
await write({ file: 'style.css', content: 'body { color: green; }\n', category: 'css' });
|
|
1193
|
+
await settle();
|
|
1194
|
+
expect(rebuilt).toBe(2);
|
|
1195
|
+
});
|
|
1196
|
+
it.each([
|
|
1197
|
+
['an asset', 'images/hero.webp', 2],
|
|
1198
|
+
['a file no build reads', 'notes.txt', 1],
|
|
1199
|
+
])('reruns a build that throws when %s changes mid-build', async (_, file, builds) => {
|
|
1200
|
+
await dev.init();
|
|
1201
|
+
rebuilt = 0;
|
|
1202
|
+
mode.throwing = true;
|
|
1203
|
+
await duringNextBuild(() => write(css('.card { color: blue; }\n')), () => write({ file, content: 'x', category: 'staticFile' }));
|
|
1204
|
+
await until(() => rebuilt >= 1);
|
|
1205
|
+
await settle();
|
|
1206
|
+
expect(rebuilt).toBe(builds);
|
|
1207
|
+
expect(dev.getStatus().state).toBe('failed');
|
|
1208
|
+
});
|
|
1209
|
+
it('rebuilds for a file the last reporting build read, whatever its type', async () => {
|
|
1210
|
+
await fse.outputFile(join(docsRoot, 'snippets/card.jsx'), `import note from "./note.txt";\n${CARD}`);
|
|
1211
|
+
await fse.outputFile(join(docsRoot, 'snippets/note.txt'), 'a\n');
|
|
1212
|
+
await dev.init();
|
|
1213
|
+
rebuilt = 0;
|
|
1214
|
+
mode.throwing = true;
|
|
1215
|
+
await write(css('.card { color: blue; }\n'));
|
|
1216
|
+
await until(() => rebuilt === 1);
|
|
1217
|
+
expect(dev.getStatus().state).toBe('failed');
|
|
1218
|
+
mode.throwing = false;
|
|
1219
|
+
await write({ file: 'snippets/note.txt', content: 'b\n', category: 'staticFile' });
|
|
1220
|
+
await until(() => rebuilt === 2);
|
|
1221
|
+
expect(dev.getStatus()).toEqual({ state: 'ready' });
|
|
1222
|
+
});
|
|
1223
|
+
});
|
|
1224
|
+
describe('scripts and JSON', () => {
|
|
1225
|
+
const editHelper = async () => {
|
|
1226
|
+
await fse.outputFile(join(docsRoot, 'snippets/format.js'), 'export const label = "b";\n');
|
|
1227
|
+
await dev.onFileChange('snippets/format.js', 'js');
|
|
1228
|
+
};
|
|
1229
|
+
const editUnread = async () => {
|
|
1230
|
+
await fse.outputJson(join(docsRoot, 'openapi.json'), { openapi: '3.1.0', paths: {} });
|
|
1231
|
+
await dev.onFileChange('openapi.json', 'potentialJsonOpenApiSpec');
|
|
1232
|
+
await fse.outputFile(join(docsRoot, 'scripts/x.ts'), 'export const x = 2;\n');
|
|
1233
|
+
await dev.onFileChange('scripts/x.ts', null);
|
|
1234
|
+
};
|
|
1235
|
+
beforeEach(async () => {
|
|
1236
|
+
await fse.outputFile(join(docsRoot, 'snippets/card.jsx'), CARD.replace('import "./card.css";', 'import { label } from "./format.js";'));
|
|
1237
|
+
await fse.outputFile(join(docsRoot, 'snippets/format.js'), 'export const label = "a";\n');
|
|
1238
|
+
await fse.outputJson(join(docsRoot, 'openapi.json'), { openapi: '3.1.0' });
|
|
1239
|
+
await fse.outputFile(join(docsRoot, 'scripts/x.ts'), 'export const x = 1;\n');
|
|
1240
|
+
});
|
|
1241
|
+
it.each([
|
|
1242
|
+
['ready', false],
|
|
1243
|
+
['failed', true],
|
|
1244
|
+
])('when %s, rebuilds for a helper a package snippet imports, not for an OpenAPI spec or script', async (state, failing) => {
|
|
1245
|
+
mode.failing = failing;
|
|
1246
|
+
await dev.init();
|
|
1247
|
+
expect(dev.getStatus().state).toBe(state);
|
|
1248
|
+
rebuilt = 0;
|
|
1249
|
+
await editUnread();
|
|
1250
|
+
await settle();
|
|
1251
|
+
expect(rebuilt).toBe(0);
|
|
1252
|
+
expect(dev.getStatus().state).toBe(state);
|
|
1253
|
+
await editHelper();
|
|
1254
|
+
await until(() => rebuilt === 1);
|
|
1255
|
+
});
|
|
1256
|
+
it('waits for the first build to report what it reads before rebuilding for a file changed mid-build', async () => {
|
|
1257
|
+
let starting = Promise.resolve();
|
|
1258
|
+
await duringNextBuild(async () => {
|
|
1259
|
+
starting = dev.init();
|
|
1260
|
+
}, editUnread);
|
|
1261
|
+
await starting;
|
|
1262
|
+
await settle();
|
|
1263
|
+
expect(rebuilt).toBe(1);
|
|
1264
|
+
});
|
|
1265
|
+
it('rebuilds for a helper edited while the first build runs', async () => {
|
|
1266
|
+
let starting = Promise.resolve();
|
|
1267
|
+
await duringNextBuild(async () => {
|
|
1268
|
+
starting = dev.init();
|
|
1269
|
+
}, editHelper);
|
|
1270
|
+
await starting;
|
|
1271
|
+
await until(() => rebuilt === 2);
|
|
1272
|
+
});
|
|
1273
|
+
it('rebuilds for a helper edited after a build that threw before reporting what it reads', async () => {
|
|
1274
|
+
mode.throwing = true;
|
|
1275
|
+
await dev.init();
|
|
1276
|
+
expect(dev.getStatus().state).toBe('failed');
|
|
1277
|
+
mode.throwing = false;
|
|
1278
|
+
rebuilt = 0;
|
|
1279
|
+
await editHelper();
|
|
1280
|
+
await until(() => rebuilt === 1);
|
|
1281
|
+
expect(dev.getStatus()).toEqual({ state: 'ready' });
|
|
1282
|
+
});
|
|
1283
|
+
it('rebuilds for a helper a snippet newly imports when the build after that edit threw', async () => {
|
|
1284
|
+
await dev.init();
|
|
1285
|
+
rebuilt = 0;
|
|
1286
|
+
mode.throwing = true;
|
|
1287
|
+
await fse.outputFile(join(docsRoot, 'snippets/card.jsx'), CARD.replace('import "./card.css";', 'import { label } from "../lib/label.js";'));
|
|
1288
|
+
await dev.onFileChange('snippets/card.jsx', 'snippet-v2');
|
|
1289
|
+
await until(() => rebuilt === 1);
|
|
1290
|
+
expect(dev.getStatus().state).toBe('failed');
|
|
1291
|
+
mode.throwing = false;
|
|
1292
|
+
await fse.outputFile(join(docsRoot, 'lib/label.js'), 'export const label = "a";\n');
|
|
1293
|
+
await dev.onFileChange('lib/label.js', 'js');
|
|
1294
|
+
await until(() => rebuilt === 2);
|
|
1295
|
+
expect(dev.getStatus()).toEqual({ state: 'ready' });
|
|
1296
|
+
});
|
|
1297
|
+
});
|
|
1298
|
+
});
|
|
1299
|
+
it('reports island extraction errors', async () => {
|
|
1300
|
+
await fse.outputFile(join(docsRoot, 'bad.mdx'), 'import { LineChart } from "recharts";\n\n<LineChart onClick={() => {}} />\n');
|
|
1301
|
+
await dev.init();
|
|
1302
|
+
expect(report.calls.find((call) => call.startsWith('islandErrors'))).toMatch(/bad\.mdx:3/);
|
|
1303
|
+
});
|
|
1304
|
+
});
|