UNPKG

9.63 kBJavaScriptView Raw
1// @flow strict-local
2
3import 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';
16import type {Asset, Dependency, Environment, ParcelOptions} from './types';
17
18import {Readable} from 'stream';
19import crypto from 'crypto';
20import SourceMap from '@parcel/source-map';
21import {
22 bufferStream,
23 loadConfig,
24 md5FromString,
25 blobToStream,
26 TapStream,
27} from '@parcel/utils';
28import {createDependency, mergeDependencies} from './Dependency';
29import {mergeEnvironments, getEnvironmentHash} from './Environment';
30import {PARCEL_VERSION} from './constants';
31
32type 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
56export 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
88type InternalAssetOptions = {|
89 value: Asset,
90 options: ParcelOptions,
91 content?: Blob,
92 map?: ?SourceMap,
93 ast?: ?AST,
94 idBase?: ?string,
95|};
96
97export 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 * Prepares the asset for being serialized to the cache by commiting its
123 * content and map of the asset to the cache.
124 */
125 async commit(pipelineKey: string): Promise<void> {
126 this.ast = null;
127
128 let contentStream = this.getStream();
129 if (
130 // $FlowFixMe
131 typeof contentStream.bytesRead === 'number' &&
132 // If the amount of data read from this stream so far isn't exactly the amount
133 // of data that is available to be read, then it has been read from.
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 // Since we can only read from the stream once, compute the content length
145 // and hash while it's being written to the cache.
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 // eslint-disable-next-line no-unused-vars
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 // $FlowFixMe
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}