Skip to content

Commit b265ed6

Browse files
authored
feat: resolve external APIs in the local build (#211)
2 parents a44dbb0 + ff9b44d commit b265ed6

File tree

19 files changed

+148
-31
lines changed

19 files changed

+148
-31
lines changed

lib/build/bundlers/esbuild/index.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ class Esbuild extends BundlerBase {
5252

5353
if (useNodePolyfills) {
5454
if (!updatedConfig.plugins) updatedConfig.plugins = [];
55-
updatedConfig.plugins.push(ESBuildNodeModulePlugin());
55+
updatedConfig.plugins.push(ESBuildNodeModulePlugin(globalThis.buildProd));
5656
}
5757

5858
// inject content in worker initial code.

lib/build/bundlers/esbuild/plugins/node-polyfills/index.js

+25-2
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,11 @@ import path from 'path';
44
import PolyfillsManager from '../../../polyfills/polyfills-manager.js';
55

66
/**
7+
* @param {boolean} buildProd Parameter to identify whether the build is dev or prod
78
* ESBuild Node Module Plugin for polyfilling node modules.
89
* @returns {object} - ESBuild plugin object.
910
*/
10-
const ESBuildNodeModulePlugin = () => {
11+
const ESBuildNodeModulePlugin = (buildProd) => {
1112
const NAME = 'vulcan-node-modules-polyfills';
1213
const NAMESPACE = NAME;
1314

@@ -43,13 +44,30 @@ const ESBuildNodeModulePlugin = () => {
4344
});
4445
}
4546

47+
// external
48+
if (buildProd) {
49+
options.external = options.external || [];
50+
[...polyfillManager.external].forEach(([key]) => {
51+
options.external.push(key);
52+
options.external.push(`node:${key}`);
53+
});
54+
}
55+
4656
/**
4757
* Resolve callback for ESBuild.
4858
* @param {object} args - Arguments object.
4959
* @returns {object|undefined} - Object with path and namespace or undefined.
5060
*/
5161
build.onResolve({ filter: /.*/ }, async (args) => {
5262
const argsPath = args.path.replace(/^node:/, '');
63+
64+
if (!buildProd && polyfillManager.external.has(argsPath)) {
65+
return {
66+
path: args.path,
67+
namespace: NAMESPACE,
68+
};
69+
}
70+
5371
if (!polyfillManager.libs.has(argsPath)) {
5472
return;
5573
}
@@ -98,7 +116,12 @@ const ESBuildNodeModulePlugin = () => {
98116
build.onLoad({ filter: /.*/, namespace: NAMESPACE }, async (args) => {
99117
const argsPath = args.path.replace(/^node:/, '');
100118

101-
const resolved = polyfillManager.libs.get(argsPath);
119+
let resolved = polyfillManager.libs.get(argsPath);
120+
121+
if (!buildProd && polyfillManager.external.has(argsPath)) {
122+
resolved = polyfillManager.external.get(argsPath);
123+
}
124+
102125
const contents = await fs.promises.readFile(resolved, 'utf8');
103126
const resolveDir = path.dirname(resolved);
104127

lib/build/bundlers/polyfills/polyfills-manager.js

+28-3
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@ const polyfillsPath = `${libDirPath}/build/bundlers/polyfills`;
77
const nextNodePresetPath = `${libDirPath}/presets/custom/next/compute/node`;
88
const nodePolyfillsPath = `${polyfillsPath}/node`;
99

10+
/**
11+
* External polyfills are resolved in the build, but as they are for the local environment (Vulcan dev) they are located in #env/polyfills.
12+
*/
13+
const externalPolyfillsPath = `${libDirPath}/env/polyfills`;
14+
1015
/**
1116
* Manages and builds polyfills for Node and global browser environments.
1217
*/
@@ -21,6 +26,8 @@ class PolyfillsManager {
2126
this.libs = new Map();
2227
/** @type {Map<string, string|boolean>} */
2328
this.alias = new Map();
29+
/** @type {Map<string, string|boolean>} */
30+
this.external = new Map();
2431
}
2532

2633
/**
@@ -50,9 +57,18 @@ class PolyfillsManager {
5057
this.alias.set(name, path);
5158
}
5259

60+
/**
61+
* Sets a external libs.
62+
* @param {string} name - Name of the external.
63+
* @param {string|boolean} path - Path to the polyfill or a boolean value.
64+
*/
65+
setExternal(name, path) {
66+
this.external.set(name, path);
67+
}
68+
5369
/**
5470
* Builds and retrieves the polyfills for Node and globals.
55-
* @returns {{ libs: Map<string, string|boolean>, globals: Map<string, string>, alias: Map<string, string> }} - Object containing libs and globals.
71+
* @returns {{ libs: Map<string, string|boolean>, globals: Map<string, string>, alias: Map<string, string>, external: Map<string, string> }} - Object containing libs and globals.
5672
*/
5773
buildPolyfills() {
5874
this.setGlobal('buffer', `${nodePolyfillsPath}/globals/buffer.js`);
@@ -70,7 +86,6 @@ class PolyfillsManager {
7086
);
7187

7288
this.setLib('accepts', require.resolve('accepts'));
73-
this.setLib('async_hooks', `${nodePolyfillsPath}/_empty.js`);
7489
this.setLib('buffer', require.resolve('buffer/'));
7590
this.setLib('child_process', `${nodePolyfillsPath}/_empty.js`);
7691
this.setLib('cluster', `${nodePolyfillsPath}/_empty.js`);
@@ -131,7 +146,17 @@ class PolyfillsManager {
131146
this.setAlias('util', require.resolve('util/'));
132147
this.setAlias('process', `${nodePolyfillsPath}/globals/process.js`);
133148

134-
return { libs: this.libs, globals: this.globals, alias: this.alias };
149+
this.setExternal(
150+
'async_hooks',
151+
`${externalPolyfillsPath}/async_hooks/index.js`,
152+
);
153+
154+
return {
155+
libs: this.libs,
156+
globals: this.globals,
157+
alias: this.alias,
158+
external: this.external,
159+
};
135160
}
136161
}
137162

lib/build/bundlers/polyfills/polyfills-manager.test.js

-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ describe('Polyfills Manager', () => {
55
it('Should return map of polyfills', () => {
66
const expectedPolyfills = [
77
'accepts',
8-
'async_hooks',
98
'buffer',
109
'child_process',
1110
'cluster',

lib/build/bundlers/webpack/index.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ class Webpack extends BundlerBase {
8787
this.presetMode === 'compute';
8888

8989
if (useNodePolyfills) {
90-
updatedConfig.plugins.push(new NodePolyfillPlugin());
90+
updatedConfig.plugins.push(new NodePolyfillPlugin(globalThis.buildProd));
9191
}
9292
return updatedConfig;
9393
}

lib/build/bundlers/webpack/plugins/node-polyfills/index.js

+26
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@ import { generateWebpackBanner } from '#utils';
33
import PolyfillsManager from '../../../polyfills/polyfills-manager.js';
44

55
class NodePolyfillPlugin {
6+
constructor(buildProd) {
7+
this.buildProd = buildProd;
8+
}
9+
610
apply(compiler) {
711
const polyfillsManager = PolyfillsManager.buildPolyfills();
812

@@ -46,6 +50,28 @@ class NodePolyfillPlugin {
4650
}),
4751
);
4852

53+
if (this.buildProd) {
54+
compiler.options.externals = [
55+
// eslint-disable-next-line
56+
function ({ _, request }, callback) {
57+
[...polyfillsManager.external].map(([key]) => {
58+
const pattern = new RegExp(`${key}$`);
59+
if (pattern.test(request)) {
60+
return callback(null, `module ${request}`);
61+
}
62+
return callback();
63+
});
64+
},
65+
];
66+
} else {
67+
compiler.options.resolve.fallback = {
68+
...Object.fromEntries(
69+
[...polyfillsManager.external].map(([key, value]) => [key, value]),
70+
),
71+
...compiler.options.resolve.fallback,
72+
};
73+
}
74+
4975
compiler.options.resolve.alias = {
5076
...Object.fromEntries(
5177
[...polyfillsManager.alias].map(([key, value]) => [key, value]),

lib/build/dispatcher/dispatcher.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import {
1414
injectFilesInMem,
1515
} from '#utils';
1616
import { Messages } from '#constants';
17-
import { vulcan } from '#env';
17+
import vulcan from '../../env/vulcan.env.js';
1818
import {
1919
getAliasPath,
2020
createDotEnvFile,
@@ -378,6 +378,7 @@ class Dispatcher {
378378

379379
await vulcan.createVulcanEnv(
380380
{
381+
builder: builderSelected,
381382
entry: originalEntry, // original entry
382383
preset: buildConfig.preset.name,
383384
mode: buildConfig.preset.mode,

lib/commands/build.commands.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Commands } from '#namespaces';
22
import { feedback } from '#utils';
3-
import { vulcan } from '#env';
3+
import vulcan from '../env/vulcan.env.js';
44

55
/**
66
* Retrieves a configuration value based on priority.

lib/constants/messages/env.messages.js

+1
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ const env = {
3535
},
3636
errors: {
3737
load_worker_failed: (path) => `Failed load worker: ${path}`,
38+
run_build_command: 'Run the build command before running your project',
3839
},
3940
},
4041
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
/* eslint-disable */
2+
import * as async_hooks from 'async_hooks';
3+
4+
export class AsyncLocalStorage extends async_hooks.AsyncLocalStorage {}
5+
export class AsyncResource extends async_hooks.AsyncResource {}
6+
7+
export default {
8+
AsyncLocalStorage,
9+
AsyncResource,
10+
};
+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
/* eslint-disable */
2+
/**
3+
* ASYNC_LOCAL_STORAGE is defined in runtime.env.js for use on the local server
4+
*/
5+
6+
export class AsyncLocalStorage extends ASYNC_LOCAL_STORAGE.AsyncLocalStorage {}
7+
export class AsyncResource extends ASYNC_LOCAL_STORAGE.AsyncResource {}
8+
9+
export default {
10+
AsyncLocalStorage,
11+
AsyncResource,
12+
};

lib/env/polyfills/index.js

+4-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import fetchPolyfill from './fetch.polyfills.js';
2-
import FetchEventPolyfill from './FetchEvent.polyfills.js';
1+
import fetchPolyfill from './fetch/fetch.polyfills.js';
2+
import FetchEventPolyfill from './fetch/FetchEvent.polyfills.js';
3+
import AsyncHooks from './async_hooks/context/index.js';
34

4-
export { fetchPolyfill, FetchEventPolyfill };
5+
export { fetchPolyfill, FetchEventPolyfill, AsyncHooks };

lib/env/runtime.env.js

+8-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import { EdgeRuntime } from 'edge-runtime';
22

3-
import { fetchPolyfill, FetchEventPolyfill } from './polyfills/index.js';
3+
import {
4+
fetchPolyfill,
5+
FetchEventPolyfill,
6+
AsyncHooks,
7+
} from './polyfills/index.js';
48

59
/**
610
* Executes the specified JavaScript code within a sandbox environment,
@@ -56,6 +60,9 @@ function runtime(code) {
5660
context.WebSocket = undefined;
5761
/* ========================================================== */
5862

63+
// Async Hooks
64+
context.ASYNC_LOCAL_STORAGE = AsyncHooks;
65+
5966
return context;
6067
};
6168

lib/env/server.env.js

+20-17
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
import { debug, readWorkerFile, feedback, exec } from '#utils';
1+
import { debug, readWorkerFile, feedback } from '#utils';
22
import { Messages } from '#constants';
33
import { runServer, EdgeRuntime } from 'edge-runtime';
44
import chokidar from 'chokidar';
55
import runtime from './runtime.env.js';
66
import vulcan from './vulcan.env.js';
7+
import buildCommand from '../commands/build.commands.js';
78

89
let currentServer;
910
let isChangeHandlerRunning = false;
@@ -37,13 +38,30 @@ async function initializeServer(port, workerCode) {
3738
return runServer({ port, host: '0.0.0.0', runtime: execution });
3839
}
3940

41+
/**
42+
* Build to Local Server with polyfill external
43+
*/
44+
async function buildToLocalServer() {
45+
const vulcanEnv = await vulcan.readVulcanEnv('local');
46+
47+
if (!vulcanEnv) {
48+
const msg = Messages.env.server.errors.run_build_command;
49+
feedback.server.error(msg);
50+
throw new Error(msg);
51+
}
52+
globalThis.buildProd = false;
53+
await buildCommand({});
54+
}
55+
4056
/**
4157
* Handle server operations: start, restart.
4258
* @param {string} workerPath - Path to the worker file.
4359
* @param {number} port - The port number.
4460
*/
4561
async function manageServer(workerPath, port) {
4662
try {
63+
await buildToLocalServer();
64+
4765
const workerCode = await readWorkerCode(workerPath);
4866

4967
if (currentServer) {
@@ -90,23 +108,8 @@ async function handleFileChange(path, workerPath, port) {
90108
return;
91109
}
92110

93-
const { entry, preset, mode, useNodePolyfills, useOwnWorker } =
94-
await vulcan.readVulcanEnv('local');
95-
96-
let command = `vulcan build --entry ${entry} --preset ${preset} --mode ${mode}`;
97-
98-
if (useNodePolyfills) {
99-
command += ` --useNodePolyfills ${useNodePolyfills}`;
100-
}
101-
102-
if (useOwnWorker) {
103-
command += ` --useOwnWorker ${useOwnWorker}`;
104-
}
105-
106-
feedback.build.info(Messages.build.info.rebuilding);
107-
108111
try {
109-
await exec(command);
112+
feedback.build.info(Messages.build.info.rebuilding);
110113
await manageServer(workerPath, port);
111114
} catch (error) {
112115
debug.error(`Build or server restart failed: ${error}`);

lib/main.js

+4
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ const vulcanVersion = vulcanPackageJSON.version;
1717

1818
const debugEnabled = process.env.DEBUG === 'true';
1919

20+
// This global variable is to define whether the build is for prod or dev. This is to handle external modules.
21+
globalThis.buildProd = true;
22+
2023
const program = new Command();
2124

2225
/**
@@ -88,6 +91,7 @@ function startVulcanProgram() {
8891
)
8992
.action(async (options) => {
9093
const { buildCommand } = await import('#commands');
94+
globalThis.buildProd = true;
9195
await buildCommand(options);
9296
});
9397

lib/utils/manifest/manifest.utils.js

+5
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,11 @@ class Manifest {
116116
JSON.stringify(this.manifest, null, 2),
117117
'utf8',
118118
);
119+
// clear manifest to reload server
120+
this.manifest = {
121+
routes: [],
122+
fs: [],
123+
};
119124
}
120125
}
121126

0 commit comments

Comments
 (0)