UNPKG

@parcel/core

Version:
430 lines (378 loc) • 11.6 kB
// @flow strict-local import type { AST, Blob, DependencyOptions, FileCreateInvalidation, GenerateOutput, PackageName, TransformerResult, } from '@parcel/types'; import type {Asset, Dependency, ParcelOptions, Invalidations} from './types'; import invariant from 'assert'; import {Readable} from 'stream'; import SourceMap from '@parcel/source-map'; import { blobToStream, bufferStream, streamFromPromise, TapStream, loadSourceMap, SOURCEMAP_RE, } from '@parcel/utils'; import {hashString, hashBuffer, Hash} from '@parcel/rust'; import {serializeRaw} from './serializer'; import {createDependency, mergeDependencies} from './Dependency'; import {mergeEnvironments} from './Environment'; import {PARCEL_VERSION} from './constants'; import {createAsset, createAssetIdFromOptions} from './assetUtils'; import {BundleBehaviorNames} from './types'; import {invalidateOnFileCreateToInternal, createInvalidations} from './utils'; import {type ProjectPath, fromProjectPath} from './projectPath'; type UncommittedAssetOptions = {| value: Asset, options: ParcelOptions, content?: ?Blob, mapBuffer?: ?Buffer, ast?: ?AST, isASTDirty?: ?boolean, idBase?: ?string, invalidations?: Invalidations, |}; export default class UncommittedAsset { value: Asset; options: ParcelOptions; content: ?(Blob | Promise<Buffer>); mapBuffer: ?Buffer; sourceContent: ?string; map: ?SourceMap; ast: ?AST; isASTDirty: boolean; idBase: ?string; invalidations: Invalidations; generate: ?() => Promise<GenerateOutput>; transformers: Set<string>; constructor({ value, options, content, mapBuffer, ast, isASTDirty, idBase, invalidations, }: UncommittedAssetOptions) { this.value = value; this.options = options; this.content = content; this.mapBuffer = mapBuffer; this.ast = ast; this.isASTDirty = isASTDirty || false; this.idBase = idBase; this.invalidations = invalidations || createInvalidations(); this.transformers = new Set(); } /* * Prepares the asset for being serialized to the cache by committing its * content and map of the asset to the cache. */ async commit(): Promise<void> { // If there is a dirty AST, clear out any old content and map as these // must be regenerated later and shouldn't be committed. if (this.ast != null && this.isASTDirty) { this.content = null; this.mapBuffer = null; } let size = 0; let outputHash = ''; let contentKey = this.content == null ? null : this.getCacheKey('content'); let mapKey = this.mapBuffer == null ? null : this.getCacheKey('map'); let astKey = this.ast == null ? null : this.getCacheKey('ast'); // Since we can only read from the stream once, compute the content length // and hash while it's being written to the cache. await Promise.all([ contentKey != null && this.commitContent(contentKey).then( s => ((size = s.size), (outputHash = s.hash)), ), this.mapBuffer != null && mapKey != null && this.options.cache.setBlob(mapKey, this.mapBuffer), astKey != null && this.options.cache.setBlob(astKey, serializeRaw(this.ast)), ]); this.value.contentKey = contentKey; this.value.mapKey = mapKey; this.value.astKey = astKey; this.value.outputHash = outputHash; if (this.content != null) { this.value.stats.size = size; } this.value.isLargeBlob = this.content instanceof Readable; this.value.committed = true; } async commitContent( contentKey: string, ): Promise<{|size: number, hash: string|}> { let content = await this.content; if (content == null) { return {size: 0, hash: ''}; } let size = 0; if (content instanceof Readable) { let hash = new Hash(); await this.options.cache.setStream( contentKey, content.pipe( new TapStream(buf => { hash.writeBuffer(buf); size += buf.length; }), ), ); return {size, hash: hash.finish()}; } let hash; if (typeof content === 'string') { hash = hashString(content); size = Buffer.byteLength(content); } else { hash = hashBuffer(content); size = content.length; } await this.options.cache.setBlob(contentKey, content); return {size, hash}; } async getCode(): Promise<string> { if (this.ast != null && this.isASTDirty) { throw new Error( 'Cannot call getCode() on an asset with a dirty AST. For transformers, implement canReuseAST() and check asset.isASTDirty.', ); } let content = await this.content; if (typeof content === 'string' || content instanceof Buffer) { return content.toString(); } else if (content != null) { this.content = bufferStream(content); return (await this.content).toString(); } invariant(false, 'Internal error: missing content'); } async getBuffer(): Promise<Buffer> { let content = await this.content; if (content == null) { return Buffer.alloc(0); } else if (content instanceof Buffer) { return content; } else if (typeof content === 'string') { return Buffer.from(content); } this.content = bufferStream(content); return this.content; } getStream(): Readable { if (this.content instanceof Readable) { // Remove content if it's a stream, as it should not be reused. let content = this.content; this.content = null; return content; } if (this.content instanceof Promise) { return streamFromPromise(this.content); } return blobToStream(this.content ?? Buffer.alloc(0)); } setCode(code: string) { this.content = code; this.clearAST(); } setBuffer(buffer: Buffer) { this.content = buffer; this.clearAST(); } setStream(stream: Readable) { this.content = stream; this.clearAST(); } async loadExistingSourcemap(): Promise<?SourceMap> { if (this.map) { return this.map; } let code = await this.getCode(); let map = await loadSourceMap( fromProjectPath(this.options.projectRoot, this.value.filePath), code, { fs: this.options.inputFS, projectRoot: this.options.projectRoot, }, ); if (map) { this.map = map; this.mapBuffer = map.toBuffer(); this.setCode(code.replace(SOURCEMAP_RE, '')); } return this.map; } getMapBuffer(): Promise<?Buffer> { return Promise.resolve(this.mapBuffer); } async getMap(): Promise<?SourceMap> { if (this.map == null) { let mapBuffer = this.mapBuffer ?? (await this.getMapBuffer()); if (mapBuffer) { // Get sourcemap from flatbuffer this.map = new SourceMap(this.options.projectRoot, mapBuffer); } } return this.map; } setMap(map: ?SourceMap): void { // If we have sourceContent available, it means this asset is source code without // a previous source map. Ensure that the map set by the transformer has the original // source content available. if (map != null && this.sourceContent != null) { map.setSourceContent( fromProjectPath(this.options.projectRoot, this.value.filePath), // $FlowFixMe this.sourceContent, ); this.sourceContent = null; } this.map = map; this.mapBuffer = this.map?.toBuffer(); } getAST(): Promise<?AST> { return Promise.resolve(this.ast); } setAST(ast: AST): void { this.ast = ast; this.isASTDirty = true; this.value.astGenerator = { type: ast.type, version: ast.version, }; } clearAST() { this.ast = null; this.isASTDirty = false; this.value.astGenerator = null; } getCacheKey(key: string): string { return hashString(PARCEL_VERSION + key + this.value.id); } addDependency(opts: DependencyOptions): string { // eslint-disable-next-line no-unused-vars let {env, symbols, ...rest} = opts; let dep = createDependency(this.options.projectRoot, { ...rest, // $FlowFixMe "convert" the $ReadOnlyMaps to the interal mutable one symbols, env: mergeEnvironments(this.options.projectRoot, this.value.env, env), sourceAssetId: this.value.id, sourcePath: fromProjectPath( this.options.projectRoot, this.value.filePath, ), }); let existing = this.value.dependencies.get(dep.id); if (existing) { mergeDependencies(existing, dep); } else { this.value.dependencies.set(dep.id, dep); } return dep.id; } invalidateOnFileChange(filePath: ProjectPath) { this.invalidations.invalidateOnFileChange.add(filePath); } invalidateOnFileCreate(invalidation: FileCreateInvalidation) { this.invalidations.invalidateOnFileCreate.push( invalidateOnFileCreateToInternal(this.options.projectRoot, invalidation), ); } invalidateOnEnvChange(key: string) { this.invalidations.invalidateOnEnvChange.add(key); } invalidateOnBuild() { this.invalidations.invalidateOnBuild = true; } invalidateOnStartup() { this.invalidations.invalidateOnStartup = true; } getDependencies(): Array<Dependency> { return Array.from(this.value.dependencies.values()); } createChildAsset( result: TransformerResult, plugin: PackageName, configPath: ProjectPath, configKeyPath?: string, ): UncommittedAsset { let content = result.content ?? null; let asset = new UncommittedAsset({ value: createAsset(this.options.projectRoot, { idBase: this.idBase, filePath: this.value.filePath, type: result.type, bundleBehavior: result.bundleBehavior === undefined ? this.value.bundleBehavior == null ? null : BundleBehaviorNames[this.value.bundleBehavior] : result.bundleBehavior, isBundleSplittable: result.isBundleSplittable ?? this.value.isBundleSplittable, isSource: this.value.isSource, env: mergeEnvironments( this.options.projectRoot, this.value.env, result.env, ), dependencies: this.value.type === result.type ? new Map(this.value.dependencies) : new Map(), meta: { ...this.value.meta, ...result.meta, }, pipeline: result.pipeline ?? (this.value.type === result.type ? this.value.pipeline : null), stats: { time: 0, size: this.value.stats.size, }, // $FlowFixMe symbols: result.symbols, sideEffects: result.sideEffects ?? this.value.sideEffects, uniqueKey: result.uniqueKey, astGenerator: result.ast ? {type: result.ast.type, version: result.ast.version} : null, plugin, configPath, configKeyPath, }), options: this.options, content, ast: result.ast, isASTDirty: result.ast === this.ast ? this.isASTDirty : true, mapBuffer: result.map ? result.map.toBuffer() : null, idBase: this.idBase, invalidations: this.invalidations, }); let dependencies = result.dependencies; if (dependencies) { for (let dep of dependencies) { asset.addDependency(dep); } } return asset; } updateId() { // $FlowFixMe - this is fine this.value.id = createAssetIdFromOptions(this.value); } }