Skip to content

Commit 3e2dad8

Browse files
Commit to AsyncLocalStorage (#216)
* commit to use of AsyncLocalStorage and use it to store the env variables resolves #121 * remove NODE_ENV from generateGlobalJs and move it to esbuild's define field * run prettier:fix * rename NODE_ENV to __NODE_ENV__ * remove globalJs replaces as suggested * fix unit tests
1 parent b33639f commit 3e2dad8

File tree

7 files changed

+139
-89
lines changed

7 files changed

+139
-89
lines changed

.changeset/hot-eagles-love.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
'@cloudflare/next-on-pages': major
3+
---
4+
5+
use and rely on AsyncLocalStorage
6+
7+
previously we've been using the node AsyncLocalStorage in a non-breaking way but now we are committing
8+
to in and using it to store the global env variables as well
9+
10+
this is a breaking change since moving forward all app running using @cloudflare/next-on-pages must
11+
have the nodejs_compat compatibility flag set (before not all needed that)

src/buildApplication/buildWorkerFile.ts

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { tmpdir } from 'os';
66
import { cliSuccess } from '../cli';
77
import { generateGlobalJs } from './generateGlobalJs';
88
import type { ProcessedVercelOutput } from './processVercelOutput';
9+
import { getNodeEnv } from '../utils/getNodeEnv';
910

1011
/**
1112
* Construct a record for the build output map.
@@ -28,10 +29,10 @@ export function constructBuildOutputRecord(item: BuildOutputItem) {
2829

2930
return `{
3031
type: ${JSON.stringify(item.type)},
31-
entrypoint: AsyncLocalStoragePromise.then(() => import('${item.entrypoint.replace(
32+
entrypoint: import('${item.entrypoint.replace(
3233
/^\.vercel\/output\/static\/_worker\.js\/__next-on-pages-dist__\//,
3334
'./__next-on-pages-dist__/'
34-
)}')),
35+
)}')
3536
}`;
3637
}
3738

@@ -46,12 +47,7 @@ export async function buildWorkerFile(
4647

4748
await writeFile(
4849
functionsFile,
49-
`
50-
export const AsyncLocalStoragePromise = import('node:async_hooks').then(({ AsyncLocalStorage }) => {
51-
globalThis.AsyncLocalStorage = AsyncLocalStorage;
52-
}).catch(() => undefined);
53-
54-
export const __BUILD_OUTPUT__ = {${[...vercelOutput.entries()]
50+
`export const __BUILD_OUTPUT__ = {${[...vercelOutput.entries()]
5551
.map(([name, item]) => `"${name}": ${constructBuildOutputRecord(item)}`)
5652
.join(',')}};`
5753
);
@@ -73,9 +69,10 @@ export async function buildWorkerFile(
7369
inject: [functionsFile],
7470
target: 'es2022',
7571
platform: 'neutral',
76-
external: ['node:async_hooks', 'node:buffer', './__next-on-pages-dist__/*'],
72+
external: ['node:*', './__next-on-pages-dist__/*'],
7773
define: {
7874
__CONFIG__: JSON.stringify(vercelConfig),
75+
__NODE_ENV__: JSON.stringify(getNodeEnv()),
7976
},
8077
outfile: outputFile,
8178
minify,
Lines changed: 13 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,23 @@
1-
import { cliWarn } from '../cli';
2-
31
/**
42
* Generates the javascript content (as a plain string) that deals with the global scope that needs to be
53
* added to the built worker.
64
*
75
* @returns the plain javascript string that should be added at the top of the the _worker.js file
86
*/
97
export function generateGlobalJs(): string {
10-
const nodeEnv = getNodeEnv();
11-
return `globalThis.process = { env: { NODE_ENV: '${nodeEnv}' } };`;
12-
}
13-
14-
enum NextJsNodeEnv {
15-
PRODUCTION = 'production',
16-
DEVELOPMENT = 'development',
17-
TEST = 'test',
18-
}
19-
20-
function getNodeEnv(): string {
21-
const processNodeEnv = process.env.NODE_ENV;
22-
23-
if (!processNodeEnv) {
24-
return NextJsNodeEnv.PRODUCTION;
25-
}
8+
return `
9+
import { AsyncLocalStorage } from 'node:async_hooks';
10+
globalThis.AsyncLocalStorage = AsyncLocalStorage;
2611
27-
const nextJsNodeEnvs = Object.values(NextJsNodeEnv);
28-
if (!(nextJsNodeEnvs as string[]).includes(processNodeEnv)) {
29-
cliWarn(
30-
`
31-
WARNING:
32-
The current value of the environment variable NODE_ENV is "${processNodeEnv}",
33-
but the supported values are: ${nextJsNodeEnvs
34-
.map(env => `"${env}"`)
35-
.join(', ')}.
36-
See: https://nextjs.org/docs/basic-features/environment-variables.
37-
`,
38-
{ spaced: true }
39-
);
40-
}
12+
const __ENV_ALS__ = new AsyncLocalStorage();
4113
42-
return processNodeEnv;
14+
globalThis.process = {
15+
env: new Proxy(
16+
{},
17+
{
18+
get: (_, property) => Reflect.get(__ENV_ALS__.getStore(), property),
19+
set: (_, property, value) => Reflect.set(__ENV_ALS__.getStore(), property, value),
20+
}),
21+
};
22+
`;
4323
}

src/utils/getNodeEnv.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { cliWarn } from '../cli';
2+
3+
enum NextJsNodeEnv {
4+
PRODUCTION = 'production',
5+
DEVELOPMENT = 'development',
6+
TEST = 'test',
7+
}
8+
9+
export function getNodeEnv(): string {
10+
const processNodeEnv = process.env.NODE_ENV;
11+
12+
if (!processNodeEnv) {
13+
return NextJsNodeEnv.PRODUCTION;
14+
}
15+
16+
const nextJsNodeEnvs = Object.values(NextJsNodeEnv);
17+
if (!(nextJsNodeEnvs as string[]).includes(processNodeEnv)) {
18+
cliWarn(
19+
`
20+
WARNING:
21+
The current value of the environment variable NODE_ENV is "${processNodeEnv}",
22+
but the supported values are: ${nextJsNodeEnvs
23+
.map(env => `"${env}"`)
24+
.join(', ')}.
25+
See: https://nextjs.org/docs/basic-features/environment-variables.
26+
`,
27+
{ spaced: true }
28+
);
29+
}
30+
31+
return processNodeEnv;
32+
}

templates/_worker.js/index.ts

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,31 @@
11
import { handleRequest } from './handleRequest';
22
import { adjustRequestForVercel } from './utils';
3+
import type { AsyncLocalStorage } from 'node:async_hooks';
4+
5+
declare const __NODE_ENV__: string;
36

47
declare const __CONFIG__: ProcessedVercelConfig;
58

69
declare const __BUILD_OUTPUT__: VercelBuildOutput;
710

11+
declare const __ENV_ALS__: AsyncLocalStorage<unknown> & {
12+
NODE_ENV: string;
13+
};
14+
815
export default {
916
async fetch(request, env, ctx) {
10-
globalThis.process.env = { ...globalThis.process.env, ...env };
11-
12-
const adjustedRequest = adjustRequestForVercel(request);
17+
return __ENV_ALS__.run({ ...env, NODE_ENV: __NODE_ENV__ }, () => {
18+
const adjustedRequest = adjustRequestForVercel(request);
1319

14-
return handleRequest(
15-
{
16-
request: adjustedRequest,
17-
ctx,
18-
assetsFetcher: env.ASSETS,
19-
},
20-
__CONFIG__,
21-
__BUILD_OUTPUT__
22-
);
20+
return handleRequest(
21+
{
22+
request: adjustedRequest,
23+
ctx,
24+
assetsFetcher: env.ASSETS,
25+
},
26+
__CONFIG__,
27+
__BUILD_OUTPUT__
28+
);
29+
});
2330
},
2431
} as ExportedHandler<{ ASSETS: Fetcher }>;
Lines changed: 19 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,28 @@
1-
import { describe, test, expect, vi } from 'vitest';
1+
import { describe, test, expect } from 'vitest';
22
import { generateGlobalJs } from '../../../src/buildApplication/generateGlobalJs';
33

4-
describe('generateGlobalContext', async () => {
5-
test('should default NODE_ENV to "production"', async () => {
6-
runWithNodeEnv('', () => {
4+
describe('generateGlobalJs', async () => {
5+
describe('AsyncLocalStorage', () => {
6+
test('should make the AsyncLocalStorage globally available', async () => {
77
const globalJs = generateGlobalJs();
8-
const expected =
9-
"globalThis.process = { env: { NODE_ENV: 'production' } };";
10-
expect(globalJs).toBe(expected);
8+
expect(globalJs).toContain(
9+
'globalThis.AsyncLocalStorage = AsyncLocalStorage'
10+
);
1111
});
12-
});
13-
14-
['production', 'development', 'test'].forEach(testNodeEnv =>
15-
test(`should set the NODE_ENV to ${testNodeEnv} correctly`, async () => {
16-
runWithNodeEnv(testNodeEnv, () => {
17-
const globalJs = generateGlobalJs();
18-
const expected = `globalThis.process = { env: { NODE_ENV: '${testNodeEnv}' } };`;
19-
expect(globalJs).toBe(expected);
20-
});
21-
})
22-
);
2312

24-
test('should set the NODE_ENV to a non-Next.js value correctly but generate a warning', async () => {
25-
runWithNodeEnv('non-next-value', () => {
26-
const spy = vi.spyOn(console, 'warn').mockImplementation(() => null);
13+
test('create an AsyncLocalStorage and set it as a proxy to process.env', async () => {
2714
const globalJs = generateGlobalJs();
28-
const expected =
29-
"globalThis.process = { env: { NODE_ENV: 'non-next-value' } };";
30-
expect(globalJs).toBe(expected);
31-
expect(spy).toHaveBeenCalledWith(expect.stringContaining('WARNING:'));
15+
expect(globalJs).toContain('const __ENV_ALS__ = new AsyncLocalStorage()');
16+
17+
const proxyRegexMatch = globalJs.match(
18+
/globalThis.process = {[\S\s]*Proxy\(([\s\S]+)\)[\s\S]+}/
19+
);
20+
21+
expect(proxyRegexMatch?.length).toBe(2);
22+
23+
const proxyBody = proxyRegexMatch?.[1];
24+
expect(proxyBody).toContain('Reflect.get(__ENV_ALS__.getStore()');
25+
expect(proxyBody).toContain('Reflect.set(__ENV_ALS__.getStore()');
3226
});
3327
});
3428
});
35-
36-
function runWithNodeEnv<F extends (...args: unknown[]) => void>(
37-
value: string,
38-
testFn: F
39-
): void {
40-
const oldNodeEnv = process.env.NODE_ENV;
41-
process.env.NODE_ENV = value;
42-
testFn();
43-
process.env.NODE_ENV = oldNodeEnv;
44-
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { describe, test, expect, vi } from 'vitest';
2+
import { getNodeEnv } from '../../../src/utils/getNodeEnv';
3+
4+
describe('getNodeEnv', () => {
5+
test('should default NODE_ENV to "production"', async () => {
6+
runWithNodeEnv('', () => {
7+
const nodeEnv = getNodeEnv();
8+
expect(nodeEnv).toBe('production');
9+
});
10+
});
11+
12+
['production', 'development', 'test'].forEach(testNodeEnv =>
13+
test(`should set the NODE_ENV to ${testNodeEnv} correctly`, async () => {
14+
runWithNodeEnv(testNodeEnv, () => {
15+
const nodeEnv = getNodeEnv();
16+
expect(nodeEnv).toBe(testNodeEnv);
17+
});
18+
})
19+
);
20+
21+
test('should set the NODE_ENV to a non-Next.js value correctly but generate a warning', async () => {
22+
runWithNodeEnv('non-next-value', () => {
23+
const spy = vi.spyOn(console, 'warn').mockImplementation(() => null);
24+
const nodeEnv = getNodeEnv();
25+
expect(nodeEnv).toBe('non-next-value');
26+
expect(spy).toHaveBeenCalledWith(expect.stringContaining('WARNING:'));
27+
});
28+
});
29+
});
30+
31+
function runWithNodeEnv<F extends (...args: unknown[]) => void>(
32+
value: string,
33+
testFn: F
34+
): void {
35+
const oldNodeEnv = process.env.NODE_ENV;
36+
process.env.NODE_ENV = value;
37+
testFn();
38+
process.env.NODE_ENV = oldNodeEnv;
39+
}

0 commit comments

Comments
 (0)