Skip to content

Commit

Permalink
feat: add support for import maps with ESZIP (#109)
Browse files Browse the repository at this point in the history
* feat: add support for import maps with ESZIP

BREAKING CHANGE: `importMaps` now expects a `baseURL` containing the URL of the import map file

* refactor: simplify import map parsing

* refactor: use `URL` objects internally

Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
  • Loading branch information
eduardoboucas and kodiakhq[bot] committed Sep 13, 2022
1 parent c155d26 commit 19031eb
Show file tree
Hide file tree
Showing 9 changed files with 147 additions and 33 deletions.
11 changes: 11 additions & 0 deletions common/stage2.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export interface InputFunction {
name: string
path: string
}

export interface WriteStage2Options {
basePath: string
destPath: string
functions: InputFunction[]
importMapURL?: string
}
4 changes: 2 additions & 2 deletions deno/bundle.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { writeStage2 } from './lib/stage2.ts'

const [payload] = Deno.args
const { basePath, destPath, functions, imports } = JSON.parse(payload)
const { basePath, destPath, functions, importMapURL } = JSON.parse(payload)

await writeStage2({ basePath, destPath, functions, imports })
await writeStage2({ basePath, destPath, functions, importMapURL })
27 changes: 6 additions & 21 deletions deno/lib/stage2.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,11 @@
import { build, LoadResponse } from 'https://deno.land/x/eszip@v0.18.0/mod.ts'
import { build, LoadResponse } from 'https://deno.land/x/eszip@v0.28.0/mod.ts'

import * as path from 'https://deno.land/std@0.127.0/path/mod.ts'

import type { InputFunction, WriteStage2Options } from '../../common/stage2.ts'
import { PUBLIC_SPECIFIER, STAGE2_SPECIFIER, virtualRoot } from './consts.ts'
import { inlineModule, loadFromVirtualRoot, loadWithRetry } from './common.ts'

interface InputFunction {
name: string
path: string
}

interface WriteStage2Options {
basePath: string
destPath: string
functions: InputFunction[]
imports?: Record<string, string>
}

const getFunctionReference = (basePath: string, func: InputFunction, index: number) => {
const importName = `func${index}`
const exportLine = `"${func.name}": ${importName}`
Expand Down Expand Up @@ -44,7 +33,7 @@ const getVirtualPath = (basePath: string, filePath: string) => {
return url
}

const stage2Loader = (basePath: string, functions: InputFunction[], imports: Record<string, string> = {}) => {
const stage2Loader = (basePath: string, functions: InputFunction[]) => {
return async (specifier: string): Promise<LoadResponse | undefined> => {
if (specifier === STAGE2_SPECIFIER) {
const stage2Entry = getStage2Entry(basePath, functions)
Expand All @@ -59,10 +48,6 @@ const stage2Loader = (basePath: string, functions: InputFunction[], imports: Rec
}
}

if (imports[specifier] !== undefined) {
return await loadWithRetry(imports[specifier])
}

if (specifier.startsWith(virtualRoot)) {
return loadFromVirtualRoot(specifier, virtualRoot, basePath)
}
Expand All @@ -71,9 +56,9 @@ const stage2Loader = (basePath: string, functions: InputFunction[], imports: Rec
}
}

const writeStage2 = async ({ basePath, destPath, functions, imports }: WriteStage2Options) => {
const loader = stage2Loader(basePath, functions, imports)
const bytes = await build([STAGE2_SPECIFIER], loader)
const writeStage2 = async ({ basePath, destPath, functions, importMapURL }: WriteStage2Options) => {
const loader = stage2Loader(basePath, functions)
const bytes = await build([STAGE2_SPECIFIER], loader, importMapURL)

return await Deno.writeFile(destPath, bytes)
}
Expand Down
11 changes: 11 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@
"node": "^12.20.0 || ^14.14.0 || >=16.0.0"
},
"dependencies": {
"@import-maps/resolve": "^1.0.1",
"common-path-prefix": "^3.0.0",
"del": "^6.0.0",
"env-paths": "^3.0.0",
Expand Down
5 changes: 3 additions & 2 deletions src/formats/eszip.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { join, resolve } from 'path'
import { fileURLToPath } from 'url'

import type { WriteStage2Options } from '../../common/stage2.js'
import { DenoBridge } from '../bridge.js'
import type { Bundle } from '../bundle.js'
import { wrapBundleError } from '../bundle_error.js'
Expand Down Expand Up @@ -30,11 +31,11 @@ const bundleESZIP = async ({
const extension = '.eszip'
const destPath = join(distDirectory, `${buildID}${extension}`)
const bundler = getESZIPBundler()
const payload = {
const payload: WriteStage2Options = {
basePath,
destPath,
functions,
imports: importMap.imports,
importMapURL: importMap.toDataURL(),
}
const flags = ['--allow-all']

Expand Down
38 changes: 30 additions & 8 deletions src/import_map.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,46 @@ import { Buffer } from 'buffer'
import { promises as fs } from 'fs'
import { dirname } from 'path'

import { parse } from '@import-maps/resolve'

const INTERNAL_IMPORTS = {
'netlify:edge': 'https://edge.netlify.com/v1/index.ts',
'netlify:edge': new URL('https://edge.netlify.com/v1/index.ts'),
}

interface ImportMapFile {
baseURL: URL
imports: Record<string, string>
scopes?: Record<string, string>
scopes?: Record<string, Record<string, string>>
}

class ImportMap {
imports: Record<string, string>
imports: Record<string, URL | null>

constructor(files: ImportMapFile[] = []) {
let imports: ImportMap['imports'] = {}

files.forEach((file) => {
const importMap = ImportMap.resolve(file)

imports = { ...imports, ...importMap.imports }
})

// Internal imports must come last, because we need to guarantee that
// `netlify:edge` isn't user-defined.
Object.entries(INTERNAL_IMPORTS).forEach((internalImport) => {
const [specifier, url] = internalImport

imports[specifier] = url
})

this.imports = imports
}

constructor(input: ImportMapFile[] = []) {
const inputImports = input.reduce((acc, { imports }) => ({ ...acc, ...imports }), {})
static resolve(importMapFile: ImportMapFile) {
const { baseURL, ...importMap } = importMapFile
const parsedImportMap = parse(importMap, baseURL)

// `INTERNAL_IMPORTS` must come last,
// because we need to guarantee `netlify:edge` isn't user-defined.
this.imports = { ...inputImports, ...INTERNAL_IMPORTS }
return parsedImportMap
}

getContents() {
Expand Down
43 changes: 43 additions & 0 deletions test/bundler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ test('Produces a JavaScript bundle and a manifest file', async (t) => {
basePath: fixturesDir,
importMaps: [
{
baseURL: pathToFileURL(join(fixturesDir, 'import-map.json')),
imports: {
'alias:helper': pathToFileURL(join(fixturesDir, 'helper.ts')).toString(),
},
Expand Down Expand Up @@ -65,6 +66,7 @@ test('Produces only a ESZIP bundle when the `edge_functions_produce_eszip` featu
},
importMaps: [
{
baseURL: pathToFileURL(join(fixturesDir, 'import-map.json')),
imports: {
'alias:helper': pathToFileURL(join(fixturesDir, 'helper.ts')).toString(),
},
Expand Down Expand Up @@ -145,6 +147,7 @@ test('Uses the cache directory as the `DENO_DIR` value if the `edge_functions_ca
cacheDirectory: cacheDir.path,
importMaps: [
{
baseURL: pathToFileURL(join(fixturesDir, 'import-map.json')),
imports: {
'alias:helper': pathToFileURL(join(fixturesDir, 'helper.ts')).toString(),
},
Expand Down Expand Up @@ -180,3 +183,43 @@ test('Uses the cache directory as the `DENO_DIR` value if the `edge_functions_ca

await fs.rmdir(outDir.path, { recursive: true })
})

test('Supports import maps with relative paths', async (t) => {
const sourceDirectory = resolve(fixturesDir, 'project_1', 'functions')
const tmpDir = await tmp.dir()
const declarations = [
{
function: 'func1',
path: '/func1',
},
]
const result = await bundle([sourceDirectory], tmpDir.path, declarations, {
basePath: fixturesDir,
featureFlags: {
edge_functions_produce_eszip: true,
},
importMaps: [
{
baseURL: pathToFileURL(join(fixturesDir, 'import-map.json')),
imports: {
'alias:helper': './helper.ts',
},
},
],
})
const generatedFiles = await fs.readdir(tmpDir.path)

t.is(result.functions.length, 1)
t.is(generatedFiles.length, 2)

// eslint-disable-next-line unicorn/prefer-json-parse-buffer
const manifestFile = await fs.readFile(resolve(tmpDir.path, 'manifest.json'), 'utf8')
const manifest = JSON.parse(manifestFile)
const { bundles } = manifest

t.is(bundles.length, 1)
t.is(bundles[0].format, 'eszip2')
t.true(generatedFiles.includes(bundles[0].asset))

await fs.rmdir(tmpDir.path, { recursive: true })
})
40 changes: 40 additions & 0 deletions test/import_map.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import test from 'ava'

import { ImportMap } from '../src/import_map.js'

test('Handles import maps with full URLs without specifying a base URL', (t) => {
const inputFile1 = {
baseURL: new URL('file:///some/path/import-map.json'),
imports: {
'alias:jamstack': 'https://jamstack.org',
},
}
const inputFile2 = {
baseURL: new URL('file:///some/path/import-map.json'),
imports: {
'alias:pets': 'https://petsofnetlify.com/',
},
}

const map = new ImportMap([inputFile1, inputFile2])
const { imports } = JSON.parse(map.getContents())

t.is(imports['netlify:edge'], 'https://edge.netlify.com/v1/index.ts')
t.is(imports['alias:jamstack'], 'https://jamstack.org/')
t.is(imports['alias:pets'], 'https://petsofnetlify.com/')
})

test('Handles import maps with relative paths', (t) => {
const inputFile1 = {
baseURL: new URL('file:///Users/jane-doe/my-site/import-map.json'),
imports: {
'alias:pets': './heart/pets/',
},
}

const map = new ImportMap([inputFile1])
const { imports } = JSON.parse(map.getContents())

t.is(imports['netlify:edge'], 'https://edge.netlify.com/v1/index.ts')
t.is(imports['alias:pets'], 'file:///Users/jane-doe/my-site/heart/pets/')
})

0 comments on commit 19031eb

Please sign in to comment.