├── .gitignore ├── .vscode └── setting.json ├── README.md ├── _app.tsx ├── amdxg.config.js ├── config └── rollup.config.js ├── decls.d.ts ├── docs ├── 202005182044-awesome-next-issg.mdx ├── 202005271609-react-app-context.mdx ├── 202005280125-eval-esm-in-iframe.mdx ├── 202006061823-tiny-bundler.mdx ├── 202006091517-todays-react-jsonschema-form.mdx ├── 202006101314-switch-tsconfig-on-webpack.mdx ├── 202006161259-try-netlify-lambda.mdx ├── 202006161500-netlify-lambda-chromium.mdx ├── 202006210107-avoid-ie-first.mdx ├── 202006211925-support-ogp.mdx ├── amdx-0.8.mdx ├── amp-social-share-for-hatena-bookmark.mdx ├── hello-deno-1.mdx ├── mdxx-0.6.mdx ├── mdxx-0.7.mdx ├── mdxx-cli-and-components.mdx ├── mdxx-introduction.mdx ├── next-amp-tailwind-postcss.mdx ├── study-next-amp-by-mdxx-ssg.mdx └── study-recoil.mdx ├── docs_wip ├── 202005200249-engineer-career-pattern.mdx ├── 202005212041-javascript-for-starter.mdx └── age-of-next-js.mdx ├── fragments └── nextjs │ ├── 00.mdx │ ├── 01.mdx │ ├── 02.mdx │ ├── 03.mdx │ ├── 04.mdx │ ├── 05.mdx │ ├── 10.mdx │ └── 11.mdx ├── next-env.d.ts ├── next.config.js ├── package.json ├── pages ├── [slug].tsx ├── _document.tsx ├── index.tsx ├── slides │ └── develop-mizchi-dev.tsx ├── tags │ ├── [tag].tsx │ └── index.tsx └── try.tsx ├── public ├── android-chrome-192x192.png ├── android-chrome-512x512.png ├── apple-touch-icon.png ├── favicon.ico ├── icon-16x16.png ├── icon-32x32.png ├── install-sw.html ├── manifest.json ├── ogp │ ├── 202005182044-awesome-next-issg.png │ ├── 202005271609-react-app-context.png │ ├── 202005280125-eval-esm-in-iframe.png │ ├── 202006061823-tiny-bundler.png │ ├── 202006091517-todays-react-jsonschema-form.png │ ├── 202006101314-switch-tsconfig-on-webpack.png │ ├── 202006161259-try-netlify-lambda.png │ ├── 202006161500-netlify-lambda-chromium.png │ ├── 202006210107-avoid-ie-first.png │ ├── amdx-0.8.png │ ├── amp-social-share-for-hatena-bookmark.png │ ├── hello-deno-1.png │ ├── mdxx-0.6.png │ ├── mdxx-0.7.png │ ├── mdxx-cli-and-components.png │ ├── mdxx-introduction.png │ ├── next-amp-tailwind-postcss.png │ ├── study-next-amp-by-mdxx-ssg.png │ └── study-recoil.png └── sw.js ├── script ├── counter.tsx └── generate-ogp.mjs ├── slides └── develop-mizchi-dev.mdx ├── text.png ├── tsconfig.json └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | ### https://raw.github.com/github/gitignore/55df35ee63aef4a6f859559af980c9fb87bee1a1/Node.gitignore 2 | 3 | # Logs 4 | logs 5 | *.log 6 | npm-debug.log* 7 | yarn-debug.log* 8 | yarn-error.log* 9 | lerna-debug.log* 10 | 11 | # Diagnostic reports (https://nodejs.org/api/report.html) 12 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 13 | 14 | # Runtime data 15 | pids 16 | *.pid 17 | *.seed 18 | *.pid.lock 19 | 20 | # Directory for instrumented libs generated by jscoverage/JSCover 21 | lib-cov 22 | 23 | # Coverage directory used by tools like istanbul 24 | coverage 25 | *.lcov 26 | 27 | # nyc test coverage 28 | .nyc_output 29 | 30 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 31 | .grunt 32 | 33 | # Bower dependency directory (https://bower.io/) 34 | bower_components 35 | 36 | # node-waf configuration 37 | .lock-wscript 38 | 39 | # Compiled binary addons (https://nodejs.org/api/addons.html) 40 | build/Release 41 | 42 | # Dependency directories 43 | node_modules/ 44 | jspm_packages/ 45 | 46 | # TypeScript v1 declaration files 47 | typings/ 48 | 49 | # TypeScript cache 50 | *.tsbuildinfo 51 | 52 | # Optional npm cache directory 53 | .npm 54 | 55 | # Optional eslint cache 56 | .eslintcache 57 | 58 | # Microbundle cache 59 | .rpt2_cache/ 60 | .rts2_cache_cjs/ 61 | .rts2_cache_es/ 62 | .rts2_cache_umd/ 63 | 64 | # Optional REPL history 65 | .node_repl_history 66 | 67 | # Output of 'npm pack' 68 | *.tgz 69 | 70 | # Yarn Integrity file 71 | .yarn-integrity 72 | 73 | # dotenv environment variables file 74 | .env 75 | .env.test 76 | 77 | # parcel-bundler cache (https://parceljs.org/) 78 | .cache 79 | 80 | # Next.js build output 81 | .next 82 | 83 | # Nuxt.js build / generate output 84 | .nuxt 85 | dist 86 | 87 | # Gatsby files 88 | .cache/ 89 | # Comment in the public line in if your project uses Gatsby and not Next.js 90 | # https://nextjs.org/blog/next-9-1#public-directory-support 91 | # public 92 | 93 | # vuepress build output 94 | .vuepress/dist 95 | 96 | # Serverless directories 97 | .serverless/ 98 | 99 | # FuseBox cache 100 | .fusebox/ 101 | 102 | # DynamoDB Local files 103 | .dynamodb/ 104 | 105 | # TernJS port file 106 | .tern-port 107 | 108 | # Stores VSCode versions used for testing VSCode extensions 109 | .vscode-test 110 | 111 | out 112 | .netlify 113 | gen 114 | public/amp-script -------------------------------------------------------------------------------- /.vscode/setting.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.wordWrap": "on" 3 | } 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MDXX SSG 2 | 3 | ## Features 4 | 5 | - Build amdx on next ssg 6 | - Support AMP 7 | 8 | ## Create your own blog 9 | 10 | ```bash 11 | # install node and npm 12 | npx degit mizchi/amdx/packages/ssg blog 13 | cd blog 14 | npm install 15 | cp amdxg.config.example amdxg.config 16 | # edit amdxg.config for you 17 | 18 | # create new page 19 | npm run new-page 20 | # edit docs/[current-date].mdx 21 | 22 | # create new page with slug 23 | npm run new-page foo 24 | # edit docs/foo.mdx 25 | 26 | # Preview 27 | npm run dev 28 | 29 | # Deploy 30 | npm run build 31 | npm run deploy 32 | ``` 33 | -------------------------------------------------------------------------------- /_app.tsx: -------------------------------------------------------------------------------- 1 | export function MyApp({ Component, pageProps }) { 2 | return ; 3 | } 4 | -------------------------------------------------------------------------------- /amdxg.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | useOgp: true, 3 | host: "https://mizchi.dev", 4 | siteName: "mizdev", 5 | author: "mizchi", 6 | authorLink: "https://twitter.com/mizchi", 7 | lang: "ja", 8 | gtag: "UA-165420141-1", 9 | repository: "mizchi/amdx", 10 | socialShare: { 11 | twitter: true, 12 | hatena: true, 13 | facebook: true, 14 | }, 15 | }; 16 | -------------------------------------------------------------------------------- /config/rollup.config.js: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import path from "path"; 3 | import nodeResolve from "@rollup/plugin-node-resolve"; 4 | import commonjs from "@rollup/plugin-commonjs"; 5 | import virtual from "@rollup/plugin-virtual"; 6 | import typescript from "rollup-plugin-typescript2"; 7 | import builtins from "rollup-plugin-node-builtins"; 8 | import { amdx } from "rollup-plugin-amdx"; 9 | import { terser } from "rollup-plugin-terser"; 10 | 11 | const plugins = [ 12 | builtins(), 13 | typescript({ 14 | tsconfigOverride: { 15 | compilerOptions: { 16 | jsx: "react", 17 | target: "es2017", 18 | module: "esnext", 19 | jsx: "react", 20 | noEmit: true, 21 | esModuleInterop: true, 22 | moduleResolution: "node", 23 | resolveJsonModule: true, 24 | isolatedModules: true, 25 | }, 26 | }, 27 | }), 28 | nodeResolve(), 29 | commonjs(), 30 | amdx(), 31 | terser({ 32 | module: true, 33 | }), 34 | ]; 35 | 36 | const RUN_TEMPLATE = (entryPath) => ` 37 | import { h, render } from "preact"; 38 | import Entry from "${entryPath}"; 39 | const root = document.querySelector(".root"); 40 | const encoded = root.id; 41 | const props = encoded ? JSON.parse(atob(encoded)) : {}; 42 | render(h(Entry, props), root); 43 | `; 44 | 45 | const SSR_TEMPLATE = (entryPath) => ` 46 | import { h } from "preact"; 47 | import renderToString from "preact-render-to-string"; 48 | import Entry from "${entryPath}"; 49 | export default (props) => renderToString(h(Entry, props)); 50 | `; 51 | 52 | const targetDir = path.join(__dirname, "../script"); 53 | const scriptNames = fs.readdirSync(targetDir); 54 | 55 | const config = scriptNames.map((name) => { 56 | const input = path.join(targetDir, name); 57 | const base = name.replace(".tsx", ""); 58 | return [ 59 | { 60 | input: `_$_${base}_run.js`, 61 | plugins: [ 62 | virtual({ [`_$_${base}_run.js`]: RUN_TEMPLATE(input) }), 63 | ...plugins, 64 | ], 65 | output: { 66 | file: `public/amp-script/${base}/run.js`, 67 | format: "iife", 68 | }, 69 | }, 70 | { 71 | input: [`_$_${base}_ssr.js`], 72 | plugins: [ 73 | virtual({ [`_$_${base}_ssr.js`]: SSR_TEMPLATE(input) }), 74 | ...plugins, 75 | ], 76 | output: { 77 | file: `public/amp-script/${base}/ssr.js`, 78 | format: "esm", 79 | }, 80 | }, 81 | ]; 82 | }); 83 | 84 | export default config.flat(); 85 | -------------------------------------------------------------------------------- /decls.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.mdx"; 2 | declare module "*.css"; 3 | 4 | import "react"; 5 | declare module "react" { 6 | interface StyleHTMLAttributes extends React.HTMLAttributes { 7 | jsx?: boolean; 8 | global?: boolean; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /docs/202005182044-awesome-next-issg.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Incremental Static Regeneration で実現する次世代のサーバーアーキテクチャ 3 | created: 1589802285779 4 | tags: [next, react, ssg] 5 | --- 6 | 7 | next.js 9.4 に Incremental Static Regeneration という実験的な新機能があります。 8 | 9 | [Blog \- Next\.js 9\.4 \| Next\.js](https://nextjs.org/blog/next-9-4) 10 | 11 | パッと見、「段階的な静的サイト生成…?なんのことだろう…」となったのですが、手元で試してみた感じ、これが既存のサーバーの実装アプローチを変える、革命的な機能ではないかと思いました。 12 | 13 | 解説を書きつつ、どのような応用があるか解説します。 14 | 15 | ## next.js の Incremental SSG を試してみる 16 | 17 | リポジトリはここです。 [mizchi/issg\-playground](https://github.com/mizchi/issg-playground) 18 | 19 | 解説にあたっては、必要なのはほぼこのファイルだけで、短いのでそのまま貼ります。 20 | 21 | ```tsx 22 | // pages/[slug].tsx 23 | import { GetStaticProps, GetStaticPaths } from "next"; 24 | 25 | type Props = { 26 | slug: string; 27 | builtAt: number; 28 | }; 29 | 30 | export const getStaticProps: GetStaticProps = async (ctx) => { 31 | return { 32 | props: { 33 | slug: ctx.params.slug as string, 34 | builtAt: Date.now(), 35 | }, 36 | unstable_revalidate: 30, 37 | }; 38 | }; 39 | 40 | export const getStaticPaths: GetStaticPaths = async () => { 41 | return { 42 | paths: ["/foo"], 43 | fallback: true, 44 | }; 45 | }; 46 | 47 | export default (props: Props) => { 48 | return ( 49 | <> 50 | {props.slug}: {props.builtAt} 51 | 52 | ); 53 | }; 54 | ``` 55 | 56 | - `pages/[slug].tsx` は `/*` をハンドルします。 57 | - `export const getStaticProps` はそのルーティングに来たときのルート要素に渡す props を組み立てます 58 | - `export const getStaticPaths` はそのルーティングに来たときの、パス一覧を返却します。 59 | 60 | これらの機能は元々 `next export` の静的アセットの吐き出しのために、手元のビルド時に一回だけ実行されるものでした。(このブログも、この機能を使って生成されています) 61 | 62 | しかし、 Incremental SSG では、このサーバーは静的に吐き出してデプロイされるのではなく、 server or serverless モードでデプロイすることを想定されています。 63 | 静的アセットの吐き出しサーバーを、動的なサーバーとしてデプロイする、とはどういうことでしょうか。 64 | 65 | ここで、 `unstable_revalidate: 30` と `fallback: true` に注目してください。 66 | 67 | - `getStaticPaths` で `fallback: true` が指定されると、 `paths` で指定されなかったパスも、 `getStaticProps` のロジックに応じて組み立てられます。 68 | - `getStaticProps` で `unstable_revalidate: 30` のような値を返すと、 30 秒間は静的アセットとして返却されます 69 | 70 | ここからが面白くて 71 | 72 | - unstable_revalidate: 30` の秒数が経過後、次のリクエストが発生した際に、一旦はキャッシュを返しつつ、バックグラウンドでもう一度そのページを構築 73 | 74 | この挙動が面白いですね。つまりは stale-while-revalidate の挙動です。 75 | 76 | つまりは、静的サイトジェネレータとしてある程度の運用の容易さを担保しつつ、CDN のスケーラビリティを借りて、かつ、ある程度は動的な振る舞いを取れる、ということです。 77 | 78 | [https://try-issg.now.sh/bar](https://try-issg.now.sh/bar) にデプロイしてあります。この挙動を念頭に起きながら、アクセスしてみてください。 79 | 80 | ## フロントエンドベストプラクティスの実現 81 | 82 | 自分は [光を超えるためのフロントエンドアーキテクチャ \- Speaker Deck](https://speakerdeck.com/mizchi/guang-wochao-erutamefalsehurontoendoakitekutiya) という発表をしたことがあります。要約すると、パフォーマンス最適化のためには、リクエストを全部アプリケーション・サーバーに到達させてはだめで、 CDN Edge に置いた HTML に当てつつ、キャッシュごとにサロゲートキーを当てて、リソースの更新のたびにプログラマブルなインバリデーションを発行する、というものです。 83 | 84 | 当時、これを実現できるのは fastly しかありませんでした。これからは vercel も似たようなことが可能なります。 85 | 86 | まだプログラマブルなインバリデーションはないのですが、RFC のディスカッションを読む限りは、開発者の rauchg と Timer 曰く、もっと多機能なものも考えているらしいので、 それを想定してるように見えます。 87 | 88 | [RFC: Incremental Static Regeneration · Discussion \#11552 · zeit/next\.js](https://github.com/zeit/next.js/discussions/11552) 89 | 90 | ## Vercel / SmartCDN / Next.js のゴールが見えた 91 | 92 | 正直なところ、 next.js が静的 export に対応した当時は、プロダクトとしての軸がぶれているように感じました。SSR のフレームワークでいきたいのか、 SSG になりたいのか、どっちなのかと。そして自前の PaaS を運用しているのは、よくわからないところがありました。 93 | 94 | Vercel (旧名 now.sh) は SmartCDN という機能があります。これはおそらく、この機能を見据えたプログラマブルな CDN として設計されたものだったのでしょう。 95 | 96 | Incremental SSG は、NoCode や Headless CMS のガワとして、next.js を使うことが想定されています。これらの NoCode Backend はお世辞にもスケーラビリティがあるとは言えないものが多く、またレスポンスタイムに難があることが多かったのですが、Incremental SSG モードの Next.js をかぶせることで(初回アクセスをやや犠牲にしつつも) CDN のスケーラビリティの恩恵を受けることができます。 97 | 98 | おそらく Vercel + SmartCDN は、next に最適化された CMS バックエンドとして、オールインワンパッケージを提供するのがゴールなのでしょう。 99 | 100 | ## next.js に投資したいと思った 101 | 102 | とりあえず RFC に fastly の SurrogateKeys 相当のキャッシュタグみたいなものがほしい!とだけ書いておきました。 103 | 104 | [RFC: Incremental Static Regeneration · Discussion \#11552 · zeit/next\.js](https://github.com/zeit/next.js/discussions/11552#discussioncomment-14415) 105 | 106 | また、ZEIT, あらため Vercel は 20 億円の増資を受けたことで、next.js エコシステムの発展は、より加速していくように思います。 107 | 108 | [\(20\) Shu Uesugi さんは Twitter を使っています 「🆕Next\.js の開発元でもある ZEIT はこのたび社名変更し Vercel になりました。 🎉2100 万ドルの資金調達も発表。 👨🏻‍💻 私はご縁があり 2 月に開発者としてジョインしました。 🤔「○○ は今後どうなるの?」というご質問につきましては、こちらの Notion ドキュメントをご一読ください!→ https://t\.co/eCwc23gIzo」 / Twitter](https://twitter.com/chibicode/status/1252745903540105216) 109 | 110 | ちょっと前まで、next.js は意見が強いフレームワークで、正直 nuxt のほうが使いやすいよなぁ、と思ってたんですが、こういう感じで攻めてくるのは以外で、びっくりしつつも、応援したい気持ちがありますね。 111 | -------------------------------------------------------------------------------- /docs/202005271609-react-app-context.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: React Context を用いた簡易 Store 3 | created: 1590563363484 4 | tags: [react, flux] 5 | --- 6 | 7 | ## 課題 8 | 9 | - redux を引っ張り出すと大仰になる。Context 下に共有ステートを持ってそこに setState できるだけでよい。 10 | - なので、次の 2 つを用意する 11 | - 現在の state を参照する `const appState = useAppState()` 12 | - 現在の state を更新する関数を返す `const setAppState = useSetAppState()` 13 | - `React.useState()` と違って分割している理由は、主にパフォーマンス上の理由 14 | - 大域な参照なので、可能な限りステートを参照したくない 15 | - `setState()` の API は `(prevState: State) => State` も取れるので、状態更新用途に限ってはそもそも `useAppState()` せずに済むことが多い 16 | - でも毎回書いてるけどボイラープレート感強い上に忘れるのでここにメモする 17 | 18 | ## 毎回書いてるボイラープレート 19 | 20 | ```tsx 21 | // src/contexts/AppStateContext.tsx 22 | import React, { Dispatch, SetStateAction, useContext, useState } from "react"; 23 | 24 | export type AppState = { 25 | value: number; 26 | }; 27 | 28 | const initialState: AppState = { 29 | value: 0, 30 | }; 31 | 32 | const AppStateContext = React.createContext(initialState); 33 | const SetAppStateContext = React.createContext< 34 | Dispatch> 35 | >(() => {}); 36 | 37 | export function useAppState() { 38 | return useContext(AppStateContext); 39 | } 40 | export function useSetAppState() { 41 | return useContext(SetAppStateContext); 42 | } 43 | 44 | export function AppStateProvider(props: { 45 | initialState?: AppState; 46 | children: React.ReactNode; 47 | }) { 48 | const [state, setState] = useState( 49 | props.initialState ?? initialState 50 | ); 51 | return ( 52 | 53 | 54 | {props.children} 55 | 56 | 57 | ); 58 | } 59 | ``` 60 | 61 | ## 使い方 62 | 63 | ```tsx 64 | import React from "react"; 65 | import ReactDOM from "react-dom"; 66 | import { 67 | AppStateProvider, 68 | useAppState, 69 | useSetAppState, 70 | } from "./contexts/AppStateContext"; 71 | 72 | function Counter() { 73 | const state = useAppState(); 74 | const setAppState = useSetAppState(); 75 | return ( 76 |
77 | {state.value} 78 | 85 |
86 | ); 87 | } 88 | 89 | function App() { 90 | return ( 91 | 92 | 93 | 94 | ); 95 | } 96 | 97 | ReactDOM.render(, document.querySelector("#root")); 98 | ``` 99 | -------------------------------------------------------------------------------- /docs/202005280125-eval-esm-in-iframe.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: iframe の中で ES Modules のコードを評価する 3 | created: 1590596703469 4 | tags: [] 5 | --- 6 | 7 | rollup のような ESM を吐くツールのプレビューツールを作っていると、文字列として組み立てたコードを安全に評価したいことがあります。 8 | 9 | ## 方法 10 | 11 | - iframe を createObjectURL 12 | - dynamic import で data uri として文字列を評価 13 | - 得られた関数を実行 14 | 15 | ## コード 16 | 17 | ```ts 18 | // これを評価したい 19 | const code = `export default () => { console.log("xxx") }`; 20 | 21 | const encoded = btoa(code); 22 | const blob = new Blob( 23 | [ 24 | ` 25 | 26 | 27 | 28 | 29 | 32 | 33 | `, 34 | ], 35 | { type: "text/html" } 36 | ); 37 | 38 | // const iframe = document.querySelector("iframe"); 39 | iframe.src = URL.createObjectURL(blob); 40 | ``` 41 | 42 | ## 解説: data uri の import 43 | 44 | ESM のコードを script タグの中で評価しても、実行はできても export されたオブジェクトの参照を手に入れることができません。 45 | 46 | なので、 data uri 化されたコードを `import(...)` を経由することで、export オブジェクトを手に入れています。 47 | 48 | ```ts 49 | import("data:text/javascript;base64,${encoded}").then((mod) => mod.default()); 50 | ``` 51 | 52 | これでモジュールが手に入るので、関数として実行しています。 53 | 54 | ## 解説: iframe 55 | 56 | UI スレッドで何度もコードを実行すると、window に起きる副作用の影響を受けてしまうので、実行コンテキストを隔離する目的で iframe を使います。 57 | 58 | HTML を blob 化して `URL.createObjectURL(blob)` で, 何にも紐付かない iframe を作ります。 59 | 60 | ```ts 61 | const blob = new Blob([``], { 62 | type: "text/html", 63 | }); 64 | iframe.src = URL.createObjectURL(blob); 65 | ``` 66 | 67 | 本当にセキュアにやるなら realm などを使ったほうがいいです。 68 | 69 | [tc39/proposal\-realms: ECMAScript Proposal, specs, and reference implementation for Realms](https://github.com/tc39/proposal-realms) 70 | -------------------------------------------------------------------------------- /docs/202006061823-tiny-bundler.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: module bundler を作った 3 | created: 1591435424224 4 | tags: [] 5 | --- 6 | 7 | このフロントエンドの魔境に生まれたからには一度は俺が考えた最強の module bundler を作りたい。みんなそう思ってると思う。作った。 8 | 9 | [mizchi/bundler: hobby bundler](https://github.com/mizchi/bundler) 10 | 11 | ## tldr 12 | 13 | このコードが 14 | 15 | ```ts 16 | // foo.js 17 | export default 1; 18 | 19 | // index.js 20 | import foo from "./foo.js"; 21 | console.log(foo); 22 | export const index = 1; 23 | ``` 24 | 25 | こうなる 26 | 27 | ```ts 28 | // @mizchi/bundler generate 29 | const _$_exported = {}; 30 | const _$_import = (id) => 31 | _$_exported[id] || _$_modules[id]((_$_exported[id] = {})); 32 | const _$_modules = { 33 | "/foo.js": (_$_exports) => { 34 | _$_exports.default = 1; 35 | return _$_exports; 36 | }, 37 | }; 38 | // -- entry -- 39 | 40 | const { default: foo } = _$_import("/foo.js"); 41 | 42 | console.log(foo); 43 | export const index = 1; 44 | ``` 45 | 46 | ## もうちょっと複雑な例: snowpack 47 | 48 | importmap をサポートしているので、このコードが動く。 49 | 50 | ```ts 51 | import { h } from "preact"; 52 | import render from "preact-render-to-string"; 53 | console.log(render(h("div", {}, "hello"))); 54 | ``` 55 | 56 | (実際は importmap を自分で組み立てたりと色々下準備が必要) 57 | 58 | [bundler/examples/with\-snowpack at master · mizchi/bundler](https://github.com/mizchi/bundler/tree/master/examples/with-snowpack) 59 | 60 | ## もうちょっと複雑な例: dynamic import chunk / worker chunk / publicPath 61 | 62 | ```ts 63 | import { Bundler } from "@mizchi/bundler"; 64 | import { format } from "prettier"; // install yourself 65 | const fileMap = { 66 | "/worker.js": "self.onmessage = (ev) => console.log(ev);", 67 | "/foo.js": "export default 1;", 68 | "/index.js": ` 69 | const p = import("./foo.js"); 70 | const worker = new Worker("./worker.js"); 71 | worker.postMessage({hello: "world"}) 72 | `, 73 | }; 74 | const bundler = new Bundler(fileMap); 75 | (async () => { 76 | const chunks = await bundler.bundleChunks("/index.js", { 77 | publicPath: "/dist/", 78 | }); 79 | // console.log(format(code, { parser: "babel" })); 80 | console.log(chunks); 81 | })(); 82 | ``` 83 | 84 | この出力はこうなる。 85 | 86 | ```json 87 | [ 88 | { 89 | "type": "entry", 90 | "entry": "/index.js", 91 | "builtCode": "// @mizchi/bundler generate\nconst _$_exported = {};\nconst _$_import = (id) => _$_exported[id] || _$_modules[id](_$_exported[id] = {});\nconst _$_modules = {};\n\n;\n\n// -- entry --\n\nconst p = import(\"/dist/_$_foo.js\");\nconst worker = new Worker(\"/dist/_$_worker.js\");\nworker.postMessage({\n hello: \"world\"\n});;\n\n" 92 | }, 93 | { 94 | "type": "chunk", 95 | "entry": "/foo.js", 96 | "chunkName": "/dist/_$_foo.js", 97 | "builtCode": "// @mizchi/bundler generate\nconst _$_exported = {};\nconst _$_import = (id) => _$_exported[id] || _$_modules[id](_$_exported[id] = {});\nconst _$_modules = {};\n\n;\n\n// -- entry --\n\nexport default 1;;\n\n" 98 | }, 99 | { 100 | "type": "chunk", 101 | "entry": "/worker.js", 102 | "chunkName": "/dist/_$_worker.js", 103 | "builtCode": "// @mizchi/bundler generate\nconst _$_exported = {};\nconst _$_import = (id) => _$_exported[id] || _$_modules[id](_$_exported[id] = {});\nconst _$_modules = {};\n\n;\n\n// -- entry --\n\nself.onmessage = ev => console.log(ev);;\n\n" 104 | } 105 | ] 106 | ``` 107 | 108 | これを `dist/*` に配置して、`index.html` から呼べば実行できる。そのための `publicPath` 109 | 110 | ## 実装した機能 111 | 112 | - treeshake による未使用コード削除 113 | - dynamic import 向けの chunk splitting 114 | - webworker 用 chunk splitting (webpack の worker-plugin 相当) 115 | - importmap による書き換え 116 | 117 | ## 何故作ったか 118 | 119 | ### 勉強のため 120 | 121 | hiroppy が書いたやつをふんわり読んで、これなら作れそう、と思って作った。 122 | 123 | [module bundler の作り方\(ECMAScript Modules 編\) \- 技術探し](https://blog.hiroppy.me/entry/create-module-bundler-esm) 124 | 125 | 実際はあんまりちゃんと読んでないけど、babel の parser / generator / traverse を使う、 モジュール用テンプレート、実行用テンプレートという発想だけ持ち帰った感はある。 126 | 127 | ビルド用に id 降ったほうがよい、というのだけ無視していて、仮想 FS 内の絶対パスをそのまま使っている。仮想の FS のルートなら環境依存は無いので。 128 | 129 | ### ESM to Bundled ESM 130 | 131 | 複数の ESM を ESM にバンドルするだけの簡易なコンパイラが欲しかった。webpack も rollup も ESM を入力にできるし、 IE が死のうとしている今、現代のブラウザは ESM は当然のように備えているので、バンドル処理はネットワークをまたがずに RTT を減らすためだけのものでしかない。要は、エントリポイントの ESM はそのまま残して、内部の import export だけ書き換えればよい。 132 | 133 | deno を試した感じ、 importmap がありさえすれば `node_modulse` の名前解決はそこまで困らない印象だったので、import-map をサポートして、 node_modules への名前解決を実装するのをやめた。 134 | 135 | ## ブラウザフレンドリー 136 | 137 | node_modules のサポートをやめた理由でもあるんだけど、主にブラウザをターゲットに動くものなので、ブラウザ内で実行しながらプレビューしたい。 138 | 139 | また、ブラウザ内でバンドラを実行するのは、おそらくそこにエディタがあって高頻度にビルドされることが想定されるので、 AST に変換した中間状態を全部保存して、ビルド処理はグラフをなめるだけで終わるようにした。これでプレビュー速度が高速になった。 140 | 141 | WebWorker のスレッドで実行されることを想定している。worker で並列処理をすることも考えたが、後回し。これ使いたい。[developit/web\-worker: Consistent Web Workers in browser and Node\.](https://github.com/developit/web-worker) 142 | 143 | ## Babel(+ typescript-preset) First 144 | 145 | webpack / rollup は内部で acorn を使ってて、これは 90k と軽量で便利ではあるんだけど、最終的にコードの変形では babel や babel のプラグインを使っている事が多い。 複数の AST 定義を跨ぐのが面倒なので、最初から全部 babel とした。 146 | 147 | このコンパイラを webpack でバンドルしたところ、約 840kb。開発者用ツールなら十分なサイズだと思う。(あとで rollup の es 出力にする) 148 | 149 | ### 可読性のある出力 150 | 151 | sourcemap 対応をしない代わりに、比較的可読性がある出力を目指した。sourcemap を使っていても、結局 debugger などで止めた際に変数が書き換えられていると、 見えてるシンボルと実際のものが別物で、あんまり使い物にならない。async をサポートした ES2017 以上のターゲットなら、そもそもほとんどのコードが変換されないので、生で読んだほうが早い。 152 | 153 | ## 完走した感想 154 | 155 | treeshake は簡易なものを実装してみたが正直しんどい。何を持って副作用があるとするかを、とりあえず whiltelist な AST Node のパターンを作って、それを違反しない限りは副作用がないとしているが、真面目にやるとエッジケースが無限にありそう。 156 | 157 | snowpack や rome と思想が競合してる気がする。 158 | 159 | 完走してない。 160 | 161 | ## 今後 162 | 163 | - rollup で esm バンドルする 164 | - typescript support: 165 | - babel/parser のオプションを有効にしているので、parse はできてる気がするが、たぶん出力にも残ってしまっている 166 | - tsx もサポートする 167 | - エディタ 168 | 169 | ## おまけ: Treeshake 無し版のコード 170 | 171 | treeshake や各種の最適化のためにコードが膨れたので、treeshake 実装前の単純だった時のコードをここに貼る。250 行ほど。 172 | 173 | 皆も作ってみよう! 174 | 175 | [bundler](https://gist.github.com/mizchi/368c75fc1088dabfda7dfed726ef9f85) 176 | 177 | ```tsx 178 | // yarn add @babel/parser @babel/traverse @babel/generator @babel/types memfs 179 | // yarn add @types/babel__core @types/node -D 180 | import path from "path"; 181 | 182 | import { parse } from "@babel/parser"; 183 | import traverse from "@babel/traverse"; 184 | import generate from "@babel/generator"; 185 | import * as t from "@babel/types"; 186 | 187 | import type { IPromisesAPI } from "memfs/lib/promises"; 188 | import createFs from "memfs/lib/promises"; 189 | import { vol } from "memfs"; 190 | 191 | // helper 192 | function createMemoryFs(files: { [k: string]: string }): IPromisesAPI { 193 | vol.fromJSON(files, "/"); 194 | return createFs(vol) as IPromisesAPI; 195 | } 196 | 197 | type Module = { 198 | ast: t.File; 199 | filepath: string; 200 | imports: Import[]; 201 | }; 202 | 203 | type Import = { 204 | filepath: string; 205 | }; 206 | 207 | type Output = { 208 | filepath: string; 209 | code: string; 210 | imports: Import[]; 211 | }; 212 | 213 | class Bundler { 214 | public modulesMap = new Map(); 215 | public outModules: Array = []; 216 | fs: IPromisesAPI; 217 | constructor(public files: { [k: string]: string }) { 218 | this.fs = createMemoryFs(files); 219 | } 220 | public async bundle(entry: string) { 221 | await this.addModule(entry); 222 | await this.transform(entry); 223 | return await this.emit("/index.js"); 224 | } 225 | 226 | async addModule(filepath: string) { 227 | if (this.modulesMap.has(filepath)) { 228 | return; 229 | } 230 | const basepath = path.dirname(filepath); 231 | 232 | const code = (await this.fs.readFile(filepath, { 233 | encoding: "utf-8", 234 | })) as string; 235 | 236 | const ast = parse(code, { 237 | sourceFilename: filepath, 238 | sourceType: "module", 239 | }); 240 | 241 | let imports: Import[] = []; 242 | traverse(ast, { 243 | ImportDeclaration(nodePath) { 244 | const target = nodePath.node.source.value; 245 | const absPath = path.join(basepath, target); 246 | imports.push({ 247 | filepath: absPath, 248 | }); 249 | }, 250 | }); 251 | await Promise.all( 252 | imports.map((imp) => { 253 | return this.addModule(imp.filepath); 254 | }) 255 | ); 256 | this.modulesMap.set(filepath, { 257 | filepath, 258 | ast, 259 | imports, 260 | }); 261 | } 262 | 263 | async transform(filepath: string) { 264 | const mod = this.modulesMap.get(filepath)!; 265 | const alreadyIncluded = this.outModules.find( 266 | (m) => m.filepath === filepath 267 | ); 268 | if (alreadyIncluded) { 269 | return; 270 | } 271 | 272 | const basepath = path.dirname(filepath); 273 | 274 | const newImportStmts: t.Statement[] = []; 275 | 276 | traverse(mod.ast, { 277 | ImportDeclaration(nodePath) { 278 | const target = nodePath.node.source.value; 279 | const absPath = path.join(basepath, target); 280 | const names: [string, string][] = []; 281 | nodePath.node.specifiers.forEach((n) => { 282 | if (n.type === "ImportDefaultSpecifier") { 283 | names.push(["default", n.local.name]); 284 | } 285 | if (n.type === "ImportSpecifier") { 286 | names.push([n.imported.name, n.local.name]); 287 | } 288 | if (n.type === "ImportNamespaceSpecifier") { 289 | newImportStmts.push( 290 | t.variableDeclaration("const", [ 291 | t.variableDeclarator( 292 | t.identifier(n.local.name), 293 | t.callExpression(t.identifier("$$import"), [ 294 | t.stringLiteral(absPath), 295 | ]) 296 | ), 297 | ]) 298 | ); 299 | } 300 | }); 301 | 302 | const newNode = t.variableDeclaration("const", [ 303 | t.variableDeclarator( 304 | t.objectPattern( 305 | names.map(([imported, local]) => { 306 | return t.objectProperty( 307 | t.identifier(imported), 308 | t.identifier(local) 309 | ); 310 | }) 311 | ), 312 | t.callExpression(t.identifier("$$import"), [ 313 | t.stringLiteral(absPath), 314 | ]) 315 | ), 316 | ]); 317 | newImportStmts.push(newNode); 318 | nodePath.replaceWith(t.emptyStatement()); 319 | }, 320 | ExportDefaultDeclaration(nodePath) { 321 | const name = "default"; 322 | const right = nodePath.node.declaration as any; 323 | const newNode = t.expressionStatement( 324 | t.assignmentExpression( 325 | "=", 326 | t.memberExpression( 327 | t.identifier("$$exports"), 328 | t.stringLiteral(name), 329 | true 330 | ), 331 | right 332 | ) 333 | ); 334 | nodePath.replaceWith(newNode); 335 | }, 336 | ExportNamedDeclaration(nodePath) { 337 | // TODO: name mapping 338 | // TODO: Export multiple name 339 | if (nodePath.node.declaration) { 340 | const decl = nodePath.node.declaration.declarations[0]; 341 | const name = decl.id.name; 342 | const right = decl.init; 343 | const newNode = t.expressionStatement( 344 | t.assignmentExpression( 345 | "=", 346 | t.memberExpression( 347 | t.identifier("$$exports"), 348 | t.identifier(name) 349 | // true 350 | ), 351 | right 352 | ) 353 | ); 354 | nodePath.replaceWith(newNode); 355 | } else { 356 | // export { a as b } 357 | const exportNames: Array<{ exported: string; imported: string }> = []; 358 | for (const specifier of nodePath.node.specifiers) { 359 | if (specifier.type == "ExportSpecifier") { 360 | exportNames.push({ 361 | exported: specifier.exported.name, 362 | imported: specifier.local.name, 363 | }); 364 | } 365 | } 366 | nodePath.replaceWith( 367 | t.blockStatement( 368 | exportNames.map((exp) => { 369 | return t.expressionStatement( 370 | t.assignmentExpression( 371 | "=", 372 | t.memberExpression( 373 | t.identifier("$$exports"), 374 | t.identifier(exp.exported) 375 | // true 376 | ), 377 | nodePath.node.source 378 | ? t.memberExpression( 379 | t.callExpression(t.identifier("$$import"), [ 380 | t.stringLiteral( 381 | path.join(basepath, nodePath.node.source.value) 382 | ), 383 | ]), 384 | t.identifier(exp.imported) 385 | ) 386 | : t.identifier(exp.imported) 387 | ) 388 | ); 389 | }) 390 | ) 391 | ); 392 | } 393 | }, 394 | }); 395 | 396 | const out = { 397 | ...mod.ast, 398 | program: { 399 | ...mod.ast.program, 400 | body: [...newImportStmts, ...mod.ast.program.body], 401 | }, 402 | }; 403 | const gen = generate(out); 404 | this.outModules.push({ 405 | imports: mod.imports, 406 | filepath: mod.filepath, 407 | code: gen.code, 408 | }); 409 | 410 | await Promise.all( 411 | mod.imports.map((imp) => { 412 | return this.transform(imp.filepath); 413 | }) 414 | ); 415 | } 416 | 417 | async emit(entry: string) { 418 | const entryMod = this.outModules.find((m) => m.filepath === entry); 419 | 420 | const importCodes = this.outModules 421 | .filter((m) => m.filepath !== entry) 422 | .map((m) => { 423 | return `$$import("${m.filepath}");`; 424 | }) 425 | .join("\n"); 426 | 427 | const mods = this.outModules 428 | .filter((m) => m.filepath !== entry) 429 | .map((m) => { 430 | return `"${m.filepath}": ($$exports) => { 431 | ${m.code} 432 | return $$exports; 433 | } 434 | `; 435 | }) 436 | .join(","); 437 | 438 | return `// minibundle generate 439 | const $$exported = {}; 440 | const $$modules = { ${mods} }; 441 | function $$import(id){ 442 | if ($$exported[id]) { 443 | return $$exported[id]; 444 | } 445 | $$exported[id] = {}; 446 | $$modules[id]($$exported[id]); 447 | return $$exported[id]; 448 | } 449 | // evaluate as static module 450 | ${importCodes}; 451 | 452 | // -- runner -- 453 | const $$exports = {}; // dummy 454 | ${entryMod?.code}; 455 | `; 456 | } 457 | } 458 | 459 | // runtime 460 | const files = { 461 | "/bar.js": ` 462 | import foo from "./foo.js"; 463 | console.log("eval bar once") 464 | export default "bar$" + foo 465 | `, 466 | "/foo.js": ` 467 | console.log("eval foo once") 468 | export default "foo$default"; 469 | export const b = "b"; 470 | export const a = "a"; 471 | `, 472 | "/index.js": ` 473 | import * as t from "./foo.js"; 474 | import foo, {a, b as c} from "./foo.js"; 475 | import bar from "./bar.js"; 476 | export const x = c; 477 | export default 1; 478 | console.log(foo, bar); 479 | `, 480 | }; 481 | 482 | // @ts-ignore 483 | import prettier from "prettier"; 484 | async function main() { 485 | const bundler = new Bundler(files); 486 | const built = await bundler.bundle("/index.js"); 487 | console.log(prettier.format(built)); 488 | console.log("--- eval ----"); 489 | eval(built); 490 | } 491 | main(); 492 | ``` 493 | -------------------------------------------------------------------------------- /docs/202006091517-todays-react-jsonschema-form.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: react-jsonschema-form(@rjsf/core) を使う場合は material-ui を使うしかない 3 | created: 1591683426369 4 | tags: [] 5 | --- 6 | 7 | jsonschema から form を生成してくれる react-jsonschema-form というライブラリがある。デモページをみたところ、かなり良く出来てるように感じたので、使ってみた。 8 | 9 | [react\-jsonschema\-form playground](https://rjsf-team.github.io/react-jsonschema-form/) 10 | 11 | だが、いざ使ってみるとかなり混沌とした状況だった。 12 | 13 | ## tldr 14 | 15 | - `@rjsf/core` の出力は bootstrap@4 で壊れている 16 | - bootstrap@3 を避ける場合 `@rjsf/material-ui` を使うしかない 17 | 18 | ## 経緯 19 | 20 | - まず、多くの紹介記事は古く、`react-jsonschema-form` は `@rjsf/core` に名前が変わっている 21 | - bootstrap@3 は jquery 依存が残っているなど、レガシーな設計なので、可能な限り避けたい 22 | - `@rjsf/core` は bootstrap@3 の CSS を前提にした出力をする 23 | - bootstrap@4 では、アイコンで使われた gryphicon が有料化したためドロップされていおり、これを使った `@rjsf/core` の出力が壊れる(ボタンほぼ全部) 24 | - 公式には font-awesome などを使うように推奨されているが、`@rjsf/core` ではライブラリの出力に組み込まれているので、治すのが難しい 25 | - じゃあ `@rjsf/core` が bootstrap4 対応をしているかと言うと、している気配がない 26 | - その代わりに `@rjsf/material-ui` の開発をしている 27 | 28 | ## `@rjsf/material-ui` を使ったコード 29 | 30 | ```tsx 31 | import { IChangeEvent, UiSchema } from "@rjsf/core"; 32 | import JSONSchemaForm from "@rjsf/material-ui"; 33 | import { JSONSchema7 } from "json-schema"; 34 | import React, { useState } from "react"; 35 | import { utils } from "@rjsf/core"; 36 | 37 | const schema = { 38 | type: "object", 39 | properties: { 40 | message: { 41 | type: "string", 42 | default: "hello", 43 | }, 44 | }, 45 | }; 46 | 47 | const defaultState = utils.getDefaultFormState(schema, {}); 48 | 49 | export function Form() { 50 | return ( 51 | ) => { 55 | console.log(data.formData); 56 | }} 57 | /> 58 | ); 59 | } 60 | ``` 61 | 62 | ## お気持ち 63 | 64 | デザインフレームワークを指定するタイプのライブラリは、はっきりいって辛い。 65 | -------------------------------------------------------------------------------- /docs/202006101314-switch-tsconfig-on-webpack.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: webpack + ts-loader で使う tsconfig.json を動的に切り替える 3 | created: 1591762452334 4 | tags: [webpack] 5 | --- 6 | 7 | webpack を使っていると typescript の module 指定は、ESM を treeshake するために `"module": "esnext"` としたい。 8 | 9 | しかし、`ts-node` を前提としたユーティリティスクリプトを作っていると、`"module": "commonjs"` としないと実行に失敗するようになってしまう。`ts-jest` も同様。 10 | 11 | ts-node に関しては、引数で compilerOptions を書き換える方法があり、次のようになる。 12 | 13 | ```bash 14 | yarn ts-node -O '{"module":"commonjs"}' script/x.ts 15 | ``` 16 | 17 | これだと常にこの引数を付けて実行する必要があり、不便。 18 | 19 | ## ts-loader の configFile 20 | 21 | [TypeStrong/ts\-loader: TypeScript loader for webpack](https://github.com/TypeStrong/ts-loader) をちゃんと読むと、`configFile` という引数がある。これで webpack でコンパイル時の tsconfig を切り替えられる。 22 | 23 | (なお、本記事では開発中の ts-loader では型チェックは不要という立場から `transpileOnly: true` を付けているが、必要な場合は外すように) 24 | 25 | デフォルトは ts-node 用に `"module": "commonjs"` としつつ、webpack に渡す設定だけ `"module": "esnext"` としたい。ただし、 `tsconfig.json` は webpack の補完で使われているので、残しておく必要がある。なので、tsconfig.json は起きつつ、それを extends する webpack.tsconfig.js を追加した。 26 | 27 | tsconfig.json 28 | 29 | ```json 30 | { 31 | "compilerOptions": { 32 | "module": "commonjs", 33 | "target": "es2019", 34 | "allowJs": true, 35 | "skipLibCheck": true, 36 | "strict": true, 37 | "forceConsistentCasingInFileNames": true, 38 | "noEmit": true, 39 | "esModuleInterop": true, 40 | "moduleResolution": "node", 41 | "resolveJsonModule": true, 42 | "isolatedModules": true, 43 | "jsx": "react" 44 | } 45 | } 46 | ``` 47 | 48 | webpack.tsconfig.json 49 | 50 | ```json 51 | { 52 | "extends": "./tsconfig.json", 53 | "compilerOptions": { 54 | "module": "esnext" 55 | } 56 | } 57 | ``` 58 | 59 | webpack.config.js 60 | 61 | ```js 62 | const path = require("path"); 63 | const HTMLPlugin = require("html-webpack-plugin"); 64 | const WorkerPlugin = require("worker-plugin"); 65 | 66 | module.exports = { 67 | module: { 68 | rules: [ 69 | { 70 | test: /\.tsx?$/, 71 | use: { 72 | loader: "ts-loader", 73 | options: { 74 | transpileOnly: true, 75 | configFile: "webpack.tsconfig.json", 76 | }, 77 | }, 78 | }, 79 | ], 80 | }, 81 | resolve: { 82 | extensions: [".js", ".ts", ".tsx"], 83 | }, 84 | }; 85 | ``` 86 | 87 | ## 応用: プロダクションでターゲットを切り替える 88 | 89 | 動的に `tsconfig.json` を選択することで、 開発中は `target: es2015` でデバッグしやすくし、production のみ `target: es5` するという方法もある。 90 | 91 | webpack.tsconfig.prod.json を追加 92 | 93 | ```json 94 | { 95 | "extends": "./tsconfig.json", 96 | "compilerOptions": { 97 | "target": "es5", 98 | "module": "esnext" 99 | } 100 | } 101 | ``` 102 | 103 | ```js 104 | // webpack.config.js 105 | const path = require("path"); 106 | const HTMLPlugin = require("html-webpack-plugin"); 107 | const WorkerPlugin = require("worker-plugin"); 108 | 109 | module.exports = (_, argv) => { 110 | return { 111 | module: { 112 | rules: [ 113 | { 114 | test: /\.tsx?$/, 115 | use: { 116 | loader: "ts-loader", 117 | options: { 118 | transpileOnly: true, 119 | configFile: 120 | argv.mode === "production" 121 | ? "webpack.tsconfig.prod.json" 122 | : "webpack.tsconfig.json", 123 | }, 124 | }, 125 | }, 126 | ], 127 | }, 128 | resolve: { 129 | extensions: [".js", ".ts", ".tsx"], 130 | }, 131 | }; 132 | }; 133 | ``` 134 | 135 | これでプロダクションビルド(`webpack --mode production`) で es5 向けにビルドされるようになった。 136 | -------------------------------------------------------------------------------- /docs/202006161259-try-netlify-lambda.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: ローカル環境で netlify lambda のエミュレータを動かす 3 | created: 1592279996411 4 | tags: [netlify] 5 | --- 6 | 7 | netlify は雑に作り捨ての作例を置いておくのに便利でよく使っている。このブログも netlify にドメインを設定して動かしている。 8 | 9 | そして、あんまり知られていないが、netlify には aws lambda が使えて、これは月当たり 12.5 万回ほどの無料枠がある。で、これをローカルで動かすための `netilfy-lambda` がある。 10 | 11 | これがカジュアルに使えたら便利だと思って、一度素振りしておくことにした。 12 | 13 | ## netlify-lambda の問題 14 | 15 | `netlify-lambda` を使ってみたところ、本当に出来が悪い。ランナーとして、というより、開発用の環境設定の読み取りに謎の処理が多く、勝手にルート要素の `webpack.config.js` を読み取って自分用の `webpack.config.js` を生成して勝手に失敗したり、本番では `/.netlify/functions/` で動くのに(これもドキュメントのすごくわかりにくいところに書いてある)、そもそも `netlify.toml` に functions の指定ディレクトリを書かせるのに、ターゲットディレクトリの指定が必要だったりと、とにかく暗黙のユースケースを察する必要がある。 16 | 17 | 公式ドキュメントを読んでも必要な情報がわからない。まず動くサンプルが見当たらず、特定のユースケースのための設定例ばかり引っかかり、普通に動かすのが難しい。 18 | 19 | ## 結局どうやったか 20 | 21 | 動くリポジトリ [mizchi/try-netlify-lambda](https://github.com/mizchi/try-netlify-lambda) 22 | 23 | - 素朴に `netlify.toml` で `functions = "functions"` を指定 24 | - `netlify-lambda build` を一切使わず、webpack の `target: "node"` で `functions/api.js` を生成 25 | - express router で環境ごとのエンドポイントにマウントして、環境差分を吸収 26 | - デプロイ時は一旦クリーンしてから `netlify deploy --prod` する 27 | 28 | ### netlify.toml 29 | 30 | ``` 31 | [build] 32 | command = "yarn webpack --config webpack/functions.config.js" 33 | publish = "dist" 34 | functions = "functions" 35 | ``` 36 | 37 | ### 構成と npm scripts 38 | 39 | 構成 40 | 41 | ``` 42 | netlify.toml 43 | package.json 44 | tsconfig.js 45 | webpack.config.js # netlify-lamba 用のダミー 46 | src/ 47 | ├── front 48 | │ ├── index.html 49 | │ └── index.ts 50 | ├── functions 51 | │ └── api.ts 52 | └── shared 53 | └── getApiRoot.ts 54 | webpack/ 55 | ├── front.config.js 56 | └── functions.config.js 57 | ``` 58 | 59 | package.json 60 | 61 | ```json 62 | "scripts": { 63 | "build": "rimraf functions && rimraf dist && run-s build:*", 64 | "build:fns": "webpack --config webpack/functions.config.js --mode production", 65 | "build:front": "webpack --config webpack/front.config.js --mode production", 66 | "dev": "run-p dev:*", 67 | "dev:front": "webpack-dev-server --config webpack/front.config.js -w", 68 | "dev:fns": "rimraf functions && run-p dev:fns:*", 69 | "dev:fns:webpack": "webpack --config webpack/functions.config.js -w --mode development", 70 | "dev:fns:serve": "netlify-lambda serve .", 71 | "deploy": "yarn build && netlify deploy --prod" 72 | }, 73 | ``` 74 | 75 | (普通に使うだけだったら `dev` と `build`、そして `deploy` だけ叩けば良い) 76 | 77 | 注意点として、`netlify-lambda serve .` は `functions` を見るが、まずルートの `webpack.config.js` を読んで、 `functions/webpack.config.js` を生成してくる(たぶんオートリロード用?)。このディレクトリをこのままデプロイしようとすると、`webpack.config.js` を functions としてデプロイしようとして、デプロイに失敗する。なので、本番用の `build` では rimraf で `functions` を消している。 78 | 79 | なので、netlify-lambda のためのダミーとして、 `module.exports = {}` という空の `webpack.config.js` を置いて、実際に使う webpack.config は `webpack/` ディレクトリに置いた。 80 | 81 | ここの挙動を察するのに 4 時間が溶けたので、恨みがある。 82 | 83 | ### サーバー 84 | 85 | ↑ の設定で、netlify-lambda の開発用のエミュレータは `localhost:9000/`、本番は `/.netlify/functions/` で起動する。 86 | これを express.Router のマウント位置で辻褄を合わせる。 87 | 88 | AWS Lambda では、 node の http ではなく、lambda 独自の http.Request/Response が来るため、 `serverless-http` を通して express に食わせる。 89 | 90 | ```ts 91 | // src/functions/api.ts 92 | 93 | import express from "express"; 94 | import serverless from "serverless-http"; 95 | 96 | const isProd = process.env.NODE === "production"; 97 | 98 | const app = express(); 99 | const router = express.Router(); 100 | 101 | // 開発時には CORS になるので開けておく 102 | router.use((_req, res, next) => { 103 | if (!isProd) { 104 | res.header("Access-Control-Allow-Origin", "*"); 105 | } 106 | next(); 107 | }); 108 | 109 | router.get<{ id: string }>("/:id", (req, res) => { 110 | res.send("ok " + req.params.id); 111 | }); 112 | 113 | const apiRoot = 114 | process.env.NODE_ENV === "production" ? "/.netlify/functions/api" : "/api"; 115 | app.use(apiRoot, router); 116 | 117 | exports.handler = serverless(app); 118 | ``` 119 | 120 | クライアントからも環境変数を見て辻褄を合わせてリクエストする。 121 | 122 | ```ts 123 | const API_ROOT = 124 | process.env.NODE_ENV === "production" 125 | ? "/.netlify/functions" 126 | : "http://localhost:9000"; 127 | 128 | fetch(API_ROOT + "/api/foo").then(() => {...}); 129 | ``` 130 | 131 | ## 結果 132 | 133 | 動いた 134 | 135 | https://heuristic-keller-8c2758.netlify.app/ 136 | -------------------------------------------------------------------------------- /docs/202006161500-netlify-lambda-chromium.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: netlify-lambda で puppeteer を起動する 3 | created: 1592287218672 4 | tags: [netlify, puppeteer] 5 | --- 6 | 7 | [ローカル環境で netlify lambda のエミュレータを動かす \- mizdev](https://mizchi.dev/202006161259-try-netlify-lambda) を読んでることが前提 8 | 9 | ## tldr 10 | 11 | - netlify-lambda は aws lambda なので、AWS 用にビルドされた chrome が使える 12 | - ローカル開発環境では素の puppeteer にフォールバックする 13 | 14 | ## コード 15 | 16 | ``` 17 | yarn add puppeteer chrome-aws-lambda 18 | ``` 19 | 20 | webpack.config.js の `externals` でビルドしないように指定。 21 | 22 | ```js 23 | // webpack.config.js 24 | externals: { 25 | puppeteer: "puppeteer", 26 | "chrome-aws-lambda": "chrome-aws-lambda", 27 | lambdafs: "lambdafs", 28 | }, 29 | ``` 30 | 31 | (`functions/api.js` にビルドしていたのを `functions/api/index.js` にビルドを変えて、 `functions/api/package.json` に依存を記述した。) 32 | 33 | ```ts 34 | // browser.ts 35 | import puppeteer from "puppeteer"; 36 | 37 | const isProd = process.env.NODE === "production"; 38 | 39 | let _browser: null | puppeteer.Browser = null; 40 | 41 | export async function getBrowser(): Promise { 42 | if (_browser) { 43 | return _browser; 44 | } 45 | if (isProd) { 46 | const chromium = require("chrome-aws-lambda"); 47 | const browser = await chromium.puppeteer.launch({ 48 | args: chromium.args, 49 | defaultViewport: chromium.defaultViewport, 50 | executablePath: await chromium.executablePath, 51 | headless: chromium.headless, 52 | ignoreHTTPSErrors: true, 53 | }); 54 | return (_browser = browser); 55 | } 56 | return (_browser = await puppeteer.launch({ 57 | headless: true, 58 | executablePath: 59 | "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", 60 | })); 61 | } 62 | ``` 63 | 64 | このコードを使う例 65 | 66 | ```ts 67 | router.get("/chrome", async (req, res) => { 68 | let result = null; 69 | let page = null; 70 | try { 71 | const browser = await getBrowser(); 72 | page = await browser.newPage(); 73 | await page.goto("https://google.com"); 74 | result = await page.title(); 75 | } catch (error) { 76 | return res.status(400).send(error.message); 77 | } finally { 78 | if (page) { 79 | page.close(); 80 | } 81 | } 82 | res.send(result); 83 | }); 84 | ``` 85 | -------------------------------------------------------------------------------- /docs/202006210107-avoid-ie-first.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: IE First を避ける 3 | created: 1592669248929 4 | tags: [] 5 | --- 6 | 7 | まず、去年の実績として、IE のシェアが 9% から 5% になっています。 8 | 9 | [Browser Market Share Japan \| StatCounter Global Stats](https://gs.statcounter.com/browser-market-share/all/japan) 10 | 11 | 世界だと 1.4% です。これは途上国などでは Android Chrome が支配的だからです。 https://gs.statcounter.com/browser-market-share/all 12 | 13 | 国内で「IE シェア」などでググって出るサイトは 9% と出ていますが、これはデスクトップのみの数字で、モバイルを含めた数字は 5%前後です。日本はさらに Chrome ベースの新 MS Edge の正式リリースがコロナと確定申告の影響で延期されており、リリースのタイミングでさらに減ることが予想されます。 14 | 15 | 去年の実績値でいえば、来年度には 2% 台、再来年には 1%を切っている可能性があります。これは toC なら十分にサポートを外すことを検討できる数値です。フロントエンドでは、シェア 0.5%を切った段階で完全にサポートしないことを検討していいと言われています。 16 | 17 | 主流なデスクトップブラウザが 2~6 ヶ月毎のローリングアップデートを採用していること、モバイル端末はそもそもの買い替えのライフサイクルが短いことから、今後は IE のような十年単位で残るレガシーは出現しづらい環境になりつつあります。 18 | 19 | ## IE サポートをやめるわけではない 20 | 21 | 自分が主張したいのは、即座に IE のサポートをやめろ、という話ではありません。 22 | 23 | 新規に開発するソフトウェアの機能制約や開発体験で、IE の制約を取り払うことで生まれるメリットを考え、それが勝るならば IE をベースラインにせず、サポートを限定的なものにすることを検討する、という話です。 24 | 25 | 長きにわたる IE8,11 のシェアによって、フロントエンドエンジニアは、無意識に IE が可能なことがウェブの限界だ、と無意識に刷り込まれてしまっています。これだと、PWA や新しい発想を受け入れるのに時間が掛かります。例えば、ちょっと前には最新だともてはやされた ES2015 は、今ではビルドターゲットになりえます。 26 | 27 | IE の範囲内では機能要件をサポートし、非機能要件、Polyfill の使用水準や、アニメーションの簡略化や CSS の装飾を簡素なモノにする、といった選択肢を、それが可能ならば優先的に選択する、という話です。そして、そのために IE が犠牲になることは、プロジェクトマネージャ等に最初に合意を取っておく必要があります。 28 | 29 | 界隈に詳しくない人向けに、なぜ、ここまで IE を問題にするか説明すると、IE11 は 6 年間ほど時が止まっており、そこから Chrome Firefox Safari の競争で激しく進化した他のモダンブラウザと歩調を合わせることが困難になりつつあります。JS はツールの発達でそれほどではありませんが、とくに CSS の開発の工数が嵩みます。それは、場合によってはもう一つ同じものをつくるような工数です。 30 | 31 | ## 発展的な未来のために 32 | 33 | Safari はやや遅いといえど、基本的にモダンブラウザの足並みは揃っている現状です。(Chrome だけ独自機能が増えています) 34 | 35 | webpack などの OSS のエコシステムは IE11 はまだサポートされていますが、徐々に IE をサポートから外す動きがあります。 36 | 37 | また、 [Snowpack](https://www.snowpack.dev/) や [vitejs/vite: Native\-ESM powered web dev build tool\. It's fast\.](https://github.com/vitejs/vite) のような、IE を切ることでモダンブラウザの機能をふんだんに使う方向性のツールが出現し始めています。 38 | 39 | JS 周りでは Service Worker, ESM の dynamic import の活用、CSS では `display: grid` が大きいと思っています。 40 | 41 | というわけで、IE をメインラインに据えず、機能要件だけに落としていくことを提案します。 42 | 43 | ## 追記: 具体的な方針 44 | 45 | まずモダンブラウザ向けに作って、その上で IE 用に別ビルドを提供する、といった方法で、モダンブラウザビルドから負債を切り離していくことを自分は推奨しています。それが実行環境/開発環境のパフォーマンスに大きく寄与します。 46 | 47 | リリースされたものは nomodule や IE だけ polyfill.io 通すといったことが可能でしょう。 48 | 49 | - [ES Modules への橋渡しとしての nomodule 属性 \| blog\.jxck\.io](https://blog.jxck.io/entries/2017-06-21/nomodule-attribute.html) 50 | - [Polyfill\.io](https://polyfill.io/v3/) 51 | 52 | `import/export` の ESM を素で使うのはネットワーク RTT を挟むパフォーマンス上の問題があり、SPA でバンドル処理がなくなることは考えにくいです。依存の深さで 回数遅延 xRTT のレイテンシが追加されるからです。 53 | 54 | とはいえ、今後は webpack 一強を脅かすツールが出現することが予想されます。それを受けて webpack 自体も大きな変化を遂げると予想されます。 55 | 56 | フロントエンドでは採用したツールチェインがエコシステムの本体であり、採用するツールチェインの選択がアプリケーションの品質を大きく左右することがあります。 57 | 58 | ## 追記: toB での MS Edge について 59 | 60 | 今年 1 月にサポートの切れた Windows 7 にも 新 MS Edge は配布されています。これは、古い端末でも(最新アップデートを受け取っている必要はありますが) Chromium ベースの Edge が使えて、バックグラウンドでローリングアップデートされるということです。 61 | 62 | これはつまり、古いマシンでも最新 Chromium が起動することが保証されているので、IE のサポートを打ち切って、代わりに Edge での起動を促す画面を出す、みたいな方法が現実的になります。むしろ toB の方が枠組みさえ作れば IE サポートを打ち切りやすい環境があると思っています。 63 | -------------------------------------------------------------------------------- /docs/202006211925-support-ogp.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: OGP対応をした 3 | created: 1592735118538 4 | tags: [amdxg] 5 | --- 6 | 7 | こんな感じ 8 | 9 | ![](https://i.gyazo.com/8ca7358293d7f1e46a1a7e974d023f2c.png) 10 | 11 | ## やり方 12 | 13 | - `node-canvas` でタイトルを元にした画像を `public/ogp/[slug].png` に生成 14 | - `` でその出力画像を指定 15 | - 他、`og:title` や `og:url`, `` を追加 16 | 17 | ## 画像生成スクリプト 18 | 19 | 特に理由がないが node@14 の mjs で書いた。 20 | 21 | ```tsx 22 | // script/generate-ogp.mjs 23 | import canvas from "canvas"; 24 | import fs from "fs/promises"; 25 | import path from "path"; 26 | 27 | const W = 600; 28 | const H = 315; 29 | const LINE_HEIGHT = 30; 30 | 31 | function getRows(ctx, text) { 32 | const words = text.split(" "); 33 | 34 | let rows = []; 35 | let currentRow = []; 36 | let tokens = words.slice(0); 37 | let token; 38 | while ((token = tokens.shift())) { 39 | const mText = [...currentRow, token].join(" "); 40 | const measure = ctx.measureText(mText); 41 | if (measure.width <= W) { 42 | currentRow.push(token); 43 | } else { 44 | rows.push(currentRow.slice()); 45 | currentRow = [token]; 46 | } 47 | } 48 | if (currentRow.length > 0) { 49 | rows.push(currentRow); 50 | } 51 | 52 | return rows; 53 | } 54 | 55 | function renderText(ctx, rows) { 56 | const rowCount = rows.length; 57 | for (let i = 0; i < rowCount; i++) { 58 | const rowText = rows[i].join(" "); 59 | const m = ctx.measureText(rowText); 60 | 61 | const w = (W - m.width) / 2; 62 | // const h = (LINE_HEIGHT + 12) * (i + 1); 63 | const h = 40 + 210 / 2 - (LINE_HEIGHT + 12) * (rowCount - i - 1); 64 | 65 | ctx.fillText(rowText, w, h); 66 | } 67 | } 68 | 69 | async function generateImage(text, outputPath) { 70 | const cvs = canvas.createCanvas(W, H); 71 | const ctx = cvs.getContext("2d"); 72 | ctx.font = `${LINE_HEIGHT}px Impact`; 73 | 74 | const rows = getRows(ctx, text); 75 | 76 | ctx.fillStyle = "white"; 77 | ctx.fillRect(0, 0, W, H); 78 | 79 | ctx.fillStyle = "black"; 80 | renderText(ctx, rows); 81 | 82 | const m = ctx.measureText("mizdev"); 83 | ctx.fillText("mizdev", (W - m.width) / 2, 250); 84 | const buf = cvs.toBuffer(); 85 | await fs.writeFile(outputPath, buf); 86 | } 87 | 88 | async function main() { 89 | // const out = path.join(dirname, "../public/ogp/", p.slug + ".png"); 90 | await generateImage("ここにタイトルを入れる", "ogp-image.png"); 91 | console.log("[gen:ogp]", out); 92 | } 93 | 94 | main(); 95 | ``` 96 | -------------------------------------------------------------------------------- /docs/amdx-0.8.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: amdx-0.8 MDXX => AMDX へ 3 | created: 1589389359440 4 | --- 5 | 6 | ## mdxx => amdx 7 | 8 | MDX eXtended => Accelarated MDX 9 | 10 | - `mdxx-ssg` => `amdxg` 11 | - `mdxx-*` => `amdx-*` 12 | - `mdxx-parser` => `amdx` 13 | - `mdxx-compiler` => `amdx-runner` 14 | 15 | mdxx-parser がほぼ全部の変換を担ってたので、リネームついでに `amdx` というライブラリに変更。 16 | 17 | パッケージ名を変えた理由ですが、 mdxx という名前は、MDMA などの合成ドラッグの総称として使われる言葉であんまりイメージがよくなく、また、AMP のフィーチャーが重くなってきたので、A の文字を頭に付けました。 18 | 19 | [Urban Dictionary: mdxx](https://www.urbandictionary.com/define.php?term=mdxx) 20 | 21 | ## 変更 22 | 23 | - `amdxg-cli`: `amdxg new:page [slug]` でのページ生成に、必ず日時プレフィックスがつくようにしました。記事が増えたときの 24 | 25 | ## バグ修正 26 | 27 | - `amdxg-components`: tailwind の導入と purgecss の導入で本来必要な CSS を削ってしまっていたのをもとに戻しました。 28 | 29 | ## 開発中の機能 30 | 31 | ### next.js 9.4 の SSG Auto generation で now 対応 32 | 33 | 現在は `next export` する前提で作っているが、9.4 の段階的 SSG で, now にデプロイできるようにしたい。 34 | 35 | ### `amdxg new:script` 36 | 37 | `amp-script` を使った Component の雛形を追加します。amp-sciprt は 150kb 制限があり、また動的コンポーネントの追加には、SSR 相当の処理が必要です。なので今回は React ではなく preact を使うことにしました。 38 | 39 | あるいは、 markdown で `tsx:amp-script` のようなコードブロックから自動生成することや、wasm を使った処理を考えています。 40 | 41 | ```tsx:amp-script 42 | /** @jsx h */ 43 | import { h } from "preact"; 44 | import { useState } from "preact/hooks"; 45 | 46 | export default function Counter(props: { initialValue: number }) { 47 | const [state, setState] = useState(props.initialValue); 48 | return ( 49 |
50 |

