UNPKG

8.06 kBJavaScriptView Raw
1// @flow
2
3import type {Config, PluginOptions} from '@parcel/types';
4import type {BabelConfig} from './types';
5import type {PluginLogger} from '@parcel/logger';
6
7import path from 'path';
8import * as babelCore from '@babel/core';
9import {md5FromObject, relativePath} from '@parcel/utils';
10
11import getEnvOptions from './env';
12import getJSXOptions from './jsx';
13import getFlowOptions from './flow';
14import getTypescriptOptions from './typescript';
15import {enginesToBabelTargets} from './utils';
16
17const TYPESCRIPT_EXTNAME_RE = /^\.tsx?/;
18const BABEL_TRANSFORMER_DIR = path.dirname(__dirname);
19const JS_EXTNAME_RE = /^\.(js|cjs|mjs)$/;
20const 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
33export async function load(
34 config: Config,
35 options: PluginOptions,
36 logger: PluginLogger,
37): Promise<void> {
38 // Don't transpile inside node_modules
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 // Invalidate when any babel config file is added.
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 // We need to invalidate on startup in case the config is non-static,
71 // e.g. uses unknown environment variables, reads from the filesystem, etc.
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 // But also add the config as a dev dependency so we can at least attempt invalidation in watch mode.
81 config.addDevDependency({
82 moduleSpecifier: relativePath(options.projectRoot, file),
83 resolveFrom: path.join(options.projectRoot, 'index'),
84 // Also invalidate @parcel/transformer-babel when the config or a dependency updates.
85 // This ensures that the caches in @babel/core are also invalidated.
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 // Old versions of @babel/core return null from loadPartialConfig when the file should explicitly not be run through babel (ignore/exclude)
102 if (partialConfig == null) {
103 warnOldVersion();
104 return;
105 }
106
107 if (partialConfig.files == null) {
108 // If the files property is missing, we're on an old version of @babel/core.
109 // We need to invalidate on startup because we can't properly track dependencies.
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 // If the config has plugins loaded with require(), or inline plugins in the config,
140 // we can't cache the result of the compilation because we don't know where they came from.
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
158async 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; // For Flow
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
204function 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
224function hasRequire(options) {
225 let configItems = [...options.presets, ...options.plugins];
226 return configItems.some(item => !item.file);
227}
228
229function 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 // FIXME: this uses a relative path from the project root rather than resolving
238 // from the config location because configItem.file.request can be a shorthand
239 // rather than a full package name.
240 config.addDevDependency({
241 moduleSpecifier: relativePath(
242 options.projectRoot,
243 configItem.file.resolved,
244 ),
245 resolveFrom: path.join(options.projectRoot, 'index'),
246 // Also invalidate @parcel/transformer-babel when the plugin or a dependency updates.
247 // This ensures that the caches in @babel/core are also invalidated.
248 invalidateParcelPlugin: true,
249 });
250 }
251}