@netlify/edge-bundler 4.1.0 → 4.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/deno/config.ts +4 -0
- package/deno/lib/consts.ts +5 -4
- package/deno/lib/stage2.ts +5 -2
- package/dist/node/bundle_error.d.ts +3 -0
- package/dist/node/bundle_error.js +3 -0
- package/dist/node/bundler.d.ts +1 -1
- package/dist/node/bundler.js +2 -1
- package/dist/node/bundler.test.js +51 -0
- package/dist/node/config.js +3 -0
- package/dist/node/config.test.js +76 -0
- package/dist/node/formats/eszip.js +2 -1
- package/dist/node/formats/javascript.js +3 -2
- package/dist/node/npm_import_error.d.ts +5 -0
- package/dist/node/npm_import_error.js +20 -0
- package/package.json +1 -1
package/deno/config.ts
CHANGED
package/deno/lib/consts.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
export const
|
|
2
|
-
export const
|
|
3
|
-
export const
|
|
4
|
-
export const
|
|
1
|
+
export const CUSTOM_LAYER_PREFIX = 'layer:'
|
|
2
|
+
export const PUBLIC_SPECIFIER = 'netlify:edge'
|
|
3
|
+
export const STAGE1_SPECIFIER = 'netlify:bootstrap-stage1'
|
|
4
|
+
export const STAGE2_SPECIFIER = 'netlify:bootstrap-stage2'
|
|
5
|
+
export const virtualRoot = 'file:///root/'
|
package/deno/lib/stage2.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { build, LoadResponse } from 'https://deno.land/x/eszip@v0.28.0/mod.ts'
|
|
|
3
3
|
import * as path from 'https://deno.land/std@0.127.0/path/mod.ts'
|
|
4
4
|
|
|
5
5
|
import type { InputFunction, WriteStage2Options } from '../../shared/stage2.ts'
|
|
6
|
-
import { PUBLIC_SPECIFIER, STAGE2_SPECIFIER, virtualRoot } from './consts.ts'
|
|
6
|
+
import { CUSTOM_LAYER_PREFIX, PUBLIC_SPECIFIER, STAGE2_SPECIFIER, virtualRoot } from './consts.ts'
|
|
7
7
|
import { inlineModule, loadFromVirtualRoot, loadWithRetry } from './common.ts'
|
|
8
8
|
|
|
9
9
|
interface FunctionReference {
|
|
@@ -70,7 +70,7 @@ const stage2Loader = (basePath: string, functions: InputFunction[]) => {
|
|
|
70
70
|
return inlineModule(specifier, stage2Entry)
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
-
if (specifier === PUBLIC_SPECIFIER) {
|
|
73
|
+
if (specifier === PUBLIC_SPECIFIER || specifier.startsWith(CUSTOM_LAYER_PREFIX)) {
|
|
74
74
|
return {
|
|
75
75
|
kind: 'external',
|
|
76
76
|
specifier,
|
|
@@ -88,6 +88,9 @@ const stage2Loader = (basePath: string, functions: InputFunction[]) => {
|
|
|
88
88
|
const writeStage2 = async ({ basePath, destPath, functions, importMapURL }: WriteStage2Options) => {
|
|
89
89
|
const loader = stage2Loader(basePath, functions)
|
|
90
90
|
const bytes = await build([STAGE2_SPECIFIER], loader, importMapURL)
|
|
91
|
+
const directory = path.dirname(destPath)
|
|
92
|
+
|
|
93
|
+
await Deno.mkdir(directory, { recursive: true })
|
|
91
94
|
|
|
92
95
|
return await Deno.writeFile(destPath, bytes)
|
|
93
96
|
}
|
|
@@ -12,5 +12,8 @@ declare class BundleError extends Error {
|
|
|
12
12
|
customErrorInfo: ReturnType<typeof getCustomErrorInfo>;
|
|
13
13
|
constructor(originalError: Error, options: BundleErrorOptions);
|
|
14
14
|
}
|
|
15
|
+
/**
|
|
16
|
+
* BundleErrors are treated as user-error, so Netlify Team is not alerted about them.
|
|
17
|
+
*/
|
|
15
18
|
declare const wrapBundleError: (input: unknown, options: BundleErrorOptions) => unknown;
|
|
16
19
|
export { BundleError, wrapBundleError };
|
|
@@ -15,6 +15,9 @@ class BundleError extends Error {
|
|
|
15
15
|
Object.setPrototypeOf(this, BundleError.prototype);
|
|
16
16
|
}
|
|
17
17
|
}
|
|
18
|
+
/**
|
|
19
|
+
* BundleErrors are treated as user-error, so Netlify Team is not alerted about them.
|
|
20
|
+
*/
|
|
18
21
|
const wrapBundleError = (input, options) => {
|
|
19
22
|
if (input instanceof Error) {
|
|
20
23
|
return new BundleError(input, options);
|
package/dist/node/bundler.d.ts
CHANGED
|
@@ -17,7 +17,7 @@ interface BundleOptions {
|
|
|
17
17
|
onBeforeDownload?: OnBeforeDownloadHook;
|
|
18
18
|
systemLogger?: LogFunction;
|
|
19
19
|
}
|
|
20
|
-
declare const bundle: (sourceDirectories: string[], distDirectory: string, tomlDeclarations?: Declaration[], { basePath: inputBasePath, cacheDirectory, debug, distImportMapPath, featureFlags: inputFeatureFlags, importMaps, onAfterDownload, onBeforeDownload, systemLogger, }?: BundleOptions) => Promise<{
|
|
20
|
+
declare const bundle: (sourceDirectories: string[], distDirectory: string, tomlDeclarations?: Declaration[], { basePath: inputBasePath, cacheDirectory, debug, distImportMapPath, featureFlags: inputFeatureFlags, importMaps, layers, onAfterDownload, onBeforeDownload, systemLogger, }?: BundleOptions) => Promise<{
|
|
21
21
|
functions: EdgeFunction[];
|
|
22
22
|
manifest: import("./manifest.js").Manifest;
|
|
23
23
|
}>;
|
package/dist/node/bundler.js
CHANGED
|
@@ -35,7 +35,7 @@ const createBundle = ({ basePath, buildID, debug, deno, distDirectory, functions
|
|
|
35
35
|
importMap,
|
|
36
36
|
});
|
|
37
37
|
};
|
|
38
|
-
const bundle = async (sourceDirectories, distDirectory, tomlDeclarations = [], { basePath: inputBasePath, cacheDirectory, debug, distImportMapPath, featureFlags: inputFeatureFlags, importMaps, onAfterDownload, onBeforeDownload, systemLogger, } = {}) => {
|
|
38
|
+
const bundle = async (sourceDirectories, distDirectory, tomlDeclarations = [], { basePath: inputBasePath, cacheDirectory, debug, distImportMapPath, featureFlags: inputFeatureFlags, importMaps, layers, onAfterDownload, onBeforeDownload, systemLogger, } = {}) => {
|
|
39
39
|
const logger = getLogger(systemLogger, debug);
|
|
40
40
|
const featureFlags = getFlags(inputFeatureFlags);
|
|
41
41
|
const options = {
|
|
@@ -90,6 +90,7 @@ const bundle = async (sourceDirectories, distDirectory, tomlDeclarations = [], {
|
|
|
90
90
|
declarations,
|
|
91
91
|
distDirectory,
|
|
92
92
|
functions,
|
|
93
|
+
layers,
|
|
93
94
|
});
|
|
94
95
|
if (distImportMapPath) {
|
|
95
96
|
await importMap.writeToFile(distImportMapPath);
|
|
@@ -132,6 +132,28 @@ test('Adds a custom error property to user errors during bundling', async () =>
|
|
|
132
132
|
});
|
|
133
133
|
}
|
|
134
134
|
});
|
|
135
|
+
test('Prints a nice error message when user tries importing NPM module', async () => {
|
|
136
|
+
expect.assertions(2);
|
|
137
|
+
const sourceDirectory = resolve(fixturesDir, 'imports_npm_module', 'functions');
|
|
138
|
+
const tmpDir = await tmp.dir();
|
|
139
|
+
const declarations = [
|
|
140
|
+
{
|
|
141
|
+
function: 'func1',
|
|
142
|
+
path: '/func1',
|
|
143
|
+
},
|
|
144
|
+
];
|
|
145
|
+
try {
|
|
146
|
+
await bundle([sourceDirectory], tmpDir.path, declarations, {
|
|
147
|
+
featureFlags: {
|
|
148
|
+
edge_functions_produce_eszip: true,
|
|
149
|
+
},
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
catch (error) {
|
|
153
|
+
expect(error).toBeInstanceOf(BundleError);
|
|
154
|
+
expect(error.message).toEqual(`It seems like you're trying to import an npm module. This is only supported in Deno via CDNs like esm.sh. Have you tried 'import mod from "https://esm.sh/p-retry"'?`);
|
|
155
|
+
}
|
|
156
|
+
});
|
|
135
157
|
test('Does not add a custom error property to system errors during bundling', async () => {
|
|
136
158
|
expect.assertions(1);
|
|
137
159
|
try {
|
|
@@ -278,3 +300,32 @@ test('Ignores any user-defined `deno.json` files', async () => {
|
|
|
278
300
|
})).not.toThrow();
|
|
279
301
|
await deleteAsync([tmpDir.path, denoConfigPath, importMapFile.path], { force: true });
|
|
280
302
|
});
|
|
303
|
+
test('Processes a function that imports a custom layer', async () => {
|
|
304
|
+
const sourceDirectory = resolve(fixturesDir, 'with_layers', 'functions');
|
|
305
|
+
const tmpDir = await tmp.dir();
|
|
306
|
+
const declarations = [
|
|
307
|
+
{
|
|
308
|
+
function: 'func1',
|
|
309
|
+
path: '/func1',
|
|
310
|
+
},
|
|
311
|
+
];
|
|
312
|
+
const layer = { name: 'test', flag: 'edge-functions-layer-test' };
|
|
313
|
+
const result = await bundle([sourceDirectory], tmpDir.path, declarations, {
|
|
314
|
+
basePath: fixturesDir,
|
|
315
|
+
featureFlags: {
|
|
316
|
+
edge_functions_produce_eszip: true,
|
|
317
|
+
},
|
|
318
|
+
layers: [layer],
|
|
319
|
+
});
|
|
320
|
+
const generatedFiles = await fs.readdir(tmpDir.path);
|
|
321
|
+
expect(result.functions.length).toBe(1);
|
|
322
|
+
expect(generatedFiles.length).toBe(2);
|
|
323
|
+
const manifestFile = await fs.readFile(resolve(tmpDir.path, 'manifest.json'), 'utf8');
|
|
324
|
+
const manifest = JSON.parse(manifestFile);
|
|
325
|
+
const { bundles, layers } = manifest;
|
|
326
|
+
expect(bundles.length).toBe(1);
|
|
327
|
+
expect(bundles[0].format).toBe('eszip2');
|
|
328
|
+
expect(generatedFiles.includes(bundles[0].asset)).toBe(true);
|
|
329
|
+
expect(layers).toEqual([layer]);
|
|
330
|
+
await fs.rmdir(tmpDir.path, { recursive: true });
|
|
331
|
+
});
|
package/dist/node/config.js
CHANGED
|
@@ -12,6 +12,7 @@ var ConfigExitCode;
|
|
|
12
12
|
ConfigExitCode[ConfigExitCode["InvalidExport"] = 4] = "InvalidExport";
|
|
13
13
|
ConfigExitCode[ConfigExitCode["RuntimeError"] = 5] = "RuntimeError";
|
|
14
14
|
ConfigExitCode[ConfigExitCode["SerializationError"] = 6] = "SerializationError";
|
|
15
|
+
ConfigExitCode[ConfigExitCode["InvalidDefaultExport"] = 7] = "InvalidDefaultExport";
|
|
15
16
|
})(ConfigExitCode || (ConfigExitCode = {}));
|
|
16
17
|
const getConfigExtractor = () => {
|
|
17
18
|
const packagePath = getPackagePath();
|
|
@@ -82,6 +83,8 @@ const logConfigError = (func, exitCode, stderr, log) => {
|
|
|
82
83
|
case ConfigExitCode.SerializationError:
|
|
83
84
|
log.user(`'config' function in edge function at '${func.path}' must return an object with primitive values only`);
|
|
84
85
|
break;
|
|
86
|
+
case ConfigExitCode.InvalidDefaultExport:
|
|
87
|
+
throw new Error(`Default export in '${func.path}' must be a function. More on the Edge Functions API at https://ntl.fyi/edge-api.`);
|
|
85
88
|
default:
|
|
86
89
|
log.user(`Could not load configuration for edge function at '${func.path}'`);
|
|
87
90
|
log.user(stderr);
|
package/dist/node/config.test.js
CHANGED
|
@@ -15,6 +15,7 @@ const importMapFile = {
|
|
|
15
15
|
'alias:helper': pathToFileURL(join(fixturesDir, 'helper.ts')).toString(),
|
|
16
16
|
},
|
|
17
17
|
};
|
|
18
|
+
const invalidDefaultExportErr = (path) => `Default export in '${path}' must be a function. More on the Edge Functions API at https://ntl.fyi/edge-api.`;
|
|
18
19
|
test('`getFunctionConfig` extracts configuration properties from function file', async () => {
|
|
19
20
|
const { path: tmpDir } = await tmp.dir();
|
|
20
21
|
const deno = new DenoBridge({
|
|
@@ -184,3 +185,78 @@ test('Loads function paths from the in-source `config` function', async () => {
|
|
|
184
185
|
expect(postCacheRoutes[0]).toEqual({ function: 'user-func4', pattern: '^/user-func4/?$' });
|
|
185
186
|
await fs.rmdir(tmpDir.path, { recursive: true });
|
|
186
187
|
});
|
|
188
|
+
test('Passes validation if default export exists and is a function', async () => {
|
|
189
|
+
const { path: tmpDir } = await tmp.dir();
|
|
190
|
+
const deno = new DenoBridge({
|
|
191
|
+
cacheDirectory: tmpDir,
|
|
192
|
+
});
|
|
193
|
+
const func = {
|
|
194
|
+
name: 'func1',
|
|
195
|
+
source: `
|
|
196
|
+
const func = () => new Response("Hello world!")
|
|
197
|
+
export default func
|
|
198
|
+
`,
|
|
199
|
+
};
|
|
200
|
+
const logger = {
|
|
201
|
+
user: vi.fn().mockResolvedValue(null),
|
|
202
|
+
system: vi.fn().mockResolvedValue(null),
|
|
203
|
+
};
|
|
204
|
+
const path = join(tmpDir, `${func.name}.ts`);
|
|
205
|
+
await fs.writeFile(path, func.source);
|
|
206
|
+
expect(async () => {
|
|
207
|
+
await getFunctionConfig({
|
|
208
|
+
name: func.name,
|
|
209
|
+
path,
|
|
210
|
+
}, new ImportMap([importMapFile]), deno, logger);
|
|
211
|
+
}).not.toThrow();
|
|
212
|
+
await deleteAsync(tmpDir, { force: true });
|
|
213
|
+
});
|
|
214
|
+
test('Fails validation if default export is not function', async () => {
|
|
215
|
+
const { path: tmpDir } = await tmp.dir();
|
|
216
|
+
const deno = new DenoBridge({
|
|
217
|
+
cacheDirectory: tmpDir,
|
|
218
|
+
});
|
|
219
|
+
const func = {
|
|
220
|
+
name: 'func2',
|
|
221
|
+
source: `
|
|
222
|
+
const func = new Response("Hello world!")
|
|
223
|
+
export default func
|
|
224
|
+
`,
|
|
225
|
+
};
|
|
226
|
+
const logger = {
|
|
227
|
+
user: vi.fn().mockResolvedValue(null),
|
|
228
|
+
system: vi.fn().mockResolvedValue(null),
|
|
229
|
+
};
|
|
230
|
+
const path = join(tmpDir, `${func.name}.ts`);
|
|
231
|
+
await fs.writeFile(path, func.source);
|
|
232
|
+
const config = getFunctionConfig({
|
|
233
|
+
name: func.name,
|
|
234
|
+
path,
|
|
235
|
+
}, new ImportMap([importMapFile]), deno, logger);
|
|
236
|
+
await expect(config).rejects.toThrowError(invalidDefaultExportErr(path));
|
|
237
|
+
await deleteAsync(tmpDir, { force: true });
|
|
238
|
+
});
|
|
239
|
+
test('Fails validation if default export is not present', async () => {
|
|
240
|
+
const { path: tmpDir } = await tmp.dir();
|
|
241
|
+
const deno = new DenoBridge({
|
|
242
|
+
cacheDirectory: tmpDir,
|
|
243
|
+
});
|
|
244
|
+
const func = {
|
|
245
|
+
name: 'func3',
|
|
246
|
+
source: `
|
|
247
|
+
export const func = () => new Response("Hello world!")
|
|
248
|
+
`,
|
|
249
|
+
};
|
|
250
|
+
const logger = {
|
|
251
|
+
user: vi.fn().mockResolvedValue(null),
|
|
252
|
+
system: vi.fn().mockResolvedValue(null),
|
|
253
|
+
};
|
|
254
|
+
const path = join(tmpDir, `${func.name}.ts`);
|
|
255
|
+
await fs.writeFile(path, func.source);
|
|
256
|
+
const config = getFunctionConfig({
|
|
257
|
+
name: func.name,
|
|
258
|
+
path,
|
|
259
|
+
}, new ImportMap([importMapFile]), deno, logger);
|
|
260
|
+
await expect(config).rejects.toThrowError(invalidDefaultExportErr(path));
|
|
261
|
+
await deleteAsync(tmpDir, { force: true });
|
|
262
|
+
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { join } from 'path';
|
|
2
2
|
import { wrapBundleError } from '../bundle_error.js';
|
|
3
|
+
import { wrapNpmImportError } from '../npm_import_error.js';
|
|
3
4
|
import { getPackagePath } from '../package_json.js';
|
|
4
5
|
import { getFileHash } from '../utils/sha256.js';
|
|
5
6
|
const bundleESZIP = async ({ basePath, buildID, debug, deno, distDirectory, functions, importMap, }) => {
|
|
@@ -23,7 +24,7 @@ const bundleESZIP = async ({ basePath, buildID, debug, deno, distDirectory, func
|
|
|
23
24
|
await deno.run(['run', ...flags, bundler, JSON.stringify(payload)], { pipeOutput: true });
|
|
24
25
|
}
|
|
25
26
|
catch (error) {
|
|
26
|
-
throw wrapBundleError(error, { format: 'eszip' });
|
|
27
|
+
throw wrapBundleError(wrapNpmImportError(error), { format: 'eszip' });
|
|
27
28
|
}
|
|
28
29
|
const hash = await getFileHash(destPath);
|
|
29
30
|
return { extension, format: 'eszip2', hash };
|
|
@@ -4,8 +4,9 @@ import { env } from 'process';
|
|
|
4
4
|
import { pathToFileURL } from 'url';
|
|
5
5
|
import { deleteAsync } from 'del';
|
|
6
6
|
import { wrapBundleError } from '../bundle_error.js';
|
|
7
|
+
import { wrapNpmImportError } from '../npm_import_error.js';
|
|
7
8
|
import { getFileHash } from '../utils/sha256.js';
|
|
8
|
-
const BOOTSTRAP_LATEST = 'https://
|
|
9
|
+
const BOOTSTRAP_LATEST = 'https://63760359c1267a000900d7fb--edge.netlify.com/bootstrap/index-combined.ts';
|
|
9
10
|
const bundleJS = async ({ buildID, debug, deno, distDirectory, functions, importMap, }) => {
|
|
10
11
|
const stage2Path = await generateStage2({ distDirectory, functions, fileName: `${buildID}-pre.js` });
|
|
11
12
|
const extension = '.js';
|
|
@@ -18,7 +19,7 @@ const bundleJS = async ({ buildID, debug, deno, distDirectory, functions, import
|
|
|
18
19
|
await deno.run(['bundle', ...flags, stage2Path, jsBundlePath], { pipeOutput: true });
|
|
19
20
|
}
|
|
20
21
|
catch (error) {
|
|
21
|
-
throw wrapBundleError(error, { format: 'javascript' });
|
|
22
|
+
throw wrapBundleError(wrapNpmImportError(error), { format: 'javascript' });
|
|
22
23
|
}
|
|
23
24
|
await fs.unlink(stage2Path);
|
|
24
25
|
const hash = await getFileHash(jsBundlePath);
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
class NPMImportError extends Error {
|
|
2
|
+
constructor(originalError, moduleName) {
|
|
3
|
+
super(`It seems like you're trying to import an npm module. This is only supported in Deno via CDNs like esm.sh. Have you tried 'import mod from "https://esm.sh/${moduleName}"'?`);
|
|
4
|
+
this.name = 'NPMImportError';
|
|
5
|
+
this.stack = originalError.stack;
|
|
6
|
+
// https://github.com/microsoft/TypeScript-wiki/blob/8a66ecaf77118de456f7cd9c56848a40fe29b9b4/Breaking-Changes.md#implicit-any-error-raised-for-un-annotated-callback-arguments-with-no-matching-overload-arguments
|
|
7
|
+
Object.setPrototypeOf(this, NPMImportError.prototype);
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
const wrapNpmImportError = (input) => {
|
|
11
|
+
if (input instanceof Error) {
|
|
12
|
+
const match = input.message.match(/Relative import path "(.*)" not prefixed with/);
|
|
13
|
+
if (match !== null) {
|
|
14
|
+
const [, moduleName] = match;
|
|
15
|
+
return new NPMImportError(input, moduleName);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return input;
|
|
19
|
+
};
|
|
20
|
+
export { NPMImportError, wrapNpmImportError };
|