Counter Example

51 | {String(state)} 52 | 53 | 54 |
55 | ); 56 | } 57 | ``` 58 | 59 | こういうコードブロックを抽出して、rollup でビルドしたコードを生成できそう 60 | -------------------------------------------------------------------------------- /docs/amp-social-share-for-hatena-bookmark.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: amp-social-share で、はてなブックマークのシェアボタンを設置する 3 | created: 1588784852166 4 | tags: [amp] 5 | --- 6 | 7 | amp-social-share は設定済みプロバイダとして twitter/facebook/line などに対応しているが、はてなブックマークはカスタムプロバイダーとして実装できる。 8 | 9 | [Documentation: \- amp\.dev](https://amp.dev/ja/documentation/components/amp-social-share/) 10 | 11 | ## 実装 12 | 13 | AMP 14 | 15 | ```html 16 | 17 | 18 | 22 | ``` 23 | 24 | [参考にした記事](https://ti-tomo-knowledge.hatenablog.com/entry/2018/07/03/202719) では動的 URL 部分で何やら小難しいことをやっているが、実際には AMP の予約キーワードを使って `CANONICAL_URL` に向けるだけでよい。 25 | 26 | [amphtml/amp\-var\-substitutions\.md at master · ampproject/amphtml](https://github.com/ampproject/amphtml/blob/master/spec/amp-var-substitutions.md) 27 | 28 | しかし、これだけだと何もデザインが当たらないので、ドキュメントに従ってカスタムプロバイダーに CSS を当ててみた。 29 | 30 | ```css 31 | amp-social-share[type="hatena_bookmark"] { 32 | display: inline-block; 33 | position: relative; 34 | width: 60px; 35 | height: 44px; 36 | background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 500 500'%3E%3Crect width='500' height='500' rx='101.9' ry='101.9' fill='%2300a4de'/%3E%3Cg fill='%23fff'%3E%3Cpath d='M278.2,258.1q-13.6-15.2-37.8-17c14.4-3.9,24.8-9.6,31.4-17.3s9.8-17.8,9.8-30.7A55,55,0,0,0,275,166a48.8,48.8,0,0,0-19.2-18.6c-7.3-4-16-6.9-26.2-8.6s-28.1-2.4-53.7-2.4H113.6V363.6h64.2q38.7,0,55.8-2.6c11.4-1.8,20.9-4.8,28.6-8.9a52.5,52.5,0,0,0,21.9-21.4c5.1-9.2,7.7-19.9,7.7-32.1C291.8,281.7,287.3,268.2,278.2,258.1Zm-107-71.4h13.3q23.1,0,31,5.2c5.3,3.5,7.9,9.5,7.9,18s-2.9,14-8.5,17.4-16.1,5-31.4,5H171.2V186.7Zm52.8,130.3c-6.1,3.7-16.5,5.5-31.1,5.5H171.2V273h22.6c15,0,25.4,1.9,30.9,5.7s8.4,10.4,8.4,20S230.1,313.4,223.9,317.1Z'/%3E%3Cpath d='M357.6,306.1a28.8,28.8,0,1,0,28.8,28.8A28.8,28.8,0,0,0,357.6,306.1Z'/%3E%3Crect x='332.6' y='136.4' width='50' height='151.52'/%3E%3C/g%3E%3C/svg%3E"); 37 | } 38 | ``` 39 | 40 | こんな感じ。 41 | 42 | ![:400](https://i.gyazo.com/05ce72f55d5262961290fb5cf6170b9d.png) 43 | 44 | ロゴの SVG は [素材集 \- 株式会社はてな](https://hatenacorp.jp/press/resource) より 45 | 46 | というわけで設置できた。 47 | -------------------------------------------------------------------------------- /docs/hello-deno-1.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Hello, Deno v1.0.0 3 | created: 1589719008195 4 | --- 5 | 6 | Deno 1.0.0 がリリースされて、ちょっと遊んでみたので、その感想。 7 | 8 | ## 圧倒的ゼロインストール感 9 | 10 | 自分は mac なので `brew install deno` しました。`deno` コマンドが入ります。セットアップはこれで終わり。 11 | 12 | GitHub の trending に上がっていた https://github.com/oakserver/oak という web server を試してみます。 13 | 14 | ```ts 15 | // server.ts 16 | import { Application } from "https://deno.land/x/oak/mod.ts"; 17 | const app = new Application(); 18 | 19 | app.use((ctx) => { 20 | ctx.response.body = "Hello World!"; 21 | }); 22 | 23 | await app.listen({ port: 8000 }); 24 | ``` 25 | 26 | このコードを保存して、実行します 27 | 28 | ``` 29 | deno run --allow-net server.ts 30 | ``` 31 | 32 | これだけ。コマンド実行時に `https://deno.land/x/oak/mod.ts` から依存がダウンロードされて、サーバーが立ちます。 33 | 34 | ここで、インストールコマンドを何も叩いてないのに注目してください。Deno では実行時に URL が静的に解決されて実行されます。パッケージや `package.json` といったものがありません。 35 | 36 | `--allow-net` も特徴的ですね。deno ではデフォルトでは権限が限定されています。ローカルファイルに触るには、 `--allow-read` や `--allow-write` が必要になります。 37 | 38 | ## 開発環境 39 | 40 | ここはまだ難があるように思います。 41 | 42 | Deno ではモジュールシステムやパス解決が純粋な TypeScript と非互換(`.ts` の拡張子省略ができない) ので、 vscode 拡張などで専用の typescript server を起使う、といったソリューションが試みられています。 で、`axetroy/deno` と `justjavac/deno` があるんですが、どちらも中途半端です。どちらも import-map 対応のオプションがあるように見えるんですが、動きません。ちょっと変なことをすると、すぐ動かなくなります。 43 | 44 | deno-ja の slack で聞いた限りでは justjavac の方が公式に近い立ち位置なんですが、 axetroy/deno の fork らしいんですが、まだ axetroy/deno のがちゃんと動いてるみたいです。 45 | 46 | (そもそも deno やってる人たちは vscode を TS の違反の警告を無視するのになれてるらしく、エディタ支援はない前提っぽいで書いてる人が多い印象) 47 | 48 | ## モジュールシステム 49 | 50 | 公式には deno.land/std と、サードパーティ相当の deno.land/x があります 51 | 52 | - https://deno.land/std 53 | - https://deno.land/x 54 | 55 | それとは別に、ESM で node の標準ライブラリに頼らずビルドされたコードは import できます。 npm をバックエンドにした CDN としては、以下のようなものがあります。 56 | 57 | - [jsDelivr \- A free, fast, and reliable Open Source CDN for npm & GitHub](https://www.jsdelivr.com/) のうち、rollup の ESM ビルドが配布されているもの 58 | - [UNPKG](https://unpkg.com/) の `?module` での esm build 59 | - [Pika CDN](https://www.pika.dev/cdn) で ESM 用にビルドされた JS 60 | 61 | ## Next.js クローンを作ろうとしてみた 62 | 63 | [mizchi/toxen](https://github.com/mizchi/toxen) っていうリポジトリで、 next.js っぽいサーバー書いて遊んでました。(まだ実験中です) 64 | 65 | 技術スタック 66 | 67 | - deno 68 | - snowpack 69 | - oak 70 | - preact 71 | - htm 72 | 73 | snowpack は一種のフロントエンド向けのバンドラーなんですが、webpack に頼らずネイティブ ESM で動くモジュールを吐いて、その際に snowpack が生成する `import-map.json` を deno の importmap として使う、といった方法を選んでみました。 74 | 75 | - [Snowpack](https://www.snowpack.dev/) 76 | - [WICG/import\-maps: How to control the behavior of JavaScript imports](https://github.com/WICG/import-maps) 77 | 78 | やってみた感じ、 `import { h } from "preact"` という感じの、比較的 node と似たような書き味で、快適です。 79 | 80 | ただ、事前に snowpack でビルドして、動的に `pages/*.ts` を読んで、 `dist/*.js` に吐き出して…とやっていたら、結局こんな感じの実行スクリプトに 81 | 82 | ``` 83 | snowpack && deno run --unstable --allow-write --allow-read --allow-net --importmap web_modules/import-map.json server.ts 84 | ``` 85 | 86 | 権限を厳密に管理するので、多少辛いですね… 87 | 88 | deno ならではの工夫として、next.js のクローンなので `pages/*.ts` のエンドポイントをターゲットに SSR する必要があるんですが、webpack や rollup のバンドラを使うのではなく `Deno.bundle(Deno.cwd() + "/pages/foo.ts")` みたいなコードで Deno の内部バンドラをそのまま使って、静的な JS を吐いてブラウザに食わせています。 89 | 90 | あと、 amp + amp-script(worker-dom) で動かしているので、 amp ページでありながら動的に動く画面を作れます。 worker-dom は技術的な詳細はここでは解説しませんが、動的なコンポーネントを初期化するには SSR が必須です。 91 | 92 | [Documentation: \- amp\.dev](https://amp.dev/documentation/components/amp-script/) 93 | 94 | React ではなく preact なのは、 amp-script の 150kb 制限をクリアするためです。tsx を使っていなくて htm なのは、preact pragma を使って tsx を書くと React の型がないので、実行時例外になるのをかわすためのワークアラウンドです。 95 | 96 | 今後の TODO として、動的 URL に対応する、Deno ライブラリとして抽象的にする、export できるようにする、といった方向性があると思ってるんですが、これを本気で開発するかはまだ悩んでいて、もうちょっと Deno 自体が安定してほしい気持ちもありますね。 97 | 98 | ただ、このブログのような SSG なら、ランタイムに deno がいないので、比較的安心して採用できる気がしました。 99 | 100 | ## Universal JavaScript を考え直す 101 | 102 | ここまでやってみて、 Deno に対応する、というのは 2 つの水準があると思いました。 103 | 104 | - CLI ツールなど向けに、 deno の標準ライブラリを使って、 `.ts` で deno べったりで書き直す 105 | - Universal な JavaScript として、 rollup でビルドして配布する 106 | 107 | 要は esm に対応してさえいれば読み込みはできるので、Universal な JavaScript としては、rollup でビルドしていると deno 対応している、と言えなくはないです。npm にあげてしまった後は、npm の CDN である jsdelivr を経由して読み込むといいでしょう。 108 | 109 | deno を使っていくかどうかはともかく、 snowpack など esm 前提の実行環境なども増えてるので、この辺を意識しておくと、将来的な可用性が増えるのではないでしょうか。 110 | -------------------------------------------------------------------------------- /docs/mdxx-0.6.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: amdx@0.6 - PWA Support, Lighthouse 100, RSS対応, コンポーネントの Tailwind 化 3 | created: 1589106473785 4 | tags: [amdx] 5 | --- 6 | 7 | 例によってこのブログ自身が実装例です。 8 | 9 | ![](https://gyazo.com/766cb68e83165ff078ae997f9f1f852e.png) 10 | 11 | Accessbility のスコアがちょっと低いのは、 amp-social-share の生成する要素に `aria-label` がついてないから。どうしろと 12 | 13 | ## Done 14 | 15 | ### amdxg 16 | 17 | - Lighthouse 100 18 | - PWA Support 19 | - RSS 対応 `/rss.xml` 20 | 21 | ### amdxg-cli 22 | 23 | - `amdxg postbuild:rss` - rss 対応 24 | - `amdxg postbuild:sitemap`: WIP - sitemap.xml 生成する(Search Console に認識されない…) 25 | 26 | ### amdxg-components 27 | 28 | - Tailwindcss + postcss + purgecss 29 | - Better CSS markup 30 | 31 | ## 次の TODO 32 | 33 | - タグのサポート: frontmatter で `tags: [a,b,c]` とした場合、タグによるインデックスページを生成 `/tags/hoge` 34 | -------------------------------------------------------------------------------- /docs/mdxx-0.7.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: amdx@0.7 tag 3 | created: 1589127481769 4 | tags: [amdx] 5 | --- 6 | 7 | ## 仕様変更 8 | 9 | `amdxg new:page` が `pages/*.tsx` を生成しないように 10 | 11 | かわりに、 `pages/[slug].tsx` が共通の記事要素になりました。 12 | 13 | ## new feature: tags support 14 | 15 | frontmatter の yaml を使って、タグを宣言できるようになりました 16 | 17 | ```yaml 18 | --- 19 | title: my-title 20 | created: 1589127481769 21 | tags: [a, b, c] 22 | --- 23 | # hello 24 | ``` 25 | 26 | トップページにタグ一覧が表示されます。 27 | 28 | `/tags/[tag]` ページで、そのページに紐付いている記事一覧を出せるようにしました。 29 | 30 | tags 31 | 32 | ![](https://gyazo.com/a9248604c3c553701ca93632dbacd847.png) 33 | 34 | ## v1.0.0 に向けて 35 | 36 | - 画像の拡大が壊れている気がするので、それを修正する 37 | - parser/compiler のリネーム 38 | - そろそろ使えるようになってきた 39 | -------------------------------------------------------------------------------- /docs/mdxx-cli-and-components.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: amdxg@0.5.0 - amdxg-cli & amdx-components 3 | created: 1588952818281 4 | tags: [amdx] 5 | --- 6 | 7 | ## ChangeLog 8 | 9 | - amdx 10 | - `$$ ~ $$` の数式ブロックを amp-mathml に変換 11 | - amdxg-components 12 | - `amdxg` は現状単なるボイラープレートだが、設定が最小で済むように `Layout` `Article` などに抽出 13 | - [amp-social-share で、はてなブックマークに対応](/amp-social-share-for-hatena-bookmark) 14 | - amdxg-cli: CLI ツール 15 | - 記事一覧の生成 (`gen/pages.json`) 16 | - 記事のヒストリーの生成 (`gen/*.history.json`) 17 | 18 | ## amdxg-cli のインストール 19 | 20 | ```bash 21 | npm install -g amdxg-cli 22 | ``` 23 | 24 | ## amdxg new 25 | 26 | WIP 27 | 28 | ## new feature: 新規記事の作成 29 | 30 | ``` 31 | amdxg new:page [foo] 32 | ``` 33 | 34 | ## new feature: インデックスの記事一覧とヒストリーの生成 35 | 36 | ``` 37 | amdxg build:index 38 | ``` 39 | 40 | `docs` ディレクトリの記事一覧から、`gen/pages.json` を生成する。 41 | 42 | ``` 43 | amdxg build:history 44 | ``` 45 | 46 | `docs/*.mdx` の git コミットログから変更ヒストリーを生成する。 47 | 48 | ```tsx 49 | import history from "../gen/pages/foo.history.json"; 50 | import { History } from "amdxg-components"; 51 | 52 | ; 53 | ``` 54 | 55 | amdxg.config で、もし `"repository": "mizchi/amdx"` のように GitHub のリポジトリを指定していると、そのコミットへのリンクになる。 56 | 57 | ## TODO 58 | 59 | - `pages/sitemap.xml`: sitemap 対応 60 | - `pages/feed.rss`: feed 対応 61 | - `amdxg new your-blog`: 最新版から scaffold できるようにする 62 | - `amdxg new:page` 時、記事生成のテンプレート化できるようにする対応 63 | - `amdxg-components`: `Layout` を使わない、カスタマイズ度が高い作例を用意する 64 | - frontmatter のタグ一覧対応 65 | - amdxg を github actions でデプロイできるようにしておく 66 | - amdx preview の vscode extension 67 | -------------------------------------------------------------------------------- /docs/mdxx-introduction.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: MDX eXtended = MDXX | AMP対応 Markdown Compiler と静的サイトジェネレーター 3 | created: 1588688918620 4 | tags: [amdx] 5 | --- 6 | 7 | 最近作っている amdx という markdown コンパイラとそのツール郡を紹介します。 8 | 9 | GitHub はここ [mizchi/amdx](https://github.com/mizchi/amdx) 10 | 11 | このサイト自体も、 [amdxg](https://github.com/mizchi/amdx/tree/master/packages/ssg) というツールで作られています。 12 | 13 | ## ゴールをどこに設定したか 14 | 15 | - パフォーマンスを突き詰めると、ブログは静的サイトジェネレータで AMP 対応するのが一番と考えた 16 | - next.js/SSG export + AMP が便利だったので、next.js 上でコンパイルすることを前提に、静的解析を行う webpack loader を作ることにした 17 | - mdx の parser を借りて、 `.mdx` をロードすると React Component として振る舞いつつ、他の `mdx` を import できると、長い文章を書くときにファイル分割できて便利なのでは 18 | - Markdown プレビュー高速化+ランタイム最小化のために、AST(JSON) 生成する部分と、AST からの実行処理部分を分割した 19 | - 基本は Full AMP で、React Component 動くページを作れる余地も残したい 20 | 21 | ## 何を作ったか/amdx とはなにか 22 | 23 | - remark ベースで React Component を出力する parser / compiler / cli / next.js template 24 | - AMP 対応: img は `amp-img` を吐くなど 25 | - yaml frontmatter 対応 26 | - Code Syntax Highlight 対応: prismjs で静的解析をするので、CSS のみでランタイム不要 27 | - ToC 対応 28 | 29 | 基本は mdx をベースに、高速化+静的解析で最適化+AMP 対応+周辺ツールという感じ 30 | 31 | ## 周辺ツール: npm packages 32 | 33 | - amdx-loader: webpack loader 34 | - rollup-plugin-amdx: rollup 35 | - amdx-cli: コマンドラインで mdx をコンパイルできる CLI ツール 36 | 37 | ## 周辺ツール amdxg 38 | 39 | このブログ自身がサンプルなのですが、自分自身のサイトを作ることもできます。 40 | 41 | こんな感じ。 42 | 43 | ```bash 44 | # install node and npm 45 | npx degit mizchi/amdx/packages/ssg blog 46 | cd blog 47 | npm install 48 | cp amdxg.config.example amdxg.config 49 | # edit amdxg.config for you 50 | 51 | # create new page 52 | npm run new-page 53 | # edit docs/[current-date].mdx 54 | 55 | # create new page with slug 56 | npm run new-page foo 57 | # edit docs/foo.mdx 58 | 59 | # Preview 60 | npm run dev 61 | 62 | # Deploy 63 | npm run build 64 | ``` 65 | 66 | netlify や github pages や firebase hosting を想定しています。 67 | 68 | netlify にデプロイする例。 69 | 70 | ``` 71 | yarn build 72 | netlify deploy -d out --prod 73 | ``` 74 | 75 | まだ色々と手抜きで、かつ頻繁に更新しているので、使いたい人は PR or 自分で CSS 書くなどしてください。 76 | 77 | ## 現在のステータス 78 | 79 | - ドキュメントが足りてない 80 | - SSG といいつつ、ただの愚直な next.js プリセットという感じ。コンポーネント化+ちゃんとしたデザインをする 81 | 82 | ## TODO 83 | 84 | - [ ] カスタムな Remark Plugin を読み込む口を開ける 85 | - [ ] amp-script 対応: AMP 内でバンドルした js を生成して、worker-dom で動かす 86 | - [ ] amp-mathml: 非 AMP 環境向けでは KaTex で対応しているが、amp 時は amp-mathml に切り替える 87 | - [ ] https://mdbuf.netlify.com/ で使うコンパイラを amdx にする 88 | - [ ] 推奨の CSS を配布する 89 | - [ ] commit log から history pages を作成し、かつ GitHub の PR を作れる機能を作りたい(fork せずにできるのか?) 90 | 91 | ## 余談: mdx ではだめだったのか 92 | 93 | https://github.com/mdx-js/mdx/tree/master/packages/mdx を読んだ限りは、拡張しづらく、自分の用途に合いませんでした。 94 | 95 | - remark の parse と complile(stringify) を分離できない 96 | - [mdbuf](https://mdbuf.netlify.com/) を作った経験だと、 React Element は worker で渡せないので、pure json な AST を作る必要がある 97 | - がっつり拡張するなら amdx が良さそうだった 98 | 99 | ## おわり 100 | 101 | 買っておいた秘蔵の `https://mizchi.dev` を netlify から繋いで、今後も自分のブログとしてドッグフーディングしつつ作っていきます。 102 | 103 | 機能要望などあったら、[GitHub の Issue](https://github.com/mizchi/amdx/issues) か [@mizchi](https://twitter.com/mizchi) まで Twitter でお知らせください。 104 | -------------------------------------------------------------------------------- /docs/next-amp-tailwind-postcss.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: next.js の amp モードで tailwind.css を purgecss と合わせて使う 3 | created: 1589132870983 4 | tags: 5 | - next 6 | - amp 7 | - tailwindcss 8 | - purgecss 9 | --- 10 | 11 | [このリポジトリ](https://github.com/mizchi/next-amp-tailwind-purgecss) でやったこと。 12 | 13 | ## やろうとしたこと 14 | 15 | tailwind.css は Utility First と銘打った CSS フレームワークで、コンポーネント化を前提としたモダンフレームワークと相性がいいです。今回は next.js の amp-mode で tailwind を使おうとしてみました。 16 | 17 | [Tailwind CSS \- A Utility\-First CSS Framework for Rapidly Building Custom Designs](https://tailwindcss.com/) 18 | 19 | ## 問題 20 | 21 | 前提として、 amp には inline css の容量制限があり、75kb を超えると AMP と認識されなくなります。 22 | 23 | tailwind はビルドして使うのが前提のフレームワークですが、全部入りの tailwind.min.css は 1.3 M あります。これでは到底、75 kb に収まりません。 24 | 25 | [AMP の CSS サイズ上限が 50000 バイトから 75000 バイトへ 50%増量 ⬆ \| 海外 SEO 情報ブログ](https://www.suzukikenichi.com/blog/amp-increases-css-limit-from-50000-bytes-to-75-bytes/) 26 | 27 | tailwind css は reset.css が重いというより、utility の数が膨大で、それらをすべて読み込むのでビルドサイズがかさんでるという感じでした。 28 | 29 | なので、 purgecss を使って、使っている CSS だけ使えばよい、という発想で CSS のサイズを削ってみました。 30 | 31 | ## purgecss 32 | 33 | purgecss は指定した html,js,tsx, vue のソースコード上に出現する CSS セレクタのみ抽出するライブラリです 34 | 35 | [FullHuman/purgecss: Remove unused CSS](https://github.com/FullHuman/purgecss) 36 | 37 | もちろん、素朴にやるとそれだけでは動かないので、さらに whitelist で許可する CSS を宣言します。今回は postcss を通して使います。 38 | 39 | ## postcss の設定 40 | 41 | ついでに、`postcss-import` を使って、単一ファイルに bundle しつつ、 `cssnano` で圧縮しています。 42 | 43 | 準備 44 | 45 | ``` 46 | yarn add postcss postcss-import taliwindcss @fullhuman/postcss-purgecss postcss-preset-env cssnano postcss-cli -D 47 | ``` 48 | 49 | `postcss.config.js` 50 | 51 | ```js 52 | module.exports = { 53 | plugins: [ 54 | require("postcss-import"), 55 | require("tailwindcss"), 56 | require("@fullhuman/postcss-purgecss")({ 57 | content: [ 58 | "./pages/**/*.{js,jsx,ts,tsx}", 59 | "./components/**/*.{js,jsx,ts,tsx}", 60 | ], 61 | defaultExtractor: (content) => content.match(/[\w-/:]+(? (props) => ); 93 | const initialProps: any = await Document.getInitialProps(ctx); 94 | return { 95 | ...page, 96 | styles: [ 97 | ...initialProps.styles, 98 | ` と一つの CSS タグで展開されます。 192 | 193 | ## Google Analytics 対応 194 | 195 | amp-analytics を使います。 196 | 197 | [AMP ページにアナリティクスを追加する  \|  AMP ページ向けアナリティクス  \|  Google Developers](https://developers.google.com/analytics/devguides/collection/amp-analytics?hl=ja) 198 | 199 | Google Analytics 側で新しいサービスを登録し、発行された埋め込みタグの gtag_id だけ抜き出しておきます。 200 | 201 | ``` 202 | // components/ItemLayout.tsx 203 | 204 | function Analytics() { 205 | const json = JSON.stringify({ 206 | vars: { 207 | gtag_id: "[your-gtag-id]", 208 | config: { 209 | "[your-gtag-id]": { groups: "default" }, 210 | }, 211 | }, 212 | }); 213 | return ( 214 | // @ts-ignore 215 | // prettier-ignore 216 | 100 | main.js 101 | ``` 102 | 103 | で、 これと組み合わせる `webpack-dev-server` を今回は使います。 webpack によるビルド対象が更新されると自動で再ビルドしつつ、output するディレクトリをルートとする静的アセットのサーバーです。 104 | 105 | 実行します。 106 | 107 | ``` 108 | npx webpack-dev-server 109 | ``` 110 | 111 | 先の dist をルートとして、 `http://localhost:8080` にサーバーが立ち上がりました。多くのサーバーと同じく、 `/` は `index.html` が返却されるため、これで `src/index.tsx` が自動でビルドされて更新される環境が実現できました。 112 | 113 | 試しに、`
Hello
` を `
Hoge
` などに書き換えて保存してみてください。画面に反映されるはずです。 114 | 115 | この html-webpack-plugin + webpack-dev-server の組み合わせは、React に限らずフロントエンドのライブラリや新しい機能を実験をする際にとても有利です。是非覚えておきましょう。 116 | 117 | ## Counter の例 118 | 119 | ボタンを押すと、値が 1 増えるという、よくあるカウンタ実装の例を示します 120 | 121 | ```tsx 122 | // src/index.tsx 123 | import React, { useState } from "react"; 124 | import ReactDOM from "react-dom"; 125 | 126 | function Counter() { 127 | const [counter, setCounter] = useState(0); 128 | return ( 129 |
130 |
Counter: {counter}
131 | 138 |
139 | ); 140 | } 141 | 142 | // マウント用のElementを生成 143 | const el = document.createElement("div"); 144 | document.body.appendChild(el); 145 | ReactDOM.render(, el); 146 | ``` 147 | 148 | これは Function Component という機能で、`` はその関数の実行結果で展開されます。他に Class Copmonent があるのですが、現在の主流は Function Component なので、今回は解説しません。 149 | 150 | React で状態を扱うには、 hooks という API を使います。 `useState()` は引数として初期値を受け取り、 `[現在の値, 値を更新する関数]` という配列を返します。これを分割代入という記法で、それぞれ受け取っています。 151 | 152 | `onClick={() => {...}}` という構文で、click イベントリスナーに onClick の際に実行される関数を渡しています。つまり、 ボタンをクリックするたびに `setCounter((n) => n + 1)` を実行します。setCounter の引数は 2 種類あって、「次の値」、もしくは「前の値を引数として、次の値を返す関数」です。 153 | 154 | `setCounter` が実行されると、この Counter の関数コンポーネントが、関数として再度実行されます。この時、 `const [counter, setCounter] = useState(0);` の `counter` は 1 を返します。 155 | 156 | この結果、 157 | 158 | ```tsx 159 |
160 |
Counter: 1
161 | 168 |
169 | ``` 170 | 171 | という仮想 DOM が生成されます。React の差分適用アルゴリズムにより、目に見える部分としては、 `Counter: 1` の部分が更新されます。目には見えませんが、 button のイベントハンドラの関数も更新されます。 172 | 173 | このように、都度差分オブジェクトをまるごと生成し、React の差分検知で必要な部分だけ書き換えが走る、というのが基本的なライフサイクルとなります。 174 | 175 | ## Component と props と TypeScript 176 | 177 | ```tsx 178 | // src/index.tsx 179 | import React, { useState } from "react"; 180 | import ReactDOM from "react-dom"; 181 | 182 | function Counter(props: { text: string; initialValue: number }) { 183 | const [counter, setCounter] = useState(0); 184 | return ( 185 |
186 |
187 | {props.text}: {props.counter} 188 |
189 | 196 |
197 | ); 198 | } 199 | 200 | function App() { 201 | return ( 202 | <> 203 | 204 | 205 | 206 | ); 207 | } 208 | 209 | const el = document.createElement("div"); 210 | document.body.appendChild(el); 211 | ReactDOM.render(, el); 212 | ``` 213 | 214 | まず `<>...` は `Fragment` という仮想のノードで、DOM としての実体をもちません。Component として、複数の要素を返す時に便利な機能です。 215 | 216 | この Fragment の中で、 2 つの Counter を返しています。 ここでコンポーネントへの引数として、 `text` と `initialValue` という値を渡しています。`text="A"` は文字列として渡すときに使える構文で、 `{0}` は 数値型に限らず、JavaScript のオブジェクトを渡す時に使える構文です。なので、 `{"A"}` でも良いです。 217 | 218 | 次に、 Counter の関数コンポーネントの引数を見ていきましょう。ここまで tsx を使いつつ TypeScript の機能を使っていませんでしたが、ここでやっと TypeScript の型の出番です。 219 | 220 | `function Counter(props: { text: string, initialValue: number }) {...}` 221 | 222 | は、第一引数の `props` として `{ text: string, initialValue: number }` のオブジェクトを受け取る、という意味になります。慣習的に、この値は `props` と名付けることになっています。 223 | 224 | このとき、 A と B の2つの `Counter` は独自の初期値と、独自のライフサイクルを持ちます。試しに、2つのカウンターをクリックしてみてください。再描画、再更新の単位は、それぞれ独自となっているはずです。このように、 Component は展開するビューを宣言する関数であるだけでなく、ライフサイクルの単位でもあります。 225 | 226 | ## Context 227 | 228 | props は必ず親から渡す必要があります。親子間で自明ですが、これとは別に、「特定の親に依存した値」という値を参照するために、 context という機能があります。 229 | 230 | ```tsx 231 | // src/index.tsx 232 | import React, { useState, useContext } from "react"; 233 | import ReactDOM from "react-dom"; 234 | 235 | const ThemeContext = React.createContext<{ color: string }>({ 236 | color: "transparent", // 初期値。今回は使わない 237 | }); 238 | 239 | function View() { 240 | const theme = useContext(ThemeContext); 241 | return
Text with color
; 242 | } 243 | 244 | function App() { 245 | return ( 246 | <> 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | ); 255 | } 256 | 257 | const el = document.createElement("div"); 258 | document.body.appendChild(el); 259 | ReactDOM.render(, el); 260 | ``` 261 | 262 | `React.createContext` で生成された、 `ThemeContext.Provider` という要素の下では、 `useContext` すると、`` の value が取れるようになります。 263 | 264 | ## 簡易 Flux 265 | 266 | ```tsx 267 | // src/index.tsx 268 | import React, { useState, useContext, SetStateAction, Dispatch } from "react"; 269 | import ReactDOM from "react-dom"; 270 | 271 | type State = { 272 | value: number; 273 | }; 274 | 275 | const AppStateContext = React.createContext({ 276 | value: 0, 277 | }); 278 | 279 | const SetAppStateContext = React.createContext>>( 280 | () => {} 281 | ); 282 | 283 | function View() { 284 | const appState = useContext(AppStateContext); 285 | const setAppState = useContext(SetAppStateContext); 286 | 287 | return ( 288 |
289 | {appState.value} 290 | 295 |
296 | ); 297 | } 298 | 299 | function App() { 300 | const [state, setState] = useState({ value: 0 }); 301 | return ( 302 | <> 303 | 304 | 305 | 306 | 307 | 308 | 309 | ); 310 | } 311 | 312 | const el = document.createElement("div"); 313 | document.body.appendChild(el); 314 | ReactDOM.render(, el); 315 | ``` 316 | 317 | これは、 View が一つしか無いので必要性がわかりづらいのですが、 大仰な Counter です。 318 | 319 | ## おわり 320 | 321 | React の機能を全部紹介すると大変な分量になるので、ここでは状態管理のためのイディオムの紹介に留めました。 322 | 323 | ## Next.js においての react 324 | 325 | React は効率的な DOM 管理のために生まれました。しかし、その発展の過程で、一つのテンプレートとしても受容されてきたという歴史があります。JSX 記法は React だけの記法ではなく、 vue.js や preact といったフレームワークでも使用することができます。 326 | 327 | next.js ではテンプレートとして React を使いつつ、クライアントサイドでの React の力を 100% 使うことができます。初期状態を初期状態の HTML として返却しつつ、その初期 HTML を引き継いで、シームレスにクライアントで動かすことができます。これは、他の Web Application Framework にない唯一無二の機能です。 328 | 329 | しかし、クライアント・サーバーの状態引き継ぎは実はとても難しいテクニックを必要とします。具体的には Server Side Rendering と、 Client での Hydration という2つを組み合わせます。 330 | 331 | 次の章では、 Server Side Node.js として、クライアントで動くようにするためのテクニックを紹介します。 332 | -------------------------------------------------------------------------------- /fragments/nextjs/05.mdx: -------------------------------------------------------------------------------- 1 | # 第 5 章: Server Side Node.js と React SSR 2 | 3 | ## はじめに 4 | 5 | node.js はフロントエンドツールチェインを扱うものとして扱ってきましたが、この章では Server Side の言語としての側面をやっていきつつ、ここまでやってきた React と協調する SSR という技術を紹介します。 6 | 7 | 先に言っておきますが、この章の内容は難しいですし、next.js というものを扱うにあたって、実際必須ではありません。ただ、知っておかないと next.js のありがたみも半減します。 8 | 9 | next.js がやってくれる SSR を、next.js でやりながら紹介しつつ、その難易度を感じることを目的とします。 10 | 11 | ## express の環境構築 12 | 13 | ``` 14 | npm install express --save 15 | npm install @types/express --save-dev 16 | ``` 17 | 18 | ```ts 19 | import express from "express"; 20 | const app = express(); 21 | 22 | app.get<{ id: string }, { foo: number }>("/foo/:id", (req, res) => { 23 | res.json({ 24 | foo: 1, 25 | }); 26 | }); 27 | 28 | app.listen(3000, () => { 29 | console.log("server strated!"); 30 | }); 31 | ``` 32 | 33 | ## SSR 34 | 35 | ```tsx 36 | // server.tsx 37 | import React from "react"; 38 | import ReactDOMServer from "react-dom/server"; 39 | import express from "express"; 40 | import path from "path"; 41 | 42 | const app = express(); 43 | 44 | type State = { value: 0 }; 45 | 46 | function Html(props: { App: React.ComponentType; initialState: State }) { 47 | const html = ; 48 | return ( 49 | 50 | 51 | 8 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "mz", 3 | "name": "mizchi.dev", 4 | "start_url": "/", 5 | "icons": [ 6 | { 7 | "src": "/android-chrome-192x192.png", 8 | "sizes": "192x192", 9 | "type": "image/png" 10 | }, 11 | { 12 | "src": "/android-chrome-512x512.png", 13 | "sizes": "512x512", 14 | "type": "image/png" 15 | } 16 | ], 17 | "display": "standalone", 18 | "scope": "/", 19 | "theme_color": "#005af0", 20 | "background_color": "#ffffff" 21 | } 22 | -------------------------------------------------------------------------------- /public/ogp/202005182044-awesome-next-issg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mizchi/dev/f55d43737302d408a08722c6f76af0330f7ca34c/public/ogp/202005182044-awesome-next-issg.png -------------------------------------------------------------------------------- /public/ogp/202005271609-react-app-context.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mizchi/dev/f55d43737302d408a08722c6f76af0330f7ca34c/public/ogp/202005271609-react-app-context.png -------------------------------------------------------------------------------- /public/ogp/202005280125-eval-esm-in-iframe.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mizchi/dev/f55d43737302d408a08722c6f76af0330f7ca34c/public/ogp/202005280125-eval-esm-in-iframe.png -------------------------------------------------------------------------------- /public/ogp/202006061823-tiny-bundler.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mizchi/dev/f55d43737302d408a08722c6f76af0330f7ca34c/public/ogp/202006061823-tiny-bundler.png -------------------------------------------------------------------------------- /public/ogp/202006091517-todays-react-jsonschema-form.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mizchi/dev/f55d43737302d408a08722c6f76af0330f7ca34c/public/ogp/202006091517-todays-react-jsonschema-form.png -------------------------------------------------------------------------------- /public/ogp/202006101314-switch-tsconfig-on-webpack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mizchi/dev/f55d43737302d408a08722c6f76af0330f7ca34c/public/ogp/202006101314-switch-tsconfig-on-webpack.png -------------------------------------------------------------------------------- /public/ogp/202006161259-try-netlify-lambda.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mizchi/dev/f55d43737302d408a08722c6f76af0330f7ca34c/public/ogp/202006161259-try-netlify-lambda.png -------------------------------------------------------------------------------- /public/ogp/202006161500-netlify-lambda-chromium.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mizchi/dev/f55d43737302d408a08722c6f76af0330f7ca34c/public/ogp/202006161500-netlify-lambda-chromium.png -------------------------------------------------------------------------------- /public/ogp/202006210107-avoid-ie-first.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mizchi/dev/f55d43737302d408a08722c6f76af0330f7ca34c/public/ogp/202006210107-avoid-ie-first.png -------------------------------------------------------------------------------- /public/ogp/amdx-0.8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mizchi/dev/f55d43737302d408a08722c6f76af0330f7ca34c/public/ogp/amdx-0.8.png -------------------------------------------------------------------------------- /public/ogp/amp-social-share-for-hatena-bookmark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mizchi/dev/f55d43737302d408a08722c6f76af0330f7ca34c/public/ogp/amp-social-share-for-hatena-bookmark.png -------------------------------------------------------------------------------- /public/ogp/hello-deno-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mizchi/dev/f55d43737302d408a08722c6f76af0330f7ca34c/public/ogp/hello-deno-1.png -------------------------------------------------------------------------------- /public/ogp/mdxx-0.6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mizchi/dev/f55d43737302d408a08722c6f76af0330f7ca34c/public/ogp/mdxx-0.6.png -------------------------------------------------------------------------------- /public/ogp/mdxx-0.7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mizchi/dev/f55d43737302d408a08722c6f76af0330f7ca34c/public/ogp/mdxx-0.7.png -------------------------------------------------------------------------------- /public/ogp/mdxx-cli-and-components.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mizchi/dev/f55d43737302d408a08722c6f76af0330f7ca34c/public/ogp/mdxx-cli-and-components.png -------------------------------------------------------------------------------- /public/ogp/mdxx-introduction.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mizchi/dev/f55d43737302d408a08722c6f76af0330f7ca34c/public/ogp/mdxx-introduction.png -------------------------------------------------------------------------------- /public/ogp/next-amp-tailwind-postcss.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mizchi/dev/f55d43737302d408a08722c6f76af0330f7ca34c/public/ogp/next-amp-tailwind-postcss.png -------------------------------------------------------------------------------- /public/ogp/study-next-amp-by-mdxx-ssg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mizchi/dev/f55d43737302d408a08722c6f76af0330f7ca34c/public/ogp/study-next-amp-by-mdxx-ssg.png -------------------------------------------------------------------------------- /public/ogp/study-recoil.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mizchi/dev/f55d43737302d408a08722c6f76af0330f7ca34c/public/ogp/study-recoil.png -------------------------------------------------------------------------------- /public/sw.js: -------------------------------------------------------------------------------- 1 | importScripts("https://cdn.ampproject.org/sw/amp-sw.js"); 2 | 3 | /* 4 | Checkout https://github.com/ampproject/amp-sw/ to learn more about how to configure 5 | */ 6 | AMP_SW.init({ 7 | assetCachingOptions: [ 8 | { 9 | regexp: /\.(png|jpg|woff2|woff|css|js|html)/, 10 | cachingStrategy: "CACHE_FIRST", // options are NETWORK_FIRST | CACHE_FIRST | STALE_WHILE_REVALIDATE 11 | }, 12 | ], 13 | // offlinePageOptions: { 14 | // url: "/offline", 15 | // assets: [], 16 | // }, 17 | }); 18 | 19 | console.log("start sw"); 20 | -------------------------------------------------------------------------------- /script/counter.tsx: -------------------------------------------------------------------------------- 1 | /** @jsx h */ 2 | import { h } from "preact"; 3 | import { useState } from "preact/hooks"; 4 | 5 | export default function Counter(props: { initialValue: number }) { 6 | const [state, setState] = useState(props.initialValue); 7 | return ( 8 |
9 |

