マークアップエンジニアが考えたNode.jsのFrontend Starter環境

2023年4月27日

以前、マークアップエンジニアが考える最低限のwebpack構成という記事を書きましたが、それから約1.5年。

当時は「もうWebpack環境のテンプレートあれば、しばらく環境構築しなくても大丈夫やろ」と思ってました。

そんな訳なかった。甘かった。。。

ViteやらTurbopackやら新しいビルドツールがドンドン流通してきて、もはやWebpackは古いみたいな風潮が生まれ始めているし、Webpackを利用してHTML/CSS/JS/画像を出力するためのライブラリが全然更新されなくて脆弱性でエラー出まくりになってきているし…

もういっそNode.jsで機能毎のJSを自分で作ったほうが早くて楽じゃね!?…

となり、作ってみましたのでご紹介。

機能概要

  • ejs -> HTML
  • scss -> CSS
  • js(es6) -> js(es5)
  • image -> image.min ※pngとsvgのみ対応
  • svg(icon) -> webfont
  • ローカルサーバー
  • その他(ウォッチ/フォルダ削除/フォルダ作成)

環境

  • MacBook Pro M1チップ (Winは未検証)
  • Node.js 16.x / 18.x

フォルダ構成

root
├── npm-scripts
│ ├── compile-ejs.js         ・・・ejs -> HTML
│ ├── compile-scss.js        ・・・scss -> css
│ ├── compile-js.js          ・・・js(es6) -> js(es5)
│ ├── compile-images.js      ・・・image -> image.min
│ ├── compile-icon-font.js   ・・・svg -> webfont
│ ├── server.js              ・・・ローカルサーバー
│ ├── watch.js               ・・・ウォッチ
│ ├── create-directory.js    ・・・フォルダ作成
│ └── delete.js              ・・・フォルダ削除
└── src
    ├── ejs
    ├── icon-font
    ├── images
    ├── scripts
    └── scss

各ファイル

package構成

"devDependencies": {
 "@rollup/plugin-terser": "^0.4.1",
 "autoprefixer": "^10.4.14",
 "browser-sync": "^2.29.1",
 "chokidar": "^3.5.3",
 "clean-css": "^5.3.2",
 "cross-env": "^7.0.3",
 "crypto": "^1.0.1",
 "dotenv": "^16.0.3",
 "ejs": "^3.1.9",
 "eslint": "^8.38.0",
 "eslint-config-prettier": "^8.8.0",
 "glob": "^10.2.1",
 "html-minifier": "^4.0.0",
 "htmlhint": "^1.1.4",
 "imagemin": "^8.0.1",
 "imagemin-svgo": "^10.0.1",
 "imagemin-upng": "^4.0.0",
 "js-beautify": "^1.14.7",
 "lint-staged": "^13.2.1",
 "lodash": "^4.17.21",
 "npm-run-all": "^4.1.5",
 "nunjucks": "^3.2.4",
 "postcss": "^8.4.23",
 "postcss-scss": "^4.0.6",
 "prettier": "^2.8.7",
 "rollup": "^3.20.6",
 "sass": "^1.62.0",
 "stylelint": "^15.5.0",
 "stylelint-config-recess-order": "^4.0.0",
 "stylelint-config-standard-scss": "^8.0.0",
 "stylelint-declaration-block-no-ignored-properties": "^2.7.0",
 "svg2ttf": "^6.0.3",
 "svgicons2svgfont": "^12.0.0",
 "ttf2eot": "^3.1.0",
 "ttf2woff": "^3.0.0",
 "ttf2woff2": "^5.0.0"
},
"dependencies": {
 "normalize.css": "^8.0.1"
}

ejs -> HTML

import path from 'path';
import fs from 'fs';
import { glob } from 'glob';
import ejs from 'ejs';
import { minify } from 'html-minifier';
import jsBeautify from 'js-beautify';
import { ensureDirectoryExistence } from './create-directory.js';
import dotenv from 'dotenv';
dotenv.config({ path: `.env.${process.env.NODE_ENV === 'production' ? 'production' : 'development'}` });

