diff --git a/.editorconfig b/.editorconfig index 7e66cb8..6726639 100644 --- a/.editorconfig +++ b/.editorconfig @@ -8,5 +8,5 @@ insert_final_newline = false trim_trailing_whitespace = true charset = utf-8 -[*.js] -insert_final_newline = true +[*.{js,md}] +insert_final_newline = true \ No newline at end of file diff --git a/.eleventy.js b/.eleventy.js deleted file mode 100644 index 0195bc3..0000000 --- a/.eleventy.js +++ /dev/null @@ -1,18 +0,0 @@ -// @ts-check -const getGitCommitDateFromPath = require("./src/getGitCommitDateFromPath"); -const getCollectionNewestGitCommitDate = require("./src/getCollectionNewestGitCommitDate"); - -module.exports = function (eleventyConfig) { - eleventyConfig.addFilter( - "getGitCommitDateFromPath", - getGitCommitDateFromPath - ); - eleventyConfig.addFilter( - "getCollectionNewestGitCommitDate", - getCollectionNewestGitCommitDate - ); -}; - -module.exports.getGitCommitDateFromPath = getGitCommitDateFromPath; -module.exports.getCollectionNewestGitCommitDate = - getCollectionNewestGitCommitDate; diff --git a/.eslintrc.js b/.eslintrc.js deleted file mode 100644 index 41b891e..0000000 --- a/.eslintrc.js +++ /dev/null @@ -1,11 +0,0 @@ -module.exports = { - root: true, - extends: ["xo-space", "plugin:prettier/recommended"], - plugins: ["prettier"], - rules: { - "prettier/prettier": "error", - }, - env: { - browser: true, - }, -}; diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..4c334af --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,26 @@ +on: + push: + branches-ignore: + - "gh-pages" +jobs: + build: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: ["ubuntu-latest", "macos-latest", "windows-latest"] + node: ["18", "22"] + name: Node.js ${{ matrix.node }} on ${{ matrix.os }} + steps: + - name: Checkout repo + uses: actions/checkout@v3 + with: + fetch-depth: 0 + - name: Setup node + uses: actions/setup-node@v3 + with: + node-version: ${{ matrix.node }} + - run: npm install + - run: npm run lint + - run: npm test +env: + YARN_GPG: no \ No newline at end of file diff --git a/.husky/.gitignore b/.husky/.gitignore deleted file mode 100644 index 31354ec..0000000 --- a/.husky/.gitignore +++ /dev/null @@ -1 +0,0 @@ -_ diff --git a/.husky/pre-commit b/.husky/pre-commit deleted file mode 100755 index 6a06103..0000000 --- a/.husky/pre-commit +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/sh -. "$(dirname "$0")/_/husky.sh" - -npm test -npx lint-staged \ No newline at end of file diff --git a/.npmignore b/.npmignore index c346e3e..8aeaba2 100644 --- a/.npmignore +++ b/.npmignore @@ -1,9 +1,10 @@ .editorconfig .eslintcache -.eslintrc.js .github/ -.husky/pre-commit .nyc_output +.gitattributes +eslint.config.js tests -.travis.yml -.gitattributes \ No newline at end of file +prettier.config.js +lefthook.yml +ava.config.js \ No newline at end of file diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 37b1386..0000000 --- a/.travis.yml +++ /dev/null @@ -1,16 +0,0 @@ -language: node_js - -node_js: - - 14 - - 16 - -os: - - linux - - osx - - windows - -before_script: - - npm install -script: - - npm run lint - - npm run test diff --git a/README.md b/README.md index ed5dd47..b69bf4a 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ This Eleventy plugin provides two [template filters](https://www.11ty.dev/docs/f 🌏 This plugin is made primarily to populate `` fields in an RSS feed. Here is [a blog post on how to use this plugin](https://saneef.com/tutorials/fix-dates-on-eleventy-rss-feeds/) with [`eleventy-plugin-rss`](https://www.11ty.dev/docs/plugins/rss/). -⚠️ Getting Git commit date is a bit slow (\~50ms for each path). So, use it sparingly. It's recommended to call this filter within a production flag. +⚠️ Git commit date is a bit slow. So, it's recommended to call this filter within a production flag. ## Usage @@ -22,13 +22,13 @@ npm install eleventy-plugin-git-commit-date ### 2. Add to Eleventy config ```js -// .eleventy.js +// eleventy.config.js -const pluginGitCommitDate = require("eleventy-plugin-git-commit-date"); +import pluginGitCommitDate from "eleventy-plugin-git-commit-date"; -module.exports = function (eleventyConfig) { +export default function (eleventyConfig) { eleventyConfig.addPlugin(pluginGitCommitDate); -}; +} ``` ### 3. Use in templates diff --git a/ava.config.js b/ava.config.js new file mode 100644 index 0000000..d283bd1 --- /dev/null +++ b/ava.config.js @@ -0,0 +1,6 @@ +export default { + files: ["tests/**/*", "!tests/utils.js"], + watchMode: { + ignoreChanges: ["tests/output/**"], + }, +}; diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..08269f2 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,38 @@ +// @ts-check +import { FlatCompat } from "@eslint/eslintrc"; +import js from "@eslint/js"; +import prettier from "eslint-plugin-prettier"; +import { defineConfig } from "eslint/config"; +import globals from "globals"; +import path from "path"; +import { fileURLToPath } from "url"; + +// Mimic CommonJS variables -- not needed if using CommonJS +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +const compat = new FlatCompat({ + baseDirectory: __dirname, + recommendedConfig: js.configs.recommended, + allConfig: js.configs.all, +}); + +export default defineConfig([ + { + extends: compat.extends("xo-space", "plugin:prettier/recommended"), + + plugins: { + prettier, + }, + + rules: { + "prettier/prettier": "error", + }, + + languageOptions: { + globals: { + ...globals.browser, + }, + }, + }, +]); diff --git a/index.js b/index.js new file mode 100644 index 0000000..c8412a3 --- /dev/null +++ b/index.js @@ -0,0 +1,16 @@ +// @ts-check +import getCollectionNewestGitCommitDate from "./src/getCollectionNewestGitCommitDate.js"; +import getGitCommitDateFromPath from "./src/getGitCommitDateFromPath.js"; + +export default function (eleventyConfig) { + eleventyConfig.addAsyncFilter( + "getGitCommitDateFromPath", + getGitCommitDateFromPath, + ); + eleventyConfig.addAsyncFilter( + "getCollectionNewestGitCommitDate", + getCollectionNewestGitCommitDate, + ); +} + +export { getCollectionNewestGitCommitDate, getGitCommitDateFromPath }; diff --git a/lefthook.yml b/lefthook.yml new file mode 100644 index 0000000..4b49612 --- /dev/null +++ b/lefthook.yml @@ -0,0 +1,15 @@ +pre-commit: + commands: + prettier: + glob: "*.{js,md,json}" + run: npx prettier --write {staged_files} + stage_fixed: true + + eslint: + glob: "*.{js}" + run: npx eslint --fix {staged_files} + stage_fixed: true + + tests: + glob: "*.{js}" + run: npm run test \ No newline at end of file diff --git a/package.json b/package.json index d37d908..1f31175 100644 --- a/package.json +++ b/package.json @@ -2,15 +2,15 @@ "name": "eleventy-plugin-git-commit-date", "version": "0.1.3", "description": "Eleventy plugin to get Git commit time of a file, or a Eleventy collection.", - "main": ".eleventy.js", + "main": "index.js", + "type": "module", "repository": { "type": "git", "url": "git+https://github.com:saneef/eleventy-plugin-git-commit-date.git" }, "scripts": { "lint": "eslint src/**.js tests/**.js", - "test": "nyc ava --timeout=1m -v --color", - "prepare": "husky install" + "test": "ava" }, "keywords": [ "last-updated", @@ -26,28 +26,17 @@ "cross-spawn": "^7.0.3" }, "devDependencies": { - "ava": "^3.15.0", - "eslint": "^7.32.0", - "eslint-config-prettier": "^8.3.0", - "eslint-config-xo-space": "^0.29.0", - "eslint-plugin-prettier": "^3.4.0", - "husky": "^7.0.1", - "lint-staged": "^11.1.2", - "nyc": "^15.1.0", - "prettier": "^2.3.2", - "rimraf": "^3.0.2" - }, - "lint-staged": { - "*.js": "eslint --cache --fix", - "*.{js,md,json}": "prettier --write" - }, - "ava": { - "files": [ - "tests/**/*", - "!tests/utils.js" - ], - "ignoredByWatcher": [ - "tests/output/**" - ] + "@eslint/eslintrc": "^3.3.3", + "@eslint/js": "^9.39.2", + "ava": "^6.4.1", + "eslint": "^9.39.2", + "eslint-config-prettier": "^10.1.8", + "eslint-config-xo-space": "^0.35.0", + "eslint-plugin-prettier": "^5.5.4", + "globals": "^16.5.0", + "lefthook": "^2.0.13", + "prettier": "^3.7.4", + "prettier-plugin-jsdoc": "^1.8.0", + "rimraf": "^5.0.9" } } diff --git a/prettier.config.js b/prettier.config.js new file mode 100644 index 0000000..fb9800e --- /dev/null +++ b/prettier.config.js @@ -0,0 +1,7 @@ +// @ts-check +/** @type {import("prettier").Config} */ +const config = { + plugins: ["prettier-plugin-jsdoc"], +}; + +export default config; diff --git a/src/getCollectionNewestGitCommitDate.js b/src/getCollectionNewestGitCommitDate.js index 64db7c9..fa0c940 100644 --- a/src/getCollectionNewestGitCommitDate.js +++ b/src/getCollectionNewestGitCommitDate.js @@ -1,26 +1,32 @@ // @ts-check -const getGitCommitDateFromPath = require("./getGitCommitDateFromPath"); +import getGitCommitDateFromPath from "./getGitCommitDateFromPath.js"; +import memoize from "./utils/memoize.js"; /** * Gets the collection's newest Git commit date. * - * @param {Array} collection The collection - * - * @return {Date} The collection newest git commit date. + * @param {object[]} collection Collection + * @returns {Promise} Newest git commit date among the items + * in the collection */ -module.exports = function (collection) { +async function getCollectionNewestGitCommitDate(collection) { if (!collection || !collection.length) { return; } - const timestamps = collection - .map((item) => getGitCommitDateFromPath(item.inputPath)) + const timestamps = await Promise.all( + collection.map((item) => getGitCommitDateFromPath(item.inputPath)), + ); + + const dates = timestamps // Timestamps will be undefined for the paths not // yet commited to Git. So weeding them out. .filter((ts) => Boolean(ts)) .map((ts) => ts.getTime()); - if (timestamps.length) { - return new Date(Math.max(...timestamps)); + if (dates.length) { + return new Date(Math.max(...dates)); } -}; +} + +export default memoize(getCollectionNewestGitCommitDate); diff --git a/src/getGitCommitDateFromPath.js b/src/getGitCommitDateFromPath.js index 0865abc..d83fef0 100644 --- a/src/getGitCommitDateFromPath.js +++ b/src/getGitCommitDateFromPath.js @@ -1,6 +1,6 @@ // @ts-check -const path = require("path"); -const spawn = require("cross-spawn"); +import memoize from "./utils/memoize.js"; +import { spawnAsync } from "./utils/spawn.js"; /** * Gets the Git commit date from path. @@ -8,30 +8,27 @@ const spawn = require("cross-spawn"); * The code is based on @vuepress/plugin-last-updated, * https://github.com/vuejs/vuepress/blob/master/packages/%40vuepress/plugin-last-updated/ * - * @param {string} filePath The file path - * - * @return {Date} The git commit date if path is commited to Git. + * @param {string} filePath The file path + * @returns {Promise} Commit date if path is commited to Git, + * otherwise `undefined` */ -module.exports = function (filePath) { +async function getGitCommitDateFromPath(filePath) { let output; try { - output = spawn.sync( - "git", - ["log", "-1", "--format=%at", path.basename(filePath)], - { cwd: path.dirname(filePath) } - ); - } catch { + output = await spawnAsync("git", ["log", "-1", "--format=%at", filePath]); + } catch (e) { + console.log(e); throw new Error("Fail to run 'git log'"); } - if (output && output.stdout) { - const ts = parseInt(output.stdout.toString("utf-8"), 10) * 1000; + const ts = parseInt(output, 10) * 1000; - // Paths not commited to Git returns empty timestamps, resulting in NaN. - // So, convert only valid timestamps. - if (!isNaN(ts)) { - return new Date(ts); - } + // Paths not commited to Git returns empty timestamps, resulting in NaN. + // So, convert only valid timestamps. + if (!isNaN(ts)) { + return new Date(ts); } -}; +} + +export default memoize(getGitCommitDateFromPath); diff --git a/src/utils/memoize.js b/src/utils/memoize.js new file mode 100644 index 0000000..0e2a6fa --- /dev/null +++ b/src/utils/memoize.js @@ -0,0 +1,27 @@ +/** + * Memoize function + * + * The code is adapted from MemoizeFunction() of Eleventy project See + * https://github.com/11ty/eleventy/blob/5a65b244235bfcb64ecf085bfc65a99a670e9df4/src/Util/MemoizeFunction.js + * + * @param {Function} fn The function to memoize + * @returns {Function} + */ +export default function memoize(fn) { + const cache = new Map(); + + return (...args) => { + if (args.filter(Boolean).length > 1) { + console.warn("memoize() only supports single argument functions"); + return fn(...args); + } + + const [cacheKey] = args; + + if (!cache.has(cacheKey)) { + cache.set(cacheKey, fn(...args)); + } + + return cache.get(cacheKey); + }; +} diff --git a/src/utils/spawn.js b/src/utils/spawn.js new file mode 100644 index 0000000..f4aa6cc --- /dev/null +++ b/src/utils/spawn.js @@ -0,0 +1,28 @@ +import { spawn } from "node:child_process"; + +export function spawnAsync(command, args, options) { + return new Promise((resolve, reject) => { + const cmd = spawn(command, args, options); + const res = []; + cmd.stdout.on("data", (data) => { + res.push(data.toString("utf8")); + }); + + const err = []; + cmd.stderr.on("data", (data) => { + err.push(data.toString("utf8")); + }); + + cmd.on("close", (code) => { + if (err.length > 0) { + reject(err.join("\n")); + } else if (code === 1) { + reject( + new Error("Internal error: process closed with error exit code."), + ); + } else { + resolve(res.join("\n")); + } + }); + }); +} diff --git a/tests/getCollectionNewestGitCommitDate.js b/tests/getCollectionNewestGitCommitDate.js index 7478f06..d8c0469 100644 --- a/tests/getCollectionNewestGitCommitDate.js +++ b/tests/getCollectionNewestGitCommitDate.js @@ -1,18 +1,18 @@ -const test = require("ava"); -const path = require("path"); -const fs = require("fs/promises"); -const { promisify } = require("util"); -const rimraf = promisify(require("rimraf")); -const getCollectionNewestGitCommitDate = require("../src/getCollectionNewestGitCommitDate.js"); +import test from "ava"; +import { mkdir, writeFile } from "node:fs/promises"; +import path from "node:path"; +import { rimraf } from "rimraf"; +import { fileURLToPath } from "url"; +import { getCollectionNewestGitCommitDate } from "../index.js"; -const outputBase = path.join("tests/output/"); +const __dirname = path.dirname(fileURLToPath(import.meta.url)); -test("Get newest commit date of collection", (t) => { +test("Get newest commit date of collection", async (t) => { const collection = [ { inputPath: path.join(__dirname, "./fixtures/sample.md") }, { inputPath: path.join(__dirname, "./fixtures/another-sample-file.md") }, ]; - const date = getCollectionNewestGitCommitDate(collection); + const date = await getCollectionNewestGitCommitDate(collection); t.truthy(date); t.is(date.toISOString(), "2021-08-19T09:57:47.000Z"); }); @@ -20,10 +20,11 @@ test("Get newest commit date of collection", (t) => { test("Shouldn't get commit date from an empty collection", async (t) => { const collection = []; - t.is(getCollectionNewestGitCommitDate(collection), undefined); + t.is(await getCollectionNewestGitCommitDate(collection), undefined); }); test("Shouldn't get commit date from collection of uncommited files", async (t) => { + const outputBase = path.join(__dirname, "output/collection"); const collection = [ { inputPath: path.join(outputBase, "test-01.md") }, { inputPath: path.join(outputBase, "test-02.md") }, @@ -31,10 +32,10 @@ test("Shouldn't get commit date from collection of uncommited files", async (t) await rimraf(outputBase); - await fs.mkdir(outputBase, { recursive: true }); - await Promise.all(collection.map((p) => fs.writeFile(p.inputPath, ""))); + await mkdir(outputBase, { recursive: true }); + await Promise.all(collection.map((p) => writeFile(p.inputPath, ""))); - t.is(getCollectionNewestGitCommitDate(collection), undefined); + t.is(await getCollectionNewestGitCommitDate(collection), undefined); await rimraf(outputBase); }); diff --git a/tests/getGitCommitDateFromPath.js b/tests/getGitCommitDateFromPath.js index ca3314c..4a0a1d7 100644 --- a/tests/getGitCommitDateFromPath.js +++ b/tests/getGitCommitDateFromPath.js @@ -1,28 +1,28 @@ -const test = require("ava"); -const path = require("path"); -const fs = require("fs/promises"); -const { promisify } = require("util"); -const rimraf = promisify(require("rimraf")); -const getGitCommitDateFromPath = require("../src/getGitCommitDateFromPath.js"); +import test from "ava"; +import { mkdir, writeFile } from "node:fs/promises"; +import path from "node:path"; +import { rimraf } from "rimraf"; +import { fileURLToPath } from "url"; +import { getGitCommitDateFromPath } from "../index.js"; -const outputBase = path.join("tests/output/"); -const tempFileName = "test.md"; +const __dirname = path.dirname(fileURLToPath(import.meta.url)); -test("Get commit date of a committed file", (t) => { +test("Get commit date of a committed file", async (t) => { const filePath = path.join(__dirname, "./fixtures/sample.md"); - const date = getGitCommitDateFromPath(filePath); + const date = await getGitCommitDateFromPath(filePath); t.truthy(date); t.is(date.toISOString(), "2021-08-19T09:57:47.000Z"); }); test("Should not get commit date of a uncommitted file", async (t) => { - const filePath = path.join(outputBase, tempFileName); + const outputBase = path.join(__dirname, "/output/single-file-path"); + const filePath = path.join(outputBase, "test.md"); await rimraf(outputBase); - await fs.mkdir(outputBase, { recursive: true }); - await fs.writeFile(filePath, ""); + await mkdir(outputBase, { recursive: true }); + await writeFile(filePath, ""); - t.is(getGitCommitDateFromPath(filePath), undefined); + t.is(await getGitCommitDateFromPath(filePath), undefined); await rimraf(outputBase); });