Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add rootPath for monorepo setups #521

Merged
merged 4 commits into from
Nov 5, 2023
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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>`,
)
})
Loading