// envから値を取得
const isMinify = JSON.parse(process.env.MINIFY);
const dist = process.env.DIST;
const isHTMLDir = JSON.parse(process.env.IS_HTML_DIR); // dist/配下にHTMLフォルダを作成するかどうか
const argTargetFile = process.env.TARGET_FILE;

// 設定
const config = {
 dir: {
  ejs: 'src/ejs/', // EJSファイルのディレクトリ
 },
 ejsOptions: {
  root: `${path.resolve(process.cwd(), 'src/ejs/')}`,
 },
 ejsData: {
  path: {
   comp: '/components', // コンポーネントのパス(src/ejs/をrootとしたルート絶対パス)
  },
 },
};

// 出力先のHTMLファイルを削除
if (process.env.NODE_ENV !== 'production' && !argTargetFile) {
 glob.sync(`${dist}/html/**/*.html`).forEach((file) => {
  fs.unlinkSync(file);
 });
}

// EJSファイルをコンパイルする関数
const compileTemplate = (templatePath, data, options) => {
 const template = fs.readFileSync(templatePath, 'utf8');
 const compiledTemplate = ejs.render(template, data, options);

 // スペースやインデント起因の表示バグ等を回避のためフォーマットする
 const jsBeautifyOption = JSON.parse(fs.readFileSync('.ejsbrc.json', 'utf8'));
 const formatedTemplate = jsBeautify.html(compiledTemplate, jsBeautifyOption);

 // minify判定
 if (isMinify) {
  return minify(formatedTemplate, {
   collapseWhitespace: true,
   removeComments: true,
   minifyCSS: true,
   minifyJS: true,
   minifyURLs: true,
   decodeEntities: true,
   removeRedundantAttributes: true,
  });
 } else {
  return formatedTemplate;
 }
};

// 引数があれば引数のファイルをコンパイルする、なければ全てのファイルをコンパイルする
const files = argTargetFile ? new Array(argTargetFile) : glob.sync(`${config.dir.ejs}/**/!(_)*.ejs`);
files.forEach((file) => {
 // EJSファイルのrootへの相対パスを設定する
 config.ejsData.path.static = path.relative(file, config.dir.ejs);

 // EJSファイルをコンパイルする
 const compiledTemplate = compileTemplate(file, config.ejsData, config.ejsOptions);

 // コンパイルされたHTMLを出力する
 const HTMLFolder = isHTMLDir ? `${dist}/html/` : `${dist}/`;
 const distPath = file.replace(config.dir.ejs, HTMLFolder).replace('.ejs', '.html');
 ensureDirectoryExistence(path.dirname(distPath));
 fs.writeFileSync(distPath, compiledTemplate);
 console.log(`\x1b[36;1m${file} -> ${distPath.replace('./', '')} ...\x1b[0m`);
});

scss -> CSS

import path from 'path';
import fs from 'fs';
import { glob } from 'glob';
import sass from 'sass';
import postcss from 'postcss';
import autoprefixer from 'autoprefixer';
import { ensureDirectoryExistence } from './create-directory.js';
import dotenv from 'dotenv';
dotenv.config({ path: `.env.${process.env.NODE_ENV === 'production' ? 'production' : 'development'}` });

// envから値を取得
const isMinify = JSON.parse(process.env.MINIFY);
const dist = process.env.DIST;
const argTargetFile = process.env.TARGET_FILE;

// 設定
const config = {
 dir: {
  scss: 'src/scss/', // SCSSファイルのディレクトリ
  dist: `${dist}/css/`,
 },
};

// 出力先のCSSファイルを削除
if (process.env.NODE_ENV !== 'production' && !argTargetFile) {
 glob.sync(`${config.dir.dist}/**/*.css`).forEach((file) => {
  fs.unlinkSync(file);
 });
}

// SCSSファイルをコンパイルする関数
const compileScss = (scssFilePath) => {
 // CSSソースをresultに格納
 const result = sass.compile(scssFilePath, {
  style: isMinify ? 'compressed' : 'expanded',
  loadPaths: ['./src/scss/'],
 });

 return postcss([autoprefixer]).process(result.css.toString()).css;
};

