@netlify/edge-bundler 9.5.0 → 10.0.0
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.
- package/deno/lib/stage2.test.ts +58 -0
- package/dist/node/bootstrap.test.d.ts +1 -0
- package/dist/node/bootstrap.test.js +26 -0
- package/dist/node/bundler.d.ts +2 -1
- package/dist/node/bundler.js +4 -2
- package/dist/node/bundler.test.js +28 -0
- package/dist/node/deno_config.d.ts +5 -0
- package/dist/node/deno_config.js +40 -0
- package/dist/node/deno_config.test.d.ts +1 -0
- package/dist/node/deno_config.test.js +37 -0
- package/dist/node/index.d.ts +1 -0
- package/dist/node/manifest.d.ts +6 -2
- package/dist/node/manifest.js +17 -4
- package/dist/node/manifest.test.js +44 -16
- package/dist/node/npm_dependencies.d.ts +2 -1
- package/dist/node/npm_dependencies.js +13 -10
- package/dist/node/server/server.d.ts +2 -6
- package/dist/node/server/server.js +82 -2
- package/dist/node/server/server.test.js +50 -0
- package/dist/node/serving.test.d.ts +1 -0
- package/dist/node/serving.test.js +31 -0
- package/dist/node/utils/fs.d.ts +5 -0
- package/dist/node/utils/fs.js +12 -0
- package/package.json +2 -2
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { assertEquals, assertStringIncludes } from 'https://deno.land/std@0.177.0/testing/asserts.ts'
|
|
2
|
+
|
|
3
|
+
import { join } from 'https://deno.land/std@0.177.0/path/mod.ts'
|
|
4
|
+
import { pathToFileURL } from 'https://deno.land/std@0.177.0/node/url.ts'
|
|
5
|
+
|
|
6
|
+
import { getStage2Entry } from './stage2.ts'
|
|
7
|
+
import { virtualRoot } from './consts.ts'
|
|
8
|
+
|
|
9
|
+
Deno.test('`getStage2Entry` returns a valid stage 2 file', async () => {
|
|
10
|
+
const directory = await Deno.makeTempDir()
|
|
11
|
+
const functions = [
|
|
12
|
+
{
|
|
13
|
+
name: 'func1',
|
|
14
|
+
path: join(directory, 'func1.ts'),
|
|
15
|
+
response: 'Hello from function 1',
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
name: 'func2',
|
|
19
|
+
path: join(directory, 'func2.ts'),
|
|
20
|
+
response: 'Hello from function 2',
|
|
21
|
+
},
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
for (const func of functions) {
|
|
25
|
+
const contents = `export default async () => new Response(${JSON.stringify(func.response)})`
|
|
26
|
+
|
|
27
|
+
await Deno.writeTextFile(func.path, contents)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const baseURL = pathToFileURL(directory)
|
|
31
|
+
const stage2 = getStage2Entry(
|
|
32
|
+
directory,
|
|
33
|
+
functions.map(({ name, path }) => ({ name, path })),
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
// Ensuring that the stage 2 paths have the virtual root before we strip it.
|
|
37
|
+
assertStringIncludes(stage2, virtualRoot)
|
|
38
|
+
|
|
39
|
+
// Replacing the virtual root with the URL of the temporary directory so that
|
|
40
|
+
// we can actually import the module.
|
|
41
|
+
const normalizedStage2 = stage2.replaceAll(virtualRoot, `${baseURL.href}/`)
|
|
42
|
+
|
|
43
|
+
const stage2Path = join(directory, 'stage2.ts')
|
|
44
|
+
const stage2URL = pathToFileURL(stage2Path)
|
|
45
|
+
|
|
46
|
+
await Deno.writeTextFile(stage2Path, normalizedStage2)
|
|
47
|
+
|
|
48
|
+
const mod = await import(stage2URL.href)
|
|
49
|
+
|
|
50
|
+
await Deno.remove(directory, { recursive: true })
|
|
51
|
+
|
|
52
|
+
for (const func of functions) {
|
|
53
|
+
const result = await mod.functions[func.name]()
|
|
54
|
+
|
|
55
|
+
assertEquals(await result.text(), func.response)
|
|
56
|
+
assertEquals(mod.metadata.functions[func.name].url, pathToFileURL(func.path).toString())
|
|
57
|
+
}
|
|
58
|
+
})
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { Buffer } from 'buffer';
|
|
2
|
+
import { env } from 'process';
|
|
3
|
+
import fetch, { Headers } from 'node-fetch';
|
|
4
|
+
import { test, expect } from 'vitest';
|
|
5
|
+
import { getBootstrapURL } from './formats/javascript.js';
|
|
6
|
+
test('Imports the bootstrap layer from a valid URL', async () => {
|
|
7
|
+
const importURL = new URL(getBootstrapURL());
|
|
8
|
+
const headers = new Headers();
|
|
9
|
+
// `node-fetch` doesn't let us send credentials as part of the URL, so we
|
|
10
|
+
// have to transform them into an `Authorization` header.
|
|
11
|
+
if (importURL.username) {
|
|
12
|
+
const auth = Buffer.from(`${importURL.username}:${importURL.password}`);
|
|
13
|
+
importURL.username = '';
|
|
14
|
+
importURL.password = '';
|
|
15
|
+
headers.set('Authorization', `Basic ${auth.toString('base64')}`);
|
|
16
|
+
}
|
|
17
|
+
const canonicalURL = importURL.toString();
|
|
18
|
+
const { status } = await fetch(canonicalURL, { headers });
|
|
19
|
+
expect(status).toBe(200);
|
|
20
|
+
});
|
|
21
|
+
test('Imports the bootstrap layer from the URL present in the `NETLIFY_EDGE_BOOTSTRAP` environment variable, if present', () => {
|
|
22
|
+
const mockURL = 'https://example.com/boot.ts';
|
|
23
|
+
env.NETLIFY_EDGE_BOOTSTRAP = mockURL;
|
|
24
|
+
expect(getBootstrapURL()).toBe(mockURL);
|
|
25
|
+
env.NETLIFY_EDGE_BOOTSTRAP = undefined;
|
|
26
|
+
});
|
package/dist/node/bundler.d.ts
CHANGED
|
@@ -15,11 +15,12 @@ export interface BundleOptions {
|
|
|
15
15
|
internalSrcFolder?: string;
|
|
16
16
|
onAfterDownload?: OnAfterDownloadHook;
|
|
17
17
|
onBeforeDownload?: OnBeforeDownloadHook;
|
|
18
|
+
rootPath?: string;
|
|
18
19
|
systemLogger?: LogFunction;
|
|
19
20
|
userLogger?: LogFunction;
|
|
20
21
|
vendorDirectory?: string;
|
|
21
22
|
}
|
|
22
|
-
export declare const bundle: (sourceDirectories: string[], distDirectory: string, tomlDeclarations?: Declaration[], { basePath: inputBasePath, cacheDirectory, configPath, debug, distImportMapPath, featureFlags: inputFeatureFlags, importMapPaths, internalSrcFolder, onAfterDownload, onBeforeDownload, userLogger, systemLogger, vendorDirectory, }?: BundleOptions) => Promise<{
|
|
23
|
+
export declare const bundle: (sourceDirectories: string[], distDirectory: string, tomlDeclarations?: Declaration[], { basePath: inputBasePath, cacheDirectory, configPath, debug, distImportMapPath, featureFlags: inputFeatureFlags, importMapPaths, internalSrcFolder, onAfterDownload, onBeforeDownload, rootPath, userLogger, systemLogger, vendorDirectory, }?: BundleOptions) => Promise<{
|
|
23
24
|
functions: EdgeFunction[];
|
|
24
25
|
manifest: import("./manifest.js").Manifest;
|
|
25
26
|
}>;
|
package/dist/node/bundler.js
CHANGED
|
@@ -15,7 +15,7 @@ import { getLogger } from './logger.js';
|
|
|
15
15
|
import { writeManifest } from './manifest.js';
|
|
16
16
|
import { vendorNPMSpecifiers } from './npm_dependencies.js';
|
|
17
17
|
import { ensureLatestTypes } from './types.js';
|
|
18
|
-
export const bundle = async (sourceDirectories, distDirectory, tomlDeclarations = [], { basePath: inputBasePath, cacheDirectory, configPath, debug, distImportMapPath, featureFlags: inputFeatureFlags, importMapPaths = [], internalSrcFolder, onAfterDownload, onBeforeDownload, userLogger, systemLogger, vendorDirectory, } = {}) => {
|
|
18
|
+
export const bundle = async (sourceDirectories, distDirectory, tomlDeclarations = [], { basePath: inputBasePath, cacheDirectory, configPath, debug, distImportMapPath, featureFlags: inputFeatureFlags, importMapPaths = [], internalSrcFolder, onAfterDownload, onBeforeDownload, rootPath, userLogger, systemLogger, vendorDirectory, } = {}) => {
|
|
19
19
|
const logger = getLogger(systemLogger, userLogger, debug);
|
|
20
20
|
const featureFlags = getFlags(inputFeatureFlags);
|
|
21
21
|
const options = {
|
|
@@ -53,6 +53,7 @@ export const bundle = async (sourceDirectories, distDirectory, tomlDeclarations
|
|
|
53
53
|
functions,
|
|
54
54
|
importMap,
|
|
55
55
|
logger,
|
|
56
|
+
rootPath: rootPath !== null && rootPath !== void 0 ? rootPath : basePath,
|
|
56
57
|
vendorDirectory,
|
|
57
58
|
});
|
|
58
59
|
if (vendor) {
|
|
@@ -147,7 +148,7 @@ const createFunctionConfig = ({ internalFunctionsWithConfig, declarations }) =>
|
|
|
147
148
|
[functionName]: addGeneratorFallback(mergedConfigFields),
|
|
148
149
|
};
|
|
149
150
|
}, {});
|
|
150
|
-
const safelyVendorNPMSpecifiers = async ({ basePath, functions, importMap, logger, vendorDirectory, }) => {
|
|
151
|
+
const safelyVendorNPMSpecifiers = async ({ basePath, functions, importMap, logger, rootPath, vendorDirectory, }) => {
|
|
151
152
|
try {
|
|
152
153
|
return await vendorNPMSpecifiers({
|
|
153
154
|
basePath,
|
|
@@ -156,6 +157,7 @@ const safelyVendorNPMSpecifiers = async ({ basePath, functions, importMap, logge
|
|
|
156
157
|
importMap,
|
|
157
158
|
logger,
|
|
158
159
|
referenceTypes: false,
|
|
160
|
+
rootPath,
|
|
159
161
|
});
|
|
160
162
|
}
|
|
161
163
|
catch (error) {
|
|
@@ -380,6 +380,34 @@ test('Loads npm modules from bare specifiers', async () => {
|
|
|
380
380
|
await cleanup();
|
|
381
381
|
await rm(vendorDirectory.path, { force: true, recursive: true });
|
|
382
382
|
});
|
|
383
|
+
test('Loads npm modules in a monorepo setup', async () => {
|
|
384
|
+
const systemLogger = vi.fn();
|
|
385
|
+
const { basePath: rootPath, cleanup, distPath } = await useFixture('monorepo_npm_module');
|
|
386
|
+
const basePath = join(rootPath, 'packages', 'frontend');
|
|
387
|
+
const sourceDirectory = join(basePath, 'functions');
|
|
388
|
+
const declarations = [
|
|
389
|
+
{
|
|
390
|
+
function: 'func1',
|
|
391
|
+
path: '/func1',
|
|
392
|
+
},
|
|
393
|
+
];
|
|
394
|
+
const vendorDirectory = await tmp.dir();
|
|
395
|
+
await bundle([sourceDirectory], distPath, declarations, {
|
|
396
|
+
basePath,
|
|
397
|
+
importMapPaths: [join(basePath, 'import_map.json')],
|
|
398
|
+
rootPath,
|
|
399
|
+
vendorDirectory: vendorDirectory.path,
|
|
400
|
+
systemLogger,
|
|
401
|
+
});
|
|
402
|
+
expect(systemLogger.mock.calls.find((call) => call[0] === 'Could not track dependencies in edge function:')).toBeUndefined();
|
|
403
|
+
const manifestFile = await readFile(resolve(distPath, 'manifest.json'), 'utf8');
|
|
404
|
+
const manifest = JSON.parse(manifestFile);
|
|
405
|
+
const bundlePath = join(distPath, manifest.bundles[0].asset);
|
|
406
|
+
const { func1 } = await runESZIP(bundlePath, vendorDirectory.path);
|
|
407
|
+
expect(func1).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>`);
|
|
408
|
+
await cleanup();
|
|
409
|
+
await rm(vendorDirectory.path, { force: true, recursive: true });
|
|
410
|
+
});
|
|
383
411
|
test('Loads JSON modules', async () => {
|
|
384
412
|
const { basePath, cleanup, distPath } = await useFixture('imports_json');
|
|
385
413
|
const sourceDirectory = join(basePath, 'functions');
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { promises as fs } from 'fs';
|
|
2
|
+
import { join, resolve } from 'path';
|
|
3
|
+
import { parse as parseJSONC } from 'jsonc-parser';
|
|
4
|
+
import { isNodeError } from './utils/error.js';
|
|
5
|
+
const filenames = ['deno.json', 'deno.jsonc'];
|
|
6
|
+
export const getConfig = async (basePath) => {
|
|
7
|
+
if (basePath === undefined) {
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
for (const filename of filenames) {
|
|
11
|
+
const candidatePath = join(basePath, filename);
|
|
12
|
+
const config = await getConfigFromFile(candidatePath);
|
|
13
|
+
if (config !== undefined) {
|
|
14
|
+
return normalizeConfig(config, basePath);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
const getConfigFromFile = async (filePath) => {
|
|
19
|
+
try {
|
|
20
|
+
const data = await fs.readFile(filePath, 'utf8');
|
|
21
|
+
const config = parseJSONC(data);
|
|
22
|
+
return config;
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
if (isNodeError(error) && error.code === 'ENOENT') {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
return {};
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
const normalizeConfig = (rawConfig, basePath) => {
|
|
32
|
+
const config = {};
|
|
33
|
+
if (rawConfig.importMap) {
|
|
34
|
+
if (typeof rawConfig.importMap !== 'string') {
|
|
35
|
+
throw new TypeError(`'importMap' property in Deno config must be a string`);
|
|
36
|
+
}
|
|
37
|
+
config.importMap = resolve(basePath, rawConfig.importMap);
|
|
38
|
+
}
|
|
39
|
+
return config;
|
|
40
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { promises as fs } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import tmp from 'tmp-promise';
|
|
4
|
+
import { expect, test } from 'vitest';
|
|
5
|
+
import { getConfig } from './deno_config.js';
|
|
6
|
+
test('Returns `undefined` if no config file is found', async () => {
|
|
7
|
+
const { cleanup, path } = await tmp.dir({ unsafeCleanup: true });
|
|
8
|
+
const config = await getConfig(path);
|
|
9
|
+
expect(config).toBeUndefined();
|
|
10
|
+
await cleanup();
|
|
11
|
+
});
|
|
12
|
+
test('Returns an empty object if the config file cannot be parsed', async () => {
|
|
13
|
+
const { cleanup, path } = await tmp.dir({ unsafeCleanup: true });
|
|
14
|
+
const configPath = join(path, 'deno.json');
|
|
15
|
+
await fs.writeFile(configPath, '{');
|
|
16
|
+
const config = await getConfig(path);
|
|
17
|
+
expect(config).toEqual({});
|
|
18
|
+
await cleanup();
|
|
19
|
+
});
|
|
20
|
+
test('Resolves `importMap` into an absolute path', async () => {
|
|
21
|
+
const { cleanup, path } = await tmp.dir({ unsafeCleanup: true });
|
|
22
|
+
const configPath = join(path, 'deno.json');
|
|
23
|
+
const data = JSON.stringify({ importMap: 'import_map.json' });
|
|
24
|
+
await fs.writeFile(configPath, data);
|
|
25
|
+
const config = await getConfig(path);
|
|
26
|
+
expect(config).toEqual({ importMap: join(path, 'import_map.json') });
|
|
27
|
+
await cleanup();
|
|
28
|
+
});
|
|
29
|
+
test('Supports JSONC', async () => {
|
|
30
|
+
const { cleanup, path } = await tmp.dir({ unsafeCleanup: true });
|
|
31
|
+
const configPath = join(path, 'deno.jsonc');
|
|
32
|
+
const data = JSON.stringify({ importMap: 'import_map.json' });
|
|
33
|
+
await fs.writeFile(configPath, `// This is a comment\n${data}`);
|
|
34
|
+
const config = await getConfig(path);
|
|
35
|
+
expect(config).toEqual({ importMap: join(path, 'import_map.json') });
|
|
36
|
+
await cleanup();
|
|
37
|
+
});
|
package/dist/node/index.d.ts
CHANGED
|
@@ -5,5 +5,6 @@ export { Declaration, mergeDeclarations } from './declaration.js';
|
|
|
5
5
|
export type { EdgeFunction } from './edge_function.js';
|
|
6
6
|
export { findFunctions as find } from './finder.js';
|
|
7
7
|
export { generateManifest } from './manifest.js';
|
|
8
|
+
export type { EdgeFunctionConfig, Manifest } from './manifest.js';
|
|
8
9
|
export { serve } from './server/server.js';
|
|
9
10
|
export { validateManifest, ManifestValidationError } from './validation/manifest/index.js';
|
package/dist/node/manifest.d.ts
CHANGED
|
@@ -11,7 +11,7 @@ interface Route {
|
|
|
11
11
|
path?: string;
|
|
12
12
|
methods?: string[];
|
|
13
13
|
}
|
|
14
|
-
interface EdgeFunctionConfig {
|
|
14
|
+
export interface EdgeFunctionConfig {
|
|
15
15
|
excluded_patterns: string[];
|
|
16
16
|
on_error?: string;
|
|
17
17
|
generator?: string;
|
|
@@ -42,7 +42,11 @@ interface GenerateManifestOptions {
|
|
|
42
42
|
layers?: Layer[];
|
|
43
43
|
userFunctionConfig?: Record<string, FunctionConfig>;
|
|
44
44
|
}
|
|
45
|
-
declare const generateManifest: ({ bundles, declarations, functions, userFunctionConfig, internalFunctionConfig, importMap, layers, }: GenerateManifestOptions) =>
|
|
45
|
+
declare const generateManifest: ({ bundles, declarations, functions, userFunctionConfig, internalFunctionConfig, importMap, layers, }: GenerateManifestOptions) => {
|
|
46
|
+
declarationsWithoutFunction: string[];
|
|
47
|
+
manifest: Manifest;
|
|
48
|
+
unroutedFunctions: string[];
|
|
49
|
+
};
|
|
46
50
|
interface WriteManifestOptions extends GenerateManifestOptions {
|
|
47
51
|
distDirectory: string;
|
|
48
52
|
}
|
package/dist/node/manifest.js
CHANGED
|
@@ -29,7 +29,7 @@ const sanitizeEdgeFunctionConfig = (config) => {
|
|
|
29
29
|
const addExcludedPatterns = (name, manifestFunctionConfig, excludedPath) => {
|
|
30
30
|
if (excludedPath) {
|
|
31
31
|
const paths = Array.isArray(excludedPath) ? excludedPath : [excludedPath];
|
|
32
|
-
const excludedPatterns = paths.map(
|
|
32
|
+
const excludedPatterns = paths.map(pathToRegularExpression).filter(nonNullable).map(serializePattern);
|
|
33
33
|
manifestFunctionConfig[name].excluded_patterns.push(...excludedPatterns);
|
|
34
34
|
}
|
|
35
35
|
};
|
|
@@ -50,6 +50,8 @@ const generateManifest = ({ bundles = [], declarations = [], functions, userFunc
|
|
|
50
50
|
const preCacheRoutes = [];
|
|
51
51
|
const postCacheRoutes = [];
|
|
52
52
|
const manifestFunctionConfig = Object.fromEntries(functions.map(({ name }) => [name, { excluded_patterns: [] }]));
|
|
53
|
+
const routedFunctions = new Set();
|
|
54
|
+
const declarationsWithoutFunction = new Set();
|
|
53
55
|
for (const [name, { excludedPath, onError }] of Object.entries(userFunctionConfig)) {
|
|
54
56
|
// If the config block is for a function that is not defined, discard it.
|
|
55
57
|
if (manifestFunctionConfig[name] === undefined) {
|
|
@@ -69,9 +71,16 @@ const generateManifest = ({ bundles = [], declarations = [], functions, userFunc
|
|
|
69
71
|
declarations.forEach((declaration) => {
|
|
70
72
|
const func = functions.find(({ name }) => declaration.function === name);
|
|
71
73
|
if (func === undefined) {
|
|
74
|
+
declarationsWithoutFunction.add(declaration.function);
|
|
72
75
|
return;
|
|
73
76
|
}
|
|
74
77
|
const pattern = getRegularExpression(declaration);
|
|
78
|
+
// If there is no `pattern`, the declaration will never be triggered, so we
|
|
79
|
+
// can discard it.
|
|
80
|
+
if (!pattern) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
routedFunctions.add(declaration.function);
|
|
75
84
|
const excludedPattern = getExcludedRegularExpressions(declaration);
|
|
76
85
|
const route = {
|
|
77
86
|
function: func.name,
|
|
@@ -104,9 +113,13 @@ const generateManifest = ({ bundles = [], declarations = [], functions, userFunc
|
|
|
104
113
|
import_map: importMap,
|
|
105
114
|
function_config: sanitizeEdgeFunctionConfig(manifestFunctionConfig),
|
|
106
115
|
};
|
|
107
|
-
|
|
116
|
+
const unroutedFunctions = functions.filter(({ name }) => !routedFunctions.has(name)).map(({ name }) => name);
|
|
117
|
+
return { declarationsWithoutFunction: [...declarationsWithoutFunction], manifest, unroutedFunctions };
|
|
108
118
|
};
|
|
109
119
|
const pathToRegularExpression = (path) => {
|
|
120
|
+
if (!path) {
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
110
123
|
try {
|
|
111
124
|
const pattern = new ExtendedURLPattern({ pathname: path });
|
|
112
125
|
// Removing the `^` and `$` delimiters because we'll need to modify what's
|
|
@@ -149,12 +162,12 @@ const getExcludedRegularExpressions = (declaration) => {
|
|
|
149
162
|
}
|
|
150
163
|
if ('path' in declaration && declaration.excludedPath) {
|
|
151
164
|
const paths = Array.isArray(declaration.excludedPath) ? declaration.excludedPath : [declaration.excludedPath];
|
|
152
|
-
return paths.map(
|
|
165
|
+
return paths.map(pathToRegularExpression).filter(nonNullable);
|
|
153
166
|
}
|
|
154
167
|
return [];
|
|
155
168
|
};
|
|
156
169
|
const writeManifest = async ({ distDirectory, ...rest }) => {
|
|
157
|
-
const manifest = generateManifest(rest);
|
|
170
|
+
const { manifest } = generateManifest(rest);
|
|
158
171
|
const manifestPath = join(distDirectory, 'manifest.json');
|
|
159
172
|
await fs.writeFile(manifestPath, JSON.stringify(manifest));
|
|
160
173
|
return manifest;
|
|
@@ -17,7 +17,7 @@ test('Generates a manifest with different bundles', () => {
|
|
|
17
17
|
};
|
|
18
18
|
const functions = [{ name: 'func-1', path: '/path/to/func-1.ts' }];
|
|
19
19
|
const declarations = [{ function: 'func-1', path: '/f1' }];
|
|
20
|
-
const manifest = generateManifest({ bundles: [bundle1, bundle2], declarations, functions });
|
|
20
|
+
const { manifest } = generateManifest({ bundles: [bundle1, bundle2], declarations, functions });
|
|
21
21
|
const expectedBundles = [
|
|
22
22
|
{ asset: bundle1.hash + bundle1.extension, format: bundle1.format },
|
|
23
23
|
{ asset: bundle2.hash + bundle2.extension, format: bundle2.format },
|
|
@@ -35,7 +35,7 @@ test('Generates a manifest with display names', () => {
|
|
|
35
35
|
name: 'Display Name',
|
|
36
36
|
},
|
|
37
37
|
};
|
|
38
|
-
const manifest = generateManifest({
|
|
38
|
+
const { manifest } = generateManifest({
|
|
39
39
|
bundles: [],
|
|
40
40
|
declarations,
|
|
41
41
|
functions,
|
|
@@ -56,7 +56,7 @@ test('Generates a manifest with a generator field', () => {
|
|
|
56
56
|
generator: '@netlify/fake-plugin@1.0.0',
|
|
57
57
|
},
|
|
58
58
|
};
|
|
59
|
-
const manifest = generateManifest({
|
|
59
|
+
const { manifest } = generateManifest({
|
|
60
60
|
bundles: [],
|
|
61
61
|
declarations,
|
|
62
62
|
functions,
|
|
@@ -79,7 +79,7 @@ test('Generates a manifest with excluded paths and patterns', () => {
|
|
|
79
79
|
{ function: 'func-2', pattern: '^/f2(?:/(.*))/?$', excludedPattern: ['^/f2/exclude$', '^/f2/exclude-as-well$'] },
|
|
80
80
|
{ function: 'func-3', path: '/*', excludedPath: '/**/*.html' },
|
|
81
81
|
];
|
|
82
|
-
const manifest = generateManifest({
|
|
82
|
+
const { manifest } = generateManifest({
|
|
83
83
|
bundles: [],
|
|
84
84
|
declarations,
|
|
85
85
|
functions,
|
|
@@ -111,7 +111,7 @@ test('TOML-defined paths can be combined with ISC-defined excluded paths', () =>
|
|
|
111
111
|
const userFunctionConfig = {
|
|
112
112
|
'func-1': { excludedPath: '/f1/exclude' },
|
|
113
113
|
};
|
|
114
|
-
const manifest = generateManifest({
|
|
114
|
+
const { manifest } = generateManifest({
|
|
115
115
|
bundles: [],
|
|
116
116
|
declarations,
|
|
117
117
|
functions,
|
|
@@ -153,7 +153,7 @@ test('Filters out internal in-source configurations in user created functions',
|
|
|
153
153
|
generator: 'internal-generator',
|
|
154
154
|
},
|
|
155
155
|
};
|
|
156
|
-
const manifest = generateManifest({
|
|
156
|
+
const { manifest } = generateManifest({
|
|
157
157
|
bundles: [],
|
|
158
158
|
declarations,
|
|
159
159
|
functions,
|
|
@@ -186,7 +186,7 @@ test('excludedPath from ISC goes into function_config, TOML goes into routes', (
|
|
|
186
186
|
},
|
|
187
187
|
};
|
|
188
188
|
const internalFunctionConfig = {};
|
|
189
|
-
const manifest = generateManifest({
|
|
189
|
+
const { manifest } = generateManifest({
|
|
190
190
|
bundles: [],
|
|
191
191
|
declarations,
|
|
192
192
|
functions,
|
|
@@ -224,7 +224,7 @@ test('URLPattern named groups are supported', () => {
|
|
|
224
224
|
const declarations = [{ function: 'customisation', path: '/products/:productId' }];
|
|
225
225
|
const userFunctionConfig = {};
|
|
226
226
|
const internalFunctionConfig = {};
|
|
227
|
-
const manifest = generateManifest({
|
|
227
|
+
const { manifest } = generateManifest({
|
|
228
228
|
bundles: [],
|
|
229
229
|
declarations,
|
|
230
230
|
functions,
|
|
@@ -269,7 +269,7 @@ test('Includes failure modes in manifest', () => {
|
|
|
269
269
|
onError: '/custom-error',
|
|
270
270
|
},
|
|
271
271
|
};
|
|
272
|
-
const manifest = generateManifest({ bundles: [], declarations, functions, userFunctionConfig });
|
|
272
|
+
const { manifest } = generateManifest({ bundles: [], declarations, functions, userFunctionConfig });
|
|
273
273
|
expect(manifest.function_config).toEqual({
|
|
274
274
|
'func-1': { on_error: '/custom-error' },
|
|
275
275
|
});
|
|
@@ -285,7 +285,7 @@ test('Excludes functions for which there are function files but no matching conf
|
|
|
285
285
|
{ name: 'func-2', path: '/path/to/func-2.ts' },
|
|
286
286
|
];
|
|
287
287
|
const declarations = [{ function: 'func-1', path: '/f1' }];
|
|
288
|
-
const manifest = generateManifest({ bundles: [bundle1], declarations, functions });
|
|
288
|
+
const { manifest } = generateManifest({ bundles: [bundle1], declarations, functions });
|
|
289
289
|
const expectedRoutes = [{ function: 'func-1', pattern: '^/f1/?$', excluded_patterns: [], path: '/f1' }];
|
|
290
290
|
expect(manifest.routes).toEqual(expectedRoutes);
|
|
291
291
|
});
|
|
@@ -300,14 +300,14 @@ test('Excludes functions for which there are config declarations but no matching
|
|
|
300
300
|
{ function: 'func-1', path: '/f1' },
|
|
301
301
|
{ function: 'func-2', path: '/f2' },
|
|
302
302
|
];
|
|
303
|
-
const manifest = generateManifest({ bundles: [bundle1], declarations, functions });
|
|
303
|
+
const { manifest } = generateManifest({ bundles: [bundle1], declarations, functions });
|
|
304
304
|
const expectedRoutes = [{ function: 'func-2', pattern: '^/f2/?$', excluded_patterns: [], path: '/f2' }];
|
|
305
305
|
expect(manifest.routes).toEqual(expectedRoutes);
|
|
306
306
|
});
|
|
307
307
|
test('Generates a manifest without bundles', () => {
|
|
308
308
|
const functions = [{ name: 'func-1', path: '/path/to/func-1.ts' }];
|
|
309
309
|
const declarations = [{ function: 'func-1', path: '/f1' }];
|
|
310
|
-
const manifest = generateManifest({ bundles: [], declarations, functions });
|
|
310
|
+
const { manifest } = generateManifest({ bundles: [], declarations, functions });
|
|
311
311
|
const expectedRoutes = [{ function: 'func-1', pattern: '^/f1/?$', excluded_patterns: [], path: '/f1' }];
|
|
312
312
|
expect(manifest.bundles).toEqual([]);
|
|
313
313
|
expect(manifest.routes).toEqual(expectedRoutes);
|
|
@@ -334,7 +334,7 @@ test('Generates a manifest with pre and post-cache routes', () => {
|
|
|
334
334
|
{ function: 'func-2', cache: 'not_a_supported_value', path: '/f2' },
|
|
335
335
|
{ function: 'func-3', cache: 'manual', path: '/f3' },
|
|
336
336
|
];
|
|
337
|
-
const manifest = generateManifest({ bundles: [bundle1, bundle2], declarations, functions });
|
|
337
|
+
const { manifest } = generateManifest({ bundles: [bundle1, bundle2], declarations, functions });
|
|
338
338
|
const expectedBundles = [
|
|
339
339
|
{ asset: bundle1.hash + bundle1.extension, format: bundle1.format },
|
|
340
340
|
{ asset: bundle2.hash + bundle2.extension, format: bundle2.format },
|
|
@@ -370,12 +370,12 @@ test('Generates a manifest with layers', () => {
|
|
|
370
370
|
flag: 'edge_functions_onion_layer',
|
|
371
371
|
},
|
|
372
372
|
];
|
|
373
|
-
const manifest1 = generateManifest({
|
|
373
|
+
const { manifest: manifest1 } = generateManifest({
|
|
374
374
|
bundles: [],
|
|
375
375
|
declarations,
|
|
376
376
|
functions,
|
|
377
377
|
});
|
|
378
|
-
const manifest2 = generateManifest({
|
|
378
|
+
const { manifest: manifest2 } = generateManifest({
|
|
379
379
|
bundles: [],
|
|
380
380
|
declarations,
|
|
381
381
|
functions,
|
|
@@ -398,6 +398,34 @@ test('Throws an error if the regular expression contains a negative lookahead',
|
|
|
398
398
|
test('Converts named capture groups to unnamed capture groups in regular expressions', () => {
|
|
399
399
|
const functions = [{ name: 'func-1', path: '/path/to/func-1.ts' }];
|
|
400
400
|
const declarations = [{ function: 'func-1', pattern: '^/(?<name>\\w+)$' }];
|
|
401
|
-
const manifest = generateManifest({ bundles: [], declarations, functions });
|
|
401
|
+
const { manifest } = generateManifest({ bundles: [], declarations, functions });
|
|
402
402
|
expect(manifest.routes).toEqual([{ function: 'func-1', pattern: '^/(\\w+)$', excluded_patterns: [] }]);
|
|
403
403
|
});
|
|
404
|
+
test('Returns functions without a declaration and unrouted functions', () => {
|
|
405
|
+
const bundle = {
|
|
406
|
+
extension: '.ext1',
|
|
407
|
+
format: BundleFormat.ESZIP2,
|
|
408
|
+
hash: '123456',
|
|
409
|
+
};
|
|
410
|
+
const functions = [
|
|
411
|
+
{ name: 'func-1', path: '/path/to/func-1.ts' },
|
|
412
|
+
{ name: 'func-2', path: '/path/to/func-2.ts' },
|
|
413
|
+
{ name: 'func-4', path: '/path/to/func-4.ts' },
|
|
414
|
+
];
|
|
415
|
+
const declarations = [
|
|
416
|
+
{ function: 'func-1', path: '/f1' },
|
|
417
|
+
{ function: 'func-3', path: '/f3' },
|
|
418
|
+
// @ts-expect-error Error is expected due to neither `path` or `pattern`
|
|
419
|
+
// being present.
|
|
420
|
+
{ function: 'func-4', name: 'Some name' },
|
|
421
|
+
];
|
|
422
|
+
const { declarationsWithoutFunction, manifest, unroutedFunctions } = generateManifest({
|
|
423
|
+
bundles: [bundle],
|
|
424
|
+
declarations,
|
|
425
|
+
functions,
|
|
426
|
+
});
|
|
427
|
+
const expectedRoutes = [{ function: 'func-1', pattern: '^/f1/?$', excluded_patterns: [], path: '/f1' }];
|
|
428
|
+
expect(manifest.routes).toEqual(expectedRoutes);
|
|
429
|
+
expect(declarationsWithoutFunction).toEqual(['func-3']);
|
|
430
|
+
expect(unroutedFunctions).toEqual(['func-2', 'func-4']);
|
|
431
|
+
});
|
|
@@ -8,8 +8,9 @@ interface VendorNPMSpecifiersOptions {
|
|
|
8
8
|
importMap: ImportMap;
|
|
9
9
|
logger: Logger;
|
|
10
10
|
referenceTypes: boolean;
|
|
11
|
+
rootPath?: string;
|
|
11
12
|
}
|
|
12
|
-
export declare const vendorNPMSpecifiers: ({ basePath, directory, functions, importMap, referenceTypes, }: VendorNPMSpecifiersOptions) => Promise<{
|
|
13
|
+
export declare const vendorNPMSpecifiers: ({ basePath, directory, functions, importMap, referenceTypes, rootPath, }: VendorNPMSpecifiersOptions) => Promise<{
|
|
13
14
|
cleanup: () => Promise<void>;
|
|
14
15
|
directory: string;
|
|
15
16
|
importMap: {
|
|
@@ -8,6 +8,7 @@ import { build } from 'esbuild';
|
|
|
8
8
|
import { findUp } from 'find-up';
|
|
9
9
|
import getPackageName from 'get-package-name';
|
|
10
10
|
import tmp from 'tmp-promise';
|
|
11
|
+
import { pathsBetween } from './utils/fs.js';
|
|
11
12
|
const TYPESCRIPT_EXTENSIONS = new Set(['.ts', '.cts', '.mts']);
|
|
12
13
|
const slugifyPackageName = (specifier) => {
|
|
13
14
|
if (!specifier.startsWith('@'))
|
|
@@ -76,16 +77,12 @@ const banner = {
|
|
|
76
77
|
/**
|
|
77
78
|
* Parses a set of functions and returns a list of specifiers that correspond
|
|
78
79
|
* to npm modules.
|
|
79
|
-
*
|
|
80
|
-
* @param basePath Root of the project
|
|
81
|
-
* @param functions Functions to parse
|
|
82
|
-
* @param importMap Import map to apply when resolving imports
|
|
83
|
-
* @param referenceTypes Whether to detect typescript declarations and reference them in the output
|
|
84
80
|
*/
|
|
85
|
-
const getNPMSpecifiers = async (basePath, functions, importMap, referenceTypes) => {
|
|
81
|
+
const getNPMSpecifiers = async ({ basePath, functions, importMap, referenceTypes, rootPath, }) => {
|
|
86
82
|
const baseURL = pathToFileURL(basePath);
|
|
87
83
|
const { reasons } = await nodeFileTrace(functions, {
|
|
88
|
-
base:
|
|
84
|
+
base: rootPath,
|
|
85
|
+
processCwd: basePath,
|
|
89
86
|
readFile: async (filePath) => {
|
|
90
87
|
// If this is a TypeScript file, we need to compile in before we can
|
|
91
88
|
// parse it.
|
|
@@ -165,17 +162,23 @@ const getNPMSpecifiers = async (basePath, functions, importMap, referenceTypes)
|
|
|
165
162
|
npmSpecifiersWithExtraneousFiles: [...npmSpecifiersWithExtraneousFiles],
|
|
166
163
|
};
|
|
167
164
|
};
|
|
168
|
-
export const vendorNPMSpecifiers = async ({ basePath, directory, functions, importMap, referenceTypes, }) => {
|
|
165
|
+
export const vendorNPMSpecifiers = async ({ basePath, directory, functions, importMap, referenceTypes, rootPath = basePath, }) => {
|
|
169
166
|
// The directories that esbuild will use when resolving Node modules. We must
|
|
170
167
|
// set these manually because esbuild will be operating from a temporary
|
|
171
168
|
// directory that will not live inside the project root, so the normal
|
|
172
169
|
// resolution logic won't work.
|
|
173
|
-
const nodePaths =
|
|
170
|
+
const nodePaths = pathsBetween(basePath, rootPath).map((directory) => path.join(directory, 'node_modules'));
|
|
174
171
|
// We need to create some files on disk, which we don't want to write to the
|
|
175
172
|
// project directory. If a custom directory has been specified, we use it.
|
|
176
173
|
// Otherwise, create a random temporary directory.
|
|
177
174
|
const temporaryDirectory = directory ? { path: directory } : await tmp.dir();
|
|
178
|
-
const { npmSpecifiers, npmSpecifiersWithExtraneousFiles } = await getNPMSpecifiers(
|
|
175
|
+
const { npmSpecifiers, npmSpecifiersWithExtraneousFiles } = await getNPMSpecifiers({
|
|
176
|
+
basePath,
|
|
177
|
+
functions,
|
|
178
|
+
importMap: importMap.getContentsWithURLObjects(),
|
|
179
|
+
referenceTypes,
|
|
180
|
+
rootPath,
|
|
181
|
+
});
|
|
179
182
|
// If we found no specifiers, there's nothing left to do here.
|
|
180
183
|
if (Object.keys(npmSpecifiers).length === 0) {
|
|
181
184
|
return;
|
|
@@ -1,8 +1,3 @@
|
|
|
1
|
-
/// <reference types="node" />
|
|
2
|
-
/// <reference types="node" />
|
|
3
|
-
/// <reference types="node" />
|
|
4
|
-
/// <reference types="node" />
|
|
5
|
-
/// <reference types="node" />
|
|
6
1
|
import { OnAfterDownloadHook, OnBeforeDownloadHook } from '../bridge.js';
|
|
7
2
|
import { FunctionConfig } from '../config.js';
|
|
8
3
|
import type { EdgeFunction } from '../edge_function.js';
|
|
@@ -31,11 +26,12 @@ interface ServeOptions {
|
|
|
31
26
|
formatExportTypeError?: FormatFunction;
|
|
32
27
|
formatImportError?: FormatFunction;
|
|
33
28
|
port: number;
|
|
29
|
+
rootPath?: string;
|
|
34
30
|
servePath: string;
|
|
35
31
|
userLogger?: LogFunction;
|
|
36
32
|
systemLogger?: LogFunction;
|
|
37
33
|
}
|
|
38
|
-
export declare const serve: ({ basePath, bootstrapURL, certificatePath, debug, distImportMapPath, inspectSettings, featureFlags, formatExportTypeError, formatImportError, importMapPaths, onAfterDownload, onBeforeDownload, port, servePath, userLogger, systemLogger, }: ServeOptions) => Promise<(functions: EdgeFunction[], env?: NodeJS.ProcessEnv, options?: StartServerOptions) => Promise<{
|
|
34
|
+
export declare const serve: ({ basePath, bootstrapURL, certificatePath, debug, distImportMapPath, inspectSettings, featureFlags, formatExportTypeError, formatImportError, importMapPaths, onAfterDownload, onBeforeDownload, port, rootPath, servePath, userLogger, systemLogger, }: ServeOptions) => Promise<(functions: EdgeFunction[], env?: NodeJS.ProcessEnv, options?: StartServerOptions) => Promise<{
|
|
39
35
|
features: Record<string, boolean>;
|
|
40
36
|
functionsConfig: FunctionConfig[];
|
|
41
37
|
graph: any;
|
|
@@ -18,7 +18,7 @@ const cleanDirectory = async (directory, except) => {
|
|
|
18
18
|
const toBeDeleted = files.filter((file) => !except.includes(join(directory, file)));
|
|
19
19
|
await Promise.all(toBeDeleted.map((file) => unlink(join(directory, file))));
|
|
20
20
|
};
|
|
21
|
-
const prepareServer = ({ basePath, bootstrapURL, deno, distDirectory, distImportMapPath, flags: denoFlags, formatExportTypeError, formatImportError, importMap: baseImportMap, logger, port, }) => {
|
|
21
|
+
const prepareServer = ({ basePath, bootstrapURL, deno, distDirectory, distImportMapPath, flags: denoFlags, formatExportTypeError, formatImportError, importMap: baseImportMap, logger, port, rootPath, }) => {
|
|
22
22
|
const processRef = {};
|
|
23
23
|
const startServer = async (functions, env = {}, options = {}) => {
|
|
24
24
|
if ((processRef === null || processRef === void 0 ? void 0 : processRef.ps) !== undefined) {
|
|
@@ -45,6 +45,7 @@ const prepareServer = ({ basePath, bootstrapURL, deno, distDirectory, distImport
|
|
|
45
45
|
importMap,
|
|
46
46
|
logger,
|
|
47
47
|
referenceTypes: true,
|
|
48
|
+
rootPath,
|
|
48
49
|
});
|
|
49
50
|
if (vendor) {
|
|
50
51
|
features.npmModules = true;
|
|
@@ -92,7 +93,85 @@ const prepareServer = ({ basePath, bootstrapURL, deno, distDirectory, distImport
|
|
|
92
93
|
};
|
|
93
94
|
return startServer;
|
|
94
95
|
};
|
|
95
|
-
export const serve = async ({
|
|
96
|
+
export const serve = async ({
|
|
97
|
+
/**
|
|
98
|
+
* Path that is common to all functions. Works as the root directory in the
|
|
99
|
+
* generated bundle.
|
|
100
|
+
*/
|
|
101
|
+
basePath,
|
|
102
|
+
/**
|
|
103
|
+
* URL of the bootstrap layer to use.
|
|
104
|
+
*/
|
|
105
|
+
bootstrapURL,
|
|
106
|
+
/**
|
|
107
|
+
* Path to an SSL certificate to run the Deno server with.
|
|
108
|
+
*/
|
|
109
|
+
certificatePath,
|
|
110
|
+
/**
|
|
111
|
+
* Whether to print verbose information about the server process.
|
|
112
|
+
*/
|
|
113
|
+
debug,
|
|
114
|
+
/**
|
|
115
|
+
* Path of an import map file to be generated using the built-in specifiers
|
|
116
|
+
* and any npm modules found during the bundling process.
|
|
117
|
+
*/
|
|
118
|
+
distImportMapPath,
|
|
119
|
+
/**
|
|
120
|
+
* Debug settings to use with Deno's `--inspect` and `--inspect-brk` flags.
|
|
121
|
+
*/
|
|
122
|
+
inspectSettings,
|
|
123
|
+
/**
|
|
124
|
+
* Map of feature flags.
|
|
125
|
+
*/
|
|
126
|
+
featureFlags,
|
|
127
|
+
/**
|
|
128
|
+
* Callback function to be triggered whenever a function has a default export
|
|
129
|
+
* with the wrong type.
|
|
130
|
+
*/
|
|
131
|
+
formatExportTypeError,
|
|
132
|
+
/**
|
|
133
|
+
* Callback function to be triggered whenever an error occurs while importing
|
|
134
|
+
* a function.
|
|
135
|
+
*/
|
|
136
|
+
formatImportError,
|
|
137
|
+
/**
|
|
138
|
+
* Paths to any additional import map files.
|
|
139
|
+
*/
|
|
140
|
+
importMapPaths = [],
|
|
141
|
+
/**
|
|
142
|
+
* Callback function to be triggered after the Deno CLI has been downloaded.
|
|
143
|
+
*/
|
|
144
|
+
onAfterDownload,
|
|
145
|
+
/**
|
|
146
|
+
* Callback function to be triggered before we attempt to download the Deno
|
|
147
|
+
* CLI.
|
|
148
|
+
*/
|
|
149
|
+
onBeforeDownload,
|
|
150
|
+
/**
|
|
151
|
+
* Port where the server should listen on.
|
|
152
|
+
*/
|
|
153
|
+
port,
|
|
154
|
+
/**
|
|
155
|
+
* Root path of the project. Defines a boundary outside of which files or npm
|
|
156
|
+
* modules cannot be included from. This is usually the same as `basePath`,
|
|
157
|
+
* with monorepos being the main exception, where `basePath` maps to the
|
|
158
|
+
* package path and `rootPath` is the repository root.
|
|
159
|
+
*/
|
|
160
|
+
rootPath,
|
|
161
|
+
/**
|
|
162
|
+
* Path to write ephemeral files that need to be generated for the server to
|
|
163
|
+
* operate.
|
|
164
|
+
*/
|
|
165
|
+
servePath,
|
|
166
|
+
/**
|
|
167
|
+
* Custom logging function to be used for user-facing messages. Defaults to
|
|
168
|
+
* `console.log`.
|
|
169
|
+
*/
|
|
170
|
+
userLogger,
|
|
171
|
+
/**
|
|
172
|
+
* Custom logging function to be used for system-level messages.
|
|
173
|
+
*/
|
|
174
|
+
systemLogger, }) => {
|
|
96
175
|
const logger = getLogger(systemLogger, userLogger, debug);
|
|
97
176
|
const deno = new DenoBridge({
|
|
98
177
|
debug,
|
|
@@ -137,6 +216,7 @@ export const serve = async ({ basePath, bootstrapURL, certificatePath, debug, di
|
|
|
137
216
|
importMap,
|
|
138
217
|
logger,
|
|
139
218
|
port,
|
|
219
|
+
rootPath,
|
|
140
220
|
});
|
|
141
221
|
return server;
|
|
142
222
|
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { readFile } from 'fs/promises';
|
|
2
2
|
import { join } from 'path';
|
|
3
|
+
import process from 'process';
|
|
3
4
|
import getPort from 'get-port';
|
|
4
5
|
import fetch from 'node-fetch';
|
|
5
6
|
import { v4 as uuidv4 } from 'uuid';
|
|
@@ -86,3 +87,52 @@ test('Starts a server and serves requests for edge functions', async () => {
|
|
|
86
87
|
const identidadeBarrelFile = await readFile(join(servePath, 'bundled-pt-committee__identidade.js'), 'utf-8');
|
|
87
88
|
expect(identidadeBarrelFile).toContain(`/// <reference types="${join('..', '..', 'node_modules', '@types', 'pt-committee__identidade', 'index.d.ts')}" />`);
|
|
88
89
|
});
|
|
90
|
+
test('Serves edge functions in a monorepo setup', async () => {
|
|
91
|
+
const rootPath = join(fixturesDir, 'monorepo_npm_module');
|
|
92
|
+
const basePath = join(rootPath, 'packages', 'frontend');
|
|
93
|
+
const paths = {
|
|
94
|
+
user: join(basePath, 'functions'),
|
|
95
|
+
};
|
|
96
|
+
const port = await getPort();
|
|
97
|
+
const importMapPaths = [join(basePath, 'import_map.json')];
|
|
98
|
+
const servePath = join(basePath, '.netlify', 'edge-functions-serve');
|
|
99
|
+
const server = await serve({
|
|
100
|
+
basePath,
|
|
101
|
+
bootstrapURL: 'https://edge.netlify.com/bootstrap/index-combined.ts',
|
|
102
|
+
importMapPaths,
|
|
103
|
+
port,
|
|
104
|
+
rootPath,
|
|
105
|
+
servePath,
|
|
106
|
+
});
|
|
107
|
+
const functions = [
|
|
108
|
+
{
|
|
109
|
+
name: 'func1',
|
|
110
|
+
path: join(paths.user, 'func1.ts'),
|
|
111
|
+
},
|
|
112
|
+
];
|
|
113
|
+
const options = {
|
|
114
|
+
getFunctionsConfig: true,
|
|
115
|
+
};
|
|
116
|
+
const { features, functionsConfig, graph, success, npmSpecifiersWithExtraneousFiles } = await server(functions, {
|
|
117
|
+
very_secret_secret: 'i love netlify',
|
|
118
|
+
}, options);
|
|
119
|
+
expect(features).toEqual({ npmModules: true });
|
|
120
|
+
expect(success).toBe(true);
|
|
121
|
+
expect(functionsConfig).toEqual([{ path: '/func1' }]);
|
|
122
|
+
expect(npmSpecifiersWithExtraneousFiles).toEqual(['child-1']);
|
|
123
|
+
for (const key in functions) {
|
|
124
|
+
const graphEntry = graph === null || graph === void 0 ? void 0 : graph.modules.some(
|
|
125
|
+
// @ts-expect-error TODO: Module graph is currently not typed
|
|
126
|
+
({ kind, mediaType, local }) => kind === 'esm' && mediaType === 'TypeScript' && local === functions[key].path);
|
|
127
|
+
expect(graphEntry).toBe(true);
|
|
128
|
+
}
|
|
129
|
+
const response1 = await fetch(`http://0.0.0.0:${port}/func1`, {
|
|
130
|
+
headers: {
|
|
131
|
+
'x-nf-edge-functions': 'func1',
|
|
132
|
+
'x-ef-passthrough': 'passthrough',
|
|
133
|
+
'X-NF-Request-ID': uuidv4(),
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
expect(response1.status).toBe(200);
|
|
137
|
+
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>`);
|
|
138
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { join } from 'path';
|
|
2
|
+
import getPort from 'get-port';
|
|
3
|
+
import fetch from 'node-fetch';
|
|
4
|
+
import { test, expect } from 'vitest';
|
|
5
|
+
import { fixturesDir } from '../test/util.js';
|
|
6
|
+
import { serve } from './index.js';
|
|
7
|
+
test('bundler serving functionality', async () => {
|
|
8
|
+
const port = await getPort();
|
|
9
|
+
const server = await serve({
|
|
10
|
+
port,
|
|
11
|
+
});
|
|
12
|
+
const { success } = await server([
|
|
13
|
+
{
|
|
14
|
+
name: 'echo_env',
|
|
15
|
+
path: join(fixturesDir, 'serve_test', 'echo_env.ts'),
|
|
16
|
+
},
|
|
17
|
+
], {
|
|
18
|
+
very_secret_secret: 'i love netlify',
|
|
19
|
+
});
|
|
20
|
+
expect(success).toBe(true);
|
|
21
|
+
const response = await fetch(`http://0.0.0.0:${port}/foo`, {
|
|
22
|
+
headers: {
|
|
23
|
+
'x-deno-functions': 'echo_env',
|
|
24
|
+
'x-deno-pass': 'passthrough',
|
|
25
|
+
'X-NF-Request-ID': 'foo',
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
expect(response.status).toBe(200);
|
|
29
|
+
const body = (await response.json());
|
|
30
|
+
expect(body.very_secret_secret).toBe('i love netlify');
|
|
31
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
/**
|
|
3
|
+
* Returns all the directories obtained by traversing `inner` and its parents
|
|
4
|
+
* all the way to `outer`, inclusive.
|
|
5
|
+
*/
|
|
6
|
+
export const pathsBetween = (inner, outer, paths = []) => {
|
|
7
|
+
const parent = path.dirname(inner);
|
|
8
|
+
if (inner === outer || inner === parent) {
|
|
9
|
+
return [...paths, outer];
|
|
10
|
+
}
|
|
11
|
+
return [inner, ...pathsBetween(parent, outer)];
|
|
12
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@netlify/edge-bundler",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "10.0.0",
|
|
4
4
|
"description": "Intelligently prepare Netlify Edge Functions for deployment",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/node/index.js",
|
|
@@ -80,7 +80,7 @@
|
|
|
80
80
|
"better-ajv-errors": "^1.2.0",
|
|
81
81
|
"common-path-prefix": "^3.0.0",
|
|
82
82
|
"env-paths": "^3.0.0",
|
|
83
|
-
"esbuild": "0.19.
|
|
83
|
+
"esbuild": "0.19.5",
|
|
84
84
|
"execa": "^6.0.0",
|
|
85
85
|
"find-up": "^6.3.0",
|
|
86
86
|
"get-package-name": "^2.2.0",
|