-
Notifications
You must be signed in to change notification settings - Fork 8
/
Copy pathbundler.ts
191 lines (161 loc) · 6.31 KB
/
bundler.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
import { promises as fs } from 'fs'
import { join, resolve } from 'path'
import commonPathPrefix from 'common-path-prefix'
import isPathInside from 'is-path-inside'
import { v4 as uuidv4 } from 'uuid'
import { importMapSpecifier } from '../shared/consts.js'
import { DenoBridge, DenoOptions, OnAfterDownloadHook, OnBeforeDownloadHook } from './bridge.js'
import type { Bundle } from './bundle.js'
import { FunctionConfig, getFunctionConfig } from './config.js'
import { Declaration, mergeDeclarations } from './declaration.js'
import { load as loadDeployConfig } from './deploy_config.js'
import { EdgeFunction } from './edge_function.js'
import { FeatureFlags, getFlags } from './feature_flags.js'
import { findFunctions } from './finder.js'
import { bundle as bundleESZIP } from './formats/eszip.js'
import { ImportMap } from './import_map.js'
import { getLogger, LogFunction } from './logger.js'
import { writeManifest } from './manifest.js'
import { ensureLatestTypes } from './types.js'
interface BundleOptions {
basePath?: string
cacheDirectory?: string
configPath?: string
debug?: boolean
distImportMapPath?: string
featureFlags?: FeatureFlags
importMapPaths?: (string | undefined)[]
onAfterDownload?: OnAfterDownloadHook
onBeforeDownload?: OnBeforeDownloadHook
systemLogger?: LogFunction
internalSrcFolder?: string
}
const bundle = async (
sourceDirectories: string[],
distDirectory: string,
tomlDeclarations: Declaration[] = [],
{
basePath: inputBasePath,
cacheDirectory,
configPath,
debug,
distImportMapPath,
featureFlags: inputFeatureFlags,
importMapPaths = [],
onAfterDownload,
onBeforeDownload,
systemLogger,
internalSrcFolder,
}: BundleOptions = {},
) => {
const logger = getLogger(systemLogger, debug)
const featureFlags = getFlags(inputFeatureFlags)
const options: DenoOptions = {
debug,
cacheDirectory,
logger,
onAfterDownload,
onBeforeDownload,
}
const internalFunctionsPath = internalSrcFolder && resolve(internalSrcFolder)
if (cacheDirectory !== undefined) {
options.denoDir = join(cacheDirectory, 'deno_dir')
}
const deno = new DenoBridge(options)
const basePath = getBasePath(sourceDirectories, inputBasePath)
await ensureLatestTypes(deno, logger)
// The name of the bundle will be the hash of its contents, which we can't
// compute until we run the bundle process. For now, we'll use a random ID
// to create the bundle artifacts and rename them later.
const buildID = uuidv4()
// Loading any configuration options from the deploy configuration API, if it
// exists.
const deployConfig = await loadDeployConfig(configPath, logger)
// Layers are marked as externals in the ESZIP, so that those specifiers are
// not actually included in the bundle.
const externals = deployConfig.layers.map((layer) => layer.name)
const importMap = new ImportMap()
await importMap.addFiles([deployConfig?.importMap, ...importMapPaths], logger)
const functions = await findFunctions(sourceDirectories)
const functionBundle = await bundleESZIP({
basePath,
buildID,
debug,
deno,
distDirectory,
externals,
functions,
featureFlags,
importMap,
})
// The final file name of the bundles contains a SHA256 hash of the contents,
// which we can only compute now that the files have been generated. So let's
// rename the bundles to their permanent names.
await createFinalBundles([functionBundle], distDirectory, buildID)
// Retrieving a configuration object for each function.
const functionsConfig = await Promise.all(
functions.map((func) => getFunctionConfig(func, importMap, deno, logger, featureFlags)),
)
// Creating a hash of function names to configuration objects.
const functionsWithConfig = functions.reduce(
(acc, func, index) => ({ ...acc, [func.name]: functionsConfig[index] }),
{} as Record<string, FunctionConfig>,
)
// Creating a final declarations array by combining the TOML file with the
// deploy configuration API and the in-source configuration.
const declarationsFromConfig = mergeDeclarations(tomlDeclarations, functionsWithConfig, deployConfig.declarations)
// If any declarations are autogenerated and are missing the generator field
// add a default string.
const declarations = internalFunctionsPath
? declarationsFromConfig.map((declaration) =>
addGeneratorFieldIfMissing(declaration, functions, internalFunctionsPath),
)
: declarationsFromConfig
const manifest = await writeManifest({
bundles: [functionBundle],
declarations,
distDirectory,
featureFlags,
functions,
functionConfig: functionsWithConfig,
importMap: importMapSpecifier,
layers: deployConfig.layers,
})
if (distImportMapPath) {
await importMap.writeToFile(distImportMapPath)
}
return { functions, manifest }
}
const createFinalBundles = async (bundles: Bundle[], distDirectory: string, buildID: string) => {
const renamingOps = bundles.map(async ({ extension, hash }) => {
const tempBundlePath = join(distDirectory, `${buildID}${extension}`)
const finalBundlePath = join(distDirectory, `${hash}${extension}`)
await fs.rename(tempBundlePath, finalBundlePath)
})
await Promise.all(renamingOps)
}
const getBasePath = (sourceDirectories: string[], inputBasePath?: string) => {
// If there's a specific base path supplied, that takes precedence.
if (inputBasePath !== undefined) {
return inputBasePath
}
// `common-path-prefix` returns an empty string when called with a single
// path, so we check for that case and return the path itself instead.
if (sourceDirectories.length === 1) {
return sourceDirectories[0]
}
return commonPathPrefix(sourceDirectories)
}
export const addGeneratorFieldIfMissing = (
declaration: Declaration,
functions: EdgeFunction[],
internalFunctionsPath?: string,
) => {
const fullFuncPath = functions?.find((func) => func.name === declaration.function)?.path
// If function path is in the internalFunctionsPath, we assume it is autogenerated.
const isInternal = Boolean(internalFunctionsPath && fullFuncPath && isPathInside(fullFuncPath, internalFunctionsPath))
const generatorFallback = isInternal ? 'internalFunc' : undefined
return { ...declaration, generator: declaration.generator || generatorFallback }
}
export { bundle }
export type { BundleOptions }