// SCSSファイルを取得してコンパイル実行からファイル書き出し
const scssFiles = argTargetFile ? new Array(argTargetFile) : glob.sync(`${config.dir.scss}/**/!(_)*.scss`);
scssFiles.forEach((file) => {
 const cssFilePath = file.replace(config.dir.scss, config.dir.dist).replace('.scss', '.css');

 ensureDirectoryExistence(path.dirname(cssFilePath));
 const css = compileScss(file);
 fs.writeFileSync(cssFilePath, css);
 console.log(`\x1b[36;1m${file} -> ${cssFilePath} ...\x1b[0m`);
});

js(es6) -> js(es5)

import fs from 'fs';
import { glob } from 'glob';
import terser from '@rollup/plugin-terser';
import dotenv from 'dotenv';
dotenv.config({ path: `.env.${process.env.NODE_ENV === 'production' ? 'production' : 'development'}` });

// envから値を取得
const isMinify = JSON.parse(process.env.MINIFY);
const dist = process.env.DIST;
const argTargetFile = process.env.TARGET_FILE;

// rollupの設定
const setting = (name) => {
 return {
  input: name,
  output: {
   file: name.replace(`src/scripts`, `${dist}/scripts`),
   format: 'iife',
  },
  plugins: [isMinify ? terser({}) : null],
 };
};

// 出力先のJSファイルを削除
if (process.env.NODE_ENV !== 'production' && !argTargetFile) {
 glob.sync(`${dist}/scripts/**/*.js`).forEach((file) => {
  fs.unlinkSync(file);
 });
}
const files = argTargetFile ? new Array(argTargetFile) : glob.sync(`src/scripts/**/!(_)*.js`);
const settings = files.map((file) => setting(file));
export default settings;

image -> image.min ※pngとsvgのみ対応

import path from 'path';
import fs from 'fs';
import { glob } from 'glob';
import imagemin from 'imagemin';
import imageminUpng from 'imagemin-upng';
import imageminSvgo from 'imagemin-svgo';
import dotenv from 'dotenv';
dotenv.config({ path: `.env.${process.env.NODE_ENV === 'production' ? 'production' : 'development'}` });

// envから値を取得
const dist = process.env.DIST;
const argTargetFile = process.env.TARGET_FILE;

// 設定
const config = {
	dir: {
		src: `src/images/`, // 画像ファイルのディレクトリ
		dist: `${dist}/images/`, // 出力先のディレクトリ
	},
	quality: {
		png: 256, // 0 or 256 = lossless, 1-255 = lossy
	},
};

// pngかsvgかを判定
const isValidFile = (file) => {
	const extension = path.basename(file).split('.').pop().toLowerCase();
	if (extension !== 'png' && extension !== 'svg') {
		return false;
	}
	return true;
};

const compressImages = async () => {
	if (!argTargetFile) await fs.promises.rm(config.dir.dist, { recursive: true, force: true });
	const pngFiles = glob.sync(`${config.dir.src}/**/*.png`);
	const svgFiles = glob.sync(`${config.dir.src}/**/*.svg`);

	const allFiles = argTargetFile ? [argTargetFile] : [...pngFiles, ...svgFiles];

	for (const file of allFiles) {
		const fileName = file.replace(config.dir.src, '');
		const buffer = await imagemin([file], {
			destination: path.join(config.dir.dist, path.dirname(fileName)),
			plugins: [
				imageminUpng({
					quality: config.quality.png,
				}),
				imageminSvgo(),
			],
		});
		console.log(`\x1b[36;1mminify ${file} -> ${buffer[0].destinationPath}\x1b[0m`);
		fs.writeFileSync(buffer[0].destinationPath, buffer[0].data);
	}
};

// src/imagesフォルダの存在していればcompressImages()を実行
try {
	fs.accessSync(config.dir.src);
	// pngかsvgの対象ファイルが指定されているか、対象ファイルの指定がなければ実行
	if ((argTargetFile && isValidFile(argTargetFile)) || !argTargetFile) compressImages();
} catch (error) {
	console.log('\x1b[31;1mNo images directory\x1b[0m');
}

svg(icon) -> webfont

