├── .commitlintrc.json ├── .editorconfig ├── .gitattributes ├── .github ├── .kodiak.toml ├── CODEOWNERS ├── ISSUE_TEMPLATE │ ├── bug_report.yml │ └── feature_request.yml ├── PULL_REQUEST_TEMPLATE.md ├── codeql │ └── codeql-config.yml ├── styles │ └── .gitkeep └── workflows │ ├── benchmark-post.yml │ ├── benchmark.yml │ ├── codeql.yml │ ├── e2e-tests.yml │ ├── format.yml │ ├── fossa.yml │ ├── integration-tests.yml │ ├── lint.yml │ ├── pre-release-sha.yml │ ├── pre-release.yml │ ├── release-please.yml │ ├── typecheck.yml │ ├── unit-tests.yml │ ├── vale.yml │ └── verify-docs.yml ├── .gitignore ├── .prettierignore ├── .prettierrc.json ├── .vale.ini ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── __mocks__ ├── fs.js ├── fs │ └── promises.js └── write-file-atomic.js ├── bin └── run.js ├── cli.png ├── codecov.yml ├── docs ├── commands │ ├── api.md │ ├── blobs.md │ ├── build.md │ ├── clone.md │ ├── completion.md │ ├── db.md │ ├── deploy.md │ ├── dev.md │ ├── env.md │ ├── functions.md │ ├── init.md │ ├── link.md │ ├── login.md │ ├── logout.md │ ├── logs.md │ ├── open.md │ ├── recipes.md │ ├── serve.md │ ├── sites.md │ ├── status.md │ ├── switch.md │ ├── unlink.md │ └── watch.md └── index.md ├── e2e ├── install.e2e.ts └── utils.js ├── eslint.config.js ├── eslint_temporary_suppressions.js ├── functions-templates ├── go │ └── hello-world │ │ ├── .netlify-function-template.mjs │ │ ├── go.mod │ │ ├── go.sum │ │ └── main.go ├── javascript │ ├── hello-world │ │ ├── .netlify-function-template.mjs │ │ └── {{name}}.mjs │ ├── hello │ │ ├── .netlify-function-template.mjs │ │ └── {{name}}.js │ ├── image-external │ │ ├── .netlify-function-template.mjs │ │ └── {{name}}.js │ ├── localized-content │ │ ├── .netlify-function-template.mjs │ │ └── {{name}}.js │ ├── scheduled-function │ │ ├── .netlify-function-template.mjs │ │ ├── package.json │ │ └── {{name}}.mjs │ ├── set-cookies │ │ ├── .netlify-function-template.mjs │ │ └── {{name}}.js │ ├── set-req-header │ │ ├── .netlify-function-template.mjs │ │ └── {{name}}.js │ ├── set-res-header │ │ ├── .netlify-function-template.mjs │ │ └── {{name}}.js │ └── transform-response │ │ ├── .netlify-function-template.mjs │ │ └── {{name}}.js ├── rust │ └── hello-world │ │ ├── .gitignore │ │ ├── .netlify-function-template.mjs │ │ ├── Cargo.lock │ │ ├── Cargo.toml │ │ └── src │ │ └── main.rs └── typescript │ ├── abtest │ ├── .netlify-function-template.mjs │ └── {{name}}.ts │ ├── geolocation │ ├── .netlify-function-template.mjs │ └── {{name}}.ts │ ├── hello-world │ ├── .netlify-function-template.mjs │ ├── package-lock.json │ ├── package.json │ └── {{name}}.mts │ ├── json │ ├── .netlify-function-template.mjs │ └── {{name}}.ts │ ├── log │ ├── .netlify-function-template.mjs │ └── {{name}}.ts │ ├── scheduled-function │ ├── .netlify-function-template.mjs │ ├── package.json │ └── {{name}}.mts │ ├── set-cookies │ ├── .netlify-function-template.mjs │ └── {{name}}.ts │ ├── set-req-header │ ├── .netlify-function-template.mjs │ └── {{name}}.ts │ ├── set-res-header │ ├── .netlify-function-template.mjs │ └── {{name}}.ts │ └── transform-response │ ├── .netlify-function-template.mjs │ └── {{name}}.ts ├── layout.html ├── package-lock.json ├── package.json ├── renovate.json ├── scripts ├── bash.sh ├── fish.sh ├── netlifyPackage.js ├── path.ps1 ├── postinstall.js ├── prepublishOnly.js └── zsh.sh ├── site ├── README.md ├── astro.config.js ├── config.js ├── netlify.toml ├── netlify │ └── functions │ │ ├── error-reporting.ts │ │ └── telemetry.js ├── package-lock.json ├── package.json ├── public │ ├── favicon.svg │ └── og.png ├── scripts │ ├── docs.js │ ├── sync.js │ └── util │ │ ├── fs.js │ │ └── generate-command-data.js ├── src │ ├── assets │ │ ├── logo-dark.svg │ │ └── logo-light.svg │ ├── content.config.ts │ ├── fonts │ │ ├── font-face.css │ │ ├── mulishvar-italic-latin-nohint.woff2 │ │ ├── mulishvar-latin-nohint.woff2 │ │ └── pacaembuvar-latin-nohint.woff2 │ └── styles │ │ ├── colors.css │ │ ├── custom.css │ │ ├── sizes.css │ │ └── transitions.css └── tsconfig.json ├── src ├── commands │ ├── api │ │ ├── api.ts │ │ └── index.ts │ ├── base-command.ts │ ├── blobs │ │ ├── blobs-delete.ts │ │ ├── blobs-get.ts │ │ ├── blobs-list.ts │ │ ├── blobs-set.ts │ │ └── blobs.ts │ ├── build │ │ ├── build.ts │ │ └── index.ts │ ├── clone │ │ ├── clone.ts │ │ ├── index.ts │ │ └── option_values.ts │ ├── completion │ │ ├── completion.ts │ │ └── index.ts │ ├── database │ │ ├── constants.ts │ │ ├── database.ts │ │ ├── drizzle.ts │ │ ├── index.ts │ │ ├── init.ts │ │ ├── status.ts │ │ └── utils.ts │ ├── deploy │ │ ├── deploy.ts │ │ ├── index.ts │ │ └── option_values.ts │ ├── dev-exec │ │ ├── dev-exec.ts │ │ └── index.ts │ ├── dev │ │ ├── dev.ts │ │ ├── index.ts │ │ └── types.d.ts │ ├── env │ │ ├── env-clone.ts │ │ ├── env-get.ts │ │ ├── env-import.ts │ │ ├── env-list.ts │ │ ├── env-set.ts │ │ ├── env-unset.ts │ │ ├── env.ts │ │ └── index.ts │ ├── functions │ │ ├── functions-build.ts │ │ ├── functions-create.ts │ │ ├── functions-invoke.ts │ │ ├── functions-list.ts │ │ ├── functions-serve.ts │ │ ├── functions.ts │ │ └── index.ts │ ├── index.ts │ ├── init │ │ ├── constants.ts │ │ ├── index.ts │ │ └── init.ts │ ├── link │ │ ├── index.ts │ │ ├── link.ts │ │ └── option_values.ts │ ├── login │ │ ├── index.ts │ │ └── login.ts │ ├── logout │ │ ├── index.ts │ │ └── logout.ts │ ├── logs │ │ ├── build.ts │ │ ├── functions.ts │ │ ├── index.ts │ │ └── log-levels.ts │ ├── main.ts │ ├── open │ │ ├── index.ts │ │ ├── open-admin.ts │ │ ├── open-site.ts │ │ └── open.ts │ ├── recipes │ │ ├── common.ts │ │ ├── index.ts │ │ ├── recipes-list.ts │ │ └── recipes.ts │ ├── serve │ │ ├── index.ts │ │ └── serve.ts │ ├── sites │ │ ├── index.ts │ │ ├── sites-create-template.ts │ │ ├── sites-create.ts │ │ ├── sites-delete.ts │ │ ├── sites-list.ts │ │ └── sites.ts │ ├── status │ │ ├── index.ts │ │ ├── status-hooks.ts │ │ └── status.ts │ ├── switch │ │ ├── index.ts │ │ └── switch.ts │ ├── types.d.ts │ ├── unlink │ │ ├── index.ts │ │ └── unlink.ts │ └── watch │ │ ├── index.ts │ │ └── watch.ts ├── index.ts ├── lib │ ├── account.ts │ ├── api.ts │ ├── blobs │ │ └── blobs.ts │ ├── build.ts │ ├── completion │ │ ├── constants.ts │ │ ├── generate-autocompletion.ts │ │ ├── get-autocompletion.ts │ │ ├── index.ts │ │ └── script.ts │ ├── edge-functions │ │ ├── bootstrap.ts │ │ ├── consts.ts │ │ ├── deploy.ts │ │ ├── editor-helper.ts │ │ ├── headers.ts │ │ ├── proxy.ts │ │ └── registry.ts │ ├── exec-fetcher.ts │ ├── extensions.ts │ ├── fs.ts │ ├── functions │ │ ├── background.ts │ │ ├── config.ts │ │ ├── form-submissions-handler.ts │ │ ├── local-proxy.ts │ │ ├── memoized-build.ts │ │ ├── netlify-function.ts │ │ ├── registry.ts │ │ ├── runtimes │ │ │ ├── go │ │ │ │ └── index.ts │ │ │ ├── index.ts │ │ │ ├── js │ │ │ │ ├── builders │ │ │ │ │ └── zisi.ts │ │ │ │ ├── constants.ts │ │ │ │ ├── index.ts │ │ │ │ └── worker.ts │ │ │ └── rust │ │ │ │ └── index.ts │ │ ├── scheduled.ts │ │ ├── server.ts │ │ ├── synchronous.ts │ │ └── utils.ts │ ├── geo-location.ts │ ├── http-agent.ts │ ├── images │ │ └── proxy.ts │ ├── log.ts │ ├── path.ts │ ├── render-error-template.ts │ ├── settings.ts │ ├── spinner.ts │ ├── string.ts │ └── templates │ │ └── function-error.html ├── recipes │ ├── ai-context │ │ ├── context.ts │ │ └── index.ts │ ├── blobs-migrate │ │ └── index.ts │ └── vscode │ │ ├── index.ts │ │ └── settings.ts └── utils │ ├── addons │ └── prepare.ts │ ├── build-info.ts │ ├── cli-state.ts │ ├── command-helpers.ts │ ├── copy-template-dir │ └── copy-template-dir.ts │ ├── create-deferred.ts │ ├── create-stream-promise.ts │ ├── deploy │ ├── constants.ts │ ├── deploy-site.ts │ ├── hash-config.ts │ ├── hash-files.ts │ ├── hash-fns.ts │ ├── hasher-segments.ts │ ├── status-cb.ts │ ├── upload-files.ts │ └── util.ts │ ├── detect-server-settings.ts │ ├── dev-server-banner.ts │ ├── dev.ts │ ├── dot-env.ts │ ├── env │ └── index.ts │ ├── execa.ts │ ├── feature-flags.ts │ ├── framework-server.ts │ ├── frameworks-api.ts │ ├── functions │ ├── constants.ts │ ├── functions.ts │ ├── get-functions.ts │ └── index.ts │ ├── get-cli-package-json.ts │ ├── get-global-config-store.ts │ ├── get-repo-data.ts │ ├── get-site.ts │ ├── gh-auth.ts │ ├── gitignore.ts │ ├── headers.ts │ ├── hooks │ └── requires-site-info.ts │ ├── init │ ├── config-github.ts │ ├── config-manual.ts │ ├── config.ts │ ├── plugins.ts │ └── utils.ts │ ├── live-tunnel.ts │ ├── multimap.ts │ ├── nodejs-compile-cache.ts │ ├── normalize-repo-url.ts │ ├── open-browser.ts │ ├── parse-raw-flags.ts │ ├── prompts │ ├── blob-delete-prompts.ts │ ├── blob-set-prompt.ts │ ├── confirm-prompt.ts │ ├── env-clone-prompt.ts │ ├── env-set-prompts.ts │ ├── env-unset-prompts.ts │ └── prompt-messages.ts │ ├── proxy-server.ts │ ├── proxy.ts │ ├── read-repo-url.ts │ ├── redirects.ts │ ├── request-id.ts │ ├── rules-proxy.ts │ ├── run-build.ts │ ├── run-git.ts │ ├── run-program.ts │ ├── scripted-commands.ts │ ├── shell.ts │ ├── sign-redirect.ts │ ├── sites │ ├── create-template.ts │ └── utils.ts │ ├── static-server.ts │ ├── telemetry │ ├── index.ts │ ├── report-error.ts │ ├── request.ts │ ├── telemetry.ts │ ├── utils.ts │ └── validation.ts │ ├── temporary-file.ts │ ├── types.ts │ ├── validation.ts │ └── websockets │ └── index.ts ├── tests ├── integration │ ├── __fixtures__ │ │ ├── dev-server-with-edge-functions-and-npm-modules │ │ │ ├── netlify.toml │ │ │ ├── netlify │ │ │ │ └── edge-functions │ │ │ │ │ └── blobs.ts │ │ │ ├── package-lock.json │ │ │ ├── package.json │ │ │ └── public │ │ │ │ └── ordertest.html │ │ ├── dev-server-with-edge-functions │ │ │ ├── .netlify │ │ │ │ └── _edge-functions │ │ │ │ │ ├── integration-iscA.ts │ │ │ │ │ ├── integration-iscB.ts │ │ │ │ │ ├── integration-manifestA.ts │ │ │ │ │ ├── integration-manifestB.ts │ │ │ │ │ ├── integration-manifestC.ts │ │ │ │ │ └── manifest.json │ │ │ ├── netlify.toml │ │ │ ├── netlify │ │ │ │ └── edge-functions │ │ │ │ │ ├── context-with-params.ts │ │ │ │ │ ├── context.ts │ │ │ │ │ ├── delete-product.js │ │ │ │ │ ├── echo-env.ts │ │ │ │ │ ├── manual-cache-context-with-params.ts │ │ │ │ │ ├── uncaught-exception.ts │ │ │ │ │ ├── user-iscA.ts │ │ │ │ │ ├── user-iscB.ts │ │ │ │ │ ├── user-tomlA.ts │ │ │ │ │ ├── user-tomlB.ts │ │ │ │ │ └── user-tomlC.ts │ │ │ ├── public │ │ │ │ └── ordertest.html │ │ │ └── src │ │ │ │ └── edge-function.ts │ │ ├── dev-server-with-functions │ │ │ ├── functions │ │ │ │ ├── echo.js │ │ │ │ ├── identity-validate-background.js │ │ │ │ ├── ping.js │ │ │ │ ├── scheduled-isc-body.js │ │ │ │ ├── scheduled-isc.js │ │ │ │ ├── scheduled-v2.mjs │ │ │ │ └── scheduled.js │ │ │ ├── netlify.toml │ │ │ └── node_modules │ │ │ │ └── @netlify │ │ │ │ └── functions │ │ │ │ ├── index.js │ │ │ │ └── package.json │ │ ├── dev-server-with-v2-functions │ │ │ ├── functions │ │ │ │ ├── blobs.js │ │ │ │ ├── brotli.js │ │ │ │ ├── context.js │ │ │ │ ├── custom-path-catchall.js │ │ │ │ ├── custom-path-excluded.js │ │ │ │ ├── custom-path-expression.js │ │ │ │ ├── custom-path-literal.js │ │ │ │ ├── custom-path-root.js │ │ │ │ ├── delete.js │ │ │ │ ├── favicon.js │ │ │ │ ├── log.js │ │ │ │ ├── ping-ts.ts │ │ │ │ ├── ping.js │ │ │ │ ├── shadow-imagecdn.js │ │ │ │ ├── stream.js │ │ │ │ └── uncaught-exception.js │ │ │ ├── netlify.toml │ │ │ ├── package-lock.json │ │ │ ├── package.json │ │ │ └── public │ │ │ │ ├── products.html │ │ │ │ ├── products │ │ │ │ └── static.html │ │ │ │ ├── test.png │ │ │ │ ├── v2-to-custom-without-force.html │ │ │ │ └── v2-to-legacy-without-force.html │ │ ├── empty-project │ │ │ └── .gitkeep │ │ ├── hugo-site │ │ │ ├── .gitignore │ │ │ ├── config.toml │ │ │ ├── content │ │ │ │ └── _index.html │ │ │ ├── layouts │ │ │ │ └── _default │ │ │ │ │ └── list.html │ │ │ ├── netlify.toml │ │ │ ├── package-lock.json │ │ │ ├── package.json │ │ │ └── static │ │ │ │ └── _redirects │ │ ├── images │ │ │ └── test.jpg │ │ ├── monorepo │ │ │ ├── .gitignore │ │ │ ├── .npmrc │ │ │ ├── package.json │ │ │ ├── packages │ │ │ │ └── app-1 │ │ │ │ │ ├── dist │ │ │ │ │ └── index.html │ │ │ │ │ ├── netlify.toml │ │ │ │ │ └── package.json │ │ │ ├── pnpm-lock.yaml │ │ │ ├── pnpm-workspace.yaml │ │ │ └── tools │ │ │ │ └── build-plugin │ │ │ │ ├── manifest.yaml │ │ │ │ ├── package.json │ │ │ │ └── src │ │ │ │ └── index.ts │ │ ├── next-app-without-config │ │ │ ├── .gitignore │ │ │ ├── app │ │ │ │ ├── favicon.ico │ │ │ │ ├── globals.css │ │ │ │ ├── layout.js │ │ │ │ ├── page.js │ │ │ │ └── page.module.css │ │ │ ├── jsconfig.json │ │ │ ├── next.config.mjs │ │ │ ├── package-lock.json │ │ │ ├── package.json │ │ │ └── public │ │ │ │ ├── next.svg │ │ │ │ └── vercel.svg │ │ ├── next-app │ │ │ ├── .gitignore │ │ │ ├── app │ │ │ │ ├── favicon.ico │ │ │ │ ├── globals.css │ │ │ │ ├── layout.js │ │ │ │ ├── page.js │ │ │ │ └── page.module.css │ │ │ ├── netlify.toml │ │ │ ├── next.config.mjs │ │ │ ├── package-lock.json │ │ │ ├── package.json │ │ │ └── public │ │ │ │ ├── next.svg │ │ │ │ └── test.txt │ │ ├── nx-integrated-monorepo │ │ │ ├── nx.json │ │ │ ├── package.json │ │ │ └── packages │ │ │ │ ├── blog │ │ │ │ └── project.json │ │ │ │ └── website │ │ │ │ └── project.json │ │ └── plugin-changing-publish-dir │ │ │ ├── dist │ │ │ └── index.html │ │ │ ├── netlify.toml │ │ │ └── plugin │ │ │ ├── index.js │ │ │ ├── manifest.yml │ │ │ └── package.json │ ├── __snapshots__ │ │ └── framework-detection.test.ts.snap │ ├── assets │ │ └── bundled-function-1.zip │ ├── commands │ │ ├── blobs │ │ │ ├── blobs-delete.test.ts │ │ │ ├── blobs-set.test.ts │ │ │ └── blobs.test.ts │ │ ├── build │ │ │ ├── build-program.test.ts │ │ │ └── build.test.ts │ │ ├── clone │ │ │ └── clone.test.ts │ │ ├── completion │ │ │ └── completion-install.test.ts │ │ ├── deploy │ │ │ └── deploy.test.ts │ │ ├── dev │ │ │ ├── dev-forms-and-redirects.test.ts │ │ │ ├── dev-miscellaneous.test.ts │ │ │ ├── dev.config.test.ts │ │ │ ├── dev.exec.test.ts │ │ │ ├── dev.geo.test.ts │ │ │ ├── dev.test.ts │ │ │ ├── dev.zisi.test.ts │ │ │ ├── edge-functions.test.ts │ │ │ ├── functions.test.ts │ │ │ ├── images.test.ts │ │ │ ├── redirects.test.ts │ │ │ ├── responses.dev.test.ts │ │ │ ├── scheduled-functions.test.ts │ │ │ ├── serve.test.ts │ │ │ └── v2-api.test.ts │ │ ├── didyoumean │ │ │ ├── __snapshots__ │ │ │ │ └── didyoumean.test.ts.snap │ │ │ └── didyoumean.test.ts │ │ ├── env │ │ │ ├── __snapshots__ │ │ │ │ └── env.test.ts.snap │ │ │ ├── api-routes.ts │ │ │ ├── env-clone.test.ts │ │ │ ├── env-get.test.ts │ │ │ ├── env-list.test.ts │ │ │ ├── env-set.test.ts │ │ │ ├── env-unset.test.ts │ │ │ └── env.test.ts │ │ ├── envelope │ │ │ ├── __snapshots__ │ │ │ │ └── envelope.test.ts.snap │ │ │ └── envelope.test.ts │ │ ├── functions-create │ │ │ ├── __snapshots__ │ │ │ │ └── functions-create.test.ts.snap │ │ │ └── functions-create.test.ts │ │ ├── functions-invoke │ │ │ └── functions-invoke.test.ts │ │ ├── functions-serve │ │ │ └── functions-serve.test.ts │ │ ├── functions-with-args │ │ │ └── functions-with-args.test.ts │ │ ├── help │ │ │ ├── __snapshots__ │ │ │ │ └── help.test.ts.snap │ │ │ └── help.test.ts │ │ ├── init │ │ │ └── init.test.ts │ │ ├── link │ │ │ └── link.test.ts │ │ ├── logs │ │ │ ├── build.test.ts │ │ │ └── functions.test.ts │ │ ├── recipes │ │ │ ├── __snapshots__ │ │ │ │ └── recipes.test.ts.snap │ │ │ └── recipes.test.ts │ │ ├── sites │ │ │ ├── sites-create-template.test.ts │ │ │ └── sites.test.ts │ │ └── status │ │ │ ├── __snapshots__ │ │ │ └── status.test.ts.snap │ │ │ └── status.test.ts │ ├── framework-detection.test.ts │ ├── frameworks │ │ └── hugo.test.ts │ ├── rules-proxy.test.ts │ ├── serve │ │ ├── functions-go.test.ts │ │ └── functions-rust.test.ts │ ├── telemetry.test.ts │ └── utils │ │ ├── call-cli.ts │ │ ├── cli-path.ts │ │ ├── create-live-test-site.ts │ │ ├── curl.ts │ │ ├── dev-server.ts │ │ ├── external-server-cli.ts │ │ ├── external-server.ts │ │ ├── fixture.ts │ │ ├── handle-questions.ts │ │ ├── inquirer-mock-prompt.ts │ │ ├── mock-api-vitest.ts │ │ ├── mock-api.ts │ │ ├── mock-execa.ts │ │ ├── mock-program.ts │ │ ├── pause.ts │ │ ├── process.ts │ │ ├── site-builder.ts │ │ └── snapshots.ts └── unit │ ├── lib │ ├── account.test.ts │ ├── completion │ │ ├── __snapshots__ │ │ │ └── generate-autocompletion.test.ts.snap │ │ ├── generate-autocompletion.test.ts │ │ └── get-autocompletion.test.ts │ ├── edge-functions │ │ ├── bootstrap.test.ts │ │ └── proxy.test.ts │ ├── exec-fetcher.test.ts │ ├── functions │ │ ├── netlify-function.test.ts │ │ ├── registry.test.ts │ │ ├── runtimes │ │ │ ├── go │ │ │ │ └── index.test.ts │ │ │ └── rust │ │ │ │ └── index.test.ts │ │ ├── scheduled.test.ts │ │ └── server.test.ts │ ├── geo-location.test.ts │ ├── http-agent.test.ts │ └── images │ │ └── proxy.test.ts │ ├── recipes │ └── ai-context │ │ ├── context.test.ts │ │ └── download-context-files.test.ts │ └── utils │ ├── command-helpers.test.ts │ ├── copy-template-dir │ ├── copy-template-dir.test.ts │ └── fixtures │ │ ├── 1.txt │ │ ├── 2.txt │ │ ├── 3.txt │ │ ├── _.a │ │ ├── _c │ │ ├── foo │ │ ├── 4.txt │ │ ├── _.b │ │ └── _d │ │ ├── {{foo}}.txt │ │ └── {{foo}} │ │ └── 5.txt │ ├── deploy │ ├── hash-files.test.ts │ ├── hash-fns.test.ts │ ├── upload-files.test.ts │ └── util.test.ts │ ├── dot-env.test.ts │ ├── env │ └── index.test.ts │ ├── feature-flags.test.ts │ ├── functions │ └── get-functions.test.ts │ ├── get-cli-package-json.test.ts │ ├── get-global-config-store.test.ts │ ├── gh-auth.test.ts │ ├── headers.test.ts │ ├── init │ └── config-github.test.ts │ ├── normalize-repo-url.test.ts │ ├── parse-raw-flags.test.ts │ ├── read-repo-url.test.ts │ ├── redirects.test.ts │ ├── rules-proxy.test.ts │ └── telemetry │ └── validation.test.ts ├── tsconfig.base.json ├── tsconfig.build.json ├── tsconfig.json ├── types ├── ascii-table │ └── index.d.ts ├── express-logging │ └── index.d.ts ├── lambda-local │ └── index.d.ts ├── maxstache-stream │ └── index.d.ts ├── maxstache │ └── index.d.ts ├── netlify-redirector │ └── index.d.ts └── tomlify-j04 │ └── index.d.ts ├── vitest.config.ts └── vitest.e2e.config.ts /.commitlintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["@commitlint/config-conventional"] 3 | } 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | max_line_length = 120 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /.github/.kodiak.toml: -------------------------------------------------------------------------------- 1 | version = 1 2 | 3 | [merge.automerge_dependencies] 4 | versions = ["minor", "patch"] 5 | usernames = ["renovate"] 6 | 7 | [approve] 8 | auto_approve_usernames = ["renovate"] -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @netlify/composable-platform 2 | docs/ @netlify/composable-platform @netlify/department-docs 3 | site/ @netlify/composable-platform @netlify/department-docs 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: Bug report 2 | description: Create a report to help us improve 3 | title: Please replace with a clear and descriptive title 4 | labels: ['type: bug'] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: Thanks for reporting this bug! 9 | - type: textarea 10 | attributes: 11 | label: Describe the bug 12 | placeholder: A clear and concise description of what the bug is. 13 | validations: 14 | required: true 15 | - type: textarea 16 | attributes: 17 | label: Steps to reproduce 18 | placeholder: | 19 | Step-by-step instructions on how to reproduce the behavior. 20 | 21 | Example: 22 | 1. Run `git clone git@github.com:owner/repo-with-cli-bug.git` 23 | 2. Navigate to the cloned repository 24 | 3. Run `netlify dev` 25 | 4. See that the CLI exits with an error 26 | validations: 27 | required: true 28 | - type: textarea 29 | attributes: 30 | label: Configuration 31 | placeholder: If possible, please copy/paste below your `netlify.toml`. 32 | - type: textarea 33 | attributes: 34 | label: Environment 35 | description: | 36 | Enter the following command in a terminal and copy/paste its output: 37 | ```bash 38 | npx envinfo --system --binaries --npmPackages netlify-cli --npmGlobalPackages netlify-cli 39 | ``` 40 | validations: 41 | required: true 42 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: Feature request 2 | description: Suggest an idea for this project 3 | title: Please replace with a clear and descriptive title 4 | labels: ['type: feature'] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: Thanks for suggesting a new feature! 9 | - type: textarea 10 | attributes: 11 | label: Which problem is this feature request solving? 12 | placeholder: I'm always frustrated when [...] 13 | validations: 14 | required: true 15 | - type: textarea 16 | attributes: 17 | label: Describe the solution you'd like 18 | placeholder: This could be fixed by [...] 19 | validations: 20 | required: true 21 | - type: checkboxes 22 | attributes: 23 | label: Pull request (optional) 24 | description: 25 | Pull requests are welcome! If you would like to help us fix this bug, please check our [contributions 26 | guidelines](../blob/main/CONTRIBUTING.md). 27 | options: 28 | - label: I can submit a pull request. 29 | required: false 30 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 🎉 Thanks for submitting a pull request! 🎉 2 | 3 | #### Summary 4 | 5 | Fixes # 6 | 7 | 10 | 11 | --- 12 | 13 | For us to review and ship your PR efficiently, please perform the following steps: 14 | 15 | - [ ] Open a [bug/issue](https://github.com/netlify/cli/issues/new/choose) before writing your code 🧑‍💻. This ensures we can discuss the changes and get feedback from everyone that should be involved. If you\`re fixing a typo or something that\`s on fire 🔥 (e.g. incident related), you can skip this step. 16 | - [ ] Read the [contribution guidelines](../CONTRIBUTING.md) 📖. This ensures your code follows our style guide and 17 | passes our tests. 18 | - [ ] Update or add tests (if any source code was changed or added) 🧪 19 | - [ ] Update or add documentation (if features were changed or added) 📝 20 | - [ ] Make sure the status checks below are successful ✅ 21 | 22 | **A picture of a cute animal (not mandatory, but encouraged)** 23 | -------------------------------------------------------------------------------- /.github/codeql/codeql-config.yml: -------------------------------------------------------------------------------- 1 | paths-ignore: 2 | - '**/*.test.js' 3 | -------------------------------------------------------------------------------- /.github/styles/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netlify/cli/18ba162a78ab98eed3f93dde0a537485bcccb450/.github/styles/.gitkeep -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: CodeQL 3 | 4 | on: 5 | push: 6 | branches: 7 | - main 8 | pull_request: 9 | branches: 10 | - '**' 11 | - '!release-please--**' 12 | schedule: 13 | - cron: '0 0 * * 0' 14 | workflow_dispatch: 15 | 16 | jobs: 17 | analyze: 18 | name: Analyze 19 | runs-on: ubuntu-latest 20 | permissions: 21 | actions: read 22 | contents: read 23 | security-events: write 24 | 25 | steps: 26 | - name: Checkout repository 27 | uses: actions/checkout@v4 28 | 29 | - name: Initialize CodeQL 30 | uses: github/codeql-action/init@v3 31 | with: 32 | config-file: ./.github/codeql/codeql-config.yml 33 | languages: 'javascript' 34 | 35 | - name: Perform CodeQL Analysis 36 | uses: github/codeql-action/analyze@v3 37 | -------------------------------------------------------------------------------- /.github/workflows/format.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Format 3 | 4 | on: 5 | push: 6 | branches: 7 | - main 8 | pull_request: 9 | branches: 10 | - '**' 11 | - '!release-please--**' 12 | 13 | jobs: 14 | format: 15 | name: Format 16 | runs-on: ubuntu-24.04 17 | steps: 18 | - uses: actions/checkout@v4 19 | 20 | - uses: actions/setup-node@v4 21 | with: 22 | node-version: 20.12.2 23 | cache: npm 24 | 25 | - name: Install dependencies 26 | run: npm ci --no-audit 27 | 28 | - name: Check formatting 29 | run: npm run format:check 30 | -------------------------------------------------------------------------------- /.github/workflows/fossa.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Dependency License Scanning 3 | 4 | on: 5 | push: 6 | branches: 7 | - main 8 | - chore/fossa-workflow 9 | 10 | defaults: 11 | run: 12 | shell: bash 13 | 14 | jobs: 15 | fossa: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: Checkout 19 | uses: actions/checkout@v4 20 | 21 | - name: Download fossa cli 22 | run: |- 23 | mkdir -p $HOME/.local/bin 24 | curl https://raw.githubusercontent.com/fossas/fossa-cli/master/install.sh | bash -s -- -b $HOME/.local/bin 25 | echo "$HOME/.local/bin" >> $GITHUB_PATH 26 | 27 | - name: Fossa init 28 | run: fossa init 29 | 30 | - name: Set env 31 | run: echo "line_number=$(grep -n "project" .fossa.yml | cut -f1 -d:)" >> $GITHUB_ENV 32 | 33 | - name: Configuration 34 | run: |- 35 | sed -i "${line_number}s|.*| project: git@github.com:${GITHUB_REPOSITORY}.git|" .fossa.yml 36 | cat .fossa.yml 37 | 38 | - name: Upload dependencies 39 | run: fossa analyze --debug 40 | env: 41 | FOSSA_API_KEY: ${{ secrets.FOSSA_API_KEY }} 42 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Lint 3 | 4 | on: 5 | push: 6 | branches: 7 | - main 8 | pull_request: 9 | branches: 10 | - '**' 11 | - '!release-please--**' 12 | 13 | jobs: 14 | lint: 15 | name: Lint 16 | runs-on: ubuntu-24.04 17 | steps: 18 | - uses: actions/checkout@v4 19 | 20 | - uses: actions/setup-node@v4 21 | with: 22 | node-version: 20.12.2 23 | cache: npm 24 | 25 | - name: Install dependencies 26 | run: npm ci --no-audit 27 | 28 | - name: Lint 29 | run: npm run lint 30 | -------------------------------------------------------------------------------- /.github/workflows/typecheck.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Typecheck 3 | 4 | on: 5 | push: 6 | branches: 7 | - main 8 | pull_request: 9 | branches: 10 | - '**' 11 | - '!release-please--**' 12 | 13 | jobs: 14 | typecheck: 15 | runs-on: ubuntu-24.04 16 | steps: 17 | - uses: actions/checkout@v4 18 | 19 | - uses: actions/setup-node@v4 20 | with: 21 | node-version: 20.12.2 22 | cache: npm 23 | 24 | - name: Install dependencies 25 | run: npm ci --no-audit 26 | 27 | - name: Typecheck 28 | run: npm run typecheck 29 | -------------------------------------------------------------------------------- /.github/workflows/unit-tests.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Unit Tests 3 | 4 | on: 5 | push: 6 | branches: 7 | - main 8 | pull_request: 9 | branches: 10 | - '**' 11 | - '!release-please--**' 12 | 13 | jobs: 14 | unit: 15 | name: Unit 16 | runs-on: ${{ matrix.os }} 17 | strategy: 18 | matrix: 19 | os: [ubuntu-latest, macOS-latest, windows-latest] 20 | node-version: ['20.12.2', '22.x'] 21 | exclude: 22 | - os: windows-latest 23 | node-version: '22.x' 24 | fail-fast: false 25 | steps: 26 | - name: Git checkout 27 | uses: actions/checkout@v4 28 | 29 | - name: Use Node.js ${{ matrix.node-version }} 30 | uses: actions/setup-node@v4 31 | with: 32 | node-version: ${{ matrix.node-version }} 33 | cache: npm 34 | check-latest: true 35 | 36 | - name: Install core dependencies 37 | run: npm ci --no-audit 38 | 39 | - name: Build project 40 | run: npm run build 41 | 42 | - name: Run unit tests 43 | run: npm run test:unit -- --coverage 44 | -------------------------------------------------------------------------------- /.github/workflows/vale.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Lint Docs 3 | 4 | on: 5 | push: 6 | branches: 7 | - main 8 | pull_request: 9 | branches: 10 | - '**' 11 | - '!release-please--**' 12 | 13 | permissions: 14 | checks: write 15 | 16 | jobs: 17 | lint-docs: 18 | runs-on: ubuntu-22.04 19 | steps: 20 | - name: Git checkout 21 | uses: actions/checkout@v4 22 | 23 | - name: Download styles 24 | run: | 25 | curl -s https://vale-library.netlify.app/styles.zip -o styles.zip 26 | unzip styles.zip -d .github/styles 27 | rm styles.zip 28 | 29 | - name: Vale 30 | uses: errata-ai/vale-action@v2 31 | with: 32 | files: '["docs", "src", "README.md", "CODE_OF_CONDUCT.md", "CONTRIBUTING.md"]' 33 | fail_on_error: true 34 | env: 35 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 36 | -------------------------------------------------------------------------------- /.github/workflows/verify-docs.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Verify Docs 3 | 4 | on: 5 | push: 6 | branches: 7 | - main 8 | pull_request: 9 | branches: 10 | - '**' 11 | - '!release-please--**' 12 | 13 | jobs: 14 | verify-docs: 15 | runs-on: ubuntu-latest 16 | timeout-minutes: 10 17 | steps: 18 | - uses: actions/checkout@v4 19 | 20 | - uses: actions/setup-node@v4 21 | with: 22 | node-version: lts/* 23 | cache: npm 24 | 25 | - name: Install dependencies 26 | run: npm ci --no-audit 27 | 28 | - name: Install ./site dependencies 29 | run: npm ci --prefix=site --no-audit 30 | 31 | - name: Generate docs 32 | run: npm run --prefix=site build 33 | 34 | - name: Check for changes 35 | run: | 36 | if [ -z "$(git status --porcelain)" ]; then 37 | echo "No changes to docs files detected" 38 | else 39 | echo "Changes to docs files detected, please run 'npm run docs' to sync docs" 40 | exit 1 41 | fi 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *-debug.log 2 | *-error.log 3 | /dist 4 | /tmp 5 | node_modules 6 | !**/__fixtures__/**/node_modules 7 | misc 8 | coverage 9 | .idea 10 | .vscode 11 | !.vscode/launch.json 12 | .github/styles 13 | 14 | 15 | # node sdk 16 | vendor 17 | 18 | # osx 19 | .DS_STORE 20 | 21 | # Local Netlify folder 22 | /.netlify 23 | 24 | # site 25 | site/dist 26 | site/.astro/ 27 | site/src/**/*.md 28 | 29 | # tests 30 | .verdaccio-storage 31 | .eslintcache 32 | _test_out/** 33 | *.crt 34 | *.key 35 | 36 | # Used in local dev by tsc: https://www.typescriptlang.org/tsconfig/#tsBuildInfoFile 37 | tsconfig.tsbuildinfo 38 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | src/**/*.js 3 | src/**/*.d.ts 4 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "singleQuote": true, 4 | "printWidth": 120, 5 | "proseWrap": "always", 6 | "trailingComma": "all" 7 | } 8 | -------------------------------------------------------------------------------- /.vale.ini: -------------------------------------------------------------------------------- 1 | StylesPath = ./.github/styles 2 | MinAlertLevel = suggestion 3 | 4 | [*.md] 5 | BasedOnStyles = base, netlify-terms, smart-marks 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 Netlify 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /__mocks__/fs.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line no-undef, @typescript-eslint/no-require-imports 2 | const { fs } = require('memfs') 3 | 4 | // eslint-disable-next-line no-undef 5 | module.exports = fs 6 | -------------------------------------------------------------------------------- /__mocks__/fs/promises.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line no-undef, @typescript-eslint/no-require-imports 2 | const { fs } = require('memfs') 3 | 4 | // eslint-disable-next-line no-undef 5 | module.exports = fs.promises 6 | -------------------------------------------------------------------------------- /__mocks__/write-file-atomic.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line no-undef, @typescript-eslint/no-require-imports 2 | const { fs } = require('memfs') 3 | 4 | export const sync = fs.writeFileSync 5 | export default fs.writeFile 6 | -------------------------------------------------------------------------------- /cli.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netlify/cli/18ba162a78ab98eed3f93dde0a537485bcccb450/cli.png -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | codecov: 2 | strict_yaml_branch: main 3 | coverage: 4 | range: [80, 100] 5 | parsers: 6 | javascript: 7 | enable_partials: true 8 | status: 9 | project: 10 | default: 11 | informational: true 12 | patch: 13 | default: 14 | informational: true 15 | comment: false 16 | -------------------------------------------------------------------------------- /docs/commands/api.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Netlify CLI api command 3 | sidebar: 4 | label: api 5 | --- 6 | 7 | # `api` 8 | 9 | The `api` command will let you call any [Netlify open API methods](https://open-api.netlify.com/) 10 | 11 | 12 | Run any Netlify API method 13 | For more information on available methods check out https://open-api.netlify.com/ or run 'netlify api --list' 14 | 15 | **Usage** 16 | 17 | ```bash 18 | netlify api 19 | ``` 20 | 21 | **Arguments** 22 | 23 | - apiMethod - Open API method to run 24 | 25 | **Flags** 26 | 27 | - `data` (*string*) - Data to use 28 | - `list` (*boolean*) - List out available API methods 29 | - `debug` (*boolean*) - Print debugging information 30 | - `auth` (*string*) - Netlify auth token - can be used to run this command without logging in 31 | 32 | **Examples** 33 | 34 | ```bash 35 | netlify api --list 36 | netlify api getSite --data '{ "site_id": "123456" }' 37 | ``` 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /docs/commands/build.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Netlify CLI build command 3 | sidebar: 4 | label: build 5 | --- 6 | 7 | # `build` 8 | 9 | 10 | Build on your local machine 11 | 12 | **Usage** 13 | 14 | ```bash 15 | netlify build 16 | ``` 17 | 18 | **Flags** 19 | 20 | - `context` (*string*) - Specify a deploy context for environment variables read during the build (”production”, ”deploy-preview”, ”branch-deploy”, ”dev”) or `branch:your-branch` where `your-branch` is the name of a branch (default: value of CONTEXT or ”production”) 21 | - `dry` (*boolean*) - Dry run: show instructions without running them 22 | - `filter` (*string*) - For monorepos, specify the name of the application to run the command in 23 | - `debug` (*boolean*) - Print debugging information 24 | - `auth` (*string*) - Netlify auth token - can be used to run this command without logging in 25 | - `offline` (*boolean*) - Disables any features that require network access 26 | 27 | **Examples** 28 | 29 | ```bash 30 | netlify build 31 | netlify build --context deploy-preview # Build with env var values from deploy-preview context 32 | netlify build --context branch:feat/make-it-pop # Build with env var values from the feat/make-it-pop branch context or branch-deploy context 33 | ``` 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /docs/commands/completion.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Netlify CLI completion command 3 | sidebar: 4 | label: completion 5 | description: Shell completion script for netlify CLI 6 | --- 7 | 8 | # `completion` 9 | 10 | 11 | Generate shell completion script 12 | Run this command to see instructions for your shell. 13 | 14 | **Usage** 15 | 16 | ```bash 17 | netlify completion 18 | ``` 19 | 20 | **Flags** 21 | 22 | - `debug` (*boolean*) - Print debugging information 23 | - `auth` (*string*) - Netlify auth token - can be used to run this command without logging in 24 | 25 | | Subcommand | description | 26 | |:--------------------------- |:-----| 27 | | [`completion:install`](/commands/completion#completioninstall) | Generates completion script for your preferred shell | 28 | 29 | 30 | **Examples** 31 | 32 | ```bash 33 | netlify completion:install 34 | ``` 35 | 36 | --- 37 | ## `completion:install` 38 | 39 | Generates completion script for your preferred shell 40 | 41 | **Usage** 42 | 43 | ```bash 44 | netlify completion:install 45 | ``` 46 | 47 | **Flags** 48 | 49 | - `filter` (*string*) - For monorepos, specify the name of the application to run the command in 50 | - `debug` (*boolean*) - Print debugging information 51 | - `auth` (*string*) - Netlify auth token - can be used to run this command without logging in 52 | 53 | --- 54 | 55 | 56 | -------------------------------------------------------------------------------- /docs/commands/init.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Netlify CLI init command 3 | sidebar: 4 | label: init 5 | description: Initialize a new project locally 6 | --- 7 | 8 | # `init` 9 | 10 | 11 | Configure continuous deployment for a new or existing project. To create a new project without continuous deployment, use `netlify sites:create` 12 | 13 | **Usage** 14 | 15 | ```bash 16 | netlify init 17 | ``` 18 | 19 | **Flags** 20 | 21 | - `filter` (*string*) - For monorepos, specify the name of the application to run the command in 22 | - `force` (*boolean*) - Reinitialize CI hooks if the linked project is already configured to use CI 23 | - `git-remote-name` (*string*) - Name of Git remote to use. e.g. "origin" 24 | - `manual` (*boolean*) - Manually configure a git remote for CI 25 | - `debug` (*boolean*) - Print debugging information 26 | - `auth` (*string*) - Netlify auth token - can be used to run this command without logging in 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /docs/commands/link.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Netlify CLI link command 3 | sidebar: 4 | label: link 5 | description: Link an existing project to a local project directory 6 | --- 7 | 8 | # `link` 9 | 10 | 11 | Link a local repo or project folder to an existing project on Netlify 12 | 13 | **Usage** 14 | 15 | ```bash 16 | netlify link 17 | ``` 18 | 19 | **Flags** 20 | 21 | - `filter` (*string*) - For monorepos, specify the name of the application to run the command in 22 | - `git-remote-name` (*string*) - Name of Git remote to use. e.g. "origin" 23 | - `git-remote-url` (*string*) - URL of the repository (or Github `owner/repo`) to link to 24 | - `id` (*string*) - ID of project to link to 25 | - `name` (*string*) - Name of project to link to 26 | - `debug` (*boolean*) - Print debugging information 27 | - `auth` (*string*) - Netlify auth token - can be used to run this command without logging in 28 | 29 | **Examples** 30 | 31 | ```bash 32 | netlify link 33 | netlify link --id 123-123-123-123 34 | netlify link --name my-project-name 35 | netlify link --git-remote-url https://github.com/vibecoder/my-unicorn.git 36 | ``` 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /docs/commands/login.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Netlify CLI login command 3 | sidebar: 4 | label: login 5 | description: Login to your Netlify account 6 | --- 7 | 8 | # `login` 9 | 10 | 11 | Login to your Netlify account 12 | Opens a web browser to acquire an OAuth token. 13 | 14 | **Usage** 15 | 16 | ```bash 17 | netlify login 18 | ``` 19 | 20 | **Flags** 21 | 22 | - `new` (*boolean*) - Login to new Netlify account 23 | - `debug` (*boolean*) - Print debugging information 24 | - `auth` (*string*) - Netlify auth token - can be used to run this command without logging in 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /docs/commands/logout.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Netlify CLI logout command 3 | sidebar: 4 | label: logout 5 | description: Login to your Netlify account 6 | hidden: true 7 | --- 8 | 9 | # `logout` 10 | 11 | 12 | Logout of your Netlify account 13 | 14 | **Usage** 15 | 16 | ```bash 17 | netlify logout 18 | ``` 19 | 20 | -------------------------------------------------------------------------------- /docs/commands/status.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Netlify CLI status command 3 | sidebar: 4 | label: status 5 | description: Get the current context of the netlify CLI 6 | --- 7 | 8 | # `status` 9 | 10 | 11 | Print status information 12 | 13 | **Usage** 14 | 15 | ```bash 16 | netlify status 17 | ``` 18 | 19 | **Flags** 20 | 21 | - `json` (*boolean*) - Output status information as JSON 22 | - `verbose` (*boolean*) - Output system info 23 | - `debug` (*boolean*) - Print debugging information 24 | - `auth` (*string*) - Netlify auth token - can be used to run this command without logging in 25 | 26 | | Subcommand | description | 27 | |:--------------------------- |:-----| 28 | | [`status:hooks`](/commands/status#statushooks) | Print hook information of the linked project | 29 | 30 | 31 | --- 32 | ## `status:hooks` 33 | 34 | Print hook information of the linked project 35 | 36 | **Usage** 37 | 38 | ```bash 39 | netlify status:hooks 40 | ``` 41 | 42 | **Flags** 43 | 44 | - `filter` (*string*) - For monorepos, specify the name of the application to run the command in 45 | - `debug` (*boolean*) - Print debugging information 46 | - `auth` (*string*) - Netlify auth token - can be used to run this command without logging in 47 | 48 | --- 49 | 50 | 51 | -------------------------------------------------------------------------------- /docs/commands/switch.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Netlify CLI switch command 3 | sidebar: 4 | label: switch 5 | description: Switch your active Netlify account 6 | --- 7 | 8 | # `switch` 9 | 10 | 11 | Switch your active Netlify account 12 | 13 | **Usage** 14 | 15 | ```bash 16 | netlify switch 17 | ``` 18 | 19 | **Flags** 20 | 21 | - `debug` (*boolean*) - Print debugging information 22 | - `auth` (*string*) - Netlify auth token - can be used to run this command without logging in 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /docs/commands/unlink.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Netlify CLI unlink command 3 | sidebar: 4 | label: unlink 5 | description: Unlink a local project directory from an existing project 6 | --- 7 | 8 | # `unlink` 9 | 10 | 11 | Unlink a local folder from a Netlify project 12 | 13 | **Usage** 14 | 15 | ```bash 16 | netlify unlink 17 | ``` 18 | 19 | **Flags** 20 | 21 | - `filter` (*string*) - For monorepos, specify the name of the application to run the command in 22 | - `debug` (*boolean*) - Print debugging information 23 | - `auth` (*string*) - Netlify auth token - can be used to run this command without logging in 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /docs/commands/watch.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Netlify CLI watch command 3 | sidebar: 4 | label: watch 5 | description: Watch for project deploy to finish 6 | --- 7 | 8 | # `watch` 9 | 10 | 11 | Watch for project deploy to finish 12 | 13 | **Usage** 14 | 15 | ```bash 16 | netlify watch 17 | ``` 18 | 19 | **Flags** 20 | 21 | - `filter` (*string*) - For monorepos, specify the name of the application to run the command in 22 | - `debug` (*boolean*) - Print debugging information 23 | - `auth` (*string*) - Netlify auth token - can be used to run this command without logging in 24 | 25 | **Examples** 26 | 27 | ```bash 28 | netlify watch 29 | git push && netlify watch 30 | ``` 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /e2e/utils.js: -------------------------------------------------------------------------------- 1 | import { execSync } from 'child_process' 2 | 3 | /** 4 | * Checks if a package manager exists 5 | * @param {string} packageManager 6 | * @returns {boolean} 7 | */ 8 | export const packageManagerExists = (packageManager) => { 9 | try { 10 | execSync(`${packageManager} --version`) 11 | return true 12 | } catch { 13 | return false 14 | } 15 | } 16 | 17 | export const packageManagerConfig = { 18 | npm: { 19 | install: ['npm', ['install', 'netlify-cli@testing']], 20 | lockFile: 'package-lock.json', 21 | }, 22 | pnpm: { 23 | install: ['pnpm', ['add', 'netlify-cli@testing']], 24 | lockFile: 'pnpm-lock.yaml', 25 | }, 26 | yarn: { 27 | install: ['yarn', ['add', 'netlify-cli@testing']], 28 | lockFile: 'yarn.lock', 29 | }, 30 | } 31 | -------------------------------------------------------------------------------- /functions-templates/go/hello-world/.netlify-function-template.mjs: -------------------------------------------------------------------------------- 1 | export default { 2 | name: 'hello-world', 3 | priority: 1, 4 | description: 'Basic function that shows how to create a handler and return a response', 5 | functionType: 'serverless', 6 | } 7 | -------------------------------------------------------------------------------- /functions-templates/go/hello-world/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/someone/{{name}} 2 | 3 | go 1.19 4 | 5 | require github.com/aws/aws-lambda-go v1.47.0 6 | -------------------------------------------------------------------------------- /functions-templates/go/hello-world/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/aws/aws-lambda-go/events" 8 | "github.com/aws/aws-lambda-go/lambda" 9 | ) 10 | 11 | func handler(ctx context.Context, request events.APIGatewayProxyRequest) (*events.APIGatewayProxyResponse, error) { 12 | fmt.Println("This message will show up in the CLI console.") 13 | 14 | return &events.APIGatewayProxyResponse{ 15 | StatusCode: 200, 16 | Headers: map[string]string{"Content-Type": "text/plain"}, 17 | Body: "Hello, world!", 18 | IsBase64Encoded: false, 19 | }, nil 20 | } 21 | 22 | func main() { 23 | lambda.Start(handler) 24 | } 25 | -------------------------------------------------------------------------------- /functions-templates/javascript/hello-world/.netlify-function-template.mjs: -------------------------------------------------------------------------------- 1 | export default { 2 | name: 'hello-world', 3 | priority: 1, 4 | description: 'Basic function that shows async/await usage, and response formatting', 5 | functionType: 'serverless', 6 | } 7 | -------------------------------------------------------------------------------- /functions-templates/javascript/hello-world/{{name}}.mjs: -------------------------------------------------------------------------------- 1 | // Docs on request and context https://docs.netlify.com/functions/build/#code-your-function-2 2 | export default (request, context) => { 3 | try { 4 | const url = new URL(request.url) 5 | const subject = url.searchParams.get('name') || 'World' 6 | 7 | return new Response(`Hello ${subject}`) 8 | } catch (error) { 9 | return new Response(error.toString(), { 10 | status: 500, 11 | }) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /functions-templates/javascript/hello/.netlify-function-template.mjs: -------------------------------------------------------------------------------- 1 | export default { 2 | name: 'hello', 3 | description: 'Basic function that shows async/await usage, and response formatting', 4 | functionType: 'edge', 5 | } 6 | -------------------------------------------------------------------------------- /functions-templates/javascript/hello/{{name}}.js: -------------------------------------------------------------------------------- 1 | export default async (request) => 2 | new Response('Hello, World!', { 3 | headers: { 'content-type': 'text/html' }, 4 | }) 5 | -------------------------------------------------------------------------------- /functions-templates/javascript/image-external/.netlify-function-template.mjs: -------------------------------------------------------------------------------- 1 | export default { 2 | name: 'image-external', 3 | description: 'Fetches and serves an image from an external site', 4 | functionType: 'edge', 5 | } 6 | -------------------------------------------------------------------------------- /functions-templates/javascript/image-external/{{name}}.js: -------------------------------------------------------------------------------- 1 | export default async (request, context) => { 2 | // Return an internal image using context.rewrite() 3 | // This image is stored in the /public directory of this project 4 | 5 | // return context.rewrite("/apple-touch-icon.png"); 6 | 7 | // OR 8 | 9 | // Use fetch() and return the image response 10 | const kitten = await fetch('https://placekitten.com/g/300/300') 11 | return kitten 12 | } 13 | -------------------------------------------------------------------------------- /functions-templates/javascript/localized-content/.netlify-function-template.mjs: -------------------------------------------------------------------------------- 1 | export default { 2 | name: 'localized-content', 3 | description: 'Uses geolocation data to serve localized countent according to country code', 4 | functionType: 'edge', 5 | } 6 | -------------------------------------------------------------------------------- /functions-templates/javascript/localized-content/{{name}}.js: -------------------------------------------------------------------------------- 1 | export default async (request, context) => { 2 | const translations = { 3 | UNKNOWN: 'Hello!', 4 | US: "Howdy y'all!", 5 | GB: 'How do you do?', 6 | AU: "G'day, mate!", 7 | } 8 | 9 | const countryCode = context.geo?.country?.code || 'UNKNOWN' 10 | const countryName = context.geo?.country?.name || 'somewhere in the world' 11 | 12 | return new Response(`Your personalized greeting for ${countryName} is: ${translations[countryCode]}`, { 13 | headers: { 'content-type': 'text/html' }, 14 | }) 15 | } 16 | -------------------------------------------------------------------------------- /functions-templates/javascript/scheduled-function/.netlify-function-template.mjs: -------------------------------------------------------------------------------- 1 | export default { 2 | name: 'scheduled-function', 3 | priority: 1, 4 | description: 'Basic implementation of a scheduled function in JavaScript.', 5 | functionType: 'serverless', 6 | } 7 | -------------------------------------------------------------------------------- /functions-templates/javascript/scheduled-function/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "{{name}}", 3 | "version": "1.0.0", 4 | "description": "netlify functions:create - scheduled function in JavaScript", 5 | "main": "{{name}}.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [ 10 | "netlify", 11 | "serverless", 12 | "javascript", 13 | "schedule" 14 | ], 15 | "author": "Netlify", 16 | "license": "MIT", 17 | "dependencies": { 18 | "@netlify/functions": "^3.0.4" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /functions-templates/javascript/scheduled-function/{{name}}.mjs: -------------------------------------------------------------------------------- 1 | // To learn about scheduled functions and supported cron extensions, 2 | // see: https://ntl.fyi/sched-func 3 | export default async (req) => { 4 | const { next_run } = await req.json() 5 | 6 | console.log('Received event! Next invocation at:', next_run) 7 | } 8 | 9 | export const config = { 10 | schedule: '@hourly', 11 | } 12 | -------------------------------------------------------------------------------- /functions-templates/javascript/set-cookies/.netlify-function-template.mjs: -------------------------------------------------------------------------------- 1 | export default { 2 | name: 'set-cookies', 3 | description: 'Create and manage HTTP cookies', 4 | functionType: 'edge', 5 | } 6 | -------------------------------------------------------------------------------- /functions-templates/javascript/set-cookies/{{name}}.js: -------------------------------------------------------------------------------- 1 | export default async (request, context) => { 2 | const url = new URL(request.url) 3 | 4 | switch (url.searchParams.get('action')) { 5 | case 'set': 6 | context.cookies.set({ 7 | name: 'action', 8 | value: 'hello', 9 | }) 10 | 11 | return new Response('Cookie value has been set. Reload this page without the "action" parameter to see it.') 12 | 13 | case 'clear': 14 | context.cookies.delete('action') 15 | 16 | return new Response( 17 | 'Cookie value has been cleared. Reload this page without the "action" parameter to see the new state.', 18 | ) 19 | default: 20 | } 21 | 22 | const value = context.cookies.get('action') 23 | const message = value 24 | ? `Cookie value is "${value}". You can clear it by using "?action=clear".` 25 | : 'Cookie has not been set. You can do so by adding "?action=set" to the URL.' 26 | 27 | return new Response(message) 28 | } 29 | -------------------------------------------------------------------------------- /functions-templates/javascript/set-req-header/.netlify-function-template.mjs: -------------------------------------------------------------------------------- 1 | export default { 2 | name: 'set-req-header', 3 | description: 'Adds a custom HTTP header to HTTP request.', 4 | functionType: 'edge', 5 | } 6 | -------------------------------------------------------------------------------- /functions-templates/javascript/set-req-header/{{name}}.js: -------------------------------------------------------------------------------- 1 | export default async (request, context) => { 2 | request.headers.set('X-Your-Custom-Header', 'Your custom header value') 3 | } 4 | -------------------------------------------------------------------------------- /functions-templates/javascript/set-res-header/.netlify-function-template.mjs: -------------------------------------------------------------------------------- 1 | export default { 2 | name: 'set-res-header', 3 | description: 'Adds a custom HTTP header to HTTP response.', 4 | functionType: 'edge', 5 | } 6 | -------------------------------------------------------------------------------- /functions-templates/javascript/set-res-header/{{name}}.js: -------------------------------------------------------------------------------- 1 | export default async (request, context) => { 2 | const response = await context.next() 3 | response.headers.set('X-Your-Custom-Header', 'A custom value') 4 | return response 5 | } 6 | -------------------------------------------------------------------------------- /functions-templates/javascript/transform-response/.netlify-function-template.mjs: -------------------------------------------------------------------------------- 1 | export default { 2 | name: 'transform-response', 3 | description: 'Transform the content of an HTTP response', 4 | functionType: 'edge', 5 | } 6 | -------------------------------------------------------------------------------- /functions-templates/javascript/transform-response/{{name}}.js: -------------------------------------------------------------------------------- 1 | export default async (request, context) => { 2 | const url = new URL(request.url) 3 | 4 | // Look for the query parameter, and return if we don't find it 5 | if (url.searchParams.get('method') !== 'transform') { 6 | return 7 | } 8 | 9 | const response = await context.next() 10 | const text = await response.text() 11 | return new Response(text.toUpperCase(), response) 12 | } 13 | -------------------------------------------------------------------------------- /functions-templates/rust/hello-world/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /functions-templates/rust/hello-world/.netlify-function-template.mjs: -------------------------------------------------------------------------------- 1 | export default { 2 | name: 'hello-world', 3 | priority: 1, 4 | description: 'Basic function that shows how to create a handler and return a response', 5 | functionType: 'serverless', 6 | } 7 | -------------------------------------------------------------------------------- /functions-templates/rust/hello-world/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | edition = "2018" 3 | name = "{{name}}" 4 | version = "0.1.0" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | aws_lambda_events = "0.15.0" 10 | http = "0.2.9" 11 | lambda_runtime = "0.13.0" 12 | log = "0.4.17" 13 | simple_logger = "1.16.0" 14 | tokio = "1.38.2" 15 | -------------------------------------------------------------------------------- /functions-templates/rust/hello-world/src/main.rs: -------------------------------------------------------------------------------- 1 | use aws_lambda_events::event::apigw::{ApiGatewayProxyRequest, ApiGatewayProxyResponse}; 2 | use aws_lambda_events::encodings::Body; 3 | use http::header::HeaderMap; 4 | use lambda_runtime::{service_fn, Error, LambdaEvent}; 5 | use log::LevelFilter; 6 | use simple_logger::SimpleLogger; 7 | 8 | #[tokio::main] 9 | async fn main() -> Result<(), Error> { 10 | SimpleLogger::new().with_utc_timestamps().with_level(LevelFilter::Info).init().unwrap(); 11 | 12 | let func = service_fn(my_handler); 13 | lambda_runtime::run(func).await?; 14 | Ok(()) 15 | } 16 | 17 | pub(crate) async fn my_handler(event: LambdaEvent) -> Result { 18 | let path = event.payload.path.unwrap(); 19 | 20 | let resp = ApiGatewayProxyResponse { 21 | status_code: 200, 22 | headers: HeaderMap::new(), 23 | multi_value_headers: HeaderMap::new(), 24 | body: Some(Body::Text(format!("Hello from '{}'", path))), 25 | is_base64_encoded: Some(false), 26 | }; 27 | 28 | Ok(resp) 29 | } 30 | -------------------------------------------------------------------------------- /functions-templates/typescript/abtest/.netlify-function-template.mjs: -------------------------------------------------------------------------------- 1 | export default { 2 | name: 'abtest', 3 | description: "Function that randomly assigns users to group 'a' or 'b' and sets this value as a cookie.", 4 | functionType: 'edge', 5 | } 6 | -------------------------------------------------------------------------------- /functions-templates/typescript/abtest/{{name}}.ts: -------------------------------------------------------------------------------- 1 | import type { Context } from 'https://edge.netlify.com' 2 | 3 | export default async (request: Request, context: Context) => { 4 | // look for existing "test_bucket" cookie 5 | const bucketName = 'test_bucket' 6 | const bucket = context.cookies.get(bucketName) 7 | 8 | // return here if we find a cookie 9 | if (bucket) { 10 | return new Response(`Welcome back! You were assigned ${bucketName} **${bucket}** when you last visited the site!`) 11 | } 12 | 13 | // if no "test_bucket" cookie is found, assign the user to a bucket 14 | // in this example we're using two buckets (a, b) with an equal weighting of 50/50 15 | const weighting = 0.5 16 | 17 | // get a random number between (0-1) 18 | // this is a basic example and you may want to experiment 19 | const random = Math.random() 20 | const newBucketValue = random <= weighting ? 'a' : 'b' 21 | 22 | // set the new "test_bucket" cookie 23 | context.cookies.set({ 24 | name: bucketName, 25 | value: newBucketValue, 26 | }) 27 | 28 | return new Response( 29 | `Congratulations! You have been assigned ${bucketName} **${newBucketValue}**. View your browser cookies to check it out!`, 30 | ) 31 | } 32 | -------------------------------------------------------------------------------- /functions-templates/typescript/geolocation/.netlify-function-template.mjs: -------------------------------------------------------------------------------- 1 | export default { 2 | name: 'geolocation', 3 | description: "Returns info about user's geolocation, which can be used to serve location-specific content.", 4 | functionType: 'edge', 5 | } 6 | -------------------------------------------------------------------------------- /functions-templates/typescript/geolocation/{{name}}.ts: -------------------------------------------------------------------------------- 1 | import { Context } from 'https://edge.netlify.com' 2 | 3 | export default async (request: Request, context: Context) => 4 | // Here's what's available on context.geo 5 | 6 | // context: { 7 | // geo: { 8 | // city?: string; 9 | // country?: { 10 | // code?: string; 11 | // name?: string; 12 | // }, 13 | // subdivision?: { 14 | // code?: string; 15 | // name?: string; 16 | // }, 17 | // } 18 | // } 19 | 20 | Response.json({ 21 | geo: context.geo, 22 | header: request.headers.get('x-nf-geo'), 23 | }) 24 | -------------------------------------------------------------------------------- /functions-templates/typescript/hello-world/.netlify-function-template.mjs: -------------------------------------------------------------------------------- 1 | export default { 2 | name: 'hello-world', 3 | priority: 1, 4 | description: 'Basic function that shows async/await usage, and response formatting', 5 | functionType: 'serverless', 6 | } 7 | -------------------------------------------------------------------------------- /functions-templates/typescript/hello-world/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hello-world", 3 | "version": "1.0.0", 4 | "description": "netlify functions:create - hello world in typescript", 5 | "main": "hello-world.ts", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [ 10 | "netlify", 11 | "serverless", 12 | "typescript" 13 | ], 14 | "author": "Netlify", 15 | "license": "MIT", 16 | "dependencies": { 17 | "@netlify/functions": "^3.0.4", 18 | "@types/node": "^22.0.0", 19 | "typescript": "^4.0.0" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /functions-templates/typescript/hello-world/{{name}}.mts: -------------------------------------------------------------------------------- 1 | import { Context } from '@netlify/functions' 2 | 3 | export default (request: Request, context: Context) => { 4 | try { 5 | const url = new URL(request.url) 6 | const subject = url.searchParams.get('name') || 'World' 7 | 8 | return new Response(`Hello ${subject}`) 9 | } catch (error) { 10 | return new Response(error.toString(), { 11 | status: 500, 12 | }) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /functions-templates/typescript/json/.netlify-function-template.mjs: -------------------------------------------------------------------------------- 1 | export default { 2 | name: 'json', 3 | description: 'Function that returns a simple json response', 4 | functionType: 'edge', 5 | } 6 | -------------------------------------------------------------------------------- /functions-templates/typescript/json/{{name}}.ts: -------------------------------------------------------------------------------- 1 | import type { Context } from 'https://edge.netlify.com' 2 | 3 | export default async (request: Request, context: Context) => 4 | Response.json({ hello: 'world', location: context.geo.city }) 5 | -------------------------------------------------------------------------------- /functions-templates/typescript/log/.netlify-function-template.mjs: -------------------------------------------------------------------------------- 1 | export default { 2 | name: 'log', 3 | description: 'Basic function logging context of request', 4 | functionType: 'edge', 5 | } 6 | -------------------------------------------------------------------------------- /functions-templates/typescript/log/{{name}}.ts: -------------------------------------------------------------------------------- 1 | import type { Context } from 'https://edge.netlify.com' 2 | 3 | export default async (request: Request, context: Context) => { 4 | console.log(`There was a request from ${context.geo.city} to ${request.url}`) 5 | 6 | return new Response('The request to this URL was logged', { 7 | headers: { 'content-type': 'text/html' }, 8 | }) 9 | } 10 | -------------------------------------------------------------------------------- /functions-templates/typescript/scheduled-function/.netlify-function-template.mjs: -------------------------------------------------------------------------------- 1 | export default { 2 | name: 'scheduled-function', 3 | priority: 1, 4 | description: 'Basic implementation of a scheduled function in TypeScript.', 5 | functionType: 'serverless', 6 | } 7 | -------------------------------------------------------------------------------- /functions-templates/typescript/scheduled-function/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "{{name}}", 3 | "version": "1.0.0", 4 | "description": "netlify functions:create - scheduled function in TypeScript", 5 | "main": "{{name}}.ts", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [ 10 | "netlify", 11 | "serverless", 12 | "typescript", 13 | "schedule" 14 | ], 15 | "author": "Netlify", 16 | "license": "MIT", 17 | "dependencies": { 18 | "@netlify/functions": "^3.0.4", 19 | "@types/node": "^22.0.0", 20 | "typescript": "^4.5.5" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /functions-templates/typescript/scheduled-function/{{name}}.mts: -------------------------------------------------------------------------------- 1 | import type { Config } from "@netlify/functions" 2 | 3 | export default async (req: Request) => { 4 | const { next_run } = await req.json() 5 | 6 | console.log("Received event! Next invocation at:", next_run) 7 | } 8 | 9 | export const config: Config = { 10 | schedule: "@hourly" 11 | } 12 | -------------------------------------------------------------------------------- /functions-templates/typescript/set-cookies/.netlify-function-template.mjs: -------------------------------------------------------------------------------- 1 | export default { 2 | name: 'set-cookies', 3 | description: 'Create and manage HTTP cookies', 4 | functionType: 'edge', 5 | } 6 | -------------------------------------------------------------------------------- /functions-templates/typescript/set-cookies/{{name}}.ts: -------------------------------------------------------------------------------- 1 | import type { Context } from 'https://edge.netlify.com' 2 | 3 | export default async (request: Request, context: Context) => { 4 | const url = new URL(request.url) 5 | 6 | switch (url.searchParams.get('action')) { 7 | case 'set': 8 | context.cookies.set({ 9 | name: 'action', 10 | value: 'hello', 11 | }) 12 | 13 | return new Response('Cookie value has been set. Reload this page without the "action" parameter to see it.') 14 | 15 | case 'clear': 16 | context.cookies.delete('action') 17 | 18 | return new Response( 19 | 'Cookie value has been cleared. Reload this page without the "action" parameter to see the new state.', 20 | ) 21 | } 22 | 23 | const value = context.cookies.get('action') 24 | const message = value 25 | ? `Cookie value is "${value}". You can clear it by using "?action=clear".` 26 | : 'Cookie has not been set. You can do so by adding "?action=set" to the URL.' 27 | 28 | return new Response(message) 29 | } 30 | -------------------------------------------------------------------------------- /functions-templates/typescript/set-req-header/.netlify-function-template.mjs: -------------------------------------------------------------------------------- 1 | export default { 2 | name: 'set-req-header', 3 | description: 'Adds a custom HTTP header to HTTP request.', 4 | functionType: 'edge', 5 | } 6 | -------------------------------------------------------------------------------- /functions-templates/typescript/set-req-header/{{name}}.ts: -------------------------------------------------------------------------------- 1 | import type { Context } from 'https://edge.netlify.com' 2 | 3 | export default async (request: Request, context: Context) => { 4 | request.headers.set('X-Your-Custom-Header', 'Your custom header value') 5 | } 6 | -------------------------------------------------------------------------------- /functions-templates/typescript/set-res-header/.netlify-function-template.mjs: -------------------------------------------------------------------------------- 1 | export default { 2 | name: 'set-res-header', 3 | description: 'Adds a custom HTTP header to HTTP response.', 4 | functionType: 'edge', 5 | } 6 | -------------------------------------------------------------------------------- /functions-templates/typescript/set-res-header/{{name}}.ts: -------------------------------------------------------------------------------- 1 | import type { Context } from 'https://edge.netlify.com' 2 | 3 | export default async (request: Request, context: Context) => { 4 | const response = await context.next() 5 | response.headers.set('X-Your-Custom-Header', 'A custom value') 6 | return response 7 | } 8 | -------------------------------------------------------------------------------- /functions-templates/typescript/transform-response/.netlify-function-template.mjs: -------------------------------------------------------------------------------- 1 | export default { 2 | name: 'transform-response', 3 | description: 'Transform the content of an HTTP response', 4 | functionType: 'edge', 5 | } 6 | -------------------------------------------------------------------------------- /functions-templates/typescript/transform-response/{{name}}.ts: -------------------------------------------------------------------------------- 1 | import { Context } from 'https://edge.netlify.com' 2 | 3 | export default async (request: Request, context: Context) => { 4 | const url = new URL(request.url) 5 | 6 | // Look for the query parameter, and return if we don't find it 7 | if (url.searchParams.get('method') !== 'transform') { 8 | return 9 | } 10 | const response = await context.next() 11 | const text = await response.text() 12 | return new Response(text.toUpperCase(), response) 13 | } 14 | -------------------------------------------------------------------------------- /layout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | netlify-cli 7 | 8 | 9 | 10 | 19 | 20 | 21 | 22 | 25 |
26 | 27 | 28 | -------------------------------------------------------------------------------- /scripts/bash.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | script_link="$(command readlink "$BASH_SOURCE")" || script_link="$BASH_SOURCE" 4 | apparent_sdk_dir="${script_link%/*}" 5 | 6 | if [ "$apparent_sdk_dir" == "$script_link" ]; then 7 | apparent_sdk_dir=. 8 | fi 9 | 10 | sdk_dir="$(command cd -P "$apparent_sdk_dir" >/dev/null && command pwd -P)" 11 | bin_path="$sdk_dir/bin" 12 | 13 | if [[ ":${PATH}:" != *":${bin_path}:"* ]]; then 14 | export PATH=$bin_path:$PATH 15 | fi 16 | -------------------------------------------------------------------------------- /scripts/fish.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env fish 2 | 3 | # Only append to PATH if it isn't already part of the list 4 | # `fish_add_path` - https://fishshell.com/docs/current/cmds/fish_add_path.html?highlight=fish_add_path would be a more 5 | # suited alternative but it's only supported in fish 3.3.x 6 | if not contains (dirname (status --current-filename))/bin $fish_user_paths 7 | set -U fish_user_paths (dirname (status --current-filename))/bin $fish_user_paths 8 | end 9 | -------------------------------------------------------------------------------- /scripts/path.ps1: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env pwsh 2 | 3 | $ErrorActionPreference = "Stop" 4 | 5 | # Helper functions for pretty terminal output. 6 | function Write-Part ([string] $Text) { 7 | Write-Host $Text -NoNewline 8 | } 9 | function Write-Emphasized ([string] $Text) { 10 | Write-Host $Text -NoNewLine -ForegroundColor "Yellow" 11 | } 12 | function Write-Done { 13 | Write-Host " done" -NoNewline -ForegroundColor "Green"; 14 | Write-Host "." 15 | } 16 | 17 | # Get Path environment variable for the current user. 18 | $user = [EnvironmentVariableTarget]::User 19 | $path = [Environment]::GetEnvironmentVariable("PATH", $user) 20 | 21 | $install_dir = $args[0] 22 | 23 | # Add Helper to PATH 24 | Write-Part "Adding "; Write-Emphasized $install_dir; Write-Part " to the " 25 | Write-Emphasized "PATH"; Write-Part " environment variable..." 26 | [Environment]::SetEnvironmentVariable("PATH", "${path};${install_dir}", $user) 27 | # Add Helper to the PATH variable of the current terminal session 28 | # so `git-credential-netlify` can be used immediately without restarting the 29 | # terminal. 30 | $env:PATH += ";${install_dir}" 31 | Write-Done 32 | 33 | Write-Host "" 34 | Write-Host "Netlify Credential Helper for Git was installed successfully." -ForegroundColor "Green" 35 | Write-Host "" 36 | -------------------------------------------------------------------------------- /scripts/prepublishOnly.js: -------------------------------------------------------------------------------- 1 | import * as cp from 'node:child_process' 2 | import * as fs from 'node:fs/promises' 3 | import * as path from 'node:path' 4 | 5 | const main = async () => { 6 | // It's best practice to include a shrinkwrap when shipping a CLI. npm has a bug that makes it 7 | // not ignore development dependencies in an installed package's shrinkwrap, though: 8 | // 9 | // https://github.com/npm/cli/issues/4323 10 | // 11 | // Leaving development dependencies makes the CLI installation significantly larger and increases 12 | // the risk of platform-specific dependency installation issues. 13 | // eslint-disable-next-line no-restricted-properties 14 | const packageJSONPath = path.join(process.cwd(), 'package.json') 15 | const rawPackageJSON = await fs.readFile(packageJSONPath, 'utf8') 16 | 17 | // Remove dev dependencies from the package.json... 18 | const packageJSON = JSON.parse(rawPackageJSON) 19 | Reflect.deleteProperty(packageJSON, 'devDependencies') 20 | await fs.writeFile(packageJSONPath, JSON.stringify(packageJSON, null, 2)) 21 | 22 | // Prune out dev dependencies (this updates the `package-lock.json` lockfile) 23 | cp.spawnSync('npm', ['prune'], { stdio: 'inherit' }) 24 | 25 | // Convert `package-lock.json` lockfile to `npm-shrinkwrap.json` 26 | cp.spawnSync('npm', ['shrinkwrap'], { stdio: 'inherit' }) 27 | } 28 | 29 | await main() 30 | -------------------------------------------------------------------------------- /scripts/zsh.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env zsh 2 | 3 | export PATH=${0:A:h}/bin:$PATH 4 | -------------------------------------------------------------------------------- /site/config.js: -------------------------------------------------------------------------------- 1 | import { join } from 'node:path' 2 | import { fileURLToPath } from 'node:url' 3 | 4 | export const rootDir = fileURLToPath(new URL('..', import.meta.url)) 5 | 6 | export const docs = { 7 | srcPath: join(rootDir, 'docs'), 8 | outputPath: join(rootDir, 'site/src/content/docs'), 9 | } 10 | -------------------------------------------------------------------------------- /site/netlify/functions/error-reporting.ts: -------------------------------------------------------------------------------- 1 | import process from 'process' 2 | 3 | import Bugsnag from '@bugsnag/js' 4 | import type { Handler } from '@netlify/functions' 5 | 6 | Bugsnag.start({ 7 | apiKey: `${process.env.NETLIFY_BUGSNAG_API_KEY}`, 8 | }) 9 | 10 | export const handler: Handler = async ({ body }) => { 11 | try { 12 | if (typeof body !== 'string') { 13 | return { statusCode: 200 } 14 | } 15 | const { 16 | cause, 17 | cliVersion, 18 | message, 19 | metadata = {}, 20 | name, 21 | nodejsVersion, 22 | osName, 23 | severity = 'error', 24 | stack, 25 | user, 26 | } = JSON.parse(body) 27 | Bugsnag.notify({ name, message, stack, cause }, (event) => { 28 | event.app = { 29 | releaseStage: 'production', 30 | version: cliVersion, 31 | type: 'netlify-cli', 32 | } 33 | 34 | for (const [section, values] of Object.entries(metadata)) { 35 | event.addMetadata(section, values as Record) 36 | } 37 | event.setUser(user.id, user.email, user.name) 38 | event.severity = severity 39 | event.device = { 40 | osName, 41 | runtimeVersions: { 42 | node: nodejsVersion, 43 | }, 44 | } 45 | }) 46 | } catch (error) { 47 | Bugsnag.notify(error) 48 | } 49 | return { 50 | statusCode: 200, 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /site/netlify/functions/telemetry.js: -------------------------------------------------------------------------------- 1 | const TELEMETRY_SERVICE_URL = 'https://cli-telemetry.netlify.engineering' 2 | 3 | // This function is a workaround for our inability to redirect traffic to a ntl function in another site 4 | // using redirects (see https://github.com/netlify/cli-telemetry-service/issues/14) 5 | export const handler = async function ({ path, httpMethod, headers, body }) { 6 | const upstreamPath = path.replace(/^\/telemetry\//, '/') 7 | 8 | // Filter out some headers that shouldn't be fwded 9 | const headersToFilter = ['host'] 10 | const upstreamHeaders = Object.entries(headers) 11 | .filter(([headerName]) => headersToFilter.find((hToFilter) => hToFilter !== headerName)) 12 | .reduce((resultingHeaders, [headerName, value]) => { 13 | resultingHeaders[headerName] = value 14 | return resultingHeaders 15 | }, {}) 16 | 17 | try { 18 | const response = await fetch(`${TELEMETRY_SERVICE_URL}${upstreamPath}`, { 19 | method: httpMethod, 20 | headers: upstreamHeaders, 21 | body, 22 | }) 23 | console.log(`Telemetry service responded with ${response.status}`) 24 | } catch (error) { 25 | console.error('Telemetry service call failed', error) 26 | } 27 | 28 | return { 29 | statusCode: 200, 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /site/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cli-docs-site", 3 | "version": "1.0.0", 4 | "description": "Docs site for Netlify CLI", 5 | "type": "module", 6 | "author": "Netlify Inc.", 7 | "scripts": { 8 | "build": "npm run build:docs-md && npm run build:site", 9 | "build:docs-md": "npm run build:docs-md:gen && npm run build:docs-md:copy", 10 | "build:docs-md:copy": "./scripts/sync.js", 11 | "build:docs-md:gen": "./scripts/docs.js", 12 | "build:site": "astro build", 13 | "dev": "npm run build:docs-md:copy && astro dev", 14 | "preview": "npm run build:docs-md:copy && astro preview", 15 | "start": "npm run dev" 16 | }, 17 | "license": "MIT", 18 | "dependencies": { 19 | "@astrojs/starlight": "0.32.4", 20 | "@bugsnag/js": "8.2.0", 21 | "astro": "5.7.4", 22 | "markdown-magic": "2.6.1", 23 | "sharp": "0.32.6", 24 | "strip-ansi": "7.1.0" 25 | }, 26 | "devDependencies": { 27 | "@netlify/functions": "3.1.8", 28 | "tsx": "4.19.3" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /site/public/og.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netlify/cli/18ba162a78ab98eed3f93dde0a537485bcccb450/site/public/og.png -------------------------------------------------------------------------------- /site/scripts/util/fs.js: -------------------------------------------------------------------------------- 1 | import { mkdir, readdir, copyFile, lstat } from 'node:fs/promises' 2 | import { join, dirname } from 'node:path' 3 | 4 | export const copyDirRecursiveAsync = async (src, dest) => { 5 | try { 6 | await mkdir(dest, { recursive: true }) 7 | } catch { 8 | // ignore errors for mkdir 9 | } 10 | 11 | const childrenItems = await readdir(src) 12 | 13 | await Promise.all( 14 | childrenItems.map(async (item) => { 15 | const srcPath = join(src, item) 16 | const destPath = join(dest, item) 17 | 18 | const itemStat = await lstat(srcPath) 19 | 20 | if (itemStat.isFile()) { 21 | await copyFile(srcPath, destPath) 22 | } else { 23 | await copyDirRecursiveAsync(srcPath, destPath) 24 | } 25 | }), 26 | ) 27 | } 28 | 29 | export const ensureFilePathAsync = async (filePath) => { 30 | try { 31 | await mkdir(dirname(filePath), { recursive: true }) 32 | } catch { 33 | // ignore any errors with mkdir - it will throw if the path already exists. 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /site/src/content.config.ts: -------------------------------------------------------------------------------- 1 | import { defineCollection } from 'astro:content'; 2 | import { docsLoader } from '@astrojs/starlight/loaders'; 3 | import { docsSchema } from '@astrojs/starlight/schema'; 4 | 5 | export const collections = { 6 | docs: defineCollection({ loader: docsLoader(), schema: docsSchema() }), 7 | }; 8 | -------------------------------------------------------------------------------- /site/src/fonts/font-face.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: Pacaembu; 3 | src: url('./pacaembuvar-latin-nohint.woff2') format('woff2'); 4 | font-weight: 100 1000; 5 | font-display: swap; 6 | unicode-range: U+0-FF, U+131, U+152, U+153, U+2BB, U+2BC, U+2C6, U+2DA, U+2DC, U+2000-206F, U+2074, U+20AC, U+2122, 7 | U+2191, U+2193, U+2212, U+2215; 8 | } 9 | 10 | @font-face { 11 | font-family: Mulishvar; 12 | src: url('./mulishvar-latin-nohint.woff2') format('woff2'); 13 | font-weight: 100 1000; 14 | font-display: swap; 15 | unicode-range: U+0-FF, U+131, U+152, U+153, U+2BB, U+2BC, U+2C6, U+2DA, U+2DC, U+2000-206F, U+2074, U+20AC, U+2122, 16 | U+2191, U+2193, U+2212, U+2215; 17 | } 18 | 19 | @font-face { 20 | font-family: Mulishvar; 21 | src: url('./mulishvar-italic-latin-nohint.woff2') format('woff2'); 22 | font-weight: 200 900; 23 | font-style: italic; 24 | font-display: swap; 25 | unicode-range: U+0-FF, U+131, U+152, U+153, U+2BB, U+2BC, U+2C6, U+2DA, U+2DC, U+2000-206F, U+2074, U+20AC, U+2122, 26 | U+2191, U+2193, U+2212, U+2215; 27 | } 28 | -------------------------------------------------------------------------------- /site/src/fonts/mulishvar-italic-latin-nohint.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netlify/cli/18ba162a78ab98eed3f93dde0a537485bcccb450/site/src/fonts/mulishvar-italic-latin-nohint.woff2 -------------------------------------------------------------------------------- /site/src/fonts/mulishvar-latin-nohint.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netlify/cli/18ba162a78ab98eed3f93dde0a537485bcccb450/site/src/fonts/mulishvar-latin-nohint.woff2 -------------------------------------------------------------------------------- /site/src/fonts/pacaembuvar-latin-nohint.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netlify/cli/18ba162a78ab98eed3f93dde0a537485bcccb450/site/src/fonts/pacaembuvar-latin-nohint.woff2 -------------------------------------------------------------------------------- /site/src/styles/sizes.css: -------------------------------------------------------------------------------- 1 | :root { 2 | /* padding */ 3 | --nano: 0.15rem; 4 | --micro: 0.65rem; 5 | --tiny: 1rem; 6 | --small: 1.5rem; 7 | --medium: 2.5rem; 8 | --large: 4rem; 9 | --xl: 6.5rem; 10 | --xxl: 10.5rem; 11 | 12 | /* typography sizes */ 13 | --xxlText: 2.25rem; 14 | --xlText: 1.375rem; 15 | --largeText: 1.125rem; 16 | --mediumText: 1.25rem; 17 | /* 20px */ 18 | --defaultText: 1.09375rem; 19 | /* 17.5px */ 20 | --smallText: 0.984375rem; 21 | /* 15.75px */ 22 | --tinyText: 0.75rem; 23 | --letterSpacing: 0.02rem; 24 | /* codeText is in ems so text inside other elements scales according to closest parent text https://github.com/netlify/docs/issues/248#issue-498536370 */ 25 | --codeText: 0.886em; 26 | 27 | /* typography weights */ 28 | --regular: 400; 29 | --semibold: 600; 30 | --bold: 700; 31 | 32 | /* border radius */ 33 | --smallRadius: 0.25rem; 34 | --bigRadius: 0.5rem; 35 | } 36 | -------------------------------------------------------------------------------- /site/src/styles/transitions.css: -------------------------------------------------------------------------------- 1 | :root, 2 | ::backdrop { 3 | /* animation */ 4 | --transitionDurationLong: 0.4s; 5 | --transitionDuration: 0.2s; 6 | --transitionDurationShort: 0.1s; 7 | --transitionEasing: cubic-bezier(1, 0.5, 0.8, 1); 8 | } 9 | -------------------------------------------------------------------------------- /site/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "astro/tsconfigs/strict", 3 | "include": [".astro/types.d.ts", "**/*"], 4 | "exclude": ["dist"] 5 | } 6 | -------------------------------------------------------------------------------- /src/commands/api/index.ts: -------------------------------------------------------------------------------- 1 | import { chalk } from '../../utils/command-helpers.js' 2 | import BaseCommand from '../base-command.js' 3 | 4 | export const createApiCommand = (program: BaseCommand) => 5 | program 6 | .command('api') 7 | .argument('[apiMethod]', 'Open API method to run') 8 | .description( 9 | `Run any Netlify API method 10 | For more information on available methods check out https://open-api.netlify.com/ or run '${chalk.grey( 11 | 'netlify api --list', 12 | )}'`, 13 | ) 14 | .option('-d, --data ', 'Data to use') 15 | .option('--list', 'List out available API methods', false) 16 | .addExamples(['netlify api --list', `netlify api getSite --data '{ "site_id": "123456" }'`]) 17 | .action(async (apiMethod, options, command) => { 18 | const { apiCommand } = await import('./api.js') 19 | await apiCommand(apiMethod, options, command) 20 | }) 21 | -------------------------------------------------------------------------------- /src/commands/blobs/blobs-delete.ts: -------------------------------------------------------------------------------- 1 | import { getStore } from '@netlify/blobs' 2 | 3 | import { chalk, logAndThrowError, log } from '../../utils/command-helpers.js' 4 | import { promptBlobDelete } from '../../utils/prompts/blob-delete-prompts.js' 5 | 6 | /** 7 | * The blobs:delete command 8 | */ 9 | export const blobsDelete = async (storeName: string, key: string, _options: Record, command: any) => { 10 | const { api, siteInfo } = command.netlify 11 | const { force } = _options 12 | 13 | const store = getStore({ 14 | apiURL: `${api.scheme}://${api.host}`, 15 | name: storeName, 16 | siteID: siteInfo.id ?? '', 17 | token: api.accessToken ?? '', 18 | }) 19 | 20 | if (force === undefined) { 21 | await promptBlobDelete(key, storeName) 22 | } 23 | 24 | try { 25 | await store.delete(key) 26 | 27 | log(`${chalk.greenBright('Success')}: Blob ${chalk.yellow(key)} deleted from store ${chalk.yellow(storeName)}`) 28 | } catch { 29 | return logAndThrowError(`Could not delete blob ${chalk.yellow(key)} from store ${chalk.yellow(storeName)}`) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/commands/blobs/blobs-get.ts: -------------------------------------------------------------------------------- 1 | import { promises as fs } from 'fs' 2 | import { resolve } from 'path' 3 | 4 | import { getStore } from '@netlify/blobs' 5 | import { OptionValues } from 'commander' 6 | 7 | import { chalk, logAndThrowError } from '../../utils/command-helpers.js' 8 | import BaseCommand from '../base-command.js' 9 | 10 | interface Options extends OptionValues { 11 | output?: string 12 | } 13 | 14 | export const blobsGet = async (storeName: string, key: string, options: Options, command: BaseCommand) => { 15 | const { api, siteInfo } = command.netlify 16 | const { output } = options 17 | const store = getStore({ 18 | apiURL: `${api.scheme}://${api.host}`, 19 | name: storeName, 20 | siteID: siteInfo?.id ?? '', 21 | token: api.accessToken ?? '', 22 | }) 23 | 24 | let blob 25 | 26 | try { 27 | blob = await store.get(key) 28 | } catch { 29 | return logAndThrowError(`Could not retrieve blob ${chalk.yellow(key)} from store ${chalk.yellow(storeName)}`) 30 | } 31 | 32 | if (blob === null) { 33 | return logAndThrowError(`Blob ${chalk.yellow(key)} does not exist in store ${chalk.yellow(storeName)}`) 34 | } 35 | 36 | if (output) { 37 | const path = resolve(output) 38 | 39 | await fs.writeFile(path, blob) 40 | } else { 41 | console.log(blob) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/commands/clone/option_values.ts: -------------------------------------------------------------------------------- 1 | import type { BaseOptionValues } from '../base-command.js' 2 | 3 | export type CloneOptionValues = BaseOptionValues & { 4 | // NOTE(serhalp): Think this would be better off as `siteId`? Beware, here be dragons. 5 | // There's some magical global state mutation dance going on somewhere when you call 6 | // an option `--site-id`. Good luck, friend. 7 | id?: string | undefined 8 | name?: string | undefined 9 | } 10 | -------------------------------------------------------------------------------- /src/commands/completion/index.ts: -------------------------------------------------------------------------------- 1 | import { OptionValues } from 'commander' 2 | 3 | import BaseCommand from '../base-command.js' 4 | 5 | export const createCompletionCommand = (program: BaseCommand) => { 6 | program 7 | .command('completion:install') 8 | .alias('completion:generate') 9 | .description('Generates completion script for your preferred shell') 10 | .action(async (options: OptionValues, command: BaseCommand) => { 11 | const { completionGenerate } = await import('./completion.js') 12 | await completionGenerate(options, command) 13 | }) 14 | 15 | program 16 | .command('completion:uninstall', { hidden: true }) 17 | .alias('completion:remove') 18 | .description('Uninstalls the installed completions') 19 | .addExamples(['netlify completion:uninstall']) 20 | .action(async (options: OptionValues, command: BaseCommand) => { 21 | const { completionUninstall } = await import('./completion.js') 22 | await completionUninstall(options, command) 23 | }) 24 | 25 | return program 26 | .command('completion') 27 | .description('Generate shell completion script\nRun this command to see instructions for your shell.') 28 | .addExamples(['netlify completion:install']) 29 | .action((_options: OptionValues, command: BaseCommand) => { 30 | command.help() 31 | }) 32 | } 33 | -------------------------------------------------------------------------------- /src/commands/database/constants.ts: -------------------------------------------------------------------------------- 1 | export const NEON_DATABASE_EXTENSION_SLUG = process.env.NEON_DATABASE_EXTENSION_SLUG ?? 'neon' 2 | export const JIGSAW_URL = process.env.JIGSAW_URL ?? 'https://jigsaw.services-prod.nsvcs.net' 3 | export const NETLIFY_NEON_PACKAGE_NAME = '@netlify/neon' 4 | -------------------------------------------------------------------------------- /src/commands/database/index.ts: -------------------------------------------------------------------------------- 1 | export { createDatabaseCommand } from './database.js' 2 | -------------------------------------------------------------------------------- /src/commands/deploy/option_values.ts: -------------------------------------------------------------------------------- 1 | // This type lives in a separate file to prevent import cycles. 2 | 3 | import type { BaseOptionValues } from '../base-command.js' 4 | 5 | export type DeployOptionValues = BaseOptionValues & { 6 | alias?: string 7 | build: boolean 8 | branch?: string 9 | context?: string 10 | dir?: string 11 | functions?: string 12 | json: boolean 13 | message?: string 14 | open: boolean 15 | prod: boolean 16 | prodIfUnlocked: boolean 17 | site?: string 18 | skipFunctionsCache: boolean 19 | timeout?: number 20 | trigger?: boolean 21 | } 22 | -------------------------------------------------------------------------------- /src/commands/dev-exec/dev-exec.ts: -------------------------------------------------------------------------------- 1 | import { OptionValues } from 'commander' 2 | import execa from 'execa' 3 | 4 | import { getDotEnvVariables, injectEnvVariables } from '../../utils/dev.js' 5 | import { getEnvelopeEnv } from '../../utils/env/index.js' 6 | import BaseCommand from '../base-command.js' 7 | 8 | export const devExec = async (cmd: string, options: OptionValues, command: BaseCommand) => { 9 | const { api, cachedConfig, config, site, siteInfo } = command.netlify 10 | 11 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment 12 | const withEnvelopeEnvVars = await getEnvelopeEnv({ api, context: options.context, env: cachedConfig.env, siteInfo }) 13 | const withDotEnvVars = await getDotEnvVariables({ devConfig: { ...config.dev }, env: withEnvelopeEnvVars, site }) 14 | 15 | injectEnvVariables(withDotEnvVars) 16 | 17 | await execa(cmd, command.args.slice(1), { 18 | stdio: 'inherit', 19 | }) 20 | } 21 | -------------------------------------------------------------------------------- /src/commands/dev-exec/index.ts: -------------------------------------------------------------------------------- 1 | import type { OptionValues } from 'commander' 2 | 3 | import type BaseCommand from '../base-command.js' 4 | import { normalizeContext } from '../../utils/env/index.js' 5 | 6 | export const createDevExecCommand = (program: BaseCommand) => 7 | program 8 | .command('dev:exec') 9 | .argument('<...cmd>', `the command that should be executed`) 10 | .option( 11 | '--context ', 12 | 'Specify a deploy context for environment variables (”production”, ”deploy-preview”, ”branch-deploy”, ”dev”) or `branch:your-branch` where `your-branch` is the name of a branch (default: dev)', 13 | normalizeContext, 14 | 'dev', 15 | ) 16 | .description( 17 | 'Runs a command within the netlify dev environment. For example, with environment variables from any installed add-ons', 18 | ) 19 | .allowExcessArguments(true) 20 | .addExamples([ 21 | 'netlify dev:exec npm run bootstrap', 22 | 'netlify dev:exec --context deploy-preview npm run bootstrap # Run with env var values from deploy-preview context', 23 | 'netlify dev:exec --context branch:feat/make-it-pop npm run bootstrap # Run with env var values from the feat/make-it-pop branch context or branch-deploy context', 24 | ]) 25 | .action(async (cmd: string, options: OptionValues, command: BaseCommand) => { 26 | const { devExec } = await import('./dev-exec.js') 27 | await devExec(cmd, options, command) 28 | }) 29 | -------------------------------------------------------------------------------- /src/commands/dev/types.d.ts: -------------------------------------------------------------------------------- 1 | import type { NetlifyTOML } from '@netlify/build-info' 2 | 3 | import type { FrameworkNames } from '../../utils/types' 4 | 5 | /** The configuration specified in the netlify.toml under [build] */ 6 | export type BuildConfig = NonNullable 7 | 8 | export type DevConfig = NonNullable & { 9 | framework: FrameworkNames 10 | /** Directory of the functions */ 11 | functions?: string | undefined 12 | live?: boolean | undefined 13 | /** The base directory from the [build] section of the configuration file */ 14 | base?: string | undefined 15 | staticServerPort?: number | undefined 16 | envFiles?: string[] | undefined 17 | 18 | jwtSecret?: string | undefined 19 | jwtRolePath?: string | undefined 20 | pollingStrategies?: string[] | undefined 21 | } 22 | -------------------------------------------------------------------------------- /src/commands/env/index.ts: -------------------------------------------------------------------------------- 1 | export { createEnvCommand } from './env.js' 2 | -------------------------------------------------------------------------------- /src/commands/functions/index.ts: -------------------------------------------------------------------------------- 1 | export { createFunctionsCommand } from './functions.js' 2 | -------------------------------------------------------------------------------- /src/commands/index.ts: -------------------------------------------------------------------------------- 1 | export { default as BaseCommand } from './base-command.js' 2 | export { createMainCommand } from './main.js' 3 | -------------------------------------------------------------------------------- /src/commands/init/constants.ts: -------------------------------------------------------------------------------- 1 | export const LINKED_NEW_SITE_EXIT_CODE = 'LINKED_NEW_SITE' 2 | 3 | export const LINKED_EXISTING_SITE_EXIT_CODE = 'LINKED_EXISTING_SITE' 4 | 5 | export type InitExitCode = typeof LINKED_NEW_SITE_EXIT_CODE | typeof LINKED_EXISTING_SITE_EXIT_CODE 6 | -------------------------------------------------------------------------------- /src/commands/init/index.ts: -------------------------------------------------------------------------------- 1 | import { OptionValues } from 'commander' 2 | import terminalLink from 'terminal-link' 3 | 4 | import BaseCommand from '../base-command.js' 5 | 6 | export const createInitCommand = (program: BaseCommand) => 7 | program 8 | .command('init') 9 | .description( 10 | 'Configure continuous deployment for a new or existing project. To create a new project without continuous deployment, use `netlify sites:create`', 11 | ) 12 | .option('-m, --manual', 'Manually configure a git remote for CI') 13 | .option('--git-remote-name ', 'Name of Git remote to use. e.g. "origin"') 14 | .addHelpText('after', () => { 15 | const docsUrl = 'https://docs.netlify.com/cli/get-started/' 16 | return ` 17 | For more information about getting started with Netlify CLI, see ${terminalLink(docsUrl, docsUrl, { fallback: false })} 18 | ` 19 | }) 20 | .action(async (options: OptionValues, command: BaseCommand) => { 21 | const { init } = await import('./init.js') 22 | await init(options, command) 23 | }) 24 | -------------------------------------------------------------------------------- /src/commands/link/index.ts: -------------------------------------------------------------------------------- 1 | import terminalLink from 'terminal-link' 2 | 3 | import BaseCommand from '../base-command.js' 4 | import type { LinkOptionValues } from './option_values.js' 5 | 6 | export const createLinkCommand = (program: BaseCommand) => 7 | program 8 | .command('link') 9 | .description('Link a local repo or project folder to an existing project on Netlify') 10 | .option('--id ', 'ID of project to link to') 11 | .option('--name ', 'Name of project to link to') 12 | .option('--git-remote-name ', 'Name of Git remote to use. e.g. "origin"') 13 | .option('--git-remote-url ', 'URL of the repository (or Github `owner/repo`) to link to') 14 | .addExamples([ 15 | 'netlify link', 16 | 'netlify link --id 123-123-123-123', 17 | 'netlify link --name my-project-name', 18 | 'netlify link --git-remote-url https://github.com/vibecoder/my-unicorn.git', 19 | ]) 20 | .addHelpText('after', () => { 21 | const docsUrl = 'https://docs.netlify.com/cli/get-started/#link-and-unlink-sites' 22 | return ` 23 | For more information about linking projects, see ${terminalLink(docsUrl, docsUrl, { fallback: false })} 24 | ` 25 | }) 26 | .action(async (options: LinkOptionValues, command: BaseCommand) => { 27 | const { link } = await import('./link.js') 28 | await link(options, command) 29 | }) 30 | -------------------------------------------------------------------------------- /src/commands/link/option_values.ts: -------------------------------------------------------------------------------- 1 | // This type lives in a separate file to prevent import cycles. 2 | 3 | import type { BaseOptionValues } from '../base-command.js' 4 | 5 | export type LinkOptionValues = BaseOptionValues & { 6 | id?: string | undefined 7 | name?: string | undefined 8 | gitRemoteUrl?: string | undefined 9 | gitRemoteName?: string | undefined 10 | } 11 | -------------------------------------------------------------------------------- /src/commands/login/index.ts: -------------------------------------------------------------------------------- 1 | import { OptionValues } from 'commander' 2 | import terminalLink from 'terminal-link' 3 | 4 | import BaseCommand from '../base-command.js' 5 | 6 | export const createLoginCommand = (program: BaseCommand) => 7 | program 8 | .command('login') 9 | .description( 10 | `Login to your Netlify account 11 | Opens a web browser to acquire an OAuth token.`, 12 | ) 13 | .option('--new', 'Login to new Netlify account') 14 | .addHelpText('after', () => { 15 | const docsUrl = 'https://docs.netlify.com/cli/get-started/#authentication' 16 | return ` 17 | For more information about Netlify authentication, see ${terminalLink(docsUrl, docsUrl, { fallback: false })} 18 | ` 19 | }) 20 | .action(async (options: OptionValues, command: BaseCommand) => { 21 | const { login } = await import('./login.js') 22 | await login(options, command) 23 | }) 24 | -------------------------------------------------------------------------------- /src/commands/login/login.ts: -------------------------------------------------------------------------------- 1 | import { OptionValues } from 'commander' 2 | 3 | import { chalk, exit, getToken, log } from '../../utils/command-helpers.js' 4 | import { TokenLocation } from '../../utils/types.js' 5 | import BaseCommand from '../base-command.js' 6 | 7 | const msg = function (location: TokenLocation) { 8 | switch (location) { 9 | case 'env': 10 | return 'via process.env.NETLIFY_AUTH_TOKEN set in your terminal session' 11 | case 'flag': 12 | return 'via CLI --auth flag' 13 | case 'config': 14 | return 'via netlify config on your machine' 15 | default: 16 | return '' 17 | } 18 | } 19 | 20 | export const login = async (options: OptionValues, command: BaseCommand) => { 21 | const [accessToken, location] = await getToken() 22 | 23 | command.setAnalyticsPayload({ new: options.new }) 24 | 25 | if (accessToken && !options.new) { 26 | log(`Already logged in ${msg(location)}`) 27 | log() 28 | log(`Run ${chalk.cyanBright('netlify status')} for account details`) 29 | log() 30 | log(`or run ${chalk.cyanBright('netlify switch')} to switch accounts`) 31 | log() 32 | log(`To see all available commands run: ${chalk.cyanBright('netlify help')}`) 33 | log() 34 | return exit() 35 | } 36 | 37 | await command.expensivelyAuthenticate() 38 | } 39 | -------------------------------------------------------------------------------- /src/commands/logout/index.ts: -------------------------------------------------------------------------------- 1 | import { OptionValues } from 'commander' 2 | 3 | import BaseCommand from '../base-command.js' 4 | 5 | export const createLogoutCommand = (program: BaseCommand) => 6 | program 7 | .command('logout', { hidden: true }) 8 | .description('Logout of your Netlify account') 9 | .action(async (options: OptionValues, command: BaseCommand) => { 10 | const { logout } = await import('./logout.js') 11 | await logout(options, command) 12 | }) 13 | -------------------------------------------------------------------------------- /src/commands/logout/logout.ts: -------------------------------------------------------------------------------- 1 | import { OptionValues } from 'commander' 2 | 3 | import { exit, getToken, log } from '../../utils/command-helpers.js' 4 | import { track } from '../../utils/telemetry/index.js' 5 | import BaseCommand from '../base-command.js' 6 | 7 | export const logout = async (_options: OptionValues, command: BaseCommand) => { 8 | const [accessToken, location] = await getToken() 9 | 10 | if (!accessToken) { 11 | log(`Already logged out`) 12 | log() 13 | log('To login run "netlify login"') 14 | exit() 15 | } 16 | 17 | await track('user_logout') 18 | 19 | // unset userID without deleting key 20 | command.netlify.globalConfig.set('userId', null) 21 | 22 | if (location === 'env') { 23 | log('The "process.env.NETLIFY_AUTH_TOKEN" is still set in your terminal session') 24 | log() 25 | log('To logout completely, unset the environment variable') 26 | log() 27 | exit() 28 | } 29 | 30 | log(`Logging you out of Netlify. Come back soon!`) 31 | } 32 | -------------------------------------------------------------------------------- /src/commands/logs/log-levels.ts: -------------------------------------------------------------------------------- 1 | // Source: Source: https://docs.aws.amazon.com/lambda/latest/dg/monitoring-cloudwatchlogs.html#monitoring-cloudwatchlogs-advanced 2 | export const LOG_LEVELS = { 3 | TRACE: 'TRACE', 4 | DEBUG: 'DEBUG', 5 | INFO: 'INFO', 6 | WARN: 'WARN', 7 | ERROR: 'ERROR', 8 | FATAL: 'FATAL', 9 | } 10 | export const LOG_LEVELS_LIST = Object.values(LOG_LEVELS).map((level) => level.toLowerCase()) 11 | export const CLI_LOG_LEVEL_CHOICES_STRING = LOG_LEVELS_LIST.map((level) => ` ${level}`) 12 | -------------------------------------------------------------------------------- /src/commands/open/open-admin.ts: -------------------------------------------------------------------------------- 1 | import { exit, log } from '../../utils/command-helpers.js' 2 | import openBrowser from '../../utils/open-browser.js' 3 | import type BaseCommand from '../base-command.js' 4 | 5 | export const openAdmin = async (_options: unknown, command: BaseCommand) => { 6 | const { siteInfo } = command.netlify 7 | 8 | await command.authenticate() 9 | 10 | log(`Opening "${siteInfo.name}" project admin UI:`) 11 | log(`> ${siteInfo.admin_url}`) 12 | 13 | await openBrowser({ url: siteInfo.admin_url }) 14 | exit() 15 | } 16 | -------------------------------------------------------------------------------- /src/commands/open/open-site.ts: -------------------------------------------------------------------------------- 1 | import { OptionValues } from 'commander' 2 | 3 | import { exit, log } from '../../utils/command-helpers.js' 4 | import openBrowser from '../../utils/open-browser.js' 5 | import BaseCommand from '../base-command.js' 6 | 7 | export const openSite = async (_options: OptionValues, command: BaseCommand) => { 8 | const { siteInfo } = command.netlify 9 | 10 | await command.authenticate() 11 | 12 | const url = siteInfo.ssl_url || siteInfo.url 13 | log(`Opening "${siteInfo.name}" project url:`) 14 | log(`> ${url}`) 15 | 16 | await openBrowser({ url }) 17 | exit() 18 | } 19 | -------------------------------------------------------------------------------- /src/commands/open/open.ts: -------------------------------------------------------------------------------- 1 | import { OptionValues } from 'commander' 2 | 3 | import { log } from '../../utils/command-helpers.js' 4 | import BaseCommand from '../base-command.js' 5 | 6 | import { openAdmin } from './open-admin.js' 7 | import { openSite } from './open-site.js' 8 | 9 | export const open = async (options: OptionValues, command: BaseCommand) => { 10 | if (!options.site || !options.admin) { 11 | log(command.helpInformation()) 12 | } 13 | 14 | if (options.site) { 15 | await openSite(options, command) 16 | } 17 | // Default open netlify admin 18 | await openAdmin(options, command) 19 | } 20 | -------------------------------------------------------------------------------- /src/commands/recipes/common.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs/promises' 2 | import { dirname, join, resolve } from 'path' 3 | import { fileURLToPath, pathToFileURL } from 'url' 4 | 5 | const directoryPath = dirname(fileURLToPath(import.meta.url)) 6 | 7 | // @ts-expect-error TS(7006) FIXME: Parameter 'name' implicitly has an 'any' type. 8 | export const getRecipe = async (name) => { 9 | const recipePath = resolve(directoryPath, '../../recipes', name, 'index.js') 10 | 11 | // windows needs a URL for absolute paths 12 | 13 | const recipe = await import(pathToFileURL(recipePath).href) 14 | 15 | return recipe 16 | } 17 | 18 | export const listRecipes = async () => { 19 | const recipesPath = resolve(directoryPath, '../../recipes') 20 | const recipeNames = await fs.readdir(recipesPath) 21 | const recipes = await Promise.all( 22 | recipeNames.map(async (name) => { 23 | const recipePath = join(recipesPath, name, 'index.js') 24 | 25 | // windows needs a URL for absolute paths 26 | 27 | const recipe = await import(pathToFileURL(recipePath).href) 28 | 29 | return { 30 | ...recipe, 31 | name, 32 | } 33 | }), 34 | ) 35 | 36 | return recipes 37 | } 38 | -------------------------------------------------------------------------------- /src/commands/recipes/index.ts: -------------------------------------------------------------------------------- 1 | import { OptionValues } from 'commander' 2 | 3 | import BaseCommand from '../base-command.js' 4 | 5 | export const createRecipesCommand = (program: BaseCommand) => { 6 | program 7 | .command('recipes:list') 8 | .description(`List the recipes available to create and modify files in a project`) 9 | .addExamples(['netlify recipes:list']) 10 | .action(async () => { 11 | const { recipesListCommand } = await import('./recipes-list.js') 12 | await recipesListCommand() 13 | }) 14 | 15 | return program 16 | .command('recipes') 17 | .argument('[name]', 'name of the recipe') 18 | .description(`Create and modify files in a project using pre-defined recipes`) 19 | .option('-n, --name ', 'recipe name to use') 20 | .addExamples(['netlify recipes my-recipe', 'netlify recipes --name my-recipe']) 21 | .action(async (recipeName: string, options: OptionValues, command: BaseCommand) => { 22 | const { recipesCommand } = await import('./recipes.js') 23 | await recipesCommand(recipeName, options, command) 24 | }) 25 | } 26 | -------------------------------------------------------------------------------- /src/commands/recipes/recipes-list.ts: -------------------------------------------------------------------------------- 1 | import AsciiTable from 'ascii-table' 2 | 3 | import { listRecipes } from './common.js' 4 | 5 | /** 6 | * The recipes:list command 7 | */ 8 | export const recipesListCommand = async () => { 9 | const recipes = await listRecipes() 10 | const table = new AsciiTable(`Usage: netlify recipes `) 11 | 12 | table.setHeading('Name', 'Description') 13 | 14 | recipes.forEach(({ description, name }) => { 15 | table.addRow(name, description) 16 | }) 17 | 18 | console.log(table.toString()) 19 | } 20 | -------------------------------------------------------------------------------- /src/commands/sites/index.ts: -------------------------------------------------------------------------------- 1 | export { createSitesCommand } from './sites.js' 2 | -------------------------------------------------------------------------------- /src/commands/status/index.ts: -------------------------------------------------------------------------------- 1 | import { OptionValues } from 'commander' 2 | 3 | import requiresSiteInfo from '../../utils/hooks/requires-site-info.js' 4 | import BaseCommand from '../base-command.js' 5 | 6 | export const createStatusCommand = (program: BaseCommand) => { 7 | program 8 | .command('status:hooks') 9 | .description('Print hook information of the linked project') 10 | .hook('preAction', requiresSiteInfo) 11 | .action(async (options: OptionValues, command: BaseCommand) => { 12 | const { statusHooks } = await import('./status-hooks.js') 13 | await statusHooks(options, command) 14 | }) 15 | 16 | program 17 | .command('status') 18 | .description('Print status information') 19 | .option('--verbose', 'Output system info') 20 | .option('--json', 'Output status information as JSON') 21 | .action(async (options: OptionValues, command: BaseCommand) => { 22 | const { status } = await import('./status.js') 23 | await status(options, command) 24 | }) 25 | } 26 | -------------------------------------------------------------------------------- /src/commands/status/status-hooks.ts: -------------------------------------------------------------------------------- 1 | import type { OptionValues } from 'commander' 2 | import prettyjson from 'prettyjson' 3 | 4 | import { log } from '../../utils/command-helpers.js' 5 | import type BaseCommand from '../base-command.js' 6 | 7 | interface StatusHook { 8 | type: string | undefined 9 | event: string | undefined 10 | id: string 11 | disabled: boolean 12 | repo_url?: string 13 | } 14 | 15 | export const statusHooks = async (_options: OptionValues, command: BaseCommand): Promise => { 16 | const { api, siteInfo } = command.netlify 17 | 18 | await command.authenticate() 19 | 20 | const ntlHooks = await api.listHooksBySiteId({ siteId: siteInfo.id }) 21 | const data = { 22 | project: siteInfo.name, 23 | hooks: {} as Record, 24 | } 25 | 26 | ntlHooks.forEach((hook) => { 27 | // TODO(serhalp): Surely the `listHooksBySiteId` type is wrong about `id` being optional. Fix. 28 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 29 | const id = hook.id! 30 | data.hooks[id] = { 31 | type: hook.type, 32 | event: hook.event, 33 | id, 34 | disabled: hook.disabled ?? false, 35 | } 36 | if (siteInfo.build_settings?.repo_url) { 37 | data.hooks[id].repo_url = siteInfo.build_settings.repo_url 38 | } 39 | }) 40 | log(`─────────────────┐ 41 | Project Hook Status │ 42 | ─────────────────┘`) 43 | log(prettyjson.render(data)) 44 | } 45 | -------------------------------------------------------------------------------- /src/commands/switch/index.ts: -------------------------------------------------------------------------------- 1 | import { OptionValues } from 'commander' 2 | 3 | import BaseCommand from '../base-command.js' 4 | 5 | export const createSwitchCommand = (program: BaseCommand) => 6 | program 7 | .command('switch') 8 | .description('Switch your active Netlify account') 9 | .action(async (options: OptionValues, command: BaseCommand) => { 10 | const { switchCommand } = await import('./switch.js') 11 | await switchCommand(options, command) 12 | }) 13 | -------------------------------------------------------------------------------- /src/commands/unlink/index.ts: -------------------------------------------------------------------------------- 1 | import { OptionValues } from 'commander' 2 | import terminalLink from 'terminal-link' 3 | 4 | import BaseCommand from '../base-command.js' 5 | 6 | export const createUnlinkCommand = (program: BaseCommand) => 7 | program 8 | .command('unlink') 9 | .description('Unlink a local folder from a Netlify project') 10 | .addHelpText('after', () => { 11 | const docsUrl = 'https://docs.netlify.com/cli/get-started/#link-and-unlink-sites' 12 | return ` 13 | For more information about linking projects, see ${terminalLink(docsUrl, docsUrl, { fallback: false })} 14 | ` 15 | }) 16 | .action(async (options: OptionValues, command: BaseCommand) => { 17 | const { unlink } = await import('./unlink.js') 18 | await unlink(options, command) 19 | }) 20 | -------------------------------------------------------------------------------- /src/commands/unlink/unlink.ts: -------------------------------------------------------------------------------- 1 | import { OptionValues } from 'commander' 2 | 3 | import { exit, log } from '../../utils/command-helpers.js' 4 | import { track } from '../../utils/telemetry/index.js' 5 | import BaseCommand from '../base-command.js' 6 | import { chalk, netlifyCommand } from '../../utils/command-helpers.js' 7 | 8 | export const unlink = async (_options: OptionValues, command: BaseCommand) => { 9 | const { site, siteInfo, state } = command.netlify 10 | const siteId = site.id 11 | 12 | if (!siteId) { 13 | log(`Folder is not linked to a Netlify project. Run ${chalk.cyanBright(`${netlifyCommand()} link`)} to link it`) 14 | return exit() 15 | } 16 | 17 | const siteData = siteInfo 18 | 19 | state.delete('siteId') 20 | 21 | await track('sites_unlinked', { 22 | siteId: siteData.id || siteId, 23 | }) 24 | 25 | if (site && site.configPath) { 26 | log(`Unlinked ${site.configPath} from ${siteData ? siteData.name : siteId}`) 27 | } else { 28 | log(`Unlinked from ${siteData ? siteData.name : siteId}`) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/commands/watch/index.ts: -------------------------------------------------------------------------------- 1 | import { OptionValues } from 'commander' 2 | 3 | import BaseCommand from '../base-command.js' 4 | 5 | export const createWatchCommand = (program: BaseCommand) => 6 | program 7 | .command('watch') 8 | .description('Watch for project deploy to finish') 9 | .addExamples([`netlify watch`, `git push && netlify watch`]) 10 | .action(async (options: OptionValues, command: BaseCommand) => { 11 | const { watch } = await import('./watch.js') 12 | await watch(options, command) 13 | }) 14 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | // This is an entrypoint that mirrors the interface that the `netlify` package 2 | // used to have, before it was renamed to `@netlify/api`. We keep it for 3 | // backwards-compatibility. 4 | export { NetlifyAPI, methods } from '@netlify/api' 5 | -------------------------------------------------------------------------------- /src/lib/account.ts: -------------------------------------------------------------------------------- 1 | import type { Account, Capability } from '../utils/dev.js' 2 | 3 | const supportsBooleanCapability = (account: Account | undefined, capability: Capability) => 4 | Boolean(account?.capabilities?.[capability]?.included) 5 | 6 | export const supportsBackgroundFunctions = (account?: Account): boolean => 7 | supportsBooleanCapability(account, 'background_functions') 8 | -------------------------------------------------------------------------------- /src/lib/completion/constants.ts: -------------------------------------------------------------------------------- 1 | import { getPathInHome } from '../settings.js' 2 | 3 | export const AUTOCOMPLETION_FILE = getPathInHome(['autocompletion.json']) 4 | -------------------------------------------------------------------------------- /src/lib/completion/index.ts: -------------------------------------------------------------------------------- 1 | export { default as generateAutocompletion } from './generate-autocompletion.js' 2 | -------------------------------------------------------------------------------- /src/lib/completion/script.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | // This script is run by the completion (every log output will be displayed on tab) 4 | // src/commands/completion/completion.js -> dynamically references this file 5 | // if this file is renamed or moved then it needs to be adapted there 6 | import { existsSync, readFileSync } from 'fs' 7 | import process from 'process' 8 | 9 | import { getShellFromEnv, log, parseEnv } from '@pnpm/tabtab' 10 | 11 | import { AUTOCOMPLETION_FILE } from './constants.js' 12 | import getAutocompletion from './get-autocompletion.js' 13 | 14 | const env = parseEnv(process.env) 15 | const shell = getShellFromEnv(process.env) 16 | 17 | if (existsSync(AUTOCOMPLETION_FILE)) { 18 | const program = JSON.parse(readFileSync(AUTOCOMPLETION_FILE, 'utf-8')) 19 | const autocomplete = getAutocompletion(env, program) 20 | 21 | if (autocomplete && autocomplete.length !== 0) { 22 | log(autocomplete, shell) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/lib/edge-functions/bootstrap.ts: -------------------------------------------------------------------------------- 1 | import { env } from 'process' 2 | 3 | import { getURL } from '@netlify/edge-functions/version' 4 | 5 | import { warn } from '../../utils/command-helpers.js' 6 | 7 | export const FALLBACK_BOOTSTRAP_URL = 'https://edge.netlify.com/bootstrap/index-combined.ts' 8 | 9 | export const getBootstrapURL = async () => { 10 | if (env.NETLIFY_EDGE_BOOTSTRAP) { 11 | return env.NETLIFY_EDGE_BOOTSTRAP 12 | } 13 | 14 | try { 15 | return await getURL() 16 | } catch (error) { 17 | warn( 18 | `Could not load latest version of Edge Functions environment: ${ 19 | (error as NodeJS.ErrnoException | undefined)?.message ?? '' 20 | }`, 21 | ) 22 | 23 | // If there was an error getting the bootstrap URL from the module, let's 24 | // use the latest version of the bootstrap. This is not ideal, but better 25 | // than failing to serve requests with edge functions. 26 | return FALLBACK_BOOTSTRAP_URL 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/lib/edge-functions/consts.ts: -------------------------------------------------------------------------------- 1 | export const DIST_IMPORT_MAP_PATH = 'edge-functions-import-map.json' 2 | export const INTERNAL_EDGE_FUNCTIONS_FOLDER = 'edge-functions' 3 | export const EDGE_FUNCTIONS_FOLDER = 'edge-functions-dist' 4 | export const EDGE_FUNCTIONS_SERVE_FOLDER = 'edge-functions-serve' 5 | export const PUBLIC_URL_PATH = '.netlify/internal/edge-functions' 6 | 7 | // Feature flags related to Edge Functions that should be passed along to 8 | // Netlify Build. 9 | export const featureFlags = { 10 | edge_functions_config_export: true, 11 | edge_functions_npm_modules: true, 12 | edge_functions_read_deno_config: true, 13 | } 14 | -------------------------------------------------------------------------------- /src/lib/edge-functions/deploy.ts: -------------------------------------------------------------------------------- 1 | import { stat } from 'fs/promises' 2 | import { join } from 'path' 3 | 4 | import { getPathInProject } from '../settings.js' 5 | 6 | import { EDGE_FUNCTIONS_FOLDER, PUBLIC_URL_PATH } from './consts.js' 7 | 8 | const distPath = getPathInProject([EDGE_FUNCTIONS_FOLDER]) 9 | 10 | /** 11 | * @param {string} workingDir 12 | * @param {*} file 13 | */ 14 | // @ts-expect-error TS(7006) FIXME: Parameter 'workingDir' implicitly has an 'any' typ... Remove this comment to see the full error message 15 | export const deployFileNormalizer = (workingDir: string, file) => { 16 | const absoluteDistPath = join(workingDir, distPath) 17 | const isEdgeFunction = file.root === absoluteDistPath 18 | const normalizedPath = isEdgeFunction ? `${PUBLIC_URL_PATH}/${file.normalizedPath}` : file.normalizedPath 19 | 20 | return { 21 | ...file, 22 | normalizedPath, 23 | } 24 | } 25 | 26 | export const getDistPathIfExists = async (workingDir: string) => { 27 | try { 28 | const absoluteDistPath = join(workingDir, distPath) 29 | const stats = await stat(absoluteDistPath) 30 | 31 | if (!stats.isDirectory()) { 32 | throw new Error(`Path ${absoluteDistPath} must be a directory.`) 33 | } 34 | 35 | return absoluteDistPath 36 | } catch { 37 | // no-op 38 | } 39 | } 40 | 41 | export const isEdgeFunctionFile = (filePath: string) => filePath.startsWith(`${PUBLIC_URL_PATH}/`) 42 | -------------------------------------------------------------------------------- /src/lib/extensions.ts: -------------------------------------------------------------------------------- 1 | import type { Project } from '@netlify/build-info' 2 | import isEmpty from 'lodash/isEmpty.js' 3 | import type { NetlifySite } from '../commands/types.js' 4 | import type { SiteInfo } from '../utils/types.js' 5 | 6 | export const packagesThatNeedSites = new Set(['@netlify/neon']) 7 | 8 | export type DoesProjectRequireLinkedSiteParams = { 9 | project: Project 10 | site: NetlifySite 11 | siteInfo: SiteInfo 12 | options: Record 13 | } 14 | 15 | export const doesProjectRequireLinkedSite = async ({ 16 | options, 17 | project, 18 | site, 19 | siteInfo, 20 | }: DoesProjectRequireLinkedSiteParams): Promise<[boolean, string[]]> => { 21 | // If we don't have a site, these extensions need one initialized 22 | // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing 23 | const hasSiteData = Boolean(site.id || options.site) && !isEmpty(siteInfo) 24 | if (hasSiteData) { 25 | return [false, []] 26 | } 27 | const packageJson = await project.getPackageJSON() 28 | const dependencies = packageJson.dependencies ?? {} 29 | const packageNames = Object.keys(dependencies).filter((packageName) => packagesThatNeedSites.has(packageName)) 30 | return [packageNames.length > 0, packageNames] 31 | } 32 | -------------------------------------------------------------------------------- /src/lib/fs.ts: -------------------------------------------------------------------------------- 1 | import { constants } from 'fs' 2 | import { access, stat } from 'fs/promises' 3 | 4 | const isErrnoException = (value: unknown): value is NodeJS.ErrnoException => 5 | value instanceof Error && Object.hasOwn(value, 'code') 6 | 7 | export const fileExistsAsync = async (filePath: string) => { 8 | try { 9 | await access(filePath, constants.F_OK) 10 | return true 11 | } catch { 12 | return false 13 | } 14 | } 15 | 16 | /** 17 | * calls stat async with a function and catches potential errors 18 | */ 19 | const isType = async (filePath: string, type: 'isFile' | 'isDirectory') => { 20 | try { 21 | const stats = await stat(filePath) 22 | if (type === 'isFile') return stats.isFile() 23 | return stats.isDirectory() 24 | } catch (error) { 25 | if (isErrnoException(error) && error.code === 'ENOENT') { 26 | return false 27 | } 28 | 29 | throw error 30 | } 31 | } 32 | 33 | /** 34 | * Checks if the provided filePath is a file 35 | */ 36 | export const isFileAsync = async (filePath: string) => isType(filePath, 'isFile') 37 | 38 | /** 39 | * Checks if the provided filePath is a directory 40 | */ 41 | export const isDirectoryAsync = async (filePath: string) => isType(filePath, 'isDirectory') 42 | -------------------------------------------------------------------------------- /src/lib/functions/background.ts: -------------------------------------------------------------------------------- 1 | import express from 'express' 2 | 3 | import { NETLIFYDEVERR, NETLIFYDEVLOG } from '../../utils/command-helpers.js' 4 | 5 | import { formatLambdaError, styleFunctionName } from './utils.js' 6 | import type { InvocationError } from './netlify-function.js' 7 | 8 | const BACKGROUND_FUNCTION_STATUS_CODE = 202 9 | 10 | export const handleBackgroundFunction = (functionName: string, response: express.Response): void => { 11 | console.log(`${NETLIFYDEVLOG} Queueing background function ${styleFunctionName(functionName)} for execution`) 12 | response.status(BACKGROUND_FUNCTION_STATUS_CODE) 13 | response.end() 14 | } 15 | 16 | export const handleBackgroundFunctionResult = (functionName: string, err: null | Error | InvocationError): void => { 17 | if (err) { 18 | console.log( 19 | `${NETLIFYDEVERR} Error during background function ${styleFunctionName(functionName)} execution:`, 20 | formatLambdaError(err), 21 | ) 22 | } else { 23 | console.log(`${NETLIFYDEVLOG} Done executing background function ${styleFunctionName(functionName)}`) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/lib/functions/runtimes/js/constants.ts: -------------------------------------------------------------------------------- 1 | export const SECONDS_TO_MILLISECONDS = 1000 2 | -------------------------------------------------------------------------------- /src/lib/log.ts: -------------------------------------------------------------------------------- 1 | import { chalk } from '../utils/command-helpers.js' 2 | 3 | const RED_BACKGROUND = chalk.red('-background') 4 | const [PRO, BUSINESS, ENTERPRISE] = ['Pro', 'Business', 'Enterprise'].map((plan) => chalk.magenta(plan)) 5 | export const BACKGROUND_FUNCTIONS_WARNING = `A serverless function ending in \`${RED_BACKGROUND}\` was detected. 6 | Your team’s current plan doesn’t support Background Functions, which have names ending in \`${RED_BACKGROUND}\`. 7 | To be able to deploy this function successfully either: 8 | - change the function name to remove \`${RED_BACKGROUND}\` and execute it synchronously 9 | - upgrade your team plan to a level that supports Background Functions (${PRO}, ${BUSINESS}, or ${ENTERPRISE}) 10 | ` 11 | export const MISSING_AWS_SDK_WARNING = `A function has thrown an error due to a missing dependency: ${chalk.yellow( 12 | 'aws-sdk', 13 | )}. 14 | You should add this module to the project's dependencies, using your package manager of choice: 15 | 16 | ${chalk.yellow('npm install aws-sdk --save')} or ${chalk.yellow('yarn add aws-sdk')} 17 | 18 | For more information, see https://ntl.fyi/cli-aws-sdk.` 19 | -------------------------------------------------------------------------------- /src/lib/path.ts: -------------------------------------------------------------------------------- 1 | export const normalizeBackslash = (path: string): string => path.replace(/\\/g, '/') 2 | -------------------------------------------------------------------------------- /src/lib/render-error-template.ts: -------------------------------------------------------------------------------- 1 | import { readFile } from 'fs/promises' 2 | import { dirname, join } from 'path' 3 | import { fileURLToPath } from 'url' 4 | 5 | // @ts-expect-error TS(7034) FIXME: Variable 'errorTemplateFile' implicitly has type '... Remove this comment to see the full error message 6 | let errorTemplateFile 7 | const dir = dirname(fileURLToPath(import.meta.url)) 8 | 9 | // @ts-expect-error TS(7006) FIXME: Parameter 'errString' implicitly has an 'any' type... Remove this comment to see the full error message 10 | const renderErrorTemplate = async (errString, templatePath, functionType) => { 11 | const errorDetailsRegex = //g 12 | const functionTypeRegex = //g 13 | 14 | try { 15 | // @ts-expect-error TS(7005) FIXME: Variable 'errorTemplateFile' implicitly has an 'an... Remove this comment to see the full error message 16 | errorTemplateFile = errorTemplateFile || (await readFile(join(dir, templatePath), 'utf-8')) 17 | 18 | return errorTemplateFile.replace(errorDetailsRegex, errString).replace(functionTypeRegex, functionType) 19 | } catch { 20 | return errString 21 | } 22 | } 23 | 24 | export default renderErrorTemplate 25 | -------------------------------------------------------------------------------- /src/lib/settings.ts: -------------------------------------------------------------------------------- 1 | import os from 'os' 2 | import path from 'path' 3 | 4 | import envPaths from 'env-paths' 5 | 6 | const OSBasedPaths = envPaths('netlify', { suffix: '' }) 7 | const NETLIFY_HOME = '.netlify' 8 | 9 | /** 10 | * Deprecated method to get netlify's home config - ~/.netlify/... 11 | * @deprecated 12 | */ 13 | export const getLegacyPathInHome = (paths: string[]) => path.join(os.homedir(), NETLIFY_HOME, ...paths) 14 | 15 | /** 16 | * get a global path on the os base path 17 | */ 18 | export const getPathInHome = (paths: string[]) => path.join(OSBasedPaths.config, ...paths) 19 | 20 | /** 21 | * get a path inside the project folder "NOT WORKSPACE AWARE" 22 | */ 23 | export const getPathInProject = (paths: string[]) => path.join(NETLIFY_HOME, ...paths) 24 | -------------------------------------------------------------------------------- /src/lib/spinner.ts: -------------------------------------------------------------------------------- 1 | import { createSpinner, type Spinner } from 'nanospinner' 2 | 3 | const DOTS_SPINNER = { 4 | interval: 80, 5 | frames: ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'], 6 | } 7 | 8 | /** 9 | * Creates a spinner with the following text 10 | */ 11 | export const startSpinner = ({ text }: { text: string }) => createSpinner(text, DOTS_SPINNER).start() 12 | 13 | /** 14 | * Stops the spinner with the following text 15 | */ 16 | export const stopSpinner = ({ error, spinner, text }: { error?: boolean; spinner: Spinner; text?: string }) => { 17 | if (!spinner) { 18 | return 19 | } 20 | if (error === true) { 21 | spinner.error(text) 22 | } else { 23 | spinner.stop(text) 24 | } 25 | } 26 | 27 | /** 28 | * Clears the spinner 29 | */ 30 | export const clearSpinner = ({ spinner }: { spinner: Spinner }) => { 31 | spinner.clear() 32 | } 33 | 34 | export type { Spinner } 35 | -------------------------------------------------------------------------------- /src/lib/string.ts: -------------------------------------------------------------------------------- 1 | // @ts-expect-error TS(7006) FIXME: Parameter 't' implicitly has an 'any' type. 2 | export const capitalize = function (t) { 3 | // @ts-expect-error TS(7006) FIXME: Parameter 'string' implicitly has an 'any' type. 4 | return t.replace(/(^\w|\s\w)/g, (string) => string.toUpperCase()) 5 | } 6 | -------------------------------------------------------------------------------- /src/utils/addons/prepare.ts: -------------------------------------------------------------------------------- 1 | import { APIError, logAndThrowError } from '../command-helpers.js' 2 | 3 | // @ts-expect-error TS(7031) FIXME: Binding element 'addonName' implicitly has an 'any... Remove this comment to see the full error message 4 | export const getCurrentAddon = ({ addonName, addons }) => addons.find((addon) => addon.service_slug === addonName) 5 | 6 | // @ts-expect-error TS(7031) FIXME: Binding element 'api' implicitly has an 'any' type... Remove this comment to see the full error message 7 | export const getSiteData = async ({ api, siteId }) => { 8 | let siteData 9 | try { 10 | siteData = await api.getSite({ siteId }) 11 | } catch (error_) { 12 | return logAndThrowError(`Failed getting list of project data: ${(error_ as APIError).message}`) 13 | } 14 | return siteData 15 | } 16 | 17 | // @ts-expect-error TS(7031) FIXME: Binding element 'api' implicitly has an 'any' type... Remove this comment to see the full error message 18 | export const getAddons = async ({ api, siteId }) => { 19 | let addons 20 | try { 21 | addons = await api.listServiceInstancesForSite({ siteId }) 22 | } catch (error_) { 23 | return logAndThrowError(`Failed getting list of addons: ${(error_ as APIError).message}`) 24 | } 25 | return addons 26 | } 27 | -------------------------------------------------------------------------------- /src/utils/create-deferred.ts: -------------------------------------------------------------------------------- 1 | const createDeferred = () => { 2 | let resolveDeferred!: (value: T) => void 3 | let rejectDeferred!: (reason: unknown) => void 4 | const promise = new Promise((resolve, reject) => { 5 | resolveDeferred = resolve 6 | rejectDeferred = reject 7 | }) 8 | 9 | return { promise, reject: rejectDeferred, resolve: resolveDeferred } 10 | } 11 | 12 | export default createDeferred 13 | -------------------------------------------------------------------------------- /src/utils/deploy/constants.ts: -------------------------------------------------------------------------------- 1 | // Local deploy timeout in ms: 20 mins 2 | export const DEFAULT_DEPLOY_TIMEOUT = 1_200_000 3 | // Concurrent file hash calls 4 | export const DEFAULT_CONCURRENT_HASH = 100 5 | // Number of concurrent uploads 6 | export const DEFAULT_CONCURRENT_UPLOAD = 5 7 | // Number of files 8 | export const DEFAULT_SYNC_LIMIT = 100 9 | // Number of times to retry an upload 10 | export const DEFAULT_MAX_RETRY = 5 11 | 12 | export const UPLOAD_RANDOM_FACTOR = 0.5 13 | // 5 seconds 14 | export const UPLOAD_INITIAL_DELAY = 5000 15 | // 1.5 minute (90s) 16 | export const UPLOAD_MAX_DELAY = 90_000 17 | 18 | // 1 second 19 | export const DEPLOY_POLL = 1000 20 | -------------------------------------------------------------------------------- /src/utils/deploy/hash-config.ts: -------------------------------------------------------------------------------- 1 | import { createHash } from 'node:crypto' 2 | 3 | import tomlify from 'tomlify-j0.4' 4 | 5 | // @ts-expect-error TS(7031) FIXME: Binding element 'config' implicitly has an 'any' t... Remove this comment to see the full error message 6 | export const hashConfig = ({ config }) => { 7 | if (!config) throw new Error('Missing config option') 8 | const configString = serializeToml(config) 9 | 10 | const hash = createHash('sha1').update(configString).digest('hex') 11 | 12 | return { 13 | assetType: 'file', 14 | body: configString, 15 | hash, 16 | normalizedPath: 'netlify.toml', 17 | } 18 | } 19 | 20 | // @ts-expect-error TS(7006) FIXME: Parameter 'object' implicitly has an 'any' type. 21 | export const serializeToml = function (object) { 22 | return tomlify.toToml(object, { space: 2, replace: replaceTomlValue }) 23 | } 24 | 25 | // `tomlify-j0.4` serializes integers as floats, e.g. `200.0`. 26 | // This is a problem with `redirects[*].status`. 27 | // @ts-expect-error TS(7006) FIXME: Parameter 'key' implicitly has an 'any' type. 28 | const replaceTomlValue = function (key, value) { 29 | return Number.isInteger(value) ? String(value) : false 30 | } 31 | -------------------------------------------------------------------------------- /src/utils/deploy/status-cb.ts: -------------------------------------------------------------------------------- 1 | // TODO(serhalp): This is alternatingly called "event", "status", and "progress". Standardize. 2 | export interface DeployEvent { 3 | type: string 4 | msg: string 5 | phase: 'start' | 'progress' | 'error' | 'stop' 6 | } 7 | 8 | export type StatusCallback = (status: DeployEvent) => void 9 | -------------------------------------------------------------------------------- /src/utils/dev-server-banner.ts: -------------------------------------------------------------------------------- 1 | import boxen from 'boxen' 2 | 3 | import { chalk, log, NETLIFY_CYAN_HEX } from './command-helpers.js' 4 | 5 | export const printBanner = (options: { url: string }): void => { 6 | log( 7 | boxen(`Local dev server ready: ${chalk.inverse.cyan(options.url)}`, { 8 | padding: 1, 9 | margin: 1, 10 | textAlignment: 'center', 11 | borderStyle: 'round', 12 | borderColor: NETLIFY_CYAN_HEX, 13 | // This is an intentional half-width space to work around a unicode padding math bug in boxen 14 | title: '⬥ ', 15 | titleAlignment: 'center', 16 | }), 17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /src/utils/execa.ts: -------------------------------------------------------------------------------- 1 | import { env } from 'process' 2 | 3 | import execaLib from 'execa' 4 | 5 | // This is a thin layer on top of `execa` that allows consumers to provide an 6 | // alternative path to the module location, making it easier to mock its logic 7 | // in tests (see `tests/integration/utils/mock-execa.ts`). 8 | 9 | let execa: typeof execaLib 10 | 11 | if (env.NETLIFY_CLI_EXECA_PATH) { 12 | const execaMock = await import(env.NETLIFY_CLI_EXECA_PATH) 13 | execa = execaMock.default 14 | } else { 15 | execa = execaLib 16 | } 17 | 18 | export default execa 19 | -------------------------------------------------------------------------------- /src/utils/feature-flags.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Allows us to check if a feature flag is enabled for a site. 3 | * Due to versioning of the cli, and the desire to remove flags from 4 | * our feature flag service when they should always evaluate to true, 5 | * we can't just look for the presense of {featureFlagName: true}, as 6 | * the absense of a flag should also evaluate to the flag being enabled. 7 | * Instead, we return that the feature flag is enabled if it isn't 8 | * specifically set to false in the response 9 | */ 10 | export const isFeatureFlagEnabled = ( 11 | flagName: string, 12 | siteInfo: { feature_flags?: Record | undefined }, 13 | ): boolean => Boolean(siteInfo.feature_flags && siteInfo.feature_flags[flagName] !== false) 14 | 15 | /** 16 | * Retrieves all Feature flags from the siteInfo 17 | */ 18 | export const getFeatureFlagsFromSiteInfo = (siteInfo: { 19 | feature_flags?: Record | undefined 20 | }): FeatureFlags => ({ 21 | ...siteInfo.feature_flags, 22 | // see https://github.com/netlify/pod-dev-foundations/issues/581#issuecomment-1731022753 23 | zisi_golang_use_al2: isFeatureFlagEnabled('cli_golang_use_al2', siteInfo), 24 | netlify_build_frameworks_api: true, 25 | project_ceruledge_ui: true, 26 | }) 27 | 28 | export type FeatureFlags = Record 29 | -------------------------------------------------------------------------------- /src/utils/functions/constants.ts: -------------------------------------------------------------------------------- 1 | export const CLOCKWORK_USERAGENT = 'Netlify Clockwork' 2 | -------------------------------------------------------------------------------- /src/utils/functions/index.ts: -------------------------------------------------------------------------------- 1 | export * from './constants.js' 2 | export * from './functions.js' 3 | export * from './get-functions.js' 4 | -------------------------------------------------------------------------------- /src/utils/get-cli-package-json.ts: -------------------------------------------------------------------------------- 1 | import { readFile } from 'fs/promises' 2 | import { dirname, join } from 'path' 3 | import { fileURLToPath } from 'url' 4 | 5 | import normalizePackageData, { type Package } from 'normalize-package-data' 6 | 7 | let packageJson: Package | undefined 8 | 9 | const getPackageJson = async (): Promise => { 10 | if (!packageJson) { 11 | const packageJsonPath = join(dirname(fileURLToPath(import.meta.url)), '../../package.json') 12 | const packageData = JSON.parse(await readFile(packageJsonPath, 'utf-8')) as Record 13 | try { 14 | normalizePackageData(packageData) 15 | packageJson = packageData as Package 16 | return packageJson 17 | } catch (error) { 18 | throw new Error('Could not find package.json', { cause: error }) 19 | } 20 | } 21 | return packageJson 22 | } 23 | 24 | export default getPackageJson 25 | -------------------------------------------------------------------------------- /src/utils/get-site.ts: -------------------------------------------------------------------------------- 1 | import type { NetlifyAPI } from '@netlify/api' 2 | 3 | import { type APIError, logAndThrowError } from './command-helpers.js' 4 | import type { SiteInfo } from './types.js' 5 | 6 | export const getSiteByName = async (api: NetlifyAPI, siteName: string): Promise => { 7 | try { 8 | const sites = await api.listSites({ name: siteName, filter: 'all' }) 9 | const siteFoundByName = sites.find((filteredSite) => filteredSite.name === siteName) 10 | 11 | if (!siteFoundByName) { 12 | throw new Error(`Project "${siteName}" cannot be found`) 13 | } 14 | 15 | // FIXME(serhalp): `id` and `name` should be required in `netlify` package type 16 | return siteFoundByName as SiteInfo 17 | } catch (error_) { 18 | if ((error_ as APIError).status === 401) { 19 | return logAndThrowError(`${(error_ as APIError).message}: could not retrieve project`) 20 | } else { 21 | return logAndThrowError('Project not found. Please rerun "netlify link"') 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/utils/hooks/requires-site-info.ts: -------------------------------------------------------------------------------- 1 | import type { Command } from 'commander' 2 | 3 | import { logAndThrowError, warn, type APIError } from '../command-helpers.js' 4 | import type BaseCommand from '../../commands/base-command.js' 5 | 6 | /** 7 | * A preAction hook that errors out if siteInfo is an empty object 8 | */ 9 | const requiresSiteInfo = async (command: Command) => { 10 | // commander (at least the version we're on) is typed such that `.preAction()` can't accept 11 | // a subclass of `Command`. This type assertion avoids a lot of type noise in every call site. 12 | const { api, site } = (command as BaseCommand).netlify 13 | const siteId = site.id 14 | if (!siteId) { 15 | warn('Did you run `netlify link` yet?') 16 | return logAndThrowError(`You don't appear to be in a folder that is linked to a project`) 17 | } 18 | try { 19 | await api.getSite({ siteId }) 20 | } catch (error_) { 21 | // unauthorized 22 | if ((error_ as APIError).status === 401) { 23 | warn(`Log in with a different account or re-link to a project you have permission for`) 24 | return logAndThrowError(`Not authorized to view the currently linked project (${siteId})`) 25 | } 26 | // missing 27 | if ((error_ as APIError).status === 404) { 28 | return logAndThrowError(`The project this folder is linked to can't be found`) 29 | } 30 | 31 | return logAndThrowError(error_) 32 | } 33 | } 34 | 35 | export default requiresSiteInfo 36 | -------------------------------------------------------------------------------- /src/utils/init/plugins.ts: -------------------------------------------------------------------------------- 1 | import type { NormalizedCachedConfigConfig } from '../command-helpers.js' 2 | import type { Plugin } from '../types.js' 3 | 4 | const isPluginInstalled = (configPlugins: Plugin[], pluginName: string): boolean => 5 | configPlugins.some(({ package: configPlugin }) => configPlugin === pluginName) 6 | 7 | export const getRecommendPlugins = (frameworkPlugins: string[], config: NormalizedCachedConfigConfig): string[] => 8 | frameworkPlugins.filter((plugin) => !isPluginInstalled(config.plugins ?? [], plugin)) 9 | 10 | export const getUIPlugins = (configPlugins: Plugin[]): { package: string }[] => 11 | configPlugins.filter(({ origin }) => origin === 'ui').map(({ package: pkg }) => ({ package: pkg })) 12 | -------------------------------------------------------------------------------- /src/utils/multimap.ts: -------------------------------------------------------------------------------- 1 | export class MultiMap { 2 | private map = new Map() 3 | 4 | add(key: K, value: V) { 5 | this.map.set(key, [...(this.map.get(key) ?? []), value]) 6 | } 7 | 8 | get(key: K): readonly V[] { 9 | return this.map.get(key) ?? [] 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/utils/normalize-repo-url.ts: -------------------------------------------------------------------------------- 1 | import parseGitHubUrl from 'parse-github-url' 2 | 3 | /** 4 | * Normalize a user-provided repository specifier into a git URL and an HTTPS URL. 5 | * 6 | * @param repo Either a GitHub URL or a string in the format `owner/repo` (assumed to be GitHub) 7 | */ 8 | export const normalizeRepoUrl = (repo: string): { repoUrl: string; httpsUrl: string; repoName: string } => { 9 | const parsedRepoUrl = parseGitHubUrl(repo) 10 | if (!parsedRepoUrl?.owner || !parsedRepoUrl.name) { 11 | throw new Error(`Invalid repository URL: ${repo}`) 12 | } 13 | const repoUrl = parsedRepoUrl.hostname 14 | ? parsedRepoUrl.href 15 | : `git@github.com:${parsedRepoUrl.owner}/${parsedRepoUrl.name}.git` 16 | const httpsUrl = parsedRepoUrl.hostname 17 | ? `https://${parsedRepoUrl.hostname}/${parsedRepoUrl.owner}/${parsedRepoUrl.name}` 18 | : `https://github.com/${parsedRepoUrl.owner}/${parsedRepoUrl.name}` 19 | return { 20 | repoUrl, 21 | httpsUrl, 22 | repoName: parsedRepoUrl.name, 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/utils/open-browser.ts: -------------------------------------------------------------------------------- 1 | import process from 'process' 2 | 3 | import open from 'open' 4 | import isDockerContainer from 'is-docker' 5 | 6 | import { chalk, log } from './command-helpers.js' 7 | 8 | type BrowserUnableMessage = { 9 | message: string 10 | url: string 11 | } 12 | 13 | const unableToOpenBrowserMessage = function ({ message, url }: BrowserUnableMessage) { 14 | log('---------------------------') 15 | log(chalk.redBright(`Error: Unable to open browser automatically: ${message}`)) 16 | log(chalk.cyan('Please open your browser and open the URL below:')) 17 | log(chalk.bold(url)) 18 | log('---------------------------') 19 | } 20 | 21 | type OpenBrowsrProps = { 22 | silentBrowserNoneError?: boolean 23 | url: string 24 | } 25 | 26 | const openBrowser = async function ({ silentBrowserNoneError, url }: OpenBrowsrProps) { 27 | if (isDockerContainer()) { 28 | unableToOpenBrowserMessage({ url, message: 'Running inside a docker container' }) 29 | return 30 | } 31 | if (process.env.BROWSER === 'none') { 32 | if (!silentBrowserNoneError) { 33 | unableToOpenBrowserMessage({ url, message: "BROWSER environment variable is set to 'none'" }) 34 | } 35 | return 36 | } 37 | 38 | try { 39 | await open(url) 40 | } catch (error) { 41 | if (error instanceof Error) { 42 | unableToOpenBrowserMessage({ url, message: error.message }) 43 | } 44 | } 45 | } 46 | 47 | export default openBrowser 48 | -------------------------------------------------------------------------------- /src/utils/prompts/blob-delete-prompts.ts: -------------------------------------------------------------------------------- 1 | import { log } from '../command-helpers.js' 2 | 3 | import { confirmPrompt } from './confirm-prompt.js' 4 | import { destructiveCommandMessages } from './prompt-messages.js' 5 | 6 | export const promptBlobDelete = async (key: string, storeName: string): Promise => { 7 | const warningMessage = destructiveCommandMessages.blobDelete.generateWarning(key, storeName) 8 | 9 | log() 10 | log(warningMessage) 11 | log() 12 | log(destructiveCommandMessages.overwriteNotice) 13 | await confirmPrompt(destructiveCommandMessages.blobDelete.overwriteConfirmation) 14 | } 15 | -------------------------------------------------------------------------------- /src/utils/prompts/blob-set-prompt.ts: -------------------------------------------------------------------------------- 1 | import { log } from '../command-helpers.js' 2 | 3 | import { confirmPrompt } from './confirm-prompt.js' 4 | import { destructiveCommandMessages } from './prompt-messages.js' 5 | 6 | export const promptBlobSetOverwrite = async (key: string, storeName: string): Promise => { 7 | const warningMessage = destructiveCommandMessages.blobSet.generateWarning(key, storeName) 8 | 9 | log() 10 | log(warningMessage) 11 | log() 12 | log(destructiveCommandMessages.overwriteNotice) 13 | await confirmPrompt(destructiveCommandMessages.blobSet.overwriteConfirmation) 14 | } 15 | -------------------------------------------------------------------------------- /src/utils/prompts/confirm-prompt.ts: -------------------------------------------------------------------------------- 1 | import inquirer from 'inquirer' 2 | 3 | import { log, exit } from '../command-helpers.js' 4 | 5 | export const confirmPrompt = async (message: string): Promise => { 6 | try { 7 | const { confirm } = await inquirer.prompt({ 8 | type: 'confirm', 9 | name: 'confirm', 10 | message, 11 | default: false, 12 | }) 13 | log() 14 | if (!confirm) { 15 | exit() 16 | } 17 | } catch (error) { 18 | console.error(error) 19 | exit() 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/utils/prompts/env-clone-prompt.ts: -------------------------------------------------------------------------------- 1 | import { log } from '../command-helpers.js' 2 | import { EnvVar } from '../types.js' 3 | 4 | import { confirmPrompt } from './confirm-prompt.js' 5 | import { destructiveCommandMessages } from './prompt-messages.js' 6 | 7 | export const generateEnvVarsList = (envVarsToDelete: EnvVar[]) => envVarsToDelete.map((envVar) => envVar.key) 8 | 9 | /** 10 | * Prompts the user to confirm overwriting environment variables on a project. 11 | * 12 | * @param {string} siteId - The ID of the project. 13 | * @param {EnvVar[]} existingEnvVars - The environment variables that already exist on the project. 14 | * @returns {Promise} A promise that resolves when the user has confirmed the overwriting of the variables. 15 | */ 16 | export async function promptEnvCloneOverwrite(siteId: string, existingEnvVars: EnvVar[]): Promise { 17 | const { generateWarning } = destructiveCommandMessages.envClone 18 | 19 | const existingEnvVarKeys = generateEnvVarsList(existingEnvVars) 20 | const warningMessage = generateWarning(siteId) 21 | 22 | log() 23 | log(warningMessage) 24 | log() 25 | log(destructiveCommandMessages.envClone.noticeEnvVars) 26 | log() 27 | existingEnvVarKeys.forEach((envVar) => { 28 | log(envVar) 29 | }) 30 | log() 31 | log(destructiveCommandMessages.overwriteNotice) 32 | 33 | await confirmPrompt(destructiveCommandMessages.envClone.overwriteConfirmation) 34 | } 35 | -------------------------------------------------------------------------------- /src/utils/prompts/env-set-prompts.ts: -------------------------------------------------------------------------------- 1 | import { log } from '../command-helpers.js' 2 | 3 | import { confirmPrompt } from './confirm-prompt.js' 4 | import { destructiveCommandMessages } from './prompt-messages.js' 5 | 6 | export const promptOverwriteEnvVariable = async (key: string): Promise => { 7 | const warningMessage = destructiveCommandMessages.envSet.generateWarning(key) 8 | 9 | log() 10 | log(warningMessage) 11 | log() 12 | log(destructiveCommandMessages.overwriteNotice) 13 | await confirmPrompt(destructiveCommandMessages.envSet.overwriteConfirmation) 14 | } 15 | -------------------------------------------------------------------------------- /src/utils/prompts/env-unset-prompts.ts: -------------------------------------------------------------------------------- 1 | import { log } from '../command-helpers.js' 2 | 3 | import { confirmPrompt } from './confirm-prompt.js' 4 | import { destructiveCommandMessages } from './prompt-messages.js' 5 | 6 | /** 7 | * Logs a warning and prompts user to confirm overwriting an existing environment variable 8 | * 9 | * @param {string} key - The key of the environment variable that already exists 10 | * @returns {Promise} A promise that resolves when the user has confirmed overwriting the variable 11 | */ 12 | export const promptOverwriteEnvVariable = async (existingKey: string): Promise => { 13 | const { generateWarning } = destructiveCommandMessages.envUnset 14 | 15 | const warningMessage = generateWarning(existingKey) 16 | 17 | log(warningMessage) 18 | log() 19 | log(destructiveCommandMessages.overwriteNotice) 20 | await confirmPrompt(destructiveCommandMessages.envUnset.overwriteConfirmation) 21 | } 22 | -------------------------------------------------------------------------------- /src/utils/request-id.ts: -------------------------------------------------------------------------------- 1 | import { ulid } from 'ulid' 2 | 3 | export const generateRequestID = () => ulid() 4 | -------------------------------------------------------------------------------- /src/utils/run-git.ts: -------------------------------------------------------------------------------- 1 | import execa from './execa.js' 2 | 3 | export const runGit = async (args: string[], quiet: boolean) => { 4 | await execa('git', args, { 5 | ...(quiet ? {} : { stdio: 'inherit' }), 6 | }) 7 | } 8 | -------------------------------------------------------------------------------- /src/utils/run-program.ts: -------------------------------------------------------------------------------- 1 | import { injectForceFlagIfScripted } from './scripted-commands.js' 2 | import { BaseCommand } from '../commands/index.js' 3 | import { CI_FORCED_COMMANDS } from '../commands/main.js' 4 | 5 | // This function is used to run the program with the correct flags 6 | export const runProgram = async (program: BaseCommand, argv: string[]) => { 7 | const cmdName = argv[2] 8 | // checks if the command has a force option 9 | const isValidForceCommand = cmdName in CI_FORCED_COMMANDS 10 | 11 | if (isValidForceCommand) { 12 | injectForceFlagIfScripted(argv) 13 | } 14 | 15 | await program.parseAsync(argv) 16 | } 17 | -------------------------------------------------------------------------------- /src/utils/scripted-commands.ts: -------------------------------------------------------------------------------- 1 | import process from 'process' 2 | import { isCI } from 'ci-info' 3 | 4 | export const shouldForceFlagBeInjected = (argv: string[]): boolean => { 5 | // Is the command run in a non-interactive shell or CI/CD environment? 6 | const scriptedCommand = Boolean(!process.stdin.isTTY || isCI || process.env.CI) 7 | 8 | // Is the `--force` flag not already present? 9 | const noForceFlag = !argv.includes('--force') 10 | 11 | // ENV Variable used to tests prompts in CI/CD enviroment 12 | const testingPrompts = process.env.TESTING_PROMPTS !== 'true' 13 | 14 | // Prevents prompts from blocking scripted commands 15 | return Boolean(scriptedCommand && testingPrompts && noForceFlag) 16 | } 17 | 18 | export const injectForceFlagIfScripted = (argv: string[]) => { 19 | if (shouldForceFlagBeInjected(argv)) { 20 | argv.push('--force') 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/utils/sign-redirect.ts: -------------------------------------------------------------------------------- 1 | import jwt from 'jsonwebtoken' 2 | 3 | // https://docs.netlify.com/routing/redirects/rewrites-proxies/#signed-proxy-redirects 4 | // @ts-expect-error TS(7031) FIXME: Binding element 'deployContext' implicitly has an ... Remove this comment to see the full error message 5 | export const signRedirect = ({ deployContext, secret, siteID, siteURL }) => { 6 | const claims = { 7 | deploy_context: deployContext, 8 | netlify_id: siteID, 9 | site_url: siteURL, 10 | } 11 | const options = { 12 | expiresIn: '5 minutes' as const, 13 | issuer: 'netlify', 14 | } 15 | 16 | return jwt.sign(claims, secret, options) 17 | } 18 | -------------------------------------------------------------------------------- /src/utils/static-server.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | 3 | import fastifyStatic from '@fastify/static' 4 | import Fastify from 'fastify' 5 | 6 | import { log, NETLIFYDEVLOG } from './command-helpers.js' 7 | 8 | /** 9 | * @param {object} config 10 | * @param {import('./types.js').ServerSettings} config.settings 11 | */ 12 | // @ts-expect-error TS(7031) FIXME: Binding element 'settings' implicitly has an 'any'... Remove this comment to see the full error message 13 | export const startStaticServer = async ({ settings }) => { 14 | const server = Fastify() 15 | const rootPath = path.resolve(settings.dist) 16 | server.register(fastifyStatic, { 17 | root: rootPath, 18 | etag: false, 19 | acceptRanges: false, 20 | lastModified: false, 21 | }) 22 | 23 | server.setNotFoundHandler((_req, res) => { 24 | res.code(404).sendFile('404.html', rootPath) 25 | }) 26 | 27 | server.addHook('onRequest', (req, reply, done) => { 28 | reply.header('age', '0') 29 | reply.header('cache-control', 'public, max-age=0, must-revalidate') 30 | const validMethods = ['GET', 'HEAD'] 31 | if (!validMethods.includes(req.method)) { 32 | reply.code(405).send('Method Not Allowed') 33 | } 34 | done() 35 | }) 36 | await server.listen({ port: settings.frameworkPort }) 37 | const [address] = server.addresses() 38 | log(`\n${NETLIFYDEVLOG} Static server listening to`, settings.frameworkPort) 39 | return { family: address.family } 40 | } 41 | -------------------------------------------------------------------------------- /src/utils/telemetry/index.ts: -------------------------------------------------------------------------------- 1 | export { track, identify } from './telemetry.js' 2 | export { reportError } from './report-error.js' 3 | -------------------------------------------------------------------------------- /src/utils/telemetry/utils.ts: -------------------------------------------------------------------------------- 1 | import getCLIPackageJson from '../get-cli-package-json.js' 2 | 3 | export const { version: cliVersion } = await getCLIPackageJson() 4 | 5 | // @ts-expect-error TS(7006) FIXME: Parameter 'config' implicitly has an 'any' type. 6 | export const isTelemetryDisabled = function (config) { 7 | return config.get('telemetryDisabled') 8 | } 9 | -------------------------------------------------------------------------------- /src/utils/temporary-file.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'node:path' 2 | import * as os from 'node:os' 3 | import * as crypto from 'node:crypto' 4 | import * as fs from 'node:fs' 5 | 6 | const uniqueString = () => crypto.randomBytes(8).toString('hex') 7 | 8 | const tempDir = os.tmpdir() 9 | 10 | export function temporaryFile({ extension }: { extension?: string } = {}): string { 11 | const baseName = uniqueString() 12 | const ext = extension ? '.' + extension.replace(/^\./, '') : '' 13 | return path.join(tempDir, baseName + ext) 14 | } 15 | 16 | export function temporaryDirectory({ prefix = '' } = {}): string { 17 | const directory = fs.mkdtempSync(`${tempDir}${path.sep}${prefix}`) 18 | return directory 19 | } 20 | -------------------------------------------------------------------------------- /src/utils/validation.ts: -------------------------------------------------------------------------------- 1 | import { BANG, chalk } from './command-helpers.js' 2 | 3 | /** 4 | * @param {string} exampleCommand 5 | * @returns {(value:string, previous: unknown) => unknown} 6 | */ 7 | // @ts-expect-error TS(7006) FIXME: Parameter 'exampleCommand' implicitly has an 'any'... Remove this comment to see the full error message 8 | export const getGeoCountryArgParser = (exampleCommand) => (arg) => { 9 | // Validate that the arg passed is two letters only for country 10 | // See https://en.wikipedia.org/wiki/List_of_ISO_3166_country_codes 11 | if (!/^[a-z]{2}$/i.test(arg)) { 12 | throw new Error( 13 | `The geo country code must use a two letter abbreviation. 14 | ${chalk.red(BANG)} Example: 15 | ${exampleCommand}`, 16 | ) 17 | } 18 | return arg.toUpperCase() 19 | } 20 | -------------------------------------------------------------------------------- /src/utils/websockets/index.ts: -------------------------------------------------------------------------------- 1 | import WebSocket from 'ws' 2 | 3 | export const getWebSocket = (url: string) => new WebSocket(url) 4 | -------------------------------------------------------------------------------- /tests/integration/__fixtures__/dev-server-with-edge-functions-and-npm-modules/netlify.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | publish = "public" 3 | -------------------------------------------------------------------------------- /tests/integration/__fixtures__/dev-server-with-edge-functions-and-npm-modules/netlify/edge-functions/blobs.ts: -------------------------------------------------------------------------------- 1 | import { getStore } from '@netlify/blobs' 2 | 3 | export default async () => { 4 | const store = getStore('my-store') 5 | const metadata = { 6 | name: 'Netlify', 7 | features: { 8 | blobs: true, 9 | functions: true, 10 | }, 11 | } 12 | 13 | await store.set('my-key', 'hello world', { metadata }) 14 | 15 | const entry = await store.getWithMetadata('my-key') 16 | 17 | return Response.json(entry) 18 | } 19 | 20 | export const config = { 21 | path: '/blobs', 22 | } 23 | -------------------------------------------------------------------------------- /tests/integration/__fixtures__/dev-server-with-edge-functions-and-npm-modules/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dev-server-with-edge-functions-and-npm-modules", 3 | "lockfileVersion": 3, 4 | "requires": true, 5 | "packages": { 6 | "": { 7 | "dependencies": { 8 | "@netlify/blobs": "^6.0.0" 9 | } 10 | }, 11 | "node_modules/@netlify/blobs": { 12 | "version": "6.0.0", 13 | "resolved": "https://registry.npmjs.org/@netlify/blobs/-/blobs-6.0.0.tgz", 14 | "integrity": "sha512-xazBbwwZJX61nCAJXvZeHH/J46w1g6bjXscvgTFicdQrLbva9tndPuw8XjnFlFj+BsVC02oAmtRNMLaQTM7DSA==", 15 | "engines": { 16 | "node": "^14.16.0 || >=16.0.0" 17 | } 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /tests/integration/__fixtures__/dev-server-with-edge-functions-and-npm-modules/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "@netlify/blobs": "^6.0.0" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /tests/integration/__fixtures__/dev-server-with-edge-functions-and-npm-modules/public/ordertest.html: -------------------------------------------------------------------------------- 1 | origin 2 | -------------------------------------------------------------------------------- /tests/integration/__fixtures__/dev-server-with-edge-functions/.netlify/_edge-functions/integration-iscA.ts: -------------------------------------------------------------------------------- 1 | import { IntegrationsConfig } from 'https://edge.netlify.com' 2 | import createEdgeFunction from '../../src/edge-function.ts' 3 | 4 | export default createEdgeFunction('integration-iscA') 5 | 6 | export const config: IntegrationsConfig = { 7 | path: '/ordertest', 8 | } 9 | -------------------------------------------------------------------------------- /tests/integration/__fixtures__/dev-server-with-edge-functions/.netlify/_edge-functions/integration-iscB.ts: -------------------------------------------------------------------------------- 1 | import { IntegrationsConfig } from 'https://edge.netlify.com' 2 | import createEdgeFunction from '../../src/edge-function.ts' 3 | 4 | export default createEdgeFunction('integration-iscB') 5 | 6 | export const config: IntegrationsConfig = { 7 | path: '/ordertest', 8 | } 9 | -------------------------------------------------------------------------------- /tests/integration/__fixtures__/dev-server-with-edge-functions/.netlify/_edge-functions/integration-manifestA.ts: -------------------------------------------------------------------------------- 1 | import createEdgeFunction from '../../src/edge-function.ts' 2 | 3 | export default createEdgeFunction('integration-manifestA') 4 | -------------------------------------------------------------------------------- /tests/integration/__fixtures__/dev-server-with-edge-functions/.netlify/_edge-functions/integration-manifestB.ts: -------------------------------------------------------------------------------- 1 | import createEdgeFunction from '../../src/edge-function.ts' 2 | 3 | export default createEdgeFunction('integration-manifestB') 4 | -------------------------------------------------------------------------------- /tests/integration/__fixtures__/dev-server-with-edge-functions/.netlify/_edge-functions/integration-manifestC.ts: -------------------------------------------------------------------------------- 1 | import createEdgeFunction from '../../src/edge-function.ts' 2 | 3 | export default createEdgeFunction('integration-manifestC') 4 | -------------------------------------------------------------------------------- /tests/integration/__fixtures__/dev-server-with-edge-functions/.netlify/_edge-functions/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "functions": [ 3 | { 4 | "function": "integration-manifestB", 5 | "path": "/ordertest" 6 | }, 7 | { 8 | "function": "integration-manifestC", 9 | "path": "/ordertest" 10 | }, 11 | { 12 | "function": "integration-manifestA", 13 | "path": "/ordertest" 14 | } 15 | ], 16 | "version": 1 17 | } 18 | -------------------------------------------------------------------------------- /tests/integration/__fixtures__/dev-server-with-edge-functions/netlify.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | publish = "public" 3 | 4 | [[edge_functions]] 5 | path = "/ordertest" 6 | function = "user-tomlB" 7 | 8 | [[edge_functions]] 9 | path = "/ordertest" 10 | function = "user-tomlC" 11 | 12 | [[edge_functions]] 13 | path = "/ordertest" 14 | function = "user-tomlA" 15 | -------------------------------------------------------------------------------- /tests/integration/__fixtures__/dev-server-with-edge-functions/netlify/edge-functions/context-with-params.ts: -------------------------------------------------------------------------------- 1 | import { Config, Context } from 'https://edge.netlify.com' 2 | 3 | export default (_, context: Context) => Response.json(context) 4 | 5 | export const config: Config = { 6 | path: '/categories/:category/products/:product', 7 | } 8 | -------------------------------------------------------------------------------- /tests/integration/__fixtures__/dev-server-with-edge-functions/netlify/edge-functions/context.ts: -------------------------------------------------------------------------------- 1 | import { Config, Context } from 'https://edge.netlify.com' 2 | 3 | export default (_, context: Context) => Response.json(context) 4 | 5 | export const config: Config = { 6 | path: '/context', 7 | } 8 | -------------------------------------------------------------------------------- /tests/integration/__fixtures__/dev-server-with-edge-functions/netlify/edge-functions/delete-product.js: -------------------------------------------------------------------------------- 1 | export default (_, context) => new Response(`Deleted item successfully: ${context.params.sku}`) 2 | 3 | export const config = { 4 | path: '/products/:sku', 5 | method: 'DELETE', 6 | } 7 | -------------------------------------------------------------------------------- /tests/integration/__fixtures__/dev-server-with-edge-functions/netlify/edge-functions/echo-env.ts: -------------------------------------------------------------------------------- 1 | import { Config, Context } from 'https://edge.netlify.com' 2 | 3 | export default (_, context: Context) => Response.json(Netlify.env.toObject()) 4 | 5 | export const config: Config = { 6 | path: '/echo-env', 7 | } 8 | -------------------------------------------------------------------------------- /tests/integration/__fixtures__/dev-server-with-edge-functions/netlify/edge-functions/manual-cache-context-with-params.ts: -------------------------------------------------------------------------------- 1 | import { Config, Context } from 'https://edge.netlify.com' 2 | 3 | export default (_, context: Context) => Response.json(context) 4 | 5 | export const config: Config = { 6 | cache: 'manual', 7 | path: '/categories-after-cache/:category/products/:product', 8 | } 9 | -------------------------------------------------------------------------------- /tests/integration/__fixtures__/dev-server-with-edge-functions/netlify/edge-functions/uncaught-exception.ts: -------------------------------------------------------------------------------- 1 | import { Config, Context } from 'https://edge.netlify.com' 2 | 3 | export default (_, context: Context) => { 4 | thisWillThrow() 5 | } 6 | 7 | export const config: Config = { 8 | path: '/uncaught-exception', 9 | } 10 | -------------------------------------------------------------------------------- /tests/integration/__fixtures__/dev-server-with-edge-functions/netlify/edge-functions/user-iscA.ts: -------------------------------------------------------------------------------- 1 | import { Config } from 'https://edge.netlify.com' 2 | import createEdgeFunction from '../../src/edge-function.ts' 3 | 4 | export default createEdgeFunction('user-iscA') 5 | 6 | export const config: Config = { 7 | path: '/ordertest', 8 | } 9 | -------------------------------------------------------------------------------- /tests/integration/__fixtures__/dev-server-with-edge-functions/netlify/edge-functions/user-iscB.ts: -------------------------------------------------------------------------------- 1 | import { Config } from 'https://edge.netlify.com' 2 | import createEdgeFunction from '../../src/edge-function.ts' 3 | 4 | export default createEdgeFunction('user-iscB') 5 | 6 | export const config: Config = { 7 | path: '/ordertest', 8 | } 9 | -------------------------------------------------------------------------------- /tests/integration/__fixtures__/dev-server-with-edge-functions/netlify/edge-functions/user-tomlA.ts: -------------------------------------------------------------------------------- 1 | import createEdgeFunction from '../../src/edge-function.ts' 2 | 3 | export default createEdgeFunction('user-tomlA') 4 | -------------------------------------------------------------------------------- /tests/integration/__fixtures__/dev-server-with-edge-functions/netlify/edge-functions/user-tomlB.ts: -------------------------------------------------------------------------------- 1 | import createEdgeFunction from '../../src/edge-function.ts' 2 | 3 | export default createEdgeFunction('user-tomlB') 4 | -------------------------------------------------------------------------------- /tests/integration/__fixtures__/dev-server-with-edge-functions/netlify/edge-functions/user-tomlC.ts: -------------------------------------------------------------------------------- 1 | import createEdgeFunction from '../../src/edge-function.ts' 2 | 3 | export default createEdgeFunction('user-tomlC') 4 | -------------------------------------------------------------------------------- /tests/integration/__fixtures__/dev-server-with-edge-functions/public/ordertest.html: -------------------------------------------------------------------------------- 1 | origin 2 | -------------------------------------------------------------------------------- /tests/integration/__fixtures__/dev-server-with-edge-functions/src/edge-function.ts: -------------------------------------------------------------------------------- 1 | import { Context } from 'https://edge.netlify.com' 2 | 3 | export default (name: string) => async (_request: Request, context: Context) => { 4 | const response = await context.next() 5 | const content = await response.text() 6 | 7 | return new Response(`${name}|${String(content).trim()}`, response) 8 | } 9 | -------------------------------------------------------------------------------- /tests/integration/__fixtures__/dev-server-with-functions/functions/echo.js: -------------------------------------------------------------------------------- 1 | exports.handler = async (event) => ({ 2 | statusCode: 200, 3 | body: JSON.stringify(event), 4 | }) 5 | -------------------------------------------------------------------------------- /tests/integration/__fixtures__/dev-server-with-functions/functions/identity-validate-background.js: -------------------------------------------------------------------------------- 1 | exports.handler = async (event) => ({ 2 | statusCode: 200, 3 | body: JSON.stringify(event.body), 4 | }) 5 | -------------------------------------------------------------------------------- /tests/integration/__fixtures__/dev-server-with-functions/functions/ping.js: -------------------------------------------------------------------------------- 1 | exports.handler = async () => ({ 2 | statusCode: 200, 3 | body: 'ping', 4 | }) 5 | -------------------------------------------------------------------------------- /tests/integration/__fixtures__/dev-server-with-functions/functions/scheduled-isc-body.js: -------------------------------------------------------------------------------- 1 | const { schedule } = require('@netlify/functions') 2 | 3 | module.exports.handler = schedule('@daily', async () => { 4 | return { 5 | statusCode: 200, 6 | body: 'hello world', 7 | } 8 | }) 9 | -------------------------------------------------------------------------------- /tests/integration/__fixtures__/dev-server-with-functions/functions/scheduled-isc.js: -------------------------------------------------------------------------------- 1 | const { schedule } = require('@netlify/functions') 2 | 3 | module.exports.handler = schedule('@daily', async (event) => { 4 | const { next_run } = JSON.parse(event.body) 5 | 6 | return { 7 | statusCode: !!next_run ? 200 : 400, 8 | } 9 | }) 10 | -------------------------------------------------------------------------------- /tests/integration/__fixtures__/dev-server-with-functions/functions/scheduled-v2.mjs: -------------------------------------------------------------------------------- 1 | export default async () => { 2 | console.log('Hello!') 3 | } 4 | 5 | export const config = { 6 | schedule: '* * * * *', 7 | } 8 | -------------------------------------------------------------------------------- /tests/integration/__fixtures__/dev-server-with-functions/functions/scheduled.js: -------------------------------------------------------------------------------- 1 | exports.handler = async () => ({ 2 | statusCode: 200, 3 | }) 4 | -------------------------------------------------------------------------------- /tests/integration/__fixtures__/dev-server-with-functions/netlify.toml: -------------------------------------------------------------------------------- 1 | [functions] 2 | directory = "functions" 3 | 4 | [functions.scheduled] 5 | schedule = '@daily' 6 | 7 | [[redirects]] 8 | from = "/api/*" 9 | to = "/.netlify/functions/:splat" 10 | status = 200 11 | 12 | [[redirects]] 13 | from = "/with-params" 14 | to = "/.netlify/functions/echo?param1=hello¶m2=:param2" 15 | status = 200 16 | query = { param2 = ":param2" } 17 | -------------------------------------------------------------------------------- /tests/integration/__fixtures__/dev-server-with-functions/node_modules/@netlify/functions/index.js: -------------------------------------------------------------------------------- 1 | module.exports.schedule = (schedule, handler) => handler 2 | -------------------------------------------------------------------------------- /tests/integration/__fixtures__/dev-server-with-functions/node_modules/@netlify/functions/package.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /tests/integration/__fixtures__/dev-server-with-v2-functions/functions/blobs.js: -------------------------------------------------------------------------------- 1 | import { getStore } from '@netlify/blobs' 2 | 3 | export default async (req) => { 4 | const store = getStore('my-store') 5 | const metadata = { 6 | name: 'Netlify', 7 | features: { 8 | blobs: true, 9 | functions: true, 10 | }, 11 | } 12 | 13 | await store.set('my-key', 'hello world', { metadata }) 14 | 15 | const entry = await store.getWithMetadata('my-key') 16 | 17 | return Response.json(entry) 18 | } 19 | 20 | export const config = { 21 | path: '/blobs', 22 | } 23 | -------------------------------------------------------------------------------- /tests/integration/__fixtures__/dev-server-with-v2-functions/functions/brotli.js: -------------------------------------------------------------------------------- 1 | import { brotliCompressSync } from 'node:zlib' 2 | 3 | export default async () => { 4 | const text = "What's 🍞🏄‍♀️? A breadboard!".repeat(100) 5 | const buffer = new TextEncoder().encode(text) 6 | const brotli = brotliCompressSync(buffer) 7 | return new Response(brotli, { 8 | status: 200, 9 | headers: { 10 | 'Content-Encoding': 'br', 11 | }, 12 | }) 13 | } 14 | -------------------------------------------------------------------------------- /tests/integration/__fixtures__/dev-server-with-v2-functions/functions/context.js: -------------------------------------------------------------------------------- 1 | export default async (_request, context) => 2 | new Response( 3 | JSON.stringify({ 4 | ...context, 5 | cookies: { foo: context.cookies.get('foo') }, 6 | }), 7 | { 8 | status: 200, 9 | }, 10 | ) 11 | -------------------------------------------------------------------------------- /tests/integration/__fixtures__/dev-server-with-v2-functions/functions/custom-path-catchall.js: -------------------------------------------------------------------------------- 1 | export default async (req) => new Response(`Catchall Path`) 2 | 3 | export const config = { 4 | path: '/*', 5 | method: 'PATCH', 6 | } 7 | -------------------------------------------------------------------------------- /tests/integration/__fixtures__/dev-server-with-v2-functions/functions/custom-path-excluded.js: -------------------------------------------------------------------------------- 1 | export default async (req, context) => new Response(`Your product: ${context.params.sku}`) 2 | 3 | export const config = { 4 | path: '/custom-path-excluded/:sku', 5 | excludedPath: ['/custom-path-excluded/jacket'], 6 | } 7 | -------------------------------------------------------------------------------- /tests/integration/__fixtures__/dev-server-with-v2-functions/functions/custom-path-expression.js: -------------------------------------------------------------------------------- 1 | export default async (req, context) => new Response(`With expression path: ${JSON.stringify(context.params)}`) 2 | 3 | export const config = { 4 | path: '/products/:sku', 5 | preferStatic: true, 6 | } 7 | -------------------------------------------------------------------------------- /tests/integration/__fixtures__/dev-server-with-v2-functions/functions/custom-path-literal.js: -------------------------------------------------------------------------------- 1 | export default async (req) => new Response(`With literal path: ${req.url}`) 2 | 3 | export const config = { 4 | path: '/products', 5 | } 6 | -------------------------------------------------------------------------------- /tests/integration/__fixtures__/dev-server-with-v2-functions/functions/custom-path-root.js: -------------------------------------------------------------------------------- 1 | export default async (req) => new Response(`With literal path: ${req.url}`) 2 | 3 | export const config = { 4 | path: '/', 5 | method: 'GET', 6 | } 7 | -------------------------------------------------------------------------------- /tests/integration/__fixtures__/dev-server-with-v2-functions/functions/delete.js: -------------------------------------------------------------------------------- 1 | export default async (req, context) => new Response(`Deleted item successfully: ${context.params.sku}`) 2 | 3 | export const config = { 4 | path: '/products/:sku', 5 | method: 'DELETE', 6 | } 7 | -------------------------------------------------------------------------------- /tests/integration/__fixtures__/dev-server-with-v2-functions/functions/favicon.js: -------------------------------------------------------------------------------- 1 | export default () => new Response(`custom-generated favicon`) 2 | 3 | export const config = { 4 | path: '/favicon.ico', 5 | method: 'GET', 6 | } 7 | -------------------------------------------------------------------------------- /tests/integration/__fixtures__/dev-server-with-v2-functions/functions/log.js: -------------------------------------------------------------------------------- 1 | export default async () => { 2 | console.log('🪵🪵🪵') 3 | return new Response('') 4 | } 5 | -------------------------------------------------------------------------------- /tests/integration/__fixtures__/dev-server-with-v2-functions/functions/ping-ts.ts: -------------------------------------------------------------------------------- 1 | export default async () => 2 | new Response('pong', { 3 | status: 200, 4 | }) 5 | -------------------------------------------------------------------------------- /tests/integration/__fixtures__/dev-server-with-v2-functions/functions/ping.js: -------------------------------------------------------------------------------- 1 | export default async () => 2 | new Response('pong', { 3 | status: 200, 4 | }) 5 | -------------------------------------------------------------------------------- /tests/integration/__fixtures__/dev-server-with-v2-functions/functions/shadow-imagecdn.js: -------------------------------------------------------------------------------- 1 | export default () => new Response(`you were shadowed!`) 2 | 3 | export const config = { 4 | path: '/.netlify/images', 5 | } 6 | -------------------------------------------------------------------------------- /tests/integration/__fixtures__/dev-server-with-v2-functions/functions/stream.js: -------------------------------------------------------------------------------- 1 | export default async () => 2 | new Response( 3 | new ReadableStream({ 4 | start(controller) { 5 | controller.enqueue('first chunk') 6 | setTimeout(() => { 7 | controller.enqueue('second chunk') 8 | controller.close() 9 | }, 200) 10 | }, 11 | }), 12 | { 13 | status: 200, 14 | }, 15 | ) 16 | -------------------------------------------------------------------------------- /tests/integration/__fixtures__/dev-server-with-v2-functions/functions/uncaught-exception.js: -------------------------------------------------------------------------------- 1 | export default async () => { 2 | throw new Error('💣') 3 | } 4 | -------------------------------------------------------------------------------- /tests/integration/__fixtures__/dev-server-with-v2-functions/netlify.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | publish = "public" 3 | 4 | [functions] 5 | directory = "functions" 6 | 7 | [[redirects]] 8 | force = true 9 | from = "/v2-to-legacy-with-force" 10 | status = 200 11 | to = "/.netlify/functions/custom-path-literal" 12 | 13 | [[redirects]] 14 | from = "/v2-to-legacy-without-force" 15 | status = 200 16 | to = "/.netlify/functions/custom-path-literal" 17 | 18 | [[redirects]] 19 | force = true 20 | from = "/v2-to-custom-with-force" 21 | status = 200 22 | to = "/products" 23 | 24 | [[redirects]] 25 | from = "/v2-to-custom-without-force" 26 | status = 200 27 | to = "/products" 28 | -------------------------------------------------------------------------------- /tests/integration/__fixtures__/dev-server-with-v2-functions/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dev-server-with-v2-functions", 3 | "version": "1.0.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "dev-server-with-v2-functions", 9 | "version": "1.0.0", 10 | "license": "ISC", 11 | "dependencies": { 12 | "@netlify/blobs": "^6.0.1" 13 | } 14 | }, 15 | "node_modules/@netlify/blobs": { 16 | "version": "6.0.1", 17 | "resolved": "https://registry.npmjs.org/@netlify/blobs/-/blobs-6.0.1.tgz", 18 | "integrity": "sha512-giwK8g0HwJO5Qmii0OTo3Wmesm/NdIZ2nNh1mYjGKyh5aP5qrVx6aqepyrnsuMQZEFkSdcbprRDY4y1QyX59OA==", 19 | "engines": { 20 | "node": "^14.16.0 || >=16.0.0" 21 | } 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /tests/integration/__fixtures__/dev-server-with-v2-functions/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dev-server-with-v2-functions", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "@netlify/blobs": "^6.0.1" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /tests/integration/__fixtures__/dev-server-with-v2-functions/public/products.html: -------------------------------------------------------------------------------- 1 | /products from origin 2 | -------------------------------------------------------------------------------- /tests/integration/__fixtures__/dev-server-with-v2-functions/public/products/static.html: -------------------------------------------------------------------------------- 1 | this is a static page 2 | -------------------------------------------------------------------------------- /tests/integration/__fixtures__/dev-server-with-v2-functions/public/test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netlify/cli/18ba162a78ab98eed3f93dde0a537485bcccb450/tests/integration/__fixtures__/dev-server-with-v2-functions/public/test.png -------------------------------------------------------------------------------- /tests/integration/__fixtures__/dev-server-with-v2-functions/public/v2-to-custom-without-force.html: -------------------------------------------------------------------------------- 1 | /v2-to-custom-without-force from origin 2 | -------------------------------------------------------------------------------- /tests/integration/__fixtures__/dev-server-with-v2-functions/public/v2-to-legacy-without-force.html: -------------------------------------------------------------------------------- 1 | /v2-to-legacy-without-force from origin 2 | -------------------------------------------------------------------------------- /tests/integration/__fixtures__/empty-project/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netlify/cli/18ba162a78ab98eed3f93dde0a537485bcccb450/tests/integration/__fixtures__/empty-project/.gitkeep -------------------------------------------------------------------------------- /tests/integration/__fixtures__/hugo-site/.gitignore: -------------------------------------------------------------------------------- 1 | out 2 | .hugo_build.lock 3 | resources 4 | node_modules 5 | -------------------------------------------------------------------------------- /tests/integration/__fixtures__/hugo-site/config.toml: -------------------------------------------------------------------------------- 1 | baseURL = "http://example.org/" 2 | languageCode = "en-us" 3 | title = "My New Hugo Site" 4 | -------------------------------------------------------------------------------- /tests/integration/__fixtures__/hugo-site/content/_index.html: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'Home' 3 | date: 2021-04-07T22:20:17-06:00 4 | --- 5 | 6 |

