@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');
@@ -16,7 +16,6 @@ export declare const vendorNPMSpecifiers: ({ basePath, directory, functions, imp
16
16
  baseURL: import("url").URL;
17
17
  imports: Record<string, string>;
18
18
  };
19
- npmSpecifiersWithExtraneousFiles: string[];
20
19
  outputFiles: string[];
21
20
  }>;
22
21
  export {};
@@ -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 getPackageName from 'get-package-name';
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
- const safelyDetectTypes = async (packageJsonPath) => {
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
- return await getTypesPath(packageJsonPath);
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 npmSpecifiersWithExtraneousFiles = new Set();
123
- for (const [filePath, reason] of reasons.entries()) {
124
- const parents = [...reason.parents];
125
- const isExtraneousFile = reason.type.every((type) => type === 'asset');
126
- // An extraneous file is a dependency that was traced by NFT and marked
127
- // as not being statically imported. We can't process dynamic importing
128
- // at runtime, so we gather the list of modules that may use these files
129
- // so that we can warn users about this caveat.
130
- if (isExtraneousFile) {
131
- parents.forEach((path) => {
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
- // every dependency will have its `package.json` in `reasons` exactly once.
139
- // by only looking at this file, we save us from doing duplicate work.
140
- const isPackageJson = filePath.endsWith('package.json');
141
- if (!isPackageJson)
142
- continue;
143
- const packageName = getPackageName(filePath);
144
- if (packageName === undefined)
145
- continue;
146
- const isDirectDependency = parents.some((parentPath) => {
147
- var _a, _b;
148
- if (!parentPath.startsWith(`node_modules${path.sep}`))
149
- return true;
150
- // typically, edge functions have no direct dependency on the `package.json` of a module.
151
- // it's the impl files that depend on `package.json`, so we need to check the parents of
152
- // the `package.json` file as well to see if the module is a direct dependency.
153
- const parents = [...((_b = (_a = reasons.get(parentPath)) === null || _a === void 0 ? void 0 : _a.parents) !== null && _b !== void 0 ? _b : [])];
154
- return parents.some((parentPath) => !parentPath.startsWith(`node_modules${path.sep}`));
155
- });
156
- // We're only interested in capturing the specifiers that are first-level
157
- // dependencies. Because we'll bundle all modules in a subsequent step,
158
- // any transitive dependencies will be handled then.
159
- if (isDirectDependency) {
160
- npmSpecifiers.push({
161
- specifier: packageName,
162
- types: environment === 'development' ? await safelyDetectTypes(path.join(basePath, filePath)) : undefined,
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 { npmSpecifiers, npmSpecifiersWithExtraneousFiles } = await getNPMSpecifiers({
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}"; export default mod.default; export * from "${specifier}";`;
193
- const filePath = path.join(temporaryDirectory.path, `bundled-${slugifyPackageName(specifier)}.js`);
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, npmSpecifiersWithExtraneousFiles } = await server(functions, {
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-pt-committee__identidade.js'), 'utf-8');
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, npmSpecifiersWithExtraneousFiles } = await server(functions, {
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": "12.4.0",
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": "0e4628f57a0a7d02594b5070074445a4ff69b809"
83
+ "gitHead": "8d10878db3be9cfefe3162fd155c76595aba045e"
84
84
  }