import path from 'path';
import fs from 'fs';
import { glob } from 'glob';
import _ from 'lodash';
import svgicons2svgfont from 'svgicons2svgfont';
import svg2ttf from 'svg2ttf';
import ttf2eot from 'ttf2eot';
import ttf2woff from 'ttf2woff';
import ttf2woff2 from 'ttf2woff2';
import nunjucks from 'nunjucks';
import crypto from 'crypto';
import prettier from 'prettier';
import { ensureDirectoryExistence } from './create-directory.js';
import dotenv from 'dotenv';
dotenv.config({ path: `.env.${process.env.NODE_ENV === 'production' ? 'production' : 'development'}` });

// envから値を取得
const dist = process.env.DIST;

// ファイル読み込み
const normalizeCss = fs.readFileSync('./node_modules/normalize.css/normalize.css').toString();
const prettierConfig = JSON.parse(fs.readFileSync('./.prettierrc', 'utf8'));

// 設定
const config = {
 fontName: 'my-icon',
 fontClassName: 'my-icon',
 cssFontPath: '../icon-font/', // cssフォルダからフォントフォルダまでの相対パス
 startUnicode: 0xf000, // nullにした場合は0xf000から始まる
 formats: ['ttf', 'eot', 'woff', 'woff2'], // 生成するフォントファイルの形式['ttf', 'eot', 'woff', 'woff2']
 template: {
  scss: `src/icon-font/templates/_icon-scss.njk`,
  html: `src/icon-font/templates/_icon-html.njk`,
 },
 output: {
  scss: `src/scss/icon-font/_icon-font.scss`,
  html: `${dist}/icon-font/index.html`,
 },
 dir: {
  svg: `src/icon-font/svg`,
  distFont: `${dist}/icon-font/`,
 },
 isHTML: process.env.NODE_ENV !== 'production', // 一覧のHTMLを生成するかどうか
 prependUnicode: false, // svgファイル名の先頭にUnicodeを付与するかどうか
 addHashInFontUrl: false, // フォントファイルのURLにハッシュを付与するかどうか
};

// SVGファイルのパスを取得する関数
const getSvgFiles = () => {
 const files = glob.sync(`${config.dir.svg}/**/*.svg`);
 return _.sortBy(files, (file) => {
  // ファイル名の先頭にUnicodeが付いている場合はUnicodeを返す
  const match = file.match(/\/u([a-fA-F0-9]{4})-/);
  if (match) {
   return parseInt(match[1], 16);
  }
  // Unicodeが付いていない場合はファイル名をそのまま返す
  return file;
 });
};

