diff --git a/.pnp.cjs b/.pnp.cjs index 8cbafe68a20..878944eeef1 100755 --- a/.pnp.cjs +++ b/.pnp.cjs @@ -4874,6 +4874,15 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["@fastify/deepmerge", [\ + ["npm:1.3.0", {\ + "packageLocation": "./.yarn/cache/@fastify-deepmerge-npm-1.3.0-72eb1f634c-6ddfc230ed.zip/node_modules/@fastify/deepmerge/",\ + "packageDependencies": [\ + ["@fastify/deepmerge", "npm:1.3.0"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["@floating-ui/core", [\ ["npm:1.3.1", {\ "packageLocation": "./.yarn/cache/@floating-ui-core-npm-1.3.1-d31cabf485-b0aeb50831.zip/node_modules/@floating-ui/core/",\ @@ -8772,6 +8781,7 @@ const RAW_RUNTIME_STATE = "packageDependencies": [\ ["@yarnpkg/core", "workspace:packages/yarnpkg-core"],\ ["@arcanis/slice-ansi", "npm:1.1.1"],\ + ["@fastify/deepmerge", "npm:1.3.0"],\ ["@octokit/webhooks-types", "npm:7.3.1"],\ ["@rollup/plugin-commonjs", "virtual:712d04b0098634bdb13868ff8f85b327022bd7d3880873ada8c0ae56847ed36cf9da1fd74a88519380129cec528fe2bd2201426bc28ac9d4a8cc6734ff25c538#npm:21.0.1"],\ ["@rollup/plugin-json", "virtual:712d04b0098634bdb13868ff8f85b327022bd7d3880873ada8c0ae56847ed36cf9da1fd74a88519380129cec528fe2bd2201426bc28ac9d4a8cc6734ff25c538#npm:6.0.0"],\ @@ -8806,6 +8816,7 @@ const RAW_RUNTIME_STATE = "esbuild-wasm",\ "npm:0.15.15"\ ]],\ + ["fast-deep-equal", "npm:3.1.3"],\ ["fast-glob", "npm:3.2.12"],\ ["got", "npm:11.8.2"],\ ["lodash", "npm:4.17.21"],\ diff --git a/.yarn/cache/@fastify-deepmerge-npm-1.3.0-72eb1f634c-6ddfc230ed.zip b/.yarn/cache/@fastify-deepmerge-npm-1.3.0-72eb1f634c-6ddfc230ed.zip new file mode 100644 index 00000000000..00d64b34be6 Binary files /dev/null and b/.yarn/cache/@fastify-deepmerge-npm-1.3.0-72eb1f634c-6ddfc230ed.zip differ diff --git a/packages/plugin-constraints/sources/constraintUtils.ts b/packages/plugin-constraints/sources/constraintUtils.ts index e25067efeb3..6d52df92feb 100644 --- a/packages/plugin-constraints/sources/constraintUtils.ts +++ b/packages/plugin-constraints/sources/constraintUtils.ts @@ -1,9 +1,5 @@ import {Configuration, formatUtils, Manifest, miscUtils, nodeUtils, Project, treeUtils, Workspace} from '@yarnpkg/core'; import {PortablePath} from '@yarnpkg/fslib'; -import get from 'lodash/get'; -import set from 'lodash/set'; -import toPath from 'lodash/toPath'; -import unset from 'lodash/unset'; export type ProcessResult = { manifestUpdates: Map>>>; @@ -125,7 +121,7 @@ function isKnownDict(parts: Array, index: number) { export function normalizePath(p: Array | string) { const parts = Array.isArray(p) ? p - : toPath(p); + : miscUtils.toPath(p); const normalizedParts = parts.map((part, t) => { if (numberRegExp.test(part)) @@ -202,7 +198,7 @@ export function applyEngineReport(project: Project, {manifestUpdates, reportedEr } else { const [[newValue]] = newValues; - const currentValue = get(manifest, fieldPath); + const currentValue = miscUtils.get(manifest, fieldPath); if (JSON.stringify(currentValue) === JSON.stringify(newValue)) continue; @@ -218,9 +214,9 @@ export function applyEngineReport(project: Project, {manifestUpdates, reportedEr } if (typeof newValue === `undefined`) - unset(manifest, fieldPath); + miscUtils.unset(manifest, fieldPath); else - set(manifest, fieldPath, newValue); + miscUtils.set(manifest, fieldPath, newValue); changedWorkspace = true; } diff --git a/packages/plugin-constraints/sources/tauModule.ts b/packages/plugin-constraints/sources/tauModule.ts index 2212675fc11..ec4b1a6f4f8 100644 --- a/packages/plugin-constraints/sources/tauModule.ts +++ b/packages/plugin-constraints/sources/tauModule.ts @@ -1,10 +1,9 @@ /// -import {Project, structUtils} from '@yarnpkg/core'; -import {PortablePath} from '@yarnpkg/fslib'; -import getPath from 'lodash/get'; -import pl from 'tau-prolog'; -import vm from 'vm'; +import {Project, miscUtils, structUtils} from '@yarnpkg/core'; +import {PortablePath} from '@yarnpkg/fslib'; +import pl from 'tau-prolog'; +import vm from 'vm'; // eslint-disable-next-line @typescript-eslint/naming-convention const {is_atom: isAtom, is_variable: isVariable, is_instantiated_list: isInstantiatedList} = pl.type; @@ -78,7 +77,7 @@ const tauModule = new pl.type.Module(`constraints`, { if (workspace == null) return; - const value = getPath(workspace.manifest.raw!, fieldName.id); + const value = miscUtils.get(workspace.manifest.raw!, miscUtils.toPath(fieldName.id)); // Field is not present => this predicate can never match if (typeof value === `undefined`) @@ -125,7 +124,7 @@ const tauModule = new pl.type.Module(`constraints`, { if (workspace == null) return; - const value = getPath(workspace.manifest.raw!, fieldName.id); + const value = miscUtils.get(workspace.manifest.raw!, miscUtils.toPath(fieldName.id)); // Field is not present => this predicate can never match if (typeof value === `undefined`) diff --git a/packages/plugin-essentials/sources/commands/config/get.ts b/packages/plugin-essentials/sources/commands/config/get.ts index c2de800151c..b6e712516f5 100644 --- a/packages/plugin-essentials/sources/commands/config/get.ts +++ b/packages/plugin-essentials/sources/commands/config/get.ts @@ -1,7 +1,6 @@ import {BaseCommand} from '@yarnpkg/cli'; import {Configuration, StreamReport, miscUtils} from '@yarnpkg/core'; import {Command, Option, Usage, UsageError} from 'clipanion'; -import getPath from 'lodash/get'; import {inspect} from 'util'; // eslint-disable-next-line arca/no-default-export @@ -66,7 +65,7 @@ export default class ConfigGetCommand extends BaseCommand { const asObject = miscUtils.convertMapsToIndexableObjects(displayedValue); const requestedObject = path - ? getPath(asObject, path) + ? miscUtils.get(asObject, miscUtils.toPath(path)) : asObject; const report = await StreamReport.start({ diff --git a/packages/plugin-essentials/sources/commands/config/set.ts b/packages/plugin-essentials/sources/commands/config/set.ts index 6ff4b2cb728..06201a1c383 100644 --- a/packages/plugin-essentials/sources/commands/config/set.ts +++ b/packages/plugin-essentials/sources/commands/config/set.ts @@ -1,9 +1,6 @@ import {BaseCommand} from '@yarnpkg/cli'; import {Configuration, StreamReport, MessageName, miscUtils} from '@yarnpkg/core'; import {Command, Option, Usage, UsageError} from 'clipanion'; -import cloneDeep from 'lodash/cloneDeep'; -import getPath from 'lodash/get'; -import setPath from 'lodash/set'; import {inspect} from 'util'; // eslint-disable-next-line arca/no-default-export @@ -84,8 +81,8 @@ export default class ConfigSetCommand extends BaseCommand { await updateConfiguration(current => { if (path) { - const clone = cloneDeep(current); - setPath(clone, this.name, value); + const clone = miscUtils.cloneDeep(current); + miscUtils.set(clone, miscUtils.toPath(this.name), value); return clone; } else { return { @@ -103,7 +100,7 @@ export default class ConfigSetCommand extends BaseCommand { const asObject = miscUtils.convertMapsToIndexableObjects(displayedValue); const requestedObject = path - ? getPath(asObject, path) + ? miscUtils.get(asObject, miscUtils.toPath(path)) : asObject; const report = await StreamReport.start({ diff --git a/packages/plugin-essentials/sources/commands/config/unset.ts b/packages/plugin-essentials/sources/commands/config/unset.ts index 47d0166eb90..dfce4ac76c9 100644 --- a/packages/plugin-essentials/sources/commands/config/unset.ts +++ b/packages/plugin-essentials/sources/commands/config/unset.ts @@ -1,9 +1,6 @@ -import {BaseCommand} from '@yarnpkg/cli'; -import {Configuration, StreamReport, MessageName} from '@yarnpkg/core'; -import {Command, Option, Usage, UsageError} from 'clipanion'; -import cloneDeep from 'lodash/cloneDeep'; -import hasPath from 'lodash/has'; -import unsetPath from 'lodash/unset'; +import {BaseCommand} from '@yarnpkg/cli'; +import {Configuration, StreamReport, MessageName, miscUtils} from '@yarnpkg/core'; +import {Command, Option, Usage, UsageError} from 'clipanion'; // eslint-disable-next-line arca/no-default-export export default class ConfigUnsetCommand extends BaseCommand { @@ -63,17 +60,17 @@ export default class ConfigUnsetCommand extends BaseCommand { }, async report => { let bailedOutEarly = false; await updateConfiguration(current => { - if (!hasPath(current, this.name)) { + if (!miscUtils.hasPath(current, miscUtils.toPath(this.name))) { report.reportWarning(MessageName.UNNAMED, `Configuration doesn't contain setting ${this.name}; there is nothing to unset`); bailedOutEarly = true; return current; } const clone = path - ? cloneDeep(current) + ? miscUtils.cloneDeep(current) : {...current}; - unsetPath(clone, this.name); + miscUtils.unset(clone, miscUtils.toPath(this.name)); return clone; }); diff --git a/packages/plugin-git/sources/gitUtils.ts b/packages/plugin-git/sources/gitUtils.ts index 9be7af23e09..02cc04de311 100644 --- a/packages/plugin-git/sources/gitUtils.ts +++ b/packages/plugin-git/sources/gitUtils.ts @@ -2,7 +2,6 @@ import {Configuration, Hooks, Locator, Project, execUtils, httpUtils, miscUtils, import {Filename, npath, PortablePath, ppath, xfs} from '@yarnpkg/fslib'; import {UsageError} from 'clipanion'; import GitUrlParse from 'git-url-parse'; -import capitalize from 'lodash/capitalize'; import querystring from 'querystring'; import semver from 'semver'; @@ -393,7 +392,7 @@ async function git(message: string, args: Array, opts: Omit { + if (typeof searchIndex !== `undefined`) + return searchIndex; + + const {default: algoliasearch} = await import(`algoliasearch`); + searchIndex = algoliasearch(algolia.appId, algolia.apiKey).initIndex(algolia.indexName); + + return searchIndex; +}; export interface AlgoliaPackage { objectID: string; @@ -34,6 +42,7 @@ export const search = async ( query: string, page: number = 0, ) => { + const client = await getSearchIndex(); const res = await client.search( query, { diff --git a/packages/plugin-npm/sources/npmHttpUtils.ts b/packages/plugin-npm/sources/npmHttpUtils.ts index 32a6bd40ce3..821738b8e6d 100644 --- a/packages/plugin-npm/sources/npmHttpUtils.ts +++ b/packages/plugin-npm/sources/npmHttpUtils.ts @@ -1,8 +1,6 @@ import {Configuration, Ident, formatUtils, httpUtils, nodeUtils, StreamReport, structUtils, hashUtils, Project, miscUtils, Cache} from '@yarnpkg/core'; import {MessageName, ReportError} from '@yarnpkg/core'; import {Filename, PortablePath, ppath, xfs} from '@yarnpkg/fslib'; -import {prompt} from 'enquirer'; -import pick from 'lodash/pick'; import semver from 'semver'; import {Hooks} from './index'; @@ -285,7 +283,7 @@ function pickPackageMetadata(metadata: PackageMetadata): PackageMetadata { 'dist-tags': metadata[`dist-tags`], versions: Object.fromEntries(Object.entries(metadata.versions).map(([key, value]) => [ key, - pick(value, CACHED_FIELDS) as any, + miscUtils.pick(value, CACHED_FIELDS) as any, ])), }; } @@ -510,6 +508,8 @@ async function askForOtp(error: any, {configuration}: {configuration: Configurat if (!process.env.YARN_IS_TEST_ENV) { const autoOpen = notice.match(/open (https?:\/\/\S+)/i); if (autoOpen && nodeUtils.openUrl) { + const {prompt} = await import(`enquirer`); + const {openNow} = await prompt<{openNow: boolean}>({ type: `confirm`, name: `openNow`, @@ -535,6 +535,8 @@ async function askForOtp(error: any, {configuration}: {configuration: Configurat if (process.env.YARN_IS_TEST_ENV) return process.env.YARN_INJECT_NPM_2FA_TOKEN || ``; + const {prompt} = await import(`enquirer`); + const {otp} = await prompt<{otp: string}>({ type: `password`, name: `otp`, diff --git a/packages/plugin-npm/sources/npmPublishUtils.ts b/packages/plugin-npm/sources/npmPublishUtils.ts index f7c0e1a69cf..b3eda8dbf48 100644 --- a/packages/plugin-npm/sources/npmPublishUtils.ts +++ b/packages/plugin-npm/sources/npmPublishUtils.ts @@ -3,7 +3,6 @@ import {Workspace, structUtils} from '@yarnpkg/core'; import {PortablePath, xfs, npath} from '@yarnpkg/fslib'; import {packUtils} from '@yarnpkg/plugin-pack'; import {createHash} from 'crypto'; -import ssri from 'ssri'; import {normalizeRegistry} from './npmConfigUtils'; @@ -21,6 +20,8 @@ export async function makePublishBody(workspace: Workspace, buffer: Buffer, {acc const name = structUtils.stringifyIdent(ident); const shasum = createHash(`sha1`).update(buffer).digest(`hex`); + + const {default: ssri} = await import(`ssri`); const integrity = ssri.fromData(buffer).toString(); const publishAccess = access ?? getPublishAccess(workspace, ident); diff --git a/packages/plugin-typescript/sources/typescriptUtils.ts b/packages/plugin-typescript/sources/typescriptUtils.ts index a5b47ef7edc..081249268b5 100644 --- a/packages/plugin-typescript/sources/typescriptUtils.ts +++ b/packages/plugin-typescript/sources/typescriptUtils.ts @@ -1,7 +1,6 @@ import {Request, Requester, Response} from '@algolia/requester-common'; import {Configuration, Descriptor} from '@yarnpkg/core'; import {httpUtils, structUtils} from '@yarnpkg/core'; -import algoliasearch from 'algoliasearch'; // Note that the appId and appKey are specific to Yarn's plugin-typescript - please // don't use them anywhere else without asking Algolia's permission @@ -32,6 +31,8 @@ export const hasDefinitelyTyped = async ( }; const createAlgoliaClient = (configuration: Configuration) => { + const algoliasearch = require(`algoliasearch`) as typeof import('algoliasearch').default; + const requester: Requester = { async send(request: Request): Promise { try { diff --git a/packages/plugin-version/sources/versionUtils.ts b/packages/plugin-version/sources/versionUtils.ts index 80ee8465241..5e220dc5e3b 100644 --- a/packages/plugin-version/sources/versionUtils.ts +++ b/packages/plugin-version/sources/versionUtils.ts @@ -3,7 +3,6 @@ import {PortablePath, npath, ppath, xfs} import {parseSyml, stringifySyml} from '@yarnpkg/parsers'; import {gitUtils} from '@yarnpkg/plugin-git'; import {UsageError} from 'clipanion'; -import omit from 'lodash/omit'; import semver from 'semver'; // Basically we only support auto-upgrading the ranges that are very simple (^x.y.z, ~x.y.z, >=x.y.z, and of course x.y.z) @@ -27,7 +26,7 @@ export function validateReleaseDecision(decision: unknown): string { if (semverDecision) return semverDecision; - return miscUtils.validateEnum(omit(Decision, `UNDECIDED`), decision as string); + return miscUtils.validateEnum(miscUtils.omit(Decision, [`UNDECIDED`]), decision as string); } export type VersionFile = { diff --git a/packages/yarnpkg-builder/sources/commands/build/bundle.ts b/packages/yarnpkg-builder/sources/commands/build/bundle.ts index 4407b2ff241..d488b8d5c77 100644 --- a/packages/yarnpkg-builder/sources/commands/build/bundle.ts +++ b/packages/yarnpkg-builder/sources/commands/build/bundle.ts @@ -4,7 +4,7 @@ import {npath, ppath, xfs} from import chalk from 'chalk'; import cp from 'child_process'; import {Command, Option, Usage} from 'clipanion'; -import {build, Plugin} from 'esbuild'; +import {build, Loader, Plugin} from 'esbuild'; import {createRequire} from 'module'; import path from 'path'; import semver from 'semver'; @@ -137,10 +137,77 @@ export default class BuildBundleCommand extends Command { logLevel: `silent`, format: `iife`, platform: `node`, - plugins: [valLoader], minify: !this.noMinify, sourcemap: this.sourceMap ? `inline` : false, target: `node${semver.minVersion(pkg.engines.node)!.version}`, + plugins: [valLoader, { + name: `foo`, + setup: build => { + build.onLoad({namespace: `lazy-command`, filter: /^/}, async args => { + const load = await xfs.readFilePromise(npath.toPortablePath(args.path), `utf8`); + const fixed = load.replace(/^ {2}async execute\(\) \{\n.*?^ {2}\}\n/ms, ` + async execute() { + const mod = await import('${args.path}?real'); + return mod.default.prototype.execute.call(this); + } + `); + + return { + contents: fixed, + resolveDir: path.dirname(args.path), + loader: path.extname(args.path).slice(1) as Loader, + }; + }); + + build.onResolve({filter: /commands\/.*$/}, async args => { + if (!args.path.endsWith(`?real`)) + return undefined; + + const resolution = await build.resolve(args.path.replace(/\?real$/, ``), { + resolveDir: args.resolveDir, + pluginData: {skipFoo: true}, + }); + + return { + path: resolution.path, + }; + }); + + build.onResolve({filter: /commands\/.*$/}, async args => { + if (args.pluginData?.skipFoo) + return undefined; + + const resolution = await build.resolve(args.path, { + resolveDir: args.resolveDir, + pluginData: {skipFoo: true}, + }); + + return { + path: resolution.path, + namespace: `lazy-command`, + }; + }); + }, + }, { + name: `bar`, + setup: build => { + return; + + build.onLoad({filter: /^/}, async args => { + let loader = path.extname(args.path).slice(1) as Loader; + if (loader === `mjs` as Loader) + loader = `js`; + + if (![`js`, `jsx`, `ts`, `tsx`].includes(loader)) + return undefined; + + return { + contents: `console.log(${JSON.stringify(args.path)});\n${await xfs.readFilePromise(npath.toPortablePath(args.path), `utf8`)}`, + loader, + }; + }); + }, + }], }); for (const warning of res.warnings) { diff --git a/packages/yarnpkg-core/package.json b/packages/yarnpkg-core/package.json index 4060005d7f7..2903973d081 100644 --- a/packages/yarnpkg-core/package.json +++ b/packages/yarnpkg-core/package.json @@ -10,6 +10,7 @@ "sideEffects": false, "dependencies": { "@arcanis/slice-ansi": "^1.1.1", + "@fastify/deepmerge": "^1.3.0", "@types/semver": "^7.1.0", "@types/treeify": "^1.0.0", "@yarnpkg/fslib": "workspace:^", @@ -23,6 +24,7 @@ "cross-spawn": "7.0.3", "diff": "^5.1.0", "dotenv": "^16.3.1", + "fast-deep-equal": "^3.1.3", "fast-glob": "^3.2.2", "got": "^11.7.0", "lodash": "^4.17.15", diff --git a/packages/yarnpkg-core/sources/Project.ts b/packages/yarnpkg-core/sources/Project.ts index 2e07b869076..e1735fd3a5c 100644 --- a/packages/yarnpkg-core/sources/Project.ts +++ b/packages/yarnpkg-core/sources/Project.ts @@ -4,7 +4,6 @@ import {parseSyml, stringifySyml} from '@y import {UsageError} from 'clipanion'; import {createHash} from 'crypto'; import {structuredPatch} from 'diff'; -import pick from 'lodash/pick'; import pLimit from 'p-limit'; import semver from 'semver'; import internal from 'stream'; @@ -2017,7 +2016,7 @@ export class Project { for (const category of Object.values(INSTALL_STATE_FIELDS)) fields.push(...category); - const installState = pick(this, fields) as InstallState; + const installState = miscUtils.pick(this, fields) as InstallState; const serializedState = v8.serialize(installState); const newInstallStateChecksum = hashUtils.makeHash(serializedState); if (this.installStateChecksum === newInstallStateChecksum) @@ -2052,13 +2051,13 @@ export class Project { this.linkersCustomData = installState.linkersCustomData; if (restoreBuildState) - Object.assign(this, pick(installState, INSTALL_STATE_FIELDS.restoreBuildState)); + Object.assign(this, miscUtils.pick(installState, INSTALL_STATE_FIELDS.restoreBuildState)); // Resolutions needs to be restored last otherwise applyLightResolution will persist a new state // before the rest is restored if (restoreResolutions) { if (installState.lockFileChecksum === this.lockFileChecksum) { - Object.assign(this, pick(installState, INSTALL_STATE_FIELDS.restoreResolutions)); + Object.assign(this, miscUtils.pick(installState, INSTALL_STATE_FIELDS.restoreResolutions)); } else { await this.applyLightResolution(); } diff --git a/packages/yarnpkg-core/sources/Report.ts b/packages/yarnpkg-core/sources/Report.ts index 2ba2250e91b..b86a829cf57 100644 --- a/packages/yarnpkg-core/sources/Report.ts +++ b/packages/yarnpkg-core/sources/Report.ts @@ -1,4 +1,3 @@ -import throttle from 'lodash/throttle'; import {PassThrough} from 'stream'; import {StringDecoder} from 'string_decoder'; @@ -130,6 +129,20 @@ export abstract class Report { unlock = resolve; }); + function throttle>(fn: (...args: T) => void, delay: number) { + let lastCall = 0; + + return (...args: T) => { + const now = Date.now(); + + if (now - lastCall < delay) + return; + + lastCall = now; + fn(...args); + }; + } + const setTitle: (title: string) => void = throttle((title: string) => { const thisUnlock = unlock; diff --git a/packages/yarnpkg-core/sources/miscUtils.ts b/packages/yarnpkg-core/sources/miscUtils.ts index 98dcf1f1015..e41a4d99740 100644 --- a/packages/yarnpkg-core/sources/miscUtils.ts +++ b/packages/yarnpkg-core/sources/miscUtils.ts @@ -1,7 +1,7 @@ +import deepMerge from '@fastify/deepmerge'; import {PortablePath, npath, xfs} from '@yarnpkg/fslib'; import {UsageError} from 'clipanion'; -import isEqual from 'lodash/isEqual'; -import mergeWith from 'lodash/mergeWith'; +import deepEqual from 'fast-deep-equal'; import micromatch from 'micromatch'; import pLimit, {Limit} from 'p-limit'; import semver from 'semver'; @@ -538,6 +538,16 @@ type MergeObjects, Accumulator> = T extends [infer U, . ? MergeObjects : Accumulator; +const deepMergeImpl = deepMerge({ + mergeArray: () => (target, source) => { + for (const sourceItem of source) + if (!target.find(targetItem => deepEqual(targetItem, sourceItem))) + target.push(sourceItem); + + return target; + }, +}); + /** * Merges multiple objects into the target argument. * @@ -548,38 +558,13 @@ type MergeObjects, Accumulator> = T extends [infer U, . * @see toMerged for a version that doesn't mutate the target argument * */ -export function mergeIntoTarget>(target: T, ...sources: S): MergeObjects { - // We need to wrap everything in an object because otherwise lodash fails to merge 2 top-level arrays - const wrap = (value: T) => ({value}); - - const wrappedTarget = wrap(target); - const wrappedSources = sources.map(source => wrap(source)); - - const {value} = mergeWith(wrappedTarget, ...wrappedSources, (targetValue: unknown, sourceValue: unknown) => { - // We need to preserve comments in custom Array classes such as comment-json's `CommentArray`, so we can't use spread or `Set`s - if (Array.isArray(targetValue) && Array.isArray(sourceValue)) { - for (const sourceItem of sourceValue) { - if (!targetValue.find(targetItem => isEqual(targetItem, sourceItem))) { - targetValue.push(sourceItem); - } - } - - return targetValue; - } - - return undefined; - }); +export function toMerged>(target: T, ...sources: S): MergeObjects { + let current: any = cloneDeep(target); - return value; -} + for (const source of sources) + current = deepMergeImpl(current, source); -/** - * Merges multiple objects into a single one, without mutating any arguments. - * - * Custom classes are not supported (i.e. comment-json's comments will be lost). - */ -export function toMerged>(...sources: S): MergeObjects { - return mergeIntoTarget({}, ...sources); + return current as MergeObjects; } export function groupBy, K extends keyof T>(items: Iterable, key: K): {[V in T[K]]?: Array>} { @@ -598,3 +583,88 @@ export function groupBy, K extends keyof T>(items: export function parseInt(val: string | number) { return typeof val === `string` ? Number.parseInt(val, 10) : val; } + +export function pick(obj: Record, keys: ReadonlyArray) { + const result: Record = {}; + + for (const key of keys) + if (Object.hasOwn(obj, key)) + result[key] = obj[key]; + + return result; +} + +export function omit(obj: Record, keys: ReadonlyArray) { + const result: Record = {}; + const keysSet = new Set(keys); + + for (const key of Object.keys(obj)) + if (!keysSet.has(key)) + result[key] = obj[key]; + + return result; +} + +export function capitalize(str: string) { + return str.charAt(0).toUpperCase() + str.slice(1); +} + +export function startCase(str: string) { + return str.split(/[-_]/g).map(capitalize).join(` `); +} + +export function cloneDeep(obj: T): T { + return JSON.parse(JSON.stringify(obj)); +} + +export function toPath(p: string) { + return p.split(/[.[\]]/g); +} + +export function get(obj: any, path: Array) { + for (const segment of path) { + if (!Object.hasOwn(obj, segment)) + return undefined; + + obj = obj[segment]; + } + + return obj; +} + +export function hasPath(obj: any, path: Array) { + for (const segment of path) { + if (!Object.hasOwn(obj, segment)) + return false; + + obj = obj[segment]; + } + + return true; +} + +export function set(obj: any, path: Array, value: any) { + for (let i = 0; i < path.length - 1; ++i) { + const segment = path[i]; + + if (!Object.hasOwn(obj, segment)) + obj[segment] = {}; + + obj = obj[segment]; + } + + obj[path[path.length - 1]] = value; +} + +export function unset(obj: any, path: Array) { + for (let i = 0; i < path.length - 1; ++i) { + const segment = path[i]; + + if (!Object.hasOwn(obj, segment)) + return; + + obj = obj[segment]; + } + + delete obj[path[path.length - 1]]; +} diff --git a/packages/yarnpkg-core/sources/scriptUtils.ts b/packages/yarnpkg-core/sources/scriptUtils.ts index 6957ed29025..59e05a73ca8 100644 --- a/packages/yarnpkg-core/sources/scriptUtils.ts +++ b/packages/yarnpkg-core/sources/scriptUtils.ts @@ -2,7 +2,6 @@ import {CwdFS, Filename, NativePath, PortablePath} from '@yarnpkg/fslib'; import {xfs, npath, ppath} from '@yarnpkg/fslib'; import {ZipOpenFS} from '@yarnpkg/libzip'; import {execute} from '@yarnpkg/shell'; -import capitalize from 'lodash/capitalize'; import pLimit from 'p-limit'; import {PassThrough, Readable, Writable} from 'stream'; @@ -593,7 +592,7 @@ export async function executeWorkspaceLifecycleScript(workspace: Workspace, life if (exitCode !== 0) { xfs.detachTemp(logDir); - throw new ReportError(MessageName.LIFECYCLE_SCRIPT, `${capitalize(lifecycleScriptName)} script failed (exit code ${formatUtils.pretty(configuration, exitCode, formatUtils.Type.NUMBER)}, logs can be found here: ${formatUtils.pretty(configuration, logFile, formatUtils.Type.PATH)}); run ${formatUtils.pretty(configuration, `yarn ${lifecycleScriptName}`, formatUtils.Type.CODE)} to investigate`); + throw new ReportError(MessageName.LIFECYCLE_SCRIPT, `${miscUtils.capitalize(lifecycleScriptName)} script failed (exit code ${formatUtils.pretty(configuration, exitCode, formatUtils.Type.NUMBER)}, logs can be found here: ${formatUtils.pretty(configuration, logFile, formatUtils.Type.PATH)}); run ${formatUtils.pretty(configuration, `yarn ${lifecycleScriptName}`, formatUtils.Type.CODE)} to investigate`); } }); } diff --git a/packages/yarnpkg-core/sources/tgzUtils.ts b/packages/yarnpkg-core/sources/tgzUtils.ts index e492c877301..793ca734c64 100644 --- a/packages/yarnpkg-core/sources/tgzUtils.ts +++ b/packages/yarnpkg-core/sources/tgzUtils.ts @@ -2,7 +2,7 @@ import {Configuration, nodeUtils} f import {FakeFS, PortablePath, NodeFS, ppath, xfs, npath, constants, statUtils} from '@yarnpkg/fslib'; import {ZipCompression, ZipFS} from '@yarnpkg/libzip'; import {PassThrough, Readable} from 'stream'; -import tar from 'tar'; +import {ParseStream, ReadEntry} from 'tar'; import {AsyncPool, TaskPool, WorkerPool} from './TaskPool'; import * as miscUtils from './miscUtils'; @@ -133,12 +133,14 @@ export async function convertToZip(tgz: Buffer, opts: ConvertToZipOptions = {}) } async function * parseTar(tgz: Buffer) { + const tar = await import(`tar`); + // @ts-expect-error - Types are wrong about what this function returns - const parser: tar.ParseStream = new tar.Parse(); + const parser: ParseStream = new tar.Parse(); const passthrough = new PassThrough({objectMode: true, autoDestroy: true, emitClose: true}); - parser.on(`entry`, (entry: tar.ReadEntry) => { + parser.on(`entry`, (entry: ReadEntry) => { passthrough.write(entry); }); @@ -155,14 +157,14 @@ async function * parseTar(tgz: Buffer) { parser.end(tgz); for await (const entry of passthrough) { - const it = entry as tar.ReadEntry; + const it = entry as ReadEntry; yield it; it.resume(); } } export async function extractArchiveTo>(tgz: Buffer, targetFs: T, {stripComponents = 0, prefixPath = PortablePath.dot}: ExtractBufferOptions = {}): Promise { - function ignore(entry: tar.ReadEntry) { + function ignore(entry: ReadEntry) { // Disallow absolute paths; might be malicious (ex: /etc/passwd) if (entry.path[0] === `/`) return true; diff --git a/packages/yarnpkg-core/tests/miscUtils.test.ts b/packages/yarnpkg-core/tests/miscUtils.test.ts index 9cdda3bc60d..a1c0740aceb 100644 --- a/packages/yarnpkg-core/tests/miscUtils.test.ts +++ b/packages/yarnpkg-core/tests/miscUtils.test.ts @@ -116,41 +116,4 @@ describe(`miscUtils`, () => { expect(c).toStrictEqual({n: [{a: 1}, {b: 2}, {c: 3}]}); }); }); - - describe(`mergeIntoTarget`, () => { - it(`should preserve comments when the target is an object created by comment-json`, () => { - const a = CJSON.parse(`{ - // n - "n": - // array - [ - // 1 - 1, - // 2 - 2, - // 3 - 3 - ] - }`); - const b = {n: [4, 5, 6]}; - const c = miscUtils.mergeIntoTarget(a, b); - - expect(CJSON.stringify(c, null, 2)).toStrictEqual(CJSON.stringify(CJSON.parse(`{ - // n - "n": - // array - [ - // 1 - 1, - // 2 - 2, - // 3 - 3, - 4, - 5, - 6 - ] - }`), null, 2)); - }); - }); }); diff --git a/packages/yarnpkg-pnp/sources/generatePnpScript.ts b/packages/yarnpkg-pnp/sources/generatePnpScript.ts index c12fafd5f48..e45583be3f8 100644 --- a/packages/yarnpkg-pnp/sources/generatePnpScript.ts +++ b/packages/yarnpkg-pnp/sources/generatePnpScript.ts @@ -2,11 +2,11 @@ import {Filename} from '@yarnpkg/fslib'; import {generatePrettyJson} from './generatePrettyJson'; import {generateSerializedState} from './generateSerializedState'; -import getTemplate from './hook'; import {SerializedState} from './types'; import {PnpSettings} from './types'; export function generateLoader(shebang: string | null | undefined, loader: string) { + const getTemplate = require(`./hook`); return [ shebang ? `${shebang}\n` : ``, `/* eslint-disable */\n`, diff --git a/packages/yarnpkg-sdks/sources/generateSdk.ts b/packages/yarnpkg-sdks/sources/generateSdk.ts index 3b15c56bca5..c1cd308262a 100644 --- a/packages/yarnpkg-sdks/sources/generateSdk.ts +++ b/packages/yarnpkg-sdks/sources/generateSdk.ts @@ -4,8 +4,6 @@ import {parseSyml, stringifySyml} from '@yarnp import {PnpApi} from '@yarnpkg/pnp'; import chalk from 'chalk'; import {UsageError} from 'clipanion'; -import capitalize from 'lodash/capitalize'; -import startCase from 'lodash/startCase'; import {dynamicRequire} from './dynamicRequire'; import {BASE_SDKS} from './sdks/base'; @@ -22,7 +20,7 @@ export const SUPPORTED_INTEGRATIONS = new Map([ ] as const); export const getDisplayName = (name: string) => - startCase(name).split(` `).map(word => capitalize(word)).join(` `); + miscUtils.startCase(name).split(` `).map(word => miscUtils.capitalize(word)).join(` `); export const validateIntegrations = (integrations: Set) => { const unsupportedIntegrations: Array = []; diff --git a/packages/yarnpkg-sdks/sources/sdkUtils.ts b/packages/yarnpkg-sdks/sources/sdkUtils.ts index 167091f3f3b..0ad2e6b56f8 100644 --- a/packages/yarnpkg-sdks/sources/sdkUtils.ts +++ b/packages/yarnpkg-sdks/sources/sdkUtils.ts @@ -14,7 +14,7 @@ export const addSettingWorkspaceConfiguration = async (pnpApi: PnpApi, relativeF : `{}`; const data = CJSON.parse(content); - const patched = `${CJSON.stringify(miscUtils.mergeIntoTarget(data, patch), null, 2)}\n`; + const patched = `${CJSON.stringify(miscUtils.toMerged(data, patch), null, 2)}\n`; await xfs.mkdirPromise(ppath.dirname(filePath), {recursive: true}); await xfs.changeFilePromise(filePath, patched, { diff --git a/yarn.lock b/yarn.lock index 5a3c7a8ff62..7716aa09b22 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2782,6 +2782,13 @@ __metadata: languageName: node linkType: hard +"@fastify/deepmerge@npm:^1.3.0": + version: 1.3.0 + resolution: "@fastify/deepmerge@npm:1.3.0" + checksum: 10/6ddfc230ed46bfb158dbf83c2cc7f6119c9c1afb96d885cf5d95ac17b56126d04eef83ddb1ee7a1b044e65a128c76ebf8b391a26490b19f5812fa0d2d2a3a675 + languageName: node + linkType: hard + "@floating-ui/core@npm:^1.3.1": version: 1.3.1 resolution: "@floating-ui/core@npm:1.3.1" @@ -5080,6 +5087,7 @@ __metadata: resolution: "@yarnpkg/core@workspace:packages/yarnpkg-core" dependencies: "@arcanis/slice-ansi": "npm:^1.1.1" + "@fastify/deepmerge": "npm:^1.3.0" "@octokit/webhooks-types": "npm:^7.3.1" "@rollup/plugin-commonjs": "npm:^21.0.1" "@rollup/plugin-json": "npm:^6.0.0" @@ -5111,6 +5119,7 @@ __metadata: diff: "npm:^5.1.0" dotenv: "npm:^16.3.1" esbuild: "npm:esbuild-wasm@^0.15.15" + fast-deep-equal: "npm:^3.1.3" fast-glob: "npm:^3.2.2" got: "npm:^11.7.0" lodash: "npm:^4.17.15"