diff --git a/packages/open-next/package.json b/packages/open-next/package.json index 224fa4f23..013382ace 100644 --- a/packages/open-next/package.json +++ b/packages/open-next/package.json @@ -37,6 +37,7 @@ "README.md" ], "dependencies": { + "@ast-grep/napi": "^0.35.0", "@aws-sdk/client-cloudfront": "3.398.0", "@aws-sdk/client-dynamodb": "^3.398.0", "@aws-sdk/client-lambda": "^3.398.0", @@ -50,7 +51,8 @@ "esbuild": "0.19.2", "express": "5.0.1", "path-to-regexp": "^6.3.0", - "urlpattern-polyfill": "^10.0.0" + "urlpattern-polyfill": "^10.0.0", + "yaml": "^2.7.0" }, "devDependencies": { "@types/aws-lambda": "^8.10.109", diff --git a/packages/open-next/src/adapters/config/util.ts b/packages/open-next/src/adapters/config/util.ts index fb64ae1ca..147932598 100644 --- a/packages/open-next/src/adapters/config/util.ts +++ b/packages/open-next/src/adapters/config/util.ts @@ -21,10 +21,14 @@ export function loadBuildId(nextDir: string) { return fs.readFileSync(filePath, "utf-8").trim(); } -export function loadHtmlPages(nextDir: string) { +export function loadPagesManifest(nextDir: string) { const filePath = path.join(nextDir, "server/pages-manifest.json"); const json = fs.readFileSync(filePath, "utf-8"); - return Object.entries(JSON.parse(json)) + return JSON.parse(json); +} + +export function loadHtmlPages(nextDir: string) { + return Object.entries(loadPagesManifest(nextDir)) .filter(([_, value]) => (value as string).endsWith(".html")) .map(([key]) => key); } diff --git a/packages/open-next/src/build/copyTracedFiles.ts b/packages/open-next/src/build/copyTracedFiles.ts index cbacc6938..045f84c87 100644 --- a/packages/open-next/src/build/copyTracedFiles.ts +++ b/packages/open-next/src/build/copyTracedFiles.ts @@ -13,7 +13,15 @@ import { } from "node:fs"; import path from "node:path"; -import { loadConfig, loadPrerenderManifest } from "config/util.js"; +import { + loadAppPathsManifest, + loadBuildId, + loadConfig, + loadFunctionsConfigManifest, + loadMiddlewareManifest, + loadPagesManifest, + loadPrerenderManifest, +} from "config/util.js"; import { getCrossPlatformPathRegex } from "utils/regex.js"; import logger from "../logger.js"; import { MIDDLEWARE_TRACE_FILE } from "./constant.js"; @@ -50,6 +58,18 @@ interface CopyTracedFilesOptions { skipServerFiles?: boolean; } +export function getManifests(nextDir: string) { + return { + buildId: loadBuildId(nextDir), + config: loadConfig(nextDir), + prerenderManifest: loadPrerenderManifest(nextDir), + pagesManifest: loadPagesManifest(nextDir), + appPathsManifest: loadAppPathsManifest(nextDir), + middlewareManifest: loadMiddlewareManifest(nextDir), + functionsConfigManifest: loadFunctionsConfigManifest(nextDir), + }; +} + // eslint-disable-next-line sonarjs/cognitive-complexity export async function copyTracedFiles({ buildOutputPath, @@ -323,4 +343,9 @@ File ${fullFilePath} does not exist } logger.debug("copyTracedFiles:", Date.now() - tsStart, "ms"); + + return { + tracedFiles: Array.from(filesToCopy.values()), + manifests: getManifests(standaloneNextDir), + }; } diff --git a/packages/open-next/src/build/createServerBundle.ts b/packages/open-next/src/build/createServerBundle.ts index 6b4a3747d..cb1057b17 100644 --- a/packages/open-next/src/build/createServerBundle.ts +++ b/packages/open-next/src/build/createServerBundle.ts @@ -3,8 +3,10 @@ import path from "node:path"; import type { FunctionOptions, SplittedFunctionOptions } from "types/open-next"; +import type { Plugin } from "esbuild"; import logger from "../logger.js"; import { minifyAll } from "../minimize-js.js"; +import { ContentUpdater } from "../plugins/content-updater.js"; import { openNextReplacementPlugin } from "../plugins/replacement.js"; import { openNextResolvePlugin } from "../plugins/resolve.js"; import { getCrossPlatformPathRegex } from "../utils/regex.js"; @@ -14,8 +16,20 @@ import { copyTracedFiles } from "./copyTracedFiles.js"; import { generateEdgeBundle } from "./edge/createEdgeBundle.js"; import * as buildHelper from "./helper.js"; import { installDependencies } from "./installDeps.js"; +import { type CodePatcher, applyCodePatches } from "./patch/codePatcher.js"; + +interface CodeCustomization { + // These patches are meant to apply on user and next generated code + additionalCodePatches?: CodePatcher[]; + // These plugins are meant to apply during the esbuild bundling process. + // This will only apply to OpenNext code. + additionalPlugins?: (contentUpdater: ContentUpdater) => Plugin[]; +} -export async function createServerBundle(options: buildHelper.BuildOptions) { +export async function createServerBundle( + options: buildHelper.BuildOptions, + codeCustomization?: CodeCustomization, +) { const { config } = options; const foundRoutes = new Set(); // Get all functions to build @@ -36,7 +50,7 @@ export async function createServerBundle(options: buildHelper.BuildOptions) { if (fnOptions.runtime === "edge") { await generateEdgeBundle(name, options, fnOptions); } else { - await generateBundle(name, options, fnOptions); + await generateBundle(name, options, fnOptions, codeCustomization); } }); @@ -101,6 +115,7 @@ async function generateBundle( name: string, options: buildHelper.BuildOptions, fnOptions: SplittedFunctionOptions, + codeCustomization?: CodeCustomization, ) { const { appPath, appBuildOutputPath, config, outputDir, monorepoRoot } = options; @@ -153,7 +168,7 @@ async function generateBundle( buildHelper.copyEnvFile(appBuildOutputPath, packagePath, outputPath); // Copy all necessary traced files - await copyTracedFiles({ + const { tracedFiles, manifests } = await copyTracedFiles({ buildOutputPath: appBuildOutputPath, packagePath, outputDir: outputPath, @@ -161,6 +176,12 @@ async function generateBundle( bundledNextServer: isBundled, }); + const additionalCodePatches = codeCustomization?.additionalCodePatches ?? []; + + await applyCodePatches(options, tracedFiles, manifests, [ + ...additionalCodePatches, + ]); + // Build Lambda code // note: bundle in OpenNext package b/c the adapter relies on the // "serverless-http" package which is not a dependency in user's @@ -179,6 +200,12 @@ async function generateBundle( const disableRouting = isBefore13413 || config.middleware?.external; + const updater = new ContentUpdater(options); + + const additionalPlugins = codeCustomization?.additionalPlugins + ? codeCustomization.additionalPlugins(updater) + : []; + const plugins = [ openNextReplacementPlugin({ name: `requestHandlerOverride ${name}`, @@ -204,6 +231,9 @@ async function generateBundle( fnName: name, overrides, }), + ...additionalPlugins, + // The content updater plugin must be the last plugin + updater.plugin, ]; const outfileExt = fnOptions.runtime === "deno" ? "ts" : "mjs"; diff --git a/packages/open-next/src/build/edge/createEdgeBundle.ts b/packages/open-next/src/build/edge/createEdgeBundle.ts index 84d5c46d7..9c8104644 100644 --- a/packages/open-next/src/build/edge/createEdgeBundle.ts +++ b/packages/open-next/src/build/edge/createEdgeBundle.ts @@ -2,7 +2,7 @@ import { mkdirSync } from "node:fs"; import fs from "node:fs"; import path from "node:path"; -import { build } from "esbuild"; +import { type Plugin, build } from "esbuild"; import type { MiddlewareInfo } from "types/next-types"; import type { IncludedConverter, @@ -16,6 +16,7 @@ import type { import { loadMiddlewareManifest } from "config/util.js"; import type { OriginResolver } from "types/overrides.js"; import logger from "../../logger.js"; +import { ContentUpdater } from "../../plugins/content-updater.js"; import { openNextEdgePlugins } from "../../plugins/edge.js"; import { openNextExternalMiddlewarePlugin } from "../../plugins/externalMiddleware.js"; import { openNextReplacementPlugin } from "../../plugins/replacement.js"; @@ -39,6 +40,7 @@ interface BuildEdgeBundleOptions { additionalExternals?: string[]; onlyBuildOnce?: boolean; name: string; + additionalPlugins?: (contentUpdater: ContentUpdater) => Plugin[]; } export async function buildEdgeBundle({ @@ -53,6 +55,7 @@ export async function buildEdgeBundle({ additionalExternals, onlyBuildOnce, name, + additionalPlugins: additionalPluginsFn, }: BuildEdgeBundleOptions) { const isInCloudfare = await isEdgeRuntime(overrides); function override(target: T) { @@ -60,6 +63,10 @@ export async function buildEdgeBundle({ ? overrides[target] : undefined; } + const contentUpdater = new ContentUpdater(options); + const additionalPlugins = additionalPluginsFn + ? additionalPluginsFn(contentUpdater) + : []; await esbuildAsync( { entryPoints: [entrypoint], @@ -98,6 +105,9 @@ export async function buildEdgeBundle({ nextDir: path.join(options.appBuildOutputPath, ".next"), isInCloudfare, }), + ...additionalPlugins, + // The content updater plugin must be the last plugin + contentUpdater.plugin, ], treeShaking: true, alias: { @@ -173,6 +183,7 @@ export async function generateEdgeBundle( name: string, options: BuildOptions, fnOptions: SplittedFunctionOptions, + additionalPlugins: (contentUpdater: ContentUpdater) => Plugin[] = () => [], ) { logger.info(`Generating edge bundle for: ${name}`); @@ -226,5 +237,6 @@ export async function generateEdgeBundle( overrides: fnOptions.override, additionalExternals: options.config.edgeExternals, name, + additionalPlugins, }); } diff --git a/packages/open-next/src/build/patch/astCodePatcher.ts b/packages/open-next/src/build/patch/astCodePatcher.ts new file mode 100644 index 000000000..779ea5d19 --- /dev/null +++ b/packages/open-next/src/build/patch/astCodePatcher.ts @@ -0,0 +1,114 @@ +// Mostly copied from the cloudflare adapter +import { readFileSync } from "node:fs"; + +import { + type Edit, + Lang, + type NapiConfig, + type SgNode, + parse, +} from "@ast-grep/napi"; +import yaml from "yaml"; +import type { PatchCodeFn } from "./codePatcher"; + +/** + * fix has the same meaning as in yaml rules + * see https://ast-grep.github.io/guide/rewrite-code.html#using-fix-in-yaml-rule + */ +export type RuleConfig = NapiConfig & { fix?: string }; + +/** + * Returns the `Edit`s and `Match`es for an ast-grep rule in yaml format + * + * The rule must have a `fix` to rewrite the matched node. + * + * Tip: use https://ast-grep.github.io/playground.html to create rules. + * + * @param rule The rule. Either a yaml string or an instance of `RuleConfig` + * @param root The root node + * @param once only apply once + * @returns A list of edits and a list of matches. + */ +export function applyRule( + rule: string | RuleConfig, + root: SgNode, + { once = false } = {}, +) { + const ruleConfig: RuleConfig = + typeof rule === "string" ? yaml.parse(rule) : rule; + if (ruleConfig.transform) { + throw new Error("transform is not supported"); + } + if (!ruleConfig.fix) { + throw new Error("no fix to apply"); + } + + const fix = ruleConfig.fix; + + const matches = once + ? [root.find(ruleConfig)].filter((m) => m !== null) + : root.findAll(ruleConfig); + + const edits: Edit[] = []; + + matches.forEach((match) => { + edits.push( + match.replace( + // Replace known placeholders by their value + fix + .replace(/\$\$\$([A-Z0-9_]+)/g, (_m, name) => + match + .getMultipleMatches(name) + .map((n) => n.text()) + .join(""), + ) + .replace( + /\$([A-Z0-9_]+)/g, + (m, name) => match.getMatch(name)?.text() ?? m, + ), + ), + ); + }); + + return { edits, matches }; +} + +/** + * Parse a file and obtain its root. + * + * @param path The file path + * @param lang The language to parse. Defaults to TypeScript. + * @returns The root for the file. + */ +export function parseFile(path: string, lang = Lang.TypeScript) { + return parse(lang, readFileSync(path, { encoding: "utf-8" })).root(); +} + +/** + * Patches the code from by applying the rule. + * + * This function is mainly for on off edits and tests, + * use `getRuleEdits` to apply multiple rules. + * + * @param code The source code + * @param rule The astgrep rule (yaml or NapiConfig) + * @param lang The language used by the source code + * @param lang Whether to apply the rule only once + * @returns The patched code + */ +export function patchCode( + code: string, + rule: string | RuleConfig, + { lang = Lang.TypeScript, once = false } = {}, +): string { + const node = parse(lang, code).root(); + const { edits } = applyRule(rule, node, { once }); + return node.commitEdits(edits); +} + +export function createPatchCode( + rule: string | RuleConfig, + lang = Lang.TypeScript, +): PatchCodeFn { + return async ({ code }) => patchCode(code, rule, { lang }); +} diff --git a/packages/open-next/src/build/patch/codePatcher.ts b/packages/open-next/src/build/patch/codePatcher.ts new file mode 100644 index 000000000..7802bdbd3 --- /dev/null +++ b/packages/open-next/src/build/patch/codePatcher.ts @@ -0,0 +1,184 @@ +import * as fs from "node:fs/promises"; +import logger from "../../logger.js"; +import type { getManifests } from "../copyTracedFiles.js"; +import * as buildHelper from "../helper.js"; + +type Versions = + | `>=${number}.${number}.${number} <=${number}.${number}.${number}` + | `>=${number}.${number}.${number}` + | `<=${number}.${number}.${number}`; +export interface VersionedField { + /** + * The versions of Next.js that this field should be used for + * Should be in the format `">=16.0.0 <=17.0.0"` or `">=16.0.0"` or `"<=17.0.0"` + * **Be careful with spaces** + */ + versions?: Versions; + field: T; +} + +export type PatchCodeFn = (args: { + /** + * The code of the file that needs to be patched + */ + code: string; + /** + * The final path of the file that needs to be patched + */ + filePath: string; + /** + * All files that are traced and will be included in the bundle + */ + tracedFiles: string[]; + /** + * Next.js manifests that are used by Next at runtime + */ + manifests: ReturnType; +}) => Promise; + +interface IndividualPatch { + pathFilter: RegExp; + contentFilter?: RegExp; + patchCode: PatchCodeFn; +} + +export interface CodePatcher { + name: string; + patches: IndividualPatch | VersionedField[]; +} + +export function parseVersions(versions?: Versions): { + before?: string; + after?: string; +} { + if (!versions) { + return {}; + } + // We need to use regex to extract the versions + const versionRegex = /([<>]=)(\d+\.\d+\.\d+)/g; + const matches = Array.from(versions.matchAll(versionRegex)); + if (matches.length === 0) { + throw new Error("Invalid version range, no matches found"); + } + if (matches.length > 2) { + throw new Error("Invalid version range, too many matches found"); + } + let after: string | undefined; + let before: string | undefined; + for (const match of matches) { + const [_, operator, version] = match; + if (operator === "<=") { + before = version; + } else { + after = version; + } + } + // Before returning we reconstruct the version string and compare it to the original + // If they don't match we throw an error + // We have to do this because template literal types here seems to allow for extra spaces + // that could easily break the version comparison and allow some patch to be applied on incorrect versions + // This might even go unnoticed + const reconstructedVersion = `${after ? `>=${after}` : ""}${ + before ? `${after ? " " : ""}<=${before}` : "" + }`; + if (reconstructedVersion !== versions) { + throw new Error( + "Invalid version range, the reconstructed version does not match the original", + ); + } + return { + before, + after, + }; +} + +export function extractVersionedField( + fields: VersionedField[], + version: string, +): T[] { + const result: T[] = []; + + for (const field of fields) { + const { before, after } = parseVersions(field.versions); + if ( + before && + after && + buildHelper.compareSemver(version, before) <= 0 && + buildHelper.compareSemver(version, after) >= 0 + ) { + result.push(field.field); + } else if ( + before && + !after && + buildHelper.compareSemver(version, before) <= 0 + ) { + result.push(field.field); + } else if ( + after && + !before && + buildHelper.compareSemver(version, after) >= 0 + ) { + result.push(field.field); + } + } + return result; +} + +export async function applyCodePatches( + buildOptions: buildHelper.BuildOptions, + tracedFiles: string[], + manifests: ReturnType, + codePatcher: CodePatcher[], +) { + const nextVersion = buildOptions.nextVersion; + logger.time("Applying code patches"); + + // We first filter against the version + // We also flatten the array of patches so that we get both the name and all the necessary patches + const patchesToApply = codePatcher.flatMap(({ name, patches }) => + Array.isArray(patches) + ? extractVersionedField(patches, nextVersion).map((patch) => ({ + name, + patch, + })) + : [{ name, patch: patches }], + ); + + await Promise.all( + tracedFiles.map(async (filePath) => { + // We check the filename against the filter to see if we should apply the patch + const patchMatchingPath = patchesToApply.filter(({ patch }) => { + return filePath.match(patch.pathFilter); + }); + if (patchMatchingPath.length === 0) { + return; + } + const content = await fs.readFile(filePath, "utf-8"); + // We filter a last time against the content this time + const patchToApply = patchMatchingPath.filter(({ patch }) => { + if (!patch.contentFilter) { + return true; + } + return content.match(patch.contentFilter); + }); + if (patchToApply.length === 0) { + return; + } + + // We apply the patches + let patchedContent = content; + + for (const { patch, name } of patchToApply) { + logger.debug(`Applying code patch: ${name} to ${filePath}`); + patchedContent = await patch.patchCode({ + code: patchedContent, + filePath, + tracedFiles, + manifests, + }); + } + await fs.writeFile(filePath, patchedContent); + }), + ); + logger.timeEnd("Applying code patches"); +} diff --git a/packages/open-next/src/logger.ts b/packages/open-next/src/logger.ts index 2d56fb9e6..5324503c4 100644 --- a/packages/open-next/src/logger.ts +++ b/packages/open-next/src/logger.ts @@ -13,4 +13,6 @@ export default { info: console.log, warn: (...args: any[]) => console.warn(chalk.yellow("WARN"), ...args), error: (...args: any[]) => console.error(chalk.red("ERROR"), ...args), + time: console.time, + timeEnd: console.timeEnd, }; diff --git a/packages/open-next/src/plugins/content-updater.ts b/packages/open-next/src/plugins/content-updater.ts new file mode 100644 index 000000000..e62df50bd --- /dev/null +++ b/packages/open-next/src/plugins/content-updater.ts @@ -0,0 +1,106 @@ +/** + * ESBuild stops calling `onLoad` hooks after the first hook returns an updated content. + * + * The updater allows multiple plugins to update the content. + */ + +import { readFile } from "node:fs/promises"; + +import type { OnLoadArgs, OnLoadOptions, Plugin, PluginBuild } from "esbuild"; +import type { BuildOptions } from "../build/helper"; +import { + type VersionedField, + extractVersionedField, +} from "../build/patch/codePatcher.js"; + +/** + * The callbacks returns either an updated content or undefined if the content is unchanged. + */ +export type Callback = (args: { + contents: string; + path: string; +}) => string | undefined | Promise; + +/** + * The callback is called only when `contentFilter` matches the content. + * It can be used as a fast heuristic to prevent an expensive update. + */ +export type OnUpdateOptions = OnLoadOptions & { + contentFilter: RegExp; +}; + +export type Updater = OnUpdateOptions & { callback: Callback }; + +export class ContentUpdater { + updaters = new Map(); + + constructor(private buildOptions: BuildOptions) {} + + /** + * Register a callback to update the file content. + * + * The callbacks are called in order of registration. + * + * @param name The name of the plugin (must be unique). + * @param updater A versioned field with the callback and `OnUpdateOptions`. + * @returns A noop ESBuild plugin. + */ + updateContent( + name: string, + versionedUpdaters: VersionedField[], + ): Plugin { + if (this.updaters.has(name)) { + throw new Error(`Plugin "${name}" already registered`); + } + const updaters = extractVersionedField( + versionedUpdaters, + this.buildOptions.nextVersion, + ); + this.updaters.set(name, updaters); + return { + name, + setup() {}, + }; + } + + /** + * Returns an ESBuild plugin applying the registered updates. + */ + get plugin() { + return { + name: "aggregate-on-load", + + setup: async (build: PluginBuild) => { + build.onLoad( + { filter: /\.(js|mjs|cjs|jsx|ts|tsx)$/ }, + async (args: OnLoadArgs) => { + const updaters = Array.from(this.updaters.values()).flat(); + if (updaters.length === 0) { + return; + } + let contents = await readFile(args.path, "utf-8"); + for (const { + filter, + namespace, + contentFilter, + callback, + } of updaters) { + if (namespace !== undefined && args.namespace !== namespace) { + continue; + } + if (!args.path.match(filter)) { + continue; + } + if (!contents.match(contentFilter)) { + continue; + } + contents = + (await callback({ contents, path: args.path })) ?? contents; + } + return { contents }; + }, + ); + }, + }; + } +} diff --git a/packages/tests-unit/tests/build/patch/codePatcher.test.ts b/packages/tests-unit/tests/build/patch/codePatcher.test.ts new file mode 100644 index 000000000..52577cd36 --- /dev/null +++ b/packages/tests-unit/tests/build/patch/codePatcher.test.ts @@ -0,0 +1,78 @@ +import { extractVersionedField } from "@opennextjs/aws/build/patch/codePatcher.js"; + +describe("extractVersionedField", () => { + it("should return the field if the version is between before and after", () => { + const result = extractVersionedField( + [{ versions: ">=15.0.0 <=16.0.0", field: 0 }], + "15.5.0", + ); + + expect(result).toEqual([0]); + }); + + it("should return an empty array if the version is not between before and after", () => { + const result = extractVersionedField( + [{ versions: ">=15.0.0 <=16.0.0", field: 0 }], + "14.0.0", + ); + + expect(result).toEqual([]); + }); + + it("should return the field if the version is equal to before", () => { + const result = extractVersionedField( + [{ versions: "<=15.0.0", field: 0 }], + "15.0.0", + ); + + expect(result).toEqual([0]); + }); + + it("should return the field if the version is greater than after", () => { + const result = extractVersionedField( + [{ versions: ">=16.0.0", field: 0 }], + "16.5.0", + ); + + expect(result).toEqual([0]); + }); + + it("should return the field if the version is less than before", () => { + const result = extractVersionedField( + [{ versions: "<=15.0.0", field: 0 }], + "14.0.0", + ); + + expect(result).toEqual([0]); + }); + + it("should return an empty array if version is after before", () => { + const result = extractVersionedField( + [{ versions: "<=15.0.0", field: 0 }], + "15.1.0", + ); + + expect(result).toEqual([]); + }); + + it("should throw an error if a single version range is invalid because of a space before", () => { + expect(() => + extractVersionedField([{ versions: "<= 15.0.0", field: 0 }], "15.0.0"), + ).toThrow("Invalid version range"); + }); + + it("should throw an error if a single version range is invalid because of a space inside version", () => { + expect(() => + extractVersionedField([{ versions: ">=16. 0.0", field: 0 }], "15.0.0"), + ).toThrow("Invalid version range"); + }); + + it("should throw an error if one of the version range is invalid because of a space before the version", () => { + expect(() => + extractVersionedField( + [{ versions: ">=16.0.0 <= 15.0.0", field: 0 }], + "15.0.0", + ), + ).toThrow("Invalid version range"); + }); +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a53debc16..4327bfa4f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -204,6 +204,9 @@ importers: packages/open-next: dependencies: + '@ast-grep/napi': + specifier: ^0.35.0 + version: 0.35.0 '@aws-sdk/client-cloudfront': specifier: 3.398.0 version: 3.398.0(aws-crt@1.23.0) @@ -246,6 +249,9 @@ importers: urlpattern-polyfill: specifier: ^10.0.0 version: 10.0.0 + yaml: + specifier: ^2.7.0 + version: 2.7.0 devDependencies: '@types/aws-lambda': specifier: ^8.10.109 @@ -323,6 +329,64 @@ packages: resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} engines: {node: '>=6.0.0'} + '@ast-grep/napi-darwin-arm64@0.35.0': + resolution: {integrity: sha512-T+MN4Oinc+sXjXCIHzfxDDWY7r2pKgPxM6zVeVlkMTrJV2mJtyKYBIS+CABhRM6kflps2T2I6l4DGaKV/8Ym9w==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@ast-grep/napi-darwin-x64@0.35.0': + resolution: {integrity: sha512-pEYiN6JI1HY2uWhMYJ9+3yIMyVYKuYdFzeD+dL7odA3qzK0o9N9AM3/NOt4ynU2EhufaWCJr0P5NoQ636qN6MQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@ast-grep/napi-linux-arm64-gnu@0.35.0': + resolution: {integrity: sha512-NBuzQngABGKz7lhG08IQb+7nPqUx81Ol37xmS3ZhVSdSgM0mtp93rCbgFTkJcAFE8IMfCHQSg7G4g0Iotz4ABQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@ast-grep/napi-linux-arm64-musl@0.35.0': + resolution: {integrity: sha512-1EcvHPwyWpCL/96LuItBYGfeI5FaMTRvL+dHbO/hL5q1npqbb5qn+ppJwtNOjTPz8tayvgggxVk9T4C2O7taYA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@ast-grep/napi-linux-x64-gnu@0.35.0': + resolution: {integrity: sha512-FDzNdlqmQnsiWXhnLxusw5AOfEcEM+5xtmrnAf3SBRFr86JyWD9qsynnFYC2pnP9hlMfifNH2TTmMpyGJW49Xw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@ast-grep/napi-linux-x64-musl@0.35.0': + resolution: {integrity: sha512-wlmndjfBafT8u5p4DBnoRQyoCSGNuVSz7rT3TqhvlHcPzUouRWMn95epU9B1LNLyjXvr9xHeRjSktyCN28w57Q==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@ast-grep/napi-win32-arm64-msvc@0.35.0': + resolution: {integrity: sha512-gkhJeYc4rrZLX2icLxalPikTLMR57DuIYLwLr9g+StHYXIsGHrbfrE6Nnbdd8Izfs34ArFCrcwdaMrGlvOPSeg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@ast-grep/napi-win32-ia32-msvc@0.35.0': + resolution: {integrity: sha512-OdUuRa3chHCZ65y+qALfkUjz0W0Eg21YZ9TyPquV5why07M6HAK38mmYGzLxFH6294SvRQhs+FA/rAfbKeH0jA==} + engines: {node: '>= 10'} + cpu: [ia32] + os: [win32] + + '@ast-grep/napi-win32-x64-msvc@0.35.0': + resolution: {integrity: sha512-pcQRUHqbroTN1oQ56V982a7IZTUUySQYWa2KEyksiifHGuBuitlzcyzFGjT96ThcqD9XW0UVJMvpoF2Qjh006Q==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@ast-grep/napi@0.35.0': + resolution: {integrity: sha512-3ucaaSxV6fxXoqHrE/rxAvP1THnDdY5jNzGlnvx+JvnY9C/dSRKc0jlRMRz59N3El572+/yNRUUpAV1T9aBJug==} + engines: {node: '>= 10'} + '@aws-cdk/asset-awscli-v1@2.2.208': resolution: {integrity: sha512-r4CuHZaiBioU6waWhCNdEL4MO1+rfbcYVS/Ndz1XNGB5cxIRZwAS0Si6qD2D6nsgpPojiruFl67T1t5M9Va8kQ==} @@ -5449,6 +5513,11 @@ packages: engines: {node: '>= 14'} hasBin: true + yaml@2.7.0: + resolution: {integrity: sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA==} + engines: {node: '>= 14'} + hasBin: true + yargs-parser@20.2.9: resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==} engines: {node: '>=10'} @@ -5497,6 +5566,45 @@ snapshots: '@jridgewell/gen-mapping': 0.3.5 '@jridgewell/trace-mapping': 0.3.25 + '@ast-grep/napi-darwin-arm64@0.35.0': + optional: true + + '@ast-grep/napi-darwin-x64@0.35.0': + optional: true + + '@ast-grep/napi-linux-arm64-gnu@0.35.0': + optional: true + + '@ast-grep/napi-linux-arm64-musl@0.35.0': + optional: true + + '@ast-grep/napi-linux-x64-gnu@0.35.0': + optional: true + + '@ast-grep/napi-linux-x64-musl@0.35.0': + optional: true + + '@ast-grep/napi-win32-arm64-msvc@0.35.0': + optional: true + + '@ast-grep/napi-win32-ia32-msvc@0.35.0': + optional: true + + '@ast-grep/napi-win32-x64-msvc@0.35.0': + optional: true + + '@ast-grep/napi@0.35.0': + optionalDependencies: + '@ast-grep/napi-darwin-arm64': 0.35.0 + '@ast-grep/napi-darwin-x64': 0.35.0 + '@ast-grep/napi-linux-arm64-gnu': 0.35.0 + '@ast-grep/napi-linux-arm64-musl': 0.35.0 + '@ast-grep/napi-linux-x64-gnu': 0.35.0 + '@ast-grep/napi-linux-x64-musl': 0.35.0 + '@ast-grep/napi-win32-arm64-msvc': 0.35.0 + '@ast-grep/napi-win32-ia32-msvc': 0.35.0 + '@ast-grep/napi-win32-x64-msvc': 0.35.0 + '@aws-cdk/asset-awscli-v1@2.2.208': {} '@aws-cdk/asset-kubectl-v20@2.1.3': {} @@ -12200,6 +12308,8 @@ snapshots: yaml@2.6.0: {} + yaml@2.7.0: {} + yargs-parser@20.2.9: {} yargs-parser@21.1.1: {}