// ファイルを生成する関数
const generateFiles = async () => {
 const env = nunjucks.configure({ autoescape: false });
 const fontStream = new svgicons2svgfont({
  fontName: config.fontName,
  fontHeight: 1000,
  normalize: true,
 });

 let fontData = {
  svg: '',
  eot: '',
  ttf: '',
  woff: '',
  woff2: '',
 };
 let lastUnicode = config.startUnicode || 0xf000;

 fontStream.on('data', (data) => {
  fontData.svg += data;
 });
 fontStream.on('end', () => {
  // SVGファイルからTTFファイルを生成
  const ttf = svg2ttf(fontData.svg, {});
  if (config.formats.includes('ttf')) fontData.ttf = Buffer.from(ttf.buffer);

  // TTFファイルからEOTファイルを生成
  if (config.formats.includes('eot')) fontData.eot = Buffer.from(ttf2eot(ttf.buffer));

  // TTFファイルからWOFFファイルを生成
  if (config.formats.includes('woff')) fontData.woff = Buffer.from(ttf2woff(ttf.buffer));

  // TTFファイルからWOFF2ファイルを生成
  if (config.formats.includes('woff2')) fontData.woff2 = Buffer.from(ttf2woff2(ttf.buffer));

  const createFileOptions = {
   hash: config.addHashInFontUrl ? crypto.createHash('md5').update(fontData.svg).digest('hex') : '',
   fontName: config.fontName,
   fontPath: config.cssFontPath,
   className: config.fontClassName,
   glyphs: fontStream.glyphs,
   formats: config.formats,
   resetCSS: normalizeCss,
  };
  // SCSSを生成
  const scssContent = env.render(config.template.scss, createFileOptions);
  const formattedScssContent = prettier.format(scssContent, { parser: 'scss', ...prettierConfig });

  // HTMLを生成
  const htmlContent = env.render(config.template.html, createFileOptions);

  // フォントファイルを保存
  ensureDirectoryExistence(config.dir.distFont);
  if (config.formats.includes('ttf')) fs.writeFileSync(path.join(config.dir.distFont, `${config.fontName}.ttf`), fontData.ttf);
  if (config.formats.includes('eot')) fs.writeFileSync(path.join(config.dir.distFont, `${config.fontName}.eot`), fontData.eot);
  if (config.formats.includes('woff')) fs.writeFileSync(path.join(config.dir.distFont, `${config.fontName}.woff`), fontData.woff);
  if (config.formats.includes('woff2')) fs.writeFileSync(path.join(config.dir.distFont, `${config.fontName}.woff2`), fontData.woff2);
  fs.writeFileSync(config.output.scss, formattedScssContent);
  if (config.isHTML) fs.writeFileSync(config.output.html, htmlContent);

  console.log(`\x1b[36;1mWebfont generated for ${config.fontName}.\x1b[0m`);
 });

 // SVGファイルをストリームに流し込む
 getSvgFiles().forEach((file, idx) => {
  const glyph = fs.createReadStream(file);
  const basename = path.basename(file);
  const matches = basename.match(/^(?:((?:u[0-9a-f]{4,6},?)+)\-)?(.+)\.svg$/i);
  if (matches && matches[1]) {
   lastUnicode = parseInt(matches[1].replace(/u/g, ''), 16);
  } else {
   idx === 0 ? lastUnicode : lastUnicode++;
   if (config.prependUnicode) {
    const outputPath = path.join(config.dir.svg, `u${String.fromCharCode(lastUnicode).codePointAt(0).toString(16).toUpperCase()}-${basename}`);
    fs.renameSync(file, outputPath, (err) => {
     console.log(err);
    });
    glyph.path = outputPath;
   }
  }
  glyph.metadata = {
   name: basename
    .replace(/(u[\dA-F]{4}-)/g, '')
    .replace(/\.svg$/i, '')
    .replace(/\s+/g, '-'),
   unicode: [String.fromCharCode(lastUnicode)],
  };
  fontStream.write(glyph);
 });

 fontStream.end();
};

generateFiles();

ローカルサーバー

import browserSync from 'browser-sync';
import chokidar from 'chokidar';
import http from 'http';
import path from 'path';
import fs from 'fs';
import dotenv from 'dotenv';
dotenv.config({ path: `.env.${process.env.NODE_ENV === 'production' ? 'production' : 'development'}` });

// envから値を取得
const dist = process.env.DIST;

// 設定
const config = {
 port: 8282,
 bsPort: 3000,
};

// コンテンツタイプの取得
const getContentType = (filePath) => {
 const extname = path.extname(filePath);
 switch (extname) {
  case '.html':
   return 'text/html';
  case '.js':
   return 'text/javascript';
  case '.css':
   return 'text/css';
  case '.json':
   return 'application/json';
  case '.png':
   return 'image/png';
  case '.jpg':
   return 'image/jpg';
  case '.svg':
   return 'image/svg+xml';
  case '.wav':
   return 'audio/wav';
  default:
   return 'text/plain';
 }
};

// サーバーの設定
const server = http.createServer((req, res) => {
 const filePath = path.join(dist, req.url);

 fs.stat(filePath, (err, stats) => {
  if (err) {
   if (err.code === 'ENOENT') {
    res.statusCode = 404;
    res.end(`File ${req.url} not found!`);
   } else {
    res.statusCode = 500;
    res.end(`Internal server error: ${err.code}`);
   }
  } else {
   if (stats.isDirectory()) {
    fs.readdir(filePath, (err, files) => {
     if (err) {
      res.statusCode = 500;
      res.end(`Internal server error: ${err.code}`);
     } else {
      const url = req.url.endsWith('/') ? req.url : `${req.url}/`;
      res.writeHead(200, { 'Content-Type': 'text/html' });
      res.write(`<body>`);
      res.write(`<h1>Folder: ${req.url}</h1>`);
      res.write('<ul>');
      files.forEach((file) => {
       res.write(`<li><a href="${url}${file}">${file}</a></li>`);
      });
      res.write('</ul>');
      res.write(`</body>`);
      res.end();
     }
    });
   } else {
    const stream = fs.createReadStream(filePath);
    stream.on('open', () => {
     res.setHeader('Content-Type', getContentType(filePath));
     stream.pipe(res);
    });
    stream.on('error', (err) => {
     res.setHeader('Content-Type', 'text/html');
     res.statusCode = 500;
     res.end(`Internal server error: ${err.code}`);
    });
   }
  }
 });
});

