-
-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
9 changed files
with
385 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
// Copyright 2023-present Eser Ozvataf and other contributors. All rights reserved. Apache-2.0 license. | ||
|
||
import * as runtime from "../standards/runtime.ts"; | ||
import { colors, path } from "./deps.ts"; | ||
import { type BuildSnapshot, type BuildSnapshotJson } from "./mod.ts"; | ||
import { setBuildId } from "./build-id.ts"; | ||
|
||
export class AotSnapshot implements BuildSnapshot { | ||
#files: Map<string, string>; | ||
#dependencies: Map<string, Array<string>>; | ||
|
||
constructor( | ||
files: Map<string, string>, | ||
dependencies: Map<string, Array<string>>, | ||
) { | ||
this.#files = files; | ||
this.#dependencies = dependencies; | ||
} | ||
|
||
get paths(): Array<string> { | ||
return Array.from(this.#files.keys()); | ||
} | ||
|
||
async read(pathStr: string): Promise<ReadableStream<Uint8Array> | null> { | ||
const filePath = this.#files.get(pathStr); | ||
if (filePath !== undefined) { | ||
try { | ||
const file = await runtime.current.open(filePath, { read: true }); | ||
return file.readable; | ||
} catch (_err) { | ||
return null; | ||
} | ||
} | ||
|
||
// Handler will turn this into a 404 | ||
return null; | ||
} | ||
|
||
dependencies(pathStr: string): Array<string> { | ||
return this.#dependencies.get(pathStr) ?? []; | ||
} | ||
} | ||
|
||
export async function loadAotSnapshot( | ||
snapshotDirPath: string, | ||
): Promise<AotSnapshot | null> { | ||
try { | ||
if ((await runtime.current.stat(snapshotDirPath)).isDirectory) { | ||
console.log( | ||
`Using snapshot found at ${colors.cyan(snapshotDirPath)}`, | ||
); | ||
|
||
const snapshotPath = path.join(snapshotDirPath, "snapshot.json"); | ||
const json = JSON.parse( | ||
await runtime.current.readTextFile(snapshotPath), | ||
) as BuildSnapshotJson; | ||
setBuildId(json.build_id); | ||
|
||
const dependencies = new Map<string, Array<string>>( | ||
Object.entries(json.files), | ||
); | ||
|
||
const files = new Map<string, string>(); | ||
Object.keys(json.files).forEach((name) => { | ||
const filePath = path.join(snapshotDirPath, name); | ||
files.set(name, filePath); | ||
}); | ||
|
||
return new AotSnapshot(files, dependencies); | ||
} | ||
return null; | ||
} catch (err) { | ||
if (!(err instanceof runtime.current.errors.NotFound)) { | ||
throw err; | ||
} | ||
|
||
return null; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
// Copyright 2023-present Eser Ozvataf and other contributors. All rights reserved. Apache-2.0 license. | ||
|
||
import * as runtime from "../standards/runtime.ts"; | ||
import { hex } from "./deps.ts"; | ||
|
||
const env = runtime.current.getEnv(); | ||
|
||
const deploymentId = env["DENO_DEPLOYMENT_ID"] || | ||
// For CI | ||
env["GITHUB_SHA"] || | ||
crypto.randomUUID(); | ||
const buildIdHash = await crypto.subtle.digest( | ||
"SHA-1", | ||
new TextEncoder().encode(deploymentId), | ||
); | ||
|
||
export let BUILD_ID = hex.encodeHex(buildIdHash); | ||
|
||
export function setBuildId(buildId: string) { | ||
BUILD_ID = buildId; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
// Copyright 2023-present Eser Ozvataf and other contributors. All rights reserved. Apache-2.0 license. | ||
|
||
export * as colors from "https://deno.land/std@0.212.0/fmt/colors.ts"; | ||
export * as hex from "https://deno.land/std@0.212.0/encoding/hex.ts"; | ||
export * as path from "https://deno.land/std@0.212.0/path/mod.ts"; | ||
export * as regexpEscape from "https://deno.land/std@0.212.0/regexp/escape.ts"; | ||
|
||
export * as esbuild from "https://deno.land/x/esbuild_deno_loader@0.8.3/mod.ts"; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,215 @@ | ||
// Copyright 2023-present Eser Ozvataf and other contributors. All rights reserved. Apache-2.0 license. | ||
|
||
import { | ||
type BuildOptions, | ||
type OnLoadOptions, | ||
type Plugin, | ||
} from "https://deno.land/x/esbuild@v0.19.11/mod.js"; | ||
import * as runtime from "../standards/runtime.ts"; | ||
import { esbuild, path, regexpEscape } from "./deps.ts"; | ||
import { Builder, BuildSnapshot } from "./mod.ts"; | ||
// import { BUNDLE_PUBLIC_PATH } from "../server/constants.ts"; | ||
|
||
export interface EsbuildBuilderOptions { | ||
/** The build ID. */ | ||
buildID: string; | ||
/** The entrypoints, mapped from name to URL. */ | ||
entrypoints: Record<string, string>; | ||
/** Whether or not this is a dev build. */ | ||
dev: boolean; | ||
/** The path to the deno.json / deno.jsonc config file. */ | ||
configPath: string; | ||
/** The JSX configuration. */ | ||
jsx?: string; | ||
jsxImportSource?: string; | ||
target: string | Array<string>; | ||
absoluteWorkingDir: string; | ||
basePath?: string; | ||
} | ||
|
||
export class EsbuildBuilder implements Builder { | ||
#options: EsbuildBuilderOptions; | ||
|
||
constructor(options: EsbuildBuilderOptions) { | ||
this.#options = options; | ||
} | ||
|
||
async build(): Promise<EsbuildSnapshot> { | ||
const opts = this.#options; | ||
|
||
const env = runtime.current.getEnv(); | ||
|
||
const isOnDenoDeploy = env["DENO_DEPLOYMENT_ID"] !== undefined; | ||
const portableBuilder = env["LIME_ESBUILD_LOADER"] === "portable"; | ||
|
||
// Lazily initialize esbuild | ||
// @deno-types="https://deno.land/x/esbuild@v0.19.11/mod.d.ts" | ||
const esbuildInstance = isOnDenoDeploy || portableBuilder | ||
? await import("https://deno.land/x/esbuild@v0.19.11/wasm.js") | ||
: await import("https://deno.land/x/esbuild@v0.19.11/mod.js"); | ||
const esbuildWasmURL = | ||
new URL("./esbuild_v0.19.11.wasm", import.meta.url).href; | ||
|
||
if (isOnDenoDeploy) { | ||
await esbuildInstance.initialize({ | ||
wasmURL: esbuildWasmURL, | ||
worker: false, | ||
}); | ||
} else { | ||
await esbuildInstance.initialize({}); | ||
} | ||
|
||
try { | ||
const absWorkingDir = opts.absoluteWorkingDir; | ||
|
||
// In dev-mode we skip identifier minification to be able to show proper | ||
// component names in React DevTools instead of single characters. | ||
const minifyOptions: Partial<BuildOptions> = opts.dev | ||
? { | ||
minifyIdentifiers: false, | ||
minifySyntax: true, | ||
minifyWhitespace: true, | ||
} | ||
: { minify: true }; | ||
|
||
const bundle = await esbuildInstance.build({ | ||
entryPoints: opts.entrypoints, | ||
|
||
platform: "browser", | ||
target: this.#options.target, | ||
|
||
format: "esm", | ||
bundle: true, | ||
splitting: true, | ||
treeShaking: true, | ||
sourcemap: opts.dev ? "linked" : false, | ||
...minifyOptions, | ||
|
||
jsx: opts.jsx === "react" | ||
? "transform" | ||
: opts.jsx === "react-native" || opts.jsx === "preserve" | ||
? "preserve" | ||
: !opts.jsxImportSource | ||
? "transform" | ||
: "automatic", | ||
jsxImportSource: opts.jsxImportSource ?? "react", | ||
|
||
absWorkingDir, | ||
outdir: ".", | ||
// publicPath: BUNDLE_PUBLIC_PATH, | ||
write: false, | ||
metafile: true, | ||
|
||
plugins: [ | ||
devClientUrlPlugin(opts.basePath), | ||
buildIdPlugin(opts.buildID), | ||
...esbuild.denoPlugins({ configPath: opts.configPath }), | ||
], | ||
}); | ||
|
||
const files = new Map<string, Uint8Array>(); | ||
const dependencies = new Map<string, Array<string>>(); | ||
|
||
for (const file of bundle.outputFiles) { | ||
const relativePath = path.relative(absWorkingDir, file.path); | ||
files.set(relativePath, file.contents); | ||
} | ||
|
||
files.set( | ||
"metafile.json", | ||
new TextEncoder().encode(JSON.stringify(bundle.metafile)), | ||
); | ||
|
||
const metaOutputs = new Map(Object.entries(bundle.metafile.outputs)); | ||
|
||
for (const [pathStr, entry] of metaOutputs.entries()) { | ||
const imports = entry.imports | ||
.filter((importItem) => importItem.kind === "import-statement") | ||
.map((importItem) => importItem.path); | ||
dependencies.set(pathStr, imports); | ||
} | ||
|
||
return new EsbuildSnapshot(files, dependencies); | ||
} finally { | ||
esbuildInstance.stop(); | ||
} | ||
} | ||
} | ||
|
||
function devClientUrlPlugin(basePath?: string): Plugin { | ||
return { | ||
name: "dev-client-url", | ||
setup(build) { | ||
build.onLoad( | ||
{ filter: /client\.ts$/, namespace: "file" }, | ||
async (args) => { | ||
// Load the original script | ||
const contents = await runtime.current.readTextFile(args.path); | ||
|
||
// Replace the URL | ||
const modifiedContents = contents.replace( | ||
"/_lime/alive", | ||
`${basePath}/_lime/alive`, | ||
); | ||
|
||
return { | ||
contents: modifiedContents, | ||
loader: "ts", | ||
}; | ||
}, | ||
); | ||
}, | ||
}; | ||
} | ||
|
||
function buildIdPlugin(buildId: string): Plugin { | ||
const file = import.meta.resolve("../runtime/build-id.ts"); | ||
const url = new URL(file); | ||
let options: OnLoadOptions; | ||
|
||
if (url.protocol === "file:") { | ||
const pathNormalized = path.fromFileUrl(url); | ||
const filter = new RegExp(`^${regexpEscape.escape(pathNormalized)}$`); | ||
options = { filter, namespace: "file" }; | ||
} else { | ||
const namespace = url.protocol.slice(0, -1); | ||
const pathNormalized = url.href.slice(namespace.length + 1); | ||
const filter = new RegExp(`^${regexpEscape.escape(pathNormalized)}$`); | ||
options = { filter, namespace }; | ||
} | ||
|
||
return { | ||
name: "lime-build-id", | ||
setup(build) { | ||
build.onLoad( | ||
options, | ||
() => ({ contents: `export const BUILD_ID = "${buildId}";` }), | ||
); | ||
}, | ||
}; | ||
} | ||
|
||
export class EsbuildSnapshot implements BuildSnapshot { | ||
#files: Map<string, Uint8Array>; | ||
#dependencies: Map<string, Array<string>>; | ||
|
||
constructor( | ||
files: Map<string, Uint8Array>, | ||
dependencies: Map<string, Array<string>>, | ||
) { | ||
this.#files = files; | ||
this.#dependencies = dependencies; | ||
} | ||
|
||
get paths(): Array<string> { | ||
return Array.from(this.#files.keys()); | ||
} | ||
|
||
read(pathStr: string): Uint8Array | null { | ||
return this.#files.get(pathStr) ?? null; | ||
} | ||
|
||
dependencies(pathStr: string): Array<string> { | ||
return this.#dependencies.get(pathStr) ?? []; | ||
} | ||
} |
Binary file not shown.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
// Copyright 2023-present Eser Ozvataf and other contributors. All rights reserved. Apache-2.0 license. | ||
|
||
export { | ||
EsbuildBuilder, | ||
type EsbuildBuilderOptions, | ||
EsbuildSnapshot, | ||
} from "./esbuild.ts"; | ||
export { AotSnapshot } from "./aot-snapshot.ts"; | ||
export interface Builder { | ||
build(): Promise<BuildSnapshot>; | ||
} | ||
|
||
export interface BuildSnapshot { | ||
/** The list of files contained in this snapshot, not prefixed by a slash. */ | ||
readonly paths: Array<string>; | ||
|
||
/** For a given file, return it's contents. | ||
* @throws If the file is not contained in this snapshot. */ | ||
read( | ||
path: string, | ||
): | ||
| ReadableStream<Uint8Array> | ||
| Uint8Array | ||
| null | ||
| Promise<ReadableStream<Uint8Array> | Uint8Array | null>; | ||
|
||
/** For a given entrypoint, return it's list of dependencies. | ||
* | ||
* Returns an empty array if the entrypoint does not exist. */ | ||
dependencies(pathStr: string): Array<string>; | ||
} | ||
|
||
export interface BuildSnapshotJson { | ||
build_id: string; | ||
files: Record<string, Array<string>>; | ||
} |
Oops, something went wrong.