1 |
|
2 |
|
3 | import type {Config, PluginOptions} from '@parcel/types';
|
4 | import type {BabelConfig} from './types';
|
5 | import type {PluginLogger} from '@parcel/logger';
|
6 |
|
7 | import path from 'path';
|
8 | import * as babelCore from '@babel/core';
|
9 | import {md5FromObject, relativePath} from '@parcel/utils';
|
10 |
|
11 | import getEnvOptions from './env';
|
12 | import getJSXOptions from './jsx';
|
13 | import getFlowOptions from './flow';
|
14 | import getTypescriptOptions from './typescript';
|
15 | import {enginesToBabelTargets} from './utils';
|
16 |
|
17 | const TYPESCRIPT_EXTNAME_RE = /^\.tsx?/;
|
18 | const BABEL_TRANSFORMER_DIR = path.dirname(__dirname);
|
19 | const JS_EXTNAME_RE = /^\.(js|cjs|mjs)$/;
|
20 | const BABEL_CONFIG_FILENAMES = [
|
21 | '.babelrc',
|
22 | '.babelrc.js',
|
23 | '.babelrc.json',
|
24 | '.babelrc.cjs',
|
25 | '.babelrc.mjs',
|
26 | '.babelignore',
|
27 | 'babel.config.js',
|
28 | 'babel.config.json',
|
29 | 'babel.config.mjs',
|
30 | 'babel.config.cjs',
|
31 | ];
|
32 |
|
33 | export async function load(
|
34 | config: Config,
|
35 | options: PluginOptions,
|
36 | logger: PluginLogger,
|
37 | ): Promise<void> {
|
38 |
|
39 | if (!config.isSource) {
|
40 | return;
|
41 | }
|
42 |
|
43 | let babelOptions = {
|
44 | filename: config.searchPath,
|
45 | cwd: options.projectRoot,
|
46 | envName:
|
47 | options.env.BABEL_ENV ??
|
48 | options.env.NODE_ENV ??
|
49 | (options.mode === 'production' || options.mode === 'development'
|
50 | ? options.mode
|
51 | : null) ??
|
52 | 'development',
|
53 | showIgnoredFiles: true,
|
54 | };
|
55 |
|
56 | let partialConfig: ?{|
|
57 | [string]: any,
|
58 | |} = await babelCore.loadPartialConfigAsync(babelOptions);
|
59 |
|
60 |
|
61 | for (let fileName of BABEL_CONFIG_FILENAMES) {
|
62 | config.invalidateOnFileCreate({
|
63 | fileName,
|
64 | aboveFilePath: config.searchPath,
|
65 | });
|
66 | }
|
67 |
|
68 | let addIncludedFile = file => {
|
69 | if (JS_EXTNAME_RE.test(path.extname(file))) {
|
70 |
|
71 |
|
72 | logger.warn({
|
73 | message: `It looks like you're using a JavaScript Babel config file. This means the config cannot be watched for changes, and Babel transformations cannot be cached. You'll need to restart Parcel for changes to this config to take effect. Try using a ${path.basename(
|
74 | file,
|
75 | path.extname(file),
|
76 | ) + '.json'} file instead.`,
|
77 | });
|
78 | config.shouldInvalidateOnStartup();
|
79 |
|
80 |
|
81 | config.addDevDependency({
|
82 | moduleSpecifier: relativePath(options.projectRoot, file),
|
83 | resolveFrom: path.join(options.projectRoot, 'index'),
|
84 |
|
85 |
|
86 | invalidateParcelPlugin: true,
|
87 | });
|
88 | } else {
|
89 | config.addIncludedFile(file);
|
90 | }
|
91 | };
|
92 |
|
93 | let warnOldVersion = () => {
|
94 | logger.warn({
|
95 | message:
|
96 | 'You are using an old version of @babel/core which does not support the necessary features for Parcel to cache and watch babel config files safely. You may need to restart Parcel for config changes to take effect. Please upgrade to @babel/core 7.12.0 or later to resolve this issue.',
|
97 | });
|
98 | config.shouldInvalidateOnStartup();
|
99 | };
|
100 |
|
101 |
|
102 | if (partialConfig == null) {
|
103 | warnOldVersion();
|
104 | return;
|
105 | }
|
106 |
|
107 | if (partialConfig.files == null) {
|
108 |
|
109 |
|
110 | if (partialConfig.hasFilesystemConfig()) {
|
111 | warnOldVersion();
|
112 |
|
113 | if (typeof partialConfig.babelrcPath === 'string') {
|
114 | addIncludedFile(partialConfig.babelrcPath);
|
115 | }
|
116 |
|
117 | if (typeof partialConfig.configPath === 'string') {
|
118 | addIncludedFile(partialConfig.configPath);
|
119 | }
|
120 | }
|
121 | } else {
|
122 | for (let file of partialConfig.files) {
|
123 | addIncludedFile(file);
|
124 | }
|
125 | }
|
126 |
|
127 | if (
|
128 | partialConfig.fileHandling != null &&
|
129 | partialConfig.fileHandling !== 'transpile'
|
130 | ) {
|
131 | return;
|
132 | } else if (partialConfig.hasFilesystemConfig()) {
|
133 | config.setResult({
|
134 | internal: false,
|
135 | config: partialConfig.options,
|
136 | targets: enginesToBabelTargets(config.env),
|
137 | });
|
138 |
|
139 |
|
140 |
|
141 | if (hasRequire(partialConfig.options)) {
|
142 | logger.warn({
|
143 | message:
|
144 | 'It looks like you are using `require` to configure Babel plugins or presets. This means Babel transformations cannot be cached and will run on each build. Please use strings to configure Babel instead.',
|
145 | });
|
146 |
|
147 | config.setResultHash(JSON.stringify(Date.now()));
|
148 | config.shouldInvalidateOnStartup();
|
149 | } else {
|
150 | definePluginDependencies(config, options);
|
151 | config.setResultHash(md5FromObject(partialConfig.options));
|
152 | }
|
153 | } else {
|
154 | await buildDefaultBabelConfig(options, config);
|
155 | }
|
156 | }
|
157 |
|
158 | async function buildDefaultBabelConfig(options: PluginOptions, config: Config) {
|
159 | let jsxOptions = await getJSXOptions(options, config);
|
160 |
|
161 | let babelOptions;
|
162 | if (path.extname(config.searchPath).match(TYPESCRIPT_EXTNAME_RE)) {
|
163 | babelOptions = getTypescriptOptions(
|
164 | config,
|
165 | jsxOptions?.pragma,
|
166 | jsxOptions?.pragmaFrag,
|
167 | );
|
168 | } else {
|
169 | babelOptions = await getFlowOptions(config, options);
|
170 | }
|
171 |
|
172 | let babelTargets;
|
173 | let envOptions = await getEnvOptions(config);
|
174 | if (envOptions != null) {
|
175 | babelTargets = envOptions.targets;
|
176 | babelOptions = mergeOptions(babelOptions, envOptions.config);
|
177 | }
|
178 | babelOptions = mergeOptions(babelOptions, jsxOptions?.config);
|
179 |
|
180 | if (babelOptions != null) {
|
181 | let _babelOptions = babelOptions;
|
182 | _babelOptions.presets = (_babelOptions.presets || []).map(preset =>
|
183 | babelCore.createConfigItem(preset, {
|
184 | type: 'preset',
|
185 | dirname: BABEL_TRANSFORMER_DIR,
|
186 | }),
|
187 | );
|
188 | _babelOptions.plugins = (_babelOptions.plugins || []).map(plugin =>
|
189 | babelCore.createConfigItem(plugin, {
|
190 | type: 'plugin',
|
191 | dirname: BABEL_TRANSFORMER_DIR,
|
192 | }),
|
193 | );
|
194 | }
|
195 |
|
196 | config.setResult({
|
197 | internal: true,
|
198 | config: babelOptions,
|
199 | targets: babelTargets,
|
200 | });
|
201 | definePluginDependencies(config, options);
|
202 | }
|
203 |
|
204 | function mergeOptions(result, config?: null | BabelConfig) {
|
205 | if (
|
206 | !config ||
|
207 | ((!config.presets || config.presets.length === 0) &&
|
208 | (!config.plugins || config.plugins.length === 0))
|
209 | ) {
|
210 | return result;
|
211 | }
|
212 |
|
213 | let merged = result;
|
214 | if (merged) {
|
215 | merged.presets = (merged.presets || []).concat(config.presets || []);
|
216 | merged.plugins = (merged.plugins || []).concat(config.plugins || []);
|
217 | } else {
|
218 | result = config;
|
219 | }
|
220 |
|
221 | return result;
|
222 | }
|
223 |
|
224 | function hasRequire(options) {
|
225 | let configItems = [...options.presets, ...options.plugins];
|
226 | return configItems.some(item => !item.file);
|
227 | }
|
228 |
|
229 | function definePluginDependencies(config, options) {
|
230 | let babelConfig = config.result.config;
|
231 | if (babelConfig == null) {
|
232 | return;
|
233 | }
|
234 |
|
235 | let configItems = [...babelConfig.presets, ...babelConfig.plugins];
|
236 | for (let configItem of configItems) {
|
237 |
|
238 |
|
239 |
|
240 | config.addDevDependency({
|
241 | moduleSpecifier: relativePath(
|
242 | options.projectRoot,
|
243 | configItem.file.resolved,
|
244 | ),
|
245 | resolveFrom: path.join(options.projectRoot, 'index'),
|
246 |
|
247 |
|
248 | invalidateParcelPlugin: true,
|
249 | });
|
250 | }
|
251 | }
|