// ポートが既に使われている場合は、他のポートを自動で設定
server.on('error', (err) => {
 if (err.code === 'EADDRINUSE') {
  console.log(`Port ${config.bsPort} is already in use. Trying another port...`);
  config.bsPort++;
  server.listen(config.bsPort);
 }
});

// braowser-syncのインスタンス作成
const bs = browserSync.create();

// サーバーの起動
server.listen(config.bsPort, () => {
 console.log(`Server running at http://localhost:${config.bsPort}`);
 // braowser-syncの起動
 bs.init({
  proxy: `http://localhost:${config.bsPort}`,
  port: config.port,
  open: true,
  ui: false,
 });
});

// ファイルの変更を検知してブラウザをリロードする
chokidar.watch(`${dist}/**/*`).on('all', () => {
 bs.reload();
});

ウォッチ

import path from 'path';
import fs from 'fs/promises';
import chokidar from 'chokidar';
import { exec } from 'child_process';
import dotenv from 'dotenv';
dotenv.config({ path: `.env.${process.env.NODE_ENV === 'production' ? 'production' : 'development'}` });

// envから値を取得
const dist = process.env.DIST;
const isHTMLDir = JSON.parse(process.env.IS_HTML_DIR); // dist/配下にHTMLフォルダを作成するかどうか

// 監視対象のフォルダとファイルを指定
const targets = 'src/**/*';

// chokidarの設定
const watcher = chokidar.watch(targets, {
 ignored: /(^|[\\/])\../, // ignore dotfiles
 persistent: true,
});

const remove = async (type, filePath) => {
 let targetPath = filePath.replace('src', dist);

 // distの拡張子に変換
 if (type === 'scss') {
  targetPath = targetPath.replace('/scss/', '/css/').replace('.scss', '.css');
 } else if (type === 'ejs') {
  const htmlFolder = isHTMLDir ? '/html/' : '/';
  targetPath = targetPath.replace('/ejs/', htmlFolder).replace('.ejs', '.html');
 }

 // distに対象のファイル/フォルダがあれば削除
 try {
  const stats = await fs.stat(targetPath);
  if (stats.isDirectory()) {
   await fs.rm(targetPath, { recursive: true, force: true });
  } else if (stats.isFile()) {
   await fs.unlink(targetPath);
  }
 } catch (error) {
  if (error.code !== 'ENOENT') {
   throw error;
  }
 }
};

const action = (type, filePath) => {
 // filePathがある場合はファイル単体で起動
 exec(`${filePath ? `cross-env TARGET_FILE="${filePath}" ` : ''}npm run build:${type}`, (err, stdout, stderr) => {
  if (err) {
   console.error(err);
   return;
  }
  // jsファイル(rollup)の場合は結果がstderrに出力されるので
  if (stdout && type !== 'js') {
   console.error(stdout);
   return;
  }
  if (stderr) {
   console.error(stderr);
   return;
  }
 });
};

// 引数で受け取ったeventに応じて処理を分ける
const main = (event, filePath) => {
 // ターミナルのカラー設定
 const color = event === 'add' ? '\x1b[32;1m' : event === 'change' ? '\x1b[36;1m' : '\x1b[31;1m';
 let type = filePath.split('.').pop();

 /* 
  ** partialファイルの場合は
  ** 全ファイルを更新するためのフラグ設定
  **
  ** partialファイル以外は単体で更新する
  */
 const fileName = path.basename(filePath);
 let isAll = false;

 // フォルダパスで種別を判別
 if (filePath.includes('/scss/')) {
  type = 'scss';
  isAll = fileName.startsWith('_'); // partialファイル判定
 } else if (filePath.includes('/ejs/')) {
  type = 'ejs';
  isAll = fileName.startsWith('_'); // partialファイル判定
 } else if (filePath.includes('/scripts/')) {
  type = 'js';
  isAll = fileName.startsWith('_'); // partialファイル判定
 } else if (filePath.includes('/images/')) {
  type = 'image';
 } else if (filePath.includes('/icon-font/svg/')) {
  type = 'icon';
 }

 if (event === 'unlink' || event === 'unlinkDir') {
  remove(type, filePath).catch(console.error);
  console.log(`${color}${filePath} has been ${event}\x1b[0m`);

  // iconの場合は削除後に再コンパイルが必要
  if (type === 'icon') {
   action(type);
  }
 } else {
  if (isAll) {
   action(type);
  } else {
   action(type, filePath);
  }
 }
};

