From 4b11e693248e44a8c6db4a95cf90e79e00f7db08 Mon Sep 17 00:00:00 2001 From: Dominik Kundel Date: Thu, 20 May 2021 16:39:18 -0700 Subject: [PATCH 01/13] feat: extract runtime-handler and lazyLoading (#252) --- jest.config.base.js | 2 +- package.json | 2 +- .../__snapshots__/integration.test.ts.snap | 57 +++ .../__tests__/dev-runtime/integration.test.ts | 187 +++++++++ .../dev-runtime/internal/response.test.ts | 86 ++++ .../__tests__/dev-runtime/route.test.ts | 372 ++++++++++++++++++ .../__tests__/runtime-handler.test.ts | 12 +- .../runtime-handler/fixtures/assets/hello.js | 1 + .../fixtures/functions/basic-error.js | 3 + .../fixtures/functions/basic-twiml.js | 5 + packages/runtime-handler/jest.config.js | 2 +- packages/runtime-handler/package.json | 31 +- .../dev-runtime/checks/check-account-sid.ts | 58 +++ .../dev-runtime/checks/check-auth-token.ts | 55 +++ .../src/dev-runtime/dependencies.d.ts | 5 + .../src/dev-runtime/dev-runtime.ts | 2 + .../dev-runtime/internal/functionRunner.ts | 106 +++++ .../dev-runtime/internal/request-logger.ts | 73 ++++ .../src/dev-runtime/internal/response.ts | 65 +++ .../src/dev-runtime/internal/route-cache.ts | 59 +++ .../src/dev-runtime/internal/runtime.ts | 97 +++++ .../runtime-handler/src/dev-runtime/route.ts | 267 +++++++++++++ .../runtime-handler/src/dev-runtime/server.ts | 244 ++++++++++++ .../runtime-handler/src/dev-runtime/types.ts | 88 +++++ .../src/dev-runtime/utils/debug.ts | 81 ++++ .../src/dev-runtime/utils/error-html.ts | 61 +++ .../src/dev-runtime/utils/inspector.ts | 22 ++ .../dev-runtime/utils/requireFromProject.ts | 6 + .../dev-runtime/utils/stack-trace/clean-up.ts | 19 + .../dev-runtime/utils/stack-trace/helpers.ts | 62 +++ packages/serverless-api/jest.config.js | 12 +- packages/serverless-runtime-types/types.d.ts | 9 +- .../__snapshots__/integration.test.ts.snap | 14 + .../__tests__/runtime/integration.test.ts | 2 +- packages/twilio-run/jest.config.js | 2 +- packages/twilio-run/package.json | 1 + packages/twilio-run/src/commands/start.ts | 4 +- .../src/runtime/internal/route-cache.ts | 8 +- .../src/runtime/internal/runtime.ts | 4 +- packages/twilio-run/src/runtime/route.ts | 25 +- packages/twilio-run/src/runtime/server.ts | 110 +++++- packages/twilio-run/src/utils/logger.ts | 2 +- .../src/utils/requireFromProject.ts | 6 + 43 files changed, 2288 insertions(+), 41 deletions(-) create mode 100644 packages/runtime-handler/__tests__/dev-runtime/__snapshots__/integration.test.ts.snap create mode 100644 packages/runtime-handler/__tests__/dev-runtime/integration.test.ts create mode 100644 packages/runtime-handler/__tests__/dev-runtime/internal/response.test.ts create mode 100644 packages/runtime-handler/__tests__/dev-runtime/route.test.ts create mode 100644 packages/runtime-handler/fixtures/assets/hello.js create mode 100644 packages/runtime-handler/fixtures/functions/basic-error.js create mode 100644 packages/runtime-handler/fixtures/functions/basic-twiml.js create mode 100644 packages/runtime-handler/src/dev-runtime/checks/check-account-sid.ts create mode 100644 packages/runtime-handler/src/dev-runtime/checks/check-auth-token.ts create mode 100644 packages/runtime-handler/src/dev-runtime/dependencies.d.ts create mode 100644 packages/runtime-handler/src/dev-runtime/dev-runtime.ts create mode 100644 packages/runtime-handler/src/dev-runtime/internal/functionRunner.ts create mode 100644 packages/runtime-handler/src/dev-runtime/internal/request-logger.ts create mode 100644 packages/runtime-handler/src/dev-runtime/internal/response.ts create mode 100644 packages/runtime-handler/src/dev-runtime/internal/route-cache.ts create mode 100644 packages/runtime-handler/src/dev-runtime/internal/runtime.ts create mode 100644 packages/runtime-handler/src/dev-runtime/route.ts create mode 100644 packages/runtime-handler/src/dev-runtime/server.ts create mode 100644 packages/runtime-handler/src/dev-runtime/types.ts create mode 100644 packages/runtime-handler/src/dev-runtime/utils/debug.ts create mode 100644 packages/runtime-handler/src/dev-runtime/utils/error-html.ts create mode 100644 packages/runtime-handler/src/dev-runtime/utils/inspector.ts create mode 100644 packages/runtime-handler/src/dev-runtime/utils/requireFromProject.ts create mode 100644 packages/runtime-handler/src/dev-runtime/utils/stack-trace/clean-up.ts create mode 100644 packages/runtime-handler/src/dev-runtime/utils/stack-trace/helpers.ts create mode 100644 packages/twilio-run/src/utils/requireFromProject.ts diff --git a/jest.config.base.js b/jest.config.base.js index 6f0c549f..d2d482cd 100644 --- a/jest.config.base.js +++ b/jest.config.base.js @@ -13,7 +13,7 @@ module.exports = { // A set of global variables that need to be available in all test environments globals: { 'ts-jest': { - tsConfig: 'tsconfig.test.json', + tsconfig: 'tsconfig.test.json', }, }, // The test environment that will be used for testing diff --git a/package.json b/package.json index 3c314a51..353d38a5 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "lerna": "^3.22.1", "lint-staged": "^8.2.1", "npm-run-all": "^4.1.5", - "prettier": "^1.18.2", + "prettier": "^2.2.1", "rimraf": "^3.0.2", "ts-jest": "^26.0.0", "typescript": "^3.9.7" diff --git a/packages/runtime-handler/__tests__/dev-runtime/__snapshots__/integration.test.ts.snap b/packages/runtime-handler/__tests__/dev-runtime/__snapshots__/integration.test.ts.snap new file mode 100644 index 00000000..140fccf0 --- /dev/null +++ b/packages/runtime-handler/__tests__/dev-runtime/__snapshots__/integration.test.ts.snap @@ -0,0 +1,57 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`with an express app with forked process function handling Function integration tests basic-twiml.js should match snapshot 1`] = ` +Object { + "body": Object {}, + "headers": Object { + "cache-control": "no-store, no-cache, must-revalidate, proxy-revalidate", + "connection": "close", + "content-type": "text/xml; charset=utf-8", + "expires": "0", + "pragma": "no-cache", + "surrogate-control": "no-store", + "x-powered-by": "Express", + }, + "statusCode": 200, + "text": "Hello World", + "type": "text/xml", +} +`; + +exports[`with an express app with inline function handling Assets integration tests hello.js should match snapshot 1`] = ` +Object { + "body": Object {}, + "headers": Object { + "accept-ranges": "bytes", + "cache-control": "no-store, no-cache, must-revalidate, proxy-revalidate", + "connection": "close", + "content-type": "application/javascript; charset=UTF-8", + "expires": "0", + "pragma": "no-cache", + "surrogate-control": "no-store", + "x-powered-by": "Express", + }, + "statusCode": 200, + "text": "alert('Hello world!'); +", + "type": "application/javascript", +} +`; + +exports[`with an express app with inline function handling Function integration tests basic-twiml.js should match snapshot 1`] = ` +Object { + "body": Object {}, + "headers": Object { + "cache-control": "no-store, no-cache, must-revalidate, proxy-revalidate", + "connection": "close", + "content-type": "text/xml; charset=utf-8", + "expires": "0", + "pragma": "no-cache", + "surrogate-control": "no-store", + "x-powered-by": "Express", + }, + "statusCode": 200, + "text": "Hello World", + "type": "text/xml", +} +`; diff --git a/packages/runtime-handler/__tests__/dev-runtime/integration.test.ts b/packages/runtime-handler/__tests__/dev-runtime/integration.test.ts new file mode 100644 index 00000000..9595b512 --- /dev/null +++ b/packages/runtime-handler/__tests__/dev-runtime/integration.test.ts @@ -0,0 +1,187 @@ +jest.unmock('twilio'); + +import { Express } from 'express'; +import { readdirSync, readFileSync } from 'fs'; +import { basename, resolve } from 'path'; +import request from 'supertest'; +import { LocalDevelopmentServer } from '../../src/dev-runtime/server'; +import { + ServerConfig, + ServerlessResourceConfigWithFilePath, +} from '../../src/dev-runtime/types'; + +const TEST_DIR = resolve(__dirname, '../../fixtures'); + +const TEST_FUNCTIONS_DIR = resolve(TEST_DIR, 'functions'); +const TEST_ASSETS_DIR = resolve(TEST_DIR, 'assets'); +const TEST_ENV = {}; + +const availableFunctions: ServerlessResourceConfigWithFilePath[] = readdirSync( + TEST_FUNCTIONS_DIR +).map((name: string) => { + const filePath = resolve(TEST_FUNCTIONS_DIR, name); + const content = readFileSync(filePath, 'utf8'); + const url = `/${basename(name, '.js')}`; + return { name, path: url, filePath, access: 'public', content }; +}); +const availableAssets: ServerlessResourceConfigWithFilePath[] = readdirSync( + TEST_ASSETS_DIR +).map((name: string) => { + const filePath = resolve(TEST_ASSETS_DIR, name); + const url = `/${name}`; + const content = readFileSync(filePath); + return { name, filePath, path: url, access: 'public', content }; +}); + +const BASE_CONFIG: ServerConfig = { + baseDir: TEST_DIR, + env: TEST_ENV, + port: 9000, + url: 'http://localhost:9000', + detailedLogs: false, + live: true, + logs: false, + legacyMode: false, + appName: 'integration-test', + forkProcess: false, + logger: undefined, + routes: { assets: [], functions: [] }, + enableDebugLogs: false, +}; + +type InternalResponse = request.Response & { + statusCode: number; + headers: { + [key: string]: string | undefined; + }; +}; + +function responseToSnapshotJson(response: InternalResponse) { + let { statusCode, type, body, text, headers } = response; + delete headers['date']; + delete headers['last-modified']; + + if (text && text.startsWith('Error')) { + // stack traces are different in every environment + // let's not snapshot values that rely on it + text = `${text.split('\n')[0]} ...`; + } + delete headers['content-length']; + delete headers['etag']; + + return { + statusCode, + type, + body, + text, + headers, + }; +} + +describe('with an express app', () => { + let app: Express; + + describe('with inline function handling', () => { + beforeAll(async () => { + app = new LocalDevelopmentServer(9000, { + ...BASE_CONFIG, + routes: { + assets: availableAssets, + functions: availableFunctions, + }, + forkProcess: false, + } as ServerConfig).getApp(); + }); + + describe('Function integration tests', () => { + for (const testFnCode of availableFunctions) { + test(`${testFnCode.name} should match snapshot`, async () => { + const response = await request(app).get(testFnCode.path); + if (response.status === 500) { + expect(response.text).toMatch(/Error/); + } else { + const result = responseToSnapshotJson(response as InternalResponse); + expect(result).toMatchSnapshot(); + } + }); + } + }); + + describe('Assets integration tests', () => { + for (const testAsset of availableAssets) { + test(`${testAsset.name} should match snapshot`, async () => { + const response = await request(app).get(testAsset.path); + const result = responseToSnapshotJson(response as InternalResponse); + expect(result).toMatchSnapshot(); + }); + + test(`OPTIONS request to ${testAsset.name} should return CORS headers and no body`, async () => { + const response = (await request(app).options( + testAsset.path + )) as InternalResponse; + expect(response.headers['access-control-allow-origin']).toEqual('*'); + expect(response.headers['access-control-allow-headers']).toEqual( + 'Accept, Authorization, Content-Type, If-Match, If-Modified-Since, If-None-Match, If-Unmodified-Since, User-Agent' + ); + expect(response.headers['access-control-allow-methods']).toEqual( + 'GET, POST, OPTIONS' + ); + expect(response.headers['access-control-expose-headers']).toEqual( + 'ETag' + ); + expect(response.headers['access-control-max-age']).toEqual('86400'); + expect(response.headers['access-control-allow-credentials']).toEqual( + 'true' + ); + expect(response.text).toEqual(''); + }); + + test(`GET request to ${testAsset.name} should not return CORS headers`, async () => { + const response = (await request(app).get( + testAsset.path + )) as InternalResponse; + expect( + response.headers['access-control-allow-origin'] + ).toBeUndefined(); + expect( + response.headers['access-control-allow-headers'] + ).toBeUndefined(); + expect( + response.headers['access-control-allow-methods'] + ).toBeUndefined(); + expect( + response.headers['access-control-expose-headers'] + ).toBeUndefined(); + expect(response.headers['access-control-max-age']).toBeUndefined(); + expect( + response.headers['access-control-allow-credentials'] + ).toBeUndefined(); + }); + } + }); + }); + + describe('with forked process function handling', () => { + beforeAll(async () => { + app = new LocalDevelopmentServer(9000, { + ...BASE_CONFIG, + routes: { assets: availableAssets, functions: availableFunctions }, + forkProcess: true, + } as ServerConfig).getApp(); + }); + + describe('Function integration tests', () => { + for (const testFnCode of availableFunctions) { + test(`${testFnCode.name} should match snapshot`, async () => { + const response = await request(app).get(testFnCode.path); + if (response.status === 500) { + expect(response.text).toMatch(/Error/); + } else { + const result = responseToSnapshotJson(response as InternalResponse); + expect(result).toMatchSnapshot(); + } + }); + } + }); + }); +}); diff --git a/packages/runtime-handler/__tests__/dev-runtime/internal/response.test.ts b/packages/runtime-handler/__tests__/dev-runtime/internal/response.test.ts new file mode 100644 index 00000000..294857a8 --- /dev/null +++ b/packages/runtime-handler/__tests__/dev-runtime/internal/response.test.ts @@ -0,0 +1,86 @@ +import { Response as ExpressResponse } from 'express'; +import { Response } from '../../../src/dev-runtime/internal/response'; + +test('has correct defaults', () => { + const response = new Response(); + expect(response['body']).toBeUndefined(); + expect(response['statusCode']).toBe(200); + expect(response['headers']).toEqual({}); +}); + +test('sets status code', () => { + const response = new Response(); + expect(response['statusCode']).toBe(200); + response.setStatusCode(418); + expect(response['statusCode']).toBe(418); +}); + +test('sets body correctly', () => { + const response = new Response(); + expect(response['body']).toBeUndefined(); + response.setBody('Hello'); + expect(response['body']).toBe('Hello'); + response.setBody({ url: 'https://dkundel.com' }); + expect(response['body']).toEqual({ url: 'https://dkundel.com' }); +}); + +test('sets headers correctly', () => { + const response = new Response(); + expect(response['headers']).toEqual({}); + response.setHeaders({ + 'Access-Control-Allow-Origin': 'example.com', + 'Access-Control-Allow-Methods': 'GET,PUT,POST,DELETE', + 'Access-Control-Allow-Headers': 'Content-Type', + }); + const expected = { + 'Access-Control-Allow-Origin': 'example.com', + 'Access-Control-Allow-Methods': 'GET,PUT,POST,DELETE', + 'Access-Control-Allow-Headers': 'Content-Type', + }; + expect(response['headers']).toEqual(expected); + // @ts-ignore + response.setHeaders(undefined); + expect(response['headers']).toEqual(expected); +}); + +test('appends a new header correctly', () => { + const response = new Response(); + expect(response['headers']).toEqual({}); + response.appendHeader('Access-Control-Allow-Origin', 'dkundel.com'); + expect(response['headers']).toEqual({ + 'Access-Control-Allow-Origin': 'dkundel.com', + }); + response.appendHeader('Content-Type', 'application/json'); + expect(response['headers']).toEqual({ + 'Access-Control-Allow-Origin': 'dkundel.com', + 'Content-Type': 'application/json', + }); +}); + +test('appends a header correctly with no existing one', () => { + const response = new Response(); + expect(response['headers']).toEqual({}); + // @ts-ignore + response['headers'] = undefined; + response.appendHeader('Access-Control-Allow-Origin', 'dkundel.com'); + expect(response['headers']).toEqual({ + 'Access-Control-Allow-Origin': 'dkundel.com', + }); +}); + +test('calls express response correctly', () => { + const mockRes = ({ + status: jest.fn(), + set: jest.fn(), + send: jest.fn(), + } as unknown) as ExpressResponse; + const response = new Response(); + response.setBody(`I'm a teapot!`); + response.setStatusCode(418); + response.appendHeader('Content-Type', 'text/plain'); + response.applyToExpressResponse(mockRes); + + expect(mockRes.send).toHaveBeenCalledWith(`I'm a teapot!`); + expect(mockRes.status).toHaveBeenCalledWith(418); + expect(mockRes.set).toHaveBeenCalledWith({ 'Content-Type': 'text/plain' }); +}); diff --git a/packages/runtime-handler/__tests__/dev-runtime/route.test.ts b/packages/runtime-handler/__tests__/dev-runtime/route.test.ts new file mode 100644 index 00000000..45fb2e78 --- /dev/null +++ b/packages/runtime-handler/__tests__/dev-runtime/route.test.ts @@ -0,0 +1,372 @@ +jest.mock('../../node_modules/twilio', () => { + // because we don't do a traditional require of twilio but a "project require" we have to mock this differently. + + const actualTwilio = jest.requireActual('twilio'); + const twilio: any = jest.genMockFromModule('twilio'); + + twilio['twiml'] = actualTwilio.twiml; + return twilio; +}); + +import '@twilio-labs/serverless-runtime-types'; +import { + Request as ExpressRequest, + Response as ExpressResponse, +} from 'express'; +import type { UserAgent } from 'express-useragent'; +import { Request as MockRequest } from 'jest-express/lib/request'; +import { Response as MockResponse } from 'jest-express/lib/response'; +import path from 'path'; +import { twiml } from 'twilio'; +import { Response } from '../../src/dev-runtime/internal/response'; +import { + constructContext, + constructEvent, + constructGlobalScope, + handleError, + handleSuccess, + isTwiml, +} from '../../src/dev-runtime/route'; +import { + EnvironmentVariablesWithAuth, + ServerConfig, +} from '../../src/dev-runtime/types'; +import { wrapErrorInHtml } from '../../src/dev-runtime/utils/error-html'; +import { requireFromProject } from '../../src/dev-runtime/utils/requireFromProject'; +import { cleanUpStackTrace } from '../../src/dev-runtime/utils/stack-trace/clean-up'; + +const { VoiceResponse, MessagingResponse, FaxResponse } = twiml; + +const mockResponse = (new MockResponse() as unknown) as ExpressResponse; +mockResponse.type = jest.fn(() => mockResponse); + +function asExpressRequest(req: { query?: {}; body?: {} }): ExpressRequest { + return (req as unknown) as ExpressRequest; +} + +describe('handleError function', () => { + test('returns string error', () => { + const mockRequest = (new MockRequest() as unknown) as ExpressRequest; + mockRequest['useragent'] = { + isDesktop: true, + isMobile: false, + } as UserAgent; + + handleError('string error', mockRequest, mockResponse); + + expect(mockResponse.status).toHaveBeenCalledWith(500); + expect(mockResponse.send).toHaveBeenCalledWith('string error'); + }); + + test('handles objects as error argument', () => { + const mockRequest = (new MockRequest() as unknown) as ExpressRequest; + mockRequest['useragent'] = { + isDesktop: true, + isMobile: false, + } as UserAgent; + + handleError({ errorMessage: 'oh no' }, mockRequest, mockResponse); + + expect(mockResponse.status).toHaveBeenCalledWith(500); + expect(mockResponse.send).toHaveBeenCalledWith({ errorMessage: 'oh no' }); + }); + + test('wraps error object for desktop requests', () => { + const mockRequest = (new MockRequest() as unknown) as ExpressRequest; + mockRequest['useragent'] = { + isDesktop: true, + isMobile: false, + } as UserAgent; + + const err = new Error('Failed to execute'); + handleError(err, mockRequest, mockResponse); + expect(mockResponse.status).toHaveBeenCalledWith(500); + expect(mockResponse.send).toHaveBeenCalledWith(wrapErrorInHtml(err)); + }); + + test('wraps error object for mobile requests', () => { + const mockRequest = (new MockRequest() as unknown) as ExpressRequest; + mockRequest['useragent'] = { + isDesktop: false, + isMobile: true, + } as UserAgent; + + const err = new Error('Failed to execute'); + handleError(err, mockRequest, mockResponse); + expect(mockResponse.status).toHaveBeenCalledWith(500); + expect(mockResponse.send).toHaveBeenCalledWith(wrapErrorInHtml(err)); + }); + + test('returns string version of error for other requests', () => { + const mockRequest = (new MockRequest() as unknown) as ExpressRequest; + mockRequest['useragent'] = { + isDesktop: false, + isMobile: false, + } as UserAgent; + + const err = new Error('Failed to execute'); + const cleanedupError = cleanUpStackTrace(err); + handleError(err, mockRequest, mockResponse); + expect(mockResponse.status).toHaveBeenCalledWith(500); + expect(mockResponse.send).toHaveBeenCalledWith({ + message: 'Failed to execute', + name: 'Error', + stack: cleanedupError.stack, + }); + }); +}); + +describe('constructEvent function', () => { + test('merges query and body', () => { + const event = constructEvent( + asExpressRequest({ + body: { + Body: 'Hello', + }, + query: { + index: 5, + }, + }) + ); + expect(event).toEqual({ Body: 'Hello', index: 5 }); + }); + + test('overrides query with body', () => { + const event = constructEvent( + asExpressRequest({ + body: { + Body: 'Bye', + }, + query: { + Body: 'Hello', + From: '+123456789', + }, + }) + ); + expect(event).toEqual({ Body: 'Bye', From: '+123456789' }); + }); + + test('handles empty body', () => { + const event = constructEvent( + asExpressRequest({ + body: {}, + query: { + Body: 'Hello', + From: '+123456789', + }, + }) + ); + expect(event).toEqual({ Body: 'Hello', From: '+123456789' }); + }); + + test('handles empty query', () => { + const event = constructEvent( + asExpressRequest({ + body: { + Body: 'Hello', + From: '+123456789', + }, + query: {}, + }) + ); + expect(event).toEqual({ Body: 'Hello', From: '+123456789' }); + }); + + test('handles both empty', () => { + const event = constructEvent( + asExpressRequest({ + body: {}, + query: {}, + }) + ); + expect(event).toEqual({}); + }); +}); + +describe('isTwiml', () => { + test('detects Voice TwiML correctly', () => { + const twiml = new VoiceResponse(); + expect(isTwiml(twiml)).toBeTruthy(); + }); + + test('detects Messaging TwiML correctly', () => { + const twiml = new MessagingResponse(); + expect(isTwiml(twiml)).toBeTruthy(); + }); + + test('detects Fax TwiML correctly', () => { + const twiml = new FaxResponse(); + expect(isTwiml(twiml)).toBeTruthy(); + }); + + test('detects invalid object', () => { + const notTwiml = new Date(); + expect(isTwiml(notTwiml)).toBeFalsy(); + const alsoNotTwiml = {}; + expect(isTwiml(alsoNotTwiml)).toBeFalsy(); + }); +}); + +describe('constructContext function', () => { + test('returns correct values', () => { + const config = { + url: 'http://localhost:8000', + env: { + ACCOUNT_SID: 'ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', + AUTH_TOKEN: 'authauthauthauthauthauthauthauth', + }, + baseDir: path.resolve(__dirname, '../../'), + } as ServerConfig; + const context = constructContext(config, '/test'); + expect(context.DOMAIN_NAME).toBe('localhost:8000'); + expect(context.PATH).toBe('/test'); + expect(context.ACCOUNT_SID).toBe('ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'); + expect(context.AUTH_TOKEN).toBe('authauthauthauthauthauthauthauth'); + expect(typeof context.getTwilioClient).toBe('function'); + }); + + test('does not override existing PATH values', () => { + const env: EnvironmentVariablesWithAuth = { + ACCOUNT_SID: 'ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', + AUTH_TOKEN: 'authauthauthauthauthauthauthauth', + PATH: '/usr/bin:/bin', + }; + + const config = { + url: 'http://localhost:8000', + env, + baseDir: path.resolve(__dirname, '../../'), + } as ServerConfig; + const context = constructContext(config, '/test2'); + expect(context.PATH).toBe('/usr/bin:/bin'); + }); + + test('does not override existing DOMAIN_NAME values', () => { + const env: EnvironmentVariablesWithAuth = { + ACCOUNT_SID: 'ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', + AUTH_TOKEN: 'authauthauthauthauthauthauthauth', + DOMAIN_NAME: 'hello.world', + }; + + const config = { + url: 'http://localhost:8000', + env, + baseDir: path.resolve(__dirname, '../../'), + } as ServerConfig; + const context = constructContext(config, '/test2'); + expect(context.DOMAIN_NAME).toBe('hello.world'); + }); + + test('getTwilioClient calls twilio constructor', () => { + const ACCOUNT_SID = 'ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'; + const AUTH_TOKEN = 'authauthauthauthauthauthauthauth'; + + const config = { + url: 'http://localhost:8000', + env: { ACCOUNT_SID, AUTH_TOKEN }, + baseDir: path.resolve(__dirname, '../../'), + } as ServerConfig; + const context = constructContext(config, '/test'); + const twilioFn = requireFromProject(config.baseDir, 'twilio'); + context.getTwilioClient(); + expect(twilioFn).toHaveBeenCalledWith( + 'ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', + 'authauthauthauthauthauthauthauth', + { lazyLoading: true } + ); + }); +}); + +describe('constructGlobalScope function', () => { + const ACCOUNT_SID = 'ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'; + const AUTH_TOKEN = 'authauthauthauthauthauthauthauth'; + let config: ServerConfig; + + function resetGlobals() { + // @ts-ignore + global['Twilio'] = undefined; + // @ts-ignore + global['Runtime'] = undefined; + // @ts-ignore + global['Response'] = undefined; + // @ts-ignore + global['twilioClient'] = null; + // @ts-ignore + global['Functions'] = undefined; + } + + beforeEach(() => { + config = { + url: 'http://localhost:8000', + env: { ACCOUNT_SID, AUTH_TOKEN }, + baseDir: path.resolve(__dirname, '../../'), + } as ServerConfig; + resetGlobals(); + }); + + afterEach(() => { + config = {} as ServerConfig; + resetGlobals(); + }); + + test('sets the correct global variables', () => { + expect(global.Twilio).toBeUndefined(); + expect(global.Runtime).toBeUndefined(); + expect(global.Response).toBeUndefined(); + expect(global.twilioClient).toBeNull(); + expect(global.Functions).toBeUndefined(); + constructGlobalScope(config); + + const twilio = requireFromProject(config.baseDir, 'twilio'); + + expect(global.Twilio).toEqual({ ...twilio, Response }); + expect(typeof global.Runtime.getAssets).toBe('function'); + expect(typeof global.Runtime.getFunctions).toBe('function'); + expect(typeof global.Runtime.getSync).toBe('function'); + expect(Response).toEqual(Response); + expect(twilioClient).not.toBeNull(); + expect(global.Functions).not.toBeUndefined(); + }); +}); + +describe('handleSuccess function', () => { + test('handles string responses', () => { + handleSuccess('Yay', mockResponse); + expect(mockResponse.status).toHaveBeenCalledWith(200); + expect(mockResponse.send).toHaveBeenCalledWith('Yay'); + expect(mockResponse.type).toHaveBeenCalledWith('text/plain'); + }); + + test('handles twiml responses', () => { + const twiml = new MessagingResponse(); + twiml.message('Hello'); + handleSuccess(twiml, mockResponse); + expect(mockResponse.status).toHaveBeenCalledWith(200); + expect(mockResponse.send).toHaveBeenCalledWith(twiml.toString()); + expect(mockResponse.type).toHaveBeenCalledWith('text/xml'); + }); + + test('handles Response instances', () => { + const resp = new Response(); + resp.setBody({ data: 'Something' }); + resp.setStatusCode(418); + resp.setHeaders({ + 'Content-Type': 'application/json', + }); + handleSuccess(resp, mockResponse); + expect(mockResponse.status).toHaveBeenCalledWith(418); + expect(mockResponse.send).toHaveBeenCalledWith({ data: 'Something' }); + expect(mockResponse.set).toHaveBeenCalledWith({ + 'Content-Type': 'application/json', + }); + expect(mockResponse.type).not.toHaveBeenCalled(); + }); + + test('sends plain objects', () => { + const data = { values: [1, 2, 3] }; + handleSuccess(data, mockResponse); + expect(mockResponse.status).toHaveBeenCalledWith(200); + expect(mockResponse.send).toHaveBeenCalledWith({ values: [1, 2, 3] }); + expect(mockResponse.type).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/runtime-handler/__tests__/runtime-handler.test.ts b/packages/runtime-handler/__tests__/runtime-handler.test.ts index 88e7da8c..629907f5 100644 --- a/packages/runtime-handler/__tests__/runtime-handler.test.ts +++ b/packages/runtime-handler/__tests__/runtime-handler.test.ts @@ -1,9 +1,5 @@ -'use strict'; - -const runtimeHandler = require('..'); - -describe('@twilio/runtime-handler', () => { - test('exports nothing', () => { - expect(runtimeHandler).toEqual({}); - }); +test('base should not be importable', () => { + expect(() => { + require('..'); + }).toThrow(); }); diff --git a/packages/runtime-handler/fixtures/assets/hello.js b/packages/runtime-handler/fixtures/assets/hello.js new file mode 100644 index 00000000..1d156df3 --- /dev/null +++ b/packages/runtime-handler/fixtures/assets/hello.js @@ -0,0 +1 @@ +alert('Hello world!'); diff --git a/packages/runtime-handler/fixtures/functions/basic-error.js b/packages/runtime-handler/fixtures/functions/basic-error.js new file mode 100644 index 00000000..361b3a25 --- /dev/null +++ b/packages/runtime-handler/fixtures/functions/basic-error.js @@ -0,0 +1,3 @@ +exports.handler = function (context, event, callback) { + callback(new Error(`I'm throwing`)); +}; diff --git a/packages/runtime-handler/fixtures/functions/basic-twiml.js b/packages/runtime-handler/fixtures/functions/basic-twiml.js new file mode 100644 index 00000000..266e4eed --- /dev/null +++ b/packages/runtime-handler/fixtures/functions/basic-twiml.js @@ -0,0 +1,5 @@ +exports.handler = function (context, event, callback) { + let twiml = new Twilio.twiml.MessagingResponse(); + twiml.message('Hello World'); + callback(null, twiml); +}; diff --git a/packages/runtime-handler/jest.config.js b/packages/runtime-handler/jest.config.js index b016a213..c1f12360 100644 --- a/packages/runtime-handler/jest.config.js +++ b/packages/runtime-handler/jest.config.js @@ -4,7 +4,7 @@ module.exports = { ...base, globals: { 'ts-jest': { - tsConfig: './tsconfig.test.json', + tsconfig: './tsconfig.test.json', }, }, name: 'serverless-api', diff --git a/packages/runtime-handler/package.json b/packages/runtime-handler/package.json index f54c2a31..b5f5bc32 100644 --- a/packages/runtime-handler/package.json +++ b/packages/runtime-handler/package.json @@ -9,8 +9,12 @@ "author": "Twilio Inc. (https://www.twilio.com/labs)", "homepage": "https://github.com/twilio-labs/serverless-toolkit/tree/main/packages/runtime-handler#readme", "license": "MIT", - "main": "dist/index.js", - "types": "dist/index.d.ts", + "main": "dist/invalid.js", + "types": "dist/invalid.d.ts", + "exports": { + ".": "./dist/invalid.js", + "./dev": "./dist/dev-runtime/dev-runtime.js" + }, "directories": { "src": "src", "test": "__tests__", @@ -39,13 +43,36 @@ }, "devDependencies": { "@types/jest": "^24.0.16", + "@types/common-tags": "^1.8.0", + "@types/debug": "^4.1.4", + "@types/express-useragent": "^0.2.21", + "@types/lodash.debounce": "^4.0.6", + "@types/node": "^14.0.19", + "@types/supertest": "^2.0.8", "jest": "^24.8.0", "npm-run-all": "^4.1.5", "rimraf": "^2.6.3", + "supertest": "^3.1.0", "ts-jest": "^24.0.2", + "twilio": "^3.60.0", "typescript": "^3.8.3" }, "bugs": { "url": "https://github.com/twilio-labs/serverless-toolkit/issues" + }, + "dependencies": { + "@twilio-labs/serverless-runtime-types": "^2.0.0-beta.3", + "@types/express": "4.17.7", + "chalk": "^4.1.1", + "chokidar": "^3.2.3", + "common-tags": "^1.8.0", + "debug": "^3.1.0", + "express": "^4.16.3", + "express-useragent": "^1.0.13", + "fast-redact": "^1.5.0", + "lodash.debounce": "^4.0.8", + "nocache": "^2.1.0", + "normalize.css": "^8.0.1", + "serialize-error": "^7.0.1" } } diff --git a/packages/runtime-handler/src/dev-runtime/checks/check-account-sid.ts b/packages/runtime-handler/src/dev-runtime/checks/check-account-sid.ts new file mode 100644 index 00000000..4fbbe642 --- /dev/null +++ b/packages/runtime-handler/src/dev-runtime/checks/check-account-sid.ts @@ -0,0 +1,58 @@ +import { stripIndent } from 'common-tags'; +import { LoggerInstance } from '../types'; +import chalk = require('chalk'); + +type Options = { + shouldPrintMessage: boolean; + shouldThrowError: boolean; + functionName?: string; + logger?: LoggerInstance; +}; + +export function checkForValidAccountSid( + accountSid: string | undefined, + options: Options = { + shouldPrintMessage: false, + shouldThrowError: false, + } +): accountSid is string { + if (accountSid && accountSid.length === 34 && accountSid.startsWith('AC')) { + return true; + } + + let message = ''; + let title = ''; + if (!accountSid) { + title = 'Missing Account SID'; + message = stripIndent` + You are missing a Twilio Account SID. You can add one into your .env file: + + ${chalk.bold('ACCOUNT_SID=')}ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + `; + } else { + title = 'Invalid Account SID'; + message = stripIndent` + The value for your ACCOUNT_SID in your .env file is not a valid Twilio Account SID. + + It should look like this: ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + `; + } + + if (options.shouldPrintMessage && message) { + options.logger?.error(message, title); + } + + if (options.shouldThrowError && message) { + const err = new Error(title); + err.name = 'INVALID_CONFIG'; + err.message = `${title}\n${message}`; + let meta = ''; + if (options.functionName) { + meta = `\n--- at ${options.functionName}`; + } + err.stack = `${err.message}${meta}`; + throw err; + } + + return false; +} diff --git a/packages/runtime-handler/src/dev-runtime/checks/check-auth-token.ts b/packages/runtime-handler/src/dev-runtime/checks/check-auth-token.ts new file mode 100644 index 00000000..5c13faf3 --- /dev/null +++ b/packages/runtime-handler/src/dev-runtime/checks/check-auth-token.ts @@ -0,0 +1,55 @@ +import { stripIndent } from 'common-tags'; +import { LoggerInstance } from '../types'; +import chalk = require('chalk'); + +type Options = { + shouldPrintMessage: boolean; + shouldThrowError: boolean; + functionName?: string; + logger?: LoggerInstance; +}; + +export function checkForValidAuthToken( + authToken: string | undefined, + options: Options = { shouldPrintMessage: false, shouldThrowError: false } +): boolean { + if (authToken && authToken.length === 32) { + return true; + } + + let message = ''; + let title = ''; + if (!authToken) { + title = 'Missing Auth Token'; + message = stripIndent` + You are missing a Twilio Auth Token. You can add one into your .env file: + + ${chalk.bold('AUTH_TOKEN=')}xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + `; + } else { + title = 'Invalid Auth Token'; + message = stripIndent` + The value for your AUTH_TOKEN in your .env file is not a valid Twilio Auth Token. + + It should be 32 characters long and made of letters and numbers. + `; + } + + if (options.shouldPrintMessage && message) { + options.logger?.error(message, title); + } + + if (options.shouldThrowError && message) { + const err = new Error(title); + err.name = 'INVALID_CONFIG'; + err.message = `${title}\n${message}`; + let meta = ''; + if (options.functionName) { + meta = `\n--- at ${options.functionName}`; + } + err.stack = `${err.message}${meta}`; + throw err; + } + + return false; +} diff --git a/packages/runtime-handler/src/dev-runtime/dependencies.d.ts b/packages/runtime-handler/src/dev-runtime/dependencies.d.ts new file mode 100644 index 00000000..1ffdb241 --- /dev/null +++ b/packages/runtime-handler/src/dev-runtime/dependencies.d.ts @@ -0,0 +1,5 @@ +declare module 'fast-redact' { + function createRedactor(options: {}): (val: T) => string; + + export = createRedactor; +} diff --git a/packages/runtime-handler/src/dev-runtime/dev-runtime.ts b/packages/runtime-handler/src/dev-runtime/dev-runtime.ts new file mode 100644 index 00000000..1e8f8255 --- /dev/null +++ b/packages/runtime-handler/src/dev-runtime/dev-runtime.ts @@ -0,0 +1,2 @@ +export { LocalDevelopmentServer } from './server'; +export * from './types'; diff --git a/packages/runtime-handler/src/dev-runtime/internal/functionRunner.ts b/packages/runtime-handler/src/dev-runtime/internal/functionRunner.ts new file mode 100644 index 00000000..60ff4f07 --- /dev/null +++ b/packages/runtime-handler/src/dev-runtime/internal/functionRunner.ts @@ -0,0 +1,106 @@ +import { ServerlessCallback } from '@twilio-labs/serverless-runtime-types/types'; +import { serializeError } from 'serialize-error'; +import { constructContext, constructGlobalScope, isTwiml } from '../route'; +import { ServerConfig } from '../types'; +import { Response } from './response'; +import { setRoutes } from './route-cache'; + +const sendDebugMessage = (debugMessage: string, ...debugArgs: any) => { + process.send && process.send({ debugMessage, debugArgs }); +}; + +export type Reply = { + body?: string | number | boolean | object; + headers?: { [key: string]: number | string }; + statusCode: number; +}; + +export type FunctionRunnerOptions = { + functionPath: string; + event: { [key: string]: any }; + config: ServerConfig; + path: string; +}; + +const handleError = (err: Error | string | object) => { + if (err) { + process.send && process.send({ err: serializeError(err) }); + } +}; + +const handleSuccess = (responseObject?: string | number | boolean | object) => { + let reply: Reply = { statusCode: 200 }; + if (typeof responseObject === 'string') { + sendDebugMessage('Sending basic string response'); + reply.headers = { 'Content-Type': 'text/plain' }; + reply.body = responseObject; + } else if ( + responseObject && + typeof responseObject === 'object' && + isTwiml(responseObject) + ) { + sendDebugMessage('Sending TwiML response as XML string'); + reply.headers = { 'Content-Type': 'text/xml' }; + reply.body = responseObject.toString(); + } else if (responseObject && responseObject instanceof Response) { + sendDebugMessage('Sending custom response'); + reply = responseObject.serialize(); + } else { + sendDebugMessage('Sending JSON response'); + reply.body = responseObject; + reply.headers = { 'Content-Type': 'application/json' }; + } + + if (process.send) { + process.send({ reply }); + } +}; + +process.on( + 'message', + ({ functionPath, event, config, path }: FunctionRunnerOptions) => { + try { + setRoutes(config.routes); + constructGlobalScope(config); + const context = constructContext(config, path); + sendDebugMessage('Context for %s: %p', path, context); + sendDebugMessage('Event for %s: %o', path, event); + let run_timings: { start: [number, number]; end: [number, number] } = { + start: [0, 0], + end: [0, 0], + }; + + const callback: ServerlessCallback = (err, responseObject) => { + run_timings.end = process.hrtime(); + sendDebugMessage('Function execution %s finished', path); + sendDebugMessage( + `(Estimated) Total Execution Time: ${ + (run_timings.end[0] * 1e9 + + run_timings.end[1] - + (run_timings.start[0] * 1e9 + run_timings.start[1])) / + 1e6 + }ms` + ); + if (err) { + handleError(err); + } else { + handleSuccess(responseObject); + } + }; + + sendDebugMessage('Calling function for %s', path); + run_timings.start = process.hrtime(); + const { handler } = require(functionPath); + if (typeof handler !== 'function') { + throw new Error( + `Could not find a "handler" function in file ${functionPath}` + ); + } + handler(context, event, callback); + } catch (err) { + if (process.send) { + process.send({ err: serializeError(err) }); + } + } + } +); diff --git a/packages/runtime-handler/src/dev-runtime/internal/request-logger.ts b/packages/runtime-handler/src/dev-runtime/internal/request-logger.ts new file mode 100644 index 00000000..202e5eeb --- /dev/null +++ b/packages/runtime-handler/src/dev-runtime/internal/request-logger.ts @@ -0,0 +1,73 @@ +import chalk from 'chalk'; +import { stripIndent } from 'common-tags'; +import { Request, RequestHandler, Response } from 'express'; + +function simpleLogs(req: Request, res: Response): string { + const contentType = res.get('Content-Type'); + const responseCode = + res.statusCode >= 400 + ? chalk`{black.bgRed ${res.statusCode}}` + : res.statusCode < 300 + ? chalk`{black.bgGreen ${res.statusCode}}` + : chalk`{black.bgYellow ${res.statusCode}}`; + let msg = chalk` + ${responseCode} {bold ${req.method}} ${req.originalUrl}`; + + if (contentType) { + msg += chalk` │ {dim Response Type ${contentType}}`; + } + + return stripIndent`${msg}`; +} + +function detailedLogs(req: Request, res: Response): string { + debugger; + const msgLines = [chalk`{reset }`, simpleLogs(req, res)]; + + let body: string | undefined; + const bodyEntries = Object.entries(req.body); + if (bodyEntries.length > 0) { + const lines = bodyEntries + .map(([key, value]) => chalk`│ ${key}: {bold ${value}}`) + .join('\n'); + body = `│ Body:\n${lines}`; + } + + let query: string | undefined; + const queryEntries = Object.entries(req.query); + if (queryEntries.length > 0) { + const lines = queryEntries + .map(([key, value]) => chalk`│ ${key}: {bold ${value}}`) + .join(chalk.reset() + '\n'); + query = `│ Query:\n${lines}`; + } + + if (body || query) { + msgLines.push(chalk`│{underline Request:}`); + if (query) { + msgLines.push(query); + } + if (body) { + msgLines.push(body); + } + } + + return msgLines.filter(x => !!x).join('\n'); +} + +export function createLogger( + logFunction: (msg: string) => void, + outputDetailedLogs: boolean = false +): RequestHandler { + return function requestLogger(req, res, next) { + const resEnd = res.end.bind(res); + res.end = function sendInterceptor(...args: any[]) { + const msg = outputDetailedLogs + ? detailedLogs(req, res) + : simpleLogs(req, res); + logFunction(msg); + resEnd(...args); + }; + next(); + }; +} diff --git a/packages/runtime-handler/src/dev-runtime/internal/response.ts b/packages/runtime-handler/src/dev-runtime/internal/response.ts new file mode 100644 index 00000000..8b97bf95 --- /dev/null +++ b/packages/runtime-handler/src/dev-runtime/internal/response.ts @@ -0,0 +1,65 @@ +import { TwilioResponse } from '@twilio-labs/serverless-runtime-types/types'; +import { Response as ExpressResponse } from 'express'; +import debug from '../utils/debug'; + +const log = debug('twilio-runtime-handler:dev:response'); + +type HeaderValue = number | string; +type Headers = { + [key: string]: HeaderValue; +}; + +export class Response implements TwilioResponse { + private body: undefined | any; + private statusCode: number; + private headers: Headers; + + constructor() { + this.body = undefined; + this.statusCode = 200; + this.headers = {}; + } + + setStatusCode(statusCode: number): void { + log('Setting status code to %d', statusCode); + this.statusCode = statusCode; + } + + setBody(body: object | string): void { + log('Setting response body to %o', body); + this.body = body; + } + + setHeaders(headersObject: Headers): void { + log('Setting headers to: %P', headersObject); + if (typeof headersObject !== 'object') { + return; + } + this.headers = headersObject; + } + + appendHeader(key: string, value: HeaderValue): void { + log('Appending header for %s', key, value); + this.headers = this.headers || {}; + this.headers[key] = value; + } + + applyToExpressResponse(res: ExpressResponse): void { + log('Setting values on response: %P', { + statusCode: this.statusCode, + headers: this.headers, + body: this.body, + }); + res.status(this.statusCode); + res.set(this.headers); + res.send(this.body); + } + + serialize() { + return { + statusCode: this.statusCode, + body: this.body.toString(), + headers: this.headers, + }; + } +} diff --git a/packages/runtime-handler/src/dev-runtime/internal/route-cache.ts b/packages/runtime-handler/src/dev-runtime/internal/route-cache.ts new file mode 100644 index 00000000..95a45dd3 --- /dev/null +++ b/packages/runtime-handler/src/dev-runtime/internal/route-cache.ts @@ -0,0 +1,59 @@ +import { Merge } from 'type-fest'; +import { RouteInfo, ServerlessResourceConfigWithFilePath } from '../types'; + +type ExtendedRouteInfo = + | Merge<{ type: 'function' }, ServerlessResourceConfigWithFilePath> + | Merge<{ type: 'asset' }, ServerlessResourceConfigWithFilePath>; + +const allRoutes = new Map(); +const assetsCache = new Set(); +const functionsCache = new Set(); + +export function setRoutes({ functions, assets }: RouteInfo) { + allRoutes.clear(); + assetsCache.clear(); + functionsCache.clear(); + + functions.forEach(fn => { + if (!fn.path) { + return; + } + + if (allRoutes.has(fn.path)) { + throw new Error(`Duplicate. Path ${fn.path} already exists`); + } + functionsCache.add(fn); + allRoutes.set(fn.path, { + ...fn, + type: 'function', + }); + }); + + assets.forEach(asset => { + if (!asset.path) { + return; + } + + if (allRoutes.has(asset.path)) { + throw new Error(`Duplicate. Path ${asset.path} already exists`); + } + assetsCache.add(asset); + allRoutes.set(asset.path, { + ...asset, + type: 'asset', + }); + }); + + return new Map(allRoutes); +} + +export function getRoutes(): Map { + return new Map(allRoutes); +} + +export function getCachedResources(): RouteInfo { + return { + assets: Array.from(assetsCache), + functions: Array.from(functionsCache), + }; +} diff --git a/packages/runtime-handler/src/dev-runtime/internal/runtime.ts b/packages/runtime-handler/src/dev-runtime/internal/runtime.ts new file mode 100644 index 00000000..70967193 --- /dev/null +++ b/packages/runtime-handler/src/dev-runtime/internal/runtime.ts @@ -0,0 +1,97 @@ +import type { + ServiceContext, + SyncListListInstance, + SyncMapListInstance, +} from '@twilio-labs/serverless-runtime-types/types'; +import { + AssetResourceMap, + ResourceMap, + RuntimeInstance, + RuntimeSyncClientOptions, + RuntimeSyncServiceContext, +} from '@twilio-labs/serverless-runtime-types/types'; +import { readFileSync } from 'fs'; +import { checkForValidAccountSid } from '../checks/check-account-sid'; +import { ServerConfig } from '../types'; +import debug from '../utils/debug'; +import { requireFromProject } from '../utils/requireFromProject'; +import { getCachedResources } from './route-cache'; + +const log = debug('twilio-runtime-handler:dev:runtime'); + +function getAssets(): AssetResourceMap { + const { assets } = getCachedResources(); + if (assets.length === 0) { + return {}; + } + + const result: AssetResourceMap = {}; + for (const asset of assets) { + if (asset.access === 'private') { + const prefix = + process.env.TWILIO_FUNCTIONS_LEGACY_MODE === 'true' ? '/assets' : ''; + const open = () => readFileSync(asset.filePath, 'utf8'); + result[prefix + asset.path] = { path: asset.filePath, open }; + } + } + log('Found the following assets available: %o', result); + return result; +} + +function getFunctions(): ResourceMap { + const { functions } = getCachedResources(); + if (functions.length === 0) { + return {}; + } + + const result: ResourceMap = {}; + for (const fn of functions) { + result[fn.path.substr(1)] = { path: fn.filePath }; + } + log('Found the following functions available: %o', result); + return result; +} + +export type ExtendedSyncServiceContext = ServiceContext & { + maps: SyncMapListInstance; + lists: SyncListListInstance; +}; + +export function create({ + env, + logger, + baseDir, +}: ServerConfig): RuntimeInstance { + function getSync( + options?: RuntimeSyncClientOptions + ): RuntimeSyncServiceContext { + options = { serviceName: 'default', lazyLoading: true, ...options }; + const { serviceName } = options; + delete options.serviceName; + + checkForValidAccountSid(env.ACCOUNT_SID, { + shouldPrintMessage: true, + shouldThrowError: true, + logger: logger, + functionName: `Runtime.getSync(${[...arguments] + .map((x: any) => JSON.stringify(x)) + .join(',')})`, + }); + const client = requireFromProject(baseDir, 'twilio')( + env.ACCOUNT_SID, + env.AUTH_TOKEN, + options + ); + const service = client.sync.services( + serviceName || 'default' + ) as RuntimeSyncServiceContext; + + service.maps = service.syncMaps; + service.lists = service.syncLists; + return service; + } + + return { getSync, getAssets, getFunctions }; +} + +module.exports = { create }; diff --git a/packages/runtime-handler/src/dev-runtime/route.ts b/packages/runtime-handler/src/dev-runtime/route.ts new file mode 100644 index 00000000..5bc2a737 --- /dev/null +++ b/packages/runtime-handler/src/dev-runtime/route.ts @@ -0,0 +1,267 @@ +import { + Context, + ServerlessCallback, + ServerlessFunctionSignature, + TwilioClient, + TwilioClientOptions, + TwilioPackage, +} from '@twilio-labs/serverless-runtime-types/types'; +import { fork } from 'child_process'; +import { + NextFunction, + Request as ExpressRequest, + RequestHandler as ExpressRequestHandler, + Response as ExpressResponse, +} from 'express'; +import { join, resolve } from 'path'; +import { deserializeError } from 'serialize-error'; +import { checkForValidAccountSid } from './checks/check-account-sid'; +import { checkForValidAuthToken } from './checks/check-auth-token'; +import { Reply } from './internal/functionRunner'; +import { Response } from './internal/response'; +import * as Runtime from './internal/runtime'; +import { ServerConfig } from './types'; +import debug from './utils/debug'; +import { wrapErrorInHtml } from './utils/error-html'; +import { requireFromProject } from './utils/requireFromProject'; +import { cleanUpStackTrace } from './utils/stack-trace/clean-up'; + +const log = debug('twilio-runtime-handler:dev:route'); + +const RUNNER_PATH = + process.env.NODE_ENV === 'test' + ? resolve(__dirname, '../../dist/dev-runtime/internal/functionRunner') + : join(__dirname, 'internal', 'functionRunner'); + +let twilio: TwilioPackage; + +export function constructEvent(req: ExpressRequest): T { + return { ...req.query, ...req.body }; +} + +export function constructContext( + { url, env, logger, baseDir }: ServerConfig, + functionPath: string +): Context<{ + ACCOUNT_SID?: string; + AUTH_TOKEN?: string; + DOMAIN_NAME: string; + PATH: string; + [key: string]: string | undefined | Function; +}> { + function getTwilioClient(opts?: TwilioClientOptions): TwilioClient { + checkForValidAccountSid(env.ACCOUNT_SID, { + shouldPrintMessage: true, + shouldThrowError: true, + functionName: 'context.getTwilioClient()', + logger: logger, + }); + checkForValidAuthToken(env.AUTH_TOKEN, { + shouldPrintMessage: true, + shouldThrowError: true, + functionName: 'context.getTwilioClient()', + logger: logger, + }); + + return requireFromProject(baseDir, 'twilio')( + env.ACCOUNT_SID, + env.AUTH_TOKEN, + { + lazyLoading: true, + ...opts, + } + ); + } + const DOMAIN_NAME = url.replace(/^https?:\/\//, ''); + const PATH = functionPath; + return { PATH, DOMAIN_NAME, ...env, getTwilioClient }; +} + +export function constructGlobalScope(config: ServerConfig): void { + twilio = requireFromProject(config.baseDir, 'twilio'); + const GlobalRuntime = Runtime.create(config); + (global as any)['Twilio'] = { ...twilio, Response }; + (global as any)['Runtime'] = GlobalRuntime; + (global as any)['Functions'] = GlobalRuntime.getFunctions(); + (global as any)['Response'] = Response; + + if ( + checkForValidAccountSid(config.env.ACCOUNT_SID) && + config.env.AUTH_TOKEN + ) { + (global as any)['twilioClient'] = new twilio.Twilio( + config.env.ACCOUNT_SID, + config.env.AUTH_TOKEN, + { + lazyLoading: true, + } + ); + } +} + +function isError(obj: any): obj is Error { + return obj instanceof Error; +} + +export function handleError( + err: Error | string | object, + req: ExpressRequest, + res: ExpressResponse, + functionFilePath?: string +) { + res.status(500); + if (isError(err)) { + const cleanedupError = cleanUpStackTrace(err); + + if (req.useragent && (req.useragent.isDesktop || req.useragent.isMobile)) { + res.type('text/html'); + res.send(wrapErrorInHtml(cleanedupError, functionFilePath)); + } else { + res.send({ + message: cleanedupError.message, + name: cleanedupError.name, + stack: cleanedupError.stack, + }); + } + } else { + res.send(err); + } +} + +export function isTwiml(obj: object): boolean { + if (!twilio) { + log('Unexpected call of isTwiml. Require twilio manual'); + twilio = require('twilio'); + } + const { VoiceResponse, MessagingResponse, FaxResponse } = twilio.twiml; + const isVoiceTwiml = obj instanceof VoiceResponse; + const isMessagingTwiml = obj instanceof MessagingResponse; + const isFaxTwiml = obj instanceof FaxResponse; + return isVoiceTwiml || isMessagingTwiml || isFaxTwiml; +} + +export function handleSuccess( + responseObject: string | number | boolean | object | undefined, + res: ExpressResponse +) { + res.status(200); + if (typeof responseObject === 'string') { + log('Sending basic string response'); + res.type('text/plain').send(responseObject); + return; + } + + if ( + responseObject && + typeof responseObject === 'object' && + isTwiml(responseObject) + ) { + log('Sending TwiML response as XML string'); + res.type('text/xml').send(responseObject.toString()); + return; + } + + if (responseObject && responseObject instanceof Response) { + log('Sending custom response'); + responseObject.applyToExpressResponse(res); + return; + } + + log('Sending JSON response'); + res.send(responseObject); +} + +export function functionPathToRoute( + functionPath: string, + config: ServerConfig +) { + return function twilioFunctionHandler( + req: ExpressRequest, + res: ExpressResponse, + next: NextFunction + ) { + const event = constructEvent(req); + const forked = fork(RUNNER_PATH); + forked.on( + 'message', + ({ + err, + reply, + debugMessage, + debugArgs = [], + }: { + err?: Error | number | string; + reply?: Reply; + debugMessage?: string; + debugArgs?: any[]; + }) => { + if (debugMessage) { + log(debugMessage, ...debugArgs); + return; + } + if (err) { + const error = deserializeError(err); + handleError(error, req, res, functionPath); + } + if (reply) { + res.status(reply.statusCode); + res.set(reply.headers); + res.send(reply.body); + } + forked.kill(); + } + ); + + forked.send({ functionPath, event, config, path: req.path }); + }; +} + +export function functionToRoute( + fn: ServerlessFunctionSignature, + config: ServerConfig, + functionFilePath?: string +): ExpressRequestHandler { + return function twilioFunctionHandler( + req: ExpressRequest, + res: ExpressResponse, + next: NextFunction + ) { + const event = constructEvent(req); + log('Event for %s: %o', req.path, event); + const context = constructContext(config, req.path); + log('Context for %s: %p', req.path, context); + let run_timings: { + start: [number, number]; + end: [number, number]; + } = { + start: [0, 0], + end: [0, 0], + }; + + const callback: ServerlessCallback = function callback(err, payload?) { + run_timings.end = process.hrtime(); + log('Function execution %s finished', req.path); + log( + `(Estimated) Total Execution Time: ${ + (run_timings.end[0] * 1e9 + + run_timings.end[1] - + (run_timings.start[0] * 1e9 + run_timings.start[1])) / + 1e6 + }ms` + ); + if (err) { + handleError(err, req, res, functionFilePath); + return; + } + handleSuccess(payload, res); + }; + + log('Calling function for %s', req.path); + try { + run_timings.start = process.hrtime(); + fn(context, event, callback); + } catch (err) { + callback(err); + } + }; +} diff --git a/packages/runtime-handler/src/dev-runtime/server.ts b/packages/runtime-handler/src/dev-runtime/server.ts new file mode 100644 index 00000000..64dcbd74 --- /dev/null +++ b/packages/runtime-handler/src/dev-runtime/server.ts @@ -0,0 +1,244 @@ +import { ServerlessFunctionSignature } from '@twilio-labs/serverless-runtime-types/types'; +import bodyParser from 'body-parser'; +import EventEmitter from 'events'; +import express, { + Express, + NextFunction, + Request as ExpressRequest, + Response as ExpressResponse, +} from 'express'; +import userAgentMiddleware from 'express-useragent'; +import nocache from 'nocache'; +import { createLogger } from './internal/request-logger'; +import { setRoutes } from './internal/route-cache'; +import { + constructGlobalScope, + functionPathToRoute, + functionToRoute, +} from './route'; +import { RouteInfo, ServerConfig } from './types'; +import debug from './utils/debug'; +import { wrapErrorInHtml } from './utils/error-html'; + +const log = debug('twilio-runtime-handler:dev:server'); +const DEFAULT_PORT = process.env.PORT || 3000; +const RELOAD_DEBOUNCE_MS = 250; +const DEFAULT_BODY_SIZE_LAMBDA = '6mb'; + +function loadTwilioFunction(fnPath: string): ServerlessFunctionSignature { + return require(fnPath).handler; +} + +function requireCacheCleaner( + req: ExpressRequest, + res: ExpressResponse, + next: NextFunction +) { + log('Deleting require cache'); + Object.keys(require.cache).forEach((key) => { + // Entries in the cache that end with .node are compiled binaries, deleting + // those has unspecified results, so we keep them. + // Entries in the cache that include "twilio-run" are part of this module + // or its dependencies, so don't need to be cleared. + if (!(key.endsWith('.node') || key.includes('twilio-run'))) { + delete require.cache[key]; + } + }); + next(); +} + +export declare interface LocalDevelopmentServer { + on(event: 'request-log', listener: (logMessage: string) => void): this; + on( + event: 'updated-routes', + listener: (config: ServerConfig, routes: RouteInfo) => void + ): this; + new (port: number | string, config: ServerConfig): LocalDevelopmentServer; +} + +export class LocalDevelopmentServer extends EventEmitter { + private app: Express; + private routes: RouteInfo; + private routeMap: Map = new Map(); + constructor( + private port: number | string = DEFAULT_PORT, + private config: ServerConfig + ) { + super(); + if (this.config.enableDebugLogs) { + debug.enable('twilio-runtime-handler:*'); + } + log('Creating Local Development Server'); + log( + '@twilio/runtime-handler version: %s', + require('../../package.json')?.version + ); + + this.normalizeConfig(); + this.routes = this.config.routes; + this.setRoutes(this.config.routes); + this.app = this.createServer(); + } + + private normalizeConfig = () => { + this.config = { + ...this.config, + url: this.config.url || `http://localhost:${this.port}`, + baseDir: this.config.baseDir || process.cwd(), + }; + }; + + private logFunction = (msg: string) => { + this.emit('request-log', msg); + }; + + private setRoutes = (routes: RouteInfo) => { + this.routes = routes; + this.routeMap = setRoutes(this.routes); + }; + + private createServer = () => { + log('Creating server with config: %p', this.config); + + const app = express(); + app.use(userAgentMiddleware.express()); + app.use( + bodyParser.urlencoded({ + extended: false, + limit: DEFAULT_BODY_SIZE_LAMBDA, + }) + ); + app.use(bodyParser.json({ limit: DEFAULT_BODY_SIZE_LAMBDA })); + app.get('/favicon.ico', (req, res) => { + res.redirect( + 'https://www.twilio.com/marketing/bundles/marketing/img/favicons/favicon.ico' + ); + }); + + if (this.config.logs) { + app.use(createLogger(this.logFunction)); + } + + if (this.config.live) { + app.use(nocache()); + app.use(requireCacheCleaner); + } + + if (this.config.legacyMode) { + process.env.TWILIO_FUNCTIONS_LEGACY_MODE = this.config.legacyMode + ? 'true' + : undefined; + log('Legacy mode enabled'); + app.use('/assets/*', (req, res, next) => { + req.path = req.path.replace('/assets/', '/'); + next(); + }); + } + + constructGlobalScope(this.config); + + app.set('port', this.port); + app.all( + '/*', + (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => { + if (!this.routeMap.has(req.path)) { + res.status(404).send('Could not find request resource'); + return; + } + + if (req.method === 'OPTIONS') { + res.set({ + 'access-control-allow-origin': '*', + 'access-control-allow-headers': + 'Accept, Authorization, Content-Type, If-Match, If-Modified-Since, If-None-Match, If-Unmodified-Since, User-Agent', + 'access-control-allow-methods': 'GET, POST, OPTIONS', + 'access-control-expose-headers': 'ETag', + 'access-control-max-age': '86400', + 'access-control-allow-credentials': true, + 'content-type': 'text/plain; charset=UTF-8', + 'content-length': '0', + }); + res.status(204).end(); + + return; + } + + const routeInfo = this.routeMap.get(req.path); + + if (routeInfo && routeInfo.type === 'function') { + const functionPath = routeInfo.filePath; + try { + if (!functionPath) { + throw new Error('Missing function path'); + } + + if (this.config.forkProcess) { + functionPathToRoute(functionPath, this.config)(req, res, next); + } else { + log('Load & route to function at "%s"', functionPath); + const twilioFunction = loadTwilioFunction(functionPath); + if (typeof twilioFunction !== 'function') { + return res + .status(404) + .send( + `Could not find a "handler" function in file ${functionPath}` + ); + } + functionToRoute(twilioFunction, this.config, functionPath)( + req, + res, + next + ); + } + } catch (err) { + log('Failed to retrieve function. %O', err); + if (err.code === 'ENOENT') { + res.status(404).send(`Could not find function ${functionPath}`); + } else { + res.status(500).send(wrapErrorInHtml(err, functionPath)); + } + } + } else if (routeInfo && routeInfo.type === 'asset') { + if (routeInfo.filePath) { + if (routeInfo.access === 'private') { + res.status(403).send('This asset has been marked as private'); + } else { + res.sendFile(routeInfo.filePath); + } + } else { + res.status(404).send('Could not find asset'); + } + } else { + res.status(404).send('Could not find requested resource'); + } + } + ); + + return app; + }; + + listen = () => { + return new Promise((resolve, reject) => { + if (typeof this.app === 'undefined') { + reject(new Error('Unexpected error. Server did not exist.')); + return; + } + + this.app.listen(this.port, () => { + log('Server is listening.'); + resolve(this.app); + }); + }); + }; + + getApp = () => { + return this.app; + }; + + update = (routes: RouteInfo) => { + this.setRoutes(routes); + this.emit('updated-routes', this.config, this.routes); + }; +} + +export type { ServerConfig } from './types'; diff --git a/packages/runtime-handler/src/dev-runtime/types.ts b/packages/runtime-handler/src/dev-runtime/types.ts new file mode 100644 index 00000000..702cf55d --- /dev/null +++ b/packages/runtime-handler/src/dev-runtime/types.ts @@ -0,0 +1,88 @@ +export type SearchConfig = { + /** + * Ordered folder names to search for to find functions + * + * @type {string[]} + */ + functionsFolderNames?: string[]; + /** + * Ordered folder names to search for to find assets + * + * @type {string[]} + */ + assetsFolderNames?: string[]; +}; + +export type EnvironmentVariablesWithAuth = { + ACCOUNT_SID?: string; + AUTH_TOKEN?: string; + [key: string]: string | undefined; +}; + +export type InspectInfo = { + hostPort: string; + break: boolean; +}; + +export type AccessOptions = 'private' | 'protected' | 'public'; + +/** + * Necessary info to deploy a function + */ +export type ServerlessResourceConfig = { + /** + * Access for the function + */ + access: AccessOptions; + /** + * Content of the uploaded function + */ + content: string | Buffer; + /** + * Function name + */ + name: string; + /** + * Path for the serverless resource + * Functions: '/some-function' + * Assets: '/some-assets.jpg' + */ + path: string; +}; + +export type ServerlessResourceConfigWithFilePath = ServerlessResourceConfig & { + /** + * Path to the actual file on the file system. + */ + filePath: string; +}; + +export type RouteInfo = { + functions: ServerlessResourceConfigWithFilePath[]; + assets: ServerlessResourceConfigWithFilePath[]; +}; + +export type ServerConfig = { + inspect?: InspectInfo; + baseDir: string; + env: EnvironmentVariablesWithAuth; + port: number; + url: string; + detailedLogs: boolean; + live: boolean; + logs: boolean; + legacyMode: boolean; + appName: string; + forkProcess: boolean; + logger?: LoggerInstance; + routes: RouteInfo; + enableDebugLogs: boolean; +}; + +export type LoggerInstance = { + debug(msg: string): void; + info(msg: string): void; + warn(msg: string, title?: string): void; + error(msg: string, title?: string): void; + log(msg: string, level: number): void; +}; diff --git a/packages/runtime-handler/src/dev-runtime/utils/debug.ts b/packages/runtime-handler/src/dev-runtime/utils/debug.ts new file mode 100644 index 00000000..5b5bc27f --- /dev/null +++ b/packages/runtime-handler/src/dev-runtime/utils/debug.ts @@ -0,0 +1,81 @@ +import debug from 'debug'; +import fastRedact from 'fast-redact'; + +function prefixAllEntriesWithWildcard(values: string[]): string[] { + const result = []; + + for (let val of values) { + result.push(val); + result.push(`*.${val}`); + } + + return result; +} + +export const generalRedactor = fastRedact({ + paths: [ + 'env.*', + 'pkgJson.*', + ...prefixAllEntriesWithWildcard([ + 'authToken', + 'apiSecret', + 'username', + 'password', + 'cookies', + 'AUTH_TOKEN', + 'API_SECRET', + 'TWILIO_AUTH_TOKEN', + 'TWILIO_API_SECRET', + ]), + ], + serialize: false, +}) as (x: T) => T; + +export const allPropertiesRedactor = fastRedact({ + paths: ['*'], + serialize: false, +}) as (x: T) => T; + +export function copyObject(obj: object) { + return JSON.parse(JSON.stringify(obj)); +} + +export function createRedactedObject( + obj: object, + redactor: typeof generalRedactor +) { + const copiedObject = copyObject(obj); + return redactor(copiedObject); +} + +debug.formatters.P = function protectedFormatterMultiline(v: any): string { + if (typeof v === 'object') { + v = createRedactedObject(v, generalRedactor); + } + + return debug.formatters.O.bind(debug)(v); +}; + +debug.formatters.p = function protectedFormatterSameline(v: any): string { + if (typeof v === 'object') { + v = createRedactedObject(v, generalRedactor); + } + + return debug.formatters.o.bind(debug)(v); +}; + +debug.formatters.R = function redactedFormatterMultiline(v: any): string { + if (typeof v === 'object') { + v = createRedactedObject(v, allPropertiesRedactor); + } + return debug.formatters.O.bind(debug)(v); +}; + +debug.formatters.r = function redactedFormatterSameline(v: any): string { + if (typeof v === 'object') { + v = createRedactedObject(v, allPropertiesRedactor); + } + return debug.formatters.o.bind(debug)(v); +}; + +export default debug; diff --git a/packages/runtime-handler/src/dev-runtime/utils/error-html.ts b/packages/runtime-handler/src/dev-runtime/utils/error-html.ts new file mode 100644 index 00000000..37f5ea29 --- /dev/null +++ b/packages/runtime-handler/src/dev-runtime/utils/error-html.ts @@ -0,0 +1,61 @@ +import { html } from 'common-tags'; +import { readFileSync } from 'fs'; + +let normalizeCss = ''; +const normalizeCssPath = require.resolve('normalize.css'); + +export function wrapErrorInHtml( + err: T, + filePath?: string +): string { + if (!normalizeCss) { + normalizeCss = readFileSync(normalizeCssPath, 'utf8'); + } + + return html` + + +

Runtime Error

+ ${filePath ? `

Error thrown in ${filePath}

` : ''} +
+

+ ${`${err.name}: ${err.message.replace( + /\n/g, + '
' + )}`} +

+

Stack Trace

+ ${`
${err.stack}
`} + + `; +} diff --git a/packages/runtime-handler/src/dev-runtime/utils/inspector.ts b/packages/runtime-handler/src/dev-runtime/utils/inspector.ts new file mode 100644 index 00000000..7ff71bd5 --- /dev/null +++ b/packages/runtime-handler/src/dev-runtime/utils/inspector.ts @@ -0,0 +1,22 @@ +import inspector from 'inspector'; + +export function startInspector( + val: string, + wait: boolean = false +): typeof inspector { + let port = undefined; + let host = undefined; + + if (typeof val === 'string') { + if (val.includes(':')) { + const [hostRaw, portRaw] = val.split(':'); + port = parseInt(portRaw, 10); + host = hostRaw; + } else if (val.length > 0) { + port = parseInt(val, 10); + } + } + + inspector.open(port, host, wait); + return inspector; +} diff --git a/packages/runtime-handler/src/dev-runtime/utils/requireFromProject.ts b/packages/runtime-handler/src/dev-runtime/utils/requireFromProject.ts new file mode 100644 index 00000000..eb3f32b9 --- /dev/null +++ b/packages/runtime-handler/src/dev-runtime/utils/requireFromProject.ts @@ -0,0 +1,6 @@ +import { createRequire } from 'module'; +import { join } from 'path'; + +export function requireFromProject(baseDir: string, packageName: string) { + return createRequire(join(baseDir, 'node_modules'))(packageName); +} diff --git a/packages/runtime-handler/src/dev-runtime/utils/stack-trace/clean-up.ts b/packages/runtime-handler/src/dev-runtime/utils/stack-trace/clean-up.ts new file mode 100644 index 00000000..d4b0210a --- /dev/null +++ b/packages/runtime-handler/src/dev-runtime/utils/stack-trace/clean-up.ts @@ -0,0 +1,19 @@ +import { formatStackTraceWithInternals } from './helpers'; + +/** + * Uses the V8 Stack Trace API to hide any twilio-run internals from stack traces + * https://v8.dev/docs/stack-trace-api + * + * @param err Instance that inherits from Error + */ +export function cleanUpStackTrace(err: T): T { + if (typeof process.env.TWILIO_SERVERLESS_FULL_ERRORS !== 'undefined') { + return err; + } + + const backupPrepareStackTrace = Error.prepareStackTrace; + Error.prepareStackTrace = formatStackTraceWithInternals; + err.stack = err.stack; + Error.prepareStackTrace = backupPrepareStackTrace; + return err; +} diff --git a/packages/runtime-handler/src/dev-runtime/utils/stack-trace/helpers.ts b/packages/runtime-handler/src/dev-runtime/utils/stack-trace/helpers.ts new file mode 100644 index 00000000..b433720d --- /dev/null +++ b/packages/runtime-handler/src/dev-runtime/utils/stack-trace/helpers.ts @@ -0,0 +1,62 @@ +import path from 'path'; + +export const MODULE_ROOT = path.resolve(__dirname, '../../'); + +/** + * Turns a set of stack frames into a stack trace string equivalent to the one + * generated by V8 + * + * https://v8.dev/docs/stack-trace-api + * + * @param err The error instance for this stack trace + * @param stack Callsite instances for each frame from V8 + */ +export function stringifyStackTrace(err: Error, stack: NodeJS.CallSite[]) { + const callSiteStrings = stack + .map(callSite => { + return ` at ${callSite}`; + }) + .join('\n'); + const stackTrace = `${err.name}: ${err.message}\n${callSiteStrings}`; + return stackTrace; +} + +/** + * Returns all stack frames until one is reached that comes from inside twilio-run + * + * https://v8.dev/docs/stack-trace-api + * + * @param stack Array of callsite instances from the V8 Stack Trace API + */ +export function filterCallSites(stack: NodeJS.CallSite[]): NodeJS.CallSite[] { + let indexOfFirstInternalCallSite = stack.findIndex(callSite => + callSite.getFileName()?.includes(MODULE_ROOT) + ); + indexOfFirstInternalCallSite = + indexOfFirstInternalCallSite === -1 + ? stack.length + : indexOfFirstInternalCallSite; + return stack.slice(0, indexOfFirstInternalCallSite); +} + +/** + * Removes any stack traces that are internal to twilio-run and replaces it + * with one [Twilio Dev Server internals] statement. + * + * To be used with Error.prepareStackTrace from the V8 Stack Trace API + * https://v8.dev/docs/stack-trace-api + * + * @param err The error instance for this stack trace + * @param stack Callsite instances for each from from V8 Stack Trace API + */ +export function formatStackTraceWithInternals( + err: Error, + stack: NodeJS.CallSite[] +): string { + const filteredStack = filterCallSites(stack); + const stackTraceWithoutInternals = stringifyStackTrace(err, filteredStack); + if (filteredStack.length === stack.length) { + return stackTraceWithoutInternals; + } + return `${stackTraceWithoutInternals}\n at [Twilio Dev Server internals]`; +} diff --git a/packages/serverless-api/jest.config.js b/packages/serverless-api/jest.config.js index 6ad3dd11..c1f12360 100644 --- a/packages/serverless-api/jest.config.js +++ b/packages/serverless-api/jest.config.js @@ -1,12 +1,12 @@ -const base = require("../../jest.config.base.js"); +const base = require('../../jest.config.base.js'); module.exports = { ...base, globals: { - "ts-jest": { - tsConfig: "./tsconfig.test.json" - } + 'ts-jest': { + tsconfig: './tsconfig.test.json', + }, }, - name: "serverless-api", - displayName: "serverless-api" + name: 'serverless-api', + displayName: 'serverless-api', }; diff --git a/packages/serverless-runtime-types/types.d.ts b/packages/serverless-runtime-types/types.d.ts index cf255ddd..ccc0018a 100644 --- a/packages/serverless-runtime-types/types.d.ts +++ b/packages/serverless-runtime-types/types.d.ts @@ -44,7 +44,7 @@ export type RuntimeInstance = { }; export type Context = { - getTwilioClient(): twilio.Twilio; + getTwilioClient(options?: TwilioClientOptions): twilio.Twilio; DOMAIN_NAME: string; } & T; @@ -66,3 +66,10 @@ export type ResponseConstructor = new (...args: any[]) => TwilioResponse; export type GlobalTwilio = Omit & { Response: ResponseConstructor; }; + +export { ServiceContext } from 'twilio/lib/rest/sync/v1/service'; +export { SyncListListInstance } from 'twilio/lib/rest/sync/v1/service/syncList'; +export { SyncMapListInstance } from 'twilio/lib/rest/sync/v1/service/syncMap'; +export { TwilioClientOptions } from 'twilio/lib/rest/Twilio'; +export type TwilioClient = twilio.Twilio; +export type TwilioPackage = typeof twilio; diff --git a/packages/twilio-run/__tests__/runtime/__snapshots__/integration.test.ts.snap b/packages/twilio-run/__tests__/runtime/__snapshots__/integration.test.ts.snap index dc7b5639..09281a3b 100644 --- a/packages/twilio-run/__tests__/runtime/__snapshots__/integration.test.ts.snap +++ b/packages/twilio-run/__tests__/runtime/__snapshots__/integration.test.ts.snap @@ -1,5 +1,19 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`with an express app with forked process function handling Function integration tests basic-twiml.js should match snapshot 1`] = ` +Object { + "body": Object {}, + "headers": Object { + "connection": "close", + "content-type": "text/xml; charset=utf-8", + "x-powered-by": "Express", + }, + "statusCode": 200, + "text": "Hello World", + "type": "text/xml", +} +`; + exports[`with an express app with inline function handling Assets integration tests hello.js should match snapshot 1`] = ` Object { "body": Object {}, diff --git a/packages/twilio-run/__tests__/runtime/integration.test.ts b/packages/twilio-run/__tests__/runtime/integration.test.ts index cbb32ecf..66df07cc 100644 --- a/packages/twilio-run/__tests__/runtime/integration.test.ts +++ b/packages/twilio-run/__tests__/runtime/integration.test.ts @@ -134,7 +134,7 @@ describe('with an express app', () => { } }); }); - xdescribe('with forked process function handling', () => { + describe('with forked process function handling', () => { beforeAll(async () => { app = await createServer(9000, { baseDir: TEST_DIR, diff --git a/packages/twilio-run/jest.config.js b/packages/twilio-run/jest.config.js index 7daeab26..bfe4e2be 100644 --- a/packages/twilio-run/jest.config.js +++ b/packages/twilio-run/jest.config.js @@ -4,7 +4,7 @@ module.exports = { ...base, globals: { 'ts-jest': { - tsConfig: './tsconfig.test.json', + tsconfig: './tsconfig.test.json', }, }, name: 'twilio-run', diff --git a/packages/twilio-run/package.json b/packages/twilio-run/package.json index 6971307b..8b2c8ed8 100644 --- a/packages/twilio-run/package.json +++ b/packages/twilio-run/package.json @@ -85,6 +85,7 @@ "ngrok": "^3.3.0" }, "devDependencies": { + "@twilio/runtime-handler": "^1.0.2-beta.3", "@types/cheerio": "^0.22.12", "@types/common-tags": "^1.8.0", "@types/debug": "^4.1.4", diff --git a/packages/twilio-run/src/commands/start.ts b/packages/twilio-run/src/commands/start.ts index 45f1cf42..b1d08c4b 100644 --- a/packages/twilio-run/src/commands/start.ts +++ b/packages/twilio-run/src/commands/start.ts @@ -7,7 +7,7 @@ import checkProjectStructure from '../checks/project-structure'; import { getConfigFromCli, getUrl, StartCliFlags } from '../config/start'; import { ALL_FLAGS, BASE_CLI_FLAG_NAMES, getRelevantFlags } from '../flags'; import { printRouteInfo } from '../printers/start'; -import { createServer } from '../runtime/server'; +import { createLocalDevelopmentServer } from '../runtime/server'; import { startInspector } from '../runtime/utils/inspector'; import { getDebugFunction, logger, setLogLevelByName } from '../utils/logger'; import { ExternalCliOptions } from './shared'; @@ -73,7 +73,7 @@ export async function handler( startInspector(config.inspect.hostPort, config.inspect.break); } - const app = await createServer(config.port, config); + const app = await createLocalDevelopmentServer(config.port, config); let server: Server; debug('Start server on port %d', config.port); return new Promise((resolve, reject) => { diff --git a/packages/twilio-run/src/runtime/internal/route-cache.ts b/packages/twilio-run/src/runtime/internal/route-cache.ts index 26fbd18b..899b9fc2 100644 --- a/packages/twilio-run/src/runtime/internal/route-cache.ts +++ b/packages/twilio-run/src/runtime/internal/route-cache.ts @@ -1,8 +1,8 @@ import { ServerlessResourceConfigWithFilePath } from '@twilio-labs/serverless-api'; import { SearchConfig } from '@twilio-labs/serverless-api/dist/utils'; import { Merge } from 'type-fest'; -import { RouteInfo, getFunctionsAndAssets } from './runtime-paths'; import { StartCliConfig } from '../../config/start'; +import { RouteInfo, getFunctionsAndAssets } from './runtime-paths'; type ExtendedRouteInfo = | Merge<{ type: 'function' }, ServerlessResourceConfigWithFilePath> @@ -27,12 +27,12 @@ export async function getRouteMap(config: StartCliConfig) { return setRoutes(routes); } -function setRoutes({ functions, assets }: RouteInfo) { +export function setRoutes({ functions, assets }: RouteInfo) { allRoutes.clear(); assetsCache.clear(); functionsCache.clear(); - functions.forEach(fn => { + functions.forEach((fn) => { if (!fn.path) { return; } @@ -47,7 +47,7 @@ function setRoutes({ functions, assets }: RouteInfo) { }); }); - assets.forEach(asset => { + assets.forEach((asset) => { if (!asset.path) { return; } diff --git a/packages/twilio-run/src/runtime/internal/runtime.ts b/packages/twilio-run/src/runtime/internal/runtime.ts index 0a1645cc..6571c8da 100644 --- a/packages/twilio-run/src/runtime/internal/runtime.ts +++ b/packages/twilio-run/src/runtime/internal/runtime.ts @@ -72,9 +72,9 @@ export function create({ env }: StartCliConfig): RuntimeInstance { .join(',')})`, }); const client = twilio(env.ACCOUNT_SID, env.AUTH_TOKEN, options); - const service = client.sync.services( + const service = (client.sync.services( serviceName || 'default' - ) as RuntimeSyncServiceContext; + ) as unknown) as RuntimeSyncServiceContext; service.maps = service.syncMaps; service.lists = service.syncLists; diff --git a/packages/twilio-run/src/runtime/route.ts b/packages/twilio-run/src/runtime/route.ts index 4977450f..6616afaa 100644 --- a/packages/twilio-run/src/runtime/route.ts +++ b/packages/twilio-run/src/runtime/route.ts @@ -3,12 +3,15 @@ import { ServerlessCallback, ServerlessFunctionSignature, } from '@twilio-labs/serverless-runtime-types/types'; +import { fork } from 'child_process'; import { NextFunction, Request as ExpressRequest, RequestHandler as ExpressRequestHandler, Response as ExpressResponse, } from 'express'; +import { join, resolve } from 'path'; +import { deserializeError } from 'serialize-error'; import twilio, { twiml } from 'twilio'; import { checkForValidAccountSid } from '../checks/check-account-sid'; import { checkForValidAuthToken } from '../checks/check-auth-token'; @@ -16,12 +19,14 @@ import { StartCliConfig } from '../config/start'; import { wrapErrorInHtml } from '../utils/error-html'; import { getDebugFunction } from '../utils/logger'; import { cleanUpStackTrace } from '../utils/stack-trace/clean-up'; +import { Reply } from './internal/functionRunner'; import { Response } from './internal/response'; import * as Runtime from './internal/runtime'; -import { fork } from 'child_process'; -import { deserializeError } from 'serialize-error'; -import { Reply } from './internal/functionRunner'; -import { join } from 'path'; + +const RUNNER_PATH = + process.env.NODE_ENV === 'test' + ? resolve(__dirname, '../../dist/runtime/internal/functionRunner') + : join(__dirname, 'internal', 'functionRunner'); const { VoiceResponse, MessagingResponse, FaxResponse } = twiml; @@ -155,7 +160,7 @@ export function functionPathToRoute( next: NextFunction ) { const event = constructEvent(req); - const forked = fork(join(__dirname, 'internal', 'functionRunner')); + const forked = fork(RUNNER_PATH); forked.on( 'message', ({ @@ -216,10 +221,12 @@ export function functionToRoute( run_timings.end = process.hrtime(); debug('Function execution %s finished', req.path); debug( - `(Estimated) Total Execution Time: ${(run_timings.end[0] * 1e9 + - run_timings.end[1] - - (run_timings.start[0] * 1e9 + run_timings.start[1])) / - 1e6}ms` + `(Estimated) Total Execution Time: ${ + (run_timings.end[0] * 1e9 + + run_timings.end[1] - + (run_timings.start[0] * 1e9 + run_timings.start[1])) / + 1e6 + }ms` ); if (err) { handleError(err, req, res, functionFilePath); diff --git a/packages/twilio-run/src/runtime/server.ts b/packages/twilio-run/src/runtime/server.ts index a3fd3275..c364a9bd 100644 --- a/packages/twilio-run/src/runtime/server.ts +++ b/packages/twilio-run/src/runtime/server.ts @@ -1,4 +1,8 @@ import { ServerlessFunctionSignature } from '@twilio-labs/serverless-runtime-types/types'; +import type { + LocalDevelopmentServer as LDS, + ServerConfig, +} from '@twilio/runtime-handler/dist/dev-runtime/server'; import bodyParser from 'body-parser'; import chokidar from 'chokidar'; import express, { @@ -14,7 +18,9 @@ import path from 'path'; import { StartCliConfig } from '../config/start'; import { printRouteInfo } from '../printers/start'; import { wrapErrorInHtml } from '../utils/error-html'; -import { getDebugFunction } from '../utils/logger'; +import { getDebugFunction, logger } from '../utils/logger'; +import { writeOutput } from '../utils/output'; +import { requireFromProject } from '../utils/requireFromProject'; import { createLogger } from './internal/request-logger'; import { getRouteMap } from './internal/route-cache'; import { @@ -50,6 +56,108 @@ function requireCacheCleaner( next(); } +async function findRoutes(config: StartCliConfig): Promise { + const searchConfig: SearchConfig = {}; + + if (config.functionsFolderName) { + searchConfig.functionsFolderNames = [config.functionsFolderName]; + } + + if (config.assetsFolderName) { + searchConfig.assetsFolderNames = [config.assetsFolderName]; + } + + return getFunctionsAndAssets(config.baseDir, searchConfig); +} + +function configureWatcher(config: StartCliConfig, server: LDS) { + const watcher = chokidar.watch( + [ + path.join( + config.baseDir, + config.functionsFolderName + ? `/(${config.functionsFolderName})/**/*)` + : '/(functions|src)/**/*.js' + ), + path.join( + config.baseDir, + config.assetsFolderName + ? `/(${config.assetsFolderName})/**/*)` + : '/(assets|static)/**/*' + ), + ], + { + ignoreInitial: true, + } + ); + + const reloadRoutes = async () => { + const routes = await findRoutes(config); + server.update(routes); + }; + + // Debounce so we don't needlessly reload when multiple files are changed + const debouncedReloadRoutes = debounce(reloadRoutes, RELOAD_DEBOUNCE_MS); + + watcher + .on('add', (path) => { + debug(`Reloading Routes: add @ ${path}`); + debouncedReloadRoutes(); + }) + .on('unlink', (path) => { + debug(`Reloading Routes: unlink @ ${path}`); + debouncedReloadRoutes(); + }); + + // Clean the watcher up when exiting. + process.on('exit', () => watcher.close()); +} + +export async function createLocalDevelopmentServer( + port: string | number = DEFAULT_PORT, + config: StartCliConfig +): Promise { + try { + const { LocalDevelopmentServer } = requireFromProject( + config.baseDir, + '@twilio/runtime-handler/dev' + ) as { LocalDevelopmentServer: LDS }; + + const routes = await findRoutes(config); + + const server = new LocalDevelopmentServer(port, { + inspect: config.inspect, + baseDir: config.baseDir, + env: config.env, + port: config.port, + url: config.url, + detailedLogs: config.detailedLogs, + live: config.live, + logs: config.logs, + legacyMode: config.legacyMode, + appName: config.appName, + forkProcess: config.forkProcess, + logger: logger, + routes: routes, + enableDebugLogs: true, + }); + server.on('request-log', (logMessage: string) => { + writeOutput(logMessage); + }); + server.on('updated-routes', async (config: ServerConfig) => { + await printRouteInfo(config); + }); + configureWatcher(config, server); + return server.getApp(); + } catch (err) { + debug( + 'Failed to load server from @twilio/runtime-handler/dev. Falling back to built-in.' + ); + return createServer(port, config); + } +} + +/** @deprecated */ export async function createServer( port: string | number = DEFAULT_PORT, config: StartCliConfig diff --git a/packages/twilio-run/src/utils/logger.ts b/packages/twilio-run/src/utils/logger.ts index b0896a90..de4a7049 100644 --- a/packages/twilio-run/src/utils/logger.ts +++ b/packages/twilio-run/src/utils/logger.ts @@ -1,9 +1,9 @@ import { ClientApiError } from '@twilio-labs/serverless-api/dist/utils/error'; -import debug from './debug'; import ora from 'ora'; import { Writable } from 'stream'; import terminalLink from 'terminal-link'; import { errorMessage, warningMessage } from '../printers/utils'; +import debug from './debug'; // an empty stream that immediately drops everything. Like /dev/null const EmptyStream = new Writable(); diff --git a/packages/twilio-run/src/utils/requireFromProject.ts b/packages/twilio-run/src/utils/requireFromProject.ts new file mode 100644 index 00000000..300bddf4 --- /dev/null +++ b/packages/twilio-run/src/utils/requireFromProject.ts @@ -0,0 +1,6 @@ +import { createRequire } from 'module'; +import { join } from 'path'; + +export function requireFromProject(projectDir: string, packageName: string) { + return createRequire(join(projectDir, 'node_modules'))(packageName); +} From 2661a88d070a1e6284f211800927d44a9538966f Mon Sep 17 00:00:00 2001 From: Dominik Kundel Date: Thu, 20 May 2021 16:50:19 -0700 Subject: [PATCH 02/13] chore: fix build --- packages/twilio-run/src/runtime/server.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/twilio-run/src/runtime/server.ts b/packages/twilio-run/src/runtime/server.ts index c364a9bd..5ef8cbe2 100644 --- a/packages/twilio-run/src/runtime/server.ts +++ b/packages/twilio-run/src/runtime/server.ts @@ -1,3 +1,4 @@ +import { SearchConfig } from '@twilio-labs/serverless-api/dist/utils'; import { ServerlessFunctionSignature } from '@twilio-labs/serverless-runtime-types/types'; import type { LocalDevelopmentServer as LDS, @@ -23,6 +24,7 @@ import { writeOutput } from '../utils/output'; import { requireFromProject } from '../utils/requireFromProject'; import { createLogger } from './internal/request-logger'; import { getRouteMap } from './internal/route-cache'; +import { getFunctionsAndAssets, RouteInfo } from './internal/runtime-paths'; import { constructGlobalScope, functionPathToRoute, From c245ef629443df342f45192a4aa5b9dbbb05d573 Mon Sep 17 00:00:00 2001 From: Dominik Kundel Date: Thu, 20 May 2021 17:11:31 -0700 Subject: [PATCH 03/13] docs(runtime-handler): fill in the readme with some basic instructions --- packages/runtime-handler/README.md | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/packages/runtime-handler/README.md b/packages/runtime-handler/README.md index ed0ab40e..bd40d152 100644 --- a/packages/runtime-handler/README.md +++ b/packages/runtime-handler/README.md @@ -1,6 +1,31 @@ # `@twilio/runtime-handler` -This package defines the Twilio Functions Runtime version. It is currently empty. +## How to use + +Define the version of the `@twilio/runtime-handler` inside your `dependencies` section of the `package.json`. For example: + +```json +{ + "dependencies": { + "@twilio/runtime-handler": "1.1.0" + } +} +``` + +**Important:** You need to specify the exact version you want to use. Semver ranges are at the moment not supported. + +## Local Development Feature + +The following features are primarily designed to be used by `twilio-run` and the Serverless Toolkit. This part of the package emulates Twilio Functions for local development and **is NOT available when deployed to Twilio Functions**. + +If for some reason you are trying to set up your own local development environment outside of the Serverless Toolkit you can do so through + +```js +const { LocalDevelopmentServer } = require('@twilio/runtime-handler/dev'); + +const server = new LocalDevelopmentServer(port, config); +server.getApp().listen(port); +``` ## Contributing From 33ed4faad85a0a631a2f9a1b3c8371ca256f1a6a Mon Sep 17 00:00:00 2001 From: Dominik Kundel Date: Mon, 24 May 2021 16:23:05 -0700 Subject: [PATCH 04/13] chore(runtime-handler): catch up version --- packages/runtime-handler/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/runtime-handler/package.json b/packages/runtime-handler/package.json index b5f5bc32..bdfca3cf 100644 --- a/packages/runtime-handler/package.json +++ b/packages/runtime-handler/package.json @@ -1,6 +1,6 @@ { "name": "@twilio/runtime-handler", - "version": "1.0.2", + "version": "1.1.0-rc.2", "description": "Stub runtime for Twilio Functions", "keywords": [ "twilio", From 2d11e84eff1947f17db23ce3d5fa4649a505d35e Mon Sep 17 00:00:00 2001 From: Dominik Kundel Date: Mon, 24 May 2021 16:24:25 -0700 Subject: [PATCH 05/13] chore(lerna): fix features brand typo --- lerna.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lerna.json b/lerna.json index 52ca4391..245543bb 100644 --- a/lerna.json +++ b/lerna.json @@ -12,7 +12,7 @@ "npmClientArgs": ["--no-package-lock"] }, "version": { - "allowBranch": ["main", "feature/*"] + "allowBranch": ["main", "features/*"] } }, "ignoreChanges": ["**/__fixtures__/**", "**/__tests__/**"], From f0ac4c0e6b1b0717ae7db7c7c1908447ca7a51d3 Mon Sep 17 00:00:00 2001 From: Dominik Kundel Date: Mon, 24 May 2021 16:43:55 -0700 Subject: [PATCH 06/13] chore: fix configuration --- packages/twilio-run/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/twilio-run/package.json b/packages/twilio-run/package.json index 8b2c8ed8..97a45900 100644 --- a/packages/twilio-run/package.json +++ b/packages/twilio-run/package.json @@ -85,7 +85,7 @@ "ngrok": "^3.3.0" }, "devDependencies": { - "@twilio/runtime-handler": "^1.0.2-beta.3", + "@twilio/runtime-handler": "1.1.0-rc.2", "@types/cheerio": "^0.22.12", "@types/common-tags": "^1.8.0", "@types/debug": "^4.1.4", From 0118fac3e34644d3de0117102f3fbc33ae178f9d Mon Sep 17 00:00:00 2001 From: Dominik Kundel Date: Mon, 24 May 2021 16:50:11 -0700 Subject: [PATCH 07/13] chore(release): publish %s - create-twilio-function@3.0.2-rc.0 - @twilio-labs/plugin-assets@1.0.1-rc.0 - @twilio-labs/plugin-serverless@2.0.2-rc.0 - @twilio/runtime-handler@1.1.0-rc.3 - @twilio-labs/serverless-api@5.1.0-rc.0 - @twilio-labs/serverless-runtime-types@2.1.0-rc.0 - twilio-run@3.1.0-rc.0 --- packages/create-twilio-function/CHANGELOG.md | 8 ++++++++ packages/create-twilio-function/package.json | 4 ++-- packages/plugin-assets/CHANGELOG.md | 8 ++++++++ packages/plugin-assets/README.md | 6 +++--- packages/plugin-assets/package.json | 4 ++-- packages/plugin-serverless/CHANGELOG.md | 8 ++++++++ packages/plugin-serverless/README.md | 16 ++++++++-------- packages/plugin-serverless/package.json | 6 +++--- packages/runtime-handler/CHANGELOG.md | 11 +++++++++++ packages/runtime-handler/package.json | 6 +++--- packages/serverless-api/CHANGELOG.md | 11 +++++++++++ packages/serverless-api/package.json | 2 +- packages/serverless-runtime-types/CHANGELOG.md | 11 +++++++++++ packages/serverless-runtime-types/package.json | 2 +- packages/twilio-run/CHANGELOG.md | 11 +++++++++++ packages/twilio-run/package.json | 8 ++++---- 16 files changed, 95 insertions(+), 27 deletions(-) diff --git a/packages/create-twilio-function/CHANGELOG.md b/packages/create-twilio-function/CHANGELOG.md index f7b5db5f..4061bca0 100644 --- a/packages/create-twilio-function/CHANGELOG.md +++ b/packages/create-twilio-function/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [3.0.2-rc.0](https://github.com/twilio-labs/serverless-toolkit/compare/create-twilio-function@3.0.1...create-twilio-function@3.0.2-rc.0) (2021-05-24) + +**Note:** Version bump only for package create-twilio-function + + + + + ## [3.0.1](https://github.com/twilio-labs/serverless-toolkit/compare/create-twilio-function@3.0.0...create-twilio-function@3.0.1) (2021-05-20) **Note:** Version bump only for package create-twilio-function diff --git a/packages/create-twilio-function/package.json b/packages/create-twilio-function/package.json index 91f65a75..305dfd56 100644 --- a/packages/create-twilio-function/package.json +++ b/packages/create-twilio-function/package.json @@ -1,6 +1,6 @@ { "name": "create-twilio-function", - "version": "3.0.1", + "version": "3.0.2-rc.0", "description": "A CLI tool to generate a new Twilio Function using that can be run locally with twilio-run.", "bin": "./bin/create-twilio-function", "main": "./src/create-twilio-function.js", @@ -38,7 +38,7 @@ "pkg-install": "^1.0.0", "rimraf": "^2.6.3", "terminal-link": "^2.0.0", - "twilio-run": "^3.0.1", + "twilio-run": "3.1.0-rc.0", "window-size": "^1.1.1", "wrap-ansi": "^6.0.0", "yargs": "^12.0.5" diff --git a/packages/plugin-assets/CHANGELOG.md b/packages/plugin-assets/CHANGELOG.md index 31dc27ff..1ec561b4 100644 --- a/packages/plugin-assets/CHANGELOG.md +++ b/packages/plugin-assets/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [1.0.1-rc.0](https://github.com/twilio-labs/serverless-toolkit/compare/@twilio-labs/plugin-assets@1.0.0...@twilio-labs/plugin-assets@1.0.1-rc.0) (2021-05-24) + +**Note:** Version bump only for package @twilio-labs/plugin-assets + + + + + # [1.0.0](https://github.com/twilio-labs/serverless-toolkit/compare/@twilio-labs/plugin-assets@0.1.0-beta.2...@twilio-labs/plugin-assets@1.0.0) (2021-05-19) **Note:** Version bump only for package @twilio-labs/plugin-assets diff --git a/packages/plugin-assets/README.md b/packages/plugin-assets/README.md index 34011289..21c3f6b2 100644 --- a/packages/plugin-assets/README.md +++ b/packages/plugin-assets/README.md @@ -65,7 +65,7 @@ OPTIONS would like to display (JSON output always shows all properties). ``` -_See code: [src/commands/assets/init.js](https://github.com/twilio-labs/serverless-toolkit/blob/v1.0.0/src/commands/assets/init.js)_ +_See code: [src/commands/assets/init.js](https://github.com/twilio-labs/serverless-toolkit/blob/v1.0.1-rc.0/src/commands/assets/init.js)_ ## `twilio assets:list` @@ -86,7 +86,7 @@ OPTIONS (JSON output always shows all properties). ``` -_See code: [src/commands/assets/list.js](https://github.com/twilio-labs/serverless-toolkit/blob/v1.0.0/src/commands/assets/list.js)_ +_See code: [src/commands/assets/list.js](https://github.com/twilio-labs/serverless-toolkit/blob/v1.0.1-rc.0/src/commands/assets/list.js)_ ## `twilio assets:upload FILE` @@ -110,7 +110,7 @@ OPTIONS (JSON output always shows all properties). ``` -_See code: [src/commands/assets/upload.js](https://github.com/twilio-labs/serverless-toolkit/blob/v1.0.0/src/commands/assets/upload.js)_ +_See code: [src/commands/assets/upload.js](https://github.com/twilio-labs/serverless-toolkit/blob/v1.0.1-rc.0/src/commands/assets/upload.js)_ ## Contributing diff --git a/packages/plugin-assets/package.json b/packages/plugin-assets/package.json index ffba91ec..56d6ef3e 100644 --- a/packages/plugin-assets/package.json +++ b/packages/plugin-assets/package.json @@ -1,7 +1,7 @@ { "name": "@twilio-labs/plugin-assets", "description": "Easily upload assets to a Twilio Assets service", - "version": "1.0.0", + "version": "1.0.1-rc.0", "author": "Twilio Inc. (https://www.twilio.com/labs)", "contributors": [ "Phil Nash " @@ -9,7 +9,7 @@ "dependencies": { "@oclif/command": "^1.5.19", "@oclif/config": "^1.13.3", - "@twilio-labs/serverless-api": "^5.0.0", + "@twilio-labs/serverless-api": "5.1.0-rc.0", "@twilio/cli-core": "^5.22.0", "inquirer": "^8.0.0", "ora": "^5.4.0" diff --git a/packages/plugin-serverless/CHANGELOG.md b/packages/plugin-serverless/CHANGELOG.md index 539a1643..9df708dc 100644 --- a/packages/plugin-serverless/CHANGELOG.md +++ b/packages/plugin-serverless/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [2.0.2-rc.0](https://github.com/twilio-labs/serverless-toolkit/compare/@twilio-labs/plugin-serverless@2.0.1...@twilio-labs/plugin-serverless@2.0.2-rc.0) (2021-05-24) + +**Note:** Version bump only for package @twilio-labs/plugin-serverless + + + + + ## [2.0.1](https://github.com/twilio-labs/serverless-toolkit/compare/@twilio-labs/plugin-serverless@2.0.0...@twilio-labs/plugin-serverless@2.0.1) (2021-05-20) **Note:** Version bump only for package @twilio-labs/plugin-serverless diff --git a/packages/plugin-serverless/README.md b/packages/plugin-serverless/README.md index 294cd0b8..b1b94cfe 100644 --- a/packages/plugin-serverless/README.md +++ b/packages/plugin-serverless/README.md @@ -123,7 +123,7 @@ OPTIONS deployment ``` -_See code: [src/commands/serverless/deploy.js](https://github.com/twilio-labs/serverless-toolkit/blob/v2.0.1/src/commands/serverless/deploy.js)_ +_See code: [src/commands/serverless/deploy.js](https://github.com/twilio-labs/serverless-toolkit/blob/v2.0.2-rc.0/src/commands/serverless/deploy.js)_ ## `twilio serverless:init NAME` @@ -155,7 +155,7 @@ OPTIONS --typescript Initialize your Serverless project with TypeScript ``` -_See code: [src/commands/serverless/init.js](https://github.com/twilio-labs/serverless-toolkit/blob/v2.0.1/src/commands/serverless/init.js)_ +_See code: [src/commands/serverless/init.js](https://github.com/twilio-labs/serverless-toolkit/blob/v2.0.2-rc.0/src/commands/serverless/init.js)_ ## `twilio serverless:list [TYPES]` @@ -202,7 +202,7 @@ OPTIONS --to=to [default: dev] The environment to list variables for ``` -_See code: [src/commands/serverless/list.js](https://github.com/twilio-labs/serverless-toolkit/blob/v2.0.1/src/commands/serverless/list.js)_ +_See code: [src/commands/serverless/list.js](https://github.com/twilio-labs/serverless-toolkit/blob/v2.0.2-rc.0/src/commands/serverless/list.js)_ ## `twilio serverless:list-templates` @@ -221,7 +221,7 @@ OPTIONS --env=env Path to .env file for environment variables that should be installed ``` -_See code: [src/commands/serverless/list-templates.js](https://github.com/twilio-labs/serverless-toolkit/blob/v2.0.1/src/commands/serverless/list-templates.js)_ +_See code: [src/commands/serverless/list-templates.js](https://github.com/twilio-labs/serverless-toolkit/blob/v2.0.2-rc.0/src/commands/serverless/list-templates.js)_ ## `twilio serverless:logs` @@ -269,7 +269,7 @@ OPTIONS --to=to [default: dev] The environment to retrieve the logs for ``` -_See code: [src/commands/serverless/logs.js](https://github.com/twilio-labs/serverless-toolkit/blob/v2.0.1/src/commands/serverless/logs.js)_ +_See code: [src/commands/serverless/logs.js](https://github.com/twilio-labs/serverless-toolkit/blob/v2.0.2-rc.0/src/commands/serverless/logs.js)_ ## `twilio serverless:new [NAMESPACE]` @@ -292,7 +292,7 @@ OPTIONS --template=template ``` -_See code: [src/commands/serverless/new.js](https://github.com/twilio-labs/serverless-toolkit/blob/v2.0.1/src/commands/serverless/new.js)_ +_See code: [src/commands/serverless/new.js](https://github.com/twilio-labs/serverless-toolkit/blob/v2.0.2-rc.0/src/commands/serverless/new.js)_ ## `twilio serverless:promote` @@ -353,7 +353,7 @@ ALIASES $ twilio serverless:activate ``` -_See code: [src/commands/serverless/promote.js](https://github.com/twilio-labs/serverless-toolkit/blob/v2.0.1/src/commands/serverless/promote.js)_ +_See code: [src/commands/serverless/promote.js](https://github.com/twilio-labs/serverless-toolkit/blob/v2.0.2-rc.0/src/commands/serverless/promote.js)_ ## `twilio serverless:start [DIR]` @@ -409,7 +409,7 @@ ALIASES $ twilio serverless:run ``` -_See code: [src/commands/serverless/start.js](https://github.com/twilio-labs/serverless-toolkit/blob/v2.0.1/src/commands/serverless/start.js)_ +_See code: [src/commands/serverless/start.js](https://github.com/twilio-labs/serverless-toolkit/blob/v2.0.2-rc.0/src/commands/serverless/start.js)_ ## Contributing diff --git a/packages/plugin-serverless/package.json b/packages/plugin-serverless/package.json index 07e500f7..06a36316 100644 --- a/packages/plugin-serverless/package.json +++ b/packages/plugin-serverless/package.json @@ -1,7 +1,7 @@ { "name": "@twilio-labs/plugin-serverless", "description": "Develop and deploy Twilio Serverless Functions", - "version": "2.0.1", + "version": "2.0.2-rc.0", "author": "Twilio Inc. (https://www.twilio.com/labs)", "contributors": [ "Dominik Kundel " @@ -10,10 +10,10 @@ "@oclif/command": "^1.5.19", "@oclif/config": "^1.13.3", "@twilio/cli-core": "^4.3.3", - "create-twilio-function": "^3.0.1", + "create-twilio-function": "3.0.2-rc.0", "lodash.camelcase": "^4.3.0", "lodash.kebabcase": "^4.1.1", - "twilio-run": "^3.0.1" + "twilio-run": "3.1.0-rc.0" }, "devDependencies": { "@oclif/dev-cli": "^1.22.2", diff --git a/packages/runtime-handler/CHANGELOG.md b/packages/runtime-handler/CHANGELOG.md index f37cdca6..f0fe0223 100644 --- a/packages/runtime-handler/CHANGELOG.md +++ b/packages/runtime-handler/CHANGELOG.md @@ -3,6 +3,17 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [1.1.0-rc.3](https://github.com/twilio-labs/serverless-toolkit/compare/@twilio/runtime-handler@1.0.2...@twilio/runtime-handler@1.1.0-rc.3) (2021-05-24) + + +### Features + +* extract runtime-handler and lazyLoading ([#252](https://github.com/twilio-labs/serverless-toolkit/issues/252)) ([4b11e69](https://github.com/twilio-labs/serverless-toolkit/commit/4b11e693248e44a8c6db4a95cf90e79e00f7db08)) + + + + + ## [1.0.2](https://github.com/twilio-labs/serverless-toolkit/compare/@twilio/runtime-handler@1.0.2-beta.3...@twilio/runtime-handler@1.0.2) (2021-05-19) **Note:** Version bump only for package @twilio/runtime-handler diff --git a/packages/runtime-handler/package.json b/packages/runtime-handler/package.json index bdfca3cf..9baab009 100644 --- a/packages/runtime-handler/package.json +++ b/packages/runtime-handler/package.json @@ -1,6 +1,6 @@ { "name": "@twilio/runtime-handler", - "version": "1.1.0-rc.2", + "version": "1.1.0-rc.3", "description": "Stub runtime for Twilio Functions", "keywords": [ "twilio", @@ -42,10 +42,10 @@ "prepack": "run-s clean build" }, "devDependencies": { - "@types/jest": "^24.0.16", "@types/common-tags": "^1.8.0", "@types/debug": "^4.1.4", "@types/express-useragent": "^0.2.21", + "@types/jest": "^24.0.16", "@types/lodash.debounce": "^4.0.6", "@types/node": "^14.0.19", "@types/supertest": "^2.0.8", @@ -61,7 +61,7 @@ "url": "https://github.com/twilio-labs/serverless-toolkit/issues" }, "dependencies": { - "@twilio-labs/serverless-runtime-types": "^2.0.0-beta.3", + "@twilio-labs/serverless-runtime-types": "2.1.0-rc.0", "@types/express": "4.17.7", "chalk": "^4.1.1", "chokidar": "^3.2.3", diff --git a/packages/serverless-api/CHANGELOG.md b/packages/serverless-api/CHANGELOG.md index af1f0125..2be2d8c7 100644 --- a/packages/serverless-api/CHANGELOG.md +++ b/packages/serverless-api/CHANGELOG.md @@ -3,6 +3,17 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [5.1.0-rc.0](https://github.com/twilio-labs/serverless-toolkit/compare/@twilio-labs/serverless-api@5.0.0...@twilio-labs/serverless-api@5.1.0-rc.0) (2021-05-24) + + +### Features + +* extract runtime-handler and lazyLoading ([#252](https://github.com/twilio-labs/serverless-toolkit/issues/252)) ([4b11e69](https://github.com/twilio-labs/serverless-toolkit/commit/4b11e693248e44a8c6db4a95cf90e79e00f7db08)) + + + + + # [5.0.0](https://github.com/twilio-labs/serverless-toolkit/compare/@twilio-labs/serverless-api@5.0.0-beta.6...@twilio-labs/serverless-api@5.0.0) (2021-05-19) **Note:** Version bump only for package @twilio-labs/serverless-api diff --git a/packages/serverless-api/package.json b/packages/serverless-api/package.json index 5a7223ec..dc9a153f 100644 --- a/packages/serverless-api/package.json +++ b/packages/serverless-api/package.json @@ -1,6 +1,6 @@ { "name": "@twilio-labs/serverless-api", - "version": "5.0.0", + "version": "5.1.0-rc.0", "description": "API-wrapper for the Twilio Serverless API", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/serverless-runtime-types/CHANGELOG.md b/packages/serverless-runtime-types/CHANGELOG.md index 12e1f81b..e6891b2a 100644 --- a/packages/serverless-runtime-types/CHANGELOG.md +++ b/packages/serverless-runtime-types/CHANGELOG.md @@ -3,6 +3,17 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [2.1.0-rc.0](https://github.com/twilio-labs/serverless-toolkit/compare/@twilio-labs/serverless-runtime-types@2.0.0...@twilio-labs/serverless-runtime-types@2.1.0-rc.0) (2021-05-24) + + +### Features + +* extract runtime-handler and lazyLoading ([#252](https://github.com/twilio-labs/serverless-toolkit/issues/252)) ([4b11e69](https://github.com/twilio-labs/serverless-toolkit/commit/4b11e693248e44a8c6db4a95cf90e79e00f7db08)) + + + + + # [2.0.0](https://github.com/twilio-labs/serverless-toolkit/compare/@twilio-labs/serverless-runtime-types@2.0.0-beta.3...@twilio-labs/serverless-runtime-types@2.0.0) (2021-05-19) **Note:** Version bump only for package @twilio-labs/serverless-runtime-types diff --git a/packages/serverless-runtime-types/package.json b/packages/serverless-runtime-types/package.json index f12ce46a..0af4fd63 100644 --- a/packages/serverless-runtime-types/package.json +++ b/packages/serverless-runtime-types/package.json @@ -1,6 +1,6 @@ { "name": "@twilio-labs/serverless-runtime-types", - "version": "2.0.0", + "version": "2.1.0-rc.0", "description": "TypeScript definitions to define globals for the Twilio Serverless runtime", "main": "index.js", "types": "index.d.ts", diff --git a/packages/twilio-run/CHANGELOG.md b/packages/twilio-run/CHANGELOG.md index 574e03c9..19848947 100644 --- a/packages/twilio-run/CHANGELOG.md +++ b/packages/twilio-run/CHANGELOG.md @@ -3,6 +3,17 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [3.1.0-rc.0](https://github.com/twilio-labs/serverless-toolkit/compare/twilio-run@3.0.1...twilio-run@3.1.0-rc.0) (2021-05-24) + + +### Features + +* extract runtime-handler and lazyLoading ([#252](https://github.com/twilio-labs/serverless-toolkit/issues/252)) ([4b11e69](https://github.com/twilio-labs/serverless-toolkit/commit/4b11e693248e44a8c6db4a95cf90e79e00f7db08)) + + + + + ## [3.0.1](https://github.com/twilio-labs/serverless-toolkit/compare/twilio-run@3.0.0...twilio-run@3.0.1) (2021-05-20) diff --git a/packages/twilio-run/package.json b/packages/twilio-run/package.json index 97a45900..e08fa542 100644 --- a/packages/twilio-run/package.json +++ b/packages/twilio-run/package.json @@ -1,6 +1,6 @@ { "name": "twilio-run", - "version": "3.0.1", + "version": "3.1.0-rc.0", "bin": { "twilio-functions": "./bin/twilio-run.js", "twilio-run": "./bin/twilio-run.js", @@ -35,8 +35,8 @@ ], "license": "MIT", "dependencies": { - "@twilio-labs/serverless-api": "^5.0.0", - "@twilio-labs/serverless-runtime-types": "^2.0.0", + "@twilio-labs/serverless-api": "5.1.0-rc.0", + "@twilio-labs/serverless-runtime-types": "2.1.0-rc.0", "@types/express": "4.17.7", "@types/inquirer": "^6.0.3", "@types/is-ci": "^2.0.0", @@ -85,7 +85,7 @@ "ngrok": "^3.3.0" }, "devDependencies": { - "@twilio/runtime-handler": "1.1.0-rc.2", + "@twilio/runtime-handler": "1.1.0-rc.3", "@types/cheerio": "^0.22.12", "@types/common-tags": "^1.8.0", "@types/debug": "^4.1.4", From 2fef3ca9a9f688fa1d6a91ed2e7a2a0b8e989316 Mon Sep 17 00:00:00 2001 From: Dominik Kundel Date: Tue, 8 Jun 2021 09:07:11 -0700 Subject: [PATCH 08/13] feat(create-twilio-function): add default runtime-handler --- packages/create-twilio-function/package.json | 1 + .../src/create-twilio-function/create-files.js | 1 + .../src/create-twilio-function/versions.js | 3 +++ 3 files changed, 5 insertions(+) diff --git a/packages/create-twilio-function/package.json b/packages/create-twilio-function/package.json index 305dfd56..3a0f735e 100644 --- a/packages/create-twilio-function/package.json +++ b/packages/create-twilio-function/package.json @@ -26,6 +26,7 @@ }, "license": "MIT", "devDependencies": { + "@twilio/runtime-handler": "1.1.0-rc.3", "jest": "^24.5.0", "nock": "^11.3.4" }, diff --git a/packages/create-twilio-function/src/create-twilio-function/create-files.js b/packages/create-twilio-function/src/create-twilio-function/create-files.js index be881330..a5c52af8 100644 --- a/packages/create-twilio-function/src/create-twilio-function/create-files.js +++ b/packages/create-twilio-function/src/create-twilio-function/create-files.js @@ -29,6 +29,7 @@ const typescriptDeps = { }; const javaScriptDevDeps = { 'twilio-run': versions.twilioRun }; const typescriptDevDeps = { + '@twilio/runtime-handler': versions.twilioRuntimeHandler, 'twilio-run': versions.twilioRun, typescript: versions.typescript, copyfiles: versions.copyfiles, diff --git a/packages/create-twilio-function/src/create-twilio-function/versions.js b/packages/create-twilio-function/src/create-twilio-function/versions.js index f645b3f2..c5d78af3 100644 --- a/packages/create-twilio-function/src/create-twilio-function/versions.js +++ b/packages/create-twilio-function/src/create-twilio-function/versions.js @@ -2,6 +2,9 @@ const pkgJson = require('../../package.json'); module.exports = { twilio: '^3.56', + twilioRuntimeHandler: pkgJson.devDependencies[ + '@twilio/runtime-handler' + ].replace(/[\^~]/, ''), twilioRun: pkgJson.dependencies['twilio-run'], node: '12', typescript: '^3.8', From e6586d0facb74c005c114fd79e632be1baa9bf82 Mon Sep 17 00:00:00 2001 From: Dominik Kundel Date: Tue, 8 Jun 2021 09:11:31 -0700 Subject: [PATCH 09/13] chore(runtime-handler): fix dependencies and pin default twilio --- packages/runtime-handler/package.json | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/runtime-handler/package.json b/packages/runtime-handler/package.json index 9baab009..a924f626 100644 --- a/packages/runtime-handler/package.json +++ b/packages/runtime-handler/package.json @@ -54,7 +54,6 @@ "rimraf": "^2.6.3", "supertest": "^3.1.0", "ts-jest": "^24.0.2", - "twilio": "^3.60.0", "typescript": "^3.8.3" }, "bugs": { @@ -64,15 +63,14 @@ "@twilio-labs/serverless-runtime-types": "2.1.0-rc.0", "@types/express": "4.17.7", "chalk": "^4.1.1", - "chokidar": "^3.2.3", "common-tags": "^1.8.0", "debug": "^3.1.0", "express": "^4.16.3", "express-useragent": "^1.0.13", "fast-redact": "^1.5.0", - "lodash.debounce": "^4.0.8", "nocache": "^2.1.0", "normalize.css": "^8.0.1", - "serialize-error": "^7.0.1" + "serialize-error": "^7.0.1", + "twilio": "3.29.2" } } From 8952b4b82d5645b5d8609c6da60de5626f43f0c4 Mon Sep 17 00:00:00 2001 From: Dominik Kundel Date: Tue, 8 Jun 2021 09:13:10 -0700 Subject: [PATCH 10/13] chore(runtime-handler): fix jest display name --- packages/runtime-handler/jest.config.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/runtime-handler/jest.config.js b/packages/runtime-handler/jest.config.js index c1f12360..96cb6866 100644 --- a/packages/runtime-handler/jest.config.js +++ b/packages/runtime-handler/jest.config.js @@ -7,6 +7,6 @@ module.exports = { tsconfig: './tsconfig.test.json', }, }, - name: 'serverless-api', - displayName: 'serverless-api', + name: 'runtime-handler', + displayName: 'runtime-handler', }; From 88e3b2f69d6e4beb58f3ea72346706a2e91eac15 Mon Sep 17 00:00:00 2001 From: Dominik Kundel Date: Tue, 8 Jun 2021 09:14:11 -0700 Subject: [PATCH 11/13] fix(runtime-handler): remove unncessary types --- packages/runtime-handler/src/dev-runtime/types.ts | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/packages/runtime-handler/src/dev-runtime/types.ts b/packages/runtime-handler/src/dev-runtime/types.ts index 702cf55d..3907f8b1 100644 --- a/packages/runtime-handler/src/dev-runtime/types.ts +++ b/packages/runtime-handler/src/dev-runtime/types.ts @@ -1,18 +1,3 @@ -export type SearchConfig = { - /** - * Ordered folder names to search for to find functions - * - * @type {string[]} - */ - functionsFolderNames?: string[]; - /** - * Ordered folder names to search for to find assets - * - * @type {string[]} - */ - assetsFolderNames?: string[]; -}; - export type EnvironmentVariablesWithAuth = { ACCOUNT_SID?: string; AUTH_TOKEN?: string; From e5a9ba18f85ed9d49e8179fd407e604232543f51 Mon Sep 17 00:00:00 2001 From: Dominik Kundel Date: Tue, 8 Jun 2021 09:15:01 -0700 Subject: [PATCH 12/13] fix: adjust requireFromProject logic to fallback --- .../requireFromProject.test.ts.snap | 3 ++ .../utils/requireFromProject.test.ts | 50 +++++++++++++++++++ .../src/dev-runtime/internal/runtime.ts | 2 +- .../runtime-handler/src/dev-runtime/route.ts | 4 +- .../dev-runtime/utils/requireFromProject.ts | 28 +++++++++-- .../requireFromProject.test.ts.snap | 3 ++ .../utils/requireFromProject.test.ts | 50 +++++++++++++++++++ .../src/utils/requireFromProject.ts | 27 ++++++++-- 8 files changed, 158 insertions(+), 9 deletions(-) create mode 100644 packages/runtime-handler/__tests__/dev-runtime/utils/__snapshots__/requireFromProject.test.ts.snap create mode 100644 packages/runtime-handler/__tests__/dev-runtime/utils/requireFromProject.test.ts create mode 100644 packages/twilio-run/__tests__/utils/__snapshots__/requireFromProject.test.ts.snap create mode 100644 packages/twilio-run/__tests__/utils/requireFromProject.test.ts diff --git a/packages/runtime-handler/__tests__/dev-runtime/utils/__snapshots__/requireFromProject.test.ts.snap b/packages/runtime-handler/__tests__/dev-runtime/utils/__snapshots__/requireFromProject.test.ts.snap new file mode 100644 index 00000000..100ad498 --- /dev/null +++ b/packages/runtime-handler/__tests__/dev-runtime/utils/__snapshots__/requireFromProject.test.ts.snap @@ -0,0 +1,3 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`requireFromProject should fail for unknown dependency 1`] = `"Cannot resolve module '@twilio/invalid-dependency' from paths ['/Users/dkundel/dev/twilio-run/packages/twilio-run/node_modules'] from /Users/dkundel/dev/twilio-run/packages/runtime-handler/src/dev-runtime/utils/requireFromProject.ts"`; diff --git a/packages/runtime-handler/__tests__/dev-runtime/utils/requireFromProject.test.ts b/packages/runtime-handler/__tests__/dev-runtime/utils/requireFromProject.test.ts new file mode 100644 index 00000000..8fc35885 --- /dev/null +++ b/packages/runtime-handler/__tests__/dev-runtime/utils/requireFromProject.test.ts @@ -0,0 +1,50 @@ +import { join } from 'path'; +import { requireFromProject } from '../../../src/dev-runtime/utils/requireFromProject'; + +const PROJECT_DIR = join(__dirname, '../../../../twilio-run'); + +jest.mock('../../../../twilio-run/node_modules/twilio', () => { + const x = jest.genMockFromModule('twilio'); + (x as any)['__TYPE__'] = 'PROJECT_BASED'; + return x; +}); + +jest.mock('twilio', () => { + const x = jest.genMockFromModule('twilio'); + (x as any)['__TYPE__'] = 'BUILT_IN'; + return x; +}); + +jest.mock( + '@twilio/invalid-dependency', + () => { + return { + __TYPE__: 'BUILT_IN', + }; + }, + { virtual: true } +); + +describe('requireFromProject', () => { + test('should return project based by default', () => { + const mod = requireFromProject(PROJECT_DIR, 'twilio'); + expect(mod['__TYPE__']).toBe('PROJECT_BASED'); + const mod2 = require('twilio'); + expect(mod2['__TYPE__']).toBe('BUILT_IN'); + }); + + test('should fail for unknown dependency', () => { + expect(() => { + requireFromProject(PROJECT_DIR, '@twilio/invalid-dependency'); + }).toThrowErrorMatchingSnapshot(); + }); + + test('should fallback for unmatched dependency', () => { + const mod = requireFromProject( + PROJECT_DIR, + '@twilio/invalid-dependency', + true + ); + expect(mod.__TYPE__).toBe('BUILT_IN'); + }); +}); diff --git a/packages/runtime-handler/src/dev-runtime/internal/runtime.ts b/packages/runtime-handler/src/dev-runtime/internal/runtime.ts index 70967193..6ebc7ffc 100644 --- a/packages/runtime-handler/src/dev-runtime/internal/runtime.ts +++ b/packages/runtime-handler/src/dev-runtime/internal/runtime.ts @@ -77,7 +77,7 @@ export function create({ .map((x: any) => JSON.stringify(x)) .join(',')})`, }); - const client = requireFromProject(baseDir, 'twilio')( + const client = requireFromProject(baseDir, 'twilio', true)( env.ACCOUNT_SID, env.AUTH_TOKEN, options diff --git a/packages/runtime-handler/src/dev-runtime/route.ts b/packages/runtime-handler/src/dev-runtime/route.ts index 5bc2a737..c153dc3d 100644 --- a/packages/runtime-handler/src/dev-runtime/route.ts +++ b/packages/runtime-handler/src/dev-runtime/route.ts @@ -63,7 +63,7 @@ export function constructContext( logger: logger, }); - return requireFromProject(baseDir, 'twilio')( + return requireFromProject(baseDir, 'twilio', true)( env.ACCOUNT_SID, env.AUTH_TOKEN, { @@ -78,7 +78,7 @@ export function constructContext( } export function constructGlobalScope(config: ServerConfig): void { - twilio = requireFromProject(config.baseDir, 'twilio'); + twilio = requireFromProject(config.baseDir, 'twilio', true); const GlobalRuntime = Runtime.create(config); (global as any)['Twilio'] = { ...twilio, Response }; (global as any)['Runtime'] = GlobalRuntime; diff --git a/packages/runtime-handler/src/dev-runtime/utils/requireFromProject.ts b/packages/runtime-handler/src/dev-runtime/utils/requireFromProject.ts index eb3f32b9..00c43544 100644 --- a/packages/runtime-handler/src/dev-runtime/utils/requireFromProject.ts +++ b/packages/runtime-handler/src/dev-runtime/utils/requireFromProject.ts @@ -1,6 +1,28 @@ -import { createRequire } from 'module'; import { join } from 'path'; +import debug from '../utils/debug'; -export function requireFromProject(baseDir: string, packageName: string) { - return createRequire(join(baseDir, 'node_modules'))(packageName); +const log = debug('twilio-runtime-handler:dev:requireFromProject'); + +export function requireFromProject( + baseDir: string, + packageName: string, + fallbackToBuiltIn: boolean = false +) { + try { + const lookupLocation = join(baseDir, 'node_modules'); + const moduleLocation = require.resolve(packageName, { + paths: [lookupLocation], + }); + return require(moduleLocation); + } catch (err) { + if (fallbackToBuiltIn) { + log( + 'Falling back to regular module resolution for package "%s"', + packageName + ); + return require(packageName); + } else { + throw err; + } + } } diff --git a/packages/twilio-run/__tests__/utils/__snapshots__/requireFromProject.test.ts.snap b/packages/twilio-run/__tests__/utils/__snapshots__/requireFromProject.test.ts.snap new file mode 100644 index 00000000..1661d1b3 --- /dev/null +++ b/packages/twilio-run/__tests__/utils/__snapshots__/requireFromProject.test.ts.snap @@ -0,0 +1,3 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`requireFromProject should fail for unknown dependency 1`] = `"Cannot resolve module '@twilio/invalid-dependency' from paths ['/Users/dkundel/dev/twilio-run/packages/runtime-handler/node_modules'] from /Users/dkundel/dev/twilio-run/packages/twilio-run/src/utils/requireFromProject.ts"`; diff --git a/packages/twilio-run/__tests__/utils/requireFromProject.test.ts b/packages/twilio-run/__tests__/utils/requireFromProject.test.ts new file mode 100644 index 00000000..2b68c7f5 --- /dev/null +++ b/packages/twilio-run/__tests__/utils/requireFromProject.test.ts @@ -0,0 +1,50 @@ +import { join } from 'path'; +import { requireFromProject } from '../../src/utils/requireFromProject'; + +const PROJECT_DIR = join(__dirname, '../../../runtime-handler'); + +jest.mock('../../../runtime-handler/node_modules/twilio', () => { + const x = jest.genMockFromModule('twilio'); + (x as any)['__TYPE__'] = 'PROJECT_BASED'; + return x; +}); + +jest.mock('twilio', () => { + const x = jest.genMockFromModule('twilio'); + (x as any)['__TYPE__'] = 'BUILT_IN'; + return x; +}); + +jest.mock( + '@twilio/invalid-dependency', + () => { + return { + __TYPE__: 'BUILT_IN', + }; + }, + { virtual: true } +); + +describe('requireFromProject', () => { + test('should return project based by default', () => { + const mod = requireFromProject(PROJECT_DIR, 'twilio'); + expect(mod['__TYPE__']).toBe('PROJECT_BASED'); + const mod2 = require('twilio'); + expect(mod2['__TYPE__']).toBe('BUILT_IN'); + }); + + test('should fail for unknown dependency', () => { + expect(() => { + requireFromProject(PROJECT_DIR, '@twilio/invalid-dependency'); + }).toThrowErrorMatchingSnapshot(); + }); + + test('should fallback for unmatched dependency', () => { + const mod = requireFromProject( + PROJECT_DIR, + '@twilio/invalid-dependency', + true + ); + expect(mod.__TYPE__).toBe('BUILT_IN'); + }); +}); diff --git a/packages/twilio-run/src/utils/requireFromProject.ts b/packages/twilio-run/src/utils/requireFromProject.ts index 300bddf4..97bb316f 100644 --- a/packages/twilio-run/src/utils/requireFromProject.ts +++ b/packages/twilio-run/src/utils/requireFromProject.ts @@ -1,6 +1,27 @@ -import { createRequire } from 'module'; import { join } from 'path'; +import { getDebugFunction } from '../utils/logger'; +const debug = getDebugFunction('twilio-run:requireFromProject'); -export function requireFromProject(projectDir: string, packageName: string) { - return createRequire(join(projectDir, 'node_modules'))(packageName); +export function requireFromProject( + baseDir: string, + packageName: string, + fallbackToBuiltIn: boolean = false +) { + try { + const lookupLocation = join(baseDir, 'node_modules'); + const moduleLocation = require.resolve(packageName, { + paths: [lookupLocation], + }); + return require(moduleLocation); + } catch (err) { + if (fallbackToBuiltIn) { + debug( + 'Falling back to regular module resolution for package "%s"', + packageName + ); + return require(packageName); + } else { + throw err; + } + } } From 48121c59e20f1a01844238cfdf7d1010e9a32471 Mon Sep 17 00:00:00 2001 From: Phil Nash Date: Thu, 10 Jun 2021 11:34:59 +1000 Subject: [PATCH 13/13] fix(require-from-project): tests relied on pathnames in snapshots Replaced with a looser test based on the content at the start of the error message that should be thrown. --- .../utils/__snapshots__/requireFromProject.test.ts.snap | 3 --- .../__tests__/dev-runtime/utils/requireFromProject.test.ts | 4 +++- .../utils/__snapshots__/requireFromProject.test.ts.snap | 3 --- .../twilio-run/__tests__/utils/requireFromProject.test.ts | 4 +++- 4 files changed, 6 insertions(+), 8 deletions(-) delete mode 100644 packages/runtime-handler/__tests__/dev-runtime/utils/__snapshots__/requireFromProject.test.ts.snap delete mode 100644 packages/twilio-run/__tests__/utils/__snapshots__/requireFromProject.test.ts.snap diff --git a/packages/runtime-handler/__tests__/dev-runtime/utils/__snapshots__/requireFromProject.test.ts.snap b/packages/runtime-handler/__tests__/dev-runtime/utils/__snapshots__/requireFromProject.test.ts.snap deleted file mode 100644 index 100ad498..00000000 --- a/packages/runtime-handler/__tests__/dev-runtime/utils/__snapshots__/requireFromProject.test.ts.snap +++ /dev/null @@ -1,3 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`requireFromProject should fail for unknown dependency 1`] = `"Cannot resolve module '@twilio/invalid-dependency' from paths ['/Users/dkundel/dev/twilio-run/packages/twilio-run/node_modules'] from /Users/dkundel/dev/twilio-run/packages/runtime-handler/src/dev-runtime/utils/requireFromProject.ts"`; diff --git a/packages/runtime-handler/__tests__/dev-runtime/utils/requireFromProject.test.ts b/packages/runtime-handler/__tests__/dev-runtime/utils/requireFromProject.test.ts index 8fc35885..cf53266f 100644 --- a/packages/runtime-handler/__tests__/dev-runtime/utils/requireFromProject.test.ts +++ b/packages/runtime-handler/__tests__/dev-runtime/utils/requireFromProject.test.ts @@ -36,7 +36,9 @@ describe('requireFromProject', () => { test('should fail for unknown dependency', () => { expect(() => { requireFromProject(PROJECT_DIR, '@twilio/invalid-dependency'); - }).toThrowErrorMatchingSnapshot(); + }).toThrowError( + /^Cannot resolve module '@twilio\/invalid-dependency' from paths/ + ); }); test('should fallback for unmatched dependency', () => { diff --git a/packages/twilio-run/__tests__/utils/__snapshots__/requireFromProject.test.ts.snap b/packages/twilio-run/__tests__/utils/__snapshots__/requireFromProject.test.ts.snap deleted file mode 100644 index 1661d1b3..00000000 --- a/packages/twilio-run/__tests__/utils/__snapshots__/requireFromProject.test.ts.snap +++ /dev/null @@ -1,3 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`requireFromProject should fail for unknown dependency 1`] = `"Cannot resolve module '@twilio/invalid-dependency' from paths ['/Users/dkundel/dev/twilio-run/packages/runtime-handler/node_modules'] from /Users/dkundel/dev/twilio-run/packages/twilio-run/src/utils/requireFromProject.ts"`; diff --git a/packages/twilio-run/__tests__/utils/requireFromProject.test.ts b/packages/twilio-run/__tests__/utils/requireFromProject.test.ts index 2b68c7f5..794fb53d 100644 --- a/packages/twilio-run/__tests__/utils/requireFromProject.test.ts +++ b/packages/twilio-run/__tests__/utils/requireFromProject.test.ts @@ -36,7 +36,9 @@ describe('requireFromProject', () => { test('should fail for unknown dependency', () => { expect(() => { requireFromProject(PROJECT_DIR, '@twilio/invalid-dependency'); - }).toThrowErrorMatchingSnapshot(); + }).toThrowError( + /^Cannot resolve module '@twilio\/invalid-dependency' from paths/ + ); }); test('should fallback for unmatched dependency', () => {