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..3c6a9c773 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,19 @@ interface CopyTracedFilesOptions { skipServerFiles?: boolean; } +// TODO: add all the necessary manifests here +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 +344,9 @@ File ${fullFilePath} does not exist } logger.debug("copyTracedFiles:", Date.now() - tsStart, "ms"); + + return { + tracedFiles: filesToCopy.values(), + manifests: getManifests(standaloneNextDir), + }; } diff --git a/packages/open-next/src/build/createMiddleware.ts b/packages/open-next/src/build/createMiddleware.ts index 914acce12..3ce60e239 100644 --- a/packages/open-next/src/build/createMiddleware.ts +++ b/packages/open-next/src/build/createMiddleware.ts @@ -81,6 +81,7 @@ export async function createMiddleware( additionalExternals: config.edgeExternals, onlyBuildOnce: forceOnlyBuildOnce === true, name: "middleware", + additionalPlugins: () => [], }); installDependencies(outputPath, config.middleware?.install); @@ -96,6 +97,7 @@ export async function createMiddleware( options, onlyBuildOnce: true, name: "middleware", + additionalPlugins: () => [], }); } } diff --git a/packages/open-next/src/build/createServerBundle.ts b/packages/open-next/src/build/createServerBundle.ts index 6b4a3747d..73599214b 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,21 @@ 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"; +import { patchFetchCacheSetMissingWaitUntil } from "./patch/patchFetchCacheWaitUntil.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 +51,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 +116,7 @@ async function generateBundle( name: string, options: buildHelper.BuildOptions, fnOptions: SplittedFunctionOptions, + codeCustomization?: CodeCustomization, ) { const { appPath, appBuildOutputPath, config, outputDir, monorepoRoot } = options; @@ -152,8 +168,8 @@ async function generateBundle( // Copy env files buildHelper.copyEnvFile(appBuildOutputPath, packagePath, outputPath); - // Copy all necessary traced files - await copyTracedFiles({ + // Copy all necessary traced files{ + const { tracedFiles, manifests } = await copyTracedFiles({ buildOutputPath: appBuildOutputPath, packagePath, outputDir: outputPath, @@ -161,6 +177,14 @@ async function generateBundle( bundledNextServer: isBundled, }); + const additionalCodePatches = codeCustomization?.additionalCodePatches ?? []; + + await applyCodePatches(options, Array.from(tracedFiles), manifests, [ + // TODO: create real code patchers here + patchFetchCacheSetMissingWaitUntil, + ...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 +203,10 @@ async function generateBundle( const disableRouting = isBefore13413 || config.middleware?.external; + const updater = new ContentUpdater(options); + + const additionalPlugins = codeCustomization?.additionalPlugins(updater) ?? []; + const plugins = [ openNextReplacementPlugin({ name: `requestHandlerOverride ${name}`, @@ -204,6 +232,8 @@ async function generateBundle( fnName: name, overrides, }), + ...additionalPlugins, + 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..145518750 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,8 @@ export async function buildEdgeBundle({ ? overrides[target] : undefined; } + const contentUpdater = new ContentUpdater(options); + const additionalPlugins = additionalPluginsFn(contentUpdater); await esbuildAsync( { entryPoints: [entrypoint], @@ -98,6 +103,8 @@ export async function buildEdgeBundle({ nextDir: path.join(options.appBuildOutputPath, ".next"), isInCloudfare, }), + ...additionalPlugins, + contentUpdater.plugin, ], treeShaking: true, alias: { @@ -173,6 +180,7 @@ export async function generateEdgeBundle( name: string, options: BuildOptions, fnOptions: SplittedFunctionOptions, + additionalPlugins: (contentUpdater: ContentUpdater) => Plugin[] = () => [], ) { logger.info(`Generating edge bundle for: ${name}`); @@ -226,5 +234,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..7a412c457 --- /dev/null +++ b/packages/open-next/src/build/patch/codePatcher.ts @@ -0,0 +1,124 @@ +import * as fs from "node:fs/promises"; +import logger from "../../logger.js"; +import * as buildHelper from "../helper.js"; + +// Either before or after should be provided, otherwise just use the field directly +export interface VersionedField { + // The version before which the field should be used + before?: + | `${number}` + | `${number}.${number}` + | `${number}.${number}.${number}`; + // The version after which the field should be used + after?: `${number}` | `${number}.${number}` | `${number}.${number}.${number}`; + field: T; +} + +export type PatchCodeFn = (args: { + // The content of the file that needs to be patched + code: string; + // The final path of the file that needs to be patched + filePath: string; + // All js files that will be included in this specific server function + tracedFiles: string[]; + // All next.js manifest that are present at runtime - Key relative to `.next` folder + manifests: Record; +}) => Promise; + +export interface CodePatcher { + name: string; + pathFilter: RegExp | VersionedField[]; + contentFilter?: RegExp | VersionedField[]; + patchCode: PatchCodeFn | VersionedField[]; +} + +export function extractVersionedField( + fields: VersionedField[], + version: string, +): T[] { + const result: T[] = []; + for (const field of fields) { + if ( + field.before && + field.after && + buildHelper.compareSemver(version, field.before) <= 0 && + buildHelper.compareSemver(version, field.after) > 0 + ) { + result.push(field.field); + } else if ( + field.before && + buildHelper.compareSemver(version, field.before) <= 0 + ) { + result.push(field.field); + } else if ( + field.after && + buildHelper.compareSemver(version, field.after) > 0 + ) { + result.push(field.field); + } + } + return result; +} + +export async function applyCodePatches( + buildOptions: buildHelper.BuildOptions, + tracedFiles: string[], + manifests: Record, + codePatcher: CodePatcher[], +) { + const nextVersion = buildOptions.nextVersion; + console.time("Applying code 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 = codePatcher.filter((patch) => { + const filters = Array.isArray(patch.pathFilter) + ? extractVersionedField(patch.pathFilter, nextVersion) + : [patch.pathFilter]; + return filters.some((filter) => filePath.match(filter)); + }); + 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; + } + const contentFilters = Array.isArray(patch.contentFilter) + ? extractVersionedField(patch.contentFilter, nextVersion) + : [patch.contentFilter]; + return contentFilters.some((filter) => + // If there is no filter, we just return true to apply the patch + filter ? content.match(filter) : true, + ); + }); + if (patchToApply.length === 0) { + return; + } + + // We apply the patches + + patchToApply.forEach(async (patch) => { + const patchCodeFns = Array.isArray(patch.patchCode) + ? extractVersionedField(patch.patchCode, nextVersion) + : [patch.patchCode]; + let patchedContent = content; + logger.debug( + `Applying ${patchCodeFns.length} patches to ${filePath} for ${patch.name}`, + ); + for (const patchCodeFn of patchCodeFns) { + patchedContent = await patchCodeFn({ + code: patchedContent, + filePath, + tracedFiles, + manifests, + }); + } + await fs.writeFile(filePath, patchedContent); + }); + }), + ); + console.timeEnd("Applying code patches"); +} diff --git a/packages/open-next/src/build/patch/patchFetchCacheWaitUntil.ts b/packages/open-next/src/build/patch/patchFetchCacheWaitUntil.ts new file mode 100644 index 000000000..4e13f5ac2 --- /dev/null +++ b/packages/open-next/src/build/patch/patchFetchCacheWaitUntil.ts @@ -0,0 +1,40 @@ +import { getCrossPlatformPathRegex } from "utils/regex.js"; +import { createPatchCode } from "./astCodePatcher.js"; +import type { CodePatcher } from "./codePatcher"; + +export const rule = ` +rule: + kind: call_expression + pattern: $PROMISE + all: + - has: { pattern: $_.arrayBuffer().then, stopBy: end } + - has: { pattern: "Buffer.from", stopBy: end } + - any: + - inside: + kind: sequence_expression + inside: + kind: return_statement + - inside: + kind: expression_statement + precedes: + kind: return_statement + - has: { pattern: $_.FETCH, stopBy: end } + +fix: | + globalThis.__openNextAls?.getStore()?.pendingPromiseRunner.add($PROMISE) +`; + +export const patchFetchCacheSetMissingWaitUntil: CodePatcher = { + name: "patch-fetch-cache-set-missing-wait-until", + pathFilter: [ + { + after: "15.0.0", + field: getCrossPlatformPathRegex( + String.raw`(server/chunks/.*\.js|.*\.runtime\..*\.js|patch-fetch\.js)$`, + { escape: false }, + ), + }, + ], + contentFilter: /arrayBuffer\(\)\s*\.then/, + patchCode: createPatchCode(rule), +}; 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..f34cf0367 --- /dev/null +++ b/packages/open-next/src/plugins/content-updater.ts @@ -0,0 +1,107 @@ +/** + * Mostly copied from the cloudflare adapter + * 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..acc6dfe7c --- /dev/null +++ b/packages/tests-unit/tests/build/patch/codePatcher.test.ts @@ -0,0 +1,48 @@ +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( + [{ before: "16.0.0", after: "15.0.0", field: 0 }], + "15.5.0", + ); + + expect(result).toEqual([0]); + }); + + it("should return the field if the version is equal to before", () => { + const result = extractVersionedField( + [{ before: "15.0.0", after: "16.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( + [{ after: "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( + [{ before: "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( + [{ before: "15.0.0", field: 0 }], + "15.1.0", + ); + + expect(result).toEqual([]); + }); +}); 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: {}