diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..088d240 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,17 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## Unreleased + +### Added + +- Source map support in [denofn/denopack/pull/2](https://github.com/denofn/denopack/pull/2) +- Lockfile integrity checking in [denofn/denopack/pull/4](https://github.com/denofn/denopack/pull/4) + +### Changed + +- Hooks now take one configuration object as parameter where needed in [denofn/denopack/pull/4](https://github.com/denofn/denopack/pull/4) diff --git a/README.md b/README.md index 7bc7e3d..add8931 100644 --- a/README.md +++ b/README.md @@ -41,8 +41,8 @@ There is absolutely nothing wrong with `deno bundle`, but in its current state i - [x] [Tree shaking](https://rollupjs.org/guide/en/#tree-shaking) comes built-in with Rollup - [x] Minification by the usage of the [Terser plugin](./plugin/terserTransform) - [x] Source Maps (should also come built-in with Rollup, [coming soon](https://github.com/denofn/denopack/pull/2)) +- [x] Lock file support, checking checksums from the lockfile against loaded code - [ ] File watching (pretty sure this can be implemented, coming soon) -- [ ] Lock file support, checking checksums from the lockfile against loaded code More to come, also see the `deno bundle` roadmap/wishlist over at [denoland/deno/issues/4549](https://github.com/denoland/deno/issues/4549) @@ -219,6 +219,7 @@ Contributing a hook follows the following conventions: - Hooks are stored inside of [hooks.ts](./plugin/hooks.ts) - Hooks are always functions and always return an array of plugins - Using plugins that accept configuration options in hooks should always be allowed to pass config down to that plugin +- Configuration options are stored in one object containing all configuration options, see [hooks.ts](./plugin/hooks.ts) for examples ## Acknowledgements diff --git a/plugin/cacheLoader/README.md b/plugin/cacheLoader/README.md index 5a1d0ce..bcd034b 100644 --- a/plugin/cacheLoader/README.md +++ b/plugin/cacheLoader/README.md @@ -1,11 +1,14 @@ # denopack/plugin/cacheLoader - [optional] checks if cached file exists and skips loading if it doesn't +- [optional] checks integrity of cached file against a lockfile - returns local cached file location ## Options -- `lazy [boolean]`: defaults to false. If active, this will skip checking whether the file actually exists locally and will lazily return the assumed path. +- `lazy [boolean]`: defaults to false. If active, this will skip checking whether the file actually exists locally and will lazily return the assumed path +- `cacheOnly [boolean]`: throws if an external dependency is not found inside the cache +- `lockFile [string]`: path to a lockfile (for example: `lock.json`). Throws if the integrity of the loaded cache file does not match the integrity in the lockfile ## Required flags @@ -17,11 +20,27 @@ Put this before pluginFileLoader ```ts -import { pluginRootResolver } from "https://cdn.jsdelivr.net/gh/denofn/denopack@latest/plugin/rootResolver/mod.ts"; +import { pluginImportResolver } from "https://cdn.jsdelivr.net/gh/denofn/denopack@latest/plugin/importResolver/mod.ts"; import { pluginCacheLoader } from "https://cdn.jsdelivr.net/gh/denofn/denopack@latest/plugin/cacheLoader/mod.ts"; import { pluginFileLoader } from "https://cdn.jsdelivr.net/gh/denofn/denopack@latest/plugin/filLoader/mod.ts"; export default { - plugins: [pluginRootResolver(), pluginCacheLoader(), pluginFileLoader()], + plugins: [pluginImportResolver(), pluginCacheLoader(), pluginFileLoader()], +}; +``` + +### Strict integrity checks + +```ts +import { pluginImportResolver } from "https://cdn.jsdelivr.net/gh/denofn/denopack@latest/plugin/importResolver/mod.ts"; +import { pluginCacheLoader } from "https://cdn.jsdelivr.net/gh/denofn/denopack@latest/plugin/cacheLoader/mod.ts"; +import { pluginFileLoader } from "https://cdn.jsdelivr.net/gh/denofn/denopack@latest/plugin/filLoader/mod.ts"; + +export default { + plugins: [ + pluginImportResolver(), + pluginCacheLoader({ lockFile: "lock.json", cacheOnly: true }), + pluginFileLoader({ lockFile: "lock.json" }), + ], }; ``` diff --git a/plugin/cacheLoader/mod.ts b/plugin/cacheLoader/mod.ts index f5f26f0..65a4743 100644 --- a/plugin/cacheLoader/mod.ts +++ b/plugin/cacheLoader/mod.ts @@ -1,26 +1,42 @@ -import { Plugin } from "../../deps.ts"; +import { path, Plugin } from "../../deps.ts"; import { buildCacheUrl } from "../../util/buildCacheUrl.ts"; +import { checkIntegrity } from "../../util/checkIntegrity.ts"; import { isFile } from "../../util/isFile.ts"; import { isHttpUrl } from "../../util/isHttpUrl.ts"; -type Opts = { +export type Opts = { lazy?: boolean; + cacheOnly?: boolean; + lockFile?: string; }; +async function cacheLoader( + id: string, + opts: Opts, + lockFile: Record | undefined +): Promise { + const cacheUrl = buildCacheUrl(id); + const isFileInCache = await isFile(cacheUrl); + + if (opts.cacheOnly && !isFileInCache) throw new Error(`Cannot find ${id} in Deno cache`); + if (!opts.lazy && !isFileInCache) return null; + + const code = await Deno.readTextFile(cacheUrl); + if (lockFile) checkIntegrity(lockFile, id, code); + + return code; +} + export function pluginCacheLoader(opts: Opts = {}): Plugin { + const lockFile: Record | undefined = opts.lockFile + ? JSON.parse(Deno.readTextFileSync(path.resolve(opts.lockFile))) + : undefined; + return { name: "denopack-plugin-cacheLoader", async load(id) { if (!isHttpUrl(id)) return null; - - const cacheUrl = buildCacheUrl(id); - - if (opts.lazy || (await isFile(cacheUrl))) { - const code = await Deno.readTextFile(cacheUrl); - return code; - } - - return null; + return cacheLoader(id, opts, lockFile); }, }; } diff --git a/plugin/cacheResolver/README.md b/plugin/cacheResolver/README.md index 031e30a..376682d 100644 --- a/plugin/cacheResolver/README.md +++ b/plugin/cacheResolver/README.md @@ -17,11 +17,11 @@ ## Usage ```ts -import { pluginRootResolver } from "https://cdn.jsdelivr.net/gh/denofn/denopack@latest/plugin/rootResolver/mod.ts"; +import { pluginImportResolver } from "https://cdn.jsdelivr.net/gh/denofn/denopack@latest/plugin/importResolver/mod.ts"; import { pluginChainResolver } from "https://cdn.jsdelivr.net/gh/denofn/denopack@latest/plugin/chainResolver/mod.ts"; import { pluginCacheResolver } from "https://cdn.jsdelivr.net/gh/denofn/denopack@latest/plugin/cacheResolver/mod.ts"; export default { - plugins: [pluginChainResolver(pluginRootResolver(), pluginCacheResolver())], + plugins: [pluginChainResolver(pluginImportResolver(), pluginCacheResolver())], }; ``` diff --git a/plugin/chainResolver/README.md b/plugin/chainResolver/README.md index 0ca8799..63d0227 100644 --- a/plugin/chainResolver/README.md +++ b/plugin/chainResolver/README.md @@ -5,11 +5,11 @@ If files are resolved, Rollup will skip all other resolvers for that file. Somet ## Usage ```ts -import { pluginRootResolver } from "https://cdn.jsdelivr.net/gh/denofn/denopack@latest/plugin/rootResolver/mod.ts"; +import { pluginImportResolver } from "https://cdn.jsdelivr.net/gh/denofn/denopack@latest/plugin/importResolver/mod.ts"; import { pluginChainResolver } from "https://cdn.jsdelivr.net/gh/denofn/denopack@latest/plugin/chainResolver/mod.ts"; import { pluginCacheResolver } from "https://cdn.jsdelivr.net/gh/denofn/denopack@latest/plugin/cacheResolver/mod.ts"; export default { - plugins: [pluginChainResolver(pluginRootResolver(), pluginCacheResolver())], + plugins: [pluginChainResolver(pluginImportResolver(), pluginCacheResolver())], }; ``` diff --git a/plugin/fileLoader/README.md b/plugin/fileLoader/README.md index 9282b21..d50cffb 100644 --- a/plugin/fileLoader/README.md +++ b/plugin/fileLoader/README.md @@ -2,19 +2,40 @@ - Loads locally resolved files - Fetches resolved urls +- [optional] checks integrity of files loaded from url against a lockfile + +## Options + +- `lockFile [string]`: path to a lockfile (for example: `lock.json`). Throws if the integrity of the loaded cache file does not match the integrity in the lockfile ## Required flags -- `--allow-read` for local files +- `--allow-read` for local files and lockfiles - `--allow-net` for fetching urls ## Usage ```ts -import { pluginRootResolver } from "https://cdn.jsdelivr.net/gh/denofn/denopack@latest/plugin/rootResolver/mod.ts"; +import { pluginImportResolver } from "https://cdn.jsdelivr.net/gh/denofn/denopack@latest/plugin/importResolver/mod.ts"; import { pluginFileLoader } from "https://cdn.jsdelivr.net/gh/denofn/denopack@latest/plugin/fileLoader/mod.ts"; export default { - plugins: [pluginRootResolver(), pluginFileLoader()], + plugins: [pluginImportResolver(), pluginFileLoader()], +}; +``` + +### Strict integrity checks + +```ts +import { pluginImportResolver } from "https://cdn.jsdelivr.net/gh/denofn/denopack@latest/plugin/importResolver/mod.ts"; +import { pluginCacheLoader } from "https://cdn.jsdelivr.net/gh/denofn/denopack@latest/plugin/cacheLoader/mod.ts"; +import { pluginFileLoader } from "https://cdn.jsdelivr.net/gh/denofn/denopack@latest/plugin/filLoader/mod.ts"; + +export default { + plugins: [ + pluginImportResolver(), + pluginCacheLoader({ lockFile: "lock.json", cacheOnly: true }), + pluginFileLoader({ lockFile: "lock.json" }), + ], }; ``` diff --git a/plugin/fileLoader/mod.ts b/plugin/fileLoader/mod.ts index b43457b..0380a79 100644 --- a/plugin/fileLoader/mod.ts +++ b/plugin/fileLoader/mod.ts @@ -1,16 +1,29 @@ -import { Plugin } from "../../deps.ts"; +import { path, Plugin } from "../../deps.ts"; +import { checkIntegrity } from "../../util/checkIntegrity.ts"; import { isHttpUrl } from "../../util/isHttpUrl.ts"; -export function pluginFileLoader(): Plugin { +export type Opts = { + lockFile?: string; +}; + +export function pluginFileLoader(opts: Opts = {}): Plugin { + const lockFile: Record | undefined = opts.lockFile + ? JSON.parse(Deno.readTextFileSync(path.resolve(opts.lockFile))) + : undefined; + return { name: "denopack-plugin-fileLoader", async load(id) { - if (isHttpUrl(id)) { - const response = await fetch(id); - return response.text(); - } else { + if (!isHttpUrl(id)) { return Deno.readTextFile(id); } + + const response = await fetch(id); + const code = await response.text(); + + if (lockFile) checkIntegrity(lockFile, id, code); + + return code; }, }; } diff --git a/plugin/hooks.ts b/plugin/hooks.ts index 68d1a06..a283d27 100644 --- a/plugin/hooks.ts +++ b/plugin/hooks.ts @@ -1,34 +1,79 @@ -import { pluginCacheLoader } from "./cacheLoader/mod.ts"; -import { pluginFileLoader } from "./fileLoader/mod.ts"; +import { Opts as CacheLoaderOptions, pluginCacheLoader } from "./cacheLoader/mod.ts"; +import { Opts as FileLoaderOptions, pluginFileLoader } from "./fileLoader/mod.ts"; import { pluginImportResolver } from "./importResolver/mod.ts"; import { pluginTypescriptCompile } from "./typescriptCompile/mod.ts"; import { pluginTypescriptTransform } from "./typescriptTransform/mod.ts"; -export const useAlwaysFetch = (opts: Deno.CompilerOptions = {}) => [ - pluginImportResolver(), - pluginFileLoader(), - pluginTypescriptTransform(opts), -]; +const useLoadAndTransform = ({ + fileLoaderOptions, + compilerOptions, +}: { + compilerOptions?: Deno.CompilerOptions; + fileLoaderOptions?: FileLoaderOptions; +}) => [pluginFileLoader(fileLoaderOptions), pluginTypescriptTransform(compilerOptions)]; -export const useCache = (opts: Deno.CompilerOptions = {}) => [ - pluginImportResolver(), - pluginCacheLoader(), - pluginFileLoader(), - pluginTypescriptTransform(opts), -]; +export const useAlwaysFetch = ( + opts: { + compilerOptions?: Deno.CompilerOptions; + fileLoaderOptions?: FileLoaderOptions; + } = {} +) => [pluginImportResolver(), ...useLoadAndTransform(opts)]; -export const useCacheLazy = (opts: Deno.CompilerOptions = {}) => [ +export const useCache = ( + opts: { + cacheLoaderOptions?: CacheLoaderOptions; + compilerOptions?: Deno.CompilerOptions; + fileLoaderOptions?: FileLoaderOptions; + } = {} +) => [ pluginImportResolver(), - pluginCacheLoader({ lazy: true }), - pluginFileLoader(), - pluginTypescriptTransform(opts), + pluginCacheLoader(opts.cacheLoaderOptions), + ...useLoadAndTransform({ + compilerOptions: opts.compilerOptions, + fileLoaderOptions: opts.fileLoaderOptions, + }), ]; -export const useCompile = (opts: Deno.CompilerOptions = {}) => [ - pluginTypescriptCompile({ useAsLoader: false, compilerOptions: opts }), - pluginFileLoader(), -]; +export const useCacheLazy = ( + opts: { + fileLoaderOptions?: FileLoaderOptions; + cacheLoaderOptions?: CacheLoaderOptions; + compilerOptions?: Deno.CompilerOptions; + } = {} +) => + useCache({ + cacheLoaderOptions: { ...opts.cacheLoaderOptions, lazy: true }, + compilerOptions: opts.compilerOptions, + fileLoaderOptions: opts.fileLoaderOptions, + }); + +export const useCacheOnly = ( + opts: { + fileLoaderOptions?: FileLoaderOptions; + cacheLoaderOptions?: CacheLoaderOptions; + compilerOptions?: Deno.CompilerOptions; + } = {} +) => + useCache({ + fileLoaderOptions: opts.fileLoaderOptions, + cacheLoaderOptions: { ...opts.cacheLoaderOptions, cacheOnly: true }, + compilerOptions: opts.compilerOptions, + }); -export const useCompileAsLoader = (opts: Deno.CompilerOptions = {}) => [ - pluginTypescriptCompile({ useAsLoader: true, compilerOptions: opts }), +export const useCompile = ( + opts: { + cacheLoaderOptions?: CacheLoaderOptions; + compilerOptions?: Deno.CompilerOptions; + fileLoaderOptions?: FileLoaderOptions; + } = {} +) => [ + pluginTypescriptCompile({ useAsLoader: false, compilerOptions: opts.compilerOptions }), + pluginCacheLoader(opts.cacheLoaderOptions), + pluginFileLoader(opts.fileLoaderOptions), ]; + +export const useCompileAsLoader = ( + opts: { + compilerOptions?: Deno.CompilerOptions; + } = {} +) => [pluginTypescriptCompile({ useAsLoader: true, compilerOptions: opts.compilerOptions })]; diff --git a/plugin/mod.ts b/plugin/mod.ts index 8b5bb9a..62c88a4 100644 --- a/plugin/mod.ts +++ b/plugin/mod.ts @@ -3,8 +3,8 @@ export * from "./importResolver/mod.ts"; export * from "./rootResolver/mod.ts"; export * from "./cacheResolver/mod.ts"; export * from "./chainResolver/mod.ts"; -export * from "./cacheLoader/mod.ts"; -export * from "./fileLoader/mod.ts"; +export { pluginCacheLoader, Opts as CacheLoaderOptions } from "./cacheLoader/mod.ts"; +export { pluginFileLoader, Opts as FileLoaderOptions } from "./fileLoader/mod.ts"; export * from "./terserTransform/mod.ts"; export * from "./typescriptTransform/mod.ts"; export * from "./typescriptCompile/mod.ts"; diff --git a/plugin/terserTransform/README.md b/plugin/terserTransform/README.md index bb06176..825485e 100644 --- a/plugin/terserTransform/README.md +++ b/plugin/terserTransform/README.md @@ -9,13 +9,17 @@ Use Terser to minify/compress/mangle/... your bundle. ## Usage ```ts -import { pluginRootResolver } from "https://cdn.jsdelivr.net/gh/denofn/denopack@latest/plugin/rootResolver/mod.ts"; +import { pluginImportResolver } from "https://cdn.jsdelivr.net/gh/denofn/denopack@latest/plugin/importResolver/mod.ts"; import { pluginFileLoader } from "https://cdn.jsdelivr.net/gh/denofn/denopack@latest/plugin/fileLoader/mod.ts"; import { pluginTypescriptTransform } from "https://cdn.jsdelivr.net/gh/denofn/denopack@latest/plugin/typescriptTransform/mod.ts"; import { pluginTerserTransform } from "https://cdn.jsdelivr.net/gh/denofn/denopack@latest/plugin/terserTransform/mod.ts"; export default { - plugins: [pluginRootResolver(), pluginFileLoader(), pluginTypescriptTransform({ ...myOptions })], + plugins: [ + pluginImportResolver(), + pluginFileLoader(), + pluginTypescriptTransform({ ...myOptions }), + ], output: { plugins: [pluginTerserTransform()], }, diff --git a/plugin/typescriptCompile/mod.ts b/plugin/typescriptCompile/mod.ts index 34fd35c..a29c5a2 100644 --- a/plugin/typescriptCompile/mod.ts +++ b/plugin/typescriptCompile/mod.ts @@ -4,8 +4,8 @@ import { resolver } from "../../util/resolver.ts"; import { stripFileProtocol } from "../../util/stripFileProtocol.ts"; type Options = { - useAsLoader: boolean; - compilerOptions: Deno.CompilerOptions; + useAsLoader?: boolean; + compilerOptions?: Deno.CompilerOptions; }; let modules: Record; @@ -39,13 +39,11 @@ async function resolveId( return importee; } -export function pluginTypescriptCompile( - { useAsLoader, compilerOptions }: Options = { useAsLoader: false, compilerOptions: {} } -): Plugin { +export function pluginTypescriptCompile({ useAsLoader, compilerOptions }: Options = {}): Plugin { return { name: "denopack-plugin-typescriptCompile", async resolveId(importee, importer) { - return resolveId(compilerOptions, importee, importer); + return resolveId(compilerOptions ?? {}, importee, importer); }, async load(id) { diff --git a/plugin/typescriptTransform/README.md b/plugin/typescriptTransform/README.md index 5a8dccf..c16e78b 100644 --- a/plugin/typescriptTransform/README.md +++ b/plugin/typescriptTransform/README.md @@ -13,11 +13,15 @@ Uses the internal `Deno.transpileOnly` [compiler API](https://deno.land/manual/r ## Usage ```ts -import { pluginRootResolver } from "https://cdn.jsdelivr.net/gh/denofn/denopack@latest/plugin/rootResolver/mod.ts"; +import { pluginImportResolver } from "https://cdn.jsdelivr.net/gh/denofn/denopack@latest/plugin/importResolver/mod.ts"; import { pluginFileLoader } from "https://cdn.jsdelivr.net/gh/denofn/denopack@latest/plugin/fileLoader/mod.ts"; import { pluginTypescriptTransform } from "https://cdn.jsdelivr.net/gh/denofn/denopack@latest/plugin/typescriptTransform/mod.ts"; export default { - plugins: [pluginRootResolver(), pluginFileLoader(), pluginTypescriptTransform({ ...myOptions })], + plugins: [ + pluginImportResolver(), + pluginFileLoader(), + pluginTypescriptTransform({ ...myOptions }), + ], }; ``` diff --git a/util/checkIntegrity.ts b/util/checkIntegrity.ts new file mode 100644 index 0000000..6da693b --- /dev/null +++ b/util/checkIntegrity.ts @@ -0,0 +1,17 @@ +import { hash } from "../deps.ts"; + +export function checkIntegrity( + lockFile: Record, + filePath: string, + code: string +): void { + const sha256 = hash.createHash("sha256"); + const checksum = sha256.update(code).toString(); + if (lockFile[filePath] && lockFile[filePath] !== checksum) + throw new Error( + `Integrity of ${filePath} does not match the checksum in the provided lockfile!` + ); + else { + console.log(`Integrity of ${filePath} matches lockfile`); + } +}