Skip to content
This repository was archived by the owner on Mar 20, 2025. It is now read-only.

Commit aeb76d3

Browse files
feat: add rootPath for monorepo setups (#521)
* feat: add `rootPath` for monorepo setups * chore: remove serve folder * chore: update test * chore: stop cleaning up in CI
1 parent 708a901 commit aeb76d3

File tree

27 files changed

+344
-16
lines changed

27 files changed

+344
-16
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
npm-debug.log
44
node_modules
55
!test/fixtures/**/node_modules
6+
**/.netlify/edge-functions-serve
67
/core
78
.eslintcache
89
.npmrc

node/bundler.test.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -472,6 +472,44 @@ test('Loads npm modules from bare specifiers', async () => {
472472
await rm(vendorDirectory.path, { force: true, recursive: true })
473473
})
474474

475+
test('Loads npm modules in a monorepo setup', async () => {
476+
const systemLogger = vi.fn()
477+
const { basePath: rootPath, cleanup, distPath } = await useFixture('monorepo_npm_module')
478+
const basePath = join(rootPath, 'packages', 'frontend')
479+
const sourceDirectory = join(basePath, 'functions')
480+
const declarations: Declaration[] = [
481+
{
482+
function: 'func1',
483+
path: '/func1',
484+
},
485+
]
486+
const vendorDirectory = await tmp.dir()
487+
488+
await bundle([sourceDirectory], distPath, declarations, {
489+
basePath,
490+
importMapPaths: [join(basePath, 'import_map.json')],
491+
rootPath,
492+
vendorDirectory: vendorDirectory.path,
493+
systemLogger,
494+
})
495+
496+
expect(
497+
systemLogger.mock.calls.find((call) => call[0] === 'Could not track dependencies in edge function:'),
498+
).toBeUndefined()
499+
500+
const manifestFile = await readFile(resolve(distPath, 'manifest.json'), 'utf8')
501+
const manifest = JSON.parse(manifestFile)
502+
const bundlePath = join(distPath, manifest.bundles[0].asset)
503+
const { func1 } = await runESZIP(bundlePath, vendorDirectory.path)
504+
505+
expect(func1).toBe(
506+
`<parent-1><child-1>JavaScript</child-1></parent-1>, <parent-2><child-2><grandchild-1>APIs<cwd>${process.cwd()}</cwd></grandchild-1></child-2></parent-2>, <parent-3><child-2><grandchild-1>Markup<cwd>${process.cwd()}</cwd></grandchild-1></child-2></parent-3>`,
507+
)
508+
509+
await cleanup()
510+
await rm(vendorDirectory.path, { force: true, recursive: true })
511+
})
512+
475513
test('Loads JSON modules', async () => {
476514
const { basePath, cleanup, distPath } = await useFixture('imports_json')
477515
const sourceDirectory = join(basePath, 'functions')

node/bundler.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ export interface BundleOptions {
3333
internalSrcFolder?: string
3434
onAfterDownload?: OnAfterDownloadHook
3535
onBeforeDownload?: OnBeforeDownloadHook
36+
rootPath?: string
3637
systemLogger?: LogFunction
3738
userLogger?: LogFunction
3839
vendorDirectory?: string
@@ -53,6 +54,7 @@ export const bundle = async (
5354
internalSrcFolder,
5455
onAfterDownload,
5556
onBeforeDownload,
57+
rootPath,
5658
userLogger,
5759
systemLogger,
5860
vendorDirectory,
@@ -105,6 +107,7 @@ export const bundle = async (
105107
functions,
106108
importMap,
107109
logger,
110+
rootPath: rootPath ?? basePath,
108111
vendorDirectory,
109112
})
110113

@@ -250,6 +253,7 @@ interface VendorNPMOptions {
250253
functions: EdgeFunction[]
251254
importMap: ImportMap
252255
logger: Logger
256+
rootPath: string
253257
vendorDirectory: string | undefined
254258
}
255259

@@ -258,6 +262,7 @@ const safelyVendorNPMSpecifiers = async ({
258262
functions,
259263
importMap,
260264
logger,
265+
rootPath,
261266
vendorDirectory,
262267
}: VendorNPMOptions) => {
263268
try {
@@ -268,6 +273,7 @@ const safelyVendorNPMSpecifiers = async ({
268273
importMap,
269274
logger,
270275
referenceTypes: false,
276+
rootPath,
271277
})
272278
} catch (error) {
273279
logger.system(error)

node/npm_dependencies.ts

Lines changed: 25 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import tmp from 'tmp-promise'
1212

1313
import { ImportMap } from './import_map.js'
1414
import { Logger } from './logger.js'
15+
import { pathsBetween } from './utils/fs.js'
1516

1617
const TYPESCRIPT_EXTENSIONS = new Set(['.ts', '.cts', '.mts'])
1718

@@ -89,24 +90,29 @@ const banner = {
8990
`,
9091
}
9192

93+
interface GetNPMSpecifiersOptions {
94+
basePath: string
95+
functions: string[]
96+
importMap: ParsedImportMap
97+
referenceTypes: boolean
98+
rootPath: string
99+
}
100+
92101
/**
93102
* Parses a set of functions and returns a list of specifiers that correspond
94103
* to npm modules.
95-
*
96-
* @param basePath Root of the project
97-
* @param functions Functions to parse
98-
* @param importMap Import map to apply when resolving imports
99-
* @param referenceTypes Whether to detect typescript declarations and reference them in the output
100104
*/
101-
const getNPMSpecifiers = async (
102-
basePath: string,
103-
functions: string[],
104-
importMap: ParsedImportMap,
105-
referenceTypes: boolean,
106-
) => {
105+
const getNPMSpecifiers = async ({
106+
basePath,
107+
functions,
108+
importMap,
109+
referenceTypes,
110+
rootPath,
111+
}: GetNPMSpecifiersOptions) => {
107112
const baseURL = pathToFileURL(basePath)
108113
const { reasons } = await nodeFileTrace(functions, {
109-
base: basePath,
114+
base: rootPath,
115+
processCwd: basePath,
110116
readFile: async (filePath: string) => {
111117
// If this is a TypeScript file, we need to compile in before we can
112118
// parse it.
@@ -203,6 +209,7 @@ interface VendorNPMSpecifiersOptions {
203209
importMap: ImportMap
204210
logger: Logger
205211
referenceTypes: boolean
212+
rootPath?: string
206213
}
207214

208215
export const vendorNPMSpecifiers = async ({
@@ -211,24 +218,26 @@ export const vendorNPMSpecifiers = async ({
211218
functions,
212219
importMap,
213220
referenceTypes,
221+
rootPath = basePath,
214222
}: VendorNPMSpecifiersOptions) => {
215223
// The directories that esbuild will use when resolving Node modules. We must
216224
// set these manually because esbuild will be operating from a temporary
217225
// directory that will not live inside the project root, so the normal
218226
// resolution logic won't work.
219-
const nodePaths = [path.join(basePath, 'node_modules')]
227+
const nodePaths = pathsBetween(basePath, rootPath).map((directory) => path.join(directory, 'node_modules'))
220228

221229
// We need to create some files on disk, which we don't want to write to the
222230
// project directory. If a custom directory has been specified, we use it.
223231
// Otherwise, create a random temporary directory.
224232
const temporaryDirectory = directory ? { path: directory } : await tmp.dir()
225233

226-
const { npmSpecifiers, npmSpecifiersWithExtraneousFiles } = await getNPMSpecifiers(
234+
const { npmSpecifiers, npmSpecifiersWithExtraneousFiles } = await getNPMSpecifiers({
227235
basePath,
228236
functions,
229-
importMap.getContentsWithURLObjects(),
237+
importMap: importMap.getContentsWithURLObjects(),
230238
referenceTypes,
231-
)
239+
rootPath,
240+
})
232241

233242
// If we found no specifiers, there's nothing left to do here.
234243
if (Object.keys(npmSpecifiers).length === 0) {

node/server/server.test.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { readFile } from 'fs/promises'
22
import { join } from 'path'
3+
import process from 'process'
34

45
import getPort from 'get-port'
56
import fetch from 'node-fetch'
@@ -105,3 +106,65 @@ test('Starts a server and serves requests for edge functions', async () => {
105106
`/// <reference types="${join('..', '..', 'node_modules', '@types', 'pt-committee__identidade', 'index.d.ts')}" />`,
106107
)
107108
})
109+
110+
test('Serves edge functions in a monorepo setup', async () => {
111+
const rootPath = join(fixturesDir, 'monorepo_npm_module')
112+
const basePath = join(rootPath, 'packages', 'frontend')
113+
const paths = {
114+
user: join(basePath, 'functions'),
115+
}
116+
const port = await getPort()
117+
const importMapPaths = [join(basePath, 'import_map.json')]
118+
const servePath = join(basePath, '.netlify', 'edge-functions-serve')
119+
const server = await serve({
120+
basePath,
121+
bootstrapURL: 'https://edge.netlify.com/bootstrap/index-combined.ts',
122+
importMapPaths,
123+
port,
124+
rootPath,
125+
servePath,
126+
})
127+
128+
const functions = [
129+
{
130+
name: 'func1',
131+
path: join(paths.user, 'func1.ts'),
132+
},
133+
]
134+
const options = {
135+
getFunctionsConfig: true,
136+
}
137+
138+
const { features, functionsConfig, graph, success, npmSpecifiersWithExtraneousFiles } = await server(
139+
functions,
140+
{
141+
very_secret_secret: 'i love netlify',
142+
},
143+
options,
144+
)
145+
expect(features).toEqual({ npmModules: true })
146+
expect(success).toBe(true)
147+
expect(functionsConfig).toEqual([{ path: '/func1' }])
148+
expect(npmSpecifiersWithExtraneousFiles).toEqual(['child-1'])
149+
150+
for (const key in functions) {
151+
const graphEntry = graph?.modules.some(
152+
// @ts-expect-error TODO: Module graph is currently not typed
153+
({ kind, mediaType, local }) => kind === 'esm' && mediaType === 'TypeScript' && local === functions[key].path,
154+
)
155+
156+
expect(graphEntry).toBe(true)
157+
}
158+
159+
const response1 = await fetch(`http://0.0.0.0:${port}/func1`, {
160+
headers: {
161+
'x-nf-edge-functions': 'func1',
162+
'x-ef-passthrough': 'passthrough',
163+
'X-NF-Request-ID': uuidv4(),
164+
},
165+
})
166+
expect(response1.status).toBe(200)
167+
expect(await response1.text()).toBe(
168+
`<parent-1><child-1>JavaScript</child-1></parent-1>, <parent-2><child-2><grandchild-1>APIs<cwd>${process.cwd()}</cwd></grandchild-1></child-2></parent-2>, <parent-3><child-2><grandchild-1>Markup<cwd>${process.cwd()}</cwd></grandchild-1></child-2></parent-3>`,
169+
)
170+
})

0 commit comments

Comments
 (0)