-
Notifications
You must be signed in to change notification settings - Fork 4
/
Copy pathdemo-loader.webpack.js
149 lines (138 loc) · 4.41 KB
/
demo-loader.webpack.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
const path = require('path');
const pkg = require('./package.json');
const changeCase = require('change-case');
const babel = require('@babel/core');
const parser = require('@babel/parser');
const traverse = require('@babel/traverse');
module.exports = function demoLoader(source) {
const callback = this.async();
const parsed = parser.parse(source, {
sourceType: 'module',
plugins: ['typescript', 'jsx'],
});
const files = [];
traverse.default(parsed, {
enter: function (node) {
if (node.type === 'CallExpression') {
if (node.node.callee.name === 'fetch') {
const firstArg = node.node.arguments[0];
if (firstArg.type === 'StringLiteral') {
if (firstArg.value.startsWith('/')) {
files.push(firstArg.value);
} else {
throw new Error(
'fetch should only be used with a path starting with /',
);
}
} else {
throw new Error(
'fetch should use a string literal as the first argument',
);
}
}
}
},
});
const parsedAll = parsed.program.body;
const defaultExport = parsedAll.find(
(node) => node.type === 'ExportDefaultDeclaration',
);
const parsedBeforeDefaultExport = parsedAll.slice(0, parsedAll.length - 1);
const parsedImports = parsedBeforeDefaultExport.filter(
(statement) => statement.type === 'ImportDeclaration',
);
const defaultDeclaration = defaultExport.declaration;
if (
!defaultDeclaration ||
defaultDeclaration.type !== 'FunctionDeclaration'
) {
throw new Error(
`
example should have a default export of function declaration.
Example:
// import statements
import React from 'react';
import Plot from 'react-plot';
// Default export of function declaration
export default function Example(){};
`,
);
}
const codeSandboxDependencies = parsedImports.reduce((prev, current) => {
const source = current.source.value;
if (source.startsWith('.')) {
console.warn(
`in ${this.resourcePath}, import statements with relative path will not work in code sandbox`,
);
} else {
prev[source] = pkg.dependencies[source];
}
return prev;
}, {});
const functionComponentSource = source.slice(
defaultDeclaration.start,
defaultDeclaration.end,
);
const beforeDefaultExportSource = source.slice(0, defaultExport.start);
const afterDefaultExportSource = source.slice(defaultExport.end);
const name = changeCase.paramCase(
defaultDeclaration.id?.name || 'ReactPlotDemo',
);
const codeSandboxImportPath = path
.relative(this.context, path.join('src', 'components', 'CodeSandboxer.tsx'))
.replaceAll(path.sep, path.posix.sep);
const modifiedSource = `
${beforeDefaultExportSource}
import { useState as __useState__ } from 'react';
import CodeBlock from '@theme/CodeBlock';
import CodeSandboxer from '${codeSandboxImportPath}';
const exampleSource = ${JSON.stringify(source)};
const __EXAMPLE__ = ${functionComponentSource}
export default function __EXAMPLE_DEMO__(props) {
const [showCode, setShowCode] = __useState__(false);
return (
<>
<div className="demo-example-wrapper">
<__EXAMPLE__ />
<div className="demo-example-buttons">
<button
onClick={() => setShowCode((show) => !show)}
type="button"
style={{
backgroundColor: showCode ? '#dbeafe' : undefined,
}}
>
Code
</button>
{props.noCodesandbox ? null : (
<CodeSandboxer
name="${name}"
source={exampleSource}
dependencies={${JSON.stringify(codeSandboxDependencies)}}
publicFiles={${JSON.stringify(files)}}
>
{() => {
return <button type="submit">Open sandbox</button>;
}}
</CodeSandboxer>
)}
</div>
</div>
{showCode && (
<CodeBlock className="language-jsx">{exampleSource}</CodeBlock>
)}
</>
);
}
${afterDefaultExportSource}
`;
babel
.transformAsync(modifiedSource, {
filename: this.resourcePath,
presets: ['@babel/preset-typescript'],
})
.then((result) => {
callback(null, result.code);
})
.catch((e) => callback(e));
};