1 |
|
2 |
|
3 | import type {
|
4 | AST,
|
5 | Blob,
|
6 | ConfigResult,
|
7 | DependencyOptions,
|
8 | File,
|
9 | FilePath,
|
10 | Meta,
|
11 | PackageJSON,
|
12 | Stats,
|
13 | Symbol,
|
14 | TransformerResult,
|
15 | } from '@parcel/types';
|
16 | import type {Asset, Dependency, Environment, ParcelOptions} from './types';
|
17 |
|
18 | import {Readable} from 'stream';
|
19 | import crypto from 'crypto';
|
20 | import SourceMap from '@parcel/source-map';
|
21 | import {
|
22 | bufferStream,
|
23 | loadConfig,
|
24 | md5FromString,
|
25 | blobToStream,
|
26 | TapStream,
|
27 | } from '@parcel/utils';
|
28 | import {createDependency, mergeDependencies} from './Dependency';
|
29 | import {mergeEnvironments, getEnvironmentHash} from './Environment';
|
30 | import {PARCEL_VERSION} from './constants';
|
31 |
|
32 | type AssetOptions = {|
|
33 | id?: string,
|
34 | hash?: ?string,
|
35 | idBase?: ?string,
|
36 | filePath: FilePath,
|
37 | type: string,
|
38 | contentKey?: ?string,
|
39 | mapKey?: ?string,
|
40 | dependencies?: Map<string, Dependency>,
|
41 | includedFiles?: Map<FilePath, File>,
|
42 | isIsolated?: boolean,
|
43 | isInline?: boolean,
|
44 | isSplittable?: ?boolean,
|
45 | isSource: boolean,
|
46 | outputHash?: string,
|
47 | env: Environment,
|
48 | meta?: Meta,
|
49 | pipeline?: ?string,
|
50 | stats: Stats,
|
51 | symbols?: Map<Symbol, Symbol>,
|
52 | sideEffects?: boolean,
|
53 | uniqueKey?: ?string,
|
54 | |};
|
55 |
|
56 | export function createAsset(options: AssetOptions): Asset {
|
57 | let idBase = options.idBase != null ? options.idBase : options.filePath;
|
58 | let uniqueKey = options.uniqueKey || '';
|
59 | return {
|
60 | id:
|
61 | options.id != null
|
62 | ? options.id
|
63 | : md5FromString(
|
64 | idBase + options.type + getEnvironmentHash(options.env) + uniqueKey,
|
65 | ),
|
66 | hash: options.hash,
|
67 | filePath: options.filePath,
|
68 | isIsolated: options.isIsolated == null ? false : options.isIsolated,
|
69 | isInline: options.isInline == null ? false : options.isInline,
|
70 | isSplittable: options.isSplittable,
|
71 | type: options.type,
|
72 | contentKey: options.contentKey,
|
73 | mapKey: options.mapKey,
|
74 | dependencies: options.dependencies || new Map(),
|
75 | includedFiles: options.includedFiles || new Map(),
|
76 | isSource: options.isSource,
|
77 | outputHash: options.outputHash || '',
|
78 | pipeline: options.pipeline,
|
79 | env: options.env,
|
80 | meta: options.meta || {},
|
81 | stats: options.stats,
|
82 | symbols: options.symbols || new Map(),
|
83 | sideEffects: options.sideEffects != null ? options.sideEffects : true,
|
84 | uniqueKey: uniqueKey,
|
85 | };
|
86 | }
|
87 |
|
88 | type InternalAssetOptions = {|
|
89 | value: Asset,
|
90 | options: ParcelOptions,
|
91 | content?: Blob,
|
92 | map?: ?SourceMap,
|
93 | ast?: ?AST,
|
94 | idBase?: ?string,
|
95 | |};
|
96 |
|
97 | export default class InternalAsset {
|
98 | value: Asset;
|
99 | options: ParcelOptions;
|
100 | content: Blob;
|
101 | map: ?SourceMap;
|
102 | ast: ?AST;
|
103 | idBase: ?string;
|
104 |
|
105 | constructor({
|
106 | value,
|
107 | options,
|
108 | content,
|
109 | map,
|
110 | ast,
|
111 | idBase,
|
112 | }: InternalAssetOptions) {
|
113 | this.value = value;
|
114 | this.options = options;
|
115 | this.content = content || '';
|
116 | this.map = map;
|
117 | this.ast = ast;
|
118 | this.idBase = idBase;
|
119 | }
|
120 |
|
121 | |
122 |
|
123 |
|
124 |
|
125 | async commit(pipelineKey: string): Promise<void> {
|
126 | this.ast = null;
|
127 |
|
128 | let contentStream = this.getStream();
|
129 | if (
|
130 |
|
131 | typeof contentStream.bytesRead === 'number' &&
|
132 |
|
133 |
|
134 | contentStream.bytesRead !== contentStream.readableLength
|
135 | ) {
|
136 | throw new Error(
|
137 | 'Stream has already been read. This may happen if a plugin reads from a stream and does not replace it.',
|
138 | );
|
139 | }
|
140 |
|
141 | let size = 0;
|
142 | let hash = crypto.createHash('md5');
|
143 |
|
144 |
|
145 |
|
146 | let [contentKey, mapKey] = await Promise.all([
|
147 | this.options.cache.setStream(
|
148 | this.getCacheKey('content' + pipelineKey),
|
149 | contentStream.pipe(
|
150 | new TapStream(buf => {
|
151 | size += buf.length;
|
152 | hash.update(buf);
|
153 | }),
|
154 | ),
|
155 | ),
|
156 | this.map == null
|
157 | ? Promise.resolve()
|
158 | : this.options.cache.set(
|
159 | this.getCacheKey('map' + pipelineKey),
|
160 | this.map,
|
161 | ),
|
162 | ]);
|
163 | this.value.contentKey = contentKey;
|
164 | this.value.mapKey = mapKey;
|
165 | this.value.stats.size = size;
|
166 | this.value.outputHash = hash.digest('hex');
|
167 | }
|
168 |
|
169 | async getCode(): Promise<string> {
|
170 | if (this.value.contentKey != null) {
|
171 | this.content = this.options.cache.getStream(this.value.contentKey);
|
172 | }
|
173 |
|
174 | if (typeof this.content === 'string' || this.content instanceof Buffer) {
|
175 | this.content = this.content.toString();
|
176 | } else {
|
177 | this.content = (await bufferStream(this.content)).toString();
|
178 | }
|
179 |
|
180 | return this.content;
|
181 | }
|
182 |
|
183 | async getBuffer(): Promise<Buffer> {
|
184 | if (this.value.contentKey != null) {
|
185 | this.content = this.options.cache.getStream(this.value.contentKey);
|
186 | }
|
187 |
|
188 | if (typeof this.content === 'string' || this.content instanceof Buffer) {
|
189 | return Buffer.from(this.content);
|
190 | }
|
191 |
|
192 | this.content = await bufferStream(this.content);
|
193 | return this.content;
|
194 | }
|
195 |
|
196 | getStream(): Readable {
|
197 | if (this.value.contentKey != null) {
|
198 | this.content = this.options.cache.getStream(this.value.contentKey);
|
199 | }
|
200 |
|
201 | return blobToStream(this.content);
|
202 | }
|
203 |
|
204 | setCode(code: string) {
|
205 | this.content = code;
|
206 | }
|
207 |
|
208 | setBuffer(buffer: Buffer) {
|
209 | this.content = buffer;
|
210 | }
|
211 |
|
212 | setStream(stream: Readable) {
|
213 | this.content = stream;
|
214 | }
|
215 |
|
216 | async getMap(): Promise<?SourceMap> {
|
217 | if (this.value.mapKey != null) {
|
218 | this.map = await this.options.cache.get(this.value.mapKey);
|
219 | }
|
220 |
|
221 | return this.map;
|
222 | }
|
223 |
|
224 | setMap(map: ?SourceMap): void {
|
225 | this.map = map;
|
226 | }
|
227 |
|
228 | getCacheKey(key: string): string {
|
229 | return md5FromString(
|
230 | PARCEL_VERSION + key + this.value.id + (this.value.hash || ''),
|
231 | );
|
232 | }
|
233 |
|
234 | addDependency(opts: DependencyOptions) {
|
235 |
|
236 | let {env, target, ...rest} = opts;
|
237 | let dep = createDependency({
|
238 | ...rest,
|
239 | env: mergeEnvironments(this.value.env, env),
|
240 | sourceAssetId: this.value.id,
|
241 | sourcePath: this.value.filePath,
|
242 | });
|
243 | let existing = this.value.dependencies.get(dep.id);
|
244 | if (existing) {
|
245 | mergeDependencies(existing, dep);
|
246 | } else {
|
247 | this.value.dependencies.set(dep.id, dep);
|
248 | }
|
249 | return dep.id;
|
250 | }
|
251 |
|
252 | addIncludedFile(file: File) {
|
253 | this.value.includedFiles.set(file.filePath, file);
|
254 | }
|
255 |
|
256 | getIncludedFiles(): Array<File> {
|
257 | return Array.from(this.value.includedFiles.values());
|
258 | }
|
259 |
|
260 | getDependencies(): Array<Dependency> {
|
261 | return Array.from(this.value.dependencies.values());
|
262 | }
|
263 |
|
264 | createChildAsset(result: TransformerResult): InternalAsset {
|
265 | let content = result.content ?? result.code ?? '';
|
266 |
|
267 | let hash;
|
268 | let size;
|
269 | if (content === this.content) {
|
270 | hash = this.value.hash;
|
271 | size = this.value.stats.size;
|
272 | } else if (typeof content === 'string' || content instanceof Buffer) {
|
273 | hash = md5FromString(content);
|
274 | size = content.length;
|
275 | } else {
|
276 | hash = null;
|
277 | size = NaN;
|
278 | }
|
279 |
|
280 | let asset = new InternalAsset({
|
281 | value: createAsset({
|
282 | idBase: this.idBase,
|
283 | hash,
|
284 | filePath: this.value.filePath,
|
285 | type: result.type,
|
286 | isIsolated: result.isIsolated ?? this.value.isIsolated,
|
287 | isInline: result.isInline ?? this.value.isInline,
|
288 | isSplittable: result.isSplittable ?? this.value.isSplittable,
|
289 | isSource: result.isSource ?? this.value.isSource,
|
290 | env: mergeEnvironments(this.value.env, result.env),
|
291 | dependencies:
|
292 | this.value.type === result.type
|
293 | ? new Map(this.value.dependencies)
|
294 | : new Map(),
|
295 | includedFiles: new Map(this.value.includedFiles),
|
296 | meta: {
|
297 | ...this.value.meta,
|
298 |
|
299 | ...result.meta,
|
300 | },
|
301 | pipeline:
|
302 | result.pipeline ??
|
303 | (this.value.type === result.type ? this.value.pipeline : null),
|
304 | stats: {
|
305 | time: 0,
|
306 | size,
|
307 | },
|
308 | symbols: new Map([...this.value.symbols, ...(result.symbols || [])]),
|
309 | sideEffects: result.sideEffects ?? this.value.sideEffects,
|
310 | uniqueKey: result.uniqueKey,
|
311 | }),
|
312 | options: this.options,
|
313 | content,
|
314 | ast: result.ast,
|
315 | map: result.map,
|
316 | idBase: this.idBase,
|
317 | });
|
318 |
|
319 | let dependencies = result.dependencies;
|
320 | if (dependencies) {
|
321 | for (let dep of dependencies) {
|
322 | asset.addDependency(dep);
|
323 | }
|
324 | }
|
325 |
|
326 | let includedFiles = result.includedFiles;
|
327 | if (includedFiles) {
|
328 | for (let file of includedFiles) {
|
329 | asset.addIncludedFile(file);
|
330 | }
|
331 | }
|
332 |
|
333 | return asset;
|
334 | }
|
335 |
|
336 | async getConfig(
|
337 | filePaths: Array<FilePath>,
|
338 | options: ?{|
|
339 | packageKey?: string,
|
340 | parse?: boolean,
|
341 | |},
|
342 | ): Promise<ConfigResult | null> {
|
343 | let packageKey = options?.packageKey;
|
344 | let parse = options && options.parse;
|
345 |
|
346 | if (packageKey != null) {
|
347 | let pkg = await this.getPackage();
|
348 | if (pkg && pkg[packageKey]) {
|
349 | return pkg[packageKey];
|
350 | }
|
351 | }
|
352 |
|
353 | let conf = await loadConfig(
|
354 | this.options.inputFS,
|
355 | this.value.filePath,
|
356 | filePaths,
|
357 | parse == null ? null : {parse},
|
358 | );
|
359 | if (!conf) {
|
360 | return null;
|
361 | }
|
362 |
|
363 | for (let file of conf.files) {
|
364 | this.addIncludedFile(file);
|
365 | }
|
366 |
|
367 | return conf.config;
|
368 | }
|
369 |
|
370 | getPackage(): Promise<PackageJSON | null> {
|
371 | return this.getConfig(['package.json']);
|
372 | }
|
373 | }
|