Home page!

7 | -------------------------------------------------------------------------------- /tests/integration/__fixtures__/hugo-site/layouts/_default/list.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | Home 12 | About 13 |
14 | {{ .Content }} 15 | 16 | 17 | -------------------------------------------------------------------------------- /tests/integration/__fixtures__/hugo-site/netlify.toml: -------------------------------------------------------------------------------- 1 | [dev] 2 | command = "npm run serve" 3 | framework = "#custom" 4 | functions = "functions/" 5 | port = 7000 6 | publish = "out" 7 | targetPort = 1313 8 | -------------------------------------------------------------------------------- /tests/integration/__fixtures__/hugo-site/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hugo-site", 3 | "version": "1.0.0", 4 | "scripts": { 5 | "serve": "hugo server -d out" 6 | }, 7 | "license": "MIT", 8 | "devDependencies": { 9 | "hugo-bin": "^0.108.0" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /tests/integration/__fixtures__/hugo-site/static/_redirects: -------------------------------------------------------------------------------- 1 | /api/* /.netlify/functions/:splat 200 2 | 3 | # Must be last in list or all redirects will 404 4 | /* /404.html 404 5 | -------------------------------------------------------------------------------- /tests/integration/__fixtures__/images/test.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netlify/cli/18ba162a78ab98eed3f93dde0a537485bcccb450/tests/integration/__fixtures__/images/test.jpg -------------------------------------------------------------------------------- /tests/integration/__fixtures__/monorepo/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /tests/integration/__fixtures__/monorepo/.npmrc: -------------------------------------------------------------------------------- 1 | public-hoist-pattern[]=* -------------------------------------------------------------------------------- /tests/integration/__fixtures__/monorepo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "monorepo", 3 | "version": "1.0.0" 4 | } 5 | -------------------------------------------------------------------------------- /tests/integration/__fixtures__/monorepo/packages/app-1/dist/index.html: -------------------------------------------------------------------------------- 1 |

