@netlify/edge-bundler 14.8.2 → 14.8.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,144 +0,0 @@
1
- import { createWriteStream } from 'fs';
2
- import { readFile } from 'fs/promises';
3
- import { join } from 'path';
4
- import process from 'process';
5
- // @ts-expect-error TypeScript is complaining about the values for the `module`
6
- // and `moduleResolution` configuration properties, but changing those to more
7
- // modern values causes other packages to fail. Leaving this for now, but we
8
- // should have a proper fix for this.
9
- import { getURL as getBootstrapURL } from '@netlify/edge-functions-bootstrap/version';
10
- import getPort from 'get-port';
11
- import tmp from 'tmp-promise';
12
- import { v4 as uuidv4 } from 'uuid';
13
- import { test, expect } from 'vitest';
14
- import { fixturesDir } from '../../test/util.js';
15
- import { serve } from '../index.js';
16
- test('Starts a server and serves requests for edge functions', async () => {
17
- const basePath = join(fixturesDir, 'serve_test');
18
- const paths = {
19
- internal: join(basePath, '.netlify', 'edge-functions'),
20
- user: join(basePath, 'netlify', 'edge-functions'),
21
- };
22
- const port = await getPort();
23
- const importMapPaths = [join(paths.internal, 'import_map.json'), join(paths.user, 'import-map.json')];
24
- const servePath = join(basePath, '.netlify', 'edge-functions-serve');
25
- const server = await serve({
26
- basePath,
27
- bootstrapURL: await getBootstrapURL(),
28
- port,
29
- servePath,
30
- });
31
- const functions = [
32
- {
33
- name: 'echo_env',
34
- path: join(paths.user, 'echo_env.ts'),
35
- },
36
- {
37
- name: 'greet',
38
- path: join(paths.internal, 'greet.ts'),
39
- },
40
- {
41
- name: 'global_netlify',
42
- path: join(paths.user, 'global_netlify.ts'),
43
- },
44
- ];
45
- const options = {
46
- getFunctionsConfig: true,
47
- importMapPaths,
48
- };
49
- const { features, functionsConfig, graph, success } = await server(functions, {
50
- very_secret_secret: 'i love netlify',
51
- }, options);
52
- expect(features).toEqual({ npmModules: true });
53
- expect(success).toBe(true);
54
- expect(functionsConfig).toEqual([{ path: '/my-function' }, {}, { path: '/global-netlify' }]);
55
- const modules = graph?.modules.filter(({ kind, mediaType }) => kind === 'esm' && mediaType === 'TypeScript');
56
- for (const key in functions) {
57
- const graphEntry = modules?.some(({ local }) => local === functions[key].path);
58
- expect(graphEntry).toBe(true);
59
- }
60
- const response1 = await fetch(`http://0.0.0.0:${port}/foo`, {
61
- headers: {
62
- 'x-nf-edge-functions': 'echo_env',
63
- 'x-ef-passthrough': 'passthrough',
64
- 'X-NF-Request-ID': uuidv4(),
65
- },
66
- });
67
- expect(response1.status).toBe(200);
68
- expect(await response1.text()).toBe('I LOVE NETLIFY');
69
- const response2 = await fetch(`http://0.0.0.0:${port}/greet`, {
70
- headers: {
71
- 'x-nf-edge-functions': 'greet',
72
- 'x-ef-passthrough': 'passthrough',
73
- 'X-NF-Request-ID': uuidv4(),
74
- },
75
- });
76
- expect(response2.status).toBe(200);
77
- expect(await response2.text()).toBe('HELLO!');
78
- const response3 = await fetch(`http://0.0.0.0:${port}/global-netlify`, {
79
- headers: {
80
- 'x-nf-edge-functions': 'global_netlify',
81
- 'x-ef-passthrough': 'passthrough',
82
- 'X-NF-Request-ID': uuidv4(),
83
- },
84
- });
85
- expect(await response3.json()).toEqual({
86
- global: 'i love netlify',
87
- local: 'i love netlify',
88
- });
89
- const idBarrelFile = await readFile(join(servePath, 'bundled-id.js'), 'utf-8');
90
- expect(idBarrelFile).toContain(`/// <reference types="${join('..', '..', 'node_modules', 'id', 'types.d.ts')}" />`);
91
- const identidadeBarrelFile = await readFile(join(servePath, 'bundled-@pt-committee_identidade.js'), 'utf-8');
92
- expect(identidadeBarrelFile).toContain(`/// <reference types="${join('..', '..', 'node_modules', '@types', 'pt-committee__identidade', 'index.d.ts')}" />`);
93
- });
94
- test('Serves edge functions in a monorepo setup', async () => {
95
- const tmpFile = await tmp.file();
96
- const stderr = createWriteStream(tmpFile.path);
97
- const rootPath = join(fixturesDir, 'monorepo_npm_module');
98
- const basePath = join(rootPath, 'packages', 'frontend');
99
- const paths = {
100
- user: join(basePath, 'functions'),
101
- };
102
- const port = await getPort();
103
- const importMapPaths = [join(basePath, 'import_map.json')];
104
- const servePath = join(basePath, '.netlify', 'edge-functions-serve');
105
- const server = await serve({
106
- basePath,
107
- bootstrapURL: await getBootstrapURL(),
108
- port,
109
- rootPath,
110
- servePath,
111
- stderr,
112
- });
113
- const functions = [
114
- {
115
- name: 'func1',
116
- path: join(paths.user, 'func1.ts'),
117
- },
118
- ];
119
- const options = {
120
- getFunctionsConfig: true,
121
- importMapPaths,
122
- };
123
- const { features, functionsConfig, graph, success } = await server(functions, {
124
- very_secret_secret: 'i love netlify',
125
- }, options);
126
- expect(features).toEqual({ npmModules: true });
127
- expect(success).toBe(true);
128
- expect(functionsConfig).toEqual([{ path: '/func1' }]);
129
- for (const key in functions) {
130
- const graphEntry = graph?.modules.some(({ kind, mediaType, local }) => kind === 'esm' && mediaType === 'TypeScript' && local === functions[key].path);
131
- expect(graphEntry).toBe(true);
132
- }
133
- const response1 = await fetch(`http://0.0.0.0:${port}/func1`, {
134
- headers: {
135
- 'x-nf-edge-functions': 'func1',
136
- 'x-ef-passthrough': 'passthrough',
137
- 'X-NF-Request-ID': uuidv4(),
138
- },
139
- });
140
- expect(response1.status).toBe(200);
141
- expect(await response1.text()).toBe(`<parent-1><child-1>JavaScript</child-1></parent-1>, <parent-2><child-2><grandchild-1>APIs<cwd>${process.cwd()}</cwd></grandchild-1></child-2></parent-2>, <parent-3><child-2><grandchild-1>Markup<cwd>${process.cwd()}</cwd></grandchild-1></child-2></parent-3>`);
142
- expect(await readFile(tmpFile.path, 'utf8')).toContain('[func1] Something is on fire');
143
- await tmpFile.cleanup();
144
- });
@@ -1,53 +0,0 @@
1
- import { rm, writeFile } from 'fs/promises';
2
- import { join } from 'path';
3
- import { pathToFileURL } from 'url';
4
- import { execa } from 'execa';
5
- import tmp from 'tmp-promise';
6
- import { test, expect } from 'vitest';
7
- import { getLocalEntryPoint } from './formats/javascript.js';
8
- test('`getLocalEntryPoint` returns a valid stage 2 file for local development', async () => {
9
- const { path: tmpDir } = await tmp.dir();
10
- // This is a fake bootstrap that we'll create just for the purpose of logging
11
- // the functions and the metadata that are sent to the `boot` function.
12
- const printer = `
13
- export const boot = async (functionsLoader) => {
14
- const functions = await functionsLoader()
15
- const metadata = { functions: {} }
16
-
17
- // Generate metadata for each function (simulating what the real bootstrap would have)
18
- for (const name in functions) {
19
- metadata.functions[name] = { url: new URL('./' + name + '.mjs', import.meta.url).href }
20
- }
21
-
22
- const responses = {}
23
-
24
- for (const name in functions) {
25
- responses[name] = await functions[name]()
26
- }
27
-
28
- console.log(JSON.stringify({ responses, metadata }))
29
- }
30
- `;
31
- const printerPath = join(tmpDir, 'printer.mjs');
32
- const bootstrapURL = pathToFileURL(printerPath).toString();
33
- await writeFile(printerPath, printer);
34
- const functions = [
35
- { name: 'func1', path: join(tmpDir, 'func1.mjs'), response: 'Hello from function 1' },
36
- { name: 'func2', path: join(tmpDir, 'func2.mjs'), response: 'Hello from function 2' },
37
- ];
38
- for (const func of functions) {
39
- const contents = `export default () => ${JSON.stringify(func.response)}`;
40
- await writeFile(func.path, contents);
41
- }
42
- const stage2 = getLocalEntryPoint(functions.map(({ name, path }) => ({ name, path })), { bootstrapURL });
43
- const stage2Path = join(tmpDir, 'stage2.mjs');
44
- await writeFile(stage2Path, stage2);
45
- const { stdout, stderr } = await execa('deno', ['run', '--allow-all', stage2Path]);
46
- expect(stderr).toBe('');
47
- const { metadata, responses } = JSON.parse(stdout);
48
- for (const func of functions) {
49
- expect(responses[func.name]).toBe(func.response);
50
- expect(metadata.functions[func.name].url).toBe(pathToFileURL(func.path).toString());
51
- }
52
- await rm(tmpDir, { force: true, recursive: true });
53
- });
@@ -1,63 +0,0 @@
1
- import { readFile, rm, writeFile } from 'fs/promises';
2
- import { join } from 'path';
3
- import nock from 'nock';
4
- import tmp from 'tmp-promise';
5
- import { test, expect, vi } from 'vitest';
6
- import { testLogger } from '../test/util.js';
7
- import { DenoBridge } from './bridge.js';
8
- import { ensureLatestTypes } from './types.js';
9
- test('`ensureLatestTypes` updates the Deno CLI cache if the local version of types is outdated', async () => {
10
- const mockURL = 'https://edge.netlify';
11
- const mockVersion = '123456789';
12
- const latestVersionMock = nock(mockURL).get('/version.txt').reply(200, mockVersion);
13
- const tmpDir = await tmp.dir();
14
- const deno = new DenoBridge({
15
- cacheDirectory: tmpDir.path,
16
- logger: testLogger,
17
- });
18
- // @ts-expect-error return value not used
19
- const mock = vi.spyOn(deno, 'run').mockResolvedValue({});
20
- await ensureLatestTypes(deno, testLogger, mockURL);
21
- const versionFile = await readFile(join(tmpDir.path, 'types-version.txt'), 'utf8');
22
- expect(latestVersionMock.isDone()).toBe(true);
23
- expect(mock).toHaveBeenCalledTimes(1);
24
- expect(mock).toHaveBeenCalledWith(['cache', '-r', mockURL]);
25
- expect(versionFile).toBe(mockVersion);
26
- mock.mockRestore();
27
- await rm(tmpDir.path, { force: true, recursive: true, maxRetries: 10 });
28
- });
29
- test('`ensureLatestTypes` does not update the Deno CLI cache if the local version of types is up-to-date', async () => {
30
- const mockURL = 'https://edge.netlify';
31
- const mockVersion = '987654321';
32
- const tmpDir = await tmp.dir();
33
- const versionFilePath = join(tmpDir.path, 'types-version.txt');
34
- await writeFile(versionFilePath, mockVersion);
35
- const latestVersionMock = nock(mockURL).get('/version.txt').reply(200, mockVersion);
36
- const deno = new DenoBridge({
37
- cacheDirectory: tmpDir.path,
38
- logger: testLogger,
39
- });
40
- // @ts-expect-error return value not used
41
- const mock = vi.spyOn(deno, 'run').mockResolvedValue({});
42
- await ensureLatestTypes(deno, testLogger, mockURL);
43
- expect(latestVersionMock.isDone()).toBe(true);
44
- expect(mock).not.toHaveBeenCalled();
45
- mock.mockRestore();
46
- await rm(tmpDir.path, { force: true, recursive: true, maxRetries: 10 });
47
- });
48
- test('`ensureLatestTypes` does not throw if the types URL is not available', async () => {
49
- const mockURL = 'https://edge.netlify';
50
- const latestVersionMock = nock(mockURL).get('/version.txt').reply(500);
51
- const tmpDir = await tmp.dir();
52
- const deno = new DenoBridge({
53
- cacheDirectory: tmpDir.path,
54
- logger: testLogger,
55
- });
56
- // @ts-expect-error return value not used
57
- const mock = vi.spyOn(deno, 'run').mockResolvedValue({});
58
- await ensureLatestTypes(deno, testLogger, mockURL);
59
- expect(latestVersionMock.isDone()).toBe(true);
60
- expect(mock).not.toHaveBeenCalled();
61
- mock.mockRestore();
62
- await rm(tmpDir.path, { force: true, recursive: true, maxRetries: 10 });
63
- });
@@ -1,237 +0,0 @@
1
- import chalk from 'chalk';
2
- import { test, expect, describe } from 'vitest';
3
- import { validateManifest, ManifestValidationError } from './index.js';
4
- // We need to disable all color outputs for the tests as they are different on different platforms, CI, etc.
5
- // This only works if this is the same instance of chalk that better-ajv-errors uses
6
- chalk.level = 0;
7
- // Factory so we have a new object per test
8
- const getBaseManifest = () => ({
9
- bundles: [
10
- {
11
- asset: 'f35baff44129a8f6be7db68590b2efd86ed4ba29000e2edbcaddc5d620d7d043.js',
12
- format: 'js',
13
- },
14
- ],
15
- routes: [
16
- {
17
- name: 'name',
18
- function: 'hello',
19
- pattern: '^/hello/?$',
20
- generator: '@netlify/fake-plugin@1.0.0',
21
- },
22
- ],
23
- post_cache_routes: [
24
- {
25
- name: 'name',
26
- function: 'hello',
27
- pattern: '^/hello/?$',
28
- generator: '@netlify/fake-plugin@1.0.0',
29
- },
30
- ],
31
- layers: [
32
- {
33
- flag: 'flag',
34
- name: 'name',
35
- local: 'local',
36
- },
37
- ],
38
- bundler_version: '1.6.0',
39
- });
40
- test('should not throw on valid manifest', () => {
41
- expect(() => validateManifest(getBaseManifest())).not.toThrowError();
42
- });
43
- test('should throw ManifestValidationError with correct message', () => {
44
- expect(() => validateManifest('manifest')).toThrowError(ManifestValidationError);
45
- expect(() => validateManifest('manifest')).toThrowError(/^Validation of Edge Functions manifest failed/);
46
- });
47
- test('should throw ManifestValidationError with customErrorInfo', () => {
48
- try {
49
- validateManifest('manifest');
50
- }
51
- catch (error) {
52
- expect(error).toBeInstanceOf(ManifestValidationError);
53
- const { customErrorInfo } = error;
54
- expect(customErrorInfo).toBeDefined();
55
- expect(customErrorInfo.type).toBe('functionsBundling');
56
- return;
57
- }
58
- expect.fail('should have thrown');
59
- });
60
- test('should throw on additional property on root level', () => {
61
- const manifest = getBaseManifest();
62
- manifest.foo = 'bar';
63
- expect(() => validateManifest(manifest)).toThrowErrorMatchingSnapshot();
64
- });
65
- test('should show multiple errors', () => {
66
- const manifest = getBaseManifest();
67
- manifest.foo = 'bar';
68
- manifest.baz = 'bar';
69
- expect(() => validateManifest(manifest)).toThrowErrorMatchingSnapshot();
70
- });
71
- describe('bundle', () => {
72
- test('should throw on additional property in bundle', () => {
73
- const manifest = getBaseManifest();
74
- manifest.bundles[0].foo = 'bar';
75
- expect(() => validateManifest(manifest)).toThrowErrorMatchingSnapshot();
76
- });
77
- test('should throw on missing asset', () => {
78
- const manifest = getBaseManifest();
79
- delete manifest.bundles[0].asset;
80
- expect(() => validateManifest(manifest)).toThrowErrorMatchingSnapshot();
81
- });
82
- test('should throw on missing format', () => {
83
- const manifest = getBaseManifest();
84
- delete manifest.bundles[0].format;
85
- expect(() => validateManifest(manifest)).toThrowErrorMatchingSnapshot();
86
- });
87
- test('should throw on invalid format', () => {
88
- const manifest = getBaseManifest();
89
- manifest.bundles[0].format = 'foo';
90
- expect(() => validateManifest(manifest)).toThrowErrorMatchingSnapshot();
91
- });
92
- });
93
- describe('route', () => {
94
- test('should throw on additional property', () => {
95
- const manifest = getBaseManifest();
96
- manifest.routes[0].foo = 'bar';
97
- expect(() => validateManifest(manifest)).toThrowErrorMatchingSnapshot();
98
- });
99
- test('should throw on invalid pattern', () => {
100
- const manifest = getBaseManifest();
101
- manifest.routes[0].pattern = '/^/hello/?$/';
102
- expect(() => validateManifest(manifest)).toThrowErrorMatchingSnapshot();
103
- });
104
- test('should throw on missing function', () => {
105
- const manifest = getBaseManifest();
106
- delete manifest.routes[0].function;
107
- expect(() => validateManifest(manifest)).toThrowErrorMatchingSnapshot();
108
- });
109
- test('should throw on missing pattern', () => {
110
- const manifest = getBaseManifest();
111
- delete manifest.routes[0].pattern;
112
- expect(() => validateManifest(manifest)).toThrowErrorMatchingSnapshot();
113
- });
114
- });
115
- // No tests for post_cache_routes as schema shared with routes
116
- describe('layers', () => {
117
- test('should throw on additional property', () => {
118
- const manifest = getBaseManifest();
119
- manifest.layers[0].foo = 'bar';
120
- expect(() => validateManifest(manifest)).toThrowErrorMatchingSnapshot();
121
- });
122
- test('should throw on missing name', () => {
123
- const manifest = getBaseManifest();
124
- delete manifest.layers[0].name;
125
- expect(() => validateManifest(manifest)).toThrowErrorMatchingSnapshot();
126
- });
127
- test('should throw on missing flag', () => {
128
- const manifest = getBaseManifest();
129
- delete manifest.layers[0].flag;
130
- expect(() => validateManifest(manifest)).toThrowErrorMatchingSnapshot();
131
- });
132
- });
133
- describe('import map URL', () => {
134
- test('should accept string value', () => {
135
- const manifest = getBaseManifest();
136
- manifest.import_map = 'file:///root/.netlify/edge-functions-dist/import_map.json';
137
- expect(() => validateManifest(manifest)).not.toThrowError();
138
- });
139
- test('should throw on wrong type', () => {
140
- const manifest = getBaseManifest();
141
- manifest.import_map = ['file:///root/.netlify/edge-functions-dist/import_map.json'];
142
- expect(() => validateManifest(manifest)).toThrowErrorMatchingSnapshot();
143
- });
144
- });
145
- describe('route headers', () => {
146
- test('should accept valid headers with exists matcher', () => {
147
- const manifest = getBaseManifest();
148
- manifest.routes[0].headers = {
149
- 'x-custom-header': {
150
- matcher: 'exists',
151
- },
152
- };
153
- expect(() => validateManifest(manifest)).not.toThrowError();
154
- });
155
- test('should accept valid headers with missing matcher', () => {
156
- const manifest = getBaseManifest();
157
- manifest.routes[0].headers = {
158
- 'x-custom-header': {
159
- matcher: 'missing',
160
- },
161
- };
162
- expect(() => validateManifest(manifest)).not.toThrowError();
163
- });
164
- test('should accept valid headers with regex matcher and pattern', () => {
165
- const manifest = getBaseManifest();
166
- manifest.routes[0].headers = {
167
- 'x-custom-header': {
168
- matcher: 'regex',
169
- pattern: 'Bearer .+',
170
- },
171
- };
172
- expect(() => validateManifest(manifest)).not.toThrowError();
173
- });
174
- test('should throw on missing matcher property', () => {
175
- const manifest = getBaseManifest();
176
- manifest.routes[0].headers = {
177
- 'x-custom-header': {
178
- pattern: '^Bearer .+$',
179
- },
180
- };
181
- expect(() => validateManifest(manifest)).toThrowErrorMatchingSnapshot();
182
- });
183
- test('should throw on invalid matcher value', () => {
184
- const manifest = getBaseManifest();
185
- manifest.routes[0].headers = {
186
- 'x-custom-header': {
187
- matcher: 'invalid',
188
- },
189
- };
190
- expect(() => validateManifest(manifest)).toThrowErrorMatchingSnapshot();
191
- });
192
- test('should throw when matcher is regex but pattern is missing', () => {
193
- const manifest = getBaseManifest();
194
- manifest.routes[0].headers = {
195
- 'x-custom-header': {
196
- matcher: 'regex',
197
- },
198
- };
199
- expect(() => validateManifest(manifest)).toThrowErrorMatchingSnapshot();
200
- });
201
- test('should throw on invalid pattern format', () => {
202
- const manifest = getBaseManifest();
203
- manifest.routes[0].headers = {
204
- 'x-custom-header': {
205
- matcher: 'regex',
206
- pattern: /^Bearer .+/,
207
- },
208
- };
209
- expect(() => validateManifest(manifest)).toThrowErrorMatchingSnapshot();
210
- });
211
- test('should throw on additional property in headers', () => {
212
- const manifest = getBaseManifest();
213
- manifest.routes[0].headers = {
214
- 'x-custom-header': {
215
- matcher: 'exists',
216
- foo: 'bar',
217
- },
218
- };
219
- expect(() => validateManifest(manifest)).toThrowErrorMatchingSnapshot();
220
- });
221
- test('should accept multiple headers with different matchers', () => {
222
- const manifest = getBaseManifest();
223
- manifest.routes[0].headers = {
224
- 'x-exists-header': {
225
- matcher: 'exists',
226
- },
227
- 'x-missing-header': {
228
- matcher: 'missing',
229
- },
230
- authorization: {
231
- matcher: 'regex',
232
- pattern: '^Bearer [a-zA-Z0-9]+$',
233
- },
234
- };
235
- expect(() => validateManifest(manifest)).not.toThrowError();
236
- });
237
- });