watcher.on('ready', () => {
 watcher.on('add', (filePath) => main('add', filePath));
});

// cmd + s 等を連打すると、changeイベントが複数回発火してしまうので、タイマーで制御
let timer;
watcher.on('change', (filePath) => {
 clearTimeout(timer);
 timer = setTimeout(() => main('change', filePath), 500);
});
watcher.on('unlink', (filePath) => main('unlink', filePath));
watcher.on('unlinkDir', (filePath) => main('unlinkDir', filePath));

フォルダ作成

import path from 'path';
import fs from 'fs';

export const ensureDirectoryExistence = (filePath) => {
 const dirname = path.dirname(filePath);
 if (fs.existsSync(filePath)) {
  return true;
 }
 ensureDirectoryExistence(dirname);
 fs.mkdirSync(filePath);
};

フォルダ削除

import fs from 'fs';
import dotenv from 'dotenv';
dotenv.config({ path: `.env.${process.env.NODE_ENV === 'production' ? 'production' : 'development'}` });

// envから値を取得
const dist = process.env.DIST;
fs.promises.rm(dist, { recursive: true, force: true });

使い方

  1. 以下をクローン or ダウンロード
  2. npm ci
  3. .env.development/.env.productionの設定内容を修正
    • .env.development  ・・・npm run start時に適用
    • .env.production     ・・・npm run build時に適用
    • 設定内容
      • MINIFY               ・・・出力ファイルを圧縮するか(Boolean)
      • DIST                  ・・・出力ファイルを出力するフォルダ名(String)
      • IS_HTML_DIR     ・・・出力するフォルダの中にHTMLフォルダを作成するか(Boolean)
  4. npm run startでマークアップ作業開始

メモ

  • ejs
    • ファイル名の先頭にアンダースコア( _ )があるejsファイルは出力されない
    • ejsファイル内で画像パスを指定する際は、フォルダ階層関係なく${path.static}/imageでアクセスが可能
      • JSファイルやCSSファイルも同様に${path.static}/**の形でアクセス可能
    • ejsのincludeにはフォルダ階層関係なく${path.comp}でsrc/ejs/components/へのアクセスが可能
  • scss
    • ファイル名の先頭にアンダースコア( _ )があるscssファイルは出力されない
    • rootパスにsrc/scss/が設定されているので、@useや@forwordを使用する時のパス指定は “components/" の形で指定が可能
      • =階層ごとに相対パスを書かなくて済む
  • webfont
    • 細かい設定は npm-scripts/compile-icon-font.js の中で指定しているので変更したい場合は参照
    • 以下は初期設定内容
      • src/icon-font/svg/内のsvg画像をwebfontに変換する
      • scssファイルと一覧のHTMLファイルを出力する
        • scss   ・・・src/scss/icon-font/_icon-font.scss
        • HTML ・・・${dist}/icon-font/index.html
      • 出力ファイル用のテンプレートは以下フォルダに格納
        • src/icon-font/templates/
      • 出力されるwebfontのフォーマットは以下
        • 'ttf’, 'eot’, 'woff’, 'woff2’
  • lint
    • npm run lintでsrc配下のejs/js/scssのリントが可能
    • npm run lint:**の指定で各拡張子ごとのリントが可能
      • npm run lint:html  ・・・ejsファイル
      • npm run lint:js      ・・・jsファイル
      • npm run lint:css    ・・・scssファイル

 

本日の備忘録は以上!

Node.js

Posted by takahiro