Counter Example

10 | {String(state)} 11 | 12 | 13 |
14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /script/generate-ogp.mjs: -------------------------------------------------------------------------------- 1 | import canvas from "canvas"; 2 | import fs from "fs/promises"; 3 | import path from "path"; 4 | const dirname = path.dirname(new URL(import.meta.url).pathname); 5 | 6 | const W = 600; 7 | const H = 315; 8 | const LINE_HEIGHT = 30; 9 | 10 | function getRows(ctx, text) { 11 | const words = text.split(" "); 12 | 13 | let rows = []; 14 | let currentRow = []; 15 | let tokens = words.slice(0); 16 | let token; 17 | while ((token = tokens.shift())) { 18 | const mText = [...currentRow, token].join(" "); 19 | const measure = ctx.measureText(mText); 20 | if (measure.width <= W) { 21 | currentRow.push(token); 22 | } else { 23 | rows.push(currentRow.slice()); 24 | currentRow = [token]; 25 | } 26 | } 27 | if (currentRow.length > 0) { 28 | rows.push(currentRow); 29 | } 30 | 31 | return rows; 32 | } 33 | 34 | function renderText(ctx, rows) { 35 | const rowCount = rows.length; 36 | for (let i = 0; i < rowCount; i++) { 37 | const rowText = rows[i].join(" "); 38 | const m = ctx.measureText(rowText); 39 | 40 | const w = (W - m.width) / 2; 41 | // const h = (LINE_HEIGHT + 12) * (i + 1); 42 | const h = 40 + 210 / 2 - (LINE_HEIGHT + 12) * (rowCount - i - 1); 43 | 44 | ctx.fillText(rowText, w, h); 45 | } 46 | } 47 | 48 | async function generateImage(text, outputPath) { 49 | const cvs = canvas.createCanvas(W, H); 50 | const ctx = cvs.getContext("2d"); 51 | ctx.font = `${LINE_HEIGHT}px Impact`; 52 | 53 | const rows = getRows(ctx, text); 54 | 55 | ctx.fillStyle = "white"; 56 | ctx.fillRect(0, 0, W, H); 57 | 58 | ctx.fillStyle = "black"; 59 | renderText(ctx, rows); 60 | 61 | const m = ctx.measureText("mizdev"); 62 | ctx.fillText("mizdev", (W - m.width) / 2, 250); 63 | const buf = cvs.toBuffer(); 64 | await fs.writeFile(outputPath, buf); 65 | } 66 | 67 | async function main() { 68 | const pages = JSON.parse( 69 | await fs.readFile(path.join(dirname, "../gen/pages.json")) 70 | ); 71 | await Promise.all( 72 | pages.map(async (p) => { 73 | const out = path.join(dirname, "../public/ogp/", p.slug + ".png"); 74 | await generateImage(p.title, out); 75 | console.log("[gen:ogp]", out); 76 | }) 77 | ); 78 | } 79 | main(); 80 | -------------------------------------------------------------------------------- /slides/develop-mizchi-dev.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: next.js で自分のブログを作る 3 | --- 4 | 5 | # next.js で自分のブログを作る 6 | 7 | 自分のブログとして [mizchi.dev](https://mizchi.dev) を作った話 8 | 9 | at [隅田川\.js \#1\(オンライン\) \- connpass](https://sumidagawajs.connpass.com/event/167632/) 10 | 11 | --- 12 | 13 | ## 何を作ったか 14 | 15 | このブログ自身(mizchi.dev)。スライドツールも突貫で自作 16 | 17 | ソースコード [mizchi/dev](https://github.com/mizchi/dev) 18 | 19 | --- 20 | 21 | ## Lighthouse 22 | 23 | ![](https://i.gyazo.com/718543e55f8ca7c35e81bb67aa5cfa79.png) 24 | 25 | --- 26 | 27 | ## Full AMP 28 | 29 | ![](https://i.gyazo.com/bcc2d128064304c355d3c776d41a22a8.png) 30 | 31 | --- 32 | 33 | ## GA 対応 34 | 35 | ![](https://gyazo.com/10b9063d30ff5fb73ec2cc5a3b824333.png) 36 | 37 | --- 38 | 39 | ## Git から編集ヒストリの生成 40 | 41 | ![](https://i.gyazo.com/13034861f0afceacf47ad8d8a09f239a.png) 42 | 43 | --- 44 | 45 | ## どんなブログがほしかったか 46 | 47 | - Lighthouse で満点出したい 48 | - 普通の Markdown じゃつまんないから MDX で書きたい 49 | - サーバーの運用をしたくない 50 | - next.js の最適化に乗りたい 51 | 52 | --- 53 | 54 | ## 作った 55 | 56 | - どうせ動かないし CDN 上で静的サイト + Full AMP 57 | - MDX コンパイラを自作 (amdx) 58 | - netlify + 買ったまま忘れてたカスタムドメイン(mizchi.dev) 59 | - `pages/*.tsx` が公開される仕組みを、そのまま採用 60 | 61 | --- 62 | 63 | ## (本音 64 | 65 | - next.js で静的サイト運用の知見を貯めたい 66 | - AMP の知見を貯めたい 67 | - React でガシガシ動く作例を置く場所を、自分のドメインで用意したい 68 | 69 | --- 70 | 71 | ## next.js の SSG + AMP モードの採用 72 | 73 | ```tsx 74 | // pages/foo.tsx 75 | export const config = { amp: true }; 76 | export default function Foo() { 77 | return
foo
; 78 | } 79 | ``` 80 | 81 | - `amp: true` で常に amp を生成 82 | - AMP canonical は常に自分自身を指す(のを next.js が勝手にやってくれる) 83 | 84 | --- 85 | 86 | ## AMP の plugin を諸々突っ込む 87 | 88 | - `amp-social-share` で twitter / facebook / hatena bookmark のシェアに対応 89 | - `amp-analytics` で GoogleAnalytics 対応 90 | - rollup + preact + amp-script で、AMP 上で動的なコンポーネントが作れる。後で何かに使う 91 | 92 | --- 93 | 94 | # AMP 用 Markdown Compiler を作ろう! 95 | 96 | --- 97 | 98 | ## AMP 環境の Markdown に求められる仕様 99 | 100 | - AMP の仕様を満たす 101 | - `img` => `amp-img` かつ、 amp layout 仕様を満たす固定幅の要素に 102 | - 数式ブロック(`$$ ~ $$`) を amp-mathml に変換 103 | - コードハイライト: **ランタイムで構文解析できない** ので、コードブロックのハイライトを、コンパイル時に済ませておく必要 104 | 105 | --- 106 | 107 | ## MDX について 108 | 109 | Markdown 中で import 構文が使える仕様 110 | 111 | ``` 112 | # Hello, MDX 113 | 114 | import Doc from "./doc"; 115 | 116 | ``` 117 | 118 | Markdown ドキュメントから、別の Markdown ドキュメントや、React.Component 119 | を展開できる。 120 | シンタックスハイライターなどのエコシステムの都合から、`.mdx`拡張子をそのまま採用したい。 121 | 122 | --- 123 | 124 | ## AMDX: Accelarated MDX 125 | 126 | - [mizchi/amdx: Accelarated MDX](https://github.com/mizchi/amdx) 127 | - remark ベースで `@mdx-js/mdx` を元に拡張 (中で使ってる babel plugin はそのままなので、構文は互換) 128 | - refract(prismjs parser) で、コンパイル時にコードブロックをトークン化 129 | - +色々 (toc, frontmatter や WebWorker で動くように等) 130 | 131 | --- 132 | 133 | ## AMDXG: AMDX による静的サイト生成ツールキット 134 | 135 | - amdx-loader: amdx の webpack 用のローダー 136 | - amdxg-components: ブログ用のデフォルトコンポーネント集。使わなくてもいい 137 | - amdxg-cli: ページの雛形や各種メタデータ生成用の CLI 138 | - amdxg-boilerplate: ただのボイラープレート。(注意: まだ安定してない。頻繁に変わる) 139 | 140 | --- 141 | 142 | ## ドキュメントの解析 143 | 144 | `docs/*.mdx` の frontmatter を解析 145 | 146 | ```yaml 147 | --- 148 | title: hello 149 | created: 1589877769475 150 | tags: [react, next, amp] 151 | --- 152 | 153 | ``` 154 | 155 | - タイトルを日付順に並べた `gen/pages.json` を生成 156 | - タグで逆引きする用の `gen/tagmap.json` を生成 157 | - git log のコミットログから `gen/*.history.json` を生成 158 | 159 | --- 160 | 161 | ## Index 162 | 163 | - `gen/pages.json` からページ一覧を表示 164 | - `gen/tagmap.json` からタグ一覧を表示 165 | 166 | --- 167 | 168 | ## ドキュメントの表示 169 | 170 | - `next.config.js` の `webpack` で `.mdx` 拡張子は amdx-loader (自作) を通すようにする 171 | - next.js の danymic routing で 対応する slug の component を動的に返す 172 | 173 | ```tsx 174 | // pages/[slug].tsx 175 | export const getStaticProps: GetStaticProps = async (props) => { 176 | const slug = props.params.slug; 177 | const { default: Doc, toc, frontmatter } = await import( 178 | `../docs/${slug}.mdx` 179 | ); 180 | const { default: history } = await import(`../gen/${slug}.history.json`); 181 | return { 182 | props: { 183 | slug, 184 | toc, 185 | history, 186 | tags: frontmatter.tags || [], 187 | frontmatter: frontmatter || { title: slug, created: 0, tags: [] }, 188 | html: ReactDOMServer.renderToStaticMarkup(), 189 | } as Props, 190 | }; 191 | }; 192 | ``` 193 | 194 | 後は amdxg-components で用意したコンポーネントに突っ込んで表示 195 | 196 | --- 197 | 198 | ## タグページ 199 | 200 | - `gen/tagmap.json` から `/tags/react` のようなページを生成して表示 201 | 202 | ```tsx 203 | // pages/tags/[tag].tsx 204 | export function getStaticPaths() { 205 | const paths = Object.keys(tagmap).map((tag) => { 206 | return `/tags/${tag}`; 207 | }); 208 | return { 209 | paths, 210 | fallback: false, 211 | }; 212 | } 213 | export const getStaticProps: GetStaticProps = async (props) => { 214 | const tag = props.params.tag; 215 | return { 216 | props: { 217 | tagName: tag, 218 | pages: tagmap[tag as any], 219 | } as Props, 220 | }; 221 | }; 222 | ``` 223 | 224 | --- 225 | 226 | ## 自分の環境で遊ぶ 227 | 228 | ```bash 229 | $ npx degit mizchi/mdxx/templates/blog myblog 230 | $ cd myblog 231 | $ git init && git commit -m "Init" # 編集履歴生成に git history を使う 232 | $ edit amdxg.config.js # メタデータを編集 233 | 234 | # 書く 235 | $ npm i -g amdxg-cli 236 | $ amdxg new:page new-article # docs/new-article.mdx に記事の雛形を生成 237 | $ npx run dev # localhost:3000 でプレビューしながら記事を編集 238 | 239 | # build / deploy 240 | 241 | $ npx run build # out/ に静的サイトを生成 242 | 243 | # 要: netlify account 244 | $ npm i -g netlify-cli 245 | $ netlify deploy -d out --prod 246 | ``` 247 | 248 | --- 249 | 250 | ## デモ 251 | 252 | --- 253 | 254 | ## 今後やること 255 | 256 | - CSS が雑 257 | - amxd のプレビュー環境 (mdbuf.netlify.com ベース) 258 | - ドキュメントサイト 259 | - vercel 上で任意のバックエンド(CMS)から Incremental SSG する例を作る 260 | 261 | [Incremental Static Regeneration で実現する次世代のサーバーアーキテクチャ \- mizdev](https://mizchi.dev/202005182044-awesome-next-issg) 262 | 263 | --- 264 | 265 | ## おわり 266 | 267 | Google 検索結果からの遷移が爆速 💪 268 | 269 | (CSS が苦手なので助けて) 270 | -------------------------------------------------------------------------------- /text.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mizchi/dev/f55d43737302d408a08722c6f76af0330f7ca34c/text.png -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "skipLibCheck": true, 4 | "target": "es5", 5 | "lib": ["dom", "dom.iterable", "esnext"], 6 | "allowJs": true, 7 | "strict": false, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve" 16 | }, 17 | "exclude": ["node_modules"], 18 | "include": [ 19 | "next-env.d.ts", 20 | "**/*.ts", 21 | "**/*.tsx", 22 | "decls.d.ts", 23 | "scripts/create-item.js" 24 | ] 25 | } 26 | --------------------------------------------------------------------------------