1
- import { enrichCsf , formatCsf , loadCsf } from '@storybook/csf-tools' ;
2
- import { getCacheDir } from './react-docgen-typescript' ;
1
+ import { Transformer } from '@parcel/plugin' ;
2
+ import { enrichCsf , formatCsf , babelParse , CsfFile } from '@storybook/csf-tools' ;
3
+ import * as t from '@babel/types' ;
4
+ import { parse } from '@babel/parser' ;
3
5
import path from 'path' ;
6
+ import crypto from 'crypto' ;
7
+ import { getClient , getCacheDir } from './react-docgen-typescript' ;
8
+ import { ComponentDoc } from 'react-docgen-typescript' ;
4
9
import SourceMap from '@parcel/source-map' ;
5
- import { Transformer } from '@parcel/plugin' ;
6
10
7
11
module . exports = new Transformer ( {
8
12
async transform ( { asset, options} ) {
13
+ let docs : ComponentDoc | null = null ;
14
+ if ( asset . type === 'ts' || asset . type === 'tsx' ) {
15
+ let client = await getClient ( options ) ;
16
+ docs = await client . getDocs ( asset . filePath ) ;
17
+ }
9
18
let code = await asset . getCode ( ) ;
10
- let { code : compiledCode , rawMappings} = processCsf ( code , asset . filePath ) as any ;
19
+ let name = options . hmrOptions ? `$parcel$ReactRefresh$${ asset . id . slice ( - 4 ) } ` : null ;
20
+ let { code : compiledCode , rawMappings} = processCsf ( code , asset . filePath , docs , name ) as any ;
11
21
12
22
let map = new SourceMap ( options . projectRoot ) ;
13
23
if ( rawMappings ) {
@@ -22,13 +32,249 @@ module.exports = new Transformer({
22
32
}
23
33
} ) ;
24
34
25
- function processCsf ( code : string , filePath : string ) {
26
- let csf = loadCsf ( code , {
35
+ function processCsf ( code : string , filePath : string , docs : ComponentDoc | null , refreshName : string | null ) {
36
+ let ast = parse ( code , {
37
+ sourceFilename : filePath ,
38
+ sourceType : 'module' ,
39
+ plugins : [ 'typescript' , 'jsx' , 'importAttributes' , 'classProperties' ] ,
40
+ tokens : true
41
+ } ) ;
42
+
43
+ let csf = new CsfFile ( ast , {
27
44
fileName : filePath ,
28
45
makeTitle : title => title || 'default'
29
- } ) ;
46
+ } ) . parse ( ) ;
30
47
enrichCsf ( csf , csf ) ;
31
48
49
+ // Extract story functions into separate components. This enables React Fast Refresh to work properly.
50
+ let count = 0 ;
51
+ let addComponent = ( node : t . Function ) => {
52
+ let name = 'Story' + count ++ ;
53
+ csf . _ast . program . body . push ( t . functionDeclaration (
54
+ t . identifier ( name ) ,
55
+ node . params . map ( p => t . cloneNode ( p ) ) ,
56
+ t . isExpression ( node . body ) ? t . blockStatement ( [ t . returnStatement ( node . body ) ] ) : node . body
57
+ ) ) ;
58
+ node . body = t . blockStatement ( [
59
+ t . returnStatement (
60
+ t . jsxElement (
61
+ t . jsxOpeningElement (
62
+ t . jsxIdentifier ( name ) ,
63
+ node . params . length && t . isIdentifier ( node . params [ 0 ] ) ? [ t . jsxSpreadAttribute ( t . cloneNode ( node . params [ 0 ] ) ) ] : [ ] ,
64
+ true
65
+ ) ,
66
+ null ,
67
+ [ ] ,
68
+ true
69
+ )
70
+ )
71
+ ] ) ;
72
+ return name ;
73
+ } ;
74
+
75
+ let handleRenderProperty = ( node : t . ObjectExpression ) => {
76
+ // CSF 3 style object story. Extract render function into a component.
77
+ let render = node . properties . find ( p => ( t . isObjectProperty ( p ) || t . isObjectMethod ( p ) ) && t . isIdentifier ( p . key ) && p . key . name === 'render' ) ;
78
+ if ( render ?. type === 'ObjectProperty' && t . isFunction ( render . value ) ) {
79
+ let c = addComponent ( render . value ) ;
80
+ node . properties . push ( t . objectProperty ( t . identifier ( '_internalComponent' ) , t . identifier ( c ) ) ) ;
81
+ } else if ( render ?. type === 'ObjectMethod' ) {
82
+ let c = addComponent ( render ) ;
83
+ node . properties . push ( t . objectProperty ( t . identifier ( '_internalComponent' ) , t . identifier ( c ) ) ) ;
84
+ } else if ( t . isObjectProperty ( render ) && render . value . type === 'Identifier' ) {
85
+ render . value = t . arrowFunctionExpression (
86
+ [ t . identifier ( 'args' ) ] ,
87
+ t . jsxElement (
88
+ t . jsxOpeningElement (
89
+ t . jsxIdentifier ( render . value . name ) ,
90
+ [ t . jsxSpreadAttribute ( t . identifier ( 'args' ) ) ] ,
91
+ true
92
+ ) ,
93
+ null ,
94
+ [ ] ,
95
+ true
96
+ )
97
+ ) ;
98
+ }
99
+ } ;
100
+
101
+ if ( refreshName ) {
102
+ for ( let name in csf . _storyExports ) {
103
+ let node = csf . getStoryExport ( name ) ;
104
+
105
+ // Generate a hash of the args and parameters. If this changes, we bail out of Fast Refresh.
106
+ let annotations = csf . _storyAnnotations [ name ] ;
107
+ let storyHash = '' ;
108
+ if ( annotations ) {
109
+ let hash = crypto . createHash ( 'md5' ) ;
110
+ if ( annotations . args ) {
111
+ hash . update ( code . slice ( annotations . args . start ! , annotations . args . end ! ) ) ;
112
+ }
113
+ if ( annotations . parameters ) {
114
+ hash . update ( code . slice ( annotations . parameters . start ! , annotations . parameters . end ! ) ) ;
115
+ }
116
+ storyHash = hash . digest ( 'hex' ) ;
117
+ }
118
+
119
+ if ( t . isFunction ( node ) ) {
120
+ // CSF 2 style function story.
121
+ let c = addComponent ( node ) ;
122
+ csf . _ast . program . body . push ( t . expressionStatement (
123
+ t . assignmentExpression (
124
+ '=' ,
125
+ t . memberExpression ( t . identifier ( name ) , t . identifier ( '_internalComponent' ) ) ,
126
+ t . identifier ( c )
127
+ )
128
+ ) ) ;
129
+
130
+ if ( storyHash ) {
131
+ csf . _ast . program . body . push ( t . expressionStatement (
132
+ t . assignmentExpression (
133
+ '=' ,
134
+ t . memberExpression ( t . identifier ( name ) , t . identifier ( '_hash' ) ) ,
135
+ t . stringLiteral ( storyHash )
136
+ )
137
+ ) ) ;
138
+ }
139
+ } else if ( node . type === 'ObjectExpression' ) {
140
+ handleRenderProperty ( node ) ;
141
+ if ( storyHash ) {
142
+ node . properties . push ( t . objectProperty ( t . identifier ( '_hash' ) , t . stringLiteral ( storyHash ) ) ) ;
143
+ }
144
+ }
145
+ }
146
+ }
147
+
148
+ // Hash the default export to invalidate Fast Refresh.
149
+ if ( csf . _metaNode ?. type === 'ObjectExpression' ) {
150
+ if ( docs ) {
151
+ let component = csf . _metaNode . properties . find ( p => t . isObjectProperty ( p ) && t . isIdentifier ( p . key ) && p . key . name === 'component' ) ;
152
+ if ( t . isObjectProperty ( component ) && t . isExpression ( component . value ) ) {
153
+ component . value = t . sequenceExpression ( [
154
+ t . assignmentExpression ( '=' , t . memberExpression ( component . value , t . identifier ( '__docgenInfo' ) ) , t . valueToNode ( docs ) ) ,
155
+ component . value
156
+ ] ) ;
157
+ }
158
+ }
159
+
160
+
161
+ if ( refreshName ) {
162
+ handleRenderProperty ( csf . _metaNode ) ;
163
+
164
+ let hash = crypto . createHash ( 'md5' ) ;
165
+ hash . update ( code . slice ( csf . _metaNode . start ! , csf . _metaNode . end ! ) ) ;
166
+ hash . update ( JSON . stringify ( docs ) ) ;
167
+ let metaHash = hash . digest ( 'hex' ) ;
168
+ csf . _metaNode . properties . push ( t . objectProperty ( t . identifier ( '_hash' ) , t . stringLiteral ( metaHash ) ) ) ;
169
+ }
170
+ }
171
+
172
+ if ( refreshName ) {
173
+ wrapRefresh ( csf . _ast . program , filePath , refreshName ) ;
174
+ }
175
+
32
176
// @ts -ignore
33
177
return formatCsf ( csf , { sourceFileName : filePath , sourceMaps : true , importAttributesKeyword : 'with' } ) ;
34
178
}
179
+
180
+ function wrapRefresh ( program : t . Program , filePath : string , refreshName : string ) {
181
+ let wrapperPath = `${ path . relative (
182
+ path . dirname ( filePath ) ,
183
+ __dirname ,
184
+ ) } /csf-hmr.js`;
185
+
186
+ // Group imports, exports, and body statements which will be wrapped in a try...catch.
187
+ let imports : ( t . ImportDeclaration | t . ExportDeclaration ) [ ] = [ ] ;
188
+ let statements : t . Statement [ ] = [ ] ;
189
+ let exportVars : t . VariableDeclarator [ ] = [ ] ;
190
+ let exports : t . ExportSpecifier [ ] = [ ] ;
191
+
192
+ for ( let statement of program . body ) {
193
+ if ( t . isImportDeclaration ( statement ) || t . isExportAllDeclaration ( statement ) ) {
194
+ imports . push ( statement ) ;
195
+ } else if ( t . isExportNamedDeclaration ( statement ) ) {
196
+ if ( statement . exportKind === 'type' || statement . source ) {
197
+ imports . push ( statement ) ;
198
+ } else if ( statement . declaration ) {
199
+ statements . push ( statement . declaration ) ;
200
+ for ( let id in t . getOuterBindingIdentifiers ( statement . declaration ) ) {
201
+ let name = refreshName + '$Export' + exportVars . length ;
202
+ exportVars . push ( t . variableDeclarator ( t . identifier ( name ) ) ) ;
203
+ exports . push ( t . exportSpecifier ( t . identifier ( name ) , t . identifier ( id ) ) ) ;
204
+ statements . push ( t . expressionStatement ( t . assignmentExpression ( '=' , t . identifier ( name ) , t . identifier ( id ) ) ) ) ;
205
+ }
206
+ } else if ( statement . specifiers ) {
207
+ for ( let specifier of statement . specifiers ) {
208
+ if ( t . isExportSpecifier ( specifier ) ) {
209
+ let name = refreshName + '$Export' + exportVars . length ;
210
+ exportVars . push ( t . variableDeclarator ( t . identifier ( name ) ) ) ;
211
+ exports . push ( t . exportSpecifier ( t . identifier ( name ) , specifier . exported ) ) ;
212
+ statements . push ( t . expressionStatement ( t . assignmentExpression ( '=' , t . identifier ( name ) , specifier . local ) ) ) ;
213
+ }
214
+ }
215
+ }
216
+ } else if ( t . isExportDefaultDeclaration ( statement ) ) {
217
+ if ( t . isExpression ( statement . declaration ) ) {
218
+ let name = refreshName + '$Export' + exportVars . length ;
219
+ exportVars . push ( t . variableDeclarator ( t . identifier ( name ) ) ) ;
220
+ exports . push ( t . exportSpecifier ( t . identifier ( name ) , t . identifier ( 'default' ) ) ) ;
221
+ statements . push ( t . expressionStatement ( t . assignmentExpression ( '=' , t . identifier ( name ) , statement . declaration ) ) ) ;
222
+ } else {
223
+ statements . push ( statement . declaration ) ;
224
+ let name = refreshName + '$Export' + exportVars . length ;
225
+ exportVars . push ( t . variableDeclarator ( t . identifier ( name ) ) ) ;
226
+ exports . push ( t . exportSpecifier ( t . identifier ( name ) , t . identifier ( 'default' ) ) ) ;
227
+ }
228
+ } else {
229
+ statements . push ( statement ) ;
230
+ }
231
+ }
232
+
233
+ program . body = [
234
+ ...imports ,
235
+ t . importDeclaration (
236
+ [ t . importNamespaceSpecifier ( t . identifier ( refreshName + '$Helpers' ) ) ] ,
237
+ t . stringLiteral ( wrapperPath )
238
+ ) ,
239
+ t . variableDeclaration ( 'var' , exportVars ) ,
240
+ t . variableDeclaration ( 'var' , [
241
+ t . variableDeclarator ( t . identifier ( refreshName + '$PrevRefreshReg' ) , t . memberExpression ( t . identifier ( 'window' ) , t . identifier ( '$RefreshReg$' ) ) ) ,
242
+ t . variableDeclarator ( t . identifier ( refreshName + '$PrevRefreshSig' ) , t . memberExpression ( t . identifier ( 'window' ) , t . identifier ( '$RefreshSig$' ) ) )
243
+ ] ) ,
244
+ t . expressionStatement (
245
+ t . callExpression (
246
+ t . memberExpression ( t . identifier ( refreshName + '$Helpers' ) , t . identifier ( 'prelude' ) ) ,
247
+ [ t . identifier ( 'module' ) ]
248
+ )
249
+ ) ,
250
+ t . tryStatement (
251
+ t . blockStatement ( [
252
+ ...statements ,
253
+ t . expressionStatement (
254
+ t . callExpression (
255
+ t . memberExpression ( t . identifier ( refreshName + '$Helpers' ) , t . identifier ( 'postlude' ) ) ,
256
+ [ t . identifier ( 'module' ) ]
257
+ )
258
+ ) ,
259
+ ] ) ,
260
+ null ,
261
+ t . blockStatement ( [
262
+ t . expressionStatement (
263
+ t . assignmentExpression (
264
+ '=' ,
265
+ t . memberExpression ( t . identifier ( 'window' ) , t . identifier ( '$RefreshReg$' ) ) ,
266
+ t . identifier ( refreshName + '$PrevRefreshReg' )
267
+ ) ,
268
+ ) ,
269
+ t . expressionStatement (
270
+ t . assignmentExpression (
271
+ '=' ,
272
+ t . memberExpression ( t . identifier ( 'window' ) , t . identifier ( '$RefreshSig$' ) ) ,
273
+ t . identifier ( refreshName + '$PrevRefreshSig' )
274
+ ) ,
275
+ ) ,
276
+ ] )
277
+ ) ,
278
+ t . exportNamedDeclaration ( null , exports )
279
+ ] ;
280
+ }
0 commit comments