Repository: parcel-bundler/parcel
File: packages/core/core/src/requests/AssetRequest.js (JavaScript)

Code before:
    type,
    filePath: input.filePath,
    env: input.env.id,
    isSource: input.isSource,
    sideEffects: input.sideEffects,
    code: input.code,
    pipeline: input.pipeline,
    query: input.query,
    invalidations: input.invalidations,
  });
}

async function run({input, api, options, farm}: RunInput) {
  let realpath = await options.inputFS.realpath(input.filePath);
  api.invalidateOnFileUpdate(realpath);
  let start = Date.now();
  let {optionsRef, ...request} = input;
  let {cachePath} = nullthrows(
    await api.runRequest<null, ConfigAndCachePath>(createParcelConfigRequest()),
  );

  // Add invalidations to the request if a node already exists in the graph.
  // These are used to compute the cache key for assets during transformation.
  request.invalidations = api.getInvalidations().filter(invalidation => {
    // Filter out invalidation node for the input file itself.
    return invalidation.type !== 'file' || invalidation.filePath !== realpath;
  });

  let {assets, configRequests, invalidations} = (await farm.createHandle(
    'runTransform',
  )({
    configCachePath: cachePath,
    optionsRef,
    request,
  }): TransformationResult);

  let time = Date.now() - start;
  for (let asset of assets) {
    asset.stats.time = time;
  }

  for (let invalidation of invalidations) {
    switch (invalidation.type) {
      case 'file':
        api.invalidateOnFileUpdate(invalidation.filePath);
        api.invalidateOnFileDelete(invalidation.filePath);
        break;
      case 'env':
        api.invalidateOnEnvChange(invalidation.key);
        break;
      case 'option':

Review comment (suggestion):
Subtle one: when a file was behind a symlink an extra invalidation was created (which needed to be read and hashed during transformation), because we compared the original path instead of the realpath (which is stored in the File node).

Code after:
    type,
    filePath: input.filePath,
    env: input.env.id,
    isSource: input.isSource,
    sideEffects: input.sideEffects,
    code: input.code,
    pipeline: input.pipeline,
    query: input.query ? objectSortedEntries(input.query) : null,
    invalidations: input.invalidations,
  });
}

async function run({input, api, options, farm}: RunInput) {
  let realpath = await options.inputFS.realpath(input.filePath);
  api.invalidateOnFileUpdate(realpath);
  let start = Date.now();
  let {optionsRef, ...request} = input;
  let {cachePath} = nullthrows(
    await api.runRequest<null, ConfigAndCachePath>(createParcelConfigRequest()),
  );

  // Add invalidations to the request if a node already exists in the graph.
  // These are used to compute the cache key for assets during transformation.
  request.invalidations = api.getInvalidations().filter(invalidation => {
    // Filter out invalidation node for the input file itself.
    return invalidation.type !== 'file' || invalidation.filePath !== realpath;
  });

  let {assets, configRequests, invalidations} = (await farm.createHandle(
    'runTransform',
  )({
    configCachePath: cachePath,
    optionsRef,
    request,
  }): TransformationResult);

  let time = Date.now() - start;
  for (let asset of assets) {
    asset.stats.time = time;
  }

  for (let invalidation of invalidations) {
    switch (invalidation.type) {
      case 'file':
        api.invalidateOnFileUpdate(invalidation.filePath);
        api.invalidateOnFileDelete(invalidation.filePath);
        break;
      case 'env':
        api.invalidateOnEnvChange(invalidation.key);
        break;
      case 'option':