nx
Version:
165 lines (164 loc) • 7.66 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.IntegerLikeSpreadKeyError = exports.INTEGER_LIKE_KEY_PATTERN = exports.NX_SPREAD_TOKEN = void 0;
exports.uniqueKeysInObjects = uniqueKeysInObjects;
exports.getMergeValueResult = getMergeValueResult;
const source_maps_1 = require("./source-maps");
exports.NX_SPREAD_TOKEN = '...';
/**
* Returns the union of keys across every provided object.
*/
function uniqueKeysInObjects(...objs) {
const keys = new Set();
for (const obj of objs) {
if (obj) {
for (const key of Object.keys(obj)) {
keys.add(key);
}
}
}
return keys;
}
// Integer-like string keys (`"0"`, `"42"`) are enumerated before
// insertion-order keys, so we can't tell if they were authored before or
// after `'...'`. Spread sites reject them instead of guessing.
exports.INTEGER_LIKE_KEY_PATTERN = /^(0|[1-9]\d*)$/;
class IntegerLikeSpreadKeyError extends Error {
constructor(key, context) {
super(`${context} uses an integer-like key (${JSON.stringify(key)}) alongside the '...' spread token. Integer-like keys are enumerated before other keys regardless of authored order, so their position relative to '...' is ambiguous. Rename the key (e.g. add a non-numeric prefix) or restructure the object.`);
this.name = 'IntegerLikeSpreadKeyError';
}
}
exports.IntegerLikeSpreadKeyError = IntegerLikeSpreadKeyError;
/**
* `"..."` in `newValue` (as an array element or a key set to `true`)
* expands the base at that position; otherwise `newValue` replaces
* `baseValue`. With `deferSpreadsWithoutBase`, an unresolvable spread is
* preserved so a later merge layer can expand it.
*/
function getMergeValueResult(baseValue, newValue, sourceMapContext, deferSpreadsWithoutBase) {
if (newValue === undefined && baseValue !== undefined) {
return baseValue;
}
if (Array.isArray(newValue)) {
return mergeArrayValue(baseValue, newValue, sourceMapContext, deferSpreadsWithoutBase);
}
if (isObject(newValue) && newValue[exports.NX_SPREAD_TOKEN] === true) {
return mergeObjectWithSpread(baseValue, newValue, sourceMapContext, deferSpreadsWithoutBase);
}
// Scalar / null / plain object replace — newValue fully wins.
writeTopLevelSourceMap(sourceMapContext);
return newValue;
}
function mergeArrayValue(baseValue, newValue, sourceMapContext, deferSpreadsWithoutBase) {
const newSpreadIndex = newValue.findIndex((v) => v === exports.NX_SPREAD_TOKEN);
if (newSpreadIndex === -1) {
// No spread: newValue replaces baseValue entirely.
if (sourceMapContext) {
for (let i = 0; i < newValue.length; i++) {
sourceMapContext.sourceMap[`${sourceMapContext.key}.${i}`] =
sourceMapContext.sourceInformation;
}
}
writeTopLevelSourceMap(sourceMapContext);
return newValue;
}
const baseArray = Array.isArray(baseValue) ? baseValue : [];
// Snapshot per-index base sources before we start writing — the loop
// writes into the same `${key}.${i}` entries it needs to read back when
// the spread expands. Unlike object spread, array spreads can overwrite
// indices during their own expansion (when new authors a prefix before
// `'...'`), so lazy capture isn't sufficient here.
const basePerIndexSources = sourceMapContext
? baseArray.map((_, i) => (0, source_maps_1.readArrayItemSourceInfo)(sourceMapContext.sourceMap, sourceMapContext.key, i))
: [];
const result = [];
const recordAt = (resultIdx, info) => {
if (sourceMapContext && info) {
sourceMapContext.sourceMap[`${sourceMapContext.key}.${resultIdx}`] = info;
}
};
for (let newValueIndex = 0; newValueIndex < newValue.length; newValueIndex++) {
const element = newValue[newValueIndex];
if (element === exports.NX_SPREAD_TOKEN) {
if (deferSpreadsWithoutBase && baseValue === undefined) {
recordAt(result.length, sourceMapContext?.sourceInformation);
result.push(exports.NX_SPREAD_TOKEN);
}
else {
for (let baseIndex = 0; baseIndex < baseArray.length; baseIndex++) {
recordAt(result.length, basePerIndexSources[baseIndex]);
result.push(baseArray[baseIndex]);
}
}
continue;
}
recordAt(result.length, sourceMapContext?.sourceInformation);
result.push(element);
}
writeTopLevelSourceMap(sourceMapContext);
return result;
}
function mergeObjectWithSpread(baseValue, newValue, sourceMapContext, deferSpreadsWithoutBase) {
const baseObj = isObject(baseValue) ? baseValue : {};
const result = {};
const errorContext = sourceMapContext?.key
? `Object at "${sourceMapContext.key}"`
: 'Object';
const newKeys = Object.keys(newValue);
// Integer-like keys are hoisted to the front of enumeration, so if one
// exists alongside `'...'` it must be newKeys[0].
if (newKeys[0] && exports.INTEGER_LIKE_KEY_PATTERN.test(newKeys[0])) {
throw new IntegerLikeSpreadKeyError(newKeys[0], errorContext);
}
// Base per-key sources captured lazily — only for shared keys the new
// object overwrites before `'...'`, since writing their new source
// clobbers the base entry the spread will need to restore.
const capturedBaseSources = {};
for (const newKey of newKeys) {
if (newKey === exports.NX_SPREAD_TOKEN) {
if (deferSpreadsWithoutBase && baseValue === undefined) {
// Keep the sentinel for a later merge layer to resolve.
result[exports.NX_SPREAD_TOKEN] = true;
continue;
}
for (const baseKey of Object.keys(baseObj)) {
result[baseKey] = baseObj[baseKey];
if (sourceMapContext) {
// If we captured a shared key pre-spread, use that; otherwise
// the source map still holds the base entry untouched.
const baseSource = Object.prototype.hasOwnProperty.call(capturedBaseSources, baseKey)
? capturedBaseSources[baseKey]
: (0, source_maps_1.readObjectPropertySourceInfo)(sourceMapContext.sourceMap, sourceMapContext.key, baseKey);
if (baseSource) {
sourceMapContext.sourceMap[`${sourceMapContext.key}.${baseKey}`] =
baseSource;
}
}
}
continue;
}
// About to overwrite a base key's source — capture it first so the
// spread can restore it.
if (sourceMapContext &&
newKey in baseObj &&
!Object.prototype.hasOwnProperty.call(capturedBaseSources, newKey)) {
capturedBaseSources[newKey] = (0, source_maps_1.readObjectPropertySourceInfo)(sourceMapContext.sourceMap, sourceMapContext.key, newKey);
}
result[newKey] = newValue[newKey];
if (sourceMapContext) {
sourceMapContext.sourceMap[`${sourceMapContext.key}.${newKey}`] =
sourceMapContext.sourceInformation;
}
}
writeTopLevelSourceMap(sourceMapContext);
return result;
}
function writeTopLevelSourceMap(ctx) {
if (ctx) {
ctx.sourceMap[ctx.key] = ctx.sourceInformation;
}
}
function isObject(value) {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}