UserScriptをbundleするDeno script
一部のESModuleをまとめないで import
のままにする事もできる
既に自分のprojectにあるコードをimport先にすることができる
用途
たくさんのESModuleに分かれた他者のUserScriptを自分のprojectに移植するときに使う
すべてのファイルを手作業でコピペするのは地獄なので、一つにまとめたほうがいい
2021-06-08 22:21:21 script読み込みの高速化以外にメリットなくね?
確かに自分のprojectにコピペすれば、そのscriptに誰かが悪意のあるコードを入れられなくなるが、すでに悪意のあるコードが混入している場合には無力
1ファイルずつ、自分で変なコードが混じっていないか確認する必要がある
既知の問題
dynamic importに相対パスを使っているとうまくコードが動かなくなる
workerのソースも同様
scrapboxのUserScriptの場合、 ../
や /api/code
から始まるパスなら大丈夫
./
で同じページ内のファイルを指定していたときに事故る
deno bundleだと↓はうまくコードを省いてくれるが、本ページのscriptだと余計なものまで含まれてしまう
script.jsexport {lightFormat, sub, getUnixTime} from 'https://deno.land/x/date_fns@v2.15.0/index.js';
importmap.json{
"imports": {
"../date-fns.min.js/script.js": "https://deno.land/x/date_fns@v2.15.0/index.js"
}
}
いやちゃんとtree shakingされた
気のせいだった?
reloadしなかったせい?
使い方
以下を実行する
shdeno run -A --unstable https://scrapbox.io/api/code/takker/UserScriptをbundleするDeno_script/build.ts filename.js --bundle --minify --charset=utf8 --outfile=filename.min.js --external=xxx --external=yyy --importMap=map.json
filename.js
bundleしたいscriptのentry point
localのファイルでもインターネット上のファイルでも可
--outfile
bundle後のJSファイルのファイル名
指定しないとコードが標準出力に出力される
パイプを使って | pbcopy
や | xsel
をつなげれば、そのままクリップボードにコピーできる
--external
bundleに含めたくないESModuleのパス名
JSファイルに書き込まれているパス名をそのまま書く
相対パスで指定されていたら相対パスで、絶対パスで指定されていたら絶対パスで書く
挙動はただの文字列判定
import {...} from 'xxx'
の xxx
が --external
に指定した文字列と一致した場合、そのmoduleをbundleから除外する
--importMap
localファイルでもインターネット上のファイルでも可
あとはesbuildの引数と同じ
JSファイルに埋め込むときは --sourcemap=inline
にする
関連
実装したいこと
esbuildのオプション引数を全部羅列するのが面倒か
2022-02-01
13:52:06 コード雑だな……
直したい
2021-06-05
12:45:46
build errorが出てもcacheを消す
refactor: 型をつける
12:03:40
APIとCLIを分離した
esbuildのversionを固定した
2021-05-28
22:36:22 esbuildのcache directoryを消すようにした
2021-05-26
18:17:27 --import-map
を --importMap
にした
18:08:33 --external
が一つのときにバグっていたのを直した
code
build.tsimport { run } from './script.ts';
import { parse } from 'https://deno.land/std@0.87.0/flags/mod.ts';
const {_: [entryFilePath], importMap, external, ...rest} = parse(Deno.args);
if (typeof entryFilePath === 'number') throw Error('entryFilePath must be string');
let imports: {[key: string]: string} = {};
if (importMap) {
if (/^https?:\/\//.test(importMap)) {
const res = await fetch(importMap);
imports = await res.json();
} else {
imports = JSON.parse(await Deno.readTextFile(importMap));
}
}
await run(entryFilePath, imports, {external, ...rest});
script.tsimport { exists } from "https://deno.land/std@0.97.0/fs/mod.ts";
import { build, stop } from 'https://deno.land/x/esbuild@v0.12.6/mod.js';
import type { BuildOptions, BuildResult } from 'https://deno.land/x/esbuild@v0.12.6/mod.js';
import { cache } from 'https://raw.githubusercontent.com/takker99/esbuild-plugin-cache/master/deno/mod.ts';
// 特定のpropertyを削るやつ
type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
export async function run(filename: string, imports: {[key: string]: string}, {external, ...rest }: Omit<BuildOptions, 'entryPoints' | 'platform' | 'plugins'>) {
let useTempFile = false;
let result: BuildResult | undefined = undefined;
try {
if (/^https?:\/\//.test(filename)) {
const tempname = `index-${Math.random()}.ts`;
await Deno.writeTextFile(tempname, `import '${filename}';export * from '${filename}';`);
filename = tempname;
useTempFile = true;
}
const options: BuildOptions = {
entryPoints: [filename],
platform: 'neutral',
plugins: [cache({directory: './cache', importmap: {imports}})],
external: Array.isArray(external) ? external : (external ? [external] : []),
...rest
};
result = await build(options);
stop();
} catch(e) {
throw e;
}finally {
//後始末
if (useTempFile) await Deno.remove(filename);
if (await exists('./cache')) await Deno.remove('./cache', { recursive: true });
}
return result;
}