Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat!: distribute as ESM #449

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
File renamed without changes.
6 changes: 3 additions & 3 deletions .github/workflows/workflow.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,12 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest, macOS-latest, windows-latest]
node-version: [14.0.0, '*']
node-version: [18.19.0, '*']
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As we still need to support Node 14 in our build shouldn't we keep it that way here as well?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ESM support was still experimental in Node 14: https://nodejs.org/docs/v14.2.0/api/esm.html#esm_enabling

That's why the tests failed before I updated this here: https://github.com/netlify/functions/actions/runs/7113273604/job/19364886077

They became stable in Node 16, so maybe we can set the minimum version to Node 16?

Netlify Build doesn't have a runtime dependency on @netlify/functions, so it should be fine to update this independently. If we're intending to raise the minimum Build version anyway, we might as well start here! Especially since users can choose to stay on the old version of this package, until they upgraded their Node.js to v16+.

exclude:
- os: macOS-latest
node-version: 14.0.0
node-version: 18.19.0
- os: windows-latest
node-version: 14.0.0
node-version: 18.19.0
fail-fast: false
steps:
- name: Git checkout
Expand Down
3 changes: 3 additions & 0 deletions commitlint.config.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module.exports = {
extends: ['@commitlint/config-conventional'],
}
3 changes: 0 additions & 3 deletions commitlint.config.js

This file was deleted.

8,915 changes: 2,522 additions & 6,393 deletions package-lock.json

Large diffs are not rendered by default.

21 changes: 8 additions & 13 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"name": "@netlify/functions",
"type": "module",
"main": "./dist/main.js",
"types": "./dist/main.d.ts",
"version": "2.4.0",
Expand All @@ -25,20 +26,14 @@
"format:fix:prettier": "cross-env-shell prettier --write $npm_package_config_prettier",
"test:dev": "run-s build test:dev:*",
"test:ci": "run-s build test:ci:*",
"test:dev:ava": "ava",
"test:dev:vitest": "vitest run",
"test:dev:tsd": "tsd",
"test:ci:ava": "nyc -r lcovonly -r text -r json ava"
"test:ci:vitest": "vitest run --coverage"
},
"config": {
"eslint": "--ignore-pattern README.md --ignore-path .gitignore --cache --format=codeframe --max-warnings=0 \"{src,scripts,.github,test}/**/*.{ts,js,md,html}\" \"*.{ts,js,md,html}\" \".*.{ts,js,md,html}\"",
"eslint": "--ignore-pattern README.md --ignore-path .gitignore --cache --format=codeframe --max-warnings=0 \"{src,scripts,.github}/**/*.{ts,js,md,html}\"",
"prettier": "--ignore-path .gitignore --loglevel=warn \"{src,scripts,.github}/**/*.{ts,js,md,yml,json,html}\" \"*.{ts,js,yml,json,html}\" \".*.{ts,js,yml,json,html}\" \"!**/package-lock.json\" \"!package-lock.json\""
},
"ava": {
"files": [
"test/unit/*.js"
],
"verbose": true
},
"tsd": {
"directory": "test/types/"
},
Expand All @@ -53,20 +48,20 @@
"test": "test"
},
"dependencies": {
"@netlify/serverless-functions-api": "1.12.1",
"is-promise": "^4.0.0"
"@netlify/serverless-functions-api": "1.12.1"
},
"devDependencies": {
"@commitlint/cli": "^17.0.0",
"@commitlint/config-conventional": "^17.0.0",
"@netlify/eslint-config-node": "^7.0.1",
"ava": "^2.4.0",
"@vitest/coverage-v8": "^1.0.1",
"husky": "^7.0.4",
"npm-run-all": "^4.1.5",
"nyc": "^15.0.0",
"semver": "^7.5.4",
"tsd": "^0.29.0",
"typescript": "^4.4.4"
"typescript": "^4.9.5",
"vitest": "^1.0.1"
},
"engines": {
"node": ">=14.0.0"
Expand Down
13 changes: 1 addition & 12 deletions renovate.json5
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,5 @@
ignorePresets: [':prHourlyLimit2'],
semanticCommits: true,
masterIssue: true,
automerge: true,
packageRules: [
{
// Those cannot be upgraded to a major version until we drop support for Node 8
packageNames: [
'ava',
],
major: {
enabled: false,
},
},
],
automerge: true
}
11 changes: 9 additions & 2 deletions src/lib/builder.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
import isPromise from 'is-promise'

import { BuilderHandler, Handler, HandlerCallback } from '../function/handler.js'
import { HandlerResponse, BuilderResponse } from '../function/handler_response.js'
import { HandlerContext, HandlerEvent } from '../function/index.js'

import { BUILDER_FUNCTIONS_FLAG, HTTP_STATUS_METHOD_NOT_ALLOWED, METADATA_VERSION } from './consts.js'

