webpack 5 Node.js polyfill
webpack 4 以前では Node.js core module の polyfill が webpack 自体に同梱されていて、bundle したい module が 1 つでも core module の機能を使っていれば自動的にその polyfill が追加されていました。 しかし webpack 5 からはこの自動 polyfill が廃止され、真に必要な場合にのみユーザが手動で polyfill を追加するというようになりました 1 。
具体例を挙げると、react-markdown は unified 2 を通じて vfile というパッケージに依存しており 3、vfile は path
module に依存している 4 ので、webpack 5 では何も polyfill の設定をしないと react-markdown を利用したコードのビルドに失敗します。
小さな例で実験してみましょう。
webpack v4 の場合
package.json
{ "scripts": { "build": "webpack --mode production" }, "devDependencies": { "@types/react": "^17.0.0", "@types/react-dom": "^17.0.0", "ts-loader": "^8.0.11", "typescript": "^4.1.2", "webpack": "^4.44.2", "webpack-cli": "^4.2.0" }, "dependencies": { "react": "^17.0.1", "react-dom": "^17.0.1", "react-markdown": "^5.0.3" } }
webpack.config.js
const path = require("path"); module.exports = { entry: path.resolve(__dirname, "src/index.tsx"), output: { filename: "bundle.js", path: path.resolve(__dirname, "dist"), }, module: { rules: [ { test: /\.tsx?$/, use: { loader: "ts-loader", } }, ], }, resolve: { extensions: [".ts", ".tsx", ".js"], }, }
tsconfig.json
{ "compilerOptions": { "target": "es2020", "module": "esnext", "moduleResolution": "node", "jsx": "react-jsx", "strict": true, "esModuleInterop": true } }
src/index.tsx
import ReactDOM from "react-dom"; import { Markdown } from "./components/Markdown"; ReactDOM.render(<Markdown></Markdown>, document.getElementById("root"));
import ReactMarkdown from "react-markdown"; export const Markdown = () => { const body = ` ## Hello, Markdown! Hello! react-markdown is [here](https://github.com/remarkjs/react-markdown). `; return ( <> <ReactMarkdown children={body}></ReactMarkdown> </> ); };
dist/index.html
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> </head> <body> <div id="root"></div> <script src="bundle.js"></script> </body> </html>
この状態で npm run build
を実行するとビルドは正常に完了し、 dist/index.html をブラウザで開けば想像通りの画面が表示されます
(ちなみにこの状態で bundle.js のサイズは 210 KiB でした)。
webpack v5 の場合
しかし上記の状態で webpack を最新バージョン(執筆時点では v5.9.0)に上げると:
- "webpack": "^4.44.2", + "webpack": "^5.9.0",
npm run build
は path
を解決できずに失敗します:
ERROR in ./node_modules/vfile/core.js 3:11-26 Module not found: Error: Can't resolve 'path' in '/<path-to-the-current-directory>/node_modules/vfile' BREAKING CHANGE: webpack < 5 used to include polyfills for node.js core modules by default. This is no longer the case. Verify if you need this module and configure a polyfill for it. If you want to include a polyfill, you need to: - add a fallback 'resolve.fallback: { "path": require.resolve("path-browserify") }' - install 'path-browserify' If you don't want to include a polyfill, you can use an empty module like this: resolve.fallback: { "path": false } @ ./node_modules/vfile/index.js 4:12-32 @ ./node_modules/unified/index.js 8:12-28 @ ./node_modules/react-markdown/lib/react-markdown.js 5:14-32 @ ./src/components/Markdown.tsx 2:0-43 9:45-58 @ ./src/index.tsx 3:0-49 4:21-29
そこで、エラーメッセージに従って path-browserify をインストールし resolve.fallback
の設定 5 を行います:
package.json
"devDependencies": {
"@types/react": "^17.0.0",
"@types/react-dom": "^17.0.0",
+ "path-browserify": "^1.0.1",
"ts-loader": "^8.0.11",
"typescript": "^4.1.2",
"webpack": "^5.9.0",
webpack.config.js
resolve: { extensions: [".ts", ".tsx", ".js"], + fallback: { + "path": require.resolve("path-browserify"), + } }, }
すると一応 npm run build
は成功するようになります。
が、 dist/index.html をブラウザで開くと画面には何も表示されず、コンソールに ReferenceError: process is not defined
というエラーが表示されるでしょう。これは実は vfile が process
という名のグローバルオブジェクトに依存しているためです 6 。
Node.js では process
はグローバルな文脈に存在します 7 が、ブラウザ環境では process
の shim は手で行わなければなりません。そういった特定のグローバル変数の shim を行うために imports-loader という loader が存在します:
こいつの仕事は主に test
でマッチしたファイルの先頭に何らかのモジュールを import することです。
そこで
% npm i -D imports-loader process
の後 imports-loader の設定 8 を行い
webpack.config.js
loader: "ts-loader", } }, + { + test: /node_modules\/vfile\/core\.js/, + use: { + loader: "imports-loader", + options: { + type: "commonjs", + imports: ["single process/browser process"], + }, + }, + }, ], },
改めて npm run build
を実行すれば、晴れて react-markdown が使えるようになります。
ちなみにこの状態で bundle.js のサイズは 208 KiB でした。きめ細かく polyfill を設定できるようになったことで若干バンドルサイズも小さくなり、嬉しいですね。
-
https://webpack.js.org/blog/2020-10-10-webpack-5-release/#automatic-nodejs-polyfills-removed↩
-
https://github.com/remarkjs/react-markdown/blob/5.0.0/package.json#L47↩
-
https://github.com/unifiedjs/unified/blob/9.0.0/package.json#L51↩
-
https://github.com/webpack/webpack.js.org/blob/dd7ed89d8b31bdb342fee26173d0b3c81266a292/src/content/configuration/resolve.md#resolvefallback↩
-
https://nodejs.org/docs/latest-v14.x/api/process.html#process_process↩
-
https://github.com/vfile/vfile/issues/38#issuecomment-683198538↩
warp filter 設定
warp
warp は reqwest や hyper の作者である Sean McArthur 氏が hyper の上で作っている Rust 製の Web フレームワーク: github.com
執筆時点での最新バージョンは 0.2.5: docs.rs
どのエンドポイントにどんな種類のリクエストが来たらどんな処理をするかを記述するための Filter と呼ばれる概念があり、これを使ってサーバ側の処理を書いていくのが warp の特徴。 README 1 に乗っている例はこんな感じ:
[dependencies] tokio = { version = "0.2", features = ["full"] } warp = "0.2"
use warp::Filter; #[tokio::main] async fn main() { // GET /hello/warp => 200 OK with body "Hello, warp!" let hello = warp::path!("hello" / String) .map(|name| format!("Hello, {}!", name)); warp::serve(hello) .run(([127, 0, 0, 1], 3030)) .await; }
組み立てた filter を warp::serve
に渡して走らせる。
Filter
Built-in Filter の一覧がここに載っている:
眺めてみるだけでもなんとなく使い方が予想できるのではないか。
path
起点となるのは path
filter かもしれない:
その名の通り path を定義する。
ドキュメント 2 にあるコード例は:
use warp::Filter; // GET /hi let hi = warp::path("hi").map(|| { "Hello, World!" });
param
path からパラメータを抜き出したいときは param
を使う:
warp::filters::path::param - Rust
こんな感じ:
use warp::Filter; // GET /sum/:u32/:u32 let sum = warp::path("sum") .and(warp::path::param::<u32>()) .and(warp::path::param::<u32>()) .map(|a, b| format!("{} + {} = {}", a, b, a + b));
path や param を定義するのに便利な path!
マクロもある:
ドキュメント 3 にあるコード例:
use warp::Filter; // GET /hello/from/warp let hello_from_warp = warp::path!("hello" / "from" / "warp").map(|| { "Hello from warp!" }); // GET /sum/:u32/:u32 let sum = warp::path!("sum" / u32 / u32).map(|a, b| { format!("{} + {} = {}", a, b, a + b) });
header
header
filter もよく使いそう:
例えばこんな感じ:
use warp::Filter; let authorization_token = warp::header::<String>("authorization").map(|autorization: String| { autorization .trim() .strip_prefix("Bearer ") .unwrap_or("") .to_string() });
CORS
場合によっては CORS のことも考えないといけない。
warp には cors
filter もある:
warp::cors
を呼ぶと warp::filters::cors::Builder が返るので適宜設定していく。
allow_headers
, allow_methods
, allow_origin
らへんを主に使うと思う。こんな感じに:
use warp::Filter; let allowed_origin = env::var("ALLOWED_ORIGIN").expect("ALLOWED_ORIGIN must be set"); let cors = warp::cors() .allow_origin(allowed_origin.as_str()) .allow_headers(vec!["authorization"]) .allow_methods(vec!["GET", "POST", "PUT", "DELETE"]);
warp の cors
のテストコード も参考になると思う。
ちなみに cors
filter を設定しておくとプリフライトリクエストの処理は warp 側でよしなにやってくれるので、アプリケーションコードには OPTIONS
リクエストの処理は書かずに単純に目的のエンドポイントの処理だけ書けば良い。
Filter の合成
filter たちを合成してルーティングを構築したり値を抜き出して加工したりするためのメソッドが Filter
trait には定義されている:
and, or
一番使うのは and
と or
だと思う。
前述までの例にも出てきていたが、例えばこんな感じでルーティングを定義したりできる:
use warp::Filter; let api = warp::path("api"); let v1 = warp::path("v1"); let users = warp::path("users").map(|| { let data = vec!["Alice", "Bob", "Carol"]; warp::reply::json(&data) }); let posts = warp::path("posts").map(|| { let data = vec!["Hello", "Nice", "Uh-oh"]; warp::reply::json(&data) }); let routes = api.and(v1.and(users.or(posts)));
map, and_then
filter たちが抜き出したり作ったりした値を加工して返す。
ドキュメント 4 にあるコード例:
use warp::Filter; // Map `/:id` warp::path::param().map(|id: u64| { format!("Hello #{}", id) });
and_then
に渡すクロージャは TryFuture
を返し、 Err
内部の値は Rejection
型である必要があるとドキュメントに書かれていて、実際ドキュメント 5 のコード例にも
use warp::Filter; // Validate after `/:id` warp::path::param().and_then(|id: u64| async move { if id != 0 { Ok(format!("Hello #{}", id)) } else { Err(warp::reject::not_found()) } });
と書かれている。
勘違いしやすいが、4xx/5xx エラーであったとしても必ずしも Rejection とするべきとは限らない。 warp の filter システムにおける Rejection というのは「この filter では前提条件を満たせず処理できなかったけど、他の filter なら正常に処理できるかもしれないのでそっちに期待します」という意思の表明であって、マッチしたフィルター内で 4xx/5xx エラーが発生したとしてそれを最終結果としたい(他の filter に移りたくない)のであれば、 Err(Rejection) よりむしろステータスコードとともに Ok(Reply) を返すべきである。
Sean McArthur 氏のコメント や examples/todos も参考になるかもしれない。
boxed
filter の数が増えてくると、それらを関心別に関数に分割したくなると思う。
しかし単純にやろうとすると、こういうダルい型を書かなくてはならない:
use warp::Filter; fn routes() -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone { let api = warp::path("api"); let v1 = warp::path("v1"); api.and(v1.and(users().or(posts()))) } fn users() -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone { warp::path("users").map(|| { let data = vec!["Alice", "Bob", "Carol"]; warp::reply::json(&data) }) } fn posts() -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone { warp::path("posts").map(|| { let data = vec!["Hello", "Nice", "Uh-oh"]; warp::reply::json(&data) }) }
こういう処理をより楽に書くために boxed
というメソッドが用意されている。 boxed
は BoxedFilter
という型を返し、こんな風に書ける:
use warp::{Filter, filters::BoxedFilter, Reply}; fn routes() -> BoxedFilter<(impl Reply,)> { let api = warp::path("api"); let v1 = warp::path("v1"); api.and(v1.and(users().or(posts()))).boxed() } fn users() -> BoxedFilter<(impl Reply,)> { warp::path("users") .map(|| { let data = vec!["Alice", "Bob", "Carol"]; warp::reply::json(&data) }) .boxed() } fn posts() -> BoxedFilter<(impl Reply,)> { warp::path("posts") .map(|| { let data = vec!["Hello", "Nice", "Uh-oh"]; warp::reply::json(&data) }) .boxed() }
いかがでしたか
他にもいろいろありますがとりあえずこの辺で。
Template string types and mapped type 'as' clauses 機能説明(意訳)
(これは以下の PR で Anders Hejlsberg 氏が最初にコメントした内容を意訳しただけの個人用メモです)
この PR は以下 2 つの新機能を実装します:
- テンプレート文字列型, ジェネリックなプレースホルダーが埋め込まれた文字列リテラルの形をとり、型インスタンス化を通してプレースホルダが実際の文字列リテラルで置き換えられるような型
- Mapped type
as
節, mapped types においてプロパティ名を変換できるようにする
テンプレート文字列型
テンプレート文字列型はテンプレート文字列式の型版 (the type space equivalent of template string expressions) です。テンプレート文字列式と同様、テンプレート文字列型はバックティックで囲まれ、${T}
の形のプレースホルダを含めることができます。ここで T
は string
, number
, boolean
, または bigint
にアサイン可能な型です。テンプレート文字列型ではリテラル文字列を結合したり、非 string なプリミティブ型をその文字列表現に変換したり、文字列リテラルの大文字小文字を変えることができます。さらに、型推論によって、テンプレート文字列型はシンプルな形式の文字列パターンマッチと分解を行ってくれます。
テンプレート文字列型は以下のように解決されます:
- プレースホルダ内の union 型はテンプレート文字列全体に分配されます。例えば
`[${A|B|C}]`
は`[${A}]` | `[${B}]` | `[${C}]`
に解決します。複数のプレースホルダ内の union 型はクロス積に解決します。例えば`[${A|B},${C|D}]`
は`[${A},${C}]` | `[${A},${D}]` | `[${B},${C}]` | `[${B},${D}]`
に解決します。 - プレースホルダ内の string, number, boolean, そして bigint リテラル型はそのリテラル型の文字列表現でプレースホルダを置き換えます。例えば
`[${'abc'}]`
は`[abc]`
に、`[${42}]`
は`[42]`
に解決します。 - プレースホルダ内に
any
,string
,number
,boolean
, またはbigint
型のうちのいずれかが 1 つでもあれば、そのテンプレート文字列はstring
型に解決します。 - プレースホルダ内に
never
型があれば、そのテンプレート文字列はnever
型に解決します。
例をいくつか:
type EventName<T extends string> = `${T}Changed`; type Concat<S1 extends string, S2 extends string> = `${S1}${S2}`; type ToString<T extends string | number | boolean | bigint> = `${T}`; type T0 = EventName<'foo'>; // 'fooChanged' type T1 = EventName<'foo' | 'bar' | 'baz'>; // 'fooChanged' | 'barChanged' | 'bazChanged' type T2 = Concat<'Hello', 'World'>; // 'HelloWorld' type T3 = `${'top' | 'bottom'}-${'left' | 'right'}`; // 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right' type T4 = ToString<'abc' | 42 | true | -1234n>; // 'abc' | '42' | 'true' | '-1234'
Union 型をクロス積分配するとあっという間に非常に大きくてコストのかかる型へと拡大する可能性があることに注意してください。また union 型の構成要素は 100,000 未満に制限されることにも注意してください。以下のコードはエラーになります:
type Digit = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9; type Zip = `${Digit}${Digit}${Digit}${Digit}${Digit}`; // Error
テンプレート文字列プレースホルダは型の前に uppercase
, lowercase
, capitalize
, または uncapitalize
修飾子を任意で指定できます。この修飾子は置き換えられる文字列の全体または先頭文字の大文字小文字を変化させます。例えば:
type GetterName<T extends string> = `get${capitalize T}`; type Cases<T extends string> = `${uppercase T} ${lowercase T} ${capitalize T} ${uncapitalize T}`; type T10 = GetterName<'foo'>; // 'getFoo' type T11 = Cases<'bar'>; // 'BAR bar Bar bar' type T12 = Cases<'BAR'>; // 'BAR bar BAR bAR'
テンプレート文字列型はすべて string
型とそのサブタイプにアサインできます。さらに、テンプレート文字列型 `${T}`
は `${C}`
(ここで C
は T
の文字列リテラル型制約) とそのサブタイプにアサインできます。例えば:
function test<T extends 'foo' | 'bar'>(name: `get${capitalize T}`) { let s1: string = name; let s2: 'getFoo' | 'getBar' = name; }
型推論では文字列リテラル型からテンプレート文字列型への推論をサポートしています。推論がうまくいくためには、推論先となるテンプレート文字列型の中の各プレースホルダが少なくとも1文字のスパンで隔てられていなければなりません(言い換えると、ぴったり隣り合ったプレースホルダたちへと推論することはできません)。各スパンを左から右へと非貪欲マッチを行いそのスパンで隔てられた2つの部分文字列からプレースホルダ型への推論を行うことで推論が進みます。例をいくつか:
type MatchPair<S extends string> = S extends `[${infer A},${infer B}]` ? [A, B] : unknown; type T20 = MatchPair<'[1,2]'>; // ['1', '2'] type T21 = MatchPair<'[foo,bar]'>; // ['foo', 'bar'] type T22 = MatchPair<' [1,2]'>; // unknown type T23 = MatchPair<'[1,2] '>; // unknown type T24 = MatchPair<'[1,2,3,4]'>; // ['1', '2,3,4']
テンプレート文字列型を再帰的 conditional types と組み合わせて、繰り返しパターン上をイテレートする Join
型と Split
型を書くことができます。
type Join<T extends (string | number | boolean | bigint)[], D extends string> = T extends [] ? '' : T extends [unknown] ? `${T[0]}` : T extends [unknown, ...infer U] ? `${T[0]}${D}${Join<U, D>}` : string; type T30 = Join<[1, 2, 3, 4], '.'>; // '1.2.3.4' type T31 = Join<['foo', 'bar', 'baz'], '-'>; // 'foo-bar-baz' type T32 = Join<[], '.'>; // ''
type Split<S extends string, D extends string> = string extends S ? string[] : S extends `${infer T}${D}${infer U}` ? [T, ...Split<U, D>] : [S]; type T40 = Split<'foo', '.'>; // ['foo'] type T41 = Split<'foo.bar.baz', '.'>; // ['foo', 'bar', 'baz'] type T42 = Split<any, '.'>; // string[]
再帰的推論機能は、例えば、プロパティにドット繋ぎでアクセスする関数や、 JavaScript フレームワークでたまに使われるパターンを強く型付けするのに使えます。
type PropType<T, Path extends string> = string extends Path ? unknown : Path extends keyof T ? T[Path] : Path extends `${infer K}.${infer R}` ? K extends keyof T ? PropType<T[K], R> : unknown : unknown; declare function getPropValue<T, P extends string>(obj: T, path: P): PropType<T, P>; declare const s: string; const obj = { a: { b: {c: 42, d: 'hello' }}}; getPropValue(obj, 'a'); // { b: {c: number, d: string } } getPropValue(obj, 'a.b'); // {c: number, d: string } getPropValue(obj, 'a.b.d'); // string getPropValue(obj, 'a.b.x'); // unknown getPropValue(obj, s); // unknown
Mapped type as
節
この PR により, mapped types はオプションの as
節をサポートします。 as
節を通して、生成されるプロパティ名の変換方法を指定できます:
{ [P in K as N]: X }
ここで N
は string
型にアサイン可能な型でなければなりません。 通常、N
は、P
をプレースホルダ内で利用するテンプレート文字列型のような、P
を変換した型です。例えば:
type Getters<T> = { [P in keyof T & string as `get${capitalize P}`]: () => T[P] }; type T50 = Getters<{ foo: string, bar: number }>; // { getFoo: () => string, getBar: () => number }
上記において、keyof T & string
の交差が必要です。なぜなら keyof T
はテンプレート文字列型を使って変換できない symbol
型を含むことがあるからです。
as
節内で指定された型が never
に解決した場合、その key のプロパティは生成されません。したがって、as
節はフィルターとして使えます:
type Methods<T> = { [P in keyof T as T[P] extends Function ? P : never]: T[P] }; type T60 = Methods<{ foo(): number, bar: boolean }>; // { foo(): number }
as
節内で指定された型がリテラル型の union に解決した場合、同じ型の複数のプロパティが生成されます:
type DoubleProp<T> = { [P in keyof T & string as `${P}1` | `${P}2`]: T[P] } type T70 = DoubleProp<{ a: string, b: number }>; // { a1: string, a2: string, b1: number, b2: number }
Fixes #12754.
Playground: https://www.typescriptlang.org/play?ts=4.1.0-pr-40336-88