Skip to content

Commit c319937

Browse files
authored
fix hmr of mdx and outlineInfo; support used together with @vitejs/plugin-react-swc (#146)
1 parent e8425d3 commit c319937

File tree

15 files changed

+374
-90
lines changed

15 files changed

+374
-90
lines changed

packages/playground/use-theme-doc/__tests__/hmr.spec.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,57 @@ test('hmr: edit file (md static data notation)', async ({
7777
).toHaveCount(0)
7878
})
7979

80+
test('hmr: edit md file content', async ({ page, fsUtils, testPlayground }) => {
81+
// prepare locators first
82+
const headingBeforeEdit = page
83+
.locator('.markdown-body')
84+
.getByRole('heading', { name: 'Heading one', exact: true })
85+
const headingAfterEdit = page
86+
.locator('.markdown-body')
87+
.getByRole('heading', { name: 'Heading edited', exact: true })
88+
// Also check the table-of-content
89+
const outlineLinkBeforeEdit = page
90+
.locator('.vp-local-outline')
91+
.getByRole('link', { name: 'Heading one', exact: true })
92+
const outlineLinkAfterEdit = page
93+
.locator('.vp-local-outline')
94+
.getByRole('link', { name: 'Heading edited', exact: true })
95+
const counter = page.locator('.markdown-body').getByTestId('counter')
96+
const counterStateText = counter.locator('span')
97+
const counterButton = counter.getByRole('button', { name: 'add count' })
98+
99+
page.locator('.vp-local-sider >> text="Markdown Test Page1"').click()
100+
await page.waitForURL('/md-test1')
101+
102+
// initial state
103+
await expect(headingBeforeEdit).toHaveCount(1)
104+
await expect(headingAfterEdit).toHaveCount(0)
105+
await expect(outlineLinkBeforeEdit).toHaveCount(1)
106+
await expect(outlineLinkAfterEdit).toHaveCount(0)
107+
// update component state
108+
await expect(counterStateText).toHaveText('Counter component: 0.')
109+
await counterButton.click()
110+
await expect(counterStateText).toHaveText('Counter component: 1.')
111+
112+
fsUtils.editFile('pages/md-test1$.md', (str) => {
113+
return str.replace('# Heading one', '# Heading edited')
114+
})
115+
116+
await expect(headingBeforeEdit).toHaveCount(0)
117+
await expect(headingAfterEdit).toHaveCount(1)
118+
await expect(outlineLinkBeforeEdit).toHaveCount(0)
119+
await expect(outlineLinkAfterEdit).toHaveCount(1)
120+
await expect(counterStateText).toHaveText('Counter component: 1.')
121+
122+
await testPlayground.restore()
123+
124+
await expect(headingBeforeEdit).toHaveCount(1)
125+
await expect(headingAfterEdit).toHaveCount(0)
126+
await expect(outlineLinkBeforeEdit).toHaveCount(1)
127+
await expect(outlineLinkAfterEdit).toHaveCount(0)
128+
await expect(counterStateText).toHaveText('Counter component: 1.')
129+
})
130+
80131
test('hmr: delete file, add file', async ({ page, fsUtils }) => {
81132
const page2FileContent = fsUtils.readFile('pages/page2$.md')
82133
await expect(

packages/playground/use-theme-doc/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
"devDependencies": {
2424
"@types/react": "^18.2.13",
2525
"@vitejs/plugin-react": "^4.0.1",
26+
"@vitejs/plugin-react-swc": "^3.3.2",
2627
"rimraf": "^4.4.1",
2728
"serve": "^14.2.0",
2829
"vite": "^4.3.9",
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import React, { useState } from 'react'
2+
3+
interface Props {}
4+
5+
const Counter: React.FC<Props> = (props) => {
6+
const [count, setCount] = useState(0)
7+
return (
8+
<div data-testid="counter">
9+
<span>Counter component: {count}.</span>
10+
<button onClick={() => setCount((v) => v + 1)}>add count</button>
11+
</div>
12+
)
13+
}
14+
15+
export default Counter

packages/playground/use-theme-doc/pages/md-test1$.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@
22

33
> markdown render test data from: https://github.com/fullpipe/markdown-test-page/blob/master/test-page.md
44
5+
# Counter
6+
7+
import Counter from './Counter';
8+
9+
<Counter />
10+
511
# Heading one
612

713
Sint sit cillum pariatur eiusmod nulla pariatur ipsum. Sit laborum anim qui mollit tempor pariatur nisi minim dolor. Aliquip et adipisicing sit sit fugiat commodo id sunt. Nostrud enim ad commodo incididunt cupidatat in ullamco ullamco Lorem cupidatat velit enim et Lorem. Ut laborum cillum laboris fugiat culpa sint irure do reprehenderit culpa occaecat. Exercitation esse mollit tempor magna aliqua in occaecat aliquip veniam reprehenderit nisi dolor in laboris dolore velit.

packages/playground/use-theme-doc/vite.config.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { defineConfig } from 'vite'
22
import * as path from 'path'
3-
import react from '@vitejs/plugin-react'
3+
// import react from '@vitejs/plugin-react'
4+
import react from "@vitejs/plugin-react-swc";
5+
46
import pages from 'vite-plugin-react-pages'
57

68
export default defineConfig({

packages/react-pages/client.rollup.config.js

Lines changed: 53 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -9,46 +9,60 @@ bundle client modules to reduce browser request during dev
99
also prevent conflict npm packages with users (like jotai)
1010
*/
1111

12-
export default {
13-
input: [
14-
'src/client/entries/csr.tsx',
15-
'src/client/entries/ssg-client.tsx',
16-
'src/client/entries/ssg-server.tsx',
17-
],
18-
output: {
19-
dir: 'dist/client-bundles/entries',
20-
entryFileNames: `[name].mjs`,
21-
format: 'esm',
22-
sourcemap: true,
12+
export default [
13+
{
14+
input: [
15+
'src/client/entries/ssg-client.tsx',
16+
'src/client/entries/ssg-server.tsx',
17+
],
18+
...config(),
2319
},
24-
external: [],
25-
plugins: [
26-
{
27-
name: 'client-external',
28-
resolveId(source, importer) {
29-
if (source.startsWith('/@react-pages/')) {
30-
return {
31-
id: source,
32-
external: 'absolute',
20+
// 1. stand alone bundle for dev, without code spliting. To reduce request waterfall
21+
// 2. `hoistTransitiveImports` will add import (like `import '/@react-pages/pages'`) to the entry, which will mess up the hmr handling, so turn it off
22+
{
23+
input: ['src/client/entries/csr.tsx'],
24+
...config({ hoistTransitiveImports: false }),
25+
},
26+
]
27+
28+
function config({ hoistTransitiveImports } = {}) {
29+
return {
30+
output: {
31+
dir: 'dist/client-bundles/entries',
32+
entryFileNames: `[name].mjs`,
33+
format: 'esm',
34+
sourcemap: true,
35+
hoistTransitiveImports,
36+
},
37+
external: [],
38+
plugins: [
39+
{
40+
name: 'client-external',
41+
resolveId(source, importer) {
42+
if (source.startsWith('/@react-pages/')) {
43+
return {
44+
id: source,
45+
external: 'absolute',
46+
}
3347
}
34-
}
48+
},
3549
},
36-
},
37-
resolve({
38-
// prevent bundling unexpected deps
39-
resolveOnly: ['jotai'],
40-
extensions,
41-
}),
42-
commonjs(),
43-
babel({
44-
babelHelpers: 'bundled',
45-
extensions,
46-
presets: [
47-
'@babel/preset-typescript',
48-
['@babel/preset-react', { runtime: 'automatic' }],
49-
],
50-
plugins: [],
51-
configFile: false,
52-
}),
53-
],
50+
resolve({
51+
// prevent bundling unexpected deps
52+
resolveOnly: ['jotai'],
53+
extensions,
54+
}),
55+
commonjs(),
56+
babel({
57+
babelHelpers: 'bundled',
58+
extensions,
59+
presets: [
60+
'@babel/preset-typescript',
61+
['@babel/preset-react', { runtime: 'automatic' }],
62+
],
63+
plugins: [],
64+
configFile: false,
65+
}),
66+
],
67+
}
5468
}

packages/react-pages/src/client/state.ts

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -60,15 +60,15 @@ if (import.meta.hot) {
6060
setPages?.(module.default)
6161
})
6262

63-
let setAllPagesOutlines: SetAtom<any, void> | undefined
64-
import.meta.hot!.accept('/@react-pages/allPagesOutlines', (module) => {
65-
// console.log('@@hot update /@react-pages/allPagesOutlines', module)
66-
if (!module) {
67-
console.error('unexpected hot module', module)
68-
return
69-
}
70-
setAllPagesOutlines?.(module)
71-
})
63+
// let setAllPagesOutlines: SetAtom<any, void> | undefined
64+
// import.meta.hot!.accept('/@react-pages/allPagesOutlines', (module) => {
65+
// // console.log('@@hot update /@react-pages/allPagesOutlines', module)
66+
// if (!module) {
67+
// console.error('unexpected hot module', module)
68+
// return
69+
// }
70+
// setAllPagesOutlines?.(module)
71+
// })
7272

7373
const pagesAtom = atom(initialPages)
7474
const pagePathsAtom = atom(initialPagePaths.sort())
@@ -164,7 +164,7 @@ if (import.meta.hot) {
164164

165165
useAllPagesOutlines = (timeout: number) => {
166166
const [data, set] = useAtom(allPagesOutlinesAtom)
167-
setAllPagesOutlines = set
167+
// setAllPagesOutlines = set
168168
useEffect(() => {
169169
setTimeout(() => {
170170
import('/@react-pages/allPagesOutlines').then((mod) => {

packages/react-pages/src/node/index.ts

Lines changed: 29 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import * as path from 'path'
22
import type { PluggableList } from 'unified'
3-
import type { Plugin, IndexHtmlTransformContext } from 'vite'
3+
import type { Plugin, IndexHtmlTransformContext, PluginOption } from 'vite'
44
import type { OutputPlugin } from 'rollup'
55
import type { staticSiteGenerationConfig } from './types'
66

@@ -269,21 +269,25 @@ function moveScriptTagToBodyEnd(
269269

270270
export default async function setupPlugins(
271271
vpConfig: PluginConfig = {}
272-
): Promise<Plugin[]> {
272+
): Promise<PluginOption[]> {
273273
// use dynamic import so that it supports node commonjs
274274
const mdx = await import('@mdx-js/rollup')
275+
const mdxPlugin = mdx.default({
276+
remarkPlugins: await getRemarkPlugins(),
277+
rehypePlugins: await getRehypePlugins(),
278+
// treat .md as mdx
279+
mdExtensions: [],
280+
mdxExtensions: ['.md', '.mdx'],
281+
providerImportSource: '@mdx-js/react',
282+
})
275283
return [
276-
mdx.default({
277-
remarkPlugins: await getRemarkPlugins(),
278-
rehypePlugins: await getRehypePlugins(),
279-
// treat .md as mdx
280-
mdExtensions: [],
281-
mdxExtensions: ['.md', '.mdx'],
282-
providerImportSource: '@mdx-js/react',
283-
}),
284+
{
285+
...mdxPlugin,
286+
enforce: 'pre',
287+
},
284288
createMdxTransformPlugin(),
285289
pluginFactory(vpConfig),
286-
] as Plugin[]
290+
]
287291
}
288292

289293
function getRemarkPlugins(): Promise<PluggableList> {
@@ -314,34 +318,36 @@ function getRehypePlugins(): Promise<PluggableList> {
314318
function createMdxTransformPlugin(): Plugin {
315319
let vitePluginReactTrasnform: Plugin['transform'] | undefined
316320
return {
317-
name: 'vite-pages:mdx-transform',
321+
name: 'vite-pages:mdx-fast-refresh',
322+
apply: 'serve',
318323
configResolved: ({ plugins }) => {
319324
// find this plugin to call it's transform function:
320325
// https://github.com/vitejs/vite-plugin-react/blob/b647e74c38565696bd6fb931b8bd9ac7f3bebe88/packages/plugin-react/src/index.ts#L206
326+
// or https://github.com/vitejs/vite-plugin-react-swc/blob/95e991914322e7b011d1c8d18d501b9eee21adaa/src/index.ts#L111
321327
vitePluginReactTrasnform = plugins.find(
322328
(p) =>
323-
p.name === 'vite:react-babel' && typeof p.transform === 'function'
329+
(p.name === 'vite:react-babel' &&
330+
typeof p.transform === 'function') ||
331+
(p.name === 'vite:react-swc' && typeof p.transform === 'function')
324332
)?.transform
325333
if (!vitePluginReactTrasnform) {
326334
throw new Error(
327-
`Can't find an instance of @vitejs/plugin-react. You should apply this plugin to make mdx work.`
335+
`Can't find an instance of @vitejs/plugin-react or @vitejs/plugin-react-swc. You should apply either of these plugins to make mdx work.`
328336
)
329337
}
330338
},
331339
transform: (code, id, options) => {
332-
const [filepath, querystring = ''] = id.split('?')
340+
const [filepath, ...qs] = id.split('?')
333341
if (
334342
filepath.match(/\.mdx?$/) &&
335343
!id.startsWith(OUTLINE_INFO_MODULE_ID_PREFIX)
336344
) {
337-
// make @vitejs/plugin-react treat the "output of @mdx-js/rollup transform" like a jsx file
338-
// https://github.com/vitejs/vite-plugin-react/blob/b647e74c38565696bd6fb931b8bd9ac7f3bebe88/packages/plugin-react/src/index.ts#L215
339-
let newId
340-
if (querystring) {
341-
newId = id + '&ext=.jsx'
342-
} else {
343-
newId = id + '?ext=.jsx'
344-
}
345+
// turn file path like `/path/to/md-file$.md` into `/path/to/md-file$.jsx`
346+
// make vite-plugin-react transform "the output of @mdx-js/rollup" like a jsx file
347+
// https://github.com/vitejs/vite-plugin-react/blob/caa9b5330092c70288fcb94ceb96ca42438df2a2/packages/plugin-react/src/index.ts#L170
348+
const newFilePath = filepath.replace(/\.mdx?$/, '.jsx')
349+
const newId = [newFilePath, ...qs].join('?')
350+
345351
return (vitePluginReactTrasnform as any)(code, newId, options)
346352
}
347353
},

0 commit comments

Comments
 (0)