// stolen from https://github.com/then/is-promise/blob/master/index.mjs
const isPromise = (obj: unknown): obj is Promise<unknown> =>
Skn0tt marked this conversation as resolved.
Show resolved Hide resolved
Boolean(obj) &&
(typeof obj === 'object' || typeof obj === 'function') &&
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
'then' in obj! &&
// eslint-disable-next-line promise/prefer-await-to-then
typeof obj.then === 'function'

const augmentResponse = (response: BuilderResponse) => {
if (!response) {
return response
Expand Down
4 changes: 1 addition & 3 deletions test/helpers/main.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const invokeLambda = (handler, { method = 'GET', ...options } = {}) => {
export const invokeLambda = (handler, { method = 'GET', ...options } = {}) => {
const event = {
...options,
httpMethod: method,
Expand All @@ -16,5 +16,3 @@ const invokeLambda = (handler, { method = 'GET', ...options } = {}) => {
resolve(handler(event, {}, callback))
})
}

module.exports = { invokeLambda }
4 changes: 2 additions & 2 deletions test/helpers/mock_fetch.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
const assert = require('assert')
import assert from 'assert'

module.exports = class MockFetch {
export default class MockFetch {
constructor() {
this.requests = []
}
Expand Down
28 changes: 14 additions & 14 deletions test/unit/builder.js → test/unit/builder.spec.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
const test = require('ava')
import { test } from 'vitest'

const { builder } = require('../../dist/lib/builder')
const { invokeLambda } = require('../helpers/main')
import { builder } from '../../src/main.ts'
import { invokeLambda } from '../helpers/main.js'

const METADATA_OBJECT = { metadata: { version: 1, builder_function: true, ttl: 0 } }

Expand All @@ -22,7 +22,7 @@ test('Injects the metadata object into an asynchronous handler', async (t) => {
}
const response = await invokeLambda(builder(myHandler))

t.deepEqual(response, { ...originalResponse, metadata: { version: 1, builder_function: true, ttl: 3600 } })
t.expect(response).toEqual({ ...originalResponse, metadata: { version: 1, builder_function: true, ttl: 3600 } })
})

test('Injects the metadata object into a synchronous handler', async (t) => {
Expand All @@ -35,7 +35,7 @@ test('Injects the metadata object into a synchronous handler', async (t) => {
}
const response = await invokeLambda(builder(myHandler))

t.deepEqual(response, { ...originalResponse, ...METADATA_OBJECT })
t.expect(response).toEqual({ ...originalResponse, ...METADATA_OBJECT })
})

test('Injects the metadata object for non-200 responses', async (t) => {
Expand All @@ -54,7 +54,7 @@ test('Injects the metadata object for non-200 responses', async (t) => {
}
const response = await invokeLambda(builder(myHandler))

t.deepEqual(response, { ...originalResponse, ...METADATA_OBJECT })
t.expect(response).toEqual({ ...originalResponse, ...METADATA_OBJECT })
})

test('Returns a 405 error for requests using the POST method', async (t) => {
Expand All @@ -73,7 +73,7 @@ test('Returns a 405 error for requests using the POST method', async (t) => {
}
const response = await invokeLambda(builder(myHandler), { method: 'POST' })

t.deepEqual(response, { body: 'Method Not Allowed', statusCode: 405 })
t.expect(response).toEqual({ body: 'Method Not Allowed', statusCode: 405 })
})

test('Returns a 405 error for requests using the PUT method', async (t) => {
Expand All @@ -92,7 +92,7 @@ test('Returns a 405 error for requests using the PUT method', async (t) => {
}
const response = await invokeLambda(builder(myHandler), { method: 'PUT' })

t.deepEqual(response, { body: 'Method Not Allowed', statusCode: 405 })
t.expect(response).toEqual({ body: 'Method Not Allowed', statusCode: 405 })
})

test('Returns a 405 error for requests using the DELETE method', async (t) => {
Expand All @@ -111,7 +111,7 @@ test('Returns a 405 error for requests using the DELETE method', async (t) => {
}
const response = await invokeLambda(builder(myHandler), { method: 'DELETE' })

t.deepEqual(response, { body: 'Method Not Allowed', statusCode: 405 })
t.expect(response).toEqual({ body: 'Method Not Allowed', statusCode: 405 })
})

test('Returns a 405 error for requests using the PATCH method', async (t) => {
Expand All @@ -130,7 +130,7 @@ test('Returns a 405 error for requests using the PATCH method', async (t) => {
}
const response = await invokeLambda(builder(myHandler), { method: 'PATCH' })

t.deepEqual(response, { body: 'Method Not Allowed', statusCode: 405 })
t.expect(response).toEqual({ body: 'Method Not Allowed', statusCode: 405 })
})

test('Preserves errors thrown inside the wrapped handler', async (t) => {
Expand All @@ -148,7 +148,7 @@ test('Preserves errors thrown inside the wrapped handler', async (t) => {
throw error
}

await t.throwsAsync(invokeLambda(builder(myHandler)), { is: error })
await t.expect(() => invokeLambda(builder(myHandler))).rejects.toEqual(error)
})

test('Does not pass query parameters to the wrapped handler', async (t) => {
Expand All @@ -158,8 +158,8 @@ test('Does not pass query parameters to the wrapped handler', async (t) => {
}
// eslint-disable-next-line require-await
const myHandler = async (event) => {
t.deepEqual(event.multiValueQueryStringParameters, {})
t.deepEqual(event.queryStringParameters, {})
t.expect(event.multiValueQueryStringParameters).toEqual({})
t.expect(event.queryStringParameters).toEqual({})

return originalResponse
}
Expand All @@ -170,5 +170,5 @@ test('Does not pass query parameters to the wrapped handler', async (t) => {
queryStringParameters,
})

t.deepEqual(response, { ...originalResponse, ...METADATA_OBJECT })
t.expect(response).toEqual({ ...originalResponse, ...METADATA_OBJECT })
})
40 changes: 19 additions & 21 deletions test/unit/purge_cache.js → test/unit/purge_cache.spec.js
Original file line number Diff line number Diff line change
@@ -1,29 +1,28 @@
const process = require('process')
import process from 'process'

const test = require('ava')
const semver = require('semver')
import semver from 'semver'
import { test, beforeEach, afterEach } from 'vitest'

const { purgeCache } = require('../../dist/lib/purge_cache')
const { invokeLambda } = require('../helpers/main')
const MockFetch = require('../helpers/mock_fetch')
import { purgeCache } from '../../src/main.ts'
import { invokeLambda } from '../helpers/main.js'
import MockFetch from '../helpers/mock_fetch.js'

const globalFetch = globalThis.fetch
const hasFetchAPI = semver.gte(process.version, '18.0.0')

test.beforeEach(() => {
beforeEach(() => {
delete process.env.NETLIFY_PURGE_API_TOKEN
delete process.env.SITE_ID
})

test.afterEach(() => {
afterEach(() => {
globalThis.fetch = globalFetch
})

test.serial('Calls the purge API endpoint and returns `undefined` if the operation was successful', async (t) => {
test.sequential('Calls the purge API endpoint and returns `undefined` if the operation was successful', async (t) => {
if (!hasFetchAPI) {
console.warn('Skipping test requires the fetch API')

return t.pass()
return t.skip()
}

const mockSiteID = '123456789'
Expand All @@ -36,7 +35,7 @@ test.serial('Calls the purge API endpoint and returns `undefined` if the operati
body: (payload) => {
const data = JSON.parse(payload)

t.is(data.site_id, mockSiteID)
t.expect(data.site_id).toEqual(mockSiteID)
},
headers: { Authorization: `Bearer ${mockToken}` },
method: 'post',
Expand All @@ -51,15 +50,15 @@ test.serial('Calls the purge API endpoint and returns `undefined` if the operati

const response = await invokeLambda(myFunction)

t.is(response, undefined)
t.true(mockAPI.fulfilled)
t.expect(response).toBeUndefined()
t.expect(mockAPI.fulfilled).toBe(true)
})

test.serial('Throws if the API response does not have a successful status code', async (t) => {
test.sequential('Throws if the API response does not have a successful status code', async (t) => {
if (!hasFetchAPI) {
console.warn('Skipping test requires the fetch API')

return t.pass()
return t.skip()
}

const mockSiteID = '123456789'
Expand All @@ -72,7 +71,7 @@ test.serial('Throws if the API response does not have a successful status code',
body: (payload) => {
const data = JSON.parse(payload)

t.is(data.site_id, mockSiteID)
t.expect(data.site_id).toEqual(mockSiteID)
},
headers: { Authorization: `Bearer ${mockToken}` },
method: 'post',
Expand All @@ -85,8 +84,7 @@ test.serial('Throws if the API response does not have a successful status code',

globalThis.fetch = mockAPI.fetcher

await t.throwsAsync(
async () => await invokeLambda(myFunction),
'Cache purge API call returned an unexpected status code: 500',
)
await t
.expect(async () => await invokeLambda(myFunction))
.rejects.toThrowError('Cache purge API call returned an unexpected status code: 500')
})
4 changes: 2 additions & 2 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */

/* Modules */
"module": "commonjs" /* Specify what module code is generated. */,
"module": "NodeNext" /* Specify what module code is generated. */,
"rootDir": "./src" /* Specify the root folder within your source files. */,
// "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */
"moduleResolution": "NodeNext" /* Specify how TypeScript looks up a file from a given module specifier. */,
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
Expand Down