From d935d9c73e726c9947b6b8ab005460efd9584ac3 Mon Sep 17 00:00:00 2001 From: James Date: Mon, 7 Oct 2024 23:17:38 +0100 Subject: [PATCH 1/2] refactor: retrieve cache handler kv instance inside constructor --- .changeset/slow-dolphins-sparkle.md | 7 +++++++ packages/cloudflare/env.d.ts | 1 + .../cloudflare/src/cli/build/build-worker.ts | 19 +++++++++++++++++-- .../build/patches/investigated/patch-cache.ts | 12 ++++-------- .../build/utils/copy-prerendered-routes.ts | 2 +- .../cache-handler/constants.ts | 0 .../{ => templates}/cache-handler/index.ts | 0 .../cache-handler/open-next-cache-handler.ts | 18 ++++++++++-------- .../{ => templates}/cache-handler/utils.ts | 2 +- packages/cloudflare/tsup.config.ts | 2 +- 10 files changed, 42 insertions(+), 21 deletions(-) create mode 100644 .changeset/slow-dolphins-sparkle.md rename packages/cloudflare/src/cli/{ => templates}/cache-handler/constants.ts (100%) rename packages/cloudflare/src/cli/{ => templates}/cache-handler/index.ts (100%) rename packages/cloudflare/src/cli/{ => templates}/cache-handler/open-next-cache-handler.ts (88%) rename packages/cloudflare/src/cli/{ => templates}/cache-handler/utils.ts (93%) diff --git a/.changeset/slow-dolphins-sparkle.md b/.changeset/slow-dolphins-sparkle.md new file mode 100644 index 00000000..1cc5c28f --- /dev/null +++ b/.changeset/slow-dolphins-sparkle.md @@ -0,0 +1,7 @@ +--- +"@opennextjs/cloudflare": patch +--- + +refactor: retrieve cache handler kv instance inside constructor + +The cache handler was retrieving it's KV instance as a static property on the class that was defined at some point during the execution of the Next.js server. This moves the retrieval of the KV instance to happen inside the constructor for the class, so that it is retrieved during instantiation instead. diff --git a/packages/cloudflare/env.d.ts b/packages/cloudflare/env.d.ts index bea42567..b5e1c844 100644 --- a/packages/cloudflare/env.d.ts +++ b/packages/cloudflare/env.d.ts @@ -5,6 +5,7 @@ declare global { __NEXT_PRIVATE_STANDALONE_CONFIG?: string; SKIP_NEXT_APP_BUILD?: string; NEXT_PRIVATE_DEBUG_CACHE?: string; + __OPENNEXT_KV_BINDING_NAME: string; [key: string]: string | Fetcher; } } diff --git a/packages/cloudflare/src/cli/build/build-worker.ts b/packages/cloudflare/src/cli/build/build-worker.ts index becb7ba6..2293c939 100644 --- a/packages/cloudflare/src/cli/build/build-worker.ts +++ b/packages/cloudflare/src/cli/build/build-worker.ts @@ -1,4 +1,5 @@ import { Plugin, build } from "esbuild"; +import { cacheHandlerFileName, patchCache } from "./patches/investigated/patch-cache"; import { cp, readFile, writeFile } from "node:fs/promises"; import { existsSync, readFileSync } from "node:fs"; import { Config } from "../config"; @@ -8,7 +9,6 @@ import { fileURLToPath } from "node:url"; import { inlineEvalManifest } from "./patches/to-investigate/inline-eval-manifest"; import { inlineMiddlewareManifestRequire } from "./patches/to-investigate/inline-middleware-manifest-require"; import { inlineNextRequire } from "./patches/to-investigate/inline-next-require"; -import { patchCache } from "./patches/investigated/patch-cache"; import { patchExceptionBubbling } from "./patches/to-investigate/patch-exception-bubbling"; import { patchFindDir } from "./patches/to-investigate/patch-find-dir"; import { patchReadFile } from "./patches/to-investigate/patch-read-file"; @@ -53,6 +53,9 @@ export async function buildWorker(config: Config): Promise { const templateDir = path.join(config.paths.internalPackage, "cli", "templates"); + const cacheHandlerEntrypoint = path.join(templateDir, "cache-handler", "index.ts"); + const cacheHandlerOutputFile = path.join(config.paths.builderOutput, cacheHandlerFileName); + const workerEntrypoint = path.join(templateDir, "worker.ts"); const workerOutputFile = path.join(config.paths.builderOutput, "index.mjs"); @@ -66,6 +69,18 @@ export async function buildWorker(config: Config): Promise { patchWranglerDeps(config); updateWebpackChunksFile(config); + await build({ + entryPoints: [cacheHandlerEntrypoint], + bundle: true, + outfile: cacheHandlerOutputFile, + format: "esm", + target: "esnext", + minify: true, + define: { + "process.env.__OPENNEXT_KV_BINDING_NAME": `"${config.cache.kvBindingName}"`, + }, + }); + await build({ entryPoints: [workerEntrypoint], bundle: true, @@ -167,7 +182,7 @@ async function updateWorkerBundledCode(workerOutputFile: string, config: Config) patchedCode = inlineNextRequire(patchedCode, config); patchedCode = patchFindDir(patchedCode, config); patchedCode = inlineEvalManifest(patchedCode, config); - patchedCode = patchCache(patchedCode, config); + patchedCode = patchCache(patchedCode); patchedCode = inlineMiddlewareManifestRequire(patchedCode, config); patchedCode = patchExceptionBubbling(patchedCode); diff --git a/packages/cloudflare/src/cli/build/patches/investigated/patch-cache.ts b/packages/cloudflare/src/cli/build/patches/investigated/patch-cache.ts index 9b5ef064..7ca6b57d 100644 --- a/packages/cloudflare/src/cli/build/patches/investigated/patch-cache.ts +++ b/packages/cloudflare/src/cli/build/patches/investigated/patch-cache.ts @@ -1,24 +1,20 @@ -import { Config } from "../../../config"; -import path from "node:path"; +export const cacheHandlerFileName = "cache-handler.mjs"; /** * Install the cloudflare KV cache handler */ -export function patchCache(code: string, config: Config): string { +export function patchCache(code: string): string { console.log("# patchCache"); - const cacheHandler = path.join(config.paths.internalPackage, "cli", "cache-handler", "index.mjs"); - const patchedCode = code.replace( "const { cacheHandler } = this.nextConfig;", `const cacheHandler = null; -CacheHandler = (await import('${cacheHandler}')).OpenNextCacheHandler; -CacheHandler.maybeKVNamespace = process.env["${config.cache.kvBindingName}"]; +CacheHandler = (await import('./${cacheHandlerFileName}')).OpenNextCacheHandler; ` ); if (patchedCode === code) { - throw new Error("Cache patch not applied"); + throw new Error("Patch `patchCache` not applied"); } return patchedCode; diff --git a/packages/cloudflare/src/cli/build/utils/copy-prerendered-routes.ts b/packages/cloudflare/src/cli/build/utils/copy-prerendered-routes.ts index 96577394..6a960dde 100644 --- a/packages/cloudflare/src/cli/build/utils/copy-prerendered-routes.ts +++ b/packages/cloudflare/src/cli/build/utils/copy-prerendered-routes.ts @@ -1,4 +1,4 @@ -import { NEXT_META_SUFFIX, SEED_DATA_DIR } from "../../cache-handler"; +import { NEXT_META_SUFFIX, SEED_DATA_DIR } from "../../templates/cache-handler"; import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; import { dirname, join } from "node:path"; import { Config } from "../../config"; diff --git a/packages/cloudflare/src/cli/cache-handler/constants.ts b/packages/cloudflare/src/cli/templates/cache-handler/constants.ts similarity index 100% rename from packages/cloudflare/src/cli/cache-handler/constants.ts rename to packages/cloudflare/src/cli/templates/cache-handler/constants.ts diff --git a/packages/cloudflare/src/cli/cache-handler/index.ts b/packages/cloudflare/src/cli/templates/cache-handler/index.ts similarity index 100% rename from packages/cloudflare/src/cli/cache-handler/index.ts rename to packages/cloudflare/src/cli/templates/cache-handler/index.ts diff --git a/packages/cloudflare/src/cli/cache-handler/open-next-cache-handler.ts b/packages/cloudflare/src/cli/templates/cache-handler/open-next-cache-handler.ts similarity index 88% rename from packages/cloudflare/src/cli/cache-handler/open-next-cache-handler.ts rename to packages/cloudflare/src/cli/templates/cache-handler/open-next-cache-handler.ts index 46a664a8..090d0877 100644 --- a/packages/cloudflare/src/cli/cache-handler/open-next-cache-handler.ts +++ b/packages/cloudflare/src/cli/templates/cache-handler/open-next-cache-handler.ts @@ -13,7 +13,7 @@ import { } from "./constants"; import { getSeedBodyFile, getSeedMetaFile, getSeedTextFile, parseCtx } from "./utils"; import type { IncrementalCacheValue } from "next/dist/server/response-cache"; -import { KVNamespace } from "@cloudflare/workers-types"; +import type { KVNamespace } from "@cloudflare/workers-types"; type CacheEntry = { lastModified: number; @@ -21,11 +21,13 @@ type CacheEntry = { }; export class OpenNextCacheHandler implements CacheHandler { - static maybeKVNamespace: KVNamespace | undefined = undefined; + protected kv: KVNamespace | undefined; protected debug: boolean = !!process.env.NEXT_PRIVATE_DEBUG_CACHE; - constructor(protected ctx: CacheHandlerContext) {} + constructor(protected ctx: CacheHandlerContext) { + this.kv = process.env[process.env.__OPENNEXT_KV_BINDING_NAME] as KVNamespace | undefined; + } async get(...args: Parameters): Promise { const [key, _ctx] = args; @@ -33,9 +35,9 @@ export class OpenNextCacheHandler implements CacheHandler { if (this.debug) console.log(`cache - get: ${key}, ${ctx?.kind}`); - if (OpenNextCacheHandler.maybeKVNamespace !== undefined) { + if (this.kv !== undefined) { try { - const value = await OpenNextCacheHandler.maybeKVNamespace.get(key, "json"); + const value = await this.kv.get(key, "json"); if (value) return value; } catch (e) { console.error(`Failed to get value for key = ${key}: ${e}`); @@ -115,7 +117,7 @@ export class OpenNextCacheHandler implements CacheHandler { // eslint-disable-next-line @typescript-eslint/no-unused-vars const [key, entry, _ctx] = args; - if (OpenNextCacheHandler.maybeKVNamespace === undefined) { + if (this.kv === undefined) { return; } @@ -127,7 +129,7 @@ export class OpenNextCacheHandler implements CacheHandler { }; try { - await OpenNextCacheHandler.maybeKVNamespace.put(key, JSON.stringify(data)); + await this.kv.put(key, JSON.stringify(data)); } catch (e) { console.error(`Failed to set value for key = ${key}: ${e}`); } @@ -135,7 +137,7 @@ export class OpenNextCacheHandler implements CacheHandler { async revalidateTag(...args: Parameters) { const [tags] = args; - if (OpenNextCacheHandler.maybeKVNamespace === undefined) { + if (this.kv === undefined) { return; } diff --git a/packages/cloudflare/src/cli/cache-handler/utils.ts b/packages/cloudflare/src/cli/templates/cache-handler/utils.ts similarity index 93% rename from packages/cloudflare/src/cli/cache-handler/utils.ts rename to packages/cloudflare/src/cli/templates/cache-handler/utils.ts index 2241eecc..0ea4d04c 100644 --- a/packages/cloudflare/src/cli/cache-handler/utils.ts +++ b/packages/cloudflare/src/cli/templates/cache-handler/utils.ts @@ -1,4 +1,4 @@ -import { IncrementalCache } from "next/dist/server/lib/incremental-cache"; +import type { IncrementalCache } from "next/dist/server/lib/incremental-cache"; import { NEXT_META_SUFFIX } from "./constants"; type PrerenderedRouteMeta = { diff --git a/packages/cloudflare/tsup.config.ts b/packages/cloudflare/tsup.config.ts index ade3b9fa..692a1096 100644 --- a/packages/cloudflare/tsup.config.ts +++ b/packages/cloudflare/tsup.config.ts @@ -2,7 +2,7 @@ import { cp } from "node:fs/promises"; import { defineConfig } from "tsup"; const cliConfig = defineConfig({ - entry: ["src/cli/index.ts", "src/cli/cache-handler/index.ts"], + entry: ["src/cli/index.ts"], outDir: "dist/cli", dts: false, format: ["esm"], From 0dbf79c10f0ad7bf0f59be31de708c56ba175269 Mon Sep 17 00:00:00 2001 From: James Date: Tue, 8 Oct 2024 08:25:08 +0100 Subject: [PATCH 2/2] comments + move constants to separate directory --- .../cloudflare/src/cli/build/build-worker.ts | 37 +++++-------------- .../build/patches/investigated/patch-cache.ts | 22 ++++++++++- .../build/utils/copy-prerendered-routes.ts | 2 +- packages/cloudflare/src/cli/config.ts | 4 ++ .../incremental-cache.ts} | 0 .../src/cli/templates/cache-handler/index.ts | 1 - .../cache-handler/open-next-cache-handler.ts | 2 +- .../src/cli/templates/cache-handler/utils.ts | 2 +- packages/cloudflare/tsup.config.ts | 3 ++ 9 files changed, 40 insertions(+), 33 deletions(-) rename packages/cloudflare/src/cli/{templates/cache-handler/constants.ts => constants/incremental-cache.ts} (100%) diff --git a/packages/cloudflare/src/cli/build/build-worker.ts b/packages/cloudflare/src/cli/build/build-worker.ts index 2293c939..81c0dbb2 100644 --- a/packages/cloudflare/src/cli/build/build-worker.ts +++ b/packages/cloudflare/src/cli/build/build-worker.ts @@ -1,5 +1,4 @@ import { Plugin, build } from "esbuild"; -import { cacheHandlerFileName, patchCache } from "./patches/investigated/patch-cache"; import { cp, readFile, writeFile } from "node:fs/promises"; import { existsSync, readFileSync } from "node:fs"; import { Config } from "../config"; @@ -9,6 +8,7 @@ import { fileURLToPath } from "node:url"; import { inlineEvalManifest } from "./patches/to-investigate/inline-eval-manifest"; import { inlineMiddlewareManifestRequire } from "./patches/to-investigate/inline-middleware-manifest-require"; import { inlineNextRequire } from "./patches/to-investigate/inline-next-require"; +import { patchCache } from "./patches/investigated/patch-cache"; import { patchExceptionBubbling } from "./patches/to-investigate/patch-exception-bubbling"; import { patchFindDir } from "./patches/to-investigate/patch-find-dir"; import { patchReadFile } from "./patches/to-investigate/patch-read-file"; @@ -51,12 +51,7 @@ export async function buildWorker(config: Config): Promise { copyPackageCliFiles(packageDistDir, config); - const templateDir = path.join(config.paths.internalPackage, "cli", "templates"); - - const cacheHandlerEntrypoint = path.join(templateDir, "cache-handler", "index.ts"); - const cacheHandlerOutputFile = path.join(config.paths.builderOutput, cacheHandlerFileName); - - const workerEntrypoint = path.join(templateDir, "worker.ts"); + const workerEntrypoint = path.join(config.paths.internalTemplates, "worker.ts"); const workerOutputFile = path.join(config.paths.builderOutput, "index.mjs"); const nextConfigStr = @@ -69,18 +64,6 @@ export async function buildWorker(config: Config): Promise { patchWranglerDeps(config); updateWebpackChunksFile(config); - await build({ - entryPoints: [cacheHandlerEntrypoint], - bundle: true, - outfile: cacheHandlerOutputFile, - format: "esm", - target: "esnext", - minify: true, - define: { - "process.env.__OPENNEXT_KV_BINDING_NAME": `"${config.cache.kvBindingName}"`, - }, - }); - await build({ entryPoints: [workerEntrypoint], bundle: true, @@ -88,20 +71,20 @@ export async function buildWorker(config: Config): Promise { format: "esm", target: "esnext", minify: false, - plugins: [createFixRequiresESBuildPlugin(templateDir)], + plugins: [createFixRequiresESBuildPlugin(config)], alias: { // Note: we apply an empty shim to next/dist/compiled/ws because it generates two `eval`s: // eval("require")("bufferutil"); // eval("require")("utf-8-validate"); - "next/dist/compiled/ws": path.join(templateDir, "shims", "empty.ts"), + "next/dist/compiled/ws": path.join(config.paths.internalTemplates, "shims", "empty.ts"), // Note: we apply an empty shim to next/dist/compiled/edge-runtime since (amongst others) it generated the following `eval`: // eval(getModuleCode)(module, module.exports, throwingRequire, params.context, ...Object.values(params.scopedContext)); // which comes from https://github.com/vercel/edge-runtime/blob/6e96b55f/packages/primitives/src/primitives/load.js#L57-L63 // QUESTION: Why did I encountered this but mhart didn't? - "next/dist/compiled/edge-runtime": path.join(templateDir, "shims", "empty.ts"), + "next/dist/compiled/edge-runtime": path.join(config.paths.internalTemplates, "shims", "empty.ts"), // `@next/env` is a library Next.js uses for loading dotenv files, for obvious reasons we need to stub it here // source: https://github.com/vercel/next.js/tree/0ac10d79720/packages/next-env - "@next/env": path.join(templateDir, "shims", "env.ts"), + "@next/env": path.join(config.paths.internalTemplates, "shims", "env.ts"), }, define: { // config file used by Next.js, see: https://github.com/vercel/next.js/blob/68a7128/packages/next/src/build/utils.ts#L2137-L2139 @@ -182,23 +165,23 @@ async function updateWorkerBundledCode(workerOutputFile: string, config: Config) patchedCode = inlineNextRequire(patchedCode, config); patchedCode = patchFindDir(patchedCode, config); patchedCode = inlineEvalManifest(patchedCode, config); - patchedCode = patchCache(patchedCode); + patchedCode = await patchCache(patchedCode, config); patchedCode = inlineMiddlewareManifestRequire(patchedCode, config); patchedCode = patchExceptionBubbling(patchedCode); await writeFile(workerOutputFile, patchedCode); } -function createFixRequiresESBuildPlugin(templateDir: string): Plugin { +function createFixRequiresESBuildPlugin(config: Config): Plugin { return { name: "replaceRelative", setup(build) { // Note: we (empty) shim require-hook modules as they generate problematic code that uses requires build.onResolve({ filter: /^\.\/require-hook$/ }, () => ({ - path: path.join(templateDir, "shims", "empty.ts"), + path: path.join(config.paths.internalTemplates, "shims", "empty.ts"), })); build.onResolve({ filter: /\.\/lib\/node-fs-methods$/ }, () => ({ - path: path.join(templateDir, "shims", "empty.ts"), + path: path.join(config.paths.internalTemplates, "shims", "empty.ts"), })); }, }; diff --git a/packages/cloudflare/src/cli/build/patches/investigated/patch-cache.ts b/packages/cloudflare/src/cli/build/patches/investigated/patch-cache.ts index 7ca6b57d..1f5145d9 100644 --- a/packages/cloudflare/src/cli/build/patches/investigated/patch-cache.ts +++ b/packages/cloudflare/src/cli/build/patches/investigated/patch-cache.ts @@ -1,11 +1,29 @@ -export const cacheHandlerFileName = "cache-handler.mjs"; +import { Config } from "../../../config"; +import { build } from "esbuild"; +import { join } from "node:path"; /** * Install the cloudflare KV cache handler */ -export function patchCache(code: string): string { +export async function patchCache(code: string, config: Config): Promise { console.log("# patchCache"); + const cacheHandlerFileName = "cache-handler.mjs"; + const cacheHandlerEntrypoint = join(config.paths.internalTemplates, "cache-handler", "index.ts"); + const cacheHandlerOutputFile = join(config.paths.builderOutput, cacheHandlerFileName); + + await build({ + entryPoints: [cacheHandlerEntrypoint], + bundle: true, + outfile: cacheHandlerOutputFile, + format: "esm", + target: "esnext", + minify: true, + define: { + "process.env.__OPENNEXT_KV_BINDING_NAME": `"${config.cache.kvBindingName}"`, + }, + }); + const patchedCode = code.replace( "const { cacheHandler } = this.nextConfig;", `const cacheHandler = null; diff --git a/packages/cloudflare/src/cli/build/utils/copy-prerendered-routes.ts b/packages/cloudflare/src/cli/build/utils/copy-prerendered-routes.ts index 6a960dde..e2cecb6f 100644 --- a/packages/cloudflare/src/cli/build/utils/copy-prerendered-routes.ts +++ b/packages/cloudflare/src/cli/build/utils/copy-prerendered-routes.ts @@ -1,4 +1,4 @@ -import { NEXT_META_SUFFIX, SEED_DATA_DIR } from "../../templates/cache-handler"; +import { NEXT_META_SUFFIX, SEED_DATA_DIR } from "../../constants/incremental-cache"; import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; import { dirname, join } from "node:path"; import { Config } from "../../config"; diff --git a/packages/cloudflare/src/cli/config.ts b/packages/cloudflare/src/cli/config.ts index 05d2b6fc..b79d356d 100644 --- a/packages/cloudflare/src/cli/config.ts +++ b/packages/cloudflare/src/cli/config.ts @@ -31,6 +31,8 @@ export type Config = { standaloneAppServer: string; // Package in the standalone node_modules internalPackage: string; + // Templates in the package in the standalone node_modules + internalTemplates: string; }; cache: { @@ -59,6 +61,7 @@ export function getConfig(appDir: string, outputDir: string): Config { const nodeModules = path.join(standaloneApp, "node_modules"); const internalPackage = path.join(nodeModules, ...PACKAGE_NAME.split("/")); + const internalTemplates = path.join(internalPackage, "cli", "templates"); return { buildTimestamp: Date.now(), @@ -72,6 +75,7 @@ export function getConfig(appDir: string, outputDir: string): Config { standaloneAppDotNext, standaloneAppServer, internalPackage, + internalTemplates, }, cache: { diff --git a/packages/cloudflare/src/cli/templates/cache-handler/constants.ts b/packages/cloudflare/src/cli/constants/incremental-cache.ts similarity index 100% rename from packages/cloudflare/src/cli/templates/cache-handler/constants.ts rename to packages/cloudflare/src/cli/constants/incremental-cache.ts diff --git a/packages/cloudflare/src/cli/templates/cache-handler/index.ts b/packages/cloudflare/src/cli/templates/cache-handler/index.ts index 5d23b49d..46fc26df 100644 --- a/packages/cloudflare/src/cli/templates/cache-handler/index.ts +++ b/packages/cloudflare/src/cli/templates/cache-handler/index.ts @@ -1,2 +1 @@ -export * from "./constants"; export * from "./open-next-cache-handler"; diff --git a/packages/cloudflare/src/cli/templates/cache-handler/open-next-cache-handler.ts b/packages/cloudflare/src/cli/templates/cache-handler/open-next-cache-handler.ts index 090d0877..abfbe882 100644 --- a/packages/cloudflare/src/cli/templates/cache-handler/open-next-cache-handler.ts +++ b/packages/cloudflare/src/cli/templates/cache-handler/open-next-cache-handler.ts @@ -10,7 +10,7 @@ import { RSC_PREFETCH_SUFFIX, RSC_SUFFIX, SEED_DATA_DIR, -} from "./constants"; +} from "../../constants/incremental-cache"; import { getSeedBodyFile, getSeedMetaFile, getSeedTextFile, parseCtx } from "./utils"; import type { IncrementalCacheValue } from "next/dist/server/response-cache"; import type { KVNamespace } from "@cloudflare/workers-types"; diff --git a/packages/cloudflare/src/cli/templates/cache-handler/utils.ts b/packages/cloudflare/src/cli/templates/cache-handler/utils.ts index 0ea4d04c..b1e7d08a 100644 --- a/packages/cloudflare/src/cli/templates/cache-handler/utils.ts +++ b/packages/cloudflare/src/cli/templates/cache-handler/utils.ts @@ -1,5 +1,5 @@ import type { IncrementalCache } from "next/dist/server/lib/incremental-cache"; -import { NEXT_META_SUFFIX } from "./constants"; +import { NEXT_META_SUFFIX } from "../../constants/incremental-cache"; type PrerenderedRouteMeta = { lastModified: number; diff --git a/packages/cloudflare/tsup.config.ts b/packages/cloudflare/tsup.config.ts index 692a1096..42713089 100644 --- a/packages/cloudflare/tsup.config.ts +++ b/packages/cloudflare/tsup.config.ts @@ -10,6 +10,9 @@ const cliConfig = defineConfig({ external: ["esbuild"], clean: true, onSuccess: async () => { + await cp(`${__dirname}/src/cli/constants`, `${__dirname}/dist/cli/constants`, { + recursive: true, + }); await cp(`${__dirname}/src/cli/templates`, `${__dirname}/dist/cli/templates`, { recursive: true, });