@netlify/edge-bundler 9.2.1 → 9.3.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/dist/node/bundler.test.js +1 -1
- package/dist/node/import_map.d.ts +1 -1
- package/dist/node/import_map.js +0 -3
- package/dist/node/import_map.test.js +25 -0
- package/dist/node/npm_dependencies.d.ts +2 -4
- package/dist/node/npm_dependencies.js +78 -92
- package/dist/node/server/server.d.ts +1 -0
- package/dist/node/server/server.js +3 -0
- package/package.json +3 -1
|
@@ -123,7 +123,7 @@ test('Prints a nice error message when user tries importing an npm module and np
|
|
|
123
123
|
}
|
|
124
124
|
catch (error) {
|
|
125
125
|
expect(error).toBeInstanceOf(BundleError);
|
|
126
|
-
expect(error.message).toEqual(`It seems like you're trying to import an npm module. This is only supported via CDNs like esm.sh. Have you tried 'import mod from "https://esm.sh/parent-
|
|
126
|
+
expect(error.message).toEqual(`It seems like you're trying to import an npm module. This is only supported via CDNs like esm.sh. Have you tried 'import mod from "https://esm.sh/parent-1"'?`);
|
|
127
127
|
}
|
|
128
128
|
finally {
|
|
129
129
|
await cleanup();
|
package/dist/node/import_map.js
CHANGED
|
@@ -5,6 +5,7 @@ import { pathToFileURL } from 'url';
|
|
|
5
5
|
import tmp from 'tmp-promise';
|
|
6
6
|
import { describe, test, expect } from 'vitest';
|
|
7
7
|
import { ImportMap } from './import_map.js';
|
|
8
|
+
import { getLogger } from './logger.js';
|
|
8
9
|
test('Handles import maps with full URLs without specifying a base URL', () => {
|
|
9
10
|
const basePath = join(cwd(), 'my-cool-site', 'import-map.json');
|
|
10
11
|
const inputFile1 = {
|
|
@@ -143,6 +144,30 @@ test('Writes import map file to disk', async () => {
|
|
|
143
144
|
expect(imports['@netlify/edge-functions']).toBe('https://edge.netlify.com/v1/index.ts');
|
|
144
145
|
expect(imports['alias:pets']).toBe(pathToFileURL(expectedPath).toString());
|
|
145
146
|
});
|
|
147
|
+
test('Respects import map when it has only scoped key', async () => {
|
|
148
|
+
const file = await tmp.file();
|
|
149
|
+
const importMap = {
|
|
150
|
+
scopes: {
|
|
151
|
+
'./foo': {
|
|
152
|
+
'alias:pets': './heart/pets/file.ts',
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
await fs.writeFile(file.path, JSON.stringify(importMap));
|
|
157
|
+
const map = new ImportMap();
|
|
158
|
+
await map.addFile(file.path, getLogger());
|
|
159
|
+
expect(map.getContents()).toEqual({
|
|
160
|
+
imports: {
|
|
161
|
+
'netlify:edge': 'https://edge.netlify.com/v1/index.ts?v=legacy',
|
|
162
|
+
'@netlify/edge-functions': 'https://edge.netlify.com/v1/index.ts',
|
|
163
|
+
},
|
|
164
|
+
scopes: {
|
|
165
|
+
[pathToFileURL(join(file.path, '../foo')).href]: {
|
|
166
|
+
'alias:pets': pathToFileURL(join(file.path, '../heart/pets/file.ts')).href,
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
});
|
|
170
|
+
});
|
|
146
171
|
test('Clones an import map', () => {
|
|
147
172
|
const basePath = join(cwd(), 'my-cool-site', 'import-map.json');
|
|
148
173
|
const inputFile1 = {
|
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
/// <reference types="node" />
|
|
2
|
-
import { ParsedImportMap } from '@import-maps/resolve';
|
|
3
|
-
import { Plugin } from 'esbuild';
|
|
4
2
|
import { ImportMap } from './import_map.js';
|
|
5
3
|
import { Logger } from './logger.js';
|
|
6
|
-
export declare const getDependencyTrackerPlugin: (specifiers: Set<string>, importMap: ParsedImportMap, baseURL: URL) => Plugin;
|
|
7
4
|
interface VendorNPMSpecifiersOptions {
|
|
8
5
|
basePath: string;
|
|
9
6
|
directory?: string;
|
|
@@ -11,12 +8,13 @@ interface VendorNPMSpecifiersOptions {
|
|
|
11
8
|
importMap: ImportMap;
|
|
12
9
|
logger: Logger;
|
|
13
10
|
}
|
|
14
|
-
export declare const vendorNPMSpecifiers: ({ basePath, directory, functions, importMap,
|
|
11
|
+
export declare const vendorNPMSpecifiers: ({ basePath, directory, functions, importMap, }: VendorNPMSpecifiersOptions) => Promise<{
|
|
15
12
|
cleanup: () => Promise<void>;
|
|
16
13
|
directory: string;
|
|
17
14
|
importMap: {
|
|
18
15
|
baseURL: import("url").URL;
|
|
19
16
|
imports: Record<string, string>;
|
|
20
17
|
};
|
|
18
|
+
npmSpecifiersWithExtraneousFiles: string[];
|
|
21
19
|
} | undefined>;
|
|
22
20
|
export {};
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { promises as fs } from 'fs';
|
|
2
|
-
import { builtinModules
|
|
2
|
+
import { builtinModules } from 'module';
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import { fileURLToPath, pathToFileURL } from 'url';
|
|
5
5
|
import { resolve } from '@import-maps/resolve';
|
|
6
|
+
import { nodeFileTrace, resolve as nftResolve } from '@vercel/nft';
|
|
6
7
|
import { build } from 'esbuild';
|
|
8
|
+
import getPackageName from 'get-package-name';
|
|
7
9
|
import tmp from 'tmp-promise';
|
|
8
|
-
|
|
9
|
-
const builtinModulesSet = new Set(builtinModules);
|
|
10
|
-
const require = createRequire(import.meta.url);
|
|
10
|
+
const TYPESCRIPT_EXTENSIONS = new Set(['.ts', '.cts', '.mts']);
|
|
11
11
|
// Workaround for https://github.com/evanw/esbuild/issues/1921.
|
|
12
12
|
const banner = {
|
|
13
13
|
js: `
|
|
@@ -19,72 +19,83 @@ const banner = {
|
|
|
19
19
|
let require=___nfyCreateRequire(import.meta.url);
|
|
20
20
|
`,
|
|
21
21
|
};
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
22
|
+
/**
|
|
23
|
+
* Parses a set of functions and returns a list of specifiers that correspond
|
|
24
|
+
* to npm modules.
|
|
25
|
+
*
|
|
26
|
+
* @param basePath Root of the project
|
|
27
|
+
* @param functions Functions to parse
|
|
28
|
+
* @param importMap Import map to apply when resolving imports
|
|
29
|
+
*/
|
|
30
|
+
const getNPMSpecifiers = async (basePath, functions, importMap) => {
|
|
31
|
+
const baseURL = pathToFileURL(basePath);
|
|
32
|
+
const { reasons } = await nodeFileTrace(functions, {
|
|
33
|
+
base: basePath,
|
|
34
|
+
readFile: async (filePath) => {
|
|
35
|
+
// If this is a TypeScript file, we need to compile in before we can
|
|
36
|
+
// parse it.
|
|
37
|
+
if (TYPESCRIPT_EXTENSIONS.has(path.extname(filePath))) {
|
|
38
|
+
const compiled = await build({
|
|
39
|
+
bundle: false,
|
|
40
|
+
entryPoints: [filePath],
|
|
41
|
+
logLevel: 'silent',
|
|
42
|
+
platform: 'node',
|
|
43
|
+
write: false,
|
|
44
|
+
});
|
|
45
|
+
return compiled.outputFiles[0].text;
|
|
31
46
|
}
|
|
32
|
-
|
|
33
|
-
|
|
47
|
+
return fs.readFile(filePath, 'utf8');
|
|
48
|
+
},
|
|
49
|
+
// eslint-disable-next-line require-await
|
|
50
|
+
resolve: async (specifier, ...args) => {
|
|
34
51
|
// Start by checking whether the specifier matches any import map defined
|
|
35
52
|
// by the user.
|
|
36
53
|
const { matched, resolvedImport } = resolve(specifier, importMap, baseURL);
|
|
37
54
|
// If it does, the resolved import is the specifier we'll evaluate going
|
|
38
55
|
// forward.
|
|
39
|
-
if (matched) {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
}
|
|
43
|
-
specifier = fileURLToPath(resolvedImport).replace(/\\/g, '/');
|
|
44
|
-
result.path = specifier;
|
|
45
|
-
}
|
|
46
|
-
// If the specifier is a Node.js built-in, we don't want to bundle it.
|
|
47
|
-
if (specifier.startsWith(nodePrefix) || builtinModulesSet.has(specifier)) {
|
|
48
|
-
return { external: true };
|
|
49
|
-
}
|
|
50
|
-
// We don't support the `npm:` prefix yet. Mark the specifier as external
|
|
51
|
-
// and the ESZIP bundler will handle the failure.
|
|
52
|
-
if (specifier.startsWith(npmPrefix)) {
|
|
53
|
-
return { external: true };
|
|
54
|
-
}
|
|
55
|
-
const isLocalImport = specifier.startsWith(path.sep) || specifier.startsWith('.') || path.isAbsolute(specifier);
|
|
56
|
-
// If this is a local import, return so that esbuild visits that path.
|
|
57
|
-
if (isLocalImport) {
|
|
58
|
-
return result;
|
|
59
|
-
}
|
|
60
|
-
const isRemoteURLImport = specifier.startsWith('https://') || specifier.startsWith('http://');
|
|
61
|
-
if (isRemoteURLImport) {
|
|
62
|
-
return { external: true };
|
|
63
|
-
}
|
|
64
|
-
// At this point we know we're dealing with a bare specifier that should
|
|
65
|
-
// be treated as an external module. We first try to resolve it, because
|
|
66
|
-
// in the event that it doesn't exist (e.g. user is referencing a module
|
|
67
|
-
// that they haven't installed) we won't even attempt to bundle it. This
|
|
68
|
-
// lets the ESZIP bundler handle and report the missing import instead of
|
|
69
|
-
// esbuild, which is a better experience for the user.
|
|
70
|
-
try {
|
|
71
|
-
require.resolve(specifier, { paths: [args.resolveDir] });
|
|
72
|
-
specifiers.add(specifier);
|
|
56
|
+
if (matched && resolvedImport.protocol === 'file:') {
|
|
57
|
+
const newSpecifier = fileURLToPath(resolvedImport).replace(/\\/g, '/');
|
|
58
|
+
return nftResolve(newSpecifier, ...args);
|
|
73
59
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
return
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
|
|
60
|
+
return nftResolve(specifier, ...args);
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
const npmSpecifiers = new Set();
|
|
64
|
+
const npmSpecifiersWithExtraneousFiles = new Set();
|
|
65
|
+
reasons.forEach((reason, filePath) => {
|
|
66
|
+
const packageName = getPackageName(filePath);
|
|
67
|
+
if (packageName === undefined) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
const parents = [...reason.parents];
|
|
71
|
+
const isDirectDependency = parents.some((parentPath) => !parentPath.startsWith(`node_modules${path.sep}`));
|
|
72
|
+
// We're only interested in capturing the specifiers that are first-level
|
|
73
|
+
// dependencies. Because we'll bundle all modules in a subsequent step,
|
|
74
|
+
// any transitive dependencies will be handled then.
|
|
75
|
+
if (isDirectDependency) {
|
|
76
|
+
const specifier = getPackageName(filePath);
|
|
77
|
+
npmSpecifiers.add(specifier);
|
|
78
|
+
}
|
|
79
|
+
const isExtraneousFile = reason.type.every((type) => type === 'asset');
|
|
80
|
+
// An extraneous file is a dependency that was traced by NFT and marked
|
|
81
|
+
// as not being statically imported. We can't process dynamic importing
|
|
82
|
+
// at runtime, so we gather the list of modules that may use these files
|
|
83
|
+
// so that we can warn users about this caveat.
|
|
84
|
+
if (isExtraneousFile) {
|
|
85
|
+
parents.forEach((path) => {
|
|
86
|
+
const specifier = getPackageName(path);
|
|
87
|
+
if (specifier) {
|
|
88
|
+
npmSpecifiersWithExtraneousFiles.add(specifier);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
return {
|
|
94
|
+
npmSpecifiers: [...npmSpecifiers],
|
|
95
|
+
npmSpecifiersWithExtraneousFiles: [...npmSpecifiersWithExtraneousFiles],
|
|
96
|
+
};
|
|
97
|
+
};
|
|
98
|
+
export const vendorNPMSpecifiers = async ({ basePath, directory, functions, importMap, }) => {
|
|
88
99
|
// The directories that esbuild will use when resolving Node modules. We must
|
|
89
100
|
// set these manually because esbuild will be operating from a temporary
|
|
90
101
|
// directory that will not live inside the project root, so the normal
|
|
@@ -94,41 +105,15 @@ export const vendorNPMSpecifiers = async ({ basePath, directory, functions, impo
|
|
|
94
105
|
// project directory. If a custom directory has been specified, we use it.
|
|
95
106
|
// Otherwise, create a random temporary directory.
|
|
96
107
|
const temporaryDirectory = directory ? { path: directory } : await tmp.dir();
|
|
97
|
-
|
|
98
|
-
// loaded as npm dependencies, because they either use the `npm:` prefix or
|
|
99
|
-
// they are bare specifiers. We'll collect them in `specifiers`.
|
|
100
|
-
try {
|
|
101
|
-
const { errors, warnings } = await build({
|
|
102
|
-
banner,
|
|
103
|
-
bundle: true,
|
|
104
|
-
entryPoints: functions,
|
|
105
|
-
logLevel: 'silent',
|
|
106
|
-
nodePaths,
|
|
107
|
-
outdir: temporaryDirectory.path,
|
|
108
|
-
platform: 'node',
|
|
109
|
-
plugins: [getDependencyTrackerPlugin(specifiers, importMap.getContentsWithURLObjects(), pathToFileURL(basePath))],
|
|
110
|
-
write: false,
|
|
111
|
-
format: 'esm',
|
|
112
|
-
});
|
|
113
|
-
if (errors.length !== 0) {
|
|
114
|
-
logger.system('ESBuild errored while tracking dependencies in edge function:', errors);
|
|
115
|
-
}
|
|
116
|
-
if (warnings.length !== 0) {
|
|
117
|
-
logger.system('ESBuild warned while tracking dependencies in edge function:', warnings);
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
catch (error) {
|
|
121
|
-
logger.system('Could not track dependencies in edge function:', error);
|
|
122
|
-
logger.user('An error occurred when trying to scan your edge functions for npm modules, which is an experimental feature. If you are loading npm modules, please share the link to this deploy in https://ntl.fyi/edge-functions-npm. If you are not loading npm modules, you can ignore this message.');
|
|
123
|
-
}
|
|
108
|
+
const { npmSpecifiers, npmSpecifiersWithExtraneousFiles } = await getNPMSpecifiers(basePath, functions, importMap.getContentsWithURLObjects());
|
|
124
109
|
// If we found no specifiers, there's nothing left to do here.
|
|
125
|
-
if (
|
|
110
|
+
if (npmSpecifiers.length === 0) {
|
|
126
111
|
return;
|
|
127
112
|
}
|
|
128
113
|
// To bundle an entire module and all its dependencies, create a barrel file
|
|
129
114
|
// where we re-export everything from that specifier. We do this for every
|
|
130
115
|
// specifier, and each of these files will become entry points to esbuild.
|
|
131
|
-
const ops = await Promise.all(
|
|
116
|
+
const ops = await Promise.all(npmSpecifiers.map(async (specifier, index) => {
|
|
132
117
|
const code = `import * as mod from "${specifier}"; export default mod.default; export * from "${specifier}";`;
|
|
133
118
|
const filePath = path.join(temporaryDirectory.path, `barrel-${index}.js`);
|
|
134
119
|
await fs.writeFile(filePath, code);
|
|
@@ -189,5 +174,6 @@ export const vendorNPMSpecifiers = async ({ basePath, directory, functions, impo
|
|
|
189
174
|
cleanup,
|
|
190
175
|
directory: temporaryDirectory.path,
|
|
191
176
|
importMap: newImportMap,
|
|
177
|
+
npmSpecifiersWithExtraneousFiles,
|
|
192
178
|
};
|
|
193
179
|
};
|
|
@@ -23,6 +23,7 @@ const prepareServer = ({ basePath, bootstrapURL, deno, distDirectory, distImport
|
|
|
23
23
|
});
|
|
24
24
|
const features = {};
|
|
25
25
|
const importMap = baseImportMap.clone();
|
|
26
|
+
const npmSpecifiersWithExtraneousFiles = [];
|
|
26
27
|
if (featureFlags === null || featureFlags === void 0 ? void 0 : featureFlags.edge_functions_npm_modules) {
|
|
27
28
|
const vendor = await vendorNPMSpecifiers({
|
|
28
29
|
basePath,
|
|
@@ -34,6 +35,7 @@ const prepareServer = ({ basePath, bootstrapURL, deno, distDirectory, distImport
|
|
|
34
35
|
if (vendor) {
|
|
35
36
|
features.npmModules = true;
|
|
36
37
|
importMap.add(vendor.importMap);
|
|
38
|
+
npmSpecifiersWithExtraneousFiles.push(...vendor.npmSpecifiersWithExtraneousFiles);
|
|
37
39
|
}
|
|
38
40
|
}
|
|
39
41
|
try {
|
|
@@ -69,6 +71,7 @@ const prepareServer = ({ basePath, bootstrapURL, deno, distDirectory, distImport
|
|
|
69
71
|
features,
|
|
70
72
|
functionsConfig,
|
|
71
73
|
graph,
|
|
74
|
+
npmSpecifiersWithExtraneousFiles,
|
|
72
75
|
success,
|
|
73
76
|
};
|
|
74
77
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@netlify/edge-bundler",
|
|
3
|
-
"version": "9.
|
|
3
|
+
"version": "9.3.0",
|
|
4
4
|
"description": "Intelligently prepare Netlify Edge Functions for deployment",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/node/index.js",
|
|
@@ -74,6 +74,7 @@
|
|
|
74
74
|
},
|
|
75
75
|
"dependencies": {
|
|
76
76
|
"@import-maps/resolve": "^1.0.1",
|
|
77
|
+
"@vercel/nft": "^0.24.3",
|
|
77
78
|
"ajv": "^8.11.2",
|
|
78
79
|
"ajv-errors": "^3.0.0",
|
|
79
80
|
"better-ajv-errors": "^1.2.0",
|
|
@@ -82,6 +83,7 @@
|
|
|
82
83
|
"esbuild": "0.19.4",
|
|
83
84
|
"execa": "^6.0.0",
|
|
84
85
|
"find-up": "^6.3.0",
|
|
86
|
+
"get-package-name": "^2.2.0",
|
|
85
87
|
"get-port": "^6.1.2",
|
|
86
88
|
"is-path-inside": "^4.0.0",
|
|
87
89
|
"jsonc-parser": "^3.2.0",
|