@@ -2,8 +2,10 @@ import path from 'node:path';
2
2
import type { Node } from '@babel/traverse' ;
3
3
import traverse from '@babel/traverse' ;
4
4
import * as esbuild from 'esbuild' ;
5
+ import type { RawSourceMap } from 'source-map-js' ;
5
6
import type { Config as TailwindOriginalConfig } from 'tailwindcss' ;
6
7
import type { AST } from '../../../actions/email-validation/check-compatibility' ;
8
+ import { improveErrorWithSourceMap } from '../../improve-error-with-sourcemap' ;
7
9
import { isErr } from '../../result' ;
8
10
import { runBundledCode } from '../../run-bundled-code' ;
9
11
@@ -23,8 +25,6 @@ export type TailwindConfig = Pick<
23
25
| 'plugins'
24
26
> ;
25
27
26
- type ImportDeclaration = Node & { type : 'ImportDeclaration' } ;
27
-
28
28
export const getTailwindConfig = async (
29
29
sourceCode : string ,
30
30
ast : AST ,
@@ -33,35 +33,23 @@ export const getTailwindConfig = async (
33
33
const configAttribute = getTailwindConfigNode ( ast ) ;
34
34
35
35
if ( configAttribute ) {
36
- const configIdentifierName =
37
- configAttribute . value ?. type === 'JSXExpressionContainer' &&
38
- configAttribute . value . expression . type === 'Identifier'
39
- ? configAttribute . value . expression . name
40
- : undefined ;
41
- if ( configIdentifierName ) {
42
- const tailwindConfigImport = getImportWithGivenDefaultSpecifier (
43
- ast ,
44
- configIdentifierName ,
45
- ) ;
46
- if ( tailwindConfigImport ) {
47
- return getConfigFromImport ( tailwindConfigImport , sourcePath ) ;
48
- }
49
- }
50
-
51
- const configObjectExpression =
52
- configAttribute . value ?. type === 'JSXExpressionContainer' &&
53
- configAttribute . value . expression . type === 'ObjectExpression'
36
+ const configExpressionValue =
37
+ configAttribute . value ?. type === 'JSXExpressionContainer'
54
38
? configAttribute . value . expression
55
39
: undefined ;
56
- if ( configObjectExpression ?. start && configObjectExpression . end ) {
57
- const configObjectSourceCode = sourceCode . slice (
58
- configObjectExpression . start ,
59
- configObjectExpression . end ,
40
+ if ( configExpressionValue ?. start && configExpressionValue . end ) {
41
+ const configSourceValue = sourceCode . slice (
42
+ configExpressionValue . start ,
43
+ configExpressionValue . end ,
60
44
) ;
61
45
62
46
try {
63
- const getConfig = new Function ( `return ${ configObjectSourceCode } ` ) ;
64
- return getConfig ( ) as TailwindConfig ;
47
+ return getConfigFromCode (
48
+ `${ sourceCode }
49
+
50
+ const reactEmailTailwindConfigInternal = ${ configSourceValue } ;` ,
51
+ sourcePath ,
52
+ ) ;
65
53
} catch ( exception ) {
66
54
console . warn ( exception ) ;
67
55
console . warn (
@@ -74,80 +62,77 @@ export const getTailwindConfig = async (
74
62
return { } ;
75
63
} ;
76
64
77
- const getConfigFromImport = async (
78
- tailwindConfigImport : ImportDeclaration ,
79
- sourcePath : string ,
65
+ const getConfigFromCode = async (
66
+ code : string ,
67
+ filepath : string ,
80
68
) : Promise < TailwindConfig > => {
81
- const configRelativePath = tailwindConfigImport . source . value ;
82
- const sourceDirpath = path . dirname ( sourcePath ) ;
83
- const configFilepath = path . join ( sourceDirpath , configRelativePath ) ;
69
+ const configDirpath = path . dirname ( filepath ) ;
84
70
85
71
const configBuildResult = await esbuild . build ( {
86
72
bundle : true ,
87
73
stdin : {
88
- contents : `import tailwindConfig from "${ configRelativePath } ";
89
- export { tailwindConfig };` ,
74
+ contents : `${ code }
75
+ export { reactEmailTailwindConfigInternal };` ,
76
+ sourcefile : filepath ,
90
77
loader : 'tsx' ,
91
- resolveDir : path . dirname ( sourcePath ) ,
78
+ resolveDir : configDirpath ,
92
79
} ,
93
80
platform : 'node' ,
81
+ sourcemap : 'external' ,
82
+ jsx : 'automatic' ,
83
+ outdir : 'stdout' , // just a stub for esbuild, it won't actually write to this folder
94
84
write : false ,
95
85
format : 'cjs' ,
96
86
logLevel : 'silent' ,
97
87
} ) ;
98
- const configFile = configBuildResult . outputFiles [ 0 ] ;
88
+ const sourceMapFile = configBuildResult . outputFiles [ 0 ] ! ;
89
+ const configFile = configBuildResult . outputFiles [ 1 ] ;
99
90
if ( configFile === undefined ) {
100
91
throw new Error (
101
92
'Could not build config file as it was found as undefined, this is most likely a bug, please open an issue.' ,
102
93
) ;
103
94
}
104
- const configModule = runBundledCode ( configFile . text , configFilepath ) ;
95
+
96
+ const configModule = runBundledCode ( configFile . text , filepath ) ;
105
97
if ( isErr ( configModule ) ) {
106
- throw new Error (
107
- `Error when trying to run the config file: ${ configModule . error } ` ,
98
+ const sourceMap = JSON . parse ( sourceMapFile . text ) as RawSourceMap ;
99
+ // because it will have a path like <tsconfigLocation>/stdout/email.js.map
100
+ sourceMap . sourceRoot = path . resolve ( sourceMapFile . path , '../..' ) ;
101
+ sourceMap . sources = sourceMap . sources . map ( ( source ) =>
102
+ path . resolve ( sourceMapFile . path , '..' , source ) ,
108
103
) ;
104
+ const errorObject = improveErrorWithSourceMap (
105
+ configModule . error as Error ,
106
+ filepath ,
107
+ sourceMap ,
108
+ ) ;
109
+ const error = new Error ( ) ;
110
+ error . name = errorObject . name ;
111
+ error . message = errorObject . message ;
112
+ error . stack = errorObject . stack ;
113
+ throw error ;
109
114
}
110
115
111
116
if (
112
117
typeof configModule . value === 'object' &&
113
118
configModule . value !== null &&
114
- 'tailwindConfig ' in configModule . value
119
+ 'reactEmailTailwindConfigInternal ' in configModule . value
115
120
) {
116
- return configModule . value . tailwindConfig as TailwindConfig ;
121
+ return configModule . value
122
+ . reactEmailTailwindConfigInternal as TailwindConfig ;
117
123
}
118
124
119
125
throw new Error (
120
- ` Could not read Tailwind config at ${ configFilepath } because it doesn't have a default export in it.` ,
126
+ ' Could not get the Tailwind config, this is likely a bug, please file an issue.' ,
121
127
{
122
128
cause : {
123
129
configModule,
124
- configFilepath,
130
+ configFilepath : filepath ,
125
131
} ,
126
132
} ,
127
133
) ;
128
134
} ;
129
135
130
- const getImportWithGivenDefaultSpecifier = (
131
- ast : AST ,
132
- specifierName : string ,
133
- ) => {
134
- let importNode : ImportDeclaration | undefined ;
135
- traverse ( ast , {
136
- ImportDeclaration ( nodePath ) {
137
- if (
138
- nodePath . node . specifiers . some (
139
- ( specifier ) =>
140
- specifier . type === 'ImportDefaultSpecifier' &&
141
- specifier . local . name === specifierName ,
142
- )
143
- ) {
144
- importNode = nodePath . node ;
145
- }
146
- } ,
147
- } ) ;
148
- return importNode ;
149
- } ;
150
-
151
136
type JSXAttribute = Node & { type : 'JSXAttribute' } ;
152
137
153
138
const getTailwindConfigNode = ( ast : AST ) => {
0 commit comments