├── .editorconfig ├── .eslintrc.cjs ├── .gitattributes ├── .github ├── .kodiak.toml ├── CODEOWNERS ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── pull_request_template.md └── workflows │ ├── fossa.yml │ ├── github-packages-releaser.yml │ ├── integration.yml │ ├── release-please.yml │ └── workflow.yml ├── .gitignore ├── .prettierrc.json ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── commitlint.config.cjs ├── deno ├── bundle.ts ├── config.ts ├── lib │ ├── common.ts │ ├── consts.ts │ ├── stage2.test.ts │ └── stage2.ts └── vendor │ ├── deno.land │ ├── std@0.177.0 │ │ ├── _util │ │ │ ├── asserts.ts │ │ │ └── os.ts │ │ └── path │ │ │ ├── _constants.ts │ │ │ ├── _interface.ts │ │ │ ├── _util.ts │ │ │ ├── common.ts │ │ │ ├── glob.ts │ │ │ ├── mod.ts │ │ │ ├── posix.ts │ │ │ ├── separator.ts │ │ │ └── win32.ts │ ├── std@0.98.0 │ │ └── async │ │ │ ├── deferred.ts │ │ │ ├── delay.ts │ │ │ ├── mod.ts │ │ │ ├── mux_async_iterator.ts │ │ │ ├── pool.ts │ │ │ └── tee.ts │ └── x │ │ ├── dir@1.5.1 │ │ └── data_local_dir │ │ │ └── mod.ts │ │ ├── eszip@v0.55.2 │ │ ├── eszip_wasm.generated.js │ │ ├── eszip_wasm_bg.wasm │ │ ├── loader.ts │ │ └── mod.ts │ │ ├── retry@v2.0.0 │ │ ├── deps.ts │ │ ├── misc.ts │ │ ├── mod.ts │ │ ├── retry │ │ │ ├── decorator.ts │ │ │ ├── options.ts │ │ │ ├── retry.ts │ │ │ ├── tooManyTries.ts │ │ │ └── utils │ │ │ │ ├── options.ts │ │ │ │ ├── tools.ts │ │ │ │ ├── untilDefined │ │ │ │ ├── decorators.ts │ │ │ │ └── retry.ts │ │ │ │ ├── untilResponse │ │ │ │ ├── decorators.ts │ │ │ │ └── retry.ts │ │ │ │ └── untilTruthy │ │ │ │ ├── decorators.ts │ │ │ │ └── retry.ts │ │ └── wait │ │ │ ├── decorators.ts │ │ │ ├── options.ts │ │ │ ├── timeoutError.ts │ │ │ └── wait.ts │ │ └── wasmbuild@0.15.1 │ │ ├── cache.ts │ │ └── loader.ts │ └── import_map.json ├── node ├── __snapshots__ │ └── declaration.test.ts.snap ├── bridge.test.ts ├── bridge.ts ├── bundle.ts ├── bundle_error.ts ├── bundler.test.ts ├── bundler.ts ├── config.test.ts ├── config.ts ├── declaration.test.ts ├── declaration.ts ├── deploy_config.test.ts ├── deploy_config.ts ├── downloader.test.ts ├── downloader.ts ├── edge_function.ts ├── feature_flags.ts ├── finder.test.ts ├── finder.ts ├── formats │ ├── eszip.ts │ └── javascript.ts ├── home_path.ts ├── import_map.test.ts ├── import_map.ts ├── index.ts ├── layer.ts ├── logger.test.ts ├── logger.ts ├── main.test.ts ├── manifest.test.ts ├── manifest.ts ├── npm_dependencies.ts ├── npm_import_error.ts ├── package_json.test.ts ├── package_json.ts ├── platform.ts ├── rate_limit.ts ├── server │ ├── server.test.ts │ ├── server.ts │ └── util.ts ├── stage_2.test.ts ├── types.test.ts ├── types.ts ├── utils │ ├── error.ts │ ├── fs.ts │ ├── non_nullable.ts │ ├── sha256.ts │ └── urlpattern.ts ├── validation │ └── manifest │ │ ├── __snapshots__ │ │ └── index.test.ts.snap │ │ ├── error.ts │ │ ├── index.test.ts │ │ ├── index.ts │ │ └── schema.ts └── vendor │ └── module_graph │ ├── media_type.ts │ └── module_graph.ts ├── package-lock.json ├── package.json ├── renovate.json5 ├── shared ├── consts.ts └── stage2.ts ├── test ├── fixtures │ ├── deno.win.zip │ ├── deno.zip │ ├── imports_json │ │ └── functions │ │ │ ├── dict.json │ │ │ └── func1.ts │ ├── imports_node_specifier │ │ └── netlify │ │ │ └── edge-functions │ │ │ └── func1.ts │ ├── imports_npm_module │ │ ├── functions │ │ │ ├── func1.ts │ │ │ └── lib │ │ │ │ └── util.ts │ │ ├── helper.ts │ │ ├── import_map.json │ │ ├── node_modules │ │ │ ├── child-1 │ │ │ │ ├── files │ │ │ │ │ ├── file1.txt │ │ │ │ │ └── file2.txt │ │ │ │ ├── index.js │ │ │ │ └── package.json │ │ │ ├── child-2 │ │ │ │ ├── index.js │ │ │ │ └── package.json │ │ │ ├── grandchild-1 │ │ │ │ ├── index.js │ │ │ │ └── package.json │ │ │ ├── parent-1 │ │ │ │ ├── index.js │ │ │ │ └── package.json │ │ │ ├── parent-2 │ │ │ │ ├── index.js │ │ │ │ └── package.json │ │ │ └── parent-3 │ │ │ │ ├── index.js │ │ │ │ └── package.json │ │ └── package.json │ ├── imports_npm_module_scheme │ │ └── functions │ │ │ └── func1.ts │ ├── invalid_functions │ │ └── functions │ │ │ └── func1.ts │ ├── monorepo_npm_module │ │ ├── node_modules │ │ │ ├── child-1 │ │ │ │ ├── files │ │ │ │ │ ├── file1.txt │ │ │ │ │ └── file2.txt │ │ │ │ ├── index.js │ │ │ │ └── package.json │ │ │ ├── child-2 │ │ │ │ ├── index.js │ │ │ │ └── package.json │ │ │ ├── grandchild-1 │ │ │ │ ├── index.js │ │ │ │ └── package.json │ │ │ ├── parent-1 │ │ │ │ ├── index.js │ │ │ │ └── package.json │ │ │ ├── parent-2 │ │ │ │ ├── index.js │ │ │ │ └── package.json │ │ │ └── parent-3 │ │ │ │ ├── index.js │ │ │ │ └── package.json │ │ ├── package.json │ │ └── packages │ │ │ └── frontend │ │ │ ├── functions │ │ │ ├── func1.ts │ │ │ └── lib │ │ │ │ └── util.ts │ │ │ ├── helper.ts │ │ │ └── import_map.json │ ├── serve_test │ │ ├── .netlify │ │ │ ├── .gitignore │ │ │ └── edge-functions │ │ │ │ ├── greet.ts │ │ │ │ └── import_map.json │ │ ├── helper.ts │ │ ├── netlify │ │ │ └── edge-functions │ │ │ │ ├── echo_env.ts │ │ │ │ ├── global_netlify.ts │ │ │ │ └── import-map.json │ │ └── node_modules │ │ │ ├── @pt-committee │ │ │ └── identidade │ │ │ │ ├── index.js │ │ │ │ └── package.json │ │ │ ├── @types │ │ │ └── pt-committee__identidade │ │ │ │ ├── index.d.ts │ │ │ │ └── package.json │ │ │ ├── dictionary │ │ │ ├── index.js │ │ │ ├── package.json │ │ │ └── words.txt │ │ │ └── id │ │ │ ├── index.cjs │ │ │ ├── index.mjs │ │ │ ├── package.json │ │ │ └── types.d.ts │ ├── tsx │ │ ├── functions │ │ │ └── func1.tsx │ │ └── node_modules │ │ │ └── react │ │ │ ├── cjs │ │ │ ├── react.development.js │ │ │ └── react.production.min.js │ │ │ ├── index.js │ │ │ └── package.json │ ├── with_config │ │ ├── .netlify │ │ │ └── edge-functions │ │ │ │ ├── config.json │ │ │ │ ├── framework-func1.ts │ │ │ │ ├── framework-func2.ts │ │ │ │ └── import_map.json │ │ ├── helper.ts │ │ └── netlify │ │ │ └── edge-functions │ │ │ ├── user-func1.ts │ │ │ ├── user-func2.ts │ │ │ ├── user-func3.ts │ │ │ ├── user-func4.ts │ │ │ └── user-func5.ts │ ├── with_deploy_config │ │ ├── .netlify │ │ │ └── edge-functions │ │ │ │ ├── func2.ts │ │ │ │ ├── func3.ts │ │ │ │ ├── import_map.json │ │ │ │ └── manifest.json │ │ ├── netlify │ │ │ └── edge-functions │ │ │ │ └── func1.ts │ │ └── util.ts │ ├── with_import_maps │ │ ├── functions │ │ │ ├── config.json │ │ │ ├── func1.ts │ │ │ └── import_map.json │ │ ├── helper.ts │ │ ├── helper2.ts │ │ └── user-functions │ │ │ ├── func2.ts │ │ │ ├── func3 │ │ │ └── func3.ts │ │ │ └── import_map.json │ └── with_layers │ │ ├── functions │ │ ├── config.json │ │ └── func1.ts │ │ └── layer.ts ├── integration │ ├── functions │ │ └── func1.ts │ ├── internal-functions │ │ └── func2.ts │ └── test.js └── util.ts ├── tsconfig.json └── vitest.config.ts /.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 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { overrides } = require('@netlify/eslint-config-node') 4 | 5 | module.exports = { 6 | extends: '@netlify/eslint-config-node', 7 | ignorePatterns: ['deno/**/*', 'node/vendor/**', 'test/deno/**/*', 'test/fixtures/**/*'], 8 | parserOptions: { 9 | sourceType: 'module', 10 | }, 11 | rules: { 12 | complexity: 'off', 13 | 'import/extensions': 'off', 14 | 'max-lines': 'off', 15 | 'max-lines-per-function': 'off', 16 | 'max-statements': 'off', 17 | 'node/no-missing-import': 'off', 18 | 'no-magic-numbers': 'off', 19 | 'no-shadow': 'off', 20 | 'no-use-before-define': 'off', 21 | 'unicorn/prefer-json-parse-buffer': 'off', 22 | 'no-unused-vars': 'off', 23 | '@typescript-eslint/no-unused-vars': ['error', { ignoreRestSiblings: true }], 24 | }, 25 | overrides: [ 26 | ...overrides, 27 | { 28 | files: ['node/**/*.test.ts', 'vitest.config.ts'], 29 | rules: { 30 | 'max-lines-per-function': 'off', 31 | 'max-nested-callbacks': 'off', 32 | 'max-statements': 'off', 33 | 'no-magic-numbers': 'off', 34 | }, 35 | }, 36 | ], 37 | } 38 | -------------------------------------------------------------------------------- /.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/ecosystem-pod-developer-foundations 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: 'Please replace with a clear and descriptive title' 5 | labels: 'type: bug' 6 | assignees: '' 7 | --- 8 | 9 | Thanks for reporting this bug! 10 | 11 | Please search other issues to make sure this bug has not already been reported. 12 | 13 | Then fill in the sections below. 14 | 15 | **Describe the bug** 16 | 17 | A clear and concise description of what the bug is. 18 | 19 | **Configuration** 20 | 21 | Please enter the following command in a terminal and copy/paste its output: 22 | 23 | ```bash 24 | npx envinfo --system --binaries 25 | ``` 26 | 27 | **Pull requests** 28 | 29 | Pull requests are welcome! If you would like to help us fix this bug, please check our 30 | [contributions guidelines](../blob/main/CONTRIBUTING.md). 31 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: 'Please replace with a clear and descriptive title' 5 | labels: 'type: feature' 6 | assignees: '' 7 | --- 8 | 9 | 14 | 15 | **Which problem is this feature request solving?** 16 | 17 | 20 | 21 | **Describe the solution you'd like** 22 | 23 | 26 | 27 | **Describe alternatives you've considered** 28 | 29 | 32 | 33 | **Can you submit a pull request?** 34 | 35 | Yes/No. 36 | 37 | 41 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | 🎉 Thanks for sending this pull request! 🎉 2 | 3 | Please make sure the title is clear and descriptive. 4 | 5 | If you are fixing a typo or documentation, please skip these instructions. 6 | 7 | Otherwise please fill in the sections below. 8 | 9 | **Which problem is this pull request solving?** 10 | 11 | Example: I'm always frustrated when [...] 12 | 13 | **List other issues or pull requests related to this problem** 14 | 15 | Example: This fixes #5012 16 | 17 | **Describe the solution you've chosen** 18 | 19 | Example: I've fixed this by [...] 20 | 21 | **Describe alternatives you've considered** 22 | 23 | Example: Another solution would be [...] 24 | 25 | **Checklist** 26 | 27 | Please add a `x` inside each checkbox: 28 | 29 | - [ ] I have read the [contribution guidelines](../blob/main/CONTRIBUTING.md). 30 | - [ ] The status checks are successful (continuous integration). Those can be seen below. 31 | 32 | **A picture of a cute animal (not mandatory but encouraged)** 33 | -------------------------------------------------------------------------------- /.github/workflows/fossa.yml: -------------------------------------------------------------------------------- 1 | name: Dependency License Scanning 2 | 3 | on: 4 | push: 5 | branches: 6 | - chore/fossa-workflow 7 | - main 8 | 9 | defaults: 10 | run: 11 | shell: bash 12 | 13 | jobs: 14 | fossa: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v4 19 | - name: Download fossa cli 20 | run: |- 21 | mkdir -p $HOME/.local/bin 22 | curl https://raw.githubusercontent.com/fossas/fossa-cli/master/install.sh | bash -s -- -b $HOME/.local/bin 23 | echo "$HOME/.local/bin" >> $GITHUB_PATH 24 | - name: Fossa init 25 | run: fossa init 26 | - name: Set env 27 | run: echo "line_number=$(grep -n "project" .fossa.yml | cut -f1 -d:)" >> $GITHUB_ENV 28 | - name: Configuration 29 | run: |- 30 | sed -i "${line_number}s|.*| project: git@github.com:${GITHUB_REPOSITORY}.git|" .fossa.yml 31 | cat .fossa.yml 32 | - name: Upload dependencies 33 | run: fossa analyze --debug 34 | env: 35 | FOSSA_API_KEY: ${{ secrets.FOSSA_API_KEY }} 36 | -------------------------------------------------------------------------------- /.github/workflows/github-packages-releaser.yml: -------------------------------------------------------------------------------- 1 | name: Publish package to GitHub Packages 2 | on: 3 | release: 4 | types: [created] 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | permissions: 9 | id-token: write 10 | contents: read 11 | packages: write 12 | steps: 13 | - uses: actions/checkout@v4 14 | - uses: actions/setup-node@v4 15 | with: 16 | node-version: '16.x' 17 | registry-url: 'https://npm.pkg.github.com' 18 | - run: npm ci 19 | - run: npm publish --provenance 20 | env: 21 | NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 22 | -------------------------------------------------------------------------------- /.github/workflows/integration.yml: -------------------------------------------------------------------------------- 1 | name: Integration 2 | on: 3 | # Ensure GitHub actions are not run twice for same commits 4 | push: 5 | branches: [main] 6 | tags: ['*'] 7 | pull_request: 8 | types: [opened, synchronize, reopened] 9 | jobs: 10 | build: 11 | runs-on: ${{ matrix.os }} 12 | timeout-minutes: 30 13 | strategy: 14 | matrix: 15 | os: [ubuntu-latest, macOS-latest, windows-latest] 16 | node-version: [14.16.0, '*'] 17 | exclude: 18 | - os: macOS-latest 19 | node-version: 14.16.0 20 | - os: windows-latest 21 | node-version: 14.16.0 22 | fail-fast: false 23 | steps: 24 | - name: Git checkout 25 | uses: actions/checkout@v4 26 | - name: Node.js ${{ matrix.node-version }} 27 | uses: actions/setup-node@v4 28 | with: 29 | node-version: ${{ matrix.node-version }} 30 | cache: 'npm' 31 | check-latest: true 32 | - name: Setup Deno 33 | uses: denoland/setup-deno@v1 34 | with: 35 | # Should match the `DENO_VERSION_RANGE` constant in `node/bridge.ts`. 36 | deno-version: v1.37.0 37 | - name: Install dependencies 38 | run: npm ci 39 | - name: Tests 40 | run: npm run test:integration 41 | -------------------------------------------------------------------------------- /.github/workflows/release-please.yml: -------------------------------------------------------------------------------- 1 | name: release-please 2 | on: 3 | push: 4 | branches: 5 | - main 6 | jobs: 7 | release-please: 8 | runs-on: ubuntu-latest 9 | permissions: 10 | id-token: write 11 | contents: write 12 | pull-requests: write 13 | steps: 14 | - uses: navikt/github-app-token-generator@793caf0d755fb4d6e88150825f680f188535cb48 15 | id: get-token 16 | with: 17 | private-key: ${{ secrets.TOKENS_PRIVATE_KEY }} 18 | app-id: ${{ secrets.TOKENS_APP_ID }} 19 | - uses: GoogleCloudPlatform/release-please-action@v4 20 | id: release 21 | with: 22 | token: ${{ steps.get-token.outputs.token }} 23 | release-type: node 24 | package-name: '@netlify/edge-bundler' 25 | - uses: actions/checkout@v4 26 | if: ${{ steps.release.outputs.release_created }} 27 | - uses: actions/setup-node@v4 28 | with: 29 | node-version: '*' 30 | cache: 'npm' 31 | check-latest: true 32 | registry-url: 'https://registry.npmjs.org' 33 | if: ${{ steps.release.outputs.release_created }} 34 | - name: Setup Deno 35 | uses: denoland/setup-deno@v1 36 | with: 37 | deno-version: v1.x 38 | if: ${{ steps.release.outputs.release_created }} 39 | - run: npm publish --provenance 40 | if: ${{ steps.release.outputs.release_created }} 41 | env: 42 | NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} 43 | -------------------------------------------------------------------------------- /.github/workflows/workflow.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | on: 3 | # Ensure GitHub actions are not run twice for same commits 4 | push: 5 | branches: [main] 6 | tags: ['*'] 7 | pull_request: 8 | types: [opened, synchronize, reopened] 9 | jobs: 10 | lint: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Git checkout 14 | uses: actions/checkout@v4 15 | - name: Setup Node.js 16 | uses: actions/setup-node@v4 17 | with: 18 | node-version: 'lts/*' 19 | cache: 'npm' 20 | - name: Install dependencies 21 | run: npm ci 22 | - name: Linting 23 | run: npm run format:ci 24 | build: 25 | runs-on: ${{ matrix.os }} 26 | timeout-minutes: 30 27 | strategy: 28 | matrix: 29 | os: [ubuntu-latest] 30 | node-version: ['14.18.0', '*'] 31 | # Must include the minimum deno version from the `DENO_VERSION_RANGE` constant in `node/bridge.ts`. 32 | deno-version: ['v1.37.0', 'v1.x'] 33 | include: 34 | - os: macOS-latest 35 | node-version: '*' 36 | deno-version: 'v1.x' 37 | - os: windows-latest 38 | node-version: '*' 39 | deno-version: 'v1.x' 40 | fail-fast: false 41 | steps: 42 | - name: Git checkout 43 | uses: actions/checkout@v4 44 | - name: Setup Deno 45 | uses: denoland/setup-deno@v1 46 | with: 47 | deno-version: ${{ matrix.deno-version }} 48 | - name: Setup Deno dependencies 49 | run: deno cache https://deno.land/x/eszip@v0.55.2/eszip.ts 50 | - name: Node.js ${{ matrix.node-version }} 51 | uses: actions/setup-node@v4 52 | with: 53 | node-version: ${{ matrix.node-version }} 54 | cache: 'npm' 55 | check-latest: true 56 | - name: Install dependencies 57 | run: npm ci 58 | - name: Tests 59 | run: npm run test:ci 60 | - name: Get test coverage flags 61 | id: test-coverage-flags 62 | run: |- 63 | os=${{ matrix.os }} 64 | node=$(node --version) 65 | echo "os=${os/-latest/}" >> $GITHUB_OUTPUT 66 | echo "node=node_${node//[.*]/}" >> $GITHUB_OUTPUT 67 | shell: bash 68 | - uses: codecov/codecov-action@v4 69 | with: 70 | file: coverage/coverage-final.json 71 | flags: ${{ steps.test-coverage-flags.outputs.os }},${{ steps.test-coverage-flags.outputs.node }} 72 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | *.swp 3 | npm-debug.log 4 | node_modules 5 | !test/fixtures/**/node_modules 6 | **/.netlify/edge-functions-serve 7 | /core 8 | .eslintcache 9 | .npmrc 10 | .yarn-error.log 11 | .nyc_output 12 | /coverage 13 | /build 14 | .vscode 15 | /dist 16 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | "@netlify/eslint-config-node/.prettierrc.json" 2 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making 6 | participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, 7 | disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, 8 | religion, or sexual identity and orientation. 9 | 10 | ## Our Standards 11 | 12 | Examples of behavior that contributes to creating a positive environment include: 13 | 14 | - Using welcoming and inclusive language 15 | - Being respectful of differing viewpoints and experiences 16 | - Gracefully accepting constructive criticism 17 | - Focusing on what is best for the community 18 | - Showing empathy towards other community members 19 | 20 | Examples of unacceptable behavior by participants include: 21 | 22 | - The use of sexualized language or imagery and unwelcome sexual attention or advances 23 | - Trolling, insulting/derogatory comments, and personal or political attacks 24 | - Public or private harassment 25 | - Publishing others' private information, such as a physical or electronic address, without explicit permission 26 | - Other conduct which could reasonably be considered inappropriate in a professional setting 27 | 28 | ## Our Responsibilities 29 | 30 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take 31 | appropriate and fair corrective action in response to any instances of unacceptable behavior. 32 | 33 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, 34 | issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any 35 | contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 36 | 37 | ## Scope 38 | 39 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the 40 | project or its community. Examples of representing a project or community include using an official project e-mail 41 | address, posting via an official social media account, or acting as an appointed representative at an online or offline 42 | event. Representation of a project may be further defined and clarified by project maintainers. 43 | 44 | ## Enforcement 45 | 46 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at 47 | david@netlify.com. All complaints will be reviewed and investigated and will result in a response that is deemed 48 | necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to 49 | the reporter of an incident. Further details of specific enforcement policies may be posted separately. 50 | 51 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent 52 | repercussions as determined by other members of the project's leadership. 53 | 54 | ## Attribution 55 | 56 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at 57 | [http://contributor-covenant.org/version/1/4][version] 58 | 59 | [homepage]: http://contributor-covenant.org 60 | [version]: http://contributor-covenant.org/version/1/4/ 61 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributions 2 | 3 | 🎉 Thanks for considering contributing to this project! 🎉 4 | 5 | These guidelines will help you send a pull request. 6 | 7 | Please note that this project is not intended to be used outside my own projects so new features are unlikely to be 8 | accepted. 9 | 10 | If you're submitting an issue instead, please skip this document. 11 | 12 | If your pull request is related to a typo or the documentation being unclear, please click on the relevant page's `Edit` 13 | button (pencil icon) and directly suggest a correction instead. 14 | 15 | This project was made with ❤️. The simplest way to give back is by starring and sharing it online. 16 | 17 | Everyone is welcome regardless of personal background. We enforce a [Code of conduct](CODE_OF_CONDUCT.md) in order to 18 | promote a positive and inclusive environment. 19 | 20 | # Development process 21 | 22 | First fork and clone the repository. If you're not sure how to do this, please watch 23 | [these videos](https://egghead.io/courses/how-to-contribute-to-an-open-source-project-on-github). 24 | 25 | Run: 26 | 27 | ```bash 28 | npm install 29 | ``` 30 | 31 | Make sure everything is correctly setup with: 32 | 33 | ```bash 34 | npm test 35 | ``` 36 | 37 | After submitting the pull request, please make sure the Continuous Integration checks are passing. 38 | 39 | ## Releasing 40 | 41 | 1. Merge the release PR 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Edge Bundler 2 | 3 | > [!IMPORTANT] 4 | > This project was moved into the [Netlify Build monorepo](https://github.com/netlify/build/tree/main/packages/edge-bundler). 5 | -------------------------------------------------------------------------------- /commitlint.config.cjs: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = { extends: ['@commitlint/config-conventional'] } 4 | -------------------------------------------------------------------------------- /deno/bundle.ts: -------------------------------------------------------------------------------- 1 | import { writeStage2 } from './lib/stage2.ts' 2 | 3 | const [payload] = Deno.args 4 | const { basePath, destPath, externals, functions, importMapData, vendorDirectory } = JSON.parse(payload) 5 | 6 | try { 7 | await writeStage2({ basePath, destPath, externals, functions, importMapData, vendorDirectory }) 8 | } catch (error) { 9 | if (error instanceof Error && error.message.includes("The module's source code could not be parsed")) { 10 | delete error.stack 11 | } 12 | 13 | throw error 14 | } 15 | -------------------------------------------------------------------------------- /deno/config.ts: -------------------------------------------------------------------------------- 1 | // this needs to be updated whenever there's a change to globalThis.Netlify in bootstrap 2 | import { Netlify } from "https://64e8753eae24930008fac6d9--edge.netlify.app/bootstrap/index-combined.ts" 3 | 4 | const [functionURL, collectorURL, rawExitCodes] = Deno.args 5 | const exitCodes = JSON.parse(rawExitCodes) 6 | 7 | globalThis.Netlify = Netlify 8 | 9 | let func 10 | 11 | try { 12 | func = await import(functionURL) 13 | } catch (error) { 14 | console.error(error) 15 | 16 | Deno.exit(exitCodes.ImportError) 17 | } 18 | 19 | if (typeof func.default !== 'function') { 20 | Deno.exit(exitCodes.InvalidDefaultExport) 21 | } 22 | 23 | if (func.config === undefined) { 24 | Deno.exit(exitCodes.NoConfig) 25 | } 26 | 27 | if (typeof func.config !== 'object') { 28 | Deno.exit(exitCodes.InvalidExport) 29 | } 30 | 31 | try { 32 | const result = JSON.stringify(func.config) 33 | 34 | await Deno.writeTextFile(new URL(collectorURL), result) 35 | } catch (error) { 36 | console.error(error) 37 | 38 | Deno.exit(exitCodes.SerializationError) 39 | } 40 | 41 | Deno.exit(exitCodes.Success) 42 | -------------------------------------------------------------------------------- /deno/lib/common.ts: -------------------------------------------------------------------------------- 1 | import { load } from "https://deno.land/x/eszip@v0.55.2/loader.ts"; 2 | import { LoadResponse } from "https://deno.land/x/eszip@v0.55.2/mod.ts"; 3 | import * as path from "https://deno.land/std@0.177.0/path/mod.ts"; 4 | import { retryAsync } from "https://deno.land/x/retry@v2.0.0/mod.ts"; 5 | import { isTooManyTries } from "https://deno.land/x/retry@v2.0.0/retry/tooManyTries.ts"; 6 | 7 | const inlineModule = (specifier: string, content: string): LoadResponse => { 8 | return { 9 | content, 10 | headers: { 11 | "content-type": "application/typescript", 12 | }, 13 | kind: "module", 14 | specifier, 15 | }; 16 | }; 17 | 18 | const loadFromVirtualRoot = async ( 19 | specifier: string, 20 | virtualRoot: string, 21 | basePath: string, 22 | ) => { 23 | const basePathURL = path.toFileUrl(basePath).toString(); 24 | const filePath = specifier.replace(virtualRoot.slice(0, -1), basePathURL); 25 | const file = await load(filePath); 26 | 27 | if (file === undefined) { 28 | throw new Error(`Could not find file: ${filePath}`); 29 | } 30 | 31 | return { ...file, specifier }; 32 | }; 33 | 34 | const loadWithRetry = (specifier: string, delay = 1000, maxTry = 3) => { 35 | if (!specifier.startsWith("https://")) { 36 | return load(specifier); 37 | } 38 | 39 | try { 40 | return retryAsync(() => load(specifier), { 41 | delay, 42 | maxTry, 43 | }); 44 | } catch (error) { 45 | if (isTooManyTries(error)) { 46 | console.error(`Loading ${specifier} failed after ${maxTry} tries.`); 47 | } 48 | throw error; 49 | } 50 | }; 51 | 52 | export { inlineModule, loadFromVirtualRoot, loadWithRetry }; 53 | -------------------------------------------------------------------------------- /deno/lib/consts.ts: -------------------------------------------------------------------------------- 1 | export const LEGACY_PUBLIC_SPECIFIER = 'netlify:edge' 2 | export const PUBLIC_SPECIFIER = '@netlify/edge-functions' 3 | export const STAGE1_SPECIFIER = 'netlify:bootstrap-stage1' 4 | export const STAGE2_SPECIFIER = 'netlify:bootstrap-stage2' 5 | export const virtualRoot = 'file:///root/' 6 | -------------------------------------------------------------------------------- /deno/lib/stage2.test.ts: -------------------------------------------------------------------------------- 1 | import { assertEquals, assertStringIncludes } from 'https://deno.land/std@0.177.0/testing/asserts.ts' 2 | 3 | import { join } from 'https://deno.land/std@0.177.0/path/mod.ts' 4 | import { pathToFileURL } from 'https://deno.land/std@0.177.0/node/url.ts' 5 | 6 | import { getStage2Entry } from './stage2.ts' 7 | import { virtualRoot } from './consts.ts' 8 | 9 | Deno.test('`getStage2Entry` returns a valid stage 2 file', async () => { 10 | const directory = await Deno.makeTempDir() 11 | const functions = [ 12 | { 13 | name: 'func1', 14 | path: join(directory, 'func1.ts'), 15 | response: 'Hello from function 1', 16 | }, 17 | { 18 | name: 'func2', 19 | path: join(directory, 'func2.ts'), 20 | response: 'Hello from function 2', 21 | }, 22 | ] 23 | 24 | for (const func of functions) { 25 | const contents = `export default async () => new Response(${JSON.stringify(func.response)})` 26 | 27 | await Deno.writeTextFile(func.path, contents) 28 | } 29 | 30 | const baseURL = pathToFileURL(directory) 31 | const stage2 = getStage2Entry( 32 | directory, 33 | functions.map(({ name, path }) => ({ name, path })), 34 | ) 35 | 36 | // Ensuring that the stage 2 paths have the virtual root before we strip it. 37 | assertStringIncludes(stage2, virtualRoot) 38 | 39 | // Replacing the virtual root with the URL of the temporary directory so that 40 | // we can actually import the module. 41 | const normalizedStage2 = stage2.replaceAll(virtualRoot, `${baseURL.href}/`) 42 | 43 | const stage2Path = join(directory, 'stage2.ts') 44 | const stage2URL = pathToFileURL(stage2Path) 45 | 46 | await Deno.writeTextFile(stage2Path, normalizedStage2) 47 | 48 | const mod = await import(stage2URL.href) 49 | 50 | await Deno.remove(directory, { recursive: true }) 51 | 52 | for (const func of functions) { 53 | const result = await mod.functions[func.name]() 54 | 55 | assertEquals(await result.text(), func.response) 56 | assertEquals(mod.metadata.functions[func.name].url, pathToFileURL(func.path).toString()) 57 | } 58 | }) 59 | -------------------------------------------------------------------------------- /deno/lib/stage2.ts: -------------------------------------------------------------------------------- 1 | import { build, LoadResponse } from 'https://deno.land/x/eszip@v0.55.2/mod.ts' 2 | 3 | import * as path from 'https://deno.land/std@0.177.0/path/mod.ts' 4 | 5 | import type { InputFunction, WriteStage2Options } from '../../shared/stage2.ts' 6 | import { importMapSpecifier, virtualRoot, virtualVendorRoot } from '../../shared/consts.ts' 7 | import { LEGACY_PUBLIC_SPECIFIER, PUBLIC_SPECIFIER, STAGE2_SPECIFIER } from './consts.ts' 8 | import { inlineModule, loadFromVirtualRoot, loadWithRetry } from './common.ts' 9 | 10 | interface FunctionReference { 11 | exportLine: string 12 | importLine: string 13 | metadata: { 14 | url: URL 15 | } 16 | name: string 17 | } 18 | 19 | const getMetadata = (references: FunctionReference[]) => { 20 | const functions = references.reduce( 21 | (acc, { metadata, name }) => ({ 22 | ...acc, 23 | [name]: metadata, 24 | }), 25 | {}, 26 | ) 27 | 28 | return { 29 | functions, 30 | } 31 | } 32 | 33 | const getFunctionReference = (basePath: string, func: InputFunction, index: number): FunctionReference => { 34 | const importName = `func${index}` 35 | const exportLine = `"${func.name}": ${importName}` 36 | const url = getVirtualPath(basePath, func.path) 37 | 38 | return { 39 | exportLine, 40 | importLine: `import ${importName} from "${url}";`, 41 | metadata: { 42 | url, 43 | }, 44 | name: func.name, 45 | } 46 | } 47 | 48 | export const getStage2Entry = (basePath: string, functions: InputFunction[]) => { 49 | const lines = functions.map((func, index) => getFunctionReference(basePath, func, index)) 50 | const importLines = lines.map(({ importLine }) => importLine).join('\n') 51 | const exportLines = lines.map(({ exportLine }) => exportLine).join(', ') 52 | const metadata = getMetadata(lines) 53 | const functionsExport = `export const functions = {${exportLines}};` 54 | const metadataExport = `export const metadata = ${JSON.stringify(metadata)};` 55 | 56 | return [importLines, functionsExport, metadataExport].join('\n\n') 57 | } 58 | 59 | const getVirtualPath = (basePath: string, filePath: string) => { 60 | const relativePath = path.relative(basePath, filePath) 61 | const url = new URL(relativePath, virtualRoot) 62 | 63 | return url 64 | } 65 | 66 | const stage2Loader = ( 67 | basePath: string, 68 | functions: InputFunction[], 69 | externals: Set, 70 | importMapData: string | undefined, 71 | vendorDirectory?: string, 72 | ) => { 73 | return async (specifier: string): Promise => { 74 | if (specifier === STAGE2_SPECIFIER) { 75 | const stage2Entry = getStage2Entry(basePath, functions) 76 | 77 | return inlineModule(specifier, stage2Entry) 78 | } 79 | 80 | if (specifier === importMapSpecifier && importMapData !== undefined) { 81 | return inlineModule(specifier, importMapData) 82 | } 83 | 84 | if ( 85 | specifier === LEGACY_PUBLIC_SPECIFIER || 86 | specifier === PUBLIC_SPECIFIER || 87 | externals.has(specifier) || 88 | specifier.startsWith('node:') 89 | ) { 90 | return { 91 | kind: 'external', 92 | specifier, 93 | } 94 | } 95 | 96 | if (specifier.startsWith(virtualRoot)) { 97 | return loadFromVirtualRoot(specifier, virtualRoot, basePath) 98 | } 99 | 100 | if (vendorDirectory !== undefined && specifier.startsWith(virtualVendorRoot)) { 101 | return loadFromVirtualRoot(specifier, virtualVendorRoot, vendorDirectory) 102 | } 103 | 104 | return await loadWithRetry(specifier) 105 | } 106 | } 107 | 108 | const writeStage2 = async ({ 109 | basePath, 110 | destPath, 111 | externals, 112 | functions, 113 | importMapData, 114 | vendorDirectory, 115 | }: WriteStage2Options) => { 116 | const importMapURL = importMapData ? importMapSpecifier : undefined 117 | const loader = stage2Loader(basePath, functions, new Set(externals), importMapData, vendorDirectory) 118 | const bytes = await build([STAGE2_SPECIFIER], loader, importMapURL) 119 | const directory = path.dirname(destPath) 120 | 121 | await Deno.mkdir(directory, { recursive: true }) 122 | 123 | return await Deno.writeFile(destPath, bytes) 124 | } 125 | 126 | export { writeStage2 } 127 | -------------------------------------------------------------------------------- /deno/vendor/deno.land/std@0.177.0/_util/asserts.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. 2 | // This module is browser compatible. 3 | 4 | /** 5 | * All internal non-test code, that is files that do not have `test` or `bench` in the name, must use the assertion functions within `_utils/asserts.ts` and not `testing/asserts.ts`. This is to create a separation of concerns between internal and testing assertions. 6 | */ 7 | 8 | export class DenoStdInternalError extends Error { 9 | constructor(message: string) { 10 | super(message); 11 | this.name = "DenoStdInternalError"; 12 | } 13 | } 14 | 15 | /** Make an assertion, if not `true`, then throw. */ 16 | export function assert(expr: unknown, msg = ""): asserts expr { 17 | if (!expr) { 18 | throw new DenoStdInternalError(msg); 19 | } 20 | } 21 | 22 | /** Use this to assert unreachable code. */ 23 | export function unreachable(): never { 24 | throw new DenoStdInternalError("unreachable"); 25 | } 26 | -------------------------------------------------------------------------------- /deno/vendor/deno.land/std@0.177.0/_util/os.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. 2 | // This module is browser compatible. 3 | 4 | export type OSType = "windows" | "linux" | "darwin" | "freebsd"; 5 | 6 | export const osType: OSType = (() => { 7 | // deno-lint-ignore no-explicit-any 8 | const { Deno } = globalThis as any; 9 | if (typeof Deno?.build?.os === "string") { 10 | return Deno.build.os; 11 | } 12 | 13 | // deno-lint-ignore no-explicit-any 14 | const { navigator } = globalThis as any; 15 | if (navigator?.appVersion?.includes?.("Win")) { 16 | return "windows"; 17 | } 18 | 19 | return "linux"; 20 | })(); 21 | 22 | export const isWindows = osType === "windows"; 23 | export const isLinux = osType === "linux"; 24 | -------------------------------------------------------------------------------- /deno/vendor/deno.land/std@0.177.0/path/_constants.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. 2 | // Copyright the Browserify authors. MIT License. 3 | // Ported from https://github.com/browserify/path-browserify/ 4 | // This module is browser compatible. 5 | 6 | // Alphabet chars. 7 | export const CHAR_UPPERCASE_A = 65; /* A */ 8 | export const CHAR_LOWERCASE_A = 97; /* a */ 9 | export const CHAR_UPPERCASE_Z = 90; /* Z */ 10 | export const CHAR_LOWERCASE_Z = 122; /* z */ 11 | 12 | // Non-alphabetic chars. 13 | export const CHAR_DOT = 46; /* . */ 14 | export const CHAR_FORWARD_SLASH = 47; /* / */ 15 | export const CHAR_BACKWARD_SLASH = 92; /* \ */ 16 | export const CHAR_VERTICAL_LINE = 124; /* | */ 17 | export const CHAR_COLON = 58; /* : */ 18 | export const CHAR_QUESTION_MARK = 63; /* ? */ 19 | export const CHAR_UNDERSCORE = 95; /* _ */ 20 | export const CHAR_LINE_FEED = 10; /* \n */ 21 | export const CHAR_CARRIAGE_RETURN = 13; /* \r */ 22 | export const CHAR_TAB = 9; /* \t */ 23 | export const CHAR_FORM_FEED = 12; /* \f */ 24 | export const CHAR_EXCLAMATION_MARK = 33; /* ! */ 25 | export const CHAR_HASH = 35; /* # */ 26 | export const CHAR_SPACE = 32; /* */ 27 | export const CHAR_NO_BREAK_SPACE = 160; /* \u00A0 */ 28 | export const CHAR_ZERO_WIDTH_NOBREAK_SPACE = 65279; /* \uFEFF */ 29 | export const CHAR_LEFT_SQUARE_BRACKET = 91; /* [ */ 30 | export const CHAR_RIGHT_SQUARE_BRACKET = 93; /* ] */ 31 | export const CHAR_LEFT_ANGLE_BRACKET = 60; /* < */ 32 | export const CHAR_RIGHT_ANGLE_BRACKET = 62; /* > */ 33 | export const CHAR_LEFT_CURLY_BRACKET = 123; /* { */ 34 | export const CHAR_RIGHT_CURLY_BRACKET = 125; /* } */ 35 | export const CHAR_HYPHEN_MINUS = 45; /* - */ 36 | export const CHAR_PLUS = 43; /* + */ 37 | export const CHAR_DOUBLE_QUOTE = 34; /* " */ 38 | export const CHAR_SINGLE_QUOTE = 39; /* ' */ 39 | export const CHAR_PERCENT = 37; /* % */ 40 | export const CHAR_SEMICOLON = 59; /* ; */ 41 | export const CHAR_CIRCUMFLEX_ACCENT = 94; /* ^ */ 42 | export const CHAR_GRAVE_ACCENT = 96; /* ` */ 43 | export const CHAR_AT = 64; /* @ */ 44 | export const CHAR_AMPERSAND = 38; /* & */ 45 | export const CHAR_EQUAL = 61; /* = */ 46 | 47 | // Digits 48 | export const CHAR_0 = 48; /* 0 */ 49 | export const CHAR_9 = 57; /* 9 */ 50 | -------------------------------------------------------------------------------- /deno/vendor/deno.land/std@0.177.0/path/_interface.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. 2 | // This module is browser compatible. 3 | 4 | /** 5 | * A parsed path object generated by path.parse() or consumed by path.format(). 6 | */ 7 | export interface ParsedPath { 8 | /** 9 | * The root of the path such as '/' or 'c:\' 10 | */ 11 | root: string; 12 | /** 13 | * The full directory path such as '/home/user/dir' or 'c:\path\dir' 14 | */ 15 | dir: string; 16 | /** 17 | * The file name including extension (if any) such as 'index.html' 18 | */ 19 | base: string; 20 | /** 21 | * The file extension (if any) such as '.html' 22 | */ 23 | ext: string; 24 | /** 25 | * The file name without extension (if any) such as 'index' 26 | */ 27 | name: string; 28 | } 29 | 30 | export type FormatInputPathObject = Partial; 31 | -------------------------------------------------------------------------------- /deno/vendor/deno.land/std@0.177.0/path/_util.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. 2 | // Copyright the Browserify authors. MIT License. 3 | // Ported from https://github.com/browserify/path-browserify/ 4 | // This module is browser compatible. 5 | 6 | import type { FormatInputPathObject } from "./_interface.ts"; 7 | import { 8 | CHAR_BACKWARD_SLASH, 9 | CHAR_DOT, 10 | CHAR_FORWARD_SLASH, 11 | CHAR_LOWERCASE_A, 12 | CHAR_LOWERCASE_Z, 13 | CHAR_UPPERCASE_A, 14 | CHAR_UPPERCASE_Z, 15 | } from "./_constants.ts"; 16 | 17 | export function assertPath(path: string) { 18 | if (typeof path !== "string") { 19 | throw new TypeError( 20 | `Path must be a string. Received ${JSON.stringify(path)}`, 21 | ); 22 | } 23 | } 24 | 25 | export function isPosixPathSeparator(code: number): boolean { 26 | return code === CHAR_FORWARD_SLASH; 27 | } 28 | 29 | export function isPathSeparator(code: number): boolean { 30 | return isPosixPathSeparator(code) || code === CHAR_BACKWARD_SLASH; 31 | } 32 | 33 | export function isWindowsDeviceRoot(code: number): boolean { 34 | return ( 35 | (code >= CHAR_LOWERCASE_A && code <= CHAR_LOWERCASE_Z) || 36 | (code >= CHAR_UPPERCASE_A && code <= CHAR_UPPERCASE_Z) 37 | ); 38 | } 39 | 40 | // Resolves . and .. elements in a path with directory names 41 | export function normalizeString( 42 | path: string, 43 | allowAboveRoot: boolean, 44 | separator: string, 45 | isPathSeparator: (code: number) => boolean, 46 | ): string { 47 | let res = ""; 48 | let lastSegmentLength = 0; 49 | let lastSlash = -1; 50 | let dots = 0; 51 | let code: number | undefined; 52 | for (let i = 0, len = path.length; i <= len; ++i) { 53 | if (i < len) code = path.charCodeAt(i); 54 | else if (isPathSeparator(code!)) break; 55 | else code = CHAR_FORWARD_SLASH; 56 | 57 | if (isPathSeparator(code!)) { 58 | if (lastSlash === i - 1 || dots === 1) { 59 | // NOOP 60 | } else if (lastSlash !== i - 1 && dots === 2) { 61 | if ( 62 | res.length < 2 || 63 | lastSegmentLength !== 2 || 64 | res.charCodeAt(res.length - 1) !== CHAR_DOT || 65 | res.charCodeAt(res.length - 2) !== CHAR_DOT 66 | ) { 67 | if (res.length > 2) { 68 | const lastSlashIndex = res.lastIndexOf(separator); 69 | if (lastSlashIndex === -1) { 70 | res = ""; 71 | lastSegmentLength = 0; 72 | } else { 73 | res = res.slice(0, lastSlashIndex); 74 | lastSegmentLength = res.length - 1 - res.lastIndexOf(separator); 75 | } 76 | lastSlash = i; 77 | dots = 0; 78 | continue; 79 | } else if (res.length === 2 || res.length === 1) { 80 | res = ""; 81 | lastSegmentLength = 0; 82 | lastSlash = i; 83 | dots = 0; 84 | continue; 85 | } 86 | } 87 | if (allowAboveRoot) { 88 | if (res.length > 0) res += `${separator}..`; 89 | else res = ".."; 90 | lastSegmentLength = 2; 91 | } 92 | } else { 93 | if (res.length > 0) res += separator + path.slice(lastSlash + 1, i); 94 | else res = path.slice(lastSlash + 1, i); 95 | lastSegmentLength = i - lastSlash - 1; 96 | } 97 | lastSlash = i; 98 | dots = 0; 99 | } else if (code === CHAR_DOT && dots !== -1) { 100 | ++dots; 101 | } else { 102 | dots = -1; 103 | } 104 | } 105 | return res; 106 | } 107 | 108 | export function _format( 109 | sep: string, 110 | pathObject: FormatInputPathObject, 111 | ): string { 112 | const dir: string | undefined = pathObject.dir || pathObject.root; 113 | const base: string = pathObject.base || 114 | (pathObject.name || "") + (pathObject.ext || ""); 115 | if (!dir) return base; 116 | if (base === sep) return dir; 117 | if (dir === pathObject.root) return dir + base; 118 | return dir + sep + base; 119 | } 120 | 121 | const WHITESPACE_ENCODINGS: Record = { 122 | "\u0009": "%09", 123 | "\u000A": "%0A", 124 | "\u000B": "%0B", 125 | "\u000C": "%0C", 126 | "\u000D": "%0D", 127 | "\u0020": "%20", 128 | }; 129 | 130 | export function encodeWhitespace(string: string): string { 131 | return string.replaceAll(/[\s]/g, (c) => { 132 | return WHITESPACE_ENCODINGS[c] ?? c; 133 | }); 134 | } 135 | 136 | export function lastPathSegment( 137 | path: string, 138 | isSep: (char: number) => boolean, 139 | start = 0, 140 | ): string { 141 | let matchedNonSeparator = false; 142 | let end = path.length; 143 | 144 | for (let i = path.length - 1; i >= start; --i) { 145 | if (isSep(path.charCodeAt(i))) { 146 | if (matchedNonSeparator) { 147 | start = i + 1; 148 | break; 149 | } 150 | } else if (!matchedNonSeparator) { 151 | matchedNonSeparator = true; 152 | end = i + 1; 153 | } 154 | } 155 | 156 | return path.slice(start, end); 157 | } 158 | 159 | export function stripTrailingSeparators( 160 | segment: string, 161 | isSep: (char: number) => boolean, 162 | ): string { 163 | if (segment.length <= 1) { 164 | return segment; 165 | } 166 | 167 | let end = segment.length; 168 | 169 | for (let i = segment.length - 1; i > 0; i--) { 170 | if (isSep(segment.charCodeAt(i))) { 171 | end = i; 172 | } else { 173 | break; 174 | } 175 | } 176 | 177 | return segment.slice(0, end); 178 | } 179 | 180 | export function stripSuffix(name: string, suffix: string): string { 181 | if (suffix.length >= name.length) { 182 | return name; 183 | } 184 | 185 | const lenDiff = name.length - suffix.length; 186 | 187 | for (let i = suffix.length - 1; i >= 0; --i) { 188 | if (name.charCodeAt(lenDiff + i) !== suffix.charCodeAt(i)) { 189 | return name; 190 | } 191 | } 192 | 193 | return name.slice(0, -suffix.length); 194 | } 195 | -------------------------------------------------------------------------------- /deno/vendor/deno.land/std@0.177.0/path/common.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. 2 | // This module is browser compatible. 3 | 4 | import { SEP } from "./separator.ts"; 5 | 6 | /** Determines the common path from a set of paths, using an optional separator, 7 | * which defaults to the OS default separator. 8 | * 9 | * ```ts 10 | * import { common } from "https://deno.land/std@$STD_VERSION/path/mod.ts"; 11 | * const p = common([ 12 | * "./deno/std/path/mod.ts", 13 | * "./deno/std/fs/mod.ts", 14 | * ]); 15 | * console.log(p); // "./deno/std/" 16 | * ``` 17 | */ 18 | export function common(paths: string[], sep = SEP): string { 19 | const [first = "", ...remaining] = paths; 20 | if (first === "" || remaining.length === 0) { 21 | return first.substring(0, first.lastIndexOf(sep) + 1); 22 | } 23 | const parts = first.split(sep); 24 | 25 | let endOfPrefix = parts.length; 26 | for (const path of remaining) { 27 | const compare = path.split(sep); 28 | for (let i = 0; i < endOfPrefix; i++) { 29 | if (compare[i] !== parts[i]) { 30 | endOfPrefix = i; 31 | } 32 | } 33 | 34 | if (endOfPrefix === 0) { 35 | return ""; 36 | } 37 | } 38 | const prefix = parts.slice(0, endOfPrefix).join(sep); 39 | return prefix.endsWith(sep) ? prefix : `${prefix}${sep}`; 40 | } 41 | -------------------------------------------------------------------------------- /deno/vendor/deno.land/std@0.177.0/path/mod.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. 2 | // Copyright the Browserify authors. MIT License. 3 | // Ported mostly from https://github.com/browserify/path-browserify/ 4 | 5 | /** 6 | * Utilities for working with OS-specific file paths. 7 | * 8 | * Codes in the examples uses POSIX path but it automatically use Windows path 9 | * on Windows. Use methods under `posix` or `win32` object instead to handle non 10 | * platform specific path like: 11 | * ```ts 12 | * import { posix, win32 } from "https://deno.land/std@$STD_VERSION/path/mod.ts"; 13 | * const p1 = posix.fromFileUrl("file:///home/foo"); 14 | * const p2 = win32.fromFileUrl("file:///home/foo"); 15 | * console.log(p1); // "/home/foo" 16 | * console.log(p2); // "\\home\\foo" 17 | * ``` 18 | * 19 | * This module is browser compatible. 20 | * 21 | * @module 22 | */ 23 | 24 | import { isWindows } from "../_util/os.ts"; 25 | import * as _win32 from "./win32.ts"; 26 | import * as _posix from "./posix.ts"; 27 | 28 | const path = isWindows ? _win32 : _posix; 29 | 30 | export const win32 = _win32; 31 | export const posix = _posix; 32 | export const { 33 | basename, 34 | delimiter, 35 | dirname, 36 | extname, 37 | format, 38 | fromFileUrl, 39 | isAbsolute, 40 | join, 41 | normalize, 42 | parse, 43 | relative, 44 | resolve, 45 | sep, 46 | toFileUrl, 47 | toNamespacedPath, 48 | } = path; 49 | 50 | export * from "./common.ts"; 51 | export { SEP, SEP_PATTERN } from "./separator.ts"; 52 | export * from "./_interface.ts"; 53 | export * from "./glob.ts"; 54 | -------------------------------------------------------------------------------- /deno/vendor/deno.land/std@0.177.0/path/separator.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. 2 | // This module is browser compatible. 3 | 4 | import { isWindows } from "../_util/os.ts"; 5 | 6 | export const SEP = isWindows ? "\\" : "/"; 7 | export const SEP_PATTERN = isWindows ? /[\\/]+/ : /\/+/; 8 | -------------------------------------------------------------------------------- /deno/vendor/deno.land/std@0.98.0/async/deferred.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. 2 | // TODO(ry) It'd be better to make Deferred a class that inherits from 3 | // Promise, rather than an interface. This is possible in ES2016, however 4 | // typescript produces broken code when targeting ES5 code. 5 | // See https://github.com/Microsoft/TypeScript/issues/15202 6 | // At the time of writing, the github issue is closed but the problem remains. 7 | export interface Deferred extends Promise { 8 | resolve(value?: T | PromiseLike): void; 9 | // deno-lint-ignore no-explicit-any 10 | reject(reason?: any): void; 11 | } 12 | 13 | /** Creates a Promise with the `reject` and `resolve` functions 14 | * placed as methods on the promise object itself. It allows you to do: 15 | * 16 | * const p = deferred(); 17 | * // ... 18 | * p.resolve(42); 19 | */ 20 | export function deferred(): Deferred { 21 | let methods; 22 | const promise = new Promise((resolve, reject): void => { 23 | methods = { resolve, reject }; 24 | }); 25 | return Object.assign(promise, methods) as Deferred; 26 | } 27 | -------------------------------------------------------------------------------- /deno/vendor/deno.land/std@0.98.0/async/delay.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. 2 | /* Resolves after the given number of milliseconds. */ 3 | export function delay(ms: number): Promise { 4 | return new Promise((res): number => 5 | setTimeout((): void => { 6 | res(); 7 | }, ms) 8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /deno/vendor/deno.land/std@0.98.0/async/mod.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. 2 | export * from "./deferred.ts"; 3 | export * from "./delay.ts"; 4 | export * from "./mux_async_iterator.ts"; 5 | export * from "./pool.ts"; 6 | export * from "./tee.ts"; 7 | -------------------------------------------------------------------------------- /deno/vendor/deno.land/std@0.98.0/async/mux_async_iterator.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. 2 | import { Deferred, deferred } from "./deferred.ts"; 3 | 4 | interface TaggedYieldedValue { 5 | iterator: AsyncIterator; 6 | value: T; 7 | } 8 | 9 | /** The MuxAsyncIterator class multiplexes multiple async iterators into a 10 | * single stream. It currently makes an assumption: 11 | * - The final result (the value returned and not yielded from the iterator) 12 | * does not matter; if there is any, it is discarded. 13 | */ 14 | export class MuxAsyncIterator implements AsyncIterable { 15 | private iteratorCount = 0; 16 | private yields: Array> = []; 17 | // deno-lint-ignore no-explicit-any 18 | private throws: any[] = []; 19 | private signal: Deferred = deferred(); 20 | 21 | add(iterable: AsyncIterable): void { 22 | ++this.iteratorCount; 23 | this.callIteratorNext(iterable[Symbol.asyncIterator]()); 24 | } 25 | 26 | private async callIteratorNext( 27 | iterator: AsyncIterator, 28 | ) { 29 | try { 30 | const { value, done } = await iterator.next(); 31 | if (done) { 32 | --this.iteratorCount; 33 | } else { 34 | this.yields.push({ iterator, value }); 35 | } 36 | } catch (e) { 37 | this.throws.push(e); 38 | } 39 | this.signal.resolve(); 40 | } 41 | 42 | async *iterate(): AsyncIterableIterator { 43 | while (this.iteratorCount > 0) { 44 | // Sleep until any of the wrapped iterators yields. 45 | await this.signal; 46 | 47 | // Note that while we're looping over `yields`, new items may be added. 48 | for (let i = 0; i < this.yields.length; i++) { 49 | const { iterator, value } = this.yields[i]; 50 | yield value; 51 | this.callIteratorNext(iterator); 52 | } 53 | 54 | if (this.throws.length) { 55 | for (const e of this.throws) { 56 | throw e; 57 | } 58 | this.throws.length = 0; 59 | } 60 | // Clear the `yields` list and reset the `signal` promise. 61 | this.yields.length = 0; 62 | this.signal = deferred(); 63 | } 64 | } 65 | 66 | [Symbol.asyncIterator](): AsyncIterator { 67 | return this.iterate(); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /deno/vendor/deno.land/std@0.98.0/async/pool.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. 2 | 3 | /** 4 | * pooledMap transforms values from an (async) iterable into another async 5 | * iterable. The transforms are done concurrently, with a max concurrency 6 | * defined by the poolLimit. 7 | * 8 | * If an error is thrown from `iterableFn`, no new transformations will begin. 9 | * All currently executing transformations are allowed to finish and still 10 | * yielded on success. After that, the rejections among them are gathered and 11 | * thrown by the iterator in an `AggregateError`. 12 | * 13 | * @param poolLimit The maximum count of items being processed concurrently. 14 | * @param array The input array for mapping. 15 | * @param iteratorFn The function to call for every item of the array. 16 | */ 17 | export function pooledMap( 18 | poolLimit: number, 19 | array: Iterable | AsyncIterable, 20 | iteratorFn: (data: T) => Promise, 21 | ): AsyncIterableIterator { 22 | // Create the async iterable that is returned from this function. 23 | const res = new TransformStream, R>({ 24 | async transform( 25 | p: Promise, 26 | controller: TransformStreamDefaultController, 27 | ) { 28 | controller.enqueue(await p); 29 | }, 30 | }); 31 | // Start processing items from the iterator 32 | (async () => { 33 | const writer = res.writable.getWriter(); 34 | const executing: Array> = []; 35 | try { 36 | for await (const item of array) { 37 | const p = Promise.resolve().then(() => iteratorFn(item)); 38 | // Only write on success. If we `writer.write()` a rejected promise, 39 | // that will end the iteration. We don't want that yet. Instead let it 40 | // fail the race, taking us to the catch block where all currently 41 | // executing jobs are allowed to finish and all rejections among them 42 | // can be reported together. 43 | p.then((v) => writer.write(Promise.resolve(v))).catch(() => {}); 44 | const e: Promise = p.then(() => 45 | executing.splice(executing.indexOf(e), 1) 46 | ); 47 | executing.push(e); 48 | if (executing.length >= poolLimit) { 49 | await Promise.race(executing); 50 | } 51 | } 52 | // Wait until all ongoing events have processed, then close the writer. 53 | await Promise.all(executing); 54 | writer.close(); 55 | } catch { 56 | const errors = []; 57 | for (const result of await Promise.allSettled(executing)) { 58 | if (result.status == "rejected") { 59 | errors.push(result.reason); 60 | } 61 | } 62 | writer.write(Promise.reject( 63 | new AggregateError(errors, "Threw while mapping."), 64 | )).catch(() => {}); 65 | } 66 | })(); 67 | return res.readable[Symbol.asyncIterator](); 68 | } 69 | -------------------------------------------------------------------------------- /deno/vendor/deno.land/std@0.98.0/async/tee.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. 2 | 3 | // Utility for representing n-tuple 4 | type Tuple = N extends N 5 | ? number extends N ? T[] : TupleOf 6 | : never; 7 | type TupleOf = R["length"] extends N 8 | ? R 9 | : TupleOf; 10 | 11 | const noop = () => {}; 12 | 13 | class AsyncIterableClone implements AsyncIterable { 14 | currentPromise: Promise>; 15 | resolveCurrent: (x: Promise>) => void = noop; 16 | consumed: Promise; 17 | consume: () => void = noop; 18 | 19 | constructor() { 20 | this.currentPromise = new Promise>((resolve) => { 21 | this.resolveCurrent = resolve; 22 | }); 23 | this.consumed = new Promise((resolve) => { 24 | this.consume = resolve; 25 | }); 26 | } 27 | 28 | reset() { 29 | this.currentPromise = new Promise>((resolve) => { 30 | this.resolveCurrent = resolve; 31 | }); 32 | this.consumed = new Promise((resolve) => { 33 | this.consume = resolve; 34 | }); 35 | } 36 | 37 | async next(): Promise> { 38 | const res = await this.currentPromise; 39 | this.consume(); 40 | this.reset(); 41 | return res; 42 | } 43 | 44 | async push(res: Promise>): Promise { 45 | this.resolveCurrent(res); 46 | // Wait until current promise is consumed and next item is requested. 47 | await this.consumed; 48 | } 49 | 50 | [Symbol.asyncIterator](): AsyncIterator { 51 | return this; 52 | } 53 | } 54 | 55 | /** 56 | * Branches the given async iterable into the n branches. 57 | * 58 | * Example: 59 | * 60 | * const gen = async function* gen() { 61 | * yield 1; 62 | * yield 2; 63 | * yield 3; 64 | * } 65 | * 66 | * const [branch1, branch2] = tee(gen()); 67 | * 68 | * (async () => { 69 | * for await (const n of branch1) { 70 | * console.log(n); // => 1, 2, 3 71 | * } 72 | * })(); 73 | * 74 | * (async () => { 75 | * for await (const n of branch2) { 76 | * console.log(n); // => 1, 2, 3 77 | * } 78 | * })(); 79 | */ 80 | export function tee( 81 | src: AsyncIterable, 82 | n: N = 2 as N, 83 | ): Tuple, N> { 84 | const clones: Tuple, N> = Array.from({ length: n }).map( 85 | () => new AsyncIterableClone(), 86 | // deno-lint-ignore no-explicit-any 87 | ) as any; 88 | (async () => { 89 | const iter = src[Symbol.asyncIterator](); 90 | await Promise.resolve(); 91 | while (true) { 92 | const res = iter.next(); 93 | await Promise.all(clones.map((c) => c.push(res))); 94 | if ((await res).done) { 95 | break; 96 | } 97 | } 98 | })().catch((e) => { 99 | console.error(e); 100 | }); 101 | return clones; 102 | } 103 | -------------------------------------------------------------------------------- /deno/vendor/deno.land/x/dir@1.5.1/data_local_dir/mod.ts: -------------------------------------------------------------------------------- 1 | /** Returns the path to the user's local data directory. 2 | * 3 | * The returned value depends on the operating system and is either a string, 4 | * containing a value from the following table, or `null`. 5 | * 6 | * | Platform | Value | Example | 7 | * | -------- | ---------------------------------------- | -------------------------------------------- | 8 | * | Linux | `$XDG_DATA_HOME` or `$HOME`/.local/share | /home/justjavac/.local/share | 9 | * | macOS | `$HOME`/Library/Application Support | /Users/justjavac/Library/Application Support | 10 | * | Windows | `$LOCALAPPDATA` | C:\Users\justjavac\AppData\Local | 11 | */ 12 | export default function dataDir(): string | null { 13 | switch (Deno.build.os) { 14 | case "linux": { 15 | const xdg = Deno.env.get("XDG_DATA_HOME"); 16 | if (xdg) return xdg; 17 | 18 | const home = Deno.env.get("HOME"); 19 | if (home) return `${home}/.local/share`; 20 | break; 21 | } 22 | 23 | case "darwin": { 24 | const home = Deno.env.get("HOME"); 25 | if (home) return `${home}/Library/Application Support`; 26 | break; 27 | } 28 | 29 | case "windows": 30 | return Deno.env.get("LOCALAPPDATA") ?? null; 31 | } 32 | 33 | return null; 34 | } 35 | -------------------------------------------------------------------------------- /deno/vendor/deno.land/x/eszip@v0.55.2/eszip_wasm_bg.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netlify/edge-bundler/3cdcd27c87555d01b176ffb1a8f852c27e71669e/deno/vendor/deno.land/x/eszip@v0.55.2/eszip_wasm_bg.wasm -------------------------------------------------------------------------------- /deno/vendor/deno.land/x/eszip@v0.55.2/loader.ts: -------------------------------------------------------------------------------- 1 | // Adapted from deno_graph 2 | // https://github.com/denoland/deno_graph/blob/main/lib/loader.ts 3 | 4 | export interface LoadResponseModule { 5 | /** A module with code has been loaded. */ 6 | kind: "module"; 7 | /** The string URL of the resource. If there were redirects, the final 8 | * specifier should be set here, otherwise the requested specifier. */ 9 | specifier: string; 10 | /** For remote resources, a record of headers should be set, where the key's 11 | * have been normalized to be lower case values. */ 12 | headers?: Record; 13 | /** The string value of the loaded resources. */ 14 | content: string; 15 | } 16 | 17 | export interface LoadResponseExternalBuiltIn { 18 | /** The loaded module is either _external_ or _built-in_ to the runtime. */ 19 | kind: "external" | "builtIn"; 20 | /** The string URL of the resource. If there were redirects, the final 21 | * specifier should be set here, otherwise the requested specifier. */ 22 | specifier: string; 23 | } 24 | 25 | export type LoadResponse = LoadResponseModule | LoadResponseExternalBuiltIn; 26 | 27 | export async function load( 28 | specifier: string, 29 | ): Promise { 30 | const url = new URL(specifier); 31 | try { 32 | switch (url.protocol) { 33 | case "file:": { 34 | const content = await Deno.readTextFile(url); 35 | return { 36 | kind: "module", 37 | specifier, 38 | content, 39 | }; 40 | } 41 | case "http:": 42 | case "https:": { 43 | const response = await fetch(String(url), { redirect: "follow" }); 44 | if (response.status !== 200) { 45 | // ensure the body is read as to not leak resources 46 | await response.arrayBuffer(); 47 | return undefined; 48 | } 49 | const content = await response.text(); 50 | const headers: Record = {}; 51 | for (const [key, value] of response.headers) { 52 | headers[key.toLowerCase()] = value; 53 | } 54 | return { 55 | kind: "module", 56 | specifier: response.url, 57 | headers, 58 | content, 59 | }; 60 | } 61 | default: 62 | return undefined; 63 | } 64 | } catch { 65 | return undefined; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /deno/vendor/deno.land/x/eszip@v0.55.2/mod.ts: -------------------------------------------------------------------------------- 1 | import { load, LoadResponse } from "./loader.ts"; 2 | import { 3 | instantiate, 4 | Parser as InternalParser, 5 | } from "./eszip_wasm.generated.js"; 6 | 7 | export type { LoadResponse } from "./loader.ts"; 8 | 9 | export const options: { wasmURL: URL | undefined } = { wasmURL: undefined }; 10 | 11 | export class Parser extends InternalParser { 12 | private constructor() { 13 | super(); 14 | } 15 | 16 | static async createInstance() { 17 | // insure instantiate is called 18 | await instantiate({ url: options.wasmURL }); 19 | return new Parser(); 20 | } 21 | } 22 | 23 | export async function build( 24 | roots: string[], 25 | loader: (url: string) => Promise = load, 26 | importMapUrl?: string, 27 | ): Promise { 28 | const { build } = await instantiate({ url: options.wasmURL }); 29 | return build( 30 | roots, 31 | (specifier: string) => 32 | loader(specifier).catch((err) => Promise.reject(String(err))), 33 | importMapUrl, 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /deno/vendor/deno.land/x/retry@v2.0.0/deps.ts: -------------------------------------------------------------------------------- 1 | // Copyright since 2020, FranckLdx. All rights reserved. MIT license. 2 | export { 3 | deferred, 4 | delay as denoDelay, 5 | } from "https://deno.land/std@0.98.0/async/mod.ts"; 6 | -------------------------------------------------------------------------------- /deno/vendor/deno.land/x/retry@v2.0.0/misc.ts: -------------------------------------------------------------------------------- 1 | // Copyright since 2020, FranckLdx. All rights reserved. MIT license. 2 | import { deferred } from "./deps.ts"; 3 | 4 | export const asyncDecorator = (fn: () => T) => { 5 | return (): Promise => { 6 | const promise = deferred(); 7 | try { 8 | const result = fn(); 9 | promise.resolve(result); 10 | } catch (err) { 11 | promise.reject(err); 12 | } 13 | return promise; 14 | }; 15 | }; 16 | 17 | export const assertDefined = ( 18 | value: T | undefined | null, 19 | errMsg: string, 20 | ): value is T => { 21 | if (value === undefined || value == null) { 22 | throw new Error(errMsg); 23 | } 24 | return true; 25 | }; 26 | -------------------------------------------------------------------------------- /deno/vendor/deno.land/x/retry@v2.0.0/mod.ts: -------------------------------------------------------------------------------- 1 | // Copyright since 2020, FranckLdx. All rights reserved. MIT license. 2 | 3 | export { retry, retryAsync } from "./retry/retry.ts"; 4 | export { 5 | getDefaultRetryOptions, 6 | setDefaultRetryOptions, 7 | } from "./retry/options.ts"; 8 | export { isTooManyTries, TooManyTries } from "./retry/tooManyTries.ts"; 9 | export type { RetryOptions } from "./retry/options.ts"; 10 | export { retryAsyncDecorator, retryDecorator } from "./retry/decorator.ts"; 11 | 12 | export { waitUntil, waitUntilAsync } from "./wait/wait.ts"; 13 | export type { TimeoutError } from "./wait/timeoutError.ts"; 14 | export { isTimeoutError } from "./wait/timeoutError.ts"; 15 | export { getDefaultDuration, setDefaultDuration } from "./wait/options.ts"; 16 | export { 17 | waitUntilAsyncDecorator, 18 | waitUntilDecorator, 19 | } from "./wait/decorators.ts"; 20 | 21 | export { 22 | retryAsyncUntilDefined, 23 | retryUntilDefined, 24 | } from "./retry/utils/untilDefined/retry.ts"; 25 | 26 | export { 27 | retryAsyncUntilDefinedDecorator, 28 | retryUntilDefinedDecorator, 29 | } from "./retry/utils/untilDefined/decorators.ts"; 30 | 31 | export { 32 | retryAsyncUntilTruthy, 33 | retryUntilTruthy, 34 | } from "./retry/utils/untilTruthy/retry.ts"; 35 | 36 | export { 37 | retryAsyncUntilTruthyDecorator, 38 | retryUntilTruthyDecorator, 39 | } from "./retry/utils/untilTruthy/decorators.ts"; 40 | 41 | export type { RetryUtilsOptions } from "./retry/utils/options.ts"; 42 | 43 | export { 44 | retryAsyncUntilResponse, 45 | } from "./retry/utils/untilResponse/retry.ts"; 46 | 47 | export { 48 | retryAsyncUntilResponseDecorator, 49 | } from "./retry/utils/untilResponse/decorators.ts"; 50 | 51 | -------------------------------------------------------------------------------- /deno/vendor/deno.land/x/retry@v2.0.0/retry/decorator.ts: -------------------------------------------------------------------------------- 1 | // Copyright since 2020, FranckLdx. All rights reserved. MIT license. 2 | import { retry, retryAsync } from "./retry.ts"; 3 | import { RetryOptions } from "./options.ts"; 4 | 5 | export function retryAsyncDecorator< 6 | // deno-lint-ignore no-explicit-any 7 | RETURN_TYPE extends (...args: any[]) => Promise, 8 | >( 9 | fn: RETURN_TYPE, 10 | retryOptions?: RetryOptions, 11 | ) { 12 | return (...args: Parameters): ReturnType => { 13 | const wrappedFn = () => fn(...args); 14 | return retryAsync(wrappedFn, retryOptions) as ReturnType; 15 | }; 16 | } 17 | 18 | export function retryDecorator< 19 | // deno-lint-ignore no-explicit-any 20 | RETURN_TYPE extends (...args: any[]) => any, 21 | >( 22 | fn: RETURN_TYPE, 23 | retryOptions?: RetryOptions, 24 | ) { 25 | return ( 26 | ...args: Parameters 27 | ): Promise> => { 28 | const wrappedFn = () => fn(...args); 29 | return retry(wrappedFn, retryOptions) as Promise>; 30 | }; 31 | } 32 | -------------------------------------------------------------------------------- /deno/vendor/deno.land/x/retry@v2.0.0/retry/options.ts: -------------------------------------------------------------------------------- 1 | // Copyright since 2020, FranckLdx. All rights reserved. MIT license. 2 | 3 | /** type of the unil function */ 4 | export type UNTIL = (lastResult: RETURN_TYPE) => boolean; 5 | 6 | /** 7 | * Retry options: 8 | * - maxTry: maximum number of attempts. if fn is still throwing execption afect maxtry attempts, an exepction is thrown 9 | * - delay: number of miliseconds between each attempt. 10 | * - until: if given, the function will be call until this function returns tru or until maxTry calls. 11 | */ 12 | export interface RetryOptions { 13 | maxTry?: number; 14 | delay?: number; 15 | until?: UNTIL | null; 16 | } 17 | 18 | // deno-lint-ignore no-explicit-any 19 | export let defaultRetryOptions: RetryOptions = { 20 | delay: 250, 21 | maxTry: 4 * 60, 22 | until: null, 23 | }; 24 | 25 | /** Set default retry options */ 26 | export function setDefaultRetryOptions( 27 | retryOptions: Partial>, 28 | ): RetryOptions { 29 | defaultRetryOptions = { ...defaultRetryOptions, ...retryOptions }; 30 | return getDefaultRetryOptions(); 31 | } 32 | 33 | /** Returns the current retry options. To change default options, use setDefaultRetryOptions: do not try to modify this object */ 34 | export function getDefaultRetryOptions(): Readonly> { 35 | return { ...defaultRetryOptions }; 36 | } 37 | -------------------------------------------------------------------------------- /deno/vendor/deno.land/x/retry@v2.0.0/retry/retry.ts: -------------------------------------------------------------------------------- 1 | // Copyright since 2020, FranckLdx. All rights reserved. MIT license. 2 | import { denoDelay } from "../deps.ts"; 3 | import { assertDefined, asyncDecorator } from "../misc.ts"; 4 | import { 5 | defaultRetryOptions, 6 | getDefaultRetryOptions, 7 | RetryOptions, 8 | } from "./options.ts"; 9 | import { isTooManyTries, TooManyTries } from "./tooManyTries.ts"; 10 | 11 | /** 12 | * Retry a function until it does not throw an exception. 13 | * 14 | * @param fn the function to execute 15 | * @param retryOptions retry options 16 | */ 17 | export function retry( 18 | fn: () => RETURN_TYPE, 19 | retryOptions?: RetryOptions, 20 | ): Promise { 21 | const fnAsync = asyncDecorator(fn); 22 | return retryAsync(fnAsync, retryOptions); 23 | } 24 | 25 | /** 26 | * Retry an async function until it does not throw an exception. 27 | * 28 | * @param fn the async function to execute 29 | * @param retryOptions retry options 30 | */ 31 | export async function retryAsync( 32 | fn: () => Promise, 33 | retryOptions?: RetryOptions, 34 | ): Promise { 35 | const { maxTry, delay, until } = { 36 | ...getDefaultRetryOptions(), 37 | ...retryOptions, 38 | }; 39 | assertDefined(maxTry, `maxTry must be defined`); 40 | assertDefined(delay, `delay must be defined`); 41 | const canRecall = () => maxTry! > 1; 42 | const recall = async () => { 43 | await denoDelay(delay!); 44 | return await retryAsync(fn, { delay, maxTry: maxTry! - 1, until }); 45 | }; 46 | try { 47 | const result = await fn(); 48 | const done = until ? until(result) : true; 49 | if (done) { 50 | return result; 51 | } else if (canRecall()) { 52 | return await recall(); 53 | } else { 54 | throw new TooManyTries(); 55 | } 56 | } catch (err) { 57 | if (!isTooManyTries(err) && canRecall()) { 58 | return await recall(); 59 | } else { 60 | throw err; 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /deno/vendor/deno.land/x/retry@v2.0.0/retry/tooManyTries.ts: -------------------------------------------------------------------------------- 1 | // Copyright since 2020, FranckLdx. All rights reserved. MIT license. 2 | export class TooManyTries extends Error { 3 | constructor() { 4 | super("function did not complete within allowed number of attempts"); 5 | } 6 | tooManyTries = true; 7 | } 8 | 9 | export function isTooManyTries(error: Error): error is TooManyTries { 10 | return (error as TooManyTries).tooManyTries === true; 11 | } 12 | -------------------------------------------------------------------------------- /deno/vendor/deno.land/x/retry@v2.0.0/retry/utils/options.ts: -------------------------------------------------------------------------------- 1 | // Copyright since 2020, FranckLdx. All rights reserved. MIT license. 2 | 3 | import { RetryOptions } from "../options.ts"; 4 | 5 | export type RetryUtilsOptions = Exclude, "until">; 6 | -------------------------------------------------------------------------------- /deno/vendor/deno.land/x/retry@v2.0.0/retry/utils/tools.ts: -------------------------------------------------------------------------------- 1 | import { UNTIL } from "../options.ts"; 2 | import { retry, retryAsync } from "../retry.ts"; 3 | import { RetryUtilsOptions } from "./options.ts"; 4 | 5 | export const retryHof = (until: UNTIL) => 6 | ( 7 | fn: () => RETURN_TYPE, 8 | retryOptions?: RetryUtilsOptions, 9 | ): Promise => retry(fn, { ...retryOptions, until }); 10 | 11 | export const retryAsyncHof = (until: UNTIL) => 12 | ( 13 | fn: () => Promise, 14 | retryOptions?: RetryUtilsOptions, 15 | ): Promise => retryAsync(fn, { ...retryOptions, until }); 16 | -------------------------------------------------------------------------------- /deno/vendor/deno.land/x/retry@v2.0.0/retry/utils/untilDefined/decorators.ts: -------------------------------------------------------------------------------- 1 | // Copyright since 2020, FranckLdx. All rights reserved. MIT license. 2 | import { RetryUtilsOptions } from "../options.ts"; 3 | import { retryAsyncUntilDefined, retryUntilDefined } from "./retry.ts"; 4 | 5 | export function retryUntilDefinedDecorator< 6 | // deno-lint-ignore no-explicit-any 7 | PARAMETERS_TYPE extends any[], 8 | RETURN_TYPE, 9 | >( 10 | fn: (...args: PARAMETERS_TYPE) => RETURN_TYPE | undefined | null, 11 | retryOptions?: RetryUtilsOptions, 12 | ): (...args: PARAMETERS_TYPE) => Promise { 13 | return ( 14 | ...args: PARAMETERS_TYPE 15 | ): Promise => { 16 | const wrappedFn = () => fn(...args); 17 | return retryUntilDefined(wrappedFn, retryOptions); 18 | }; 19 | } 20 | 21 | export function retryAsyncUntilDefinedDecorator< 22 | // deno-lint-ignore no-explicit-any 23 | PARAMETERS_TYPE extends any[], 24 | RETURN_TYPE, 25 | >( 26 | fn: (...args: PARAMETERS_TYPE) => Promise, 27 | retryOptions?: RetryUtilsOptions, 28 | ): (...args: PARAMETERS_TYPE) => Promise { 29 | return ( 30 | ...args: PARAMETERS_TYPE 31 | ): Promise => { 32 | const wrappedFn = () => fn(...args); 33 | return retryAsyncUntilDefined(wrappedFn, retryOptions); 34 | }; 35 | } 36 | -------------------------------------------------------------------------------- /deno/vendor/deno.land/x/retry@v2.0.0/retry/utils/untilDefined/retry.ts: -------------------------------------------------------------------------------- 1 | // Copyright since 2020, FranckLdx. All rights reserved. MIT license. 2 | import { RetryUtilsOptions } from "../options.ts"; 3 | import { retry, retryAsync } from "../../retry.ts"; 4 | 5 | const until = ( 6 | lastResult: RETURN_TYPE | undefined | null, 7 | ): boolean => lastResult !== undefined && lastResult !== null; 8 | 9 | export async function retryUntilDefined( 10 | fn: () => RETURN_TYPE | undefined | null, 11 | retryOptions?: RetryUtilsOptions, 12 | ): Promise { 13 | const result = await retry(fn, { ...retryOptions, until }); 14 | return result!; 15 | } 16 | 17 | export async function retryAsyncUntilDefined( 18 | fn: () => Promise, 19 | options?: RetryUtilsOptions, 20 | ): Promise { 21 | const result = await retryAsync(fn, { ...options, until }); 22 | return result!; 23 | } 24 | -------------------------------------------------------------------------------- /deno/vendor/deno.land/x/retry@v2.0.0/retry/utils/untilResponse/decorators.ts: -------------------------------------------------------------------------------- 1 | // Copyright since 2020, FranckLdx. All rights reserved. MIT license. 2 | import {retryAsyncUntilResponse} from './retry.ts' 3 | import { RetryUtilsOptions } from "../options.ts"; 4 | 5 | export function retryAsyncUntilResponseDecorator< 6 | // deno-lint-ignore no-explicit-any 7 | PARAMETERS_TYPE extends any[], 8 | RETURN_TYPE extends Response, 9 | >( 10 | fn: (...args: PARAMETERS_TYPE) => Promise, 11 | retryOptions?: RetryUtilsOptions, 12 | ) { 13 | return (...args: PARAMETERS_TYPE): Promise => { 14 | const wrappedFn = () => fn(...args); 15 | return retryAsyncUntilResponse(wrappedFn, retryOptions); 16 | } 17 | } 18 | 19 | -------------------------------------------------------------------------------- /deno/vendor/deno.land/x/retry@v2.0.0/retry/utils/untilResponse/retry.ts: -------------------------------------------------------------------------------- 1 | // Copyright since 2020, FranckLdx. All rights reserved. MIT license. 2 | import { retryAsyncHof } from "../tools.ts"; 3 | 4 | const until = ( 5 | lastResult: RETURN_TYPE, 6 | ): boolean => lastResult.ok; 7 | 8 | export const retryAsyncUntilResponse = retryAsyncHof(until); 9 | -------------------------------------------------------------------------------- /deno/vendor/deno.land/x/retry@v2.0.0/retry/utils/untilTruthy/decorators.ts: -------------------------------------------------------------------------------- 1 | // Copyright since 2020, FranckLdx. All rights reserved. MIT license. 2 | import { RetryUtilsOptions } from "../options.ts"; 3 | import { retryAsyncUntilTruthy, retryUntilTruthy } from "./retry.ts"; 4 | 5 | export function retryUntilTruthyDecorator< 6 | // deno-lint-ignore no-explicit-any 7 | PARAMETERS_TYPE extends any[], 8 | RETURN_TYPE, 9 | >( 10 | fn: (...args: PARAMETERS_TYPE) => RETURN_TYPE, 11 | retryOptions?: RetryUtilsOptions, 12 | ): (...args: PARAMETERS_TYPE) => Promise { 13 | return (...args: PARAMETERS_TYPE): Promise => { 14 | const wrappedFn = () => fn(...args); 15 | return retryUntilTruthy(wrappedFn, retryOptions); 16 | }; 17 | } 18 | 19 | export function retryAsyncUntilTruthyDecorator< 20 | // deno-lint-ignore no-explicit-any 21 | PARAMETERS_TYPE extends any[], 22 | RETURN_TYPE, 23 | >( 24 | fn: (...args: PARAMETERS_TYPE) => Promise, 25 | retryOptions?: RetryUtilsOptions, 26 | ): (...args: PARAMETERS_TYPE) => Promise { 27 | return (...args: PARAMETERS_TYPE): Promise => { 28 | const wrappedFn = () => fn(...args); 29 | return retryAsyncUntilTruthy(wrappedFn, retryOptions); 30 | }; 31 | } 32 | -------------------------------------------------------------------------------- /deno/vendor/deno.land/x/retry@v2.0.0/retry/utils/untilTruthy/retry.ts: -------------------------------------------------------------------------------- 1 | // Copyright since 2020, FranckLdx. All rights reserved. MIT license. 2 | import { RetryUtilsOptions } from "../options.ts"; 3 | import { retry, retryAsync } from "../../retry.ts"; 4 | 5 | const until = (lastResult: RETURN_TYPE): boolean => 6 | // deno-lint-ignore no-explicit-any 7 | (lastResult as any) == true; 8 | 9 | export function retryUntilTruthy< 10 | // deno-lint-ignore no-explicit-any 11 | PARAMETERS_TYPE extends any[], 12 | RETURN_TYPE, 13 | >( 14 | fn: (...args: PARAMETERS_TYPE) => RETURN_TYPE, 15 | retryOptions?: RetryUtilsOptions, 16 | ): Promise { 17 | return retry(fn, { ...retryOptions, until }); 18 | } 19 | 20 | export function retryAsyncUntilTruthy< 21 | // deno-lint-ignore no-explicit-any 22 | PARAMETERS_TYPE extends any[], 23 | RETURN_TYPE, 24 | >( 25 | fn: (...args: PARAMETERS_TYPE) => Promise, 26 | retryOptions?: RetryUtilsOptions, 27 | ): Promise { 28 | return retryAsync(fn, { ...retryOptions, until }); 29 | } 30 | -------------------------------------------------------------------------------- /deno/vendor/deno.land/x/retry@v2.0.0/wait/decorators.ts: -------------------------------------------------------------------------------- 1 | // Copyright since 2020, FranckLdx. All rights reserved. MIT license. 2 | import { waitUntil, waitUntilAsync } from "./wait.ts"; 3 | 4 | /** a waitUntilAsync decorator 5 | * @param fn the async function to execute 6 | * @param duration timeout in milliseconds 7 | * @param [error] custom error to throw when fn duration exceeded duration. If not provided a TimeoutError is thrown. 8 | * @returns a function hat takes same parameters as fn. It calls fn using waitUntilAsync and returns/throws the results/error of this call? 9 | */ 10 | export function waitUntilAsyncDecorator< 11 | // deno-lint-ignore no-explicit-any 12 | RETURN_TYPE extends (...args: any[]) => Promise, 13 | >(fn: RETURN_TYPE, duration?: number, error?: Error) { 14 | return (...args: Parameters): ReturnType => { 15 | const wrappedFn = () => fn(...args); 16 | return waitUntilAsync(wrappedFn, duration, error) as ReturnType< 17 | RETURN_TYPE 18 | >; 19 | }; 20 | } 21 | 22 | /** a waitUntil decorator 23 | * @param fn the function to execute 24 | * @param duration timeout in milliseconds 25 | * @param [error] custom error to throw when fn duration exceeded duration. If not provided a TimeoutError is thrown. 26 | * @returns: a function hat takes same parameters as fn. It calls fn using waitUntil and returns/throws the results/error of this call? 27 | */ 28 | export function waitUntilDecorator< 29 | // deno-lint-ignore no-explicit-any 30 | T extends (...args: any[]) => any, 31 | >(fn: T, duration?: number, error?: Error) { 32 | return (...args: Parameters): ReturnType => { 33 | const wrappedFn = () => fn(...args); 34 | return waitUntil(wrappedFn, duration, error) as ReturnType; 35 | }; 36 | } 37 | -------------------------------------------------------------------------------- /deno/vendor/deno.land/x/retry@v2.0.0/wait/options.ts: -------------------------------------------------------------------------------- 1 | // Copyright since 2020, FranckLdx. All rights reserved. MIT license. 2 | export let defaultDuration = 60 * 1000; 3 | 4 | export function setDefaultDuration(duration: number) { 5 | defaultDuration = duration; 6 | } 7 | 8 | export function getDefaultDuration(): number { 9 | return defaultDuration; 10 | } 11 | -------------------------------------------------------------------------------- /deno/vendor/deno.land/x/retry@v2.0.0/wait/timeoutError.ts: -------------------------------------------------------------------------------- 1 | // Copyright since 2020, FranckLdx. All rights reserved. MIT license. 2 | export class TimeoutError extends Error { 3 | isTimeout = true; 4 | } 5 | /** Type guard for TimeoutError */ 6 | 7 | export function isTimeoutError(error: Error): error is TimeoutError { 8 | return (error as TimeoutError).isTimeout === true; 9 | } 10 | -------------------------------------------------------------------------------- /deno/vendor/deno.land/x/retry@v2.0.0/wait/wait.ts: -------------------------------------------------------------------------------- 1 | // Copyright since 2020, FranckLdx. All rights reserved. MIT license. 2 | import { denoDelay } from "../deps.ts"; 3 | import { asyncDecorator } from "../misc.ts"; 4 | import { defaultDuration } from "./options.ts"; 5 | import { TimeoutError } from "./timeoutError.ts"; 6 | 7 | /** 8 | * wait for a function to complete within a givne duration or throw an exception. 9 | * 10 | * @param fn the async function to execute 11 | * @param duration timeout in milliseconds 12 | * @param [error] custom error to throw when fn duration exceeded duration. If not provided a TimeoutError is thrown. 13 | */ 14 | export async function waitUntilAsync( 15 | fn: () => Promise, 16 | duration: number = defaultDuration, 17 | error: Error = new TimeoutError( 18 | "function did not complete within allowed time", 19 | ), 20 | ): Promise { 21 | const canary = Symbol("RETRY_LIB_FN_EXPIRED"); 22 | const result = await Promise.race([ 23 | fn(), 24 | denoDelay(duration).then(() => canary), 25 | ]); 26 | if (result === canary) { 27 | throw error; 28 | } 29 | return result as RETURN_TYPE; 30 | } 31 | 32 | /** 33 | * wait for a function to complete within a givne duration or throw an exception. 34 | * 35 | * @param fn the function to execute 36 | * @param duration timeout in milliseconds 37 | * @param [error] custom error to throw when fn duration exceeded duration. If not provided a TimeoutError is thrown. 38 | */ 39 | export async function waitUntil( 40 | fn: () => T, 41 | duration?: number, 42 | error?: Error, 43 | ): Promise { 44 | const fnAsync = asyncDecorator(fn); 45 | return await waitUntilAsync(fnAsync, duration, error); 46 | } 47 | -------------------------------------------------------------------------------- /deno/vendor/deno.land/x/wasmbuild@0.15.1/cache.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2018-2022 the Deno authors. All rights reserved. MIT license. 2 | import { default as localDataDir } from "https://deno.land/x/dir@1.5.1/data_local_dir/mod.ts"; 3 | 4 | export async function cacheToLocalDir( 5 | url: URL, 6 | decompress: (bytes: Uint8Array) => Uint8Array, 7 | ) { 8 | const localPath = await getUrlLocalPath(url); 9 | if (localPath == null) { 10 | return undefined; 11 | } 12 | if (!await exists(localPath)) { 13 | const fileBytes = decompress(new Uint8Array(await getUrlBytes(url))); 14 | try { 15 | await Deno.writeFile(localPath, fileBytes); 16 | } catch { 17 | // ignore and return the wasm bytes 18 | return fileBytes; 19 | } 20 | } 21 | return toFileUrl(localPath); 22 | } 23 | 24 | async function getUrlLocalPath(url: URL) { 25 | try { 26 | const dataDirPath = await getInitializedLocalDataDirPath(); 27 | const hash = await getUrlHash(url); 28 | return `${dataDirPath}/${hash}.wasm`; 29 | } catch { 30 | return undefined; 31 | } 32 | } 33 | 34 | async function getInitializedLocalDataDirPath() { 35 | const dataDir = localDataDir(); 36 | if (dataDir == null) { 37 | throw new Error(`Could not find local data directory.`); 38 | } 39 | const dirPath = `${dataDir}/deno-wasmbuild`; 40 | await ensureDir(dirPath); 41 | return dirPath; 42 | } 43 | 44 | async function exists(filePath: string | URL): Promise { 45 | try { 46 | await Deno.lstat(filePath); 47 | return true; 48 | } catch (error) { 49 | if (error instanceof Deno.errors.NotFound) { 50 | return false; 51 | } 52 | throw error; 53 | } 54 | } 55 | 56 | async function ensureDir(dir: string) { 57 | try { 58 | const fileInfo = await Deno.lstat(dir); 59 | if (!fileInfo.isDirectory) { 60 | throw new Error(`Path was not a directory '${dir}'`); 61 | } 62 | } catch (err) { 63 | if (err instanceof Deno.errors.NotFound) { 64 | // if dir not exists. then create it. 65 | await Deno.mkdir(dir, { recursive: true }); 66 | return; 67 | } 68 | throw err; 69 | } 70 | } 71 | 72 | async function getUrlHash(url: URL) { 73 | // Taken from MDN: https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/digest 74 | const hashBuffer = await crypto.subtle.digest( 75 | "SHA-256", 76 | new TextEncoder().encode(url.href), 77 | ); 78 | // convert buffer to byte array 79 | const hashArray = Array.from(new Uint8Array(hashBuffer)); 80 | // convert bytes to hex string 81 | const hashHex = hashArray 82 | .map((b) => b.toString(16).padStart(2, "0")) 83 | .join(""); 84 | return hashHex; 85 | } 86 | 87 | async function getUrlBytes(url: URL) { 88 | const response = await fetchWithRetries(url); 89 | return await response.arrayBuffer(); 90 | } 91 | 92 | // the below is extracted from deno_std/path 93 | 94 | const WHITESPACE_ENCODINGS: Record = { 95 | "\u0009": "%09", 96 | "\u000A": "%0A", 97 | "\u000B": "%0B", 98 | "\u000C": "%0C", 99 | "\u000D": "%0D", 100 | "\u0020": "%20", 101 | }; 102 | 103 | function encodeWhitespace(string: string): string { 104 | return string.replaceAll(/[\s]/g, (c) => { 105 | return WHITESPACE_ENCODINGS[c] ?? c; 106 | }); 107 | } 108 | 109 | function toFileUrl(path: string): URL { 110 | return Deno.build.os === "windows" 111 | ? windowsToFileUrl(path) 112 | : posixToFileUrl(path); 113 | } 114 | 115 | function posixToFileUrl(path: string): URL { 116 | const url = new URL("file:///"); 117 | url.pathname = encodeWhitespace( 118 | path.replace(/%/g, "%25").replace(/\\/g, "%5C"), 119 | ); 120 | return url; 121 | } 122 | 123 | function windowsToFileUrl(path: string): URL { 124 | const [, hostname, pathname] = path.match( 125 | /^(?:[/\\]{2}([^/\\]+)(?=[/\\](?:[^/\\]|$)))?(.*)/, 126 | )!; 127 | const url = new URL("file:///"); 128 | url.pathname = encodeWhitespace(pathname.replace(/%/g, "%25")); 129 | if (hostname != null && hostname != "localhost") { 130 | url.hostname = hostname; 131 | if (!url.hostname) { 132 | throw new TypeError("Invalid hostname."); 133 | } 134 | } 135 | return url; 136 | } 137 | 138 | export async function fetchWithRetries(url: URL | string, maxRetries = 5) { 139 | let sleepMs = 250; 140 | let iterationCount = 0; 141 | while (true) { 142 | iterationCount++; 143 | try { 144 | const res = await fetch(url); 145 | if (res.ok || iterationCount > maxRetries) { 146 | return res; 147 | } 148 | } catch (err) { 149 | if (iterationCount > maxRetries) { 150 | throw err; 151 | } 152 | } 153 | console.warn(`Failed fetching. Retrying in ${sleepMs}ms...`); 154 | await new Promise((resolve) => setTimeout(resolve, sleepMs)); 155 | sleepMs = Math.min(sleepMs * 2, 10_000); 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /deno/vendor/deno.land/x/wasmbuild@0.15.1/loader.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2018-2022 the Deno authors. All rights reserved. MIT license. 2 | import { fetchWithRetries } from "./cache.ts"; 3 | 4 | export type DecompressCallback = (bytes: Uint8Array) => Uint8Array; 5 | 6 | export interface LoaderOptions { 7 | /** The Wasm module's imports. */ 8 | imports: WebAssembly.Imports | undefined; 9 | /** A function that caches the Wasm module to a local path so that 10 | * so that a network request isn't required on every load. 11 | * 12 | * Returns an ArrayBuffer with the bytes on download success, but 13 | * cache save failure. 14 | */ 15 | cache?: ( 16 | url: URL, 17 | decompress: DecompressCallback | undefined, 18 | ) => Promise; 19 | } 20 | 21 | export class Loader { 22 | #options: LoaderOptions; 23 | #lastLoadPromise: 24 | | Promise 25 | | undefined; 26 | #instantiated: WebAssembly.WebAssemblyInstantiatedSource | undefined; 27 | 28 | constructor(options: LoaderOptions) { 29 | this.#options = options; 30 | } 31 | 32 | get instance() { 33 | return this.#instantiated?.instance; 34 | } 35 | 36 | get module() { 37 | return this.#instantiated?.module; 38 | } 39 | 40 | load( 41 | url: URL, 42 | decompress: DecompressCallback | undefined, 43 | ): Promise { 44 | if (this.#instantiated) { 45 | return Promise.resolve(this.#instantiated); 46 | } else if (this.#lastLoadPromise == null) { 47 | this.#lastLoadPromise = (async () => { 48 | try { 49 | this.#instantiated = await this.#instantiate(url, decompress); 50 | return this.#instantiated; 51 | } finally { 52 | this.#lastLoadPromise = undefined; 53 | } 54 | })(); 55 | } 56 | return this.#lastLoadPromise; 57 | } 58 | 59 | async #instantiate(url: URL, decompress: DecompressCallback | undefined) { 60 | const imports = this.#options.imports; 61 | if (this.#options.cache != null && url.protocol !== "file:") { 62 | try { 63 | const result = await this.#options.cache( 64 | url, 65 | decompress ?? ((bytes) => bytes), 66 | ); 67 | if (result instanceof URL) { 68 | url = result; 69 | decompress = undefined; // already decompressed 70 | } else if (result != null) { 71 | return WebAssembly.instantiate(result, imports); 72 | } 73 | } catch { 74 | // ignore if caching ever fails (ex. when on deploy) 75 | } 76 | } 77 | 78 | const isFile = url.protocol === "file:"; 79 | 80 | // make file urls work in Node via dnt 81 | // deno-lint-ignore no-explicit-any 82 | const isNode = (globalThis as any).process?.versions?.node != null; 83 | if (isFile && typeof Deno !== "object") { 84 | throw new Error( 85 | "Loading local files are not supported in this environment", 86 | ); 87 | } 88 | if (isNode && isFile) { 89 | // the deno global will be shimmed by dnt 90 | const wasmCode = await Deno.readFile(url); 91 | return WebAssembly.instantiate( 92 | decompress ? decompress(wasmCode) : wasmCode, 93 | imports, 94 | ); 95 | } 96 | 97 | switch (url.protocol) { 98 | case "file:": 99 | case "https:": 100 | case "http:": { 101 | const wasmResponse = await fetchWithRetries(url); 102 | if (decompress) { 103 | const wasmCode = new Uint8Array(await wasmResponse.arrayBuffer()); 104 | return WebAssembly.instantiate(decompress(wasmCode), imports); 105 | } 106 | if ( 107 | isFile || 108 | wasmResponse.headers.get("content-type")?.toLowerCase() 109 | .startsWith("application/wasm") 110 | ) { 111 | // Cast to any so there's no type checking issues with dnt 112 | // (https://github.com/denoland/wasmbuild/issues/92) 113 | // deno-lint-ignore no-explicit-any 114 | return WebAssembly.instantiateStreaming(wasmResponse as any, imports); 115 | } else { 116 | return WebAssembly.instantiate( 117 | await wasmResponse.arrayBuffer(), 118 | imports, 119 | ); 120 | } 121 | } 122 | default: 123 | throw new Error(`Unsupported protocol: ${url.protocol}`); 124 | } 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /deno/vendor/import_map.json: -------------------------------------------------------------------------------- 1 | { 2 | "imports": { 3 | "https://deno.land/": "./deno.land/" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /node/__snapshots__/declaration.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`Ensure the order of edge functions with FF 1`] = ` 4 | [ 5 | { 6 | "function": "framework-manifest-a", 7 | "path": "/path1", 8 | }, 9 | { 10 | "function": "framework-manifest-c", 11 | "path": "/path3", 12 | }, 13 | { 14 | "function": "framework-manifest-b", 15 | "path": "/path2", 16 | }, 17 | { 18 | "function": "framework-isc-c", 19 | "path": "/path1", 20 | }, 21 | { 22 | "function": "framework-isc-c", 23 | "path": "/path2", 24 | }, 25 | { 26 | "function": "user-toml-a", 27 | "path": "/path1", 28 | }, 29 | { 30 | "function": "user-toml-c", 31 | "path": "/path3", 32 | }, 33 | { 34 | "function": "user-toml-b", 35 | "path": "/path2", 36 | }, 37 | { 38 | "function": "user-isc-c", 39 | "path": "/path1", 40 | }, 41 | { 42 | "function": "user-isc-c", 43 | "path": "/path2", 44 | }, 45 | ] 46 | `; 47 | -------------------------------------------------------------------------------- /node/bridge.test.ts: -------------------------------------------------------------------------------- 1 | import { Buffer } from 'buffer' 2 | import { rm } from 'fs/promises' 3 | import { createRequire } from 'module' 4 | import { platform, env } from 'process' 5 | import { PassThrough } from 'stream' 6 | 7 | import nock from 'nock' 8 | import semver from 'semver' 9 | import tmp, { DirectoryResult } from 'tmp-promise' 10 | import { test, expect } from 'vitest' 11 | 12 | import { DenoBridge, DENO_VERSION_RANGE } from './bridge.js' 13 | import { getPlatformTarget } from './platform.js' 14 | 15 | const require = createRequire(import.meta.url) 16 | const archiver = require('archiver') 17 | 18 | const getMockDenoBridge = function (tmpDir: DirectoryResult, mockBinaryOutput: string) { 19 | const latestVersion = semver.minVersion(DENO_VERSION_RANGE)?.version ?? '' 20 | const data = new PassThrough() 21 | const archive = archiver('zip', { zlib: { level: 9 } }) 22 | 23 | archive.pipe(data) 24 | archive.append(Buffer.from(mockBinaryOutput.replace(/@@@latestVersion@@@/g, latestVersion)), { 25 | name: platform === 'win32' ? 'deno.exe' : 'deno', 26 | }) 27 | archive.finalize() 28 | 29 | const target = getPlatformTarget() 30 | 31 | nock('https://dl.deno.land').get('/release-latest.txt').reply(200, `v${latestVersion}`) 32 | nock('https://dl.deno.land') 33 | .get(`/release/v${latestVersion}/deno-${target}.zip`) 34 | .reply(200, () => data) 35 | 36 | return new DenoBridge({ 37 | cacheDirectory: tmpDir.path, 38 | useGlobal: false, 39 | }) 40 | } 41 | 42 | test('Does not inherit environment variables if `extendEnv` is false', async () => { 43 | const tmpDir = await tmp.dir() 44 | const deno = getMockDenoBridge( 45 | tmpDir, 46 | `#!/usr/bin/env sh 47 | 48 | if [ "$1" = "test" ] 49 | then 50 | env 51 | else 52 | echo "deno @@@latestVersion@@@" 53 | fi`, 54 | ) 55 | 56 | // The environment sets some variables so let us see what they are and remove them from the result 57 | const referenceOutput = await deno.run(['test'], { env: {}, extendEnv: false }) 58 | env.TADA = 'TUDU' 59 | const result = await deno.run(['test'], { env: { LULU: 'LALA' }, extendEnv: false }) 60 | let output = result?.stdout ?? '' 61 | 62 | delete env.TADA 63 | 64 | referenceOutput?.stdout.split('\n').forEach((line) => { 65 | output = output.replace(line.trim(), '') 66 | }) 67 | output = output.trim().replace(/\n+/g, '\n') 68 | 69 | expect(output).toBe('LULU=LALA') 70 | 71 | await rm(tmpDir.path, { force: true, recursive: true, maxRetries: 10 }) 72 | }) 73 | 74 | test('Does inherit environment variables if `extendEnv` is true', async () => { 75 | const tmpDir = await tmp.dir() 76 | const deno = getMockDenoBridge( 77 | tmpDir, 78 | `#!/usr/bin/env sh 79 | 80 | if [ "$1" = "test" ] 81 | then 82 | env 83 | else 84 | echo "deno @@@latestVersion@@@" 85 | fi`, 86 | ) 87 | 88 | // The environment sets some variables so let us see what they are and remove them from the result 89 | const referenceOutput = await deno.run(['test'], { env: {}, extendEnv: true }) 90 | env.TADA = 'TUDU' 91 | const result = await deno.run(['test'], { env: { LULU: 'LALA' }, extendEnv: true }) 92 | let output = result?.stdout ?? '' 93 | 94 | delete env.TADA 95 | 96 | referenceOutput?.stdout.split('\n').forEach((line) => { 97 | output = output.replace(line.trim(), '') 98 | }) 99 | // lets remove holes, split lines and sort lines by name, as different OSes might order them different 100 | const environmentVariables = output.trim().replace(/\n+/g, '\n').split('\n').sort() 101 | 102 | expect(environmentVariables).toEqual(['LULU=LALA', 'TADA=TUDU']) 103 | 104 | await rm(tmpDir.path, { force: true, recursive: true, maxRetries: 10 }) 105 | }) 106 | 107 | test('Does inherit environment variables if `extendEnv` is not set', async () => { 108 | const tmpDir = await tmp.dir() 109 | const deno = getMockDenoBridge( 110 | tmpDir, 111 | `#!/usr/bin/env sh 112 | 113 | if [ "$1" = "test" ] 114 | then 115 | env 116 | else 117 | echo "deno @@@latestVersion@@@" 118 | fi`, 119 | ) 120 | 121 | // The environment sets some variables so let us see what they are and remove them from the result 122 | const referenceOutput = await deno.run(['test'], { env: {}, extendEnv: true }) 123 | env.TADA = 'TUDU' 124 | const result = await deno.run(['test'], { env: { LULU: 'LALA' } }) 125 | let output = result?.stdout ?? '' 126 | 127 | delete env.TADA 128 | 129 | referenceOutput?.stdout.split('\n').forEach((line) => { 130 | output = output.replace(line.trim(), '') 131 | }) 132 | // lets remove holes, split lines and sort lines by name, as different OSes might order them different 133 | const environmentVariables = output.trim().replace(/\n+/g, '\n').split('\n').sort() 134 | 135 | expect(environmentVariables).toEqual(['LULU=LALA', 'TADA=TUDU']) 136 | 137 | await rm(tmpDir.path, { force: true, recursive: true, maxRetries: 10 }) 138 | }) 139 | -------------------------------------------------------------------------------- /node/bundle.ts: -------------------------------------------------------------------------------- 1 | export enum BundleFormat { 2 | ESZIP2 = 'eszip2', 3 | JS = 'js', 4 | } 5 | 6 | export interface Bundle { 7 | extension: string 8 | format: BundleFormat 9 | hash: string 10 | } 11 | -------------------------------------------------------------------------------- /node/bundle_error.ts: -------------------------------------------------------------------------------- 1 | import type { ExecaError } from 'execa' 2 | 3 | interface BundleErrorOptions { 4 | format: string 5 | } 6 | 7 | const getCustomErrorInfo = (options?: BundleErrorOptions) => ({ 8 | location: { 9 | format: options?.format, 10 | runtime: 'deno', 11 | }, 12 | type: 'functionsBundling', 13 | }) 14 | 15 | class BundleError extends Error { 16 | customErrorInfo: ReturnType 17 | 18 | constructor(originalError: Error, options?: BundleErrorOptions) { 19 | super(originalError.message) 20 | 21 | this.customErrorInfo = getCustomErrorInfo(options) 22 | this.name = 'BundleError' 23 | this.stack = originalError.stack 24 | 25 | // https://github.com/microsoft/TypeScript-wiki/blob/8a66ecaf77118de456f7cd9c56848a40fe29b9b4/Breaking-Changes.md#implicit-any-error-raised-for-un-annotated-callback-arguments-with-no-matching-overload-arguments 26 | Object.setPrototypeOf(this, BundleError.prototype) 27 | } 28 | } 29 | 30 | /** 31 | * BundleErrors are treated as user-error, so Netlify Team is not alerted about them. 32 | */ 33 | const wrapBundleError = (input: unknown, options?: BundleErrorOptions) => { 34 | if (input instanceof Error) { 35 | if (input.message.includes("The module's source code could not be parsed")) { 36 | // eslint-disable-next-line no-param-reassign 37 | input.message = (input as ExecaError).stderr 38 | } 39 | 40 | return new BundleError(input, options) 41 | } 42 | 43 | return input 44 | } 45 | 46 | export { BundleError, wrapBundleError } 47 | -------------------------------------------------------------------------------- /node/config.ts: -------------------------------------------------------------------------------- 1 | import { promises as fs } from 'fs' 2 | import { join } from 'path' 3 | import { pathToFileURL } from 'url' 4 | 5 | import tmp from 'tmp-promise' 6 | 7 | import { DenoBridge } from './bridge.js' 8 | import { BundleError } from './bundle_error.js' 9 | import { EdgeFunction } from './edge_function.js' 10 | import { ImportMap } from './import_map.js' 11 | import { Logger } from './logger.js' 12 | import { getPackagePath } from './package_json.js' 13 | import { RateLimit } from './rate_limit.js' 14 | 15 | enum ConfigExitCode { 16 | Success = 0, 17 | UnhandledError = 1, 18 | ImportError, 19 | NoConfig, 20 | InvalidExport, 21 | SerializationError, 22 | InvalidDefaultExport, 23 | } 24 | 25 | export const enum Cache { 26 | Off = 'off', 27 | Manual = 'manual', 28 | } 29 | 30 | export type HTTPMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'OPTIONS' 31 | 32 | export type Path = `/${string}` 33 | 34 | export type OnError = 'fail' | 'bypass' | Path 35 | 36 | export const isValidOnError = (value: unknown): value is OnError => { 37 | if (typeof value === 'undefined') return true 38 | if (typeof value !== 'string') return false 39 | return value === 'fail' || value === 'bypass' || value.startsWith('/') 40 | } 41 | 42 | export interface FunctionConfig { 43 | cache?: Cache 44 | path?: Path | Path[] 45 | excludedPath?: Path | Path[] 46 | onError?: OnError 47 | name?: string 48 | generator?: string 49 | method?: HTTPMethod | HTTPMethod[] 50 | rateLimit?: RateLimit 51 | } 52 | 53 | const getConfigExtractor = () => { 54 | const packagePath = getPackagePath() 55 | const configExtractorPath = join(packagePath, 'deno', 'config.ts') 56 | 57 | return configExtractorPath 58 | } 59 | 60 | export const getFunctionConfig = async ({ 61 | func, 62 | importMap, 63 | deno, 64 | log, 65 | }: { 66 | func: EdgeFunction 67 | importMap: ImportMap 68 | deno: DenoBridge 69 | log: Logger 70 | }) => { 71 | // The extractor is a Deno script that will import the function and run its 72 | // `config` export, if one exists. 73 | const extractorPath = getConfigExtractor() 74 | 75 | // We need to collect the output of the config function, which should be a 76 | // JSON object. Rather than printing it to stdout, the extractor will write 77 | // it to a temporary file, which we then read in the Node side. This allows 78 | // the config function to write to stdout and stderr without that interfering 79 | // with the extractor. 80 | const collector = await tmp.file() 81 | 82 | // The extractor will use its exit code to signal different error scenarios, 83 | // based on the list of exit codes we send as an argument. We then capture 84 | // the exit code to know exactly what happened and guide people accordingly. 85 | const { exitCode, stderr, stdout } = await deno.run( 86 | [ 87 | 'run', 88 | '--allow-env', 89 | '--allow-net', 90 | '--allow-read', 91 | `--allow-write=${collector.path}`, 92 | '--quiet', 93 | `--import-map=${importMap.toDataURL()}`, 94 | extractorPath, 95 | pathToFileURL(func.path).href, 96 | pathToFileURL(collector.path).href, 97 | JSON.stringify(ConfigExitCode), 98 | ], 99 | { rejectOnExitCode: false }, 100 | ) 101 | 102 | if (exitCode !== ConfigExitCode.Success) { 103 | handleConfigError(func, exitCode, stderr, log) 104 | 105 | return {} 106 | } 107 | 108 | if (stdout !== '') { 109 | log.user(stdout) 110 | } 111 | 112 | let collectorData: FunctionConfig = {} 113 | 114 | try { 115 | const collectorDataJSON = await fs.readFile(collector.path, 'utf8') 116 | collectorData = JSON.parse(collectorDataJSON) as FunctionConfig 117 | } catch { 118 | handleConfigError(func, ConfigExitCode.UnhandledError, stderr, log) 119 | } finally { 120 | await collector.cleanup() 121 | } 122 | 123 | if (!isValidOnError(collectorData.onError)) { 124 | throw new BundleError( 125 | new Error( 126 | `The 'onError' configuration property in edge function at '${func.path}' must be one of 'fail', 'bypass', or a path starting with '/'. Got '${collectorData.onError}'. More on the Edge Functions API at https://ntl.fyi/edge-api.`, 127 | ), 128 | ) 129 | } 130 | 131 | return collectorData 132 | } 133 | 134 | const handleConfigError = (func: EdgeFunction, exitCode: number, stderr: string, log: Logger) => { 135 | switch (exitCode) { 136 | case ConfigExitCode.ImportError: 137 | log.user(stderr) 138 | throw new BundleError( 139 | new Error( 140 | `Could not load edge function at '${func.path}'. More on the Edge Functions API at https://ntl.fyi/edge-api.`, 141 | ), 142 | ) 143 | 144 | break 145 | 146 | case ConfigExitCode.NoConfig: 147 | log.system(`No in-source config found for edge function at '${func.path}'`) 148 | 149 | break 150 | 151 | case ConfigExitCode.InvalidExport: 152 | throw new BundleError( 153 | new Error( 154 | `The 'config' export in edge function at '${func.path}' must be an object. More on the Edge Functions API at https://ntl.fyi/edge-api.`, 155 | ), 156 | ) 157 | 158 | break 159 | 160 | case ConfigExitCode.SerializationError: 161 | throw new BundleError( 162 | new Error( 163 | `The 'config' object in the edge function at '${func.path}' must contain primitive values only. More on the Edge Functions API at https://ntl.fyi/edge-api.`, 164 | ), 165 | ) 166 | break 167 | 168 | case ConfigExitCode.InvalidDefaultExport: 169 | throw new BundleError( 170 | new Error( 171 | `Default export in '${func.path}' must be a function. More on the Edge Functions API at https://ntl.fyi/edge-api.`, 172 | ), 173 | ) 174 | 175 | default: 176 | log.user(`Could not load configuration for edge function at '${func.path}'`) 177 | log.user(stderr) 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /node/declaration.ts: -------------------------------------------------------------------------------- 1 | import { FunctionConfig, HTTPMethod, Path } from './config.js' 2 | import { FeatureFlags } from './feature_flags.js' 3 | 4 | interface BaseDeclaration { 5 | cache?: string 6 | function: string 7 | method?: HTTPMethod | HTTPMethod[] 8 | // todo: remove these two after a while and only support in-source config for non-route related configs 9 | name?: string 10 | generator?: string 11 | } 12 | 13 | type DeclarationWithPath = BaseDeclaration & { 14 | path: Path 15 | excludedPath?: Path | Path[] 16 | } 17 | 18 | type DeclarationWithPattern = BaseDeclaration & { 19 | pattern: string 20 | excludedPattern?: string | string[] 21 | } 22 | 23 | export type Declaration = DeclarationWithPath | DeclarationWithPattern 24 | 25 | export const mergeDeclarations = ( 26 | tomlDeclarations: Declaration[], 27 | userFunctionsConfig: Record, 28 | internalFunctionsConfig: Record, 29 | deployConfigDeclarations: Declaration[], 30 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 31 | _featureFlags: FeatureFlags = {}, 32 | // eslint-disable-next-line max-params 33 | ) => { 34 | const functionsVisited: Set = new Set() 35 | 36 | const declarations: Declaration[] = [ 37 | // INTEGRATIONS 38 | // 1. Declarations from the integrations deploy config 39 | ...getDeclarationsFromInput(deployConfigDeclarations, internalFunctionsConfig, functionsVisited), 40 | // 2. Declarations from the integrations ISC 41 | ...createDeclarationsFromFunctionConfigs(internalFunctionsConfig, functionsVisited), 42 | 43 | // USER 44 | // 3. Declarations from the users toml config 45 | ...getDeclarationsFromInput(tomlDeclarations, userFunctionsConfig, functionsVisited), 46 | // 4. Declarations from the users ISC 47 | ...createDeclarationsFromFunctionConfigs(userFunctionsConfig, functionsVisited), 48 | ] 49 | 50 | return declarations 51 | } 52 | 53 | const getDeclarationsFromInput = ( 54 | inputDeclarations: Declaration[], 55 | functionConfigs: Record, 56 | functionsVisited: Set, 57 | ): Declaration[] => { 58 | const declarations: Declaration[] = [] 59 | // For any declaration for which we also have a function configuration object, 60 | // we replace the path because that object takes precedence. 61 | for (const declaration of inputDeclarations) { 62 | const config = functionConfigs[declaration.function] 63 | 64 | if (!config) { 65 | // If no config is found, add the declaration as is. 66 | declarations.push(declaration) 67 | } else if (config.path?.length) { 68 | // If we have a path specified as either a string or non-empty array, 69 | // create a declaration for each path. 70 | const paths = Array.isArray(config.path) ? config.path : [config.path] 71 | 72 | paths.forEach((path) => { 73 | declarations.push({ ...declaration, cache: config.cache, path }) 74 | }) 75 | } else { 76 | // With an in-source config without a path, add the config to the declaration. 77 | const { path, excludedPath, ...rest } = config 78 | 79 | declarations.push({ ...declaration, ...rest }) 80 | } 81 | 82 | functionsVisited.add(declaration.function) 83 | } 84 | 85 | return declarations 86 | } 87 | 88 | const createDeclarationsFromFunctionConfigs = ( 89 | functionConfigs: Record, 90 | functionsVisited: Set, 91 | ): Declaration[] => { 92 | const declarations: Declaration[] = [] 93 | 94 | for (const name in functionConfigs) { 95 | const { cache, path, method } = functionConfigs[name] 96 | 97 | // If we have a path specified, create a declaration for each path. 98 | if (!functionsVisited.has(name) && path) { 99 | const paths = Array.isArray(path) ? path : [path] 100 | 101 | paths.forEach((singlePath) => { 102 | const declaration: Declaration = { function: name, path: singlePath } 103 | if (cache) { 104 | declaration.cache = cache 105 | } 106 | if (method) { 107 | declaration.method = method 108 | } 109 | declarations.push(declaration) 110 | }) 111 | } 112 | } 113 | 114 | return declarations 115 | } 116 | 117 | /** 118 | * Normalizes a regular expression, ensuring it has a leading `^` and trailing 119 | * `$` characters. 120 | */ 121 | export const normalizePattern = (pattern: string) => { 122 | let enclosedPattern = pattern 123 | 124 | if (!pattern.startsWith('^')) { 125 | enclosedPattern = `^${enclosedPattern}` 126 | } 127 | 128 | if (!pattern.endsWith('$')) { 129 | enclosedPattern = `${enclosedPattern}$` 130 | } 131 | 132 | const regexp = new RegExp(enclosedPattern) 133 | 134 | // Strip leading and forward slashes. 135 | return regexp.toString().slice(1, -1) 136 | } 137 | -------------------------------------------------------------------------------- /node/deploy_config.test.ts: -------------------------------------------------------------------------------- 1 | import { promises as fs } from 'fs' 2 | import { join } from 'path' 3 | import { cwd } from 'process' 4 | 5 | import tmp from 'tmp-promise' 6 | import { test, expect } from 'vitest' 7 | 8 | import { load } from './deploy_config.js' 9 | import { getLogger } from './logger.js' 10 | 11 | const logger = getLogger(console.log) 12 | 13 | test('Returns an empty config object if there is no file at the given path', async () => { 14 | const mockPath = join(cwd(), 'some-directory', `a-file-that-does-not-exist-${Date.now()}.json`) 15 | const config = await load(mockPath, logger) 16 | 17 | expect(config.declarations).toEqual([]) 18 | expect(config.layers).toEqual([]) 19 | }) 20 | 21 | test('Returns a config object with declarations, layers, and import map', async () => { 22 | const importMapFile = await tmp.file({ postfix: '.json' }) 23 | const importMap = { 24 | imports: { 25 | 'https://deno.land/': 'https://black.hole/', 26 | }, 27 | } 28 | 29 | await fs.writeFile(importMapFile.path, JSON.stringify(importMap)) 30 | 31 | const configFile = await tmp.file() 32 | const config = { 33 | functions: [ 34 | { 35 | function: 'func1', 36 | path: '/func1', 37 | generator: 'internalFunc', 38 | }, 39 | ], 40 | layers: [ 41 | { 42 | name: 'layer1', 43 | flag: 'edge_functions_layer1_url', 44 | local: 'https://some-url.netlify.app/mod.ts', 45 | }, 46 | ], 47 | import_map: importMapFile.path, 48 | version: 1, 49 | } 50 | 51 | await fs.writeFile(configFile.path, JSON.stringify(config)) 52 | 53 | const parsedConfig = await load(configFile.path, logger) 54 | 55 | await importMapFile.cleanup() 56 | 57 | expect(parsedConfig.declarations).toEqual(config.functions) 58 | expect(parsedConfig.layers).toEqual(config.layers) 59 | expect(parsedConfig.importMap).toBe(importMapFile.path) 60 | }) 61 | -------------------------------------------------------------------------------- /node/deploy_config.ts: -------------------------------------------------------------------------------- 1 | import { promises as fs } from 'fs' 2 | import { dirname, resolve } from 'path' 3 | 4 | import type { Declaration } from './declaration.js' 5 | import type { Layer } from './layer.js' 6 | import type { Logger } from './logger.js' 7 | import { isFileNotFoundError } from './utils/error.js' 8 | 9 | interface DeployConfigFile { 10 | functions?: Declaration[] 11 | import_map?: string 12 | layers?: Layer[] 13 | version: number 14 | } 15 | 16 | export interface DeployConfig { 17 | declarations: Declaration[] 18 | importMap?: string 19 | layers: Layer[] 20 | } 21 | 22 | export const load = async (path: string | undefined, logger: Logger): Promise => { 23 | if (path === undefined) { 24 | return { 25 | declarations: [], 26 | layers: [], 27 | } 28 | } 29 | 30 | try { 31 | const data = await fs.readFile(path, 'utf8') 32 | const config = JSON.parse(data) as DeployConfigFile 33 | 34 | return parse(config, path) 35 | } catch (error) { 36 | if (!isFileNotFoundError(error)) { 37 | logger.system('Error while parsing internal edge functions manifest:', error) 38 | } 39 | } 40 | 41 | return { 42 | declarations: [], 43 | layers: [], 44 | } 45 | } 46 | 47 | const parse = (data: DeployConfigFile, path: string) => { 48 | if (data.version !== 1) { 49 | throw new Error(`Unsupported file version: ${data.version}`) 50 | } 51 | 52 | const config: DeployConfig = { 53 | declarations: data.functions ?? [], 54 | layers: data.layers ?? [], 55 | } 56 | 57 | if (data.import_map) { 58 | const importMapPath = resolve(dirname(path), data.import_map) 59 | 60 | config.importMap = importMapPath 61 | } 62 | 63 | return config 64 | } 65 | -------------------------------------------------------------------------------- /node/downloader.test.ts: -------------------------------------------------------------------------------- 1 | import { rm } from 'fs/promises' 2 | import { platform } from 'process' 3 | import { PassThrough } from 'stream' 4 | 5 | import { execa } from 'execa' 6 | import nock from 'nock' 7 | import tmp from 'tmp-promise' 8 | import { beforeEach, afterEach, test, expect, TestContext as VitestTestContext, vi } from 'vitest' 9 | 10 | import { fixturesDir, testLogger } from '../test/util.js' 11 | 12 | import { download } from './downloader.js' 13 | import { getPlatformTarget } from './platform.js' 14 | 15 | // This changes the defaults for p-retry 16 | // minTimeout 1000 -> 10 17 | // factor 2 -> 1 18 | // This reduces the wait time in the tests from `2s, 4s, 8s` to `10ms, 10ms, 10ms` for 3 retries 19 | vi.mock('p-retry', async (importOriginal) => { 20 | const pRetry = (await importOriginal()) as typeof import('p-retry') 21 | type Params = Parameters 22 | 23 | return { 24 | default: (func: Params[0], options: Params[1]) => pRetry.default(func, { minTimeout: 10, factor: 1, ...options }), 25 | } 26 | }) 27 | 28 | const streamError = () => { 29 | const stream = new PassThrough() 30 | setTimeout(() => stream.emit('data', 'zipcontent'), 100) 31 | setTimeout(() => stream.emit('error', new Error('stream error')), 200) 32 | 33 | return stream 34 | } 35 | 36 | interface TestContext extends VitestTestContext { 37 | tmpDir: string 38 | } 39 | 40 | beforeEach(async (ctx: TestContext) => { 41 | const tmpDir = await tmp.dir() 42 | 43 | // eslint-disable-next-line no-param-reassign 44 | ctx.tmpDir = tmpDir.path 45 | }) 46 | 47 | afterEach(async (ctx: TestContext) => { 48 | await rm(ctx.tmpDir, { force: true, recursive: true, maxRetries: 10 }) 49 | }) 50 | 51 | test('tries downloading binary up to 4 times', async (ctx: TestContext) => { 52 | nock.disableNetConnect() 53 | 54 | const version = '99.99.99' 55 | const mockURL = 'https://dl.deno.land:443' 56 | const target = getPlatformTarget() 57 | const zipPath = `/release/v${version}/deno-${target}.zip` 58 | const latestVersionMock = nock(mockURL) 59 | .get('/release-latest.txt') 60 | .reply(200, `v${version}\n`) 61 | 62 | // first attempt 63 | .get(zipPath) 64 | .reply(500) 65 | 66 | // second attempt 67 | .get(zipPath) 68 | .reply(500) 69 | 70 | // third attempt 71 | .get(zipPath) 72 | .reply(500) 73 | 74 | // fourth attempt 75 | .get(zipPath) 76 | // 1 second delay 77 | .delayBody(1000) 78 | .replyWithFile(200, platform === 'win32' ? `${fixturesDir}/deno.win.zip` : `${fixturesDir}/deno.zip`, { 79 | 'Content-Type': 'application/zip', 80 | }) 81 | 82 | const deno = await download(ctx.tmpDir, `^${version}`, testLogger) 83 | 84 | expect(latestVersionMock.isDone()).toBe(true) 85 | expect(deno).toBeTruthy() 86 | 87 | const res = await execa(deno) 88 | expect(res.stdout).toBe('hello') 89 | }) 90 | 91 | test('fails downloading binary after 4th time', async (ctx: TestContext) => { 92 | expect.assertions(2) 93 | 94 | nock.disableNetConnect() 95 | 96 | const version = '99.99.99' 97 | const mockURL = 'https://dl.deno.land:443' 98 | const target = getPlatformTarget() 99 | const zipPath = `/release/v${version}/deno-${target}.zip` 100 | const latestVersionMock = nock(mockURL) 101 | .get('/release-latest.txt') 102 | .reply(200, `v${version}\n`) 103 | 104 | // first attempt 105 | .get(zipPath) 106 | .reply(500) 107 | 108 | // second attempt 109 | .get(zipPath) 110 | .reply(500) 111 | 112 | // third attempt 113 | .get(zipPath) 114 | .reply(500) 115 | 116 | // fourth attempt 117 | .get(zipPath) 118 | .reply(500) 119 | 120 | try { 121 | await download(ctx.tmpDir, `^${version}`, testLogger) 122 | } catch (error) { 123 | expect(error).toMatch(/Download failed with status code 500/) 124 | } 125 | 126 | expect(latestVersionMock.isDone()).toBe(true) 127 | }) 128 | 129 | test('fails downloading if response stream throws error', async (ctx: TestContext) => { 130 | expect.assertions(2) 131 | 132 | nock.disableNetConnect() 133 | 134 | const version = '99.99.99' 135 | const mockURL = 'https://dl.deno.land:443' 136 | const target = getPlatformTarget() 137 | const zipPath = `/release/v${version}/deno-${target}.zip` 138 | 139 | const latestVersionMock = nock(mockURL) 140 | .get('/release-latest.txt') 141 | .reply(200, `v${version}\n`) 142 | 143 | // first attempt 144 | .get(zipPath) 145 | .reply(200, streamError) 146 | 147 | // second attempt 148 | .get(zipPath) 149 | .reply(200, streamError) 150 | 151 | // third attempt 152 | .get(zipPath) 153 | .reply(200, streamError) 154 | 155 | // fourth attempt 156 | .get(zipPath) 157 | .reply(200, streamError) 158 | 159 | try { 160 | await download(ctx.tmpDir, `^${version}`, testLogger) 161 | } catch (error) { 162 | expect(error).toMatch(/stream error/) 163 | } 164 | 165 | expect(latestVersionMock.isDone()).toBe(true) 166 | }) 167 | -------------------------------------------------------------------------------- /node/downloader.ts: -------------------------------------------------------------------------------- 1 | import { createWriteStream, promises as fs } from 'fs' 2 | import path from 'path' 3 | import { promisify } from 'util' 4 | 5 | import fetch from 'node-fetch' 6 | import StreamZip from 'node-stream-zip' 7 | import pRetry from 'p-retry' 8 | import semver from 'semver' 9 | 10 | import { Logger } from './logger.js' 11 | import { getBinaryExtension, getPlatformTarget } from './platform.js' 12 | 13 | const downloadWithRetry = async (targetDirectory: string, versionRange: string, logger: Logger) => 14 | await pRetry(async () => await download(targetDirectory, versionRange), { 15 | retries: 3, 16 | onFailedAttempt: (error) => { 17 | logger.system('Deno download with retry failed', error) 18 | }, 19 | }) 20 | 21 | const download = async (targetDirectory: string, versionRange: string) => { 22 | const zipPath = path.join(targetDirectory, 'deno-cli-latest.zip') 23 | const data = await downloadVersion(versionRange) 24 | const binaryName = `deno${getBinaryExtension()}` 25 | const binaryPath = path.join(targetDirectory, binaryName) 26 | const file = createWriteStream(zipPath) 27 | 28 | try { 29 | await new Promise((resolve, reject) => { 30 | data.on('error', reject) 31 | file.on('finish', resolve) 32 | data.pipe(file) 33 | }) 34 | 35 | await extractBinaryFromZip(zipPath, binaryPath, binaryName) 36 | 37 | return binaryPath 38 | } finally { 39 | // Try closing and deleting the zip file in any case, error or not 40 | await promisify(file.close.bind(file))() 41 | 42 | try { 43 | await fs.unlink(zipPath) 44 | } catch { 45 | // no-op 46 | } 47 | } 48 | } 49 | 50 | const downloadVersion = async (versionRange: string) => { 51 | const version = await getLatestVersionForRange(versionRange) 52 | const url = getReleaseURL(version) 53 | const res = await fetch(url) 54 | 55 | if (res.body === null || res.status < 200 || res.status > 299) { 56 | throw new Error(`Download failed with status code ${res.status}`) 57 | } 58 | 59 | return res.body 60 | } 61 | 62 | const extractBinaryFromZip = async (zipPath: string, binaryPath: string, binaryName: string) => { 63 | const { async: StreamZipAsync } = StreamZip 64 | const zip = new StreamZipAsync({ file: zipPath }) 65 | 66 | await zip.extract(binaryName, binaryPath) 67 | await zip.close() 68 | await fs.chmod(binaryPath, '755') 69 | } 70 | 71 | const getLatestVersion = async () => { 72 | try { 73 | const response = await fetch('https://dl.deno.land/release-latest.txt') 74 | const data = await response.text() 75 | 76 | // We want to extract from the format `v`. 77 | const version = data.match(/^v?(\d+\.\d+\.\d+)/) 78 | 79 | if (version === null) { 80 | return 81 | } 82 | 83 | return version[1] 84 | } catch { 85 | // This is a no-op. If we failed to retrieve the latest version, let's 86 | // return `undefined` and let the code upstream handle it. 87 | } 88 | } 89 | 90 | const getLatestVersionForRange = async (range: string) => { 91 | const minimumVersion = semver.minVersion(range)?.version 92 | 93 | // We should never get here, because it means that `DENO_VERSION_RANGE` is 94 | // a malformed semver range. If this does happen, let's throw an error so 95 | // that the tests catch it. 96 | if (minimumVersion === undefined) { 97 | throw new Error('Incorrect version range specified by Edge Bundler') 98 | } 99 | 100 | const latestVersion = await getLatestVersion() 101 | 102 | if (latestVersion === undefined || !semver.satisfies(latestVersion, range)) { 103 | return minimumVersion 104 | } 105 | 106 | return latestVersion 107 | } 108 | 109 | const getReleaseURL = (version: string) => { 110 | const target = getPlatformTarget() 111 | 112 | return `https://dl.deno.land/release/v${version}/deno-${target}.zip` 113 | } 114 | 115 | export { downloadWithRetry as download } 116 | -------------------------------------------------------------------------------- /node/edge_function.ts: -------------------------------------------------------------------------------- 1 | export interface EdgeFunction { 2 | name: string 3 | path: string 4 | } 5 | -------------------------------------------------------------------------------- /node/feature_flags.ts: -------------------------------------------------------------------------------- 1 | const defaultFlags = {} 2 | 3 | type FeatureFlag = keyof typeof defaultFlags 4 | type FeatureFlags = Partial> 5 | 6 | const getFlags = (input: Record = {}, flags = defaultFlags): FeatureFlags => 7 | Object.entries(flags).reduce( 8 | (result, [key, defaultValue]) => ({ 9 | ...result, 10 | [key]: input[key] === undefined ? defaultValue : input[key], 11 | }), 12 | {}, 13 | ) 14 | 15 | export { defaultFlags, getFlags } 16 | export type { FeatureFlag, FeatureFlags } 17 | -------------------------------------------------------------------------------- /node/finder.test.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from 'vitest' 2 | 3 | import { removeDuplicatesByExtension } from './finder.js' 4 | 5 | test('filters out any duplicate files based on the extension', () => { 6 | const functions = [ 7 | 'file1.js', 8 | 'file1.ts', 9 | 'file2.tsx', 10 | 'file2.jsx', 11 | 'file3.tsx', 12 | 'file3.js', 13 | 'file4.ts', 14 | 'file5.ts', 15 | 'file5.tsx', 16 | ] 17 | const expected = ['file1.js', 'file2.jsx', 'file3.js', 'file4.ts', 'file5.ts'] 18 | 19 | expect(removeDuplicatesByExtension(functions)).toStrictEqual(expected) 20 | }) 21 | -------------------------------------------------------------------------------- /node/finder.ts: -------------------------------------------------------------------------------- 1 | import { promises as fs } from 'fs' 2 | import { basename, extname, join, parse } from 'path' 3 | 4 | import { EdgeFunction } from './edge_function.js' 5 | import { nonNullable } from './utils/non_nullable.js' 6 | 7 | // the order of the allowed extensions is also the order we remove duplicates 8 | // with a lower index meaning a higher precedence over the others 9 | const ALLOWED_EXTENSIONS = ['.js', '.jsx', '.mjs', '.mts', '.ts', '.tsx'] 10 | 11 | export const removeDuplicatesByExtension = (functions: string[]) => { 12 | const seen = new Map() 13 | 14 | return Object.values( 15 | functions.reduce((acc, path) => { 16 | const { ext, name } = parse(path) 17 | const extIndex = ALLOWED_EXTENSIONS.indexOf(ext) 18 | 19 | if (!seen.has(name) || seen.get(name) > extIndex) { 20 | seen.set(name, extIndex) 21 | return { ...acc, [name]: path } 22 | } 23 | 24 | return acc 25 | }, {}), 26 | ) as string[] 27 | } 28 | 29 | const findFunctionInDirectory = async (directory: string): Promise => { 30 | const name = basename(directory) 31 | const candidatePaths = ALLOWED_EXTENSIONS.flatMap((extension) => [`${name}${extension}`, `index${extension}`]).map( 32 | (filename) => join(directory, filename), 33 | ) 34 | 35 | let functionPath 36 | 37 | for (const candidatePath of candidatePaths) { 38 | try { 39 | const stats = await fs.stat(candidatePath) 40 | 41 | // eslint-disable-next-line max-depth 42 | if (stats.isFile()) { 43 | functionPath = candidatePath 44 | 45 | break 46 | } 47 | } catch { 48 | // no-op 49 | } 50 | } 51 | 52 | if (functionPath === undefined) { 53 | return 54 | } 55 | 56 | return { 57 | name, 58 | path: functionPath, 59 | } 60 | } 61 | 62 | const findFunctionInPath = async (path: string): Promise => { 63 | const stats = await fs.stat(path) 64 | 65 | if (stats.isDirectory()) { 66 | return findFunctionInDirectory(path) 67 | } 68 | 69 | const extension = extname(path) 70 | 71 | if (ALLOWED_EXTENSIONS.includes(extension)) { 72 | return { name: basename(path, extension), path } 73 | } 74 | } 75 | 76 | const findFunctionsInDirectory = async (baseDirectory: string) => { 77 | let items: string[] = [] 78 | 79 | try { 80 | items = await fs.readdir(baseDirectory).then(removeDuplicatesByExtension) 81 | } catch { 82 | // no-op 83 | } 84 | 85 | const functions = await Promise.all(items.map((item) => findFunctionInPath(join(baseDirectory, item)))) 86 | 87 | return functions.filter(nonNullable) 88 | } 89 | 90 | const findFunctions = async (directories: string[]) => { 91 | const functions = await Promise.all(directories.map(findFunctionsInDirectory)) 92 | 93 | return functions.flat() 94 | } 95 | 96 | export { findFunctions } 97 | -------------------------------------------------------------------------------- /node/formats/eszip.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'path' 2 | import { pathToFileURL } from 'url' 3 | 4 | import { virtualRoot, virtualVendorRoot } from '../../shared/consts.js' 5 | import type { WriteStage2Options } from '../../shared/stage2.js' 6 | import { DenoBridge } from '../bridge.js' 7 | import { Bundle, BundleFormat } from '../bundle.js' 8 | import { wrapBundleError } from '../bundle_error.js' 9 | import { EdgeFunction } from '../edge_function.js' 10 | import { FeatureFlags } from '../feature_flags.js' 11 | import { ImportMap } from '../import_map.js' 12 | import { wrapNpmImportError } from '../npm_import_error.js' 13 | import { getPackagePath } from '../package_json.js' 14 | import { getFileHash } from '../utils/sha256.js' 15 | 16 | interface BundleESZIPOptions { 17 | basePath: string 18 | buildID: string 19 | debug?: boolean 20 | deno: DenoBridge 21 | distDirectory: string 22 | externals: string[] 23 | featureFlags: FeatureFlags 24 | functions: EdgeFunction[] 25 | importMap: ImportMap 26 | vendorDirectory?: string 27 | } 28 | 29 | const bundleESZIP = async ({ 30 | basePath, 31 | buildID, 32 | debug, 33 | deno, 34 | distDirectory, 35 | externals, 36 | functions, 37 | importMap, 38 | vendorDirectory, 39 | }: BundleESZIPOptions): Promise => { 40 | const extension = '.eszip' 41 | const destPath = join(distDirectory, `${buildID}${extension}`) 42 | const importMapPrefixes: Record = { 43 | [`${pathToFileURL(basePath)}/`]: virtualRoot, 44 | } 45 | 46 | if (vendorDirectory !== undefined) { 47 | importMapPrefixes[`${pathToFileURL(vendorDirectory)}/`] = virtualVendorRoot 48 | } 49 | 50 | const { bundler, importMap: bundlerImportMap } = getESZIPPaths() 51 | const importMapData = JSON.stringify(importMap.getContents(importMapPrefixes)) 52 | const payload: WriteStage2Options = { 53 | basePath, 54 | destPath, 55 | externals, 56 | functions, 57 | importMapData, 58 | vendorDirectory, 59 | } 60 | const flags = ['--allow-all', '--no-config', `--import-map=${bundlerImportMap}`] 61 | 62 | if (!debug) { 63 | flags.push('--quiet') 64 | } 65 | 66 | try { 67 | await deno.run(['run', ...flags, bundler, JSON.stringify(payload)], { pipeOutput: true }) 68 | } catch (error: unknown) { 69 | throw wrapBundleError(wrapNpmImportError(error), { 70 | format: 'eszip', 71 | }) 72 | } 73 | 74 | const hash = await getFileHash(destPath) 75 | 76 | return { extension, format: BundleFormat.ESZIP2, hash } 77 | } 78 | 79 | const getESZIPPaths = () => { 80 | const denoPath = join(getPackagePath(), 'deno') 81 | 82 | return { 83 | bundler: join(denoPath, 'bundle.ts'), 84 | importMap: join(denoPath, 'vendor', 'import_map.json'), 85 | } 86 | } 87 | 88 | export { bundleESZIP as bundle } 89 | -------------------------------------------------------------------------------- /node/formats/javascript.ts: -------------------------------------------------------------------------------- 1 | import { mkdir, writeFile } from 'fs/promises' 2 | import { join } from 'path' 3 | import { pathToFileURL } from 'url' 4 | 5 | import { EdgeFunction } from '../edge_function.js' 6 | import type { FormatFunction } from '../server/server.js' 7 | 8 | const defaultFormatExportTypeError: FormatFunction = (name) => 9 | `The Edge Function "${name}" has failed to load. Does it have a function as the default export?` 10 | 11 | const defaultFormatImportError: FormatFunction = (name) => `There was an error with Edge Function "${name}".` 12 | 13 | interface GenerateStage2Options { 14 | bootstrapURL: string 15 | distDirectory: string 16 | fileName: string 17 | formatExportTypeError?: FormatFunction 18 | formatImportError?: FormatFunction 19 | functions: EdgeFunction[] 20 | } 21 | 22 | const generateStage2 = async ({ 23 | bootstrapURL, 24 | distDirectory, 25 | fileName, 26 | formatExportTypeError, 27 | formatImportError, 28 | functions, 29 | }: GenerateStage2Options) => { 30 | await mkdir(distDirectory, { recursive: true }) 31 | 32 | const entryPoint = getLocalEntryPoint(functions, { bootstrapURL, formatExportTypeError, formatImportError }) 33 | const stage2Path = join(distDirectory, fileName) 34 | 35 | await writeFile(stage2Path, entryPoint) 36 | 37 | return stage2Path 38 | } 39 | 40 | interface GetLocalEntryPointOptions { 41 | bootstrapURL: string 42 | formatExportTypeError?: FormatFunction 43 | formatImportError?: FormatFunction 44 | } 45 | 46 | // For the local development environment, we import the user functions with 47 | // dynamic imports to gracefully handle the case where the file doesn't have 48 | // a valid default export. 49 | const getLocalEntryPoint = ( 50 | functions: EdgeFunction[], 51 | { 52 | bootstrapURL, 53 | formatExportTypeError = defaultFormatExportTypeError, 54 | formatImportError = defaultFormatImportError, 55 | }: GetLocalEntryPointOptions, 56 | ) => { 57 | const bootImport = `import { boot } from "${bootstrapURL}";` 58 | const declaration = `const functions = {}; const metadata = { functions: {} };` 59 | const imports = functions.map((func) => { 60 | const url = pathToFileURL(func.path) 61 | const metadata = { 62 | url, 63 | } 64 | 65 | return ` 66 | try { 67 | const { default: func } = await import("${url}"); 68 | 69 | if (typeof func === "function") { 70 | functions["${func.name}"] = func; 71 | metadata.functions["${func.name}"] = ${JSON.stringify(metadata)} 72 | } else { 73 | console.log(${JSON.stringify(formatExportTypeError(func.name))}); 74 | } 75 | } catch (error) { 76 | console.log(${JSON.stringify(formatImportError(func.name))}); 77 | console.error(error); 78 | } 79 | ` 80 | }) 81 | const bootCall = `boot(functions, metadata);` 82 | 83 | return [bootImport, declaration, ...imports, bootCall].join('\n\n') 84 | } 85 | 86 | export { generateStage2, getLocalEntryPoint } 87 | -------------------------------------------------------------------------------- /node/home_path.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'path' 2 | 3 | import envPaths from 'env-paths' 4 | 5 | const OSBasedPaths = envPaths('netlify', { suffix: '' }) 6 | 7 | const getPathInHome = (path: string) => join(OSBasedPaths.config, path) 8 | 9 | export { getPathInHome } 10 | -------------------------------------------------------------------------------- /node/index.ts: -------------------------------------------------------------------------------- 1 | export { bundle } from './bundler.js' 2 | export { DenoBridge } from './bridge.js' 3 | export type { FunctionConfig } from './config.js' 4 | export { Declaration, mergeDeclarations } from './declaration.js' 5 | export type { EdgeFunction } from './edge_function.js' 6 | export { findFunctions as find } from './finder.js' 7 | export { generateManifest } from './manifest.js' 8 | export type { EdgeFunctionConfig, Manifest } from './manifest.js' 9 | export type { ModuleGraphJson as ModuleGraph } from './vendor/module_graph/module_graph.js' 10 | export { serve } from './server/server.js' 11 | export { validateManifest, ManifestValidationError } from './validation/manifest/index.js' 12 | -------------------------------------------------------------------------------- /node/layer.ts: -------------------------------------------------------------------------------- 1 | export interface Layer { 2 | flag: string 3 | name: string 4 | } 5 | -------------------------------------------------------------------------------- /node/logger.test.ts: -------------------------------------------------------------------------------- 1 | import { afterEach, test, expect, vi } from 'vitest' 2 | 3 | import { getLogger } from './logger.js' 4 | 5 | const consoleLog = console.log 6 | 7 | const noopLogger = () => { 8 | // no-op 9 | } 10 | 11 | afterEach(() => { 12 | // Restoring global `console.log`. 13 | console.log = consoleLog 14 | }) 15 | 16 | test('Prints user logs to stdout if no user logger is provided', () => { 17 | const mockConsoleLog = vi.fn() 18 | console.log = mockConsoleLog 19 | 20 | const logger1 = getLogger(noopLogger, undefined, true) 21 | const logger2 = getLogger(noopLogger, undefined, false) 22 | 23 | logger1.user('Hello with `debug: true`') 24 | logger2.user('Hello with `debug: false`') 25 | 26 | expect(mockConsoleLog).toHaveBeenCalledTimes(2) 27 | expect(mockConsoleLog).toHaveBeenNthCalledWith(1, 'Hello with `debug: true`') 28 | expect(mockConsoleLog).toHaveBeenNthCalledWith(2, 'Hello with `debug: false`') 29 | }) 30 | 31 | test('Prints user logs to user logger provided', () => { 32 | const userLogger = vi.fn() 33 | const logger = getLogger(noopLogger, userLogger, true) 34 | 35 | logger.user('Hello!') 36 | 37 | expect(userLogger).toHaveBeenCalledTimes(1) 38 | expect(userLogger).toHaveBeenNthCalledWith(1, 'Hello!') 39 | }) 40 | 41 | test('Prints system logs to the system logger provided', () => { 42 | const mockSystemLog = vi.fn() 43 | const mockConsoleLog = vi.fn() 44 | console.log = mockSystemLog 45 | 46 | const logger1 = getLogger(mockSystemLog, undefined, true) 47 | const logger2 = getLogger(mockSystemLog, undefined, false) 48 | 49 | logger1.system('Hello with `debug: true`') 50 | logger2.system('Hello with `debug: false`') 51 | 52 | expect(mockConsoleLog).toHaveBeenCalledTimes(0) 53 | expect(mockSystemLog).toHaveBeenCalledTimes(2) 54 | expect(mockSystemLog).toHaveBeenNthCalledWith(1, 'Hello with `debug: true`') 55 | expect(mockSystemLog).toHaveBeenNthCalledWith(2, 'Hello with `debug: false`') 56 | }) 57 | 58 | test('Prints system logs to stdout if there is no system logger provided and `debug` is enabled', () => { 59 | const mockConsoleLog = vi.fn() 60 | console.log = mockConsoleLog 61 | 62 | const logger1 = getLogger(undefined, undefined, true) 63 | const logger2 = getLogger(undefined, undefined, false) 64 | 65 | logger1.system('Hello with `debug: true`') 66 | logger2.system('Hello with `debug: false`') 67 | 68 | expect(mockConsoleLog).toHaveBeenCalledTimes(1) 69 | expect(mockConsoleLog).toHaveBeenNthCalledWith(1, 'Hello with `debug: true`') 70 | }) 71 | -------------------------------------------------------------------------------- /node/logger.ts: -------------------------------------------------------------------------------- 1 | type LogFunction = (...args: unknown[]) => void 2 | 3 | const noopLogger: LogFunction = () => { 4 | // no-op 5 | } 6 | 7 | interface Logger { 8 | system: LogFunction 9 | user: LogFunction 10 | } 11 | 12 | const getLogger = (systemLogger?: LogFunction, userLogger?: LogFunction, debug = false): Logger => { 13 | // If there is a system logger configured, we'll use that. If there isn't, 14 | // we'll pipe system logs to stdout if `debug` is enabled and swallow them 15 | // otherwise. 16 | const system = systemLogger ?? (debug ? console.log : noopLogger) 17 | const user = userLogger ?? console.log 18 | 19 | return { 20 | system, 21 | user, 22 | } 23 | } 24 | 25 | export { getLogger } 26 | export type { LogFunction, Logger } 27 | -------------------------------------------------------------------------------- /node/main.test.ts: -------------------------------------------------------------------------------- 1 | import { Buffer } from 'buffer' 2 | import { rm } from 'fs/promises' 3 | import { createRequire } from 'module' 4 | import { platform } from 'process' 5 | import { PassThrough } from 'stream' 6 | 7 | import nock from 'nock' 8 | import semver from 'semver' 9 | import tmp from 'tmp-promise' 10 | import { test, expect, vi } from 'vitest' 11 | 12 | import { DenoBridge, DENO_VERSION_RANGE } from './bridge.js' 13 | import { getPlatformTarget } from './platform.js' 14 | 15 | const require = createRequire(import.meta.url) 16 | const archiver = require('archiver') 17 | 18 | test('Downloads the Deno CLI on demand and caches it for subsequent calls', async () => { 19 | const latestVersion = semver.minVersion(DENO_VERSION_RANGE)?.version ?? '' 20 | const mockBinaryOutput = `#!/usr/bin/env sh\n\necho "deno ${latestVersion}"` 21 | const data = new PassThrough() 22 | const archive = archiver('zip', { zlib: { level: 9 } }) 23 | 24 | archive.pipe(data) 25 | archive.append(Buffer.from(mockBinaryOutput), { name: platform === 'win32' ? 'deno.exe' : 'deno' }) 26 | archive.finalize() 27 | 28 | const target = getPlatformTarget() 29 | const latestReleaseMock = nock('https://dl.deno.land').get('/release-latest.txt').reply(200, `v${latestVersion}`) 30 | const downloadMock = nock('https://dl.deno.land') 31 | .get(`/release/v${latestVersion}/deno-${target}.zip`) 32 | .reply(200, () => data) 33 | 34 | const tmpDir = await tmp.dir() 35 | const beforeDownload = vi.fn() 36 | const afterDownload = vi.fn() 37 | const deno = new DenoBridge({ 38 | cacheDirectory: tmpDir.path, 39 | onBeforeDownload: beforeDownload, 40 | onAfterDownload: afterDownload, 41 | useGlobal: false, 42 | }) 43 | const output1 = await deno.run(['help']) 44 | const output2 = await deno.run(['help']) 45 | const expectedOutput = /^deno [\d.]+/ 46 | 47 | expect(latestReleaseMock.isDone()).toBe(true) 48 | expect(downloadMock.isDone()).toBe(true) 49 | expect(output1?.stdout ?? '').toMatch(expectedOutput) 50 | expect(output2?.stdout ?? '').toMatch(expectedOutput) 51 | expect(beforeDownload).toHaveBeenCalledTimes(1) 52 | expect(afterDownload).toHaveBeenCalledTimes(1) 53 | 54 | await rm(tmpDir.path, { force: true, recursive: true, maxRetries: 10 }) 55 | }) 56 | -------------------------------------------------------------------------------- /node/npm_import_error.ts: -------------------------------------------------------------------------------- 1 | class NPMImportError extends Error { 2 | constructor(originalError: Error, moduleName: string) { 3 | super( 4 | `There was an error when loading the '${moduleName}' npm module. Support for npm modules in edge functions is an experimental feature. Refer to https://ntl.fyi/edge-functions-npm for more information.`, 5 | ) 6 | 7 | this.name = 'NPMImportError' 8 | this.stack = originalError.stack 9 | 10 | // https://github.com/microsoft/TypeScript-wiki/blob/8a66ecaf77118de456f7cd9c56848a40fe29b9b4/Breaking-Changes.md#implicit-any-error-raised-for-un-annotated-callback-arguments-with-no-matching-overload-arguments 11 | Object.setPrototypeOf(this, NPMImportError.prototype) 12 | } 13 | } 14 | 15 | const wrapNpmImportError = (input: unknown) => { 16 | if (input instanceof Error) { 17 | const match = input.message.match(/Relative import path "(.*)" not prefixed with/) 18 | if (match !== null) { 19 | const [, moduleName] = match 20 | return new NPMImportError(input, moduleName) 21 | } 22 | 23 | const schemeMatch = input.message.match(/Error: Module not found "npm:(.*)"/) 24 | if (schemeMatch !== null) { 25 | const [, moduleName] = schemeMatch 26 | return new NPMImportError(input, moduleName) 27 | } 28 | } 29 | 30 | return input 31 | } 32 | 33 | export { NPMImportError, wrapNpmImportError } 34 | -------------------------------------------------------------------------------- /node/package_json.test.ts: -------------------------------------------------------------------------------- 1 | import semver from 'semver' 2 | import { test, expect } from 'vitest' 3 | 4 | import { getPackageVersion } from './package_json.js' 5 | 6 | test('`getPackageVersion` returns the package version`', () => { 7 | const version = getPackageVersion() 8 | 9 | expect(semver.valid(version)).not.toBeNull() 10 | }) 11 | -------------------------------------------------------------------------------- /node/package_json.ts: -------------------------------------------------------------------------------- 1 | import { readFileSync } from 'fs' 2 | import { join } from 'path' 3 | import { fileURLToPath } from 'url' 4 | 5 | import { findUpSync, pathExistsSync } from 'find-up' 6 | 7 | const getPackagePath = () => { 8 | const packagePath = findUpSync( 9 | (directory: string) => { 10 | if (pathExistsSync(join(directory, 'package.json'))) { 11 | return directory 12 | } 13 | }, 14 | { cwd: fileURLToPath(import.meta.url), type: 'directory' }, 15 | ) 16 | 17 | // We should never get here, but let's show a somewhat useful error message. 18 | if (packagePath === undefined) { 19 | throw new Error( 20 | 'Could not find `package.json` for `@netlify/edge-bundler`. Please try running `npm install` to reinstall your dependencies.', 21 | ) 22 | } 23 | 24 | return packagePath 25 | } 26 | 27 | const getPackageVersion = () => { 28 | const packagePath = getPackagePath() 29 | 30 | try { 31 | const packageJSON = readFileSync(join(packagePath, 'package.json'), 'utf8') 32 | const { version } = JSON.parse(packageJSON) 33 | 34 | return version as string 35 | } catch { 36 | return '' 37 | } 38 | } 39 | 40 | export { getPackagePath, getPackageVersion } 41 | -------------------------------------------------------------------------------- /node/platform.ts: -------------------------------------------------------------------------------- 1 | import { arch, platform } from 'process' 2 | 3 | const getBinaryExtension = () => (platform === 'win32' ? '.exe' : '') 4 | 5 | const getPlatformTarget = () => { 6 | if (platform === 'win32') { 7 | return 'x86_64-pc-windows-msvc' 8 | } 9 | 10 | const isArm64 = arch === 'arm64' 11 | 12 | if (platform === 'darwin') { 13 | return isArm64 ? 'aarch64-apple-darwin' : 'x86_64-apple-darwin' 14 | } 15 | 16 | return 'x86_64-unknown-linux-gnu' 17 | } 18 | 19 | export { getBinaryExtension, getPlatformTarget } 20 | -------------------------------------------------------------------------------- /node/rate_limit.ts: -------------------------------------------------------------------------------- 1 | export enum RateLimitAlgorithm { 2 | SlidingWindow = 'sliding_window', 3 | } 4 | 5 | export enum RateLimitAggregator { 6 | Domain = 'domain', 7 | IP = 'ip', 8 | } 9 | 10 | export enum RateLimitAction { 11 | Limit = 'rate_limit', 12 | Rewrite = 'rewrite', 13 | } 14 | 15 | interface SlidingWindow { 16 | windowLimit: number 17 | windowSize: number 18 | } 19 | 20 | export type RewriteActionConfig = SlidingWindow & { 21 | to: string 22 | } 23 | 24 | interface RateLimitConfig { 25 | action?: RateLimitAction 26 | aggregateBy?: RateLimitAggregator | RateLimitAggregator[] 27 | algorithm?: RateLimitAlgorithm 28 | } 29 | 30 | export type RateLimit = RateLimitConfig & (SlidingWindow | RewriteActionConfig) 31 | -------------------------------------------------------------------------------- /node/server/server.test.ts: -------------------------------------------------------------------------------- 1 | import { createWriteStream } from 'fs' 2 | import { readFile } from 'fs/promises' 3 | import { join } from 'path' 4 | import process from 'process' 5 | 6 | import getPort from 'get-port' 7 | import fetch from 'node-fetch' 8 | import tmp from 'tmp-promise' 9 | import { v4 as uuidv4 } from 'uuid' 10 | import { test, expect } from 'vitest' 11 | 12 | import { fixturesDir } from '../../test/util.js' 13 | import { serve } from '../index.js' 14 | 15 | test('Starts a server and serves requests for edge functions', async () => { 16 | const basePath = join(fixturesDir, 'serve_test') 17 | const paths = { 18 | internal: join(basePath, '.netlify', 'edge-functions'), 19 | user: join(basePath, 'netlify', 'edge-functions'), 20 | } 21 | const port = await getPort() 22 | const importMapPaths = [join(paths.internal, 'import_map.json'), join(paths.user, 'import-map.json')] 23 | const servePath = join(basePath, '.netlify', 'edge-functions-serve') 24 | const server = await serve({ 25 | basePath, 26 | bootstrapURL: 'https://edge.netlify.com/bootstrap/index-combined.ts', 27 | port, 28 | servePath, 29 | }) 30 | 31 | const functions = [ 32 | { 33 | name: 'echo_env', 34 | path: join(paths.user, 'echo_env.ts'), 35 | }, 36 | { 37 | name: 'greet', 38 | path: join(paths.internal, 'greet.ts'), 39 | }, 40 | { 41 | name: 'global_netlify', 42 | path: join(paths.user, 'global_netlify.ts'), 43 | }, 44 | ] 45 | const options = { 46 | getFunctionsConfig: true, 47 | importMapPaths, 48 | } 49 | 50 | const { features, functionsConfig, graph, success, npmSpecifiersWithExtraneousFiles } = await server( 51 | functions, 52 | { 53 | very_secret_secret: 'i love netlify', 54 | }, 55 | options, 56 | ) 57 | expect(features).toEqual({ npmModules: true }) 58 | expect(success).toBe(true) 59 | expect(functionsConfig).toEqual([{ path: '/my-function' }, {}, { path: '/global-netlify' }]) 60 | expect(npmSpecifiersWithExtraneousFiles).toEqual(['dictionary']) 61 | 62 | for (const key in functions) { 63 | const graphEntry = graph?.modules.some( 64 | ({ kind, mediaType, local }) => kind === 'esm' && mediaType === 'TypeScript' && local === functions[key].path, 65 | ) 66 | 67 | expect(graphEntry).toBe(true) 68 | } 69 | 70 | const response1 = await fetch(`http://0.0.0.0:${port}/foo`, { 71 | headers: { 72 | 'x-nf-edge-functions': 'echo_env', 73 | 'x-ef-passthrough': 'passthrough', 74 | 'X-NF-Request-ID': uuidv4(), 75 | }, 76 | }) 77 | expect(response1.status).toBe(200) 78 | expect(await response1.text()).toBe('I LOVE NETLIFY') 79 | 80 | const response2 = await fetch(`http://0.0.0.0:${port}/greet`, { 81 | headers: { 82 | 'x-nf-edge-functions': 'greet', 83 | 'x-ef-passthrough': 'passthrough', 84 | 'X-NF-Request-ID': uuidv4(), 85 | }, 86 | }) 87 | expect(response2.status).toBe(200) 88 | expect(await response2.text()).toBe('HELLO!') 89 | 90 | const response3 = await fetch(`http://0.0.0.0:${port}/global-netlify`, { 91 | headers: { 92 | 'x-nf-edge-functions': 'global_netlify', 93 | 'x-ef-passthrough': 'passthrough', 94 | 'X-NF-Request-ID': uuidv4(), 95 | }, 96 | }) 97 | expect(await response3.json()).toEqual({ 98 | global: 'i love netlify', 99 | local: 'i love netlify', 100 | }) 101 | 102 | const idBarrelFile = await readFile(join(servePath, 'bundled-id.js'), 'utf-8') 103 | expect(idBarrelFile).toContain(`/// `) 104 | 105 | const identidadeBarrelFile = await readFile(join(servePath, 'bundled-pt-committee__identidade.js'), 'utf-8') 106 | expect(identidadeBarrelFile).toContain( 107 | `/// `, 108 | ) 109 | }) 110 | 111 | test('Serves edge functions in a monorepo setup', async () => { 112 | const tmpFile = await tmp.file() 113 | const stderr = createWriteStream(tmpFile.path) 114 | 115 | const rootPath = join(fixturesDir, 'monorepo_npm_module') 116 | const basePath = join(rootPath, 'packages', 'frontend') 117 | const paths = { 118 | user: join(basePath, 'functions'), 119 | } 120 | const port = await getPort() 121 | const importMapPaths = [join(basePath, 'import_map.json')] 122 | const servePath = join(basePath, '.netlify', 'edge-functions-serve') 123 | const server = await serve({ 124 | basePath, 125 | bootstrapURL: 'https://edge.netlify.com/bootstrap/index-combined.ts', 126 | port, 127 | rootPath, 128 | servePath, 129 | stderr, 130 | }) 131 | 132 | const functions = [ 133 | { 134 | name: 'func1', 135 | path: join(paths.user, 'func1.ts'), 136 | }, 137 | ] 138 | const options = { 139 | getFunctionsConfig: true, 140 | importMapPaths, 141 | } 142 | 143 | const { features, functionsConfig, graph, success, npmSpecifiersWithExtraneousFiles } = await server( 144 | functions, 145 | { 146 | very_secret_secret: 'i love netlify', 147 | }, 148 | options, 149 | ) 150 | 151 | expect(features).toEqual({ npmModules: true }) 152 | expect(success).toBe(true) 153 | expect(functionsConfig).toEqual([{ path: '/func1' }]) 154 | expect(npmSpecifiersWithExtraneousFiles).toEqual(['child-1']) 155 | 156 | for (const key in functions) { 157 | const graphEntry = graph?.modules.some( 158 | ({ kind, mediaType, local }) => kind === 'esm' && mediaType === 'TypeScript' && local === functions[key].path, 159 | ) 160 | 161 | expect(graphEntry).toBe(true) 162 | } 163 | 164 | const response1 = await fetch(`http://0.0.0.0:${port}/func1`, { 165 | headers: { 166 | 'x-nf-edge-functions': 'func1', 167 | 'x-ef-passthrough': 'passthrough', 168 | 'X-NF-Request-ID': uuidv4(), 169 | }, 170 | }) 171 | 172 | expect(response1.status).toBe(200) 173 | expect(await response1.text()).toBe( 174 | `JavaScript, APIs${process.cwd()}, Markup${process.cwd()}`, 175 | ) 176 | 177 | expect(await readFile(tmpFile.path, 'utf8')).toContain('[func1] Something is on fire') 178 | 179 | await tmpFile.cleanup() 180 | }) 181 | -------------------------------------------------------------------------------- /node/server/util.ts: -------------------------------------------------------------------------------- 1 | import { ExecaChildProcess } from 'execa' 2 | import fetch from 'node-fetch' 3 | import waitFor from 'p-wait-for' 4 | 5 | // 1 second 6 | const SERVER_KILL_TIMEOUT = 1e3 7 | 8 | // 1 second 9 | const SERVER_POLL_INTERNAL = 1e3 10 | 11 | // 10 seconds 12 | const SERVER_POLL_TIMEOUT = 1e4 13 | 14 | interface SuccessRef { 15 | success: boolean 16 | } 17 | 18 | const isServerReady = async (port: number, successRef: SuccessRef, ps?: ExecaChildProcess) => { 19 | // If the process has been killed or if it exited with an error, we return 20 | // early with `success: false`. 21 | if (ps?.killed || (ps?.exitCode && ps.exitCode > 0)) { 22 | return true 23 | } 24 | 25 | try { 26 | await fetch(`http://127.0.0.1:${port}`) 27 | 28 | // eslint-disable-next-line no-param-reassign 29 | successRef.success = true 30 | } catch { 31 | return false 32 | } 33 | 34 | return true 35 | } 36 | 37 | const killProcess = (ps: ExecaChildProcess) => { 38 | // If the process is no longer running, there's nothing left to do. 39 | if (ps?.exitCode !== null) { 40 | return 41 | } 42 | 43 | return new Promise((resolve, reject) => { 44 | ps.on('close', resolve) 45 | ps.on('error', reject) 46 | 47 | ps.kill('SIGTERM', { 48 | forceKillAfterTimeout: SERVER_KILL_TIMEOUT, 49 | }) 50 | }) 51 | } 52 | 53 | const waitForServer = async (port: number, ps?: ExecaChildProcess) => { 54 | const successRef: SuccessRef = { 55 | success: false, 56 | } 57 | 58 | await waitFor(() => isServerReady(port, successRef, ps), { 59 | interval: SERVER_POLL_INTERNAL, 60 | timeout: SERVER_POLL_TIMEOUT, 61 | }) 62 | 63 | return successRef.success 64 | } 65 | 66 | export { killProcess, waitForServer } 67 | -------------------------------------------------------------------------------- /node/stage_2.test.ts: -------------------------------------------------------------------------------- 1 | import { rm, writeFile } from 'fs/promises' 2 | import { join } from 'path' 3 | import { pathToFileURL } from 'url' 4 | 5 | import { execa } from 'execa' 6 | import tmp from 'tmp-promise' 7 | import { test, expect } from 'vitest' 8 | 9 | import { getLocalEntryPoint } from './formats/javascript.js' 10 | 11 | test('`getLocalEntryPoint` returns a valid stage 2 file for local development', async () => { 12 | const { path: tmpDir } = await tmp.dir() 13 | 14 | // This is a fake bootstrap that we'll create just for the purpose of logging 15 | // the functions and the metadata that are sent to the `boot` function. 16 | const printer = ` 17 | export const boot = async (functions, metadata) => { 18 | const responses = {} 19 | 20 | for (const name in functions) { 21 | responses[name] = await functions[name]() 22 | } 23 | 24 | console.log(JSON.stringify({ responses, metadata })) 25 | } 26 | ` 27 | const printerPath = join(tmpDir, 'printer.mjs') 28 | const bootstrapURL = pathToFileURL(printerPath).toString() 29 | 30 | await writeFile(printerPath, printer) 31 | 32 | const functions = [ 33 | { name: 'func1', path: join(tmpDir, 'func1.mjs'), response: 'Hello from function 1' }, 34 | { name: 'func2', path: join(tmpDir, 'func2.mjs'), response: 'Hello from function 2' }, 35 | ] 36 | 37 | for (const func of functions) { 38 | const contents = `export default () => ${JSON.stringify(func.response)}` 39 | 40 | await writeFile(func.path, contents) 41 | } 42 | 43 | const stage2 = getLocalEntryPoint( 44 | functions.map(({ name, path }) => ({ name, path })), 45 | { bootstrapURL }, 46 | ) 47 | const stage2Path = join(tmpDir, 'stage2.mjs') 48 | 49 | await writeFile(stage2Path, stage2) 50 | 51 | const { stdout, stderr } = await execa('deno', ['run', '--allow-all', stage2Path]) 52 | 53 | expect(stderr).toBe('') 54 | 55 | const { metadata, responses } = JSON.parse(stdout) 56 | 57 | for (const func of functions) { 58 | expect(responses[func.name]).toBe(func.response) 59 | expect(metadata.functions[func.name].url).toBe(pathToFileURL(func.path).toString()) 60 | } 61 | 62 | await rm(tmpDir, { force: true, recursive: true }) 63 | }) 64 | -------------------------------------------------------------------------------- /node/types.test.ts: -------------------------------------------------------------------------------- 1 | import { readFile, rm, writeFile } from 'fs/promises' 2 | import { join } from 'path' 3 | 4 | import nock from 'nock' 5 | import tmp from 'tmp-promise' 6 | import { test, expect, vi } from 'vitest' 7 | 8 | import { testLogger } from '../test/util.js' 9 | 10 | import { DenoBridge } from './bridge.js' 11 | import { ensureLatestTypes } from './types.js' 12 | 13 | test('`ensureLatestTypes` updates the Deno CLI cache if the local version of types is outdated', async () => { 14 | const mockURL = 'https://edge.netlify' 15 | const mockVersion = '123456789' 16 | const latestVersionMock = nock(mockURL).get('/version.txt').reply(200, mockVersion) 17 | 18 | const tmpDir = await tmp.dir() 19 | const deno = new DenoBridge({ 20 | cacheDirectory: tmpDir.path, 21 | logger: testLogger, 22 | }) 23 | 24 | // @ts-expect-error return value not used 25 | const mock = vi.spyOn(deno, 'run').mockResolvedValue({}) 26 | 27 | await ensureLatestTypes(deno, testLogger, mockURL) 28 | 29 | const versionFile = await readFile(join(tmpDir.path, 'types-version.txt'), 'utf8') 30 | 31 | expect(latestVersionMock.isDone()).toBe(true) 32 | expect(mock).toHaveBeenCalledTimes(1) 33 | expect(mock).toHaveBeenCalledWith(['cache', '-r', mockURL]) 34 | expect(versionFile).toBe(mockVersion) 35 | 36 | mock.mockRestore() 37 | 38 | await rm(tmpDir.path, { force: true, recursive: true, maxRetries: 10 }) 39 | }) 40 | 41 | test('`ensureLatestTypes` does not update the Deno CLI cache if the local version of types is up-to-date', async () => { 42 | const mockURL = 'https://edge.netlify' 43 | const mockVersion = '987654321' 44 | 45 | const tmpDir = await tmp.dir() 46 | const versionFilePath = join(tmpDir.path, 'types-version.txt') 47 | 48 | await writeFile(versionFilePath, mockVersion) 49 | 50 | const latestVersionMock = nock(mockURL).get('/version.txt').reply(200, mockVersion) 51 | const deno = new DenoBridge({ 52 | cacheDirectory: tmpDir.path, 53 | logger: testLogger, 54 | }) 55 | 56 | // @ts-expect-error return value not used 57 | const mock = vi.spyOn(deno, 'run').mockResolvedValue({}) 58 | 59 | await ensureLatestTypes(deno, testLogger, mockURL) 60 | 61 | expect(latestVersionMock.isDone()).toBe(true) 62 | expect(mock).not.toHaveBeenCalled() 63 | 64 | mock.mockRestore() 65 | 66 | await rm(tmpDir.path, { force: true, recursive: true, maxRetries: 10 }) 67 | }) 68 | 69 | test('`ensureLatestTypes` does not throw if the types URL is not available', async () => { 70 | const mockURL = 'https://edge.netlify' 71 | const latestVersionMock = nock(mockURL).get('/version.txt').reply(500) 72 | 73 | const tmpDir = await tmp.dir() 74 | const deno = new DenoBridge({ 75 | cacheDirectory: tmpDir.path, 76 | logger: testLogger, 77 | }) 78 | 79 | // @ts-expect-error return value not used 80 | const mock = vi.spyOn(deno, 'run').mockResolvedValue({}) 81 | 82 | await ensureLatestTypes(deno, testLogger, mockURL) 83 | 84 | expect(latestVersionMock.isDone()).toBe(true) 85 | expect(mock).not.toHaveBeenCalled() 86 | 87 | mock.mockRestore() 88 | 89 | await rm(tmpDir.path, { force: true, recursive: true, maxRetries: 10 }) 90 | }) 91 | -------------------------------------------------------------------------------- /node/types.ts: -------------------------------------------------------------------------------- 1 | import { promises as fs } from 'fs' 2 | import { join } from 'path' 3 | 4 | import fetch from 'node-fetch' 5 | 6 | import type { DenoBridge } from './bridge.js' 7 | import type { Logger } from './logger.js' 8 | 9 | const TYPES_URL = 'https://edge.netlify.com' 10 | 11 | const ensureLatestTypes = async (deno: DenoBridge, logger: Logger, customTypesURL?: string) => { 12 | const typesURL = customTypesURL ?? TYPES_URL 13 | 14 | let [localVersion, remoteVersion] = [await getLocalVersion(deno), ''] 15 | 16 | try { 17 | remoteVersion = await getRemoteVersion(typesURL) 18 | } catch (error) { 19 | logger.system('Could not check latest version of types:', error) 20 | 21 | return 22 | } 23 | 24 | if (localVersion === remoteVersion) { 25 | logger.system('Local version of types is up-to-date:', localVersion) 26 | 27 | return 28 | } 29 | 30 | logger.system('Local version of types is outdated, updating:', localVersion) 31 | 32 | try { 33 | await deno.run(['cache', '-r', typesURL]) 34 | } catch (error) { 35 | logger.system('Could not download latest types:', error) 36 | 37 | return 38 | } 39 | 40 | try { 41 | await writeVersionFile(deno, remoteVersion) 42 | } catch { 43 | // no-op 44 | } 45 | } 46 | 47 | const getLocalVersion = async (deno: DenoBridge) => { 48 | const versionFilePath = join(deno.cacheDirectory, 'types-version.txt') 49 | 50 | try { 51 | const version = await fs.readFile(versionFilePath, 'utf8') 52 | 53 | return version 54 | } catch { 55 | // no-op 56 | } 57 | } 58 | 59 | const getRemoteVersion = async (typesURL: string) => { 60 | const versionURL = new URL('/version.txt', typesURL) 61 | const res = await fetch(versionURL.toString()) 62 | 63 | if (res.status !== 200) { 64 | throw new Error('Unexpected status code from version endpoint') 65 | } 66 | 67 | const version = await res.text() 68 | 69 | return version 70 | } 71 | 72 | const writeVersionFile = async (deno: DenoBridge, version: string) => { 73 | await deno.ensureCacheDirectory() 74 | 75 | const versionFilePath = join(deno.cacheDirectory, 'types-version.txt') 76 | 77 | await fs.writeFile(versionFilePath, version) 78 | } 79 | 80 | export { ensureLatestTypes } 81 | -------------------------------------------------------------------------------- /node/utils/error.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 2 | export const isNodeError = (error: any): error is NodeJS.ErrnoException => error instanceof Error 3 | 4 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 5 | export const isFileNotFoundError = (error: any) => isNodeError(error) && error.code === 'ENOENT' 6 | -------------------------------------------------------------------------------- /node/utils/fs.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | 3 | /** 4 | * Returns all the directories obtained by traversing `inner` and its parents 5 | * all the way to `outer`, inclusive. 6 | */ 7 | export const pathsBetween = (inner: string, outer: string, paths: string[] = []): string[] => { 8 | const parent = path.dirname(inner) 9 | 10 | if (inner === outer || inner === parent) { 11 | return [...paths, outer] 12 | } 13 | 14 | return [inner, ...pathsBetween(parent, outer)] 15 | } 16 | -------------------------------------------------------------------------------- /node/utils/non_nullable.ts: -------------------------------------------------------------------------------- 1 | const nonNullable = (value: T): value is NonNullable => value !== null && value !== undefined 2 | 3 | export { nonNullable } 4 | -------------------------------------------------------------------------------- /node/utils/sha256.ts: -------------------------------------------------------------------------------- 1 | import crypto from 'crypto' 2 | import fs from 'fs' 3 | 4 | const getFileHash = (path: string): Promise => { 5 | const hash = crypto.createHash('sha256') 6 | 7 | hash.setEncoding('hex') 8 | 9 | return new Promise((resolve, reject) => { 10 | const file = fs.createReadStream(path) 11 | 12 | file.on('end', () => { 13 | hash.end() 14 | 15 | resolve(hash.read()) 16 | }) 17 | file.on('error', reject) 18 | 19 | file.pipe(hash) 20 | }) 21 | } 22 | 23 | export { getFileHash } 24 | -------------------------------------------------------------------------------- /node/utils/urlpattern.ts: -------------------------------------------------------------------------------- 1 | import { URLPattern } from 'urlpattern-polyfill' 2 | 3 | export class ExtendedURLPattern extends URLPattern { 4 | // @ts-expect-error Internal property that the underlying class is using but 5 | // not exposing. 6 | regexp: Record 7 | } 8 | -------------------------------------------------------------------------------- /node/validation/manifest/error.ts: -------------------------------------------------------------------------------- 1 | export default class ManifestValidationError extends Error { 2 | customErrorInfo = { 3 | type: 'functionsBundling', 4 | } 5 | 6 | constructor(message: string | undefined) { 7 | super(`Validation of Edge Functions manifest failed\n${message}`) 8 | 9 | this.name = 'ManifestValidationError' 10 | 11 | // https://github.com/microsoft/TypeScript-wiki/blob/0fecbda7263f130c57394d779b8ca13f0a2e9123/Breaking-Changes.md#extending-built-ins-like-error-array-and-map-may-no-longer-work 12 | Object.setPrototypeOf(this, ManifestValidationError.prototype) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /node/validation/manifest/index.test.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk' 2 | import { test, expect, describe } from 'vitest' 3 | 4 | import { validateManifest, ManifestValidationError } from './index.js' 5 | 6 | // We need to disable all color outputs for the tests as they are different on different platforms, CI, etc. 7 | // This only works if this is the same instance of chalk that better-ajv-errors uses 8 | chalk.level = 0 9 | 10 | // Factory so we have a new object per test 11 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 12 | const getBaseManifest = (): Record => ({ 13 | bundles: [ 14 | { 15 | asset: 'f35baff44129a8f6be7db68590b2efd86ed4ba29000e2edbcaddc5d620d7d043.js', 16 | format: 'js', 17 | }, 18 | ], 19 | routes: [ 20 | { 21 | name: 'name', 22 | function: 'hello', 23 | pattern: '^/hello/?$', 24 | generator: '@netlify/fake-plugin@1.0.0', 25 | }, 26 | ], 27 | post_cache_routes: [ 28 | { 29 | name: 'name', 30 | function: 'hello', 31 | pattern: '^/hello/?$', 32 | generator: '@netlify/fake-plugin@1.0.0', 33 | }, 34 | ], 35 | layers: [ 36 | { 37 | flag: 'flag', 38 | name: 'name', 39 | local: 'local', 40 | }, 41 | ], 42 | bundler_version: '1.6.0', 43 | }) 44 | 45 | test('should not throw on valid manifest', () => { 46 | expect(() => validateManifest(getBaseManifest())).not.toThrowError() 47 | }) 48 | 49 | test('should throw ManifestValidationError with correct message', () => { 50 | expect(() => validateManifest('manifest')).toThrowError(ManifestValidationError) 51 | expect(() => validateManifest('manifest')).toThrowError(/^Validation of Edge Functions manifest failed/) 52 | }) 53 | 54 | test('should throw ManifestValidationError with customErrorInfo', () => { 55 | try { 56 | validateManifest('manifest') 57 | } catch (error) { 58 | expect(error).toBeInstanceOf(ManifestValidationError) 59 | 60 | const { customErrorInfo } = error as ManifestValidationError 61 | expect(customErrorInfo).toBeDefined() 62 | expect(customErrorInfo.type).toBe('functionsBundling') 63 | return 64 | } 65 | 66 | expect.fail('should have thrown') 67 | }) 68 | 69 | test('should throw on additional property on root level', () => { 70 | const manifest = getBaseManifest() 71 | manifest.foo = 'bar' 72 | 73 | expect(() => validateManifest(manifest)).toThrowErrorMatchingSnapshot() 74 | }) 75 | 76 | test('should show multiple errors', () => { 77 | const manifest = getBaseManifest() 78 | manifest.foo = 'bar' 79 | manifest.baz = 'bar' 80 | 81 | expect(() => validateManifest(manifest)).toThrowErrorMatchingSnapshot() 82 | }) 83 | 84 | describe('bundle', () => { 85 | test('should throw on additional property in bundle', () => { 86 | const manifest = getBaseManifest() 87 | manifest.bundles[0].foo = 'bar' 88 | 89 | expect(() => validateManifest(manifest)).toThrowErrorMatchingSnapshot() 90 | }) 91 | 92 | test('should throw on missing asset', () => { 93 | const manifest = getBaseManifest() 94 | delete manifest.bundles[0].asset 95 | 96 | expect(() => validateManifest(manifest)).toThrowErrorMatchingSnapshot() 97 | }) 98 | 99 | test('should throw on missing format', () => { 100 | const manifest = getBaseManifest() 101 | delete manifest.bundles[0].format 102 | 103 | expect(() => validateManifest(manifest)).toThrowErrorMatchingSnapshot() 104 | }) 105 | 106 | test('should throw on invalid format', () => { 107 | const manifest = getBaseManifest() 108 | manifest.bundles[0].format = 'foo' 109 | 110 | expect(() => validateManifest(manifest)).toThrowErrorMatchingSnapshot() 111 | }) 112 | }) 113 | 114 | describe('route', () => { 115 | test('should throw on additional property', () => { 116 | const manifest = getBaseManifest() 117 | manifest.routes[0].foo = 'bar' 118 | 119 | expect(() => validateManifest(manifest)).toThrowErrorMatchingSnapshot() 120 | }) 121 | 122 | test('should throw on invalid pattern', () => { 123 | const manifest = getBaseManifest() 124 | manifest.routes[0].pattern = '/^/hello/?$/' 125 | 126 | expect(() => validateManifest(manifest)).toThrowErrorMatchingSnapshot() 127 | }) 128 | 129 | test('should throw on missing function', () => { 130 | const manifest = getBaseManifest() 131 | delete manifest.routes[0].function 132 | 133 | expect(() => validateManifest(manifest)).toThrowErrorMatchingSnapshot() 134 | }) 135 | 136 | test('should throw on missing pattern', () => { 137 | const manifest = getBaseManifest() 138 | delete manifest.routes[0].pattern 139 | 140 | expect(() => validateManifest(manifest)).toThrowErrorMatchingSnapshot() 141 | }) 142 | }) 143 | 144 | // No tests for post_cache_routes as schema shared with routes 145 | 146 | describe('layers', () => { 147 | test('should throw on additional property', () => { 148 | const manifest = getBaseManifest() 149 | manifest.layers[0].foo = 'bar' 150 | 151 | expect(() => validateManifest(manifest)).toThrowErrorMatchingSnapshot() 152 | }) 153 | 154 | test('should throw on missing name', () => { 155 | const manifest = getBaseManifest() 156 | delete manifest.layers[0].name 157 | 158 | expect(() => validateManifest(manifest)).toThrowErrorMatchingSnapshot() 159 | }) 160 | 161 | test('should throw on missing flag', () => { 162 | const manifest = getBaseManifest() 163 | delete manifest.layers[0].flag 164 | 165 | expect(() => validateManifest(manifest)).toThrowErrorMatchingSnapshot() 166 | }) 167 | }) 168 | 169 | describe('import map URL', () => { 170 | test('should accept string value', () => { 171 | const manifest = getBaseManifest() 172 | manifest.import_map = 'file:///root/.netlify/edge-functions-dist/import_map.json' 173 | 174 | expect(() => validateManifest(manifest)).not.toThrowError() 175 | }) 176 | 177 | test('should throw on wrong type', () => { 178 | const manifest = getBaseManifest() 179 | manifest.import_map = ['file:///root/.netlify/edge-functions-dist/import_map.json'] 180 | 181 | expect(() => validateManifest(manifest)).toThrowErrorMatchingSnapshot() 182 | }) 183 | }) 184 | -------------------------------------------------------------------------------- /node/validation/manifest/index.ts: -------------------------------------------------------------------------------- 1 | import Ajv from 'ajv' 2 | import type { ValidateFunction } from 'ajv/dist/core.js' 3 | import ajvErrors from 'ajv-errors' 4 | import betterAjvErrors from 'better-ajv-errors' 5 | 6 | import { FeatureFlags } from '../../feature_flags.js' 7 | import type { Manifest } from '../../manifest.js' 8 | 9 | import ManifestValidationError from './error.js' 10 | import edgeManifestSchema from './schema.js' 11 | 12 | let manifestValidator: ValidateFunction 13 | 14 | const initializeValidator = () => { 15 | if (manifestValidator === undefined) { 16 | const ajv = new Ajv({ allErrors: true }) 17 | ajvErrors(ajv) 18 | 19 | // regex pattern for manifest route pattern 20 | // checks if the pattern string starts with ^ and ends with $ 21 | const normalizedPatternRegex = /^\^.*\$$/ 22 | ajv.addFormat('regexPattern', { 23 | validate: (data: string) => normalizedPatternRegex.test(data), 24 | }) 25 | 26 | manifestValidator = ajv.compile(edgeManifestSchema) 27 | } 28 | 29 | return manifestValidator 30 | } 31 | 32 | // throws on validation error 33 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 34 | export const validateManifest = (manifestData: unknown, _featureFlags: FeatureFlags = {}): void => { 35 | const validate = initializeValidator() 36 | 37 | const valid = validate(manifestData) 38 | 39 | if (!valid) { 40 | let errorOutput 41 | 42 | if (validate.errors) { 43 | errorOutput = betterAjvErrors(edgeManifestSchema, manifestData, validate.errors, { indent: 2 }) 44 | } 45 | 46 | throw new ManifestValidationError(errorOutput) 47 | } 48 | } 49 | 50 | // eslint-disable-next-line unicorn/prefer-export-from 51 | export { ManifestValidationError } 52 | -------------------------------------------------------------------------------- /node/validation/manifest/schema.ts: -------------------------------------------------------------------------------- 1 | const bundlesSchema = { 2 | type: 'object', 3 | required: ['asset', 'format'], 4 | properties: { 5 | asset: { type: 'string' }, 6 | format: { type: 'string', enum: ['eszip2', 'js'] }, 7 | }, 8 | additionalProperties: false, 9 | } 10 | 11 | const excludedPatternsSchema = { 12 | type: 'array', 13 | items: { 14 | type: 'string', 15 | format: 'regexPattern', 16 | errorMessage: 17 | 'excluded_patterns must be an array of regex that starts with ^ and ends with $ (e.g. ^/blog/[d]{4}$)', 18 | }, 19 | } 20 | 21 | const routesSchema = { 22 | type: 'object', 23 | required: ['function', 'pattern'], 24 | properties: { 25 | name: { type: 'string' }, 26 | function: { type: 'string' }, 27 | pattern: { 28 | type: 'string', 29 | format: 'regexPattern', 30 | errorMessage: 'pattern must be a regex that starts with ^ and ends with $ (e.g. ^/blog/[d]{4}$)', 31 | }, 32 | excluded_patterns: excludedPatternsSchema, 33 | generator: { type: 'string' }, 34 | path: { type: 'string' }, 35 | methods: { 36 | type: 'array', 37 | items: { type: 'string', enum: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'] }, 38 | }, 39 | }, 40 | additionalProperties: false, 41 | } 42 | 43 | const functionConfigSchema = { 44 | type: 'object', 45 | required: [], 46 | properties: { 47 | excluded_patterns: excludedPatternsSchema, 48 | on_error: { type: 'string' }, 49 | }, 50 | } 51 | 52 | const layersSchema = { 53 | type: 'object', 54 | required: ['flag', 'name'], 55 | properties: { 56 | flag: { type: 'string' }, 57 | name: { type: 'string' }, 58 | local: { type: 'string' }, 59 | }, 60 | additionalProperties: false, 61 | } 62 | 63 | const edgeManifestSchema = { 64 | type: 'object', 65 | required: ['bundles', 'routes', 'bundler_version'], 66 | properties: { 67 | bundles: { 68 | type: 'array', 69 | items: bundlesSchema, 70 | }, 71 | routes: { 72 | type: 'array', 73 | items: routesSchema, 74 | }, 75 | post_cache_routes: { 76 | type: 'array', 77 | items: routesSchema, 78 | }, 79 | layers: { 80 | type: 'array', 81 | items: layersSchema, 82 | }, 83 | import_map: { type: 'string' }, 84 | bundler_version: { type: 'string' }, 85 | function_config: { type: 'object', additionalProperties: functionConfigSchema }, 86 | }, 87 | additionalProperties: false, 88 | } 89 | 90 | export default edgeManifestSchema 91 | -------------------------------------------------------------------------------- /node/vendor/module_graph/media_type.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. 2 | 3 | export enum MediaType { 4 | JavaScript = "JavaScript", 5 | Mjs = "Mjs", 6 | Cjs = "Cjs", 7 | Jsx = "Jsx", 8 | TypeScript = "TypeScript", 9 | Mts = "Mts", 10 | Cts = "Cts", 11 | Dts = "Dts", 12 | Dmts = "Dmts", 13 | Dcts = "Dcts", 14 | Tsx = "Tsx", 15 | Json = "Json", 16 | Wasm = "Wasm", 17 | TsBuildInfo = "TsBuildInfo", 18 | SourceMap = "SourceMap", 19 | Unknown = "Unknown", 20 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@netlify/edge-bundler", 3 | "version": "11.4.1", 4 | "description": "Intelligently prepare Netlify Edge Functions for deployment", 5 | "type": "module", 6 | "main": "./dist/node/index.js", 7 | "exports": "./dist/node/index.js", 8 | "files": [ 9 | "deno/**", 10 | "!deno/**/*.test.ts", 11 | "dist/**/*.js", 12 | "dist/**/*.d.ts", 13 | "shared/**" 14 | ], 15 | "scripts": { 16 | "build": "tsc", 17 | "build:dev": "tsc -w", 18 | "prepare": "husky install node_modules/@netlify/eslint-config-node/.husky/", 19 | "prepublishOnly": "npm ci && npm test", 20 | "prepack": "npm run build", 21 | "test": "run-s build format test:dev", 22 | "format": "run-s format:check-fix:*", 23 | "format:ci": "run-s format:check:*", 24 | "format:check-fix:lint": "run-e format:check:lint format:fix:lint", 25 | "format:check:lint": "cross-env-shell eslint $npm_package_config_eslint", 26 | "format:fix:lint": "cross-env-shell eslint --fix $npm_package_config_eslint", 27 | "format:check-fix:prettier": "run-e format:check:prettier format:fix:prettier", 28 | "format:check:prettier": "cross-env-shell prettier --check $npm_package_config_prettier", 29 | "format:fix:prettier": "cross-env-shell prettier --write $npm_package_config_prettier", 30 | "test:dev": "run-s test:dev:*", 31 | "test:ci": "run-s test:ci:*", 32 | "test:dev:vitest": "vitest run", 33 | "test:dev:vitest:watch": "vitest watch", 34 | "test:dev:deno": "deno test --allow-all deno", 35 | "test:ci:vitest": "vitest run --coverage", 36 | "test:ci:deno": "deno test --allow-all deno", 37 | "test:integration": "node --experimental-modules test/integration/test.js", 38 | "vendor": "deno vendor --force --output deno/vendor https://deno.land/x/eszip@v0.55.2/mod.ts https://deno.land/x/retry@v2.0.0/mod.ts https://deno.land/x/std@0.177.0/path/mod.ts" 39 | }, 40 | "config": { 41 | "eslint": "--ignore-path .gitignore --cache --format=codeframe --max-warnings=0 \"{node,scripts,.github}/**/*.{js,ts,md,html}\" \"*.{js,ts,md,html}\"", 42 | "prettier": "--ignore-path .gitignore --loglevel=warn \"{node,scripts,.github}/**/*.{js,ts,md,yml,json,html}\" \"*.{js,ts,yml,json,html}\" \".*.{js,ts,yml,json,html}\" \"!**/package-lock.json\" \"!package-lock.json\" \"!node/vendor/**\"" 43 | }, 44 | "keywords": [], 45 | "license": "MIT", 46 | "repository": "https://github.com/netlify/edge-bundler", 47 | "bugs": { 48 | "url": "https://github.com/netlify/edge-bundler/issues" 49 | }, 50 | "author": "Netlify Inc.", 51 | "directories": { 52 | "test": "test/node" 53 | }, 54 | "devDependencies": { 55 | "@commitlint/cli": "^17.0.0", 56 | "@commitlint/config-conventional": "^17.0.0", 57 | "@netlify/eslint-config-node": "^7.0.1", 58 | "@types/glob-to-regexp": "^0.4.1", 59 | "@types/node": "^14.18.32", 60 | "@types/semver": "^7.3.9", 61 | "@types/uuid": "^9.0.0", 62 | "@vitest/coverage-v8": "^0.34.0", 63 | "archiver": "^5.3.1", 64 | "chalk": "^4.1.2", 65 | "cpy": "^9.0.1", 66 | "cross-env": "^7.0.3", 67 | "husky": "^8.0.0", 68 | "nock": "^13.2.4", 69 | "tar": "^6.1.11", 70 | "typescript": "^5.0.0", 71 | "vitest": "^0.34.0" 72 | }, 73 | "engines": { 74 | "node": "^14.16.0 || >=16.0.0" 75 | }, 76 | "dependencies": { 77 | "@import-maps/resolve": "^1.0.1", 78 | "@vercel/nft": "^0.29.0", 79 | "ajv": "^8.11.2", 80 | "ajv-errors": "^3.0.0", 81 | "better-ajv-errors": "^1.2.0", 82 | "common-path-prefix": "^3.0.0", 83 | "env-paths": "^3.0.0", 84 | "esbuild": "0.25.1", 85 | "execa": "^6.0.0", 86 | "find-up": "^6.3.0", 87 | "get-package-name": "^2.2.0", 88 | "get-port": "^6.1.2", 89 | "is-path-inside": "^4.0.0", 90 | "jsonc-parser": "^3.2.0", 91 | "node-fetch": "^3.1.1", 92 | "node-stream-zip": "^1.15.0", 93 | "p-retry": "^5.1.1", 94 | "p-wait-for": "^4.1.0", 95 | "path-key": "^4.0.0", 96 | "semver": "^7.3.8", 97 | "tmp-promise": "^3.0.3", 98 | "urlpattern-polyfill": "8.0.2", 99 | "uuid": "^9.0.0" 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /renovate.json5: -------------------------------------------------------------------------------- 1 | { 2 | extends: ['github>netlify/renovate-config:default'], 3 | ignorePresets: [':prHourlyLimit2'], 4 | semanticCommits: true, 5 | dependencyDashboard: true, 6 | automerge: true, 7 | packageRules: [ 8 | { 9 | // We need to use the same version as better-ajv-errors 10 | packageNames: ['chalk'], 11 | major: { 12 | enabled: false, 13 | }, 14 | }, 15 | ], 16 | } 17 | -------------------------------------------------------------------------------- /shared/consts.ts: -------------------------------------------------------------------------------- 1 | export const importMapSpecifier = 'netlify:import-map' 2 | export const nodePrefix = 'node:' 3 | export const npmPrefix = 'npm:' 4 | export const virtualRoot = 'file:///root/' 5 | export const virtualVendorRoot = 'file:///vendor/' 6 | -------------------------------------------------------------------------------- /shared/stage2.ts: -------------------------------------------------------------------------------- 1 | export interface InputFunction { 2 | name: string 3 | path: string 4 | } 5 | 6 | export interface WriteStage2Options { 7 | basePath: string 8 | destPath: string 9 | externals: string[] 10 | functions: InputFunction[] 11 | importMapData?: string 12 | vendorDirectory?: string 13 | } 14 | -------------------------------------------------------------------------------- /test/fixtures/deno.win.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netlify/edge-bundler/3cdcd27c87555d01b176ffb1a8f852c27e71669e/test/fixtures/deno.win.zip -------------------------------------------------------------------------------- /test/fixtures/deno.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netlify/edge-bundler/3cdcd27c87555d01b176ffb1a8f852c27e71669e/test/fixtures/deno.zip -------------------------------------------------------------------------------- /test/fixtures/imports_json/functions/dict.json: -------------------------------------------------------------------------------- 1 | { 2 | "foo": "bar" 3 | } -------------------------------------------------------------------------------- /test/fixtures/imports_json/functions/func1.ts: -------------------------------------------------------------------------------- 1 | import dict from './dict.json' with { type: 'json' } 2 | 3 | 4 | export default async () => Response.json(dict) 5 | -------------------------------------------------------------------------------- /test/fixtures/imports_node_specifier/netlify/edge-functions/func1.ts: -------------------------------------------------------------------------------- 1 | import assert from 'node:assert' 2 | import process from 'node:process' 3 | 4 | export default () => { 5 | Deno.env.set('NETLIFY_TEST', '12345') 6 | assert.deepEqual(process.env.NETLIFY_TEST, '12345') 7 | 8 | return new Response('ok') 9 | } 10 | 11 | export const config = { 12 | path: '/func1', 13 | } 14 | -------------------------------------------------------------------------------- /test/fixtures/imports_npm_module/functions/func1.ts: -------------------------------------------------------------------------------- 1 | import parent1 from 'parent-1' 2 | import parent3 from './lib/util.ts' 3 | import { echo, parent2 } from 'alias:helper' 4 | import { HTMLRewriter } from 'html-rewriter' 5 | 6 | await Promise.resolve() 7 | 8 | new HTMLRewriter() 9 | 10 | export default async () => { 11 | const text = [parent1('JavaScript'), parent2('APIs'), parent3('Markup')].join(', ') 12 | 13 | return new Response(echo(text)) 14 | } 15 | -------------------------------------------------------------------------------- /test/fixtures/imports_npm_module/functions/lib/util.ts: -------------------------------------------------------------------------------- 1 | import parent3 from 'parent-3' 2 | 3 | export default parent3 4 | -------------------------------------------------------------------------------- /test/fixtures/imports_npm_module/helper.ts: -------------------------------------------------------------------------------- 1 | import parent2 from 'parent-2' 2 | 3 | export const greet = (name: string) => `Hello, ${name}!` 4 | export const echo = (name: string) => name 5 | export const yell = (message: string) => message.toUpperCase() 6 | export { parent2 } 7 | -------------------------------------------------------------------------------- /test/fixtures/imports_npm_module/import_map.json: -------------------------------------------------------------------------------- 1 | { 2 | "imports": { 3 | "alias:helper": "./helper.ts", 4 | "html-rewriter": "https://ghuc.cc/worker-tools/html-rewriter/index.ts" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /test/fixtures/imports_npm_module/node_modules/child-1/files/file1.txt: -------------------------------------------------------------------------------- 1 | One -------------------------------------------------------------------------------- /test/fixtures/imports_npm_module/node_modules/child-1/files/file2.txt: -------------------------------------------------------------------------------- 1 | Two -------------------------------------------------------------------------------- /test/fixtures/imports_npm_module/node_modules/child-1/index.js: -------------------------------------------------------------------------------- 1 | import { readFileSync } from "fs" 2 | import { join } from "path" 3 | import { Buffer } from "node:buffer" 4 | 5 | export default (input) => { 6 | try { 7 | const filePath = input === "one" ? 'file1.txt' : 'file2.txt' 8 | const fileContents = readFileSync(join(__dirname, "files", filePath)) 9 | 10 | console.log(Buffer.from(fileContents).toString('utf-8')) 11 | } catch { 12 | // no-op 13 | } 14 | 15 | return `${input}` 16 | } -------------------------------------------------------------------------------- /test/fixtures/imports_npm_module/node_modules/child-1/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "child-1", 3 | "version": "1.0.0", 4 | "main": "index.js" 5 | } -------------------------------------------------------------------------------- /test/fixtures/imports_npm_module/node_modules/child-2/index.js: -------------------------------------------------------------------------------- 1 | import grandchild1 from "grandchild-1" 2 | 3 | export default (input) => `${grandchild1(input)}` -------------------------------------------------------------------------------- /test/fixtures/imports_npm_module/node_modules/child-2/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "child-2", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "dependencies": { 6 | "grandchild-1": "1.0.0" 7 | } 8 | } -------------------------------------------------------------------------------- /test/fixtures/imports_npm_module/node_modules/grandchild-1/index.js: -------------------------------------------------------------------------------- 1 | import { cwd } from "process" 2 | 3 | export default (input) => `${input}${cwd()}` -------------------------------------------------------------------------------- /test/fixtures/imports_npm_module/node_modules/grandchild-1/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "grandchild-1", 3 | "version": "1.0.0", 4 | "main": "index.js" 5 | } -------------------------------------------------------------------------------- /test/fixtures/imports_npm_module/node_modules/parent-1/index.js: -------------------------------------------------------------------------------- 1 | import child1 from "child-1" 2 | 3 | export default (input) => `${child1(input)}` -------------------------------------------------------------------------------- /test/fixtures/imports_npm_module/node_modules/parent-1/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "parent-1", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "dependencies": { 6 | "child-1": "1.0.0" 7 | } 8 | } -------------------------------------------------------------------------------- /test/fixtures/imports_npm_module/node_modules/parent-2/index.js: -------------------------------------------------------------------------------- 1 | import child2 from "child-2" 2 | 3 | export default (input) => `${child2(input)}` 4 | 5 | -------------------------------------------------------------------------------- /test/fixtures/imports_npm_module/node_modules/parent-2/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "parent-2", 3 | "version": "1.0.0", 4 | "main": "index.js" 5 | } -------------------------------------------------------------------------------- /test/fixtures/imports_npm_module/node_modules/parent-3/index.js: -------------------------------------------------------------------------------- 1 | import child2 from "child-2" 2 | 3 | export default (input) => `${child2(input)}` 4 | 5 | -------------------------------------------------------------------------------- /test/fixtures/imports_npm_module/node_modules/parent-3/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "parent-3", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "dependencies": { 6 | "child-2": "1.0.0" 7 | } 8 | } -------------------------------------------------------------------------------- /test/fixtures/imports_npm_module/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "module" 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/imports_npm_module_scheme/functions/func1.ts: -------------------------------------------------------------------------------- 1 | import pRetry from "npm:p-retry" 2 | -------------------------------------------------------------------------------- /test/fixtures/invalid_functions/functions/func1.ts: -------------------------------------------------------------------------------- 1 | export default async () => 2 | -------------------------------------------------------------------------------- /test/fixtures/monorepo_npm_module/node_modules/child-1/files/file1.txt: -------------------------------------------------------------------------------- 1 | One -------------------------------------------------------------------------------- /test/fixtures/monorepo_npm_module/node_modules/child-1/files/file2.txt: -------------------------------------------------------------------------------- 1 | Two -------------------------------------------------------------------------------- /test/fixtures/monorepo_npm_module/node_modules/child-1/index.js: -------------------------------------------------------------------------------- 1 | import { readFileSync } from "fs" 2 | import { join } from "path" 3 | 4 | export default (input) => { 5 | try { 6 | const filePath = input === "one" ? 'file1.txt' : 'file2.txt' 7 | const fileContents = readFileSync(join(__dirname, "files", filePath)) 8 | 9 | console.log(fileContents) 10 | } catch { 11 | // no-op 12 | } 13 | 14 | return `${input}` 15 | } -------------------------------------------------------------------------------- /test/fixtures/monorepo_npm_module/node_modules/child-1/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "child-1", 3 | "version": "1.0.0", 4 | "main": "index.js" 5 | } -------------------------------------------------------------------------------- /test/fixtures/monorepo_npm_module/node_modules/child-2/index.js: -------------------------------------------------------------------------------- 1 | import grandchild1 from "grandchild-1" 2 | 3 | export default (input) => `${grandchild1(input)}` -------------------------------------------------------------------------------- /test/fixtures/monorepo_npm_module/node_modules/child-2/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "child-2", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "dependencies": { 6 | "grandchild-1": "1.0.0" 7 | } 8 | } -------------------------------------------------------------------------------- /test/fixtures/monorepo_npm_module/node_modules/grandchild-1/index.js: -------------------------------------------------------------------------------- 1 | import { cwd } from "process" 2 | 3 | export default (input) => `${input}${cwd()}` -------------------------------------------------------------------------------- /test/fixtures/monorepo_npm_module/node_modules/grandchild-1/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "grandchild-1", 3 | "version": "1.0.0", 4 | "main": "index.js" 5 | } -------------------------------------------------------------------------------- /test/fixtures/monorepo_npm_module/node_modules/parent-1/index.js: -------------------------------------------------------------------------------- 1 | import child1 from "child-1" 2 | 3 | export default (input) => `${child1(input)}` -------------------------------------------------------------------------------- /test/fixtures/monorepo_npm_module/node_modules/parent-1/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "parent-1", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "dependencies": { 6 | "child-1": "1.0.0" 7 | } 8 | } -------------------------------------------------------------------------------- /test/fixtures/monorepo_npm_module/node_modules/parent-2/index.js: -------------------------------------------------------------------------------- 1 | import child2 from "child-2" 2 | 3 | export default (input) => `${child2(input)}` 4 | 5 | -------------------------------------------------------------------------------- /test/fixtures/monorepo_npm_module/node_modules/parent-2/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "parent-2", 3 | "version": "1.0.0", 4 | "main": "index.js" 5 | } -------------------------------------------------------------------------------- /test/fixtures/monorepo_npm_module/node_modules/parent-3/index.js: -------------------------------------------------------------------------------- 1 | import child2 from "child-2" 2 | 3 | export default (input) => `${child2(input)}` 4 | 5 | -------------------------------------------------------------------------------- /test/fixtures/monorepo_npm_module/node_modules/parent-3/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "parent-3", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "dependencies": { 6 | "child-2": "1.0.0" 7 | } 8 | } -------------------------------------------------------------------------------- /test/fixtures/monorepo_npm_module/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "module" 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/monorepo_npm_module/packages/frontend/functions/func1.ts: -------------------------------------------------------------------------------- 1 | import parent1 from 'parent-1' 2 | import parent3 from './lib/util.ts' 3 | import { echo, parent2 } from 'alias:helper' 4 | import { HTMLRewriter } from 'html-rewriter' 5 | 6 | await Promise.resolve() 7 | 8 | new HTMLRewriter() 9 | 10 | export default async () => { 11 | console.error('Something is on fire') 12 | 13 | const text = [parent1('JavaScript'), parent2('APIs'), parent3('Markup')].join(', ') 14 | 15 | return new Response(echo(text)) 16 | } 17 | 18 | export const config = { 19 | path: '/func1', 20 | } 21 | -------------------------------------------------------------------------------- /test/fixtures/monorepo_npm_module/packages/frontend/functions/lib/util.ts: -------------------------------------------------------------------------------- 1 | import parent3 from 'parent-3' 2 | 3 | export default parent3 4 | -------------------------------------------------------------------------------- /test/fixtures/monorepo_npm_module/packages/frontend/helper.ts: -------------------------------------------------------------------------------- 1 | import parent2 from 'parent-2' 2 | 3 | export const greet = (name: string) => `Hello, ${name}!` 4 | export const echo = (name: string) => name 5 | export const yell = (message: string) => message.toUpperCase() 6 | export { parent2 } 7 | -------------------------------------------------------------------------------- /test/fixtures/monorepo_npm_module/packages/frontend/import_map.json: -------------------------------------------------------------------------------- 1 | { 2 | "imports": { 3 | "alias:helper": "./helper.ts", 4 | "html-rewriter": "https://ghuc.cc/worker-tools/html-rewriter/index.ts" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /test/fixtures/serve_test/.netlify/.gitignore: -------------------------------------------------------------------------------- 1 | edge-functions-serve -------------------------------------------------------------------------------- /test/fixtures/serve_test/.netlify/edge-functions/greet.ts: -------------------------------------------------------------------------------- 1 | import { yell } from 'internal-helper' 2 | 3 | export default async () => new Response(yell('Hello!')) 4 | -------------------------------------------------------------------------------- /test/fixtures/serve_test/.netlify/edge-functions/import_map.json: -------------------------------------------------------------------------------- 1 | { 2 | "imports": { 3 | "internal-helper": "../../helper.ts" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/fixtures/serve_test/helper.ts: -------------------------------------------------------------------------------- 1 | export const yell = (message: string) => message.toUpperCase() 2 | -------------------------------------------------------------------------------- /test/fixtures/serve_test/netlify/edge-functions/echo_env.ts: -------------------------------------------------------------------------------- 1 | import { Config } from 'https://edge.netlify.com' 2 | 3 | import { yell } from 'helper' 4 | import id from 'id' 5 | import identidade from '@pt-committee/identidade' 6 | 7 | import { getWords } from 'dictionary' 8 | 9 | // this will throw since FS access is not available in Edge Functions. 10 | // but we need this line so that `dictionary` is scanned for extraneous dependencies 11 | try { 12 | getWords() 13 | } catch {} 14 | 15 | export default () => { 16 | return new Response(yell(identidade(id(Deno.env.get('very_secret_secret'))) ?? '')) 17 | } 18 | 19 | export const config: Config = { 20 | path: '/my-function', 21 | } 22 | -------------------------------------------------------------------------------- /test/fixtures/serve_test/netlify/edge-functions/global_netlify.ts: -------------------------------------------------------------------------------- 1 | import { Config } from 'https://edge.netlify.com/' 2 | 3 | const global = globalThis.Netlify.env.get('very_secret_secret') 4 | 5 | export default () => { 6 | return Response.json({ 7 | global, 8 | local: globalThis.Netlify.env.get('very_secret_secret'), 9 | }) 10 | } 11 | 12 | export const config: Config = { 13 | path: '/global-netlify', 14 | } 15 | -------------------------------------------------------------------------------- /test/fixtures/serve_test/netlify/edge-functions/import-map.json: -------------------------------------------------------------------------------- 1 | { 2 | "imports": { 3 | "helper": "../../helper.ts" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/fixtures/serve_test/node_modules/@pt-committee/identidade/index.js: -------------------------------------------------------------------------------- 1 | module.exports = (v) => v 2 | -------------------------------------------------------------------------------- /test/fixtures/serve_test/node_modules/@pt-committee/identidade/package.json: -------------------------------------------------------------------------------- 1 | { "name": "@pt-committee/identidade" } -------------------------------------------------------------------------------- /test/fixtures/serve_test/node_modules/@types/pt-committee__identidade/index.d.ts: -------------------------------------------------------------------------------- 1 | declare function id(v: T): T 2 | 3 | export default id 4 | -------------------------------------------------------------------------------- /test/fixtures/serve_test/node_modules/@types/pt-committee__identidade/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@types/pt-committee__identidade", 3 | "typings": "index.d.ts" 4 | } -------------------------------------------------------------------------------- /test/fixtures/serve_test/node_modules/dictionary/index.js: -------------------------------------------------------------------------------- 1 | import { readFileSync } from 'node:fs' 2 | 3 | export function getWords() { 4 | const wordsFile = readFileSync(new URL('./words.txt', import.meta.url), { encoding: "utf-8" }) 5 | return wordsFile.split('\n') 6 | } 7 | -------------------------------------------------------------------------------- /test/fixtures/serve_test/node_modules/dictionary/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dictionary", 3 | "type": "module", 4 | "main": "index.js" 5 | } -------------------------------------------------------------------------------- /test/fixtures/serve_test/node_modules/dictionary/words.txt: -------------------------------------------------------------------------------- 1 | Flibberdegibbet 2 | Quibble 3 | Gobbledygook 4 | Wobblebonk 5 | Blibber-blabber 6 | Malarkey 7 | Flibbertigibbet 8 | Gobbledyguke 9 | Jibber-jabber 10 | Gibberish 11 | Noodlehead 12 | Snickersnee 13 | Skedaddle 14 | Lollygag 15 | Hootenanny 16 | Razzmatazz 17 | Higgledy-piggledy 18 | Wobblegong -------------------------------------------------------------------------------- /test/fixtures/serve_test/node_modules/id/index.cjs: -------------------------------------------------------------------------------- 1 | throw new Error('we should always prefer ESM if available'); -------------------------------------------------------------------------------- /test/fixtures/serve_test/node_modules/id/index.mjs: -------------------------------------------------------------------------------- 1 | export default (v) => v 2 | -------------------------------------------------------------------------------- /test/fixtures/serve_test/node_modules/id/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "id", 3 | "main": "index.cjs", 4 | "module": "index.mjs", 5 | "types": "types.d.ts" 6 | } -------------------------------------------------------------------------------- /test/fixtures/serve_test/node_modules/id/types.d.ts: -------------------------------------------------------------------------------- 1 | declare function id(v: T): T 2 | 3 | export default id 4 | -------------------------------------------------------------------------------- /test/fixtures/tsx/functions/func1.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export default () => new Response(

