
この記事は hacomono Advent Calendar 2025 の6日目の記事です
みゅーとんです. アドカレ 6日目です.
hacomono アドベントカレンダーでは今回 3 回のノルマがあるので頑張ります.
現在 hacomono では新規開発向けの基盤として Nuxt Layer を用いて, 様々な nuxt やら 言語設定やらを共通化する動きをしています.その一端を紹介できればと思います.
はじめに
Nuxt Layer とは ?
公式ガイドはこちら
ざっくりいえば Nuxt の既存のプロジェクトを継承・拡張したプロジェクトを作る機能です.
Nuxt のベースプロジェクトに実装してある, components, composables 等を引き継いでプロジェクトを作ることができます.
defineNuxtConfig({ extends: [ /* ここにNuxtのベースプロジェクトを設定 */ ] })
また, 公式より本機能のユースケースは以下の通りと説明されています.
- 再利用可能な設定プリセット (
nuxt.configapp.config) をプロジェクト間で共有 app/components配下の共通化によるコンポーネントライブラリの作成app/composablesapp/utils配下の共通化によるユーティリティ・コンポーザブルライブラリの作成- Nuxt Module のプリセットの用意
- プロジェクト間での標準設定の共有
- Nuxt のテーマ作成
- モジュラーアーキテクチャを実装することでコード構成を強化し、大規模プロジェクトにおけるドメイン駆動開発パターンをサポートする
Nuxt Layer を使うモジュラーアーキテクチャに関しては, 外部のブログ記事等で紹介されています.
Nuxt Layer 使ってみたサンプルをいくつか紹介
モジュラーアーキテクチャに関しては言わずもがなではありますが, それ以外の細かいユースケースについて, いくつか実際に試したものを紹介します
tsconfig の厳密化 & 共通化
たとえば typescript の設定をベースに用意してみます. ここでは, 超厳密な型ルールを定義したく, @tsconfig/strictest をベースに tsconfig を作っています.
// base/nuxt.config.ts import config from '@tsconfig/strictest/tsconfig.json' assert { type: 'json' } import defu from 'defu' const fixedTsConfig = defu( { compilerOptions: { // これだけ vue 向けではないため無効化する. exactOptionalPropertyTypes: false, }, }, tsConfig, ) export default defineNuxtConfig({ typescript: { tsConfig: fixedTsConfig, } })
// project/nuxt.config.ts export default defineNuxtConfig({ extends: ['../base'] })
この構成で project 側の .nuxt 配下の tsconfig を見てみると以下の通り生成されています.
{ "compilerOptions": { // ... "esModuleInterop": true, "skipLibCheck": true, "target": "ESNext", "allowJs": true, "resolveJsonModule": true, "moduleDetection": "force", "isolatedModules": true, "verbatimModuleSyntax": true, "strict": true, "noUncheckedIndexedAccess": true, "forceConsistentCasingInFileNames": true, "noImplicitOverride": true, "module": "preserve", "noEmit": true, "lib": [ "ESNext", "dom", "dom.iterable", "webworker" ], "jsx": "preserve", "jsxImportSource": "vue", "types": [], "moduleResolution": "Bundler", "useDefineForClassFields": true, "noImplicitThis": true, "allowSyntheticDefaultImports": true, "allowUnusedLabels": false, "allowUnreachableCode": false, "exactOptionalPropertyTypes": false, "noFallthroughCasesInSwitch": true, "noImplicitReturns": true, "noPropertyAccessFromIndexSignature": true, "noUnusedLocals": true, "noUnusedParameters": true } }
ちなみに, tsconfig の設定なしと比較すると以下の通りで, @tsconfig/strictest 由来の設定がしっかり反映されているのがわかります.
{
"compilerOptions": {
// ...
"esModuleInterop": true,
"skipLibCheck": true,
"target": "ESNext",
"allowJs": true,
"resolveJsonModule": true,
"moduleDetection": "force3",
"isolatedModules": true,
"verbatimModuleSyntax": true,
"strict": true,
"noUncheckedIndexedAccess": true,
"forceConsistentCasingInFileNames": true,
"noImplicitOverride": true,
"module": "preserve",
"noEmit": true,
"lib": [
"ESNext",
"dom",
"dom.iterable",
"webworker"
],
"jsx": "preserve",
"jsxImportSource": "vue",
"types": [],
"moduleResolution": "Bundler",
"useDefineForClassFields": true,
"noImplicitThis": true,
"allowSyntheticDefaultImports": true
+ "allowUnusedLabels": false,
+ "allowUnreachableCode": false,
+ "exactOptionalPropertyTypes": false,
+ "noFallthroughCasesInSwitch": true,
+ "noImplicitReturns": true,
+ "noPropertyAccessFromIndexSignature": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true
},
}
このような変更を加えるのは Nuxt Module でもおそらく可能ですが, nuxt config の設定を継承する方針のほうが, 直感的でしょう.
似たようなユースケースで, @nuxt/eslint などのツールチェーン設定の共通化にも役立てそうです.
TailwindCSS 設定の共通化
デザインシステムやレイアウトシステムを共通で定義したい時, TailwindCSS を利用する場合が多いと思います.
ここでの想定は, 共通コンポーネントと, そのスタイリングに TailwindCSS (v4) を使用している場合として, ベースプロジェクトは以下のような構成になっているとします.
/base-project ├ /app │ ├ /assets │ │ ├ /css/tailwind.css .. tailwindcss のエントリーポイント │ ├ /components .. 共通コンポーネント ├ nuxt.config.ts
NuxtLayer でも 共通のコンポーネントを用意することはできますが, TailwindCSS (v4) の場合は以下の問題がありました.
- /assets 配下は Nuxt Layer の仕組みでは継承対象にならない
- TailwindCSS v4 からは css first configuration がメインになっており, そもそもコンフィグ用の js を極力作るべきではない
@import "tailwindcss"のような設定をデフォルトで記載するが, これはプロジェクトのルート配下のソースコードに対して静的解析をする方式になる. node_modules 配下は対象にならない
つまるところ, ベースプロジェクトの TailwindCSS の設定は継承されず, npm パッケージとして公開されている場合はさらに, TailwindCSS の解析対象にならない, という問題があります. とはいえ npm パッケージ化しておいたほうが, バージョンでの管理の容易性など, なにかと利用時に便利であるため, うちでは避けて通れない問題になっていました.
まず理想として TailwindCSS のエントリーポイントとなる css ファイルがどうあるべきかを先に提示します.
// (current-project)/app/assets/css/tailwind.css // 静的解析対象のパスを明示的に指定するため, source(none) とする @import "tailwindcss" source(none); // メインプロジェクトへのパス @source "/Users/mewton/Workspace/current-project/app"; // ベースプロジェクトのパス @source "/Users/mewton/Workspace/current-project/node_modules/base-project/app"; // ここから先は @theme などの他の設定が記載される
対応すべき方針は以下の通り
- TailwindCSS が静的解析の対象とするパスを動的に,
@sourceとして指定する. - 動的に生成した css ファイルを nuxt の設定に組み込む.
これに対応すべく, ベースプロジェクトでの nuxt.config.ts は以下の通りにしています. 動的な処理に関してはどうしても Nuxt Layer ではなく Nuxt Module を使用する必要がありますね.
// base-project/nuxt.config.ts import { readFile } from 'node:fs/promises' import { dirname, resolve } from 'node:path' import { fileURLToPath } from 'node:url' import { addTemplate, defineNuxtModule } from '@nuxt/kit' import tailwindcss from '@tailwindcss/vite' import { defineNuxtConfig } from 'nuxt/config' const currentDir = dirname(fileURLToPath(import.meta.url)) /** * import を抽出し、元のコードから削除する */ function extractImports(css: string): { result: string importedModules: string[] } { /** * `@import "tailwindcss" source(none);`, `@import "tw-animate-css";` などの import を抽出する */ const importRegex = /@import\s+"([^"]+)"(?:\s*.*)?;/g const matches = css.match(importRegex) if (matches) { const importedModules = matches.map((match) => match.replace('@import "', '').replace('"', '').replace(';', ''), ) return { result: css.replace(importRegex, ''), importedModules } } return { result: css, importedModules: [] } } /** * tailwindcss の source を動的に設定し, 各種 layer と app を tailwind が認識可能にするための CSS を生成する */ const tailwindCustomModule = defineNuxtModule(async (_opt, nuxt) => { const baseStyle = await readFile( `${currentDir}/app/assets/css/tailwind.css`, 'utf-8', ) const { result: fixedBaseStyle, importedModules } = extractImports(baseStyle) /** * `@import "tailwindcss"` 以外の `@import` 構文 */ const tailwindAdditionalModules = importedModules.filter( (module) => module !== 'tailwindcss', ) /** * 本体含む各種 layer の app のパス */ const allLayersAppDir: string[] = nuxt.options._layers.map( ({ config }) => config.srcDir, ) /** * .nuxt 配下に css を動的生成 */ const cssAsset = addTemplate({ filename: 'layers/base-project-name/tailwind.css', getContents: () => ` @import "tailwindcss" source(none); ${tailwindAdditionalModules.map((module) => `@import "${module}";`).join('\n')} ${allLayersAppDir.map((source) => `@source "${source}";`).join('\n')} ${fixedBaseStyle}`, write: true, }) /** * nuxt の設定に, 動的生成された css を追加する */ nuxt.options.css.unshift(cssAsset.dst) }) export default defineNuxtConfig({ vite: { plugins: [tailwindcss()], }, modules: [tailwindCustomModule], })
利用側は tailwind に関する設定を意識することなく, 共通設定による class を使えるようになります. デザインシステムやレイアウトシステムなど, 共通のトンマナを揃えたい場合に, かなり便利ですね.
まとめ
Nuxt Layer を使ってフロントエンドの設定共通化しているサンプルをいくつか紹介しました.
もっと詰めれば, Nuxt のプロジェクトの立ち上げを爆速にできそうで, 夢が広がる感じがします.
まだ試せてないですが, sentry の設定共通化や, biome などの静的解析ツールの共通化など, 様々な手段がとれそうですね.