1 |
|
2 | import type {
|
3 | ResolvedParcelConfigFile,
|
4 | FilePath,
|
5 | Glob,
|
6 | Transformer,
|
7 | Resolver,
|
8 | Bundler,
|
9 | Namer,
|
10 | Runtime,
|
11 | EnvironmentContext,
|
12 | PackageName,
|
13 | Packager,
|
14 | Optimizer,
|
15 | Reporter,
|
16 | Validator,
|
17 | } from '@parcel/types';
|
18 | import type {PackageManager} from '@parcel/package-manager';
|
19 | import {isMatch} from 'micromatch';
|
20 | import {basename} from 'path';
|
21 | import loadPlugin from './loadParcelPlugin';
|
22 |
|
23 | type Pipeline = Array<PackageName>;
|
24 | type GlobMap<T> = {[Glob]: T, ...};
|
25 | type SerializedParcelConfig = {|
|
26 | $$raw: boolean,
|
27 | config: ResolvedParcelConfigFile,
|
28 | packageManager: PackageManager,
|
29 | |};
|
30 |
|
31 | export default class ParcelConfig {
|
32 | packageManager: PackageManager;
|
33 | filePath: FilePath;
|
34 | resolvers: Pipeline;
|
35 | transforms: GlobMap<Pipeline>;
|
36 | bundler: PackageName;
|
37 | namers: Pipeline;
|
38 | runtimes: {[EnvironmentContext]: Pipeline, ...};
|
39 | packagers: GlobMap<PackageName>;
|
40 | validators: GlobMap<Pipeline>;
|
41 | optimizers: GlobMap<Pipeline>;
|
42 | reporters: Pipeline;
|
43 | pluginCache: Map<PackageName, any>;
|
44 |
|
45 | constructor(
|
46 | config: ResolvedParcelConfigFile,
|
47 | packageManager: PackageManager,
|
48 | ) {
|
49 | this.packageManager = packageManager;
|
50 | this.filePath = config.filePath;
|
51 | this.resolvers = config.resolvers || [];
|
52 | this.transforms = config.transforms || {};
|
53 | this.runtimes = config.runtimes || {};
|
54 | this.bundler = config.bundler || '';
|
55 | this.namers = config.namers || [];
|
56 | this.packagers = config.packagers || {};
|
57 | this.optimizers = config.optimizers || {};
|
58 | this.reporters = config.reporters || [];
|
59 | this.validators = config.validators || {};
|
60 | this.pluginCache = new Map();
|
61 | }
|
62 |
|
63 | static deserialize(serialized: SerializedParcelConfig) {
|
64 | return new ParcelConfig(serialized.config, serialized.packageManager);
|
65 | }
|
66 |
|
67 | getConfig() {
|
68 | return {
|
69 | filePath: this.filePath,
|
70 | resolvers: this.resolvers,
|
71 | transforms: this.transforms,
|
72 | validators: this.validators,
|
73 | runtimes: this.runtimes,
|
74 | bundler: this.bundler,
|
75 | namers: this.namers,
|
76 | packagers: this.packagers,
|
77 | optimizers: this.optimizers,
|
78 | reporters: this.reporters,
|
79 | };
|
80 | }
|
81 |
|
82 | serialize(): SerializedParcelConfig {
|
83 | return {
|
84 | $$raw: false,
|
85 | packageManager: this.packageManager,
|
86 | config: this.getConfig(),
|
87 | };
|
88 | }
|
89 |
|
90 | loadPlugin(pluginName: PackageName) {
|
91 | let plugin = this.pluginCache.get(pluginName);
|
92 | if (plugin) {
|
93 | return plugin;
|
94 | }
|
95 |
|
96 | plugin = loadPlugin(this.packageManager, pluginName, this.filePath);
|
97 | this.pluginCache.set(pluginName, plugin);
|
98 | return plugin;
|
99 | }
|
100 |
|
101 | loadPlugins<T>(
|
102 | plugins: Pipeline,
|
103 | ): Promise<
|
104 | Array<{|
|
105 | name: string,
|
106 | plugin: T,
|
107 | |}>,
|
108 | > {
|
109 | return Promise.all(
|
110 | plugins.map(async pluginName => {
|
111 | return {
|
112 | name: pluginName,
|
113 | plugin: await this.loadPlugin(pluginName),
|
114 | };
|
115 | }),
|
116 | );
|
117 | }
|
118 |
|
119 | getResolverNames() {
|
120 | if (this.resolvers.length === 0) {
|
121 | throw new Error('No resolver plugins specified in .parcelrc config');
|
122 | }
|
123 |
|
124 | return this.resolvers;
|
125 | }
|
126 |
|
127 | getResolvers() {
|
128 | return this.loadPlugins<Resolver>(this.getResolverNames());
|
129 | }
|
130 |
|
131 | getValidatorNames(filePath: FilePath): Array<string> {
|
132 | let validators: Pipeline =
|
133 | this.matchGlobMapPipelines(filePath, this.validators) || [];
|
134 |
|
135 | return validators;
|
136 | }
|
137 |
|
138 | getTransformerNames(filePath: FilePath, pipeline?: ?string): Array<string> {
|
139 | let transformers: Pipeline | null = this.matchGlobMapPipelines(
|
140 | filePath,
|
141 | this.transforms,
|
142 | pipeline,
|
143 | );
|
144 | if (!transformers || transformers.length === 0) {
|
145 | throw new Error(`No transformers found for "${filePath}".`);
|
146 | }
|
147 |
|
148 | return transformers;
|
149 | }
|
150 |
|
151 | getValidators(filePath: FilePath) {
|
152 | let names = this.getValidatorNames(filePath);
|
153 | return this.loadPlugins<Validator>(names);
|
154 | }
|
155 |
|
156 | getNamedPipelines(): $ReadOnlyArray<string> {
|
157 | return Object.keys(this.transforms)
|
158 | .filter(glob => glob.includes(':'))
|
159 | .map(glob => glob.split(':')[0]);
|
160 | }
|
161 |
|
162 | getTransformers(filePath: FilePath, pipeline?: ?string) {
|
163 | return this.loadPlugins<Transformer>(
|
164 | this.getTransformerNames(filePath, pipeline),
|
165 | );
|
166 | }
|
167 |
|
168 | getBundler(): Promise<Bundler> {
|
169 | if (!this.bundler) {
|
170 | throw new Error('No bundler specified in .parcelrc config');
|
171 | }
|
172 |
|
173 | return this.loadPlugin(this.bundler);
|
174 | }
|
175 |
|
176 | getNamers() {
|
177 | if (this.namers.length === 0) {
|
178 | throw new Error('No namer plugins specified in .parcelrc config');
|
179 | }
|
180 |
|
181 | return this.loadPlugins<Namer>(this.namers);
|
182 | }
|
183 |
|
184 | getRuntimes(
|
185 | context: EnvironmentContext,
|
186 | ): Promise<
|
187 | Array<{|
|
188 | name: string,
|
189 | plugin: Runtime,
|
190 | |}>,
|
191 | > {
|
192 | let runtimes = this.runtimes[context];
|
193 | if (!runtimes) {
|
194 | return Promise.resolve([]);
|
195 | }
|
196 |
|
197 | return this.loadPlugins<Runtime>(runtimes);
|
198 | }
|
199 |
|
200 | getPackagerName(filePath: FilePath): string {
|
201 | let packagerName: ?PackageName = this.matchGlobMap(
|
202 | filePath,
|
203 | this.packagers,
|
204 | );
|
205 | if (!packagerName) {
|
206 | throw new Error(`No packager found for "${filePath}".`);
|
207 | }
|
208 | return packagerName;
|
209 | }
|
210 |
|
211 | async getPackager(
|
212 | filePath: FilePath,
|
213 | ): Promise<{|
|
214 | name: string,
|
215 | plugin: Packager,
|
216 | |}> {
|
217 | let packagerName = this.getPackagerName(filePath);
|
218 | return {
|
219 | name: packagerName,
|
220 | plugin: await this.loadPlugin(packagerName),
|
221 | };
|
222 | }
|
223 |
|
224 | getOptimizerNames(filePath: FilePath, pipeline: ?string): Array<string> {
|
225 | return (
|
226 | this.matchGlobMapPipelines(filePath, this.optimizers, pipeline) ?? []
|
227 | );
|
228 | }
|
229 |
|
230 | getOptimizers(
|
231 | filePath: FilePath,
|
232 | pipeline: ?string,
|
233 | ): Promise<
|
234 | Array<{|
|
235 | name: string,
|
236 | plugin: Optimizer,
|
237 | |}>,
|
238 | > {
|
239 | let optimizers = this.getOptimizerNames(filePath, pipeline);
|
240 | if (optimizers.length === 0) {
|
241 | return Promise.resolve([]);
|
242 | }
|
243 |
|
244 | return this.loadPlugins<Optimizer>(optimizers);
|
245 | }
|
246 |
|
247 | getReporters() {
|
248 | return this.loadPlugins<Reporter>(this.reporters);
|
249 | }
|
250 |
|
251 | isGlobMatch(filePath: FilePath, pattern: Glob, pipeline?: ?string) {
|
252 | let prefix = pipeline ? `${pipeline}:` : '';
|
253 | return (
|
254 | isMatch(prefix + filePath, pattern) ||
|
255 | isMatch(prefix + basename(filePath), pattern)
|
256 | );
|
257 | }
|
258 |
|
259 | matchGlobMap(filePath: FilePath, globMap: {[Glob]: any, ...}) {
|
260 | for (let pattern in globMap) {
|
261 | if (this.isGlobMatch(filePath, pattern)) {
|
262 | return globMap[pattern];
|
263 | }
|
264 | }
|
265 |
|
266 | return null;
|
267 | }
|
268 |
|
269 | matchGlobMapPipelines(
|
270 | filePath: FilePath,
|
271 | globMap: {[Glob]: Pipeline, ...},
|
272 | pipeline?: ?string,
|
273 | ) {
|
274 | let matches = [];
|
275 | for (let pattern in globMap) {
|
276 | if (this.isGlobMatch(filePath, pattern, pipeline)) {
|
277 | matches.push(globMap[pattern]);
|
278 | }
|
279 | }
|
280 |
|
281 | let flatten = () => {
|
282 | let pipeline = matches.shift() || [];
|
283 | let spreadIndex = pipeline.indexOf('...');
|
284 | if (spreadIndex >= 0) {
|
285 | pipeline = [
|
286 | ...pipeline.slice(0, spreadIndex),
|
287 | ...flatten(),
|
288 | ...pipeline.slice(spreadIndex + 1),
|
289 | ];
|
290 | }
|
291 |
|
292 | if (pipeline.includes('...')) {
|
293 | throw new Error(
|
294 | 'Only one spread parameter can be included in a config pipeline',
|
295 | );
|
296 | }
|
297 |
|
298 | return pipeline;
|
299 | };
|
300 |
|
301 | let res = flatten();
|
302 | return res;
|
303 | }
|
304 | }
|