Hello world

2 | -------------------------------------------------------------------------------- /tests/integration/__fixtures__/monorepo/packages/app-1/netlify.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | cmd = "hello world" 3 | publish = "dist" 4 | 5 | [[plugins]] 6 | package = "./tools/build-plugin" 7 | -------------------------------------------------------------------------------- /tests/integration/__fixtures__/monorepo/packages/app-1/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "app-1", 3 | "version": "1.0.0" 4 | } 5 | -------------------------------------------------------------------------------- /tests/integration/__fixtures__/monorepo/pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - 'packages/*' 3 | - 'tools/build-plugin' 4 | -------------------------------------------------------------------------------- /tests/integration/__fixtures__/monorepo/tools/build-plugin/manifest.yaml: -------------------------------------------------------------------------------- 1 | name: log-package-path 2 | -------------------------------------------------------------------------------- /tests/integration/__fixtures__/monorepo/tools/build-plugin/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "build-plugin", 3 | "version": "1.0.0", 4 | "main": "src/index.ts", 5 | "devDependencies": { 6 | "@netlify/build": "^29.36.1" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /tests/integration/__fixtures__/monorepo/tools/build-plugin/src/index.ts: -------------------------------------------------------------------------------- 1 | import type { OnBuild } from '@netlify/build' 2 | 3 | export const onBuild: OnBuild = async ({ constants }) => { 4 | console.log(`@@ packagePath: ${constants.PACKAGE_PATH}`) 5 | console.log(`@@ cwd: ${process.cwd()}`) 6 | } 7 | -------------------------------------------------------------------------------- /tests/integration/__fixtures__/next-app-without-config/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /tests/integration/__fixtures__/next-app-without-config/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netlify/cli/18ba162a78ab98eed3f93dde0a537485bcccb450/tests/integration/__fixtures__/next-app-without-config/app/favicon.ico -------------------------------------------------------------------------------- /tests/integration/__fixtures__/next-app-without-config/app/layout.js: -------------------------------------------------------------------------------- 1 | import { Inter } from 'next/font/google' 2 | import './globals.css' 3 | 4 | const inter = Inter({ subsets: ['latin'] }) 5 | 6 | export const metadata = { 7 | title: 'Create Next App', 8 | description: 'Generated by create next app', 9 | } 10 | 11 | export default function RootLayout({ children }) { 12 | return ( 13 | 14 | {children} 15 | 16 | ) 17 | } 18 | -------------------------------------------------------------------------------- /tests/integration/__fixtures__/next-app-without-config/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "paths": { 4 | "@/*": ["./*"] 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /tests/integration/__fixtures__/next-app-without-config/next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = {} 3 | 4 | export default nextConfig 5 | -------------------------------------------------------------------------------- /tests/integration/__fixtures__/next-app-without-config/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "next-app-without-config", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "react": "^18", 13 | "react-dom": "^18", 14 | "next": "14.2.26" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /tests/integration/__fixtures__/next-app-without-config/public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/integration/__fixtures__/next-app-without-config/public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/integration/__fixtures__/next-app/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /tests/integration/__fixtures__/next-app/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netlify/cli/18ba162a78ab98eed3f93dde0a537485bcccb450/tests/integration/__fixtures__/next-app/app/favicon.ico -------------------------------------------------------------------------------- /tests/integration/__fixtures__/next-app/app/layout.js: -------------------------------------------------------------------------------- 1 | import { Inter } from 'next/font/google' 2 | import './globals.css' 3 | 4 | const inter = Inter({ subsets: ['latin'] }) 5 | 6 | export const metadata = { 7 | title: 'Create Next App', 8 | description: 'Generated by create next app', 9 | } 10 | 11 | export default function RootLayout({ children }) { 12 | return ( 13 | 14 | {children} 15 | 16 | ) 17 | } 18 | -------------------------------------------------------------------------------- /tests/integration/__fixtures__/next-app/netlify.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | publish = "public" 3 | 4 | [dev] 5 | targetPort = 6123 6 | 7 | [[redirects]] 8 | from = "*" 9 | to = "https://www.netlify.app/:splat" 10 | status = 200 11 | -------------------------------------------------------------------------------- /tests/integration/__fixtures__/next-app/next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = {} 3 | 4 | export default nextConfig 5 | -------------------------------------------------------------------------------- /tests/integration/__fixtures__/next-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "next-app", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev -p 6123", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "react": "^18", 13 | "react-dom": "^18", 14 | "next": "14.2.25" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /tests/integration/__fixtures__/next-app/public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/integration/__fixtures__/next-app/public/test.txt: -------------------------------------------------------------------------------- 1 | hello world 2 | -------------------------------------------------------------------------------- /tests/integration/__fixtures__/nx-integrated-monorepo/packages/blog/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../node_modules/nx/schemas/project-schema.json", 3 | "name": "blog", 4 | "projectType": "application", 5 | "sourceRoot": "packages/blog/src", 6 | "targets": { 7 | "build": { 8 | "outputs": ["{workspaceRoot}/dist/{projectRoot}"], 9 | "executor": "@nxtensions/astro:build", 10 | "options": {} 11 | }, 12 | "dev": { 13 | "executor": "@nxtensions/astro:dev", 14 | "options": {} 15 | }, 16 | "preview": { 17 | "dependsOn": [ 18 | { 19 | "target": "build", 20 | "projects": "self" 21 | } 22 | ], 23 | "executor": "@nxtensions/astro:preview", 24 | "options": {} 25 | }, 26 | "check": { 27 | "executor": "@nxtensions/astro:check" 28 | }, 29 | "sync": { 30 | "executor": "@nxtensions/astro:sync" 31 | } 32 | }, 33 | "tags": [] 34 | } 35 | -------------------------------------------------------------------------------- /tests/integration/__fixtures__/plugin-changing-publish-dir/dist/index.html: -------------------------------------------------------------------------------- 1 | Hello World! 2 | -------------------------------------------------------------------------------- /tests/integration/__fixtures__/plugin-changing-publish-dir/netlify.toml: -------------------------------------------------------------------------------- 1 | [[plugins]] 2 | package="/plugin" -------------------------------------------------------------------------------- /tests/integration/__fixtures__/plugin-changing-publish-dir/plugin/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | onBuild({ netlifyConfig }) { 3 | netlifyConfig.build.publish = 'dist' 4 | }, 5 | } 6 | -------------------------------------------------------------------------------- /tests/integration/__fixtures__/plugin-changing-publish-dir/plugin/manifest.yml: -------------------------------------------------------------------------------- 1 | name: change-publish-dir 2 | -------------------------------------------------------------------------------- /tests/integration/__fixtures__/plugin-changing-publish-dir/plugin/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "module" 3 | } 4 | -------------------------------------------------------------------------------- /tests/integration/assets/bundled-function-1.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netlify/cli/18ba162a78ab98eed3f93dde0a537485bcccb450/tests/integration/assets/bundled-function-1.zip -------------------------------------------------------------------------------- /tests/integration/commands/dev/dev.exec.test.ts: -------------------------------------------------------------------------------- 1 | import process from 'process' 2 | 3 | import { test } from 'vitest' 4 | 5 | import { callCli } from '../../utils/call-cli.js' 6 | import { getCLIOptions, withMockApi } from '../../utils/mock-api.js' 7 | import { withSiteBuilder } from '../../utils/site-builder.js' 8 | import { routes } from '../env/api-routes.js' 9 | 10 | test('should pass .env variables to exec command', async (t) => { 11 | await withSiteBuilder(t, async (builder) => { 12 | builder.withEnvFile({ env: { MY_SUPER_SECRET: 'SECRET' } }) 13 | await builder.build() 14 | 15 | await withMockApi(routes, async ({ apiUrl }) => { 16 | const cmd = process.platform === 'win32' ? 'set' : 'printenv' 17 | const output = (await callCli(['dev:exec', cmd], getCLIOptions({ builder, apiUrl }))) as string 18 | 19 | t.expect(output).toContain('Injected .env file env vars: MY_SUPER_SECRET') 20 | t.expect(output).toContain('MY_SUPER_SECRET=SECRET') 21 | }) 22 | }) 23 | }) 24 | -------------------------------------------------------------------------------- /tests/integration/commands/dev/dev.geo.test.ts: -------------------------------------------------------------------------------- 1 | import process from 'process' 2 | 3 | import { test } from 'vitest' 4 | 5 | import { callCli } from '../../utils/call-cli.js' 6 | import { withSiteBuilder } from '../../utils/site-builder.js' 7 | 8 | test('should throw if invalid country arg is passed', async (t) => { 9 | await withSiteBuilder(t, async (builder) => { 10 | await builder.build() 11 | 12 | const options = { 13 | cwd: builder.directory, 14 | extendEnv: false, 15 | PATH: process.env.PATH, 16 | } 17 | 18 | const errors = await Promise.allSettled([ 19 | callCli(['dev', '--geo=mock', '--country=a1'], options), 20 | callCli(['dev', '--geo=mock', '--country=NotARealCountryCode'], options), 21 | callCli(['dev', '--geo=mock', '--country='], options), 22 | ]) 23 | 24 | errors.forEach((error) => { 25 | t.expect(error.status).toEqual('rejected') 26 | }) 27 | }) 28 | }) 29 | -------------------------------------------------------------------------------- /tests/integration/commands/didyoumean/__snapshots__/didyoumean.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`suggests closest matching command on typo 1`] = ` 4 | "› Warning: sta is not a netlify command. 5 | ? Did you mean api (y/N) " 6 | `; 7 | 8 | exports[`suggests closest matching command on typo 2`] = ` 9 | "› Warning: opeen is not a netlify command. 10 | ? Did you mean open (y/N) " 11 | `; 12 | 13 | exports[`suggests closest matching command on typo 3`] = ` 14 | "› Warning: hel is not a netlify command. 15 | ? Did you mean dev (y/N) " 16 | `; 17 | 18 | exports[`suggests closest matching command on typo 4`] = ` 19 | "› Warning: versio is not a netlify command. 20 | ? Did you mean serve (y/N) " 21 | `; 22 | -------------------------------------------------------------------------------- /tests/integration/commands/didyoumean/didyoumean.test.ts: -------------------------------------------------------------------------------- 1 | import { test } from 'vitest' 2 | 3 | import { callCli } from '../../utils/call-cli.js' 4 | import { normalize } from '../../utils/snapshots.js' 5 | 6 | test('suggests closest matching command on typo', async (t) => { 7 | // failures are expected since we effectively quit out of the prompts 8 | const errors = await Promise.allSettled([ 9 | callCli(['sta']) as Promise, 10 | callCli(['opeen']) as Promise, 11 | callCli(['hel']) as Promise, 12 | callCli(['versio']) as Promise, 13 | ]) 14 | 15 | for (const error of errors) { 16 | t.expect(error.status).toEqual('rejected') 17 | t.expect(error).toHaveProperty('reason.stdout', t.expect.any(String)) 18 | t.expect( 19 | normalize((error as { reason: { stdout: string } }).reason.stdout, { duration: true, filePath: true }), 20 | ).toMatchSnapshot() 21 | } 22 | }) 23 | -------------------------------------------------------------------------------- /tests/integration/commands/envelope/__snapshots__/envelope.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`command/envelope > env:clone should return success message 1`] = `"Successfully cloned environment variables from site-name-a to site-name-b"`; 4 | -------------------------------------------------------------------------------- /tests/integration/commands/functions-create/__snapshots__/functions-create.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`functions:create command > fixture: nx-integrated-monorepo > should create a new edge function 1`] = ` 4 | "[[edge_functions]] 5 | function = \\"abtest\\" 6 | path = \\"/test\\"" 7 | `; 8 | -------------------------------------------------------------------------------- /tests/integration/commands/help/help.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from 'vitest' 2 | 3 | import { callCli } from '../../utils/call-cli.js' 4 | import { normalize } from '../../utils/snapshots.js' 5 | 6 | describe('help command', () => { 7 | test('netlify help', async () => { 8 | const cliResponse = (await callCli(['help'])) as string 9 | expect(normalize(cliResponse)).toMatchSnapshot() 10 | }) 11 | 12 | test('netlify help completion', async () => { 13 | const cliResponse = (await callCli(['help', 'completion'])) as string 14 | expect(normalize(cliResponse)).toMatchSnapshot() 15 | }) 16 | }) 17 | -------------------------------------------------------------------------------- /tests/integration/commands/recipes/__snapshots__/recipes.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`commands/recipes > Shows a list of all the available recipes 1`] = ` 4 | ".-----------------------------------------------------------------------------------------. 5 | | Usage: netlify recipes | 6 | |-----------------------------------------------------------------------------------------| 7 | | Name | Description | 8 | |---------------|-------------------------------------------------------------------------| 9 | | ai-context | Manage context files for AI tools | 10 | | blobs-migrate | Migrate legacy Netlify Blobs stores | 11 | | vscode | Create VS Code settings for an optimal experience with Netlify projects | 12 | '-----------------------------------------------------------------------------------------'" 13 | `; 14 | 15 | exports[`commands/recipes > Suggests closest matching recipe on typo 1`] = ` 16 | "⬥ vsc is not a valid recipe name. 17 | ? Did you mean vscode (y/N) " 18 | `; 19 | -------------------------------------------------------------------------------- /tests/integration/commands/status/__snapshots__/status.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`fixture: empty-project > should print status for a linked project 1`] = ` 4 | { 5 | "admin-url": "https://app.netlify.com/projects/test-site/overview", 6 | "site-id": "site_id", 7 | "site-name": "site-name", 8 | "site-url": "https://test-site.netlify.app/", 9 | } 10 | `; 11 | 12 | exports[`fixture: empty-project > should print status for a linked project 2`] = ` 13 | { 14 | "Email": "test@netlify.com", 15 | "Name": "Test User", 16 | "Teams": [ 17 | "Test User", 18 | ], 19 | } 20 | `; 21 | -------------------------------------------------------------------------------- /tests/integration/frameworks/hugo.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from 'vitest' 2 | 3 | import { FixtureTestContext, setupFixtureTests } from '../utils/fixture.js' 4 | import fetch from 'node-fetch' 5 | 6 | setupFixtureTests('hugo-site', { devServer: true }, () => { 7 | test('should not infinite redirect when -d flag is passed', async ({ devServer }) => { 8 | const response = await fetch(`${devServer?.url}/`).then((res) => res.text()) 9 | 10 | expect(response).toContain('Home page!') 11 | }) 12 | }) 13 | -------------------------------------------------------------------------------- /tests/integration/utils/call-cli.ts: -------------------------------------------------------------------------------- 1 | import execa from 'execa' 2 | 3 | import { cliPath } from './cli-path.js' 4 | 5 | const CLI_TIMEOUT = 3e5 6 | 7 | /** 8 | * Calls the Cli with a max timeout. 9 | * 10 | * If the `parseJson` argument is specified then the result will be converted into an object. 11 | */ 12 | // FIXME(ndhoule): Discriminate on return type depending on `parseJson` option; it should be a 13 | // `Promise` when false and a `Promise` when true. 14 | export const callCli = async function ( 15 | args: string[] = [], 16 | execOptions: execa.NodeOptions = {}, 17 | parseJson = false, 18 | ): // eslint-disable-next-line @typescript-eslint/no-explicit-any 19 | Promise { 20 | const { stdout, stderr } = await execa.node(cliPath, args, { 21 | timeout: CLI_TIMEOUT, 22 | nodeOptions: [], 23 | ...execOptions, 24 | }) 25 | if (process.env.DEBUG_TESTS) { 26 | process.stdout.write(stdout) 27 | process.stderr.write(stderr) 28 | } 29 | 30 | if (parseJson) { 31 | return JSON.parse(stdout) 32 | } 33 | return stdout 34 | } 35 | -------------------------------------------------------------------------------- /tests/integration/utils/cli-path.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import { fileURLToPath } from 'url' 3 | 4 | const __dirname = path.dirname(fileURLToPath(import.meta.url)) 5 | 6 | export const cliPath = path.resolve(__dirname, '../../../bin/run.js') 7 | -------------------------------------------------------------------------------- /tests/integration/utils/curl.ts: -------------------------------------------------------------------------------- 1 | import execa from 'execa' 2 | 3 | const CURL_TIMEOUT = 1e5 4 | 5 | export const curl = async (url: string, args: string[] = []): Promise => { 6 | const { stdout } = await execa('curl', [...args, url], { timeout: CURL_TIMEOUT }) 7 | return stdout 8 | } 9 | -------------------------------------------------------------------------------- /tests/integration/utils/external-server-cli.ts: -------------------------------------------------------------------------------- 1 | import process from 'process' 2 | 3 | import { startExternalServer } from './external-server.js' 4 | 5 | const port = Number.parseInt(process.argv[2]) 6 | 7 | if (Number.isNaN(port)) { 8 | throw new TypeError(`Invalid port`) 9 | } 10 | 11 | console.log('Running external server on port', port, process.env.NETLIFY_DEV) 12 | 13 | startExternalServer({ port }) 14 | -------------------------------------------------------------------------------- /tests/integration/utils/external-server.ts: -------------------------------------------------------------------------------- 1 | import { env } from 'process' 2 | 3 | import express from 'express' 4 | 5 | export const startExternalServer = ({ host, port }: { host?: string | undefined; port?: number | undefined } = {}) => { 6 | const app = express() 7 | app.use(express.urlencoded({ extended: true })) 8 | app.all('*', function onRequest(req, res) { 9 | res.json({ 10 | url: req.url, 11 | body: req.body as string, 12 | method: req.method, 13 | headers: req.headers, 14 | env, 15 | }) 16 | }) 17 | 18 | return app.listen({ port, host }) 19 | } 20 | -------------------------------------------------------------------------------- /tests/integration/utils/inquirer-mock-prompt.ts: -------------------------------------------------------------------------------- 1 | // tests/utils/inquirer-mock.ts 2 | import inquirer from 'inquirer' 3 | import { vi } from 'vitest' 4 | 5 | export const mockPrompt = (response = { confirm: true }) => { 6 | // Create the mock function 7 | const mockFn = vi.fn().mockResolvedValue(response) 8 | 9 | // Preserve the original properties of inquirer.prompt 10 | Object.assign(mockFn, inquirer.prompt) 11 | 12 | // Create the spy with our prepared mock 13 | const spy = vi.spyOn(inquirer, 'prompt').mockImplementation(mockFn) 14 | 15 | inquirer.registerPrompt = vi.fn() 16 | inquirer.prompt.registerPrompt = vi.fn() 17 | 18 | return spy 19 | } 20 | 21 | export const spyOnMockPrompt = () => { 22 | return vi.spyOn(inquirer, 'prompt') 23 | } 24 | -------------------------------------------------------------------------------- /tests/integration/utils/mock-execa.ts: -------------------------------------------------------------------------------- 1 | import { rm, writeFile } from 'fs/promises' 2 | import { pathToFileURL } from 'url' 3 | 4 | import { temporaryFile } from '../../../src/utils/temporary-file.js' 5 | 6 | // Saves to disk a JavaScript file with the contents provided and returns 7 | // an environment variable that replaces the `execa` module implementation. 8 | // A cleanup method is also returned, allowing the consumer to remove the 9 | // mock file. 10 | export const createMock = async (contents: string) => { 11 | const path = temporaryFile({ extension: 'js' }) 12 | 13 | await writeFile(path, contents) 14 | 15 | const env = { 16 | // windows needs 'file://' paths 17 | NETLIFY_CLI_EXECA_PATH: pathToFileURL(path).href, 18 | } 19 | const cleanup = () => 20 | rm(path, { force: true, recursive: true }).catch(() => { 21 | // no-op 22 | }) 23 | 24 | return [env, cleanup] as const 25 | } 26 | -------------------------------------------------------------------------------- /tests/integration/utils/mock-program.ts: -------------------------------------------------------------------------------- 1 | import { runProgram } from '../../../src/utils/run-program.js' 2 | import { createMainCommand } from '../../../src/commands/index.js' 3 | 4 | export const runMockProgram = async (argv: string[]) => { 5 | // inject the force flag if the command is a non-interactive shell or Ci enviroment 6 | const program = createMainCommand() 7 | 8 | await runProgram(program, argv) 9 | } 10 | -------------------------------------------------------------------------------- /tests/integration/utils/pause.ts: -------------------------------------------------------------------------------- 1 | export const pause = (interval: number): Promise => 2 | new Promise((resolve) => { 3 | setTimeout(resolve, interval) 4 | }) 5 | -------------------------------------------------------------------------------- /tests/integration/utils/process.ts: -------------------------------------------------------------------------------- 1 | import pTimeout from 'p-timeout' 2 | import kill from 'tree-kill' 3 | import type execa from 'execa' 4 | 5 | const PROCESS_EXIT_TIMEOUT = 5e3 6 | 7 | export const killProcess = async (ps: execa.ExecaChildProcess): Promise => { 8 | if (ps.pid === undefined) { 9 | throw new Error('process.pid is empty; cannot kill a process that is not started') 10 | } 11 | 12 | kill(ps.pid) 13 | await pTimeout( 14 | ps.catch(() => {}), 15 | { 16 | milliseconds: PROCESS_EXIT_TIMEOUT, 17 | fallback: () => {}, 18 | }, 19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /tests/unit/lib/edge-functions/bootstrap.test.ts: -------------------------------------------------------------------------------- 1 | import { env } from 'process' 2 | 3 | import { describe, expect, test } from 'vitest' 4 | 5 | import { getBootstrapURL, FALLBACK_BOOTSTRAP_URL } from '../../../../src/lib/edge-functions/bootstrap.js' 6 | 7 | describe('`getBootstrapURL()`', () => { 8 | test('Returns the URL in the `NETLIFY_EDGE_BOOTSTRAP` URL, if set', async () => { 9 | const mockBootstrapURL = 'https://edge.netlify/bootstrap.ts' 10 | 11 | env.NETLIFY_EDGE_BOOTSTRAP = mockBootstrapURL 12 | 13 | const bootstrapURL = await getBootstrapURL() 14 | 15 | delete env.NETLIFY_EDGE_BOOTSTRAP 16 | 17 | expect(bootstrapURL).toEqual(mockBootstrapURL) 18 | }) 19 | 20 | test('Returns a publicly accessible URL', { retry: 3 }, async () => { 21 | const bootstrapURL = await getBootstrapURL() 22 | 23 | // We shouldn't get the fallback URL, because that means we couldn't get 24 | // the URL from the `@netlify/edge-functions` module. 25 | expect(bootstrapURL).not.toBe(FALLBACK_BOOTSTRAP_URL) 26 | 27 | const res = await fetch(bootstrapURL) 28 | 29 | expect(res.status).toBe(200) 30 | expect(res.headers.get('content-type')).toMatch(/^application\/typescript/) 31 | }) 32 | }) 33 | -------------------------------------------------------------------------------- /tests/unit/lib/edge-functions/proxy.test.ts: -------------------------------------------------------------------------------- 1 | import { Buffer } from 'buffer' 2 | 3 | import { describe, expect, test } from 'vitest' 4 | 5 | import { createSiteInfoHeader } from '../../../../src/lib/edge-functions/proxy.js' 6 | 7 | describe('createSiteInfoHeader', () => { 8 | test('builds a base64 string', () => { 9 | const siteInfo = { id: 'site_id', name: 'site_name', url: 'site_url' } 10 | const output = createSiteInfoHeader(siteInfo) 11 | const parsedOutput = JSON.parse(Buffer.from(output, 'base64').toString('utf-8')) as unknown 12 | 13 | expect(parsedOutput).toEqual(siteInfo) 14 | }) 15 | 16 | test('builds a base64 string if there is no siteInfo passed', () => { 17 | const siteInfo = {} 18 | const output = createSiteInfoHeader(siteInfo) 19 | const parsedOutput = JSON.parse(Buffer.from(output, 'base64').toString('utf-8')) as unknown 20 | 21 | expect(parsedOutput).toEqual({}) 22 | }) 23 | }) 24 | -------------------------------------------------------------------------------- /tests/unit/lib/functions/netlify-function.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from 'vitest' 2 | 3 | import NetlifyFunction from '../../../../src/lib/functions/netlify-function.js' 4 | 5 | test('should return the correct function url for a NetlifyFunction object', () => { 6 | const port = 7331 7 | const functionName = 'test-function' 8 | 9 | const functionUrl = `http://localhost:${port.toString()}/.netlify/functions/${functionName}` 10 | 11 | const ntlFunction = new NetlifyFunction({ 12 | name: functionName, 13 | settings: { functionsPort: port }, 14 | // @ts-expect-error TS(2741) FIXME: Property ''*'' is missing in type '{ "test-functio... Remove this comment to see the full error message 15 | config: { functions: { [functionName]: {} } }, 16 | }) 17 | 18 | expect(ntlFunction.url).toBe(functionUrl) 19 | }) 20 | -------------------------------------------------------------------------------- /tests/unit/lib/functions/runtimes/go/index.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test, vi } from 'vitest' 2 | import type { ExecaReturnValue } from 'execa' 3 | 4 | import { runFunctionsProxy } from '../../../../../../src/lib/functions/local-proxy.js' 5 | import { invokeFunction } from '../../../../../../src/lib/functions/runtimes/go/index.js' 6 | 7 | vi.mock('../../../../../../src/lib/functions/local-proxy.js', () => ({ runFunctionsProxy: vi.fn() })) 8 | 9 | test.each([ 10 | ['body', 'thebody'] as const, 11 | ['headers', { 'X-Single': 'A' }] as const, 12 | ['multiValueHeaders', { 'X-Multi': ['B', 'C'] }] as const, 13 | ['statusCode', 200] as const, 14 | ])('should return %s', async (prop, expected) => { 15 | vi.mocked(runFunctionsProxy).mockResolvedValue( 16 | // This mock doesn't implement the full execa return value API, just the part put under test 17 | { stdout: JSON.stringify({ [prop]: expected }) } as ExecaReturnValue, 18 | ) 19 | 20 | // @ts-expect-error TS(2740) FIXME: Type '{ mainFile: string; buildData: { binaryPath:... Remove this comment to see the full error message 21 | const match = await invokeFunction({ func: { mainFile: '', buildData: { binaryPath: 'foo' } } }) 22 | expect(match[prop]).toEqual(expected) 23 | }) 24 | -------------------------------------------------------------------------------- /tests/unit/lib/functions/runtimes/rust/index.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test, vi } from 'vitest' 2 | 3 | import { runFunctionsProxy } from '../../../../../../src/lib/functions/local-proxy.js' 4 | import { invokeFunction } from '../../../../../../src/lib/functions/runtimes/rust/index.js' 5 | 6 | vi.mock('../../../../../../src/lib/functions/local-proxy.js', () => ({ runFunctionsProxy: vi.fn() })) 7 | 8 | test.each([ 9 | ['body', 'thebody'] as const, 10 | ['headers', { 'X-Single': 'A' }] as const, 11 | ['multiValueHeaders', { 'X-Multi': ['B', 'C'] }] as const, 12 | ['statusCode', 200] as const, 13 | ])('should return %s', async (prop, expected) => { 14 | vi.mocked(runFunctionsProxy).mockResolvedValue( 15 | // @ts-expect-error(ndhoule): Intentionally not mocking entire execa API surface 16 | { stdout: JSON.stringify({ [prop]: expected }) }, 17 | ) 18 | 19 | // @ts-expect-error TS(2740) FIXME: Type '{ mainFile: string; buildData: { binaryPath:... Remove this comment to see the full error message 20 | const match = await invokeFunction({ func: { mainFile: '', buildData: { binaryPath: 'foo' } } }) 21 | expect(match[prop]).toEqual(expected) 22 | }) 23 | -------------------------------------------------------------------------------- /tests/unit/utils/command-helpers.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from 'vitest' 2 | 3 | import { normalizeConfig } from '../../../src/utils/command-helpers.js' 4 | 5 | describe('normalizeConfig', () => { 6 | test('should remove publish and publishOrigin property if publishOrigin is "default"', () => { 7 | const config = { build: { publish: 'a', publishOrigin: 'default' } } 8 | 9 | // @ts-expect-error TS(2345) FIXME: Argument of type '{ build: { publish: string; publ... Remove this comment to see the full error message 10 | expect(normalizeConfig(config)).toEqual({ build: {} }) 11 | }) 12 | 13 | test('should return same config object if publishOrigin is not "default"', () => { 14 | const config = { build: { publish: 'a', publishOrigin: 'b' } } 15 | 16 | // @ts-expect-error TS(2345) FIXME: Argument of type '{ build: { publish: string; publ... Remove this comment to see the full error message 17 | expect(normalizeConfig(config)).toBe(config) 18 | }) 19 | }) 20 | -------------------------------------------------------------------------------- /tests/unit/utils/copy-template-dir/fixtures/1.txt: -------------------------------------------------------------------------------- 1 | hello {{foo}} sama 2 | -------------------------------------------------------------------------------- /tests/unit/utils/copy-template-dir/fixtures/2.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netlify/cli/18ba162a78ab98eed3f93dde0a537485bcccb450/tests/unit/utils/copy-template-dir/fixtures/2.txt -------------------------------------------------------------------------------- /tests/unit/utils/copy-template-dir/fixtures/3.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netlify/cli/18ba162a78ab98eed3f93dde0a537485bcccb450/tests/unit/utils/copy-template-dir/fixtures/3.txt -------------------------------------------------------------------------------- /tests/unit/utils/copy-template-dir/fixtures/_.a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netlify/cli/18ba162a78ab98eed3f93dde0a537485bcccb450/tests/unit/utils/copy-template-dir/fixtures/_.a -------------------------------------------------------------------------------- /tests/unit/utils/copy-template-dir/fixtures/_c: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netlify/cli/18ba162a78ab98eed3f93dde0a537485bcccb450/tests/unit/utils/copy-template-dir/fixtures/_c -------------------------------------------------------------------------------- /tests/unit/utils/copy-template-dir/fixtures/foo/4.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netlify/cli/18ba162a78ab98eed3f93dde0a537485bcccb450/tests/unit/utils/copy-template-dir/fixtures/foo/4.txt -------------------------------------------------------------------------------- /tests/unit/utils/copy-template-dir/fixtures/foo/_.b: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netlify/cli/18ba162a78ab98eed3f93dde0a537485bcccb450/tests/unit/utils/copy-template-dir/fixtures/foo/_.b -------------------------------------------------------------------------------- /tests/unit/utils/copy-template-dir/fixtures/foo/_d: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netlify/cli/18ba162a78ab98eed3f93dde0a537485bcccb450/tests/unit/utils/copy-template-dir/fixtures/foo/_d -------------------------------------------------------------------------------- /tests/unit/utils/copy-template-dir/fixtures/{{foo}}.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netlify/cli/18ba162a78ab98eed3f93dde0a537485bcccb450/tests/unit/utils/copy-template-dir/fixtures/{{foo}}.txt -------------------------------------------------------------------------------- /tests/unit/utils/copy-template-dir/fixtures/{{foo}}/5.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netlify/cli/18ba162a78ab98eed3f93dde0a537485bcccb450/tests/unit/utils/copy-template-dir/fixtures/{{foo}}/5.txt -------------------------------------------------------------------------------- /tests/unit/utils/deploy/util.test.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'path' 2 | 3 | import { describe, expect, test } from 'vitest' 4 | 5 | import { normalizePath } from '../../../../src/utils/deploy/util.js' 6 | 7 | describe('normalizePath', () => { 8 | test('normalizes relative file paths', () => { 9 | const input = join('foo', 'bar', 'baz.js') 10 | expect(normalizePath(input)).toBe('foo/bar/baz.js') 11 | }) 12 | 13 | test('normalizePath should throw the error if name is invalid', () => { 14 | expect(() => normalizePath('invalid name#')).toThrowError() 15 | expect(() => normalizePath('invalid name?')).toThrowError() 16 | expect(() => normalizePath('??')).toThrowError() 17 | expect(() => normalizePath('#')).toThrowError() 18 | }) 19 | }) 20 | -------------------------------------------------------------------------------- /tests/unit/utils/parse-raw-flags.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from 'vitest' 2 | 3 | import { aggressiveJSONParse, parseRawFlags } from '../../../src/utils/parse-raw-flags.js' 4 | 5 | describe('parse-raw-flags', () => { 6 | test('JSONTruthy works with various inputs', () => { 7 | const testPairs = [ 8 | { 9 | input: 'true', 10 | wanted: true, 11 | }, 12 | { 13 | input: 'false', 14 | wanted: false, 15 | }, 16 | { 17 | input: JSON.stringify({ foo: 'bar' }), 18 | wanted: { foo: 'bar' }, 19 | }, 20 | { 21 | input: 'Hello-world 1234', 22 | wanted: 'Hello-world 1234', 23 | }, 24 | ] 25 | 26 | testPairs.forEach((pair) => { 27 | expect(aggressiveJSONParse(pair.input)).toEqual(pair.wanted) 28 | }) 29 | }) 30 | 31 | test('parseRawFlags works', () => { 32 | const input = ['FAUNA', 'FOO', 'BAR', '--hey', 'hi', '--heep'] 33 | 34 | const expected = { hey: 'hi', heep: true } 35 | 36 | expect(parseRawFlags(input)).toEqual(expected) 37 | }) 38 | }) 39 | -------------------------------------------------------------------------------- /tests/unit/utils/read-repo-url.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from 'vitest' 2 | 3 | import { parseRepoURL } from '../../../src/utils/read-repo-url.js' 4 | 5 | describe('parseRepoURL', () => { 6 | test('should parse GitHub URL', () => { 7 | const url = new URL('https://github.com/netlify-labs/all-the-functions/tree/master/functions/9-using-middleware') 8 | // parseRepoURL expects the result of url.parse 9 | const [repo, contentPath] = parseRepoURL('GitHub', { path: url.pathname }) 10 | 11 | expect(repo).toBe('netlify-labs/all-the-functions') 12 | expect(contentPath).toBe('functions/9-using-middleware') 13 | }) 14 | 15 | test('should fail on GitLab URL', () => { 16 | const url = new URL('https://gitlab.com/netlify-labs/all-the-functions/-/blob/master/functions/9-using-middleware') 17 | expect(() => parseRepoURL('GitLab', { path: url.pathname })).toThrowError('Unsupported host GitLab') 18 | }) 19 | }) 20 | -------------------------------------------------------------------------------- /tests/unit/utils/rules-proxy.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from 'vitest' 2 | 3 | import { getLanguage } from '../../../src/utils/rules-proxy.js' 4 | 5 | describe('getLanguage', () => { 6 | test('detects language', () => { 7 | const language = getLanguage({ 'accept-language': 'ur' }) 8 | 9 | expect(language).toBe('ur') 10 | }) 11 | }) 12 | -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["@tsconfig/recommended", "@tsconfig/node18"], 3 | "compilerOptions": { 4 | "allowJs": true, 5 | "downlevelIteration": true, 6 | "noEmit": true, 7 | "incremental": true, 8 | "resolveJsonModule": true, 9 | "typeRoots": ["node_modules/@types", "types"] 10 | }, 11 | "exclude": ["**/__fixtures__/**", "dist/", "functions-templates/", "site/"] 12 | } 13 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "declaration": true, 5 | "declarationMap": true, 6 | "noEmit": false, 7 | "outDir": "dist", 8 | "sourceMap": true 9 | }, 10 | "include": ["src/**/*.ts"] 11 | } 12 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.base.json" 3 | } 4 | -------------------------------------------------------------------------------- /types/express-logging/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'express-logging' { 2 | import { RequestHandler } from 'express'; 3 | 4 | interface LoggerOptions {blacklist?: string[]} 5 | 6 | interface Logger { 7 | info(...args: unknown[]): void; 8 | error(...args: unknown[]): void; 9 | warn(...args: unknown[]): void; 10 | debug?(...args: unknown[]): void; 11 | log(...args: unknown[]): void; 12 | } 13 | 14 | function expressLogging(logger?: Logger, options?: LoggerOptions): RequestHandler; 15 | 16 | export default expressLogging; 17 | } 18 | -------------------------------------------------------------------------------- /types/lambda-local/index.d.ts: -------------------------------------------------------------------------------- 1 | // This module is "TypeScript" but contains no actual type annotations, so 2 | // the resulting `.d.ts` file is just useless `any`s. 3 | declare module 'lambda-local' { 4 | declare interface Options { 5 | clientContext?: string | Record 6 | environment?: Record 7 | esm?: boolean 8 | event: Record 9 | lambdaFunc?: unknown 10 | lambdaPath?: string 11 | region?: string 12 | timeoutMs?: number 13 | verboseLevel?: -1 | 0 | 1 | 2 | 3 14 | } 15 | 16 | // See https://github.com/ashiina/lambda-local/blob/8914e6804533450fa68c56fe6c34858b645735d0/src/lambdalocal.ts#L110 17 | // But this whole thing is kind of a lie (we're exercising a code path where the `event` we pass in is returned, so 18 | // this is just our type we happen to define. 19 | declare interface LambdaEvent { 20 | body?: NodeJS.ReadableStream | string | undefined 21 | headers?: Record | undefined 22 | level?: unknown 23 | multiValueHeaders?: Record | undefined 24 | isBase64Encoded?: boolean 25 | statusCode?: number | undefined 26 | } 27 | 28 | export declare function getLogger(): { level: string } 29 | export declare function execute(opts: Options): Promise 30 | } 31 | -------------------------------------------------------------------------------- /types/maxstache-stream/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'maxstache-stream' { 2 | import { Transform } from 'stream'; 3 | 4 | function maxstacheStream(vars: Record): Transform; 5 | 6 | export default maxstacheStream; 7 | } 8 | -------------------------------------------------------------------------------- /types/maxstache/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'maxstache' { 2 | function maxstache(str: string, ctx: Record): string; 3 | 4 | export default maxstache; 5 | } 6 | -------------------------------------------------------------------------------- /types/netlify-redirector/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'netlify-redirector' { 2 | export interface Options { 3 | jwtSecret?: string 4 | jwtRoleClaim?: string 5 | } 6 | export interface Request { 7 | scheme: string 8 | host: string 9 | path: string 10 | query: string 11 | getHeader: (name: string) => string 12 | getCookie: (name: string) => string 13 | } 14 | export type Match = ( 15 | | { 16 | from: string 17 | to: string 18 | host: string 19 | scheme: string 20 | status: number 21 | force: boolean 22 | negative: boolean 23 | proxyHeaders?: Record 24 | signingSecret?: string 25 | } 26 | | { 27 | force404: true 28 | } 29 | ) & { 30 | force404?: boolean 31 | conditions: Record 32 | exceptions: Record 33 | } 34 | export interface RedirectMatcher { 35 | match(req: Request): Match | null 36 | } 37 | export function parsePlain(rules: string, options: Options): Promise 38 | export function parseJSON(rules: string, options: Options): Promise 39 | } 40 | -------------------------------------------------------------------------------- /types/tomlify-j04/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'tomlify-j0.4' { 2 | export interface Tomlify { 3 | toToml( 4 | input: unknown, 5 | options?: { 6 | space?: number | undefined 7 | replace?: ((key: string, value: unknown) => string | boolean) | undefined 8 | }, 9 | ): string 10 | } 11 | 12 | const tomlify: Tomlify 13 | 14 | export default tomlify 15 | } 16 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config' 2 | 3 | export default defineConfig({ 4 | test: { 5 | include: ['tests/**/*.test.js', 'tests/**/*.test.ts'], 6 | testTimeout: 90_000, 7 | hookTimeout: 90_000, 8 | server: { 9 | deps: { 10 | inline: [ 11 | // Force Vitest to preprocess write-file-atomic via Vite, which lets us mock its `fs` 12 | // import. 13 | 'write-file-atomic', 14 | ], 15 | }, 16 | }, 17 | snapshotFormat: { 18 | escapeString: true, 19 | }, 20 | // Pin to vitest@1 behavior: https://vitest.dev/guide/migration.html#default-pool-is-forks. 21 | // TODO(serhalp) Remove this and fix hanging `next-app-without-config` fixture on Windows. 22 | pool: 'threads', 23 | poolOptions: { 24 | threads: { 25 | singleThread: true, 26 | }, 27 | }, 28 | coverage: { 29 | provider: 'v8', 30 | reporter: ['text', 'lcov'], 31 | }, 32 | }, 33 | }) 34 | -------------------------------------------------------------------------------- /vitest.e2e.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config' 2 | 3 | export default defineConfig({ 4 | test: { 5 | include: ['e2e/**/*.e2e.[jt]s'], 6 | testTimeout: 1200_000, 7 | // Pin to vitest@1 behavior: https://vitest.dev/guide/migration.html#default-pool-is-forks. 8 | // TODO(serhalp) Remove this and fix flaky hanging e2e tests on Windows. 9 | pool: 'threads', 10 | poolOptions: { 11 | threads: { 12 | singleThread: true, 13 | }, 14 | }, 15 | }, 16 | }) 17 | --------------------------------------------------------------------------------