@parcel/core
Version:
257 lines (237 loc) • 7.06 kB
JavaScript
// @flow strict-local
import type {
ASTGenerator,
BundleBehavior,
FilePath,
GenerateOutput,
Meta,
PackageName,
Stats,
Symbol,
SourceLocation,
Transformer,
} from '@parcel/types';
import type {
Asset,
RequestInvalidation,
Dependency,
Environment,
ParcelOptions,
} from './types';
import {Readable} from 'stream';
import {PluginLogger} from '@parcel/logger';
import nullthrows from 'nullthrows';
import CommittedAsset from './CommittedAsset';
import UncommittedAsset from './UncommittedAsset';
import loadPlugin from './loadParcelPlugin';
import {Asset as PublicAsset} from './public/Asset';
import PluginOptions from './public/PluginOptions';
import {blobToStream, hashFile} from '@parcel/utils';
import {hashFromOption, toInternalSourceLocation} from './utils';
import {createBuildCache} from './buildCache';
import {
type ProjectPath,
fromProjectPath,
fromProjectPathRelative,
} from './projectPath';
import {hashString} from '@parcel/hash';
import {BundleBehavior as BundleBehaviorMap} from './types';
import {PluginTracer} from '@parcel/profiler';
type AssetOptions = {|
id?: string,
committed?: boolean,
hash?: ?string,
idBase?: ?string,
filePath: ProjectPath,
query?: ?string,
type: string,
contentKey?: ?string,
mapKey?: ?string,
astKey?: ?string,
astGenerator?: ?ASTGenerator,
dependencies?: Map<string, Dependency>,
bundleBehavior?: ?BundleBehavior,
isBundleSplittable?: ?boolean,
isSource: boolean,
env: Environment,
meta?: Meta,
outputHash?: ?string,
pipeline?: ?string,
stats: Stats,
symbols?: ?Map<Symbol, {|local: Symbol, loc: ?SourceLocation, meta?: ?Meta|}>,
sideEffects?: boolean,
uniqueKey?: ?string,
plugin?: PackageName,
configPath?: ProjectPath,
configKeyPath?: string,
|};
export function createAssetIdFromOptions(options: AssetOptions): string {
let uniqueKey = options.uniqueKey ?? '';
let idBase =
options.idBase != null
? options.idBase
: fromProjectPathRelative(options.filePath);
return hashString(
idBase +
options.type +
options.env.id +
uniqueKey +
':' +
(options.pipeline ?? '') +
':' +
(options.query ?? ''),
);
}
export function createAsset(
projectRoot: FilePath,
options: AssetOptions,
): Asset {
return {
id: options.id != null ? options.id : createAssetIdFromOptions(options),
committed: options.committed ?? false,
hash: options.hash,
filePath: options.filePath,
query: options.query,
bundleBehavior: options.bundleBehavior
? BundleBehaviorMap[options.bundleBehavior]
: null,
isBundleSplittable: options.isBundleSplittable ?? true,
type: options.type,
contentKey: options.contentKey,
mapKey: options.mapKey,
astKey: options.astKey,
astGenerator: options.astGenerator,
dependencies: options.dependencies || new Map(),
isSource: options.isSource,
outputHash: options.outputHash,
pipeline: options.pipeline,
env: options.env,
meta: options.meta || {},
stats: options.stats,
symbols:
options.symbols &&
new Map(
[...options.symbols].map(([k, v]) => [
k,
{
local: v.local,
meta: v.meta,
loc: toInternalSourceLocation(projectRoot, v.loc),
},
]),
),
sideEffects: options.sideEffects ?? true,
uniqueKey: options.uniqueKey,
plugin: options.plugin,
configPath: options.configPath,
configKeyPath: options.configKeyPath,
};
}
const generateResults: WeakMap<Asset, Promise<GenerateOutput>> = new WeakMap();
export function generateFromAST(
asset: CommittedAsset | UncommittedAsset,
): Promise<GenerateOutput> {
let output = generateResults.get(asset.value);
if (output == null) {
output = _generateFromAST(asset);
generateResults.set(asset.value, output);
}
return output;
}
async function _generateFromAST(asset: CommittedAsset | UncommittedAsset) {
let ast = await asset.getAST();
if (ast == null) {
throw new Error('Asset has no AST');
}
let pluginName = nullthrows(asset.value.plugin);
let {plugin} = await loadPlugin<Transformer<mixed>>(
pluginName,
fromProjectPath(
asset.options.projectRoot,
nullthrows(asset.value.configPath),
),
nullthrows(asset.value.configKeyPath),
asset.options,
);
let generate = plugin.generate?.bind(plugin);
if (!generate) {
throw new Error(`${pluginName} does not have a generate method`);
}
let {content, map} = await generate({
asset: new PublicAsset(asset),
ast,
options: new PluginOptions(asset.options),
logger: new PluginLogger({origin: pluginName}),
tracer: new PluginTracer({origin: pluginName, category: 'asset-generate'}),
});
let mapBuffer = map?.toBuffer();
// Store the results in the cache so we can avoid generating again next time
await Promise.all([
asset.options.cache.setStream(
nullthrows(asset.value.contentKey),
blobToStream(content),
),
mapBuffer != null &&
asset.options.cache.setBlob(nullthrows(asset.value.mapKey), mapBuffer),
]);
return {
content:
content instanceof Readable
? asset.options.cache.getStream(nullthrows(asset.value.contentKey))
: content,
map,
};
}
export function getInvalidationId(invalidation: RequestInvalidation): string {
switch (invalidation.type) {
case 'file':
return 'file:' + fromProjectPathRelative(invalidation.filePath);
case 'env':
return 'env:' + invalidation.key;
case 'option':
return 'option:' + invalidation.key;
default:
throw new Error('Unknown invalidation type: ' + invalidation.type);
}
}
const hashCache = createBuildCache();
export async function getInvalidationHash(
invalidations: Array<RequestInvalidation>,
options: ParcelOptions,
): Promise<string> {
if (invalidations.length === 0) {
return '';
}
let sortedInvalidations = invalidations
.slice()
.sort((a, b) => (getInvalidationId(a) < getInvalidationId(b) ? -1 : 1));
let hashes = '';
for (let invalidation of sortedInvalidations) {
switch (invalidation.type) {
case 'file': {
// Only recompute the hash of this file if we haven't seen it already during this build.
let fileHash = hashCache.get(invalidation.filePath);
if (fileHash == null) {
fileHash = hashFile(
options.inputFS,
fromProjectPath(options.projectRoot, invalidation.filePath),
);
hashCache.set(invalidation.filePath, fileHash);
}
hashes += await fileHash;
break;
}
case 'env':
hashes +=
invalidation.key + ':' + (options.env[invalidation.key] || '');
break;
case 'option':
hashes +=
invalidation.key + ':' + hashFromOption(options[invalidation.key]);
break;
default:
throw new Error('Unknown invalidation type: ' + invalidation.type);
}
}
return hashString(hashes);
}