@netlify/edge-bundler 12.4.0 → 13.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.
|
@@ -402,6 +402,28 @@ test('Loads npm modules from bare specifiers', async () => {
|
|
|
402
402
|
await cleanup();
|
|
403
403
|
await rm(vendorDirectory.path, { force: true, recursive: true });
|
|
404
404
|
});
|
|
405
|
+
test('Loads npm modules which use package.json.exports', async () => {
|
|
406
|
+
const { basePath, cleanup, distPath } = await useFixture('imports_npm_module_exports');
|
|
407
|
+
const sourceDirectory = join(basePath, 'functions');
|
|
408
|
+
const declarations = [
|
|
409
|
+
{
|
|
410
|
+
function: 'func1',
|
|
411
|
+
path: '/func1',
|
|
412
|
+
},
|
|
413
|
+
];
|
|
414
|
+
const vendorDirectory = await tmp.dir();
|
|
415
|
+
await bundle([sourceDirectory], distPath, declarations, {
|
|
416
|
+
basePath,
|
|
417
|
+
vendorDirectory: vendorDirectory.path,
|
|
418
|
+
});
|
|
419
|
+
const manifestFile = await readFile(resolve(distPath, 'manifest.json'), 'utf8');
|
|
420
|
+
const manifest = JSON.parse(manifestFile);
|
|
421
|
+
const bundlePath = join(distPath, manifest.bundles[0].asset);
|
|
422
|
+
const { func1 } = await runESZIP(bundlePath, vendorDirectory.path);
|
|
423
|
+
expect(func1).toBe('hello');
|
|
424
|
+
await cleanup();
|
|
425
|
+
await rm(vendorDirectory.path, { force: true, recursive: true });
|
|
426
|
+
});
|
|
405
427
|
test('Loads npm modules in a monorepo setup', async () => {
|
|
406
428
|
const systemLogger = vi.fn();
|
|
407
429
|
const { basePath: rootPath, cleanup, distPath } = await useFixture('monorepo_npm_module');
|
|
@@ -3,13 +3,15 @@ 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';
|
|
7
6
|
import { build } from 'esbuild';
|
|
8
7
|
import { findUp } from 'find-up';
|
|
9
|
-
import
|
|
8
|
+
import { parseImports } from 'parse-imports';
|
|
10
9
|
import tmp from 'tmp-promise';
|
|
11
10
|
import { pathsBetween } from './utils/fs.js';
|
|
12
11
|
const TYPESCRIPT_EXTENSIONS = new Set(['.ts', '.tsx', '.cts', '.ctsx', '.mts', '.mtsx']);
|
|
12
|
+
const slugifyFileName = (specifier) => {
|
|
13
|
+
return specifier.replace(/\//g, '_');
|
|
14
|
+
};
|
|
13
15
|
const slugifyPackageName = (specifier) => {
|
|
14
16
|
if (!specifier.startsWith('@'))
|
|
15
17
|
return specifier;
|
|
@@ -55,9 +57,20 @@ const getTypesPath = async (packageJsonPath) => {
|
|
|
55
57
|
}
|
|
56
58
|
return await getTypePathFromTypesPackage(name, packageJsonPath);
|
|
57
59
|
};
|
|
58
|
-
|
|
60
|
+
function packageName(specifier) {
|
|
61
|
+
if (!specifier.startsWith('@'))
|
|
62
|
+
return specifier.split('/')[0];
|
|
63
|
+
const [scope, pkg] = specifier.split('/');
|
|
64
|
+
return `${scope}/${pkg}`;
|
|
65
|
+
}
|
|
66
|
+
const safelyDetectTypes = async (pkg, basePath) => {
|
|
59
67
|
try {
|
|
60
|
-
|
|
68
|
+
const json = await findUp(`node_modules/${packageName(pkg)}/package.json`, {
|
|
69
|
+
cwd: basePath,
|
|
70
|
+
});
|
|
71
|
+
if (json) {
|
|
72
|
+
return await getTypesPath(json);
|
|
73
|
+
}
|
|
61
74
|
}
|
|
62
75
|
catch {
|
|
63
76
|
return undefined;
|
|
@@ -81,92 +94,80 @@ const banner = {
|
|
|
81
94
|
globalThis.Buffer = __nfyBuffer;
|
|
82
95
|
`,
|
|
83
96
|
};
|
|
97
|
+
async function compileTypeScript(file) {
|
|
98
|
+
const compiled = await build({
|
|
99
|
+
bundle: false,
|
|
100
|
+
entryPoints: [file],
|
|
101
|
+
logLevel: 'silent',
|
|
102
|
+
platform: 'node',
|
|
103
|
+
write: false,
|
|
104
|
+
});
|
|
105
|
+
return compiled.outputFiles[0].text;
|
|
106
|
+
}
|
|
107
|
+
async function parseImportsForFile(file, rootPath) {
|
|
108
|
+
const source = TYPESCRIPT_EXTENSIONS.has(path.extname(file))
|
|
109
|
+
? await compileTypeScript(file)
|
|
110
|
+
: await fs.readFile(file, 'utf-8');
|
|
111
|
+
return await parseImports(source, {
|
|
112
|
+
resolveFrom: rootPath,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
84
115
|
/**
|
|
85
116
|
* Parses a set of functions and returns a list of specifiers that correspond
|
|
86
117
|
* to npm modules.
|
|
87
118
|
*/
|
|
88
119
|
const getNPMSpecifiers = async ({ basePath, functions, importMap, environment, rootPath }) => {
|
|
120
|
+
var _a;
|
|
89
121
|
const baseURL = pathToFileURL(basePath);
|
|
90
|
-
const { reasons } = await nodeFileTrace(functions, {
|
|
91
|
-
base: rootPath,
|
|
92
|
-
processCwd: basePath,
|
|
93
|
-
readFile: async (filePath) => {
|
|
94
|
-
// If this is a TypeScript file, we need to compile in before we can
|
|
95
|
-
// parse it.
|
|
96
|
-
if (TYPESCRIPT_EXTENSIONS.has(path.extname(filePath))) {
|
|
97
|
-
const compiled = await build({
|
|
98
|
-
bundle: false,
|
|
99
|
-
entryPoints: [filePath],
|
|
100
|
-
logLevel: 'silent',
|
|
101
|
-
platform: 'node',
|
|
102
|
-
write: false,
|
|
103
|
-
});
|
|
104
|
-
return compiled.outputFiles[0].text;
|
|
105
|
-
}
|
|
106
|
-
return fs.readFile(filePath, 'utf8');
|
|
107
|
-
},
|
|
108
|
-
resolve: async (specifier, ...args) => {
|
|
109
|
-
// Start by checking whether the specifier matches any import map defined
|
|
110
|
-
// by the user.
|
|
111
|
-
const { matched, resolvedImport } = resolve(specifier, importMap, baseURL);
|
|
112
|
-
// If it does, the resolved import is the specifier we'll evaluate going
|
|
113
|
-
// forward.
|
|
114
|
-
if (matched && resolvedImport.protocol === 'file:') {
|
|
115
|
-
const newSpecifier = fileURLToPath(resolvedImport).replace(/\\/g, '/');
|
|
116
|
-
return nftResolve(newSpecifier, ...args);
|
|
117
|
-
}
|
|
118
|
-
return nftResolve(specifier, ...args);
|
|
119
|
-
},
|
|
120
|
-
});
|
|
121
122
|
const npmSpecifiers = [];
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
const
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
const specifier = getPackageName(path);
|
|
133
|
-
if (specifier) {
|
|
134
|
-
npmSpecifiersWithExtraneousFiles.add(specifier);
|
|
123
|
+
for (const func of functions) {
|
|
124
|
+
const imports = await parseImportsForFile(func, rootPath);
|
|
125
|
+
for (const i of imports) {
|
|
126
|
+
// The non-null assertion is required because typescript can not infer that `moduleSpecifier.value` can be narrowed to a string.
|
|
127
|
+
// The narrowing is possible because `moduleSpecifier.value` will always be a string when `moduleSpecifier.isConstant` is true.
|
|
128
|
+
const specifier = i.moduleSpecifier.isConstant ? i.moduleSpecifier.value : i.moduleSpecifier.code;
|
|
129
|
+
switch (i.moduleSpecifier.type) {
|
|
130
|
+
case 'absolute': {
|
|
131
|
+
npmSpecifiers.push(...(await getNPMSpecifiers({ basePath, functions: [specifier], importMap, environment, rootPath })));
|
|
132
|
+
break;
|
|
135
133
|
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
134
|
+
case 'relative': {
|
|
135
|
+
const filePath = path.join(path.dirname(func), specifier);
|
|
136
|
+
npmSpecifiers.push(...(await getNPMSpecifiers({ basePath, functions: [filePath], importMap, environment, rootPath })));
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
case 'package': {
|
|
140
|
+
// node: prefixed imports are detected as packages instead of as builtins
|
|
141
|
+
// we don't want to try and bundle builtins so we ignore node: prefixed imports
|
|
142
|
+
if (specifier.startsWith('node:')) {
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
145
|
+
const { matched, resolvedImport } = resolve(specifier, importMap, baseURL);
|
|
146
|
+
if (matched) {
|
|
147
|
+
if (resolvedImport.protocol === 'file:') {
|
|
148
|
+
const newSpecifier = fileURLToPath(resolvedImport).replace(/\\/g, '/');
|
|
149
|
+
npmSpecifiers.push(...(await getNPMSpecifiers({ basePath, functions: [newSpecifier], importMap, environment, rootPath })));
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
else if (!((_a = resolvedImport === null || resolvedImport === void 0 ? void 0 : resolvedImport.protocol) === null || _a === void 0 ? void 0 : _a.startsWith('http'))) {
|
|
153
|
+
const t = await safelyDetectTypes(specifier, basePath);
|
|
154
|
+
npmSpecifiers.push({
|
|
155
|
+
specifier: specifier,
|
|
156
|
+
types: t,
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
break;
|
|
160
|
+
}
|
|
161
|
+
case 'builtin':
|
|
162
|
+
case 'invalid':
|
|
163
|
+
case 'unknown': {
|
|
164
|
+
// We don't bundle these types of modules
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
164
168
|
}
|
|
165
169
|
}
|
|
166
|
-
return
|
|
167
|
-
npmSpecifiers,
|
|
168
|
-
npmSpecifiersWithExtraneousFiles: [...npmSpecifiersWithExtraneousFiles],
|
|
169
|
-
};
|
|
170
|
+
return npmSpecifiers;
|
|
170
171
|
};
|
|
171
172
|
export const vendorNPMSpecifiers = async ({ basePath, directory, functions, importMap, environment, rootPath = basePath, }) => {
|
|
172
173
|
// The directories that esbuild will use when resolving Node modules. We must
|
|
@@ -178,7 +179,7 @@ export const vendorNPMSpecifiers = async ({ basePath, directory, functions, impo
|
|
|
178
179
|
// project directory. If a custom directory has been specified, we use it.
|
|
179
180
|
// Otherwise, create a random temporary directory.
|
|
180
181
|
const temporaryDirectory = directory ? { path: directory } : await tmp.dir();
|
|
181
|
-
const
|
|
182
|
+
const npmSpecifiers = await getNPMSpecifiers({
|
|
182
183
|
basePath,
|
|
183
184
|
functions,
|
|
184
185
|
importMap: importMap.getContentsWithURLObjects(),
|
|
@@ -189,8 +190,8 @@ export const vendorNPMSpecifiers = async ({ basePath, directory, functions, impo
|
|
|
189
190
|
// where we re-export everything from that specifier. We do this for every
|
|
190
191
|
// specifier, and each of these files will become entry points to esbuild.
|
|
191
192
|
const ops = await Promise.all(npmSpecifiers.map(async ({ specifier, types }) => {
|
|
192
|
-
const code = `import * as mod from "${specifier}"
|
|
193
|
-
const filePath = path.join(temporaryDirectory.path, `bundled-${
|
|
193
|
+
const code = `import * as mod from "${specifier}";\nexport default mod.default;\nexport * from "${specifier}";`;
|
|
194
|
+
const filePath = path.join(temporaryDirectory.path, `bundled-${slugifyFileName(specifier)}.js`);
|
|
194
195
|
await fs.writeFile(filePath, code);
|
|
195
196
|
return { filePath, specifier, types };
|
|
196
197
|
}));
|
|
@@ -269,7 +270,6 @@ export const vendorNPMSpecifiers = async ({ basePath, directory, functions, impo
|
|
|
269
270
|
cleanup,
|
|
270
271
|
directory: temporaryDirectory.path,
|
|
271
272
|
importMap: newImportMap,
|
|
272
|
-
npmSpecifiersWithExtraneousFiles,
|
|
273
273
|
outputFiles,
|
|
274
274
|
};
|
|
275
275
|
};
|
|
@@ -39,7 +39,6 @@ export declare const serve: ({ basePath, bootstrapURL, certificatePath, debug, d
|
|
|
39
39
|
features: Record<string, boolean>;
|
|
40
40
|
functionsConfig: FunctionConfig[];
|
|
41
41
|
graph: ModuleGraphJson;
|
|
42
|
-
npmSpecifiersWithExtraneousFiles: string[];
|
|
43
42
|
success: boolean;
|
|
44
43
|
}>>;
|
|
45
44
|
export {};
|
|
@@ -42,7 +42,6 @@ const prepareServer = ({ basePath, bootstrapURL, deno, distDirectory, distImport
|
|
|
42
42
|
const features = {};
|
|
43
43
|
const importMap = new ImportMap();
|
|
44
44
|
await importMap.addFiles((_a = options.importMapPaths) !== null && _a !== void 0 ? _a : [], logger);
|
|
45
|
-
const npmSpecifiersWithExtraneousFiles = [];
|
|
46
45
|
// we keep track of the files that are relevant to the user's code, so we can clean up leftovers from past executions later
|
|
47
46
|
const relevantFiles = [stage2Path];
|
|
48
47
|
const vendor = await vendorNPMSpecifiers({
|
|
@@ -57,7 +56,6 @@ const prepareServer = ({ basePath, bootstrapURL, deno, distDirectory, distImport
|
|
|
57
56
|
if (vendor) {
|
|
58
57
|
features.npmModules = true;
|
|
59
58
|
importMap.add(vendor.importMap);
|
|
60
|
-
npmSpecifiersWithExtraneousFiles.push(...vendor.npmSpecifiersWithExtraneousFiles);
|
|
61
59
|
relevantFiles.push(...vendor.outputFiles);
|
|
62
60
|
}
|
|
63
61
|
await cleanDirectory(distDirectory, relevantFiles);
|
|
@@ -96,7 +94,6 @@ const prepareServer = ({ basePath, bootstrapURL, deno, distDirectory, distImport
|
|
|
96
94
|
features,
|
|
97
95
|
functionsConfig,
|
|
98
96
|
graph,
|
|
99
|
-
npmSpecifiersWithExtraneousFiles,
|
|
100
97
|
success,
|
|
101
98
|
};
|
|
102
99
|
};
|
|
@@ -42,13 +42,12 @@ test('Starts a server and serves requests for edge functions', async () => {
|
|
|
42
42
|
getFunctionsConfig: true,
|
|
43
43
|
importMapPaths,
|
|
44
44
|
};
|
|
45
|
-
const { features, functionsConfig, graph, success
|
|
45
|
+
const { features, functionsConfig, graph, success } = await server(functions, {
|
|
46
46
|
very_secret_secret: 'i love netlify',
|
|
47
47
|
}, options);
|
|
48
48
|
expect(features).toEqual({ npmModules: true });
|
|
49
49
|
expect(success).toBe(true);
|
|
50
50
|
expect(functionsConfig).toEqual([{ path: '/my-function' }, {}, { path: '/global-netlify' }]);
|
|
51
|
-
expect(npmSpecifiersWithExtraneousFiles).toEqual(['dictionary']);
|
|
52
51
|
const modules = graph === null || graph === void 0 ? void 0 : graph.modules.filter(({ kind, mediaType }) => kind === 'esm' && mediaType === 'TypeScript');
|
|
53
52
|
for (const key in functions) {
|
|
54
53
|
const graphEntry = modules === null || modules === void 0 ? void 0 : modules.some(({ local }) => local === functions[key].path);
|
|
@@ -85,7 +84,7 @@ test('Starts a server and serves requests for edge functions', async () => {
|
|
|
85
84
|
});
|
|
86
85
|
const idBarrelFile = await readFile(join(servePath, 'bundled-id.js'), 'utf-8');
|
|
87
86
|
expect(idBarrelFile).toContain(`/// <reference types="${join('..', '..', 'node_modules', 'id', 'types.d.ts')}" />`);
|
|
88
|
-
const identidadeBarrelFile = await readFile(join(servePath, 'bundled
|
|
87
|
+
const identidadeBarrelFile = await readFile(join(servePath, 'bundled-@pt-committee_identidade.js'), 'utf-8');
|
|
89
88
|
expect(identidadeBarrelFile).toContain(`/// <reference types="${join('..', '..', 'node_modules', '@types', 'pt-committee__identidade', 'index.d.ts')}" />`);
|
|
90
89
|
});
|
|
91
90
|
test('Serves edge functions in a monorepo setup', async () => {
|
|
@@ -117,13 +116,12 @@ test('Serves edge functions in a monorepo setup', async () => {
|
|
|
117
116
|
getFunctionsConfig: true,
|
|
118
117
|
importMapPaths,
|
|
119
118
|
};
|
|
120
|
-
const { features, functionsConfig, graph, success
|
|
119
|
+
const { features, functionsConfig, graph, success } = await server(functions, {
|
|
121
120
|
very_secret_secret: 'i love netlify',
|
|
122
121
|
}, options);
|
|
123
122
|
expect(features).toEqual({ npmModules: true });
|
|
124
123
|
expect(success).toBe(true);
|
|
125
124
|
expect(functionsConfig).toEqual([{ path: '/func1' }]);
|
|
126
|
-
expect(npmSpecifiersWithExtraneousFiles).toEqual(['child-1']);
|
|
127
125
|
for (const key in functions) {
|
|
128
126
|
const graphEntry = graph === null || graph === void 0 ? void 0 : graph.modules.some(({ kind, mediaType, local }) => kind === 'esm' && mediaType === 'TypeScript' && local === functions[key].path);
|
|
129
127
|
expect(graphEntry).toBe(true);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@netlify/edge-bundler",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "13.0.0",
|
|
4
4
|
"description": "Intelligently prepare Netlify Edge Functions for deployment",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/node/index.js",
|
|
@@ -58,7 +58,6 @@
|
|
|
58
58
|
},
|
|
59
59
|
"dependencies": {
|
|
60
60
|
"@import-maps/resolve": "^1.0.1",
|
|
61
|
-
"@vercel/nft": "0.27.7",
|
|
62
61
|
"ajv": "^8.11.2",
|
|
63
62
|
"ajv-errors": "^3.0.0",
|
|
64
63
|
"better-ajv-errors": "^1.2.0",
|
|
@@ -74,11 +73,12 @@
|
|
|
74
73
|
"node-stream-zip": "^1.15.0",
|
|
75
74
|
"p-retry": "^5.1.1",
|
|
76
75
|
"p-wait-for": "^5.0.0",
|
|
76
|
+
"parse-imports": "^2.2.1",
|
|
77
77
|
"path-key": "^4.0.0",
|
|
78
78
|
"semver": "^7.3.8",
|
|
79
79
|
"tmp-promise": "^3.0.3",
|
|
80
80
|
"urlpattern-polyfill": "8.0.2",
|
|
81
81
|
"uuid": "^9.0.0"
|
|
82
82
|
},
|
|
83
|
-
"gitHead": "
|
|
83
|
+
"gitHead": "8d10878db3be9cfefe3162fd155c76595aba045e"
|
|
84
84
|
}
|