Hello World

) 4 | -------------------------------------------------------------------------------- /test/fixtures/tsx/node_modules/react/cjs/react.development.js: -------------------------------------------------------------------------------- 1 | module.exports.createElement = () => "hippedy hoppedy, createElement is now a debugged property" -------------------------------------------------------------------------------- /test/fixtures/tsx/node_modules/react/cjs/react.production.min.js: -------------------------------------------------------------------------------- 1 | module.exports.createElement = () => 2 | Buffer.from( 3 | `hippedy hoppedy, createElement is now a production property. Here, take this env var: FOO=${process.env.FOO}`, 4 | ).toString('base64') 5 | -------------------------------------------------------------------------------- /test/fixtures/tsx/node_modules/react/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | if (process.env.NODE_ENV === 'production') { 4 | module.exports = require('./cjs/react.production.min.js'); 5 | } else { 6 | module.exports = require('./cjs/react.development.js'); 7 | } 8 | -------------------------------------------------------------------------------- /test/fixtures/tsx/node_modules/react/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react", 3 | "main": "index.js" 4 | } -------------------------------------------------------------------------------- /test/fixtures/with_config/.netlify/edge-functions/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "functions": [ 3 | { 4 | "function": "func2", 5 | "path": "/func2" 6 | } 7 | ], 8 | "import_map": "import_map.json", 9 | "version": 1 10 | } 11 | -------------------------------------------------------------------------------- /test/fixtures/with_config/.netlify/edge-functions/framework-func1.ts: -------------------------------------------------------------------------------- 1 | import { IntegrationsConfig } from 'https://edge.netlify.com' 2 | import { greet } from 'alias:helper' 3 | 4 | export default async () => { 5 | const greeting = greet('framework function 1') 6 | 7 | return new Response(greeting) 8 | } 9 | 10 | export const config: IntegrationsConfig = { 11 | path: '/framework-func1', 12 | } 13 | -------------------------------------------------------------------------------- /test/fixtures/with_config/.netlify/edge-functions/framework-func2.ts: -------------------------------------------------------------------------------- 1 | import { greet } from 'alias:helper' 2 | 3 | export default async () => { 4 | const greeting = greet('framework function 2') 5 | 6 | return new Response(greeting) 7 | } 8 | -------------------------------------------------------------------------------- /test/fixtures/with_config/.netlify/edge-functions/import_map.json: -------------------------------------------------------------------------------- 1 | { 2 | "imports": { 3 | "alias:helper": "../../helper.ts" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/fixtures/with_config/helper.ts: -------------------------------------------------------------------------------- 1 | export const greet = (name: string) => `Hello, ${name}!` 2 | export const echo = (name: string) => name 3 | -------------------------------------------------------------------------------- /test/fixtures/with_config/netlify/edge-functions/user-func1.ts: -------------------------------------------------------------------------------- 1 | import { Config } from '@netlify/edge-functions' 2 | import { greet } from 'alias:helper' 3 | 4 | // Accessing `Deno.env` in the global scope 5 | if (Deno.env.get('FOO')) { 6 | // no-op 7 | } 8 | 9 | export default async () => { 10 | const greeting = greet('user function 1') 11 | 12 | return new Response(greeting) 13 | } 14 | 15 | export const config: Config = { 16 | path: '/user-func1', 17 | } 18 | -------------------------------------------------------------------------------- /test/fixtures/with_config/netlify/edge-functions/user-func2.ts: -------------------------------------------------------------------------------- 1 | import { greet } from 'alias:helper' 2 | 3 | export default async () => { 4 | const greeting = greet('user function 2') 5 | 6 | return new Response(greeting) 7 | } 8 | -------------------------------------------------------------------------------- /test/fixtures/with_config/netlify/edge-functions/user-func3.ts: -------------------------------------------------------------------------------- 1 | export default async () => new Response('Hello from user function 3') 2 | 3 | export const config = { 4 | cache: 'not_a_supported_value', 5 | path: '/user-func3', 6 | } 7 | -------------------------------------------------------------------------------- /test/fixtures/with_config/netlify/edge-functions/user-func4.ts: -------------------------------------------------------------------------------- 1 | export default async () => 2 | new Response('Hello from user function 4. I will be cached!', { 3 | headers: { 4 | 'cache-control': 'public, s-maxage=60', 5 | }, 6 | }) 7 | 8 | export const config = { 9 | cache: 'manual', 10 | path: '/user-func4', 11 | method: ['POST', 'PUT'], 12 | } 13 | -------------------------------------------------------------------------------- /test/fixtures/with_config/netlify/edge-functions/user-func5.ts: -------------------------------------------------------------------------------- 1 | export default async () => new Response('Hello from user function 5.') 2 | 3 | export const config = { 4 | path: '/user-func5/*', 5 | excludedPath: '/user-func5/excluded', 6 | method: 'get', 7 | } 8 | -------------------------------------------------------------------------------- /test/fixtures/with_deploy_config/.netlify/edge-functions/func2.ts: -------------------------------------------------------------------------------- 1 | import { greet } from 'alias:helper' 2 | 3 | import { echo } from '../../util.ts' 4 | 5 | export default async () => { 6 | const greeting = greet(echo('Jane Doe')) 7 | 8 | return new Response(greeting) 9 | } 10 | -------------------------------------------------------------------------------- /test/fixtures/with_deploy_config/.netlify/edge-functions/func3.ts: -------------------------------------------------------------------------------- 1 | import { IntegrationsConfig } from 'https://edge.netlify.com' 2 | 3 | export default async () => { 4 | return new Response('Hello world') 5 | } 6 | 7 | export const config: IntegrationsConfig = { 8 | path: '/func-3', 9 | name: 'in-config-function', 10 | onError: 'bypass', 11 | } 12 | -------------------------------------------------------------------------------- /test/fixtures/with_deploy_config/.netlify/edge-functions/import_map.json: -------------------------------------------------------------------------------- 1 | { 2 | "imports": { 3 | "alias:helper": "../../util.ts" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/fixtures/with_deploy_config/.netlify/edge-functions/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "functions": [ 3 | { 4 | "function": "func2", 5 | "path": "/func2/*", 6 | "name": "Function two", 7 | "generator": "@netlify/fake-plugin@1.0.0", 8 | "excludedPath": "/func2/skip" 9 | } 10 | ], 11 | "import_map": "import_map.json", 12 | "version": 1 13 | } 14 | -------------------------------------------------------------------------------- /test/fixtures/with_deploy_config/netlify/edge-functions/func1.ts: -------------------------------------------------------------------------------- 1 | import { echo } from '../../util.ts' 2 | 3 | export default async () => new Response(echo('Jane Doe')) 4 | -------------------------------------------------------------------------------- /test/fixtures/with_deploy_config/util.ts: -------------------------------------------------------------------------------- 1 | export const greet = (name: string) => `Hello, ${name}!` 2 | export const echo = (name: string) => name 3 | -------------------------------------------------------------------------------- /test/fixtures/with_import_maps/functions/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "import_map": "import_map.json", 3 | "version": 1 4 | } 5 | -------------------------------------------------------------------------------- /test/fixtures/with_import_maps/functions/func1.ts: -------------------------------------------------------------------------------- 1 | import { greet } from 'alias:helper' 2 | import { yell } from 'util/helper.ts' 3 | import { echo } from '../helper.ts' 4 | 5 | export default async () => { 6 | const greeting = yell(greet(echo('Jane Doe'))) 7 | 8 | return new Response(greeting) 9 | } 10 | -------------------------------------------------------------------------------- /test/fixtures/with_import_maps/functions/import_map.json: -------------------------------------------------------------------------------- 1 | { 2 | "imports": { 3 | "alias:helper": "../helper.ts", 4 | "util/": "../" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /test/fixtures/with_import_maps/helper.ts: -------------------------------------------------------------------------------- 1 | export const greet = (name: string) => `Hello, ${name}!` 2 | export const echo = (name: string) => name 3 | export const yell = (message: string) => message.toUpperCase() 4 | -------------------------------------------------------------------------------- /test/fixtures/with_import_maps/helper2.ts: -------------------------------------------------------------------------------- 1 | export const hush = (message: string) => message.toLowerCase() 2 | -------------------------------------------------------------------------------- /test/fixtures/with_import_maps/user-functions/func2.ts: -------------------------------------------------------------------------------- 1 | import { echo } from 'helper' 2 | 3 | export default async () => { 4 | const greeting = echo('Jane Doe') 5 | 6 | return new Response(greeting) 7 | } 8 | -------------------------------------------------------------------------------- /test/fixtures/with_import_maps/user-functions/func3/func3.ts: -------------------------------------------------------------------------------- 1 | import { hush } from 'helper' 2 | 3 | export default async () => { 4 | const greeting = hush('HELLO, NETLIFY!') 5 | 6 | return new Response(greeting) 7 | } 8 | -------------------------------------------------------------------------------- /test/fixtures/with_import_maps/user-functions/import_map.json: -------------------------------------------------------------------------------- 1 | { 2 | "imports": { 3 | "helper": "../helper.ts" 4 | }, 5 | "scopes": { 6 | "func3/": { 7 | "helper": "../helper2.ts" 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /test/fixtures/with_layers/functions/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "layers": [{ "name": "https://edge-function-layer-template.netlify.app/mod.ts", "flag": "edge-functions-layer-test" }], 3 | "version": 1 4 | } 5 | -------------------------------------------------------------------------------- /test/fixtures/with_layers/functions/func1.ts: -------------------------------------------------------------------------------- 1 | import { handleRequest } from '../layer.ts' 2 | 3 | export default (req: Request) => handleRequest(req) 4 | -------------------------------------------------------------------------------- /test/fixtures/with_layers/layer.ts: -------------------------------------------------------------------------------- 1 | export const handleRequest = (_req: Request) => { 2 | return new Response('Hello! I come from a custom layer.') 3 | } 4 | -------------------------------------------------------------------------------- /test/integration/functions/func1.ts: -------------------------------------------------------------------------------- 1 | export default async () => new Response('Hello') 2 | -------------------------------------------------------------------------------- /test/integration/internal-functions/func2.ts: -------------------------------------------------------------------------------- 1 | export default async () => new Response('Hello') 2 | 3 | export const config = { path: '/func2' } 4 | -------------------------------------------------------------------------------- /test/integration/test.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert' 2 | import childProcess from 'child_process' 3 | import { rm } from 'fs/promises' 4 | import { createRequire } from 'module' 5 | import { join, resolve } from 'path' 6 | import process from 'process' 7 | import { fileURLToPath, pathToFileURL } from 'url' 8 | import { promisify } from 'util' 9 | 10 | import cpy from 'cpy' 11 | import tar from 'tar' 12 | import tmp from 'tmp-promise' 13 | 14 | const exec = promisify(childProcess.exec) 15 | const require = createRequire(import.meta.url) 16 | const functionsDir = resolve(fileURLToPath(import.meta.url), '..', 'functions') 17 | const internalFunctionsDir = resolve(fileURLToPath(import.meta.url), '..', 'internal-functions') 18 | 19 | const pathsToCleanup = new Set() 20 | 21 | const installPackage = async () => { 22 | console.log(`Getting package version...`) 23 | 24 | const { name, version } = require('../../package.json') 25 | 26 | console.log(`Running integration tests for ${name} v${version}...`) 27 | 28 | const { path } = await tmp.dir() 29 | 30 | console.log(`Creating tarball with 'npm pack'...`) 31 | 32 | await exec('npm pack --json') 33 | 34 | const normalizedName = name.replace(/@/, '').replace(/\W/g, '-') 35 | const filename = join(process.cwd(), `${normalizedName}-${version}.tgz`) 36 | 37 | console.log(`Uncompressing the tarball at '${filename}'...`) 38 | 39 | // eslint-disable-next-line id-length 40 | await tar.x({ C: path, file: filename, strip: 1 }) 41 | 42 | pathsToCleanup.add(path) 43 | pathsToCleanup.add(filename) 44 | 45 | return path 46 | } 47 | 48 | const bundleFunction = async (bundlerDir) => { 49 | console.log(`Installing dependencies at '${bundlerDir}'...`) 50 | 51 | await exec(`npm --prefix ${bundlerDir} install`) 52 | 53 | const bundlerPath = require.resolve(bundlerDir) 54 | const bundlerURL = pathToFileURL(bundlerPath) 55 | // eslint-disable-next-line import/no-dynamic-require 56 | const { bundle } = await import(bundlerURL) 57 | const { path: basePath } = await tmp.dir() 58 | 59 | console.log(`Copying test fixture to '${basePath}'...`) 60 | 61 | await cpy(`${functionsDir}/**`, join(basePath, 'functions')) 62 | await cpy(`${internalFunctionsDir}/**`, join(basePath, 'internal-functions')) 63 | 64 | pathsToCleanup.add(basePath) 65 | 66 | const destPath = join(basePath, '.netlify', 'edge-functions-dist') 67 | 68 | console.log(`Bundling functions at '${basePath}'...`) 69 | 70 | const bundleOutput = await bundle( 71 | [join(basePath, 'functions'), join(basePath, 'internal-functions')], 72 | destPath, 73 | [{ function: 'func1', path: '/func1' }], 74 | { 75 | basePath, 76 | internalSrcFolder: join(basePath, 'internal-functions'), 77 | }, 78 | ) 79 | 80 | return { 81 | basePath, 82 | bundleOutput, 83 | } 84 | } 85 | 86 | const runAssertions = ({ basePath, bundleOutput }) => { 87 | console.log('Running assertions on bundle output:') 88 | console.log(JSON.stringify(bundleOutput, null, 2)) 89 | 90 | const { functions } = bundleOutput 91 | 92 | assert.strictEqual(functions.length, 2) 93 | assert.strictEqual(functions[0].name, 'func2') 94 | assert.strictEqual(functions[0].path, join(basePath, 'internal-functions', 'func2.ts')) 95 | assert.strictEqual(functions[1].name, 'func1') 96 | assert.strictEqual(functions[1].path, join(basePath, 'functions', 'func1.ts')) 97 | } 98 | 99 | const cleanup = async () => { 100 | if (process.env.CI) { 101 | return 102 | } 103 | 104 | console.log(`Cleaning up temporary files...`) 105 | 106 | for (const folder of pathsToCleanup) { 107 | await rm(folder, { force: true, recursive: true, maxRetries: 10 }) 108 | } 109 | } 110 | 111 | installPackage() 112 | .then(bundleFunction) 113 | .then(runAssertions) 114 | .then(cleanup) 115 | // eslint-disable-next-line promise/prefer-await-to-callbacks 116 | .catch((error) => { 117 | console.error(error) 118 | 119 | throw error 120 | }) 121 | -------------------------------------------------------------------------------- /test/util.ts: -------------------------------------------------------------------------------- 1 | import { promises as fs } from 'fs' 2 | import { join, resolve } from 'path' 3 | import { stderr, stdout } from 'process' 4 | import { fileURLToPath, pathToFileURL } from 'url' 5 | 6 | import { execa } from 'execa' 7 | import tmp from 'tmp-promise' 8 | 9 | import { getLogger } from '../node/logger.js' 10 | import type { Manifest } from '../node/manifest.js' 11 | 12 | const testLogger = getLogger(() => { 13 | // no-op 14 | }) 15 | 16 | const url = new URL(import.meta.url) 17 | const dirname = fileURLToPath(url) 18 | const fixturesDir = resolve(dirname, '..', 'fixtures') 19 | 20 | const useFixture = async (fixtureName: string) => { 21 | const tmpDir = await tmp.dir({ unsafeCleanup: true }) 22 | const fixtureDir = resolve(fixturesDir, fixtureName) 23 | const distPath = join(tmpDir.path, '.netlify', 'edge-functions-dist') 24 | 25 | return { 26 | basePath: fixtureDir, 27 | cleanup: tmpDir.cleanup, 28 | distPath, 29 | } 30 | } 31 | 32 | const inspectFunction = (path: string) => ` 33 | import { functions } from "${pathToFileURL(path)}.js"; 34 | 35 | const responses = {}; 36 | 37 | for (const functionName in functions) { 38 | const req = new Request("https://test.netlify"); 39 | const res = await functions[functionName](req); 40 | 41 | responses[functionName] = await res.text(); 42 | } 43 | 44 | console.log(JSON.stringify(responses)); 45 | ` 46 | 47 | const getRouteMatcher = (manifest: Manifest) => (candidate: string) => 48 | manifest.routes.find((route) => { 49 | const regex = new RegExp(route.pattern) 50 | 51 | if (!regex.test(candidate)) { 52 | return false 53 | } 54 | 55 | if (route.excluded_patterns.some((pattern) => new RegExp(pattern).test(candidate))) { 56 | return false 57 | } 58 | 59 | const excludedPatterns = manifest.function_config[route.function]?.excluded_patterns ?? [] 60 | const isExcluded = excludedPatterns.some((pattern) => new RegExp(pattern).test(candidate)) 61 | 62 | return !isExcluded 63 | }) 64 | 65 | const runESZIP = async (eszipPath: string, vendorDirectory?: string) => { 66 | const tmpDir = await tmp.dir({ unsafeCleanup: true }) 67 | 68 | // Extract ESZIP into temporary directory. 69 | const extractCommand = execa('deno', [ 70 | 'run', 71 | '--allow-all', 72 | 'https://deno.land/x/eszip@v0.55.2/eszip.ts', 73 | 'x', 74 | eszipPath, 75 | tmpDir.path, 76 | ]) 77 | 78 | extractCommand.stderr?.pipe(stderr) 79 | extractCommand.stdout?.pipe(stdout) 80 | 81 | await extractCommand 82 | 83 | const virtualRootPath = join(tmpDir.path, 'source', 'root') 84 | const stage2Path = join(virtualRootPath, '..', 'bootstrap-stage2') 85 | const importMapPath = join(virtualRootPath, '..', 'import-map') 86 | 87 | for (const path of [importMapPath, stage2Path]) { 88 | const file = await fs.readFile(path, 'utf8') 89 | 90 | let normalizedFile = file.replace(/file:\/{3}root/g, pathToFileURL(virtualRootPath).toString()) 91 | 92 | if (vendorDirectory !== undefined) { 93 | normalizedFile = normalizedFile.replace(/file:\/{3}vendor/g, pathToFileURL(vendorDirectory).toString()) 94 | } 95 | 96 | await fs.writeFile(path, normalizedFile) 97 | } 98 | 99 | await fs.rename(stage2Path, `${stage2Path}.js`) 100 | 101 | // Run function that imports the extracted stage 2 and invokes each function. 102 | const evalCommand = execa('deno', ['eval', '--no-check', '--import-map', importMapPath, inspectFunction(stage2Path)]) 103 | 104 | evalCommand.stderr?.pipe(stderr) 105 | 106 | const result = await evalCommand 107 | 108 | await tmpDir.cleanup() 109 | 110 | return JSON.parse(result.stdout) 111 | } 112 | 113 | export { fixturesDir, getRouteMatcher, testLogger, runESZIP, useFixture } 114 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | 5 | /* Basic Options */ 6 | "incremental": true /* Enable incremental compilation */, 7 | "target": "ES2019" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */, 8 | "module": "es2020" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */, 9 | // "lib": [], /* Specify library files to be included in the compilation. */ 10 | "allowJs": true /* Allow javascript files to be compiled. */, 11 | // "checkJs": true, /* Report errors in .js files. */ 12 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 13 | "declaration": true /* Generates corresponding '.d.ts' file. */, 14 | "declarationMap": false /* Generates a sourcemap for each corresponding '.d.ts' file. */, 15 | "sourceMap": false /* Generates corresponding '.map' file. */, 16 | // "outFile": "./", /* Concatenate and emit output to single file. */ 17 | "outDir": "./dist" /* Redirect output structure to the directory. */, 18 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 19 | // "composite": true, /* Enable project compilation */ 20 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 21 | "removeComments": false /* Do not emit comments to output. */, 22 | // "noEmit": true, /* Do not emit outputs. */ 23 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 24 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 25 | 26 | /* Strict Type-Checking Options */ 27 | "strict": true /* Enable all strict type-checking options. */, 28 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 29 | // "strictNullChecks": true, /* Enable strict null checks. */ 30 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 31 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 32 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 33 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 34 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 35 | 36 | /* Additional Checks */ 37 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 38 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 39 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 40 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 41 | 42 | /* Module Resolution Options */ 43 | "moduleResolution": "node" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */, 44 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 45 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 46 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 47 | // "typeRoots": [], /* List of folders to include type definitions from. */ 48 | // "types": [], /* Type declaration files to be included in compilation. */ 49 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 50 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, 51 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 52 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 53 | 54 | /* Source Map Options */ 55 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 56 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 57 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 58 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 59 | 60 | /* Experimental Options */ 61 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 62 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 63 | 64 | /* Advanced Options */ 65 | "skipLibCheck": true /* Skip type checking of declaration files. */, 66 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ 67 | }, 68 | "include": ["node", "shared"] 69 | } 70 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config' 2 | 3 | export default defineConfig({ 4 | esbuild: { 5 | target: 'esnext', 6 | }, 7 | test: { 8 | include: ['node/**/*.test.ts'], 9 | testTimeout: 30_000, 10 | }, 11 | }) 12 | --------------------------------------------------------------------------------