Skip to content

Commit

Permalink
feat: add rootPath for monorepo setups (#521)
Browse files Browse the repository at this point in the history
* feat: add `rootPath` for monorepo setups

* chore: remove serve folder

* chore: update test

* chore: stop cleaning up in CI
  • Loading branch information
eduardoboucas committed Nov 5, 2023
1 parent 708a901 commit aeb76d3
Show file tree
Hide file tree
Showing 27 changed files with 344 additions and 16 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
npm-debug.log
node_modules
!test/fixtures/**/node_modules
**/.netlify/edge-functions-serve
/core
.eslintcache
.npmrc
Expand Down
38 changes: 38 additions & 0 deletions node/bundler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -472,6 +472,44 @@ test('Loads npm modules from bare specifiers', async () => {
await rm(vendorDirectory.path, { force: true, recursive: true })
})

test('Loads npm modules in a monorepo setup', async () => {
const systemLogger = vi.fn()
const { basePath: rootPath, cleanup, distPath } = await useFixture('monorepo_npm_module')
const basePath = join(rootPath, 'packages', 'frontend')
const sourceDirectory = join(basePath, 'functions')
const declarations: Declaration[] = [
{
function: 'func1',
path: '/func1',
},
]
const vendorDirectory = await tmp.dir()

await bundle([sourceDirectory], distPath, declarations, {
basePath,
importMapPaths: [join(basePath, 'import_map.json')],
rootPath,
vendorDirectory: vendorDirectory.path,
systemLogger,
})

expect(
systemLogger.mock.calls.find((call) => call[0] === 'Could not track dependencies in edge function:'),
).toBeUndefined()

const manifestFile = await readFile(resolve(distPath, 'manifest.json'), 'utf8')
const manifest = JSON.parse(manifestFile)
const bundlePath = join(distPath, manifest.bundles[0].asset)
const { func1 } = await runESZIP(bundlePath, vendorDirectory.path)

expect(func1).toBe(
`<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>`,
)

await cleanup()
await rm(vendorDirectory.path, { force: true, recursive: true })
})

test('Loads JSON modules', async () => {
const { basePath, cleanup, distPath } = await useFixture('imports_json')
const sourceDirectory = join(basePath, 'functions')
Expand Down
6 changes: 6 additions & 0 deletions node/bundler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export interface BundleOptions {
internalSrcFolder?: string
onAfterDownload?: OnAfterDownloadHook
onBeforeDownload?: OnBeforeDownloadHook
rootPath?: string
systemLogger?: LogFunction
userLogger?: LogFunction
vendorDirectory?: string
Expand All @@ -53,6 +54,7 @@ export const bundle = async (
internalSrcFolder,
onAfterDownload,
onBeforeDownload,
rootPath,
userLogger,
systemLogger,
vendorDirectory,
Expand Down Expand Up @@ -105,6 +107,7 @@ export const bundle = async (
functions,
importMap,
logger,
rootPath: rootPath ?? basePath,
vendorDirectory,
})

Expand Down Expand Up @@ -250,6 +253,7 @@ interface VendorNPMOptions {
functions: EdgeFunction[]
importMap: ImportMap
logger: Logger
rootPath: string
vendorDirectory: string | undefined
}

Expand All @@ -258,6 +262,7 @@ const safelyVendorNPMSpecifiers = async ({
functions,
importMap,
logger,
rootPath,
vendorDirectory,
}: VendorNPMOptions) => {
try {
Expand All @@ -268,6 +273,7 @@ const safelyVendorNPMSpecifiers = async ({
importMap,
logger,
referenceTypes: false,
rootPath,
})
} catch (error) {
logger.system(error)
Expand Down
41 changes: 25 additions & 16 deletions node/npm_dependencies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import tmp from 'tmp-promise'

import { ImportMap } from './import_map.js'
import { Logger } from './logger.js'
import { pathsBetween } from './utils/fs.js'

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

Expand Down Expand Up @@ -89,24 +90,29 @@ const banner = {
`,
}

interface GetNPMSpecifiersOptions {
basePath: string
functions: string[]
importMap: ParsedImportMap
referenceTypes: boolean
rootPath: string
}

/**
* Parses a set of functions and returns a list of specifiers that correspond
* to npm modules.
*
* @param basePath Root of the project
* @param functions Functions to parse
* @param importMap Import map to apply when resolving imports
* @param referenceTypes Whether to detect typescript declarations and reference them in the output
*/
const getNPMSpecifiers = async (
basePath: string,
functions: string[],
importMap: ParsedImportMap,
referenceTypes: boolean,
) => {
const getNPMSpecifiers = async ({
basePath,
functions,
importMap,
referenceTypes,
rootPath,
}: GetNPMSpecifiersOptions) => {
const baseURL = pathToFileURL(basePath)
const { reasons } = await nodeFileTrace(functions, {
base: basePath,
base: rootPath,
processCwd: basePath,
readFile: async (filePath: string) => {
// If this is a TypeScript file, we need to compile in before we can
// parse it.
Expand Down Expand Up @@ -203,6 +209,7 @@ interface VendorNPMSpecifiersOptions {
importMap: ImportMap
logger: Logger
referenceTypes: boolean
rootPath?: string
}

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

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

const { npmSpecifiers, npmSpecifiersWithExtraneousFiles } = await getNPMSpecifiers(
const { npmSpecifiers, npmSpecifiersWithExtraneousFiles } = await getNPMSpecifiers({
basePath,
functions,
importMap.getContentsWithURLObjects(),
importMap: importMap.getContentsWithURLObjects(),
referenceTypes,
)
rootPath,
})

// If we found no specifiers, there's nothing left to do here.
if (Object.keys(npmSpecifiers).length === 0) {
Expand Down
63 changes: 63 additions & 0 deletions node/server/server.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { readFile } from 'fs/promises'
import { join } from 'path'
import process from 'process'

import getPort from 'get-port'
import fetch from 'node-fetch'
Expand Down Expand Up @@ -105,3 +106,65 @@ test('Starts a server and serves requests for edge functions', async () => {
`/// <reference types="${join('..', '..', 'node_modules', '@types', 'pt-committee__identidade', 'index.d.ts')}" />`,
)
})

test('Serves edge functions in a monorepo setup', async () => {
const rootPath = join(fixturesDir, 'monorepo_npm_module')
const basePath = join(rootPath, 'packages', 'frontend')
const paths = {
user: join(basePath, 'functions'),
}
const port = await getPort()
const importMapPaths = [join(basePath, 'import_map.json')]
const servePath = join(basePath, '.netlify', 'edge-functions-serve')
const server = await serve({
basePath,
bootstrapURL: 'https://edge.netlify.com/bootstrap/index-combined.ts',
importMapPaths,
port,
rootPath,
servePath,
})

const functions = [
{
name: 'func1',
path: join(paths.user, 'func1.ts'),
},
]
const options = {
getFunctionsConfig: true,
}

const { features, functionsConfig, graph, success, npmSpecifiersWithExtraneousFiles } = await server(
functions,
{
very_secret_secret: 'i love netlify',
},
options,
)
expect(features).toEqual({ npmModules: true })
expect(success).toBe(true)
expect(functionsConfig).toEqual([{ path: '/func1' }])
expect(npmSpecifiersWithExtraneousFiles).toEqual(['child-1'])

for (const key in functions) {
const graphEntry = graph?.modules.some(
// @ts-expect-error TODO: Module graph is currently not typed
({ kind, mediaType, local }) => kind === 'esm' && mediaType === 'TypeScript' && local === functions[key].path,
)

expect(graphEntry).toBe(true)
}

const response1 = await fetch(`http://0.0.0.0:${port}/func1`, {
headers: {
'x-nf-edge-functions': 'func1',
'x-ef-passthrough': 'passthrough',
'X-NF-Request-ID': uuidv4(),
},
})
expect(response1.status).toBe(200)
expect(await response1.text()).toBe(
`<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>`,
)
})

0 comments on commit aeb76d3

Please sign in to comment.