├── .eslintignore
├── .eslintrc.yml
├── .gitattributes
├── .github
└── workflows
│ └── test.yml
├── .gitignore
├── .npmrc
├── .prettierignore
├── .prettierrc.yml
├── .release-it.json
├── CHANGELOG.md
├── LICENSE
├── README.md
├── lib
└── index.d.ts
├── package-lock.json
├── package.json
├── src
└── index.js
└── test
├── fixtures
├── array-merge
│ └── src
│ │ └── metadata
│ │ ├── first.json
│ │ └── second.json
├── deep-nested
│ └── src
│ │ └── path
│ │ └── path
│ │ └── data.yaml
├── duplicate
│ └── src
│ │ ├── data.json
│ │ └── data2.json
├── external-file
│ ├── data
│ │ └── test.json
│ └── src
│ │ └── main.json
├── external-folder
│ ├── data
│ │ ├── test.json
│ │ └── test2.yaml
│ └── src
│ │ └── main.json
├── ignored
│ └── src
│ │ ├── ignored.yaml
│ │ ├── implicitly-ignored.yaml
│ │ └── not-ignored.json
├── incorrect-path
│ └── src
│ │ └── data.json
├── json
│ └── src
│ │ └── data.json
├── malformed
│ └── src
│ │ └── data.json
├── nested-directories
│ ├── data
│ │ ├── nested
│ │ │ └── test.yaml
│ │ └── test.json
│ └── src
│ │ └── main.json
├── nested-keypath
│ └── src
│ │ └── metadata.yml
├── nested
│ └── src
│ │ └── path
│ │ └── data.yaml
├── object-merge
│ └── src
│ │ └── metadata
│ │ ├── metatags.json
│ │ ├── navitem.yml
│ │ └── navitems.json
├── toml
│ └── src
│ │ └── data.toml
├── unsupported-ext
│ └── src
│ │ └── data.txt
└── yaml
│ └── src
│ └── data.yaml
└── index.js
/.eslintignore:
--------------------------------------------------------------------------------
1 | lib
--------------------------------------------------------------------------------
/.eslintrc.yml:
--------------------------------------------------------------------------------
1 | env:
2 | node: true
3 | es6: true
4 | extends:
5 | - 'eslint:recommended'
6 | - 'plugin:n/recommended'
7 | - 'plugin:import/recommended'
8 | - 'prettier'
9 | parserOptions:
10 | ecmaVersion: 2020
11 | rules:
12 | no-console: error
13 | prefer-const: error
14 | no-var: error
15 | no-use-before-define: error
16 | no-await-in-loop: error
17 | n/exports-style: [0, error]
18 | import/first: error
19 | import/no-anonymous-default-export: error
20 | import/no-unassigned-import: error
21 | import/no-internal-modules:
22 | - error
23 | - allow:
24 | - src/**
25 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Automatically normalize line endings for all text-based files
2 | # http://git-scm.com/docs/gitattributes#_end_of_line_conversion
3 | * text=auto eol=lf
4 |
5 | # For binary file types, prevent converting CRLF chars
6 | *.jpg -text
7 | *.png -text
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 | on:
3 | push:
4 | branches: ['**']
5 | pull_request:
6 | branches: ['main']
7 |
8 | jobs:
9 | pre-test:
10 | runs-on: ubuntu-latest
11 | steps:
12 | - uses: actions/checkout@v3
13 | - uses: actions/setup-node@v3
14 | with:
15 | cache: 'npm'
16 |
17 | - run: npm install
18 | - run: npm run format:check
19 | - run: npm run lint:check
20 |
21 | branch-test:
22 | if: github.ref_name != 'main' && success()
23 | needs: pre-test
24 | runs-on: ${{ matrix.os }}
25 | strategy:
26 | matrix:
27 | os: ['ubuntu-latest', 'windows-latest']
28 | node: ['14.14.0']
29 | name: Testing Node ${{ matrix.node }} on ${{ matrix.os }}
30 | steps:
31 | - uses: actions/checkout@v3
32 | - uses: actions/setup-node@v3
33 | with:
34 | cache: 'npm'
35 |
36 | - run: npm install
37 | - run: npm test
38 |
39 | test:
40 | if: github.ref_name == 'main' && success()
41 | needs: pre-test
42 | runs-on: ${{ matrix.os }}
43 | strategy:
44 | matrix:
45 | os: ['ubuntu-latest', 'windows-latest']
46 | node: ['14.14.0', '16.0', '18.0']
47 | name: Testing Node ${{ matrix.node }} on ${{ matrix.os }}
48 | steps:
49 | - uses: actions/checkout@v3
50 | - uses: actions/setup-node@v3
51 | with:
52 | node-version: ${{ matrix.node }}
53 | cache: 'npm'
54 |
55 | - run: npm install
56 | - run: npm test
57 | - if: matrix.os == 'ubuntu-latest' && matrix.node == '18.0'
58 | run: npm run coverage
59 | - if: matrix.os == 'ubuntu-latest' && matrix.node == '18.0'
60 | uses: coverallsapp/github-action@v2
61 | with:
62 | file: coverage.info
63 | format: lcov
64 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # NPM
2 | node_modules
3 | npm-debug.log*
4 | .npm
5 | *.tgz
6 |
7 | # ESLint Cache
8 | .eslintcache
9 |
10 | # build output
11 | lib/index.*
12 |
13 | # tests
14 | **/build
15 | coverage.info
16 | coverage
17 |
18 | # other
19 | .DS_Store
20 | .idea
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | package-lock.json = false
2 | sign-git-tag = true
3 | message = Bump package.json to %s
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | test/fixtures/**
2 | coverage/*
3 | package-lock.json
4 | lib
--------------------------------------------------------------------------------
/.prettierrc.yml:
--------------------------------------------------------------------------------
1 | trailingComma: none
2 | tabWidth: 2
3 | semi: false
4 | singleQuote: true
5 | bracketSpacing: true
6 | arrowParens: always
7 | printWidth: 100
8 |
--------------------------------------------------------------------------------
/.release-it.json:
--------------------------------------------------------------------------------
1 | {
2 | "hooks": {
3 | "before:init": ["npm run format:check", "npm run lint:check", "npm test"],
4 | "after:bump": "auto-changelog --sort-commits date-desc -p --commit-limit false --ignore-commit-pattern '^((dev|chore|ci):|Release)'",
5 | "after:npm:bump": "npm pack",
6 | "after:release": "echo Successfully released ${name} v${version} to ${repo.repository}."
7 | },
8 | "git": {
9 | "commitMessage": "Release ${version}",
10 | "commitArgs": ["-S"],
11 | "tagAnnotation": "Release ${version}",
12 | "tagArgs": ["-s"],
13 | "changelog": "auto-changelog -u --commit-limit false --ignore-commit-pattern '^((dev|chore|ci):|Release)' --stdout -t https://raw.githubusercontent.com/release-it/release-it/master/templates/changelog-compact.hbs"
14 | },
15 | "npm": {
16 | "publish": false
17 | },
18 | "github": {
19 | "release": true,
20 | "releaseName": "@metalsmith/metadata ${version}",
21 | "tokenRef": "GITHUB_TOKEN",
22 | "assets": ["metalsmith-metadata-${version}.tgz"]
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ### Changelog
2 |
3 | All notable changes to this project will be documented in this file. Dates are displayed in UTC.
4 |
5 | Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
6 |
7 | #### [0.3.0](https://github.com/metalsmith/metadata/compare/0.2.1...0.3.0)
8 |
9 | - Includes source maps in dist for better debugging [`2555da5`](https://github.com/metalsmith/metadata/commit/2555da5216862346b6fbe11bf707a2a12c8c6614)
10 | - Updates deepmerge 4.2.2 -> 4.3.1 [`379fc4d`](https://github.com/metalsmith/metadata/commit/379fc4d5ff3e61b980faf0db1098c0f0767fc718)
11 | - Renames default export to 'metadata' for better editor intellisense [`a63c1d3`](https://github.com/metalsmith/metadata/commit/a63c1d3c19a3296db79a3c41e53a79f52230ab55)
12 | - Drops support for Node < 14.14.0 & migrates tests to ESM on src file [`28a0cc5`](https://github.com/metalsmith/metadata/commit/28a0cc515c54452ff5d834ce1a8bf8b9bd8cdfa0)
13 | - Drops support for metalsmith < 2.5.0 & uses metalsmith.debug instead of debug [`73898bf`](https://github.com/metalsmith/metadata/commit/73898bf964dfdbdc6a928506d7cf7134fde4dbf8)
14 | - Fixes a regression introduced in 0.2.1 with array merges, handles repeat runs properly [`65d480e`](https://github.com/metalsmith/metadata/commit/65d480edc3f1dc1307514ed426397a8715366ca2)
15 |
16 | #### [0.2.1](https://github.com/metalsmith/metadata/compare/0.2.0...0.2.1)
17 |
18 | > 30 May 2023
19 |
20 | - Explicitly specify array merge as concat [`9102291`](https://github.com/metalsmith/metadata/commit/9102291e94666237b7909464dacea3b3341f8d53)
21 |
22 | #### [0.2.0](https://github.com/metalsmith/metadata/compare/0.1.0...0.2.0)
23 |
24 | > 7 August 2022
25 |
26 | - Adds Typescript definitions [`e0b6f7d`](https://github.com/metalsmith/metadata/commit/e0b6f7d1a63c8b7f666f1883f6db5d7484d26cfd)
27 | - Update debug from 4.3.3 -> 4.3.4, use strict [`f6d5125`](https://github.com/metalsmith/metadata/commit/f6d5125f37ace47ed123f102e5f8c040707d60dc)
28 | - feat: provides dual bundling ESM/CJS [`6f4f9dc`](https://github.com/metalsmith/metadata/commit/6f4f9dc569451a2f6660a0cb32aa031d9b767784)
29 |
30 | #### 0.1.0
31 |
32 | > 19 March 2022
33 |
34 | - Complete re-write of metadata. [`#27`](https://github.com/metalsmith/metadata/pull/27)
35 | - Nested config files [`#19`](https://github.com/metalsmith/metadata/pull/19)
36 | - Don't attempt to parse a file if the key already exists in the metadata, even if the file is missing [`#9`](https://github.com/metalsmith/metadata/pull/9)
37 | - Revert 9e2224d2e0dd99661f2174f788a337f1add9d823 [`#16`](https://github.com/metalsmith/metadata/pull/16)
38 | - Normalizing path options for Windows compatibility. [`#7`](https://github.com/metalsmith/metadata/pull/7)
39 | - Update dependencies [`#8`](https://github.com/metalsmith/metadata/pull/8)
40 | - Allow .yml as file extension for YAML as well [`#12`](https://github.com/metalsmith/metadata/pull/12)
41 | - feat: complete & sort package.json [`387a0ea`](https://github.com/metalsmith/metadata/commit/387a0ea02b3670f460f61ca90240f921270ef942)
42 | - feat: rewrite [`030593c`](https://github.com/metalsmith/metadata/commit/030593c4a6669fe955270920d54fb57b43ca9326)
43 | - manual merge [`9a8d64c`](https://github.com/metalsmith/metadata/commit/9a8d64c27acfa31770b7d69b7ffe66d3133c0d9b)
44 | - fix: fixed file/dir path for external reads [`d3fc01e`](https://github.com/metalsmith/metadata/commit/d3fc01ecd6d15ccec7bcb6804040718ef3561ab8)
45 | - removed obsolete code [`9dbd53c`](https://github.com/metalsmith/metadata/commit/9dbd53cf681d8eae2aa652bb9078ceffc15c0121)
46 | - removed console.logs [`7edb15d`](https://github.com/metalsmith/metadata/commit/7edb15d722e96eb7651fddc14a326672080118f7)
47 | - debug next branch [`e8b0513`](https://github.com/metalsmith/metadata/commit/e8b05136128106766201fa9fc125b4fdcf67e7c5)
48 | - fixed issue with yaml files in folder [`ab93171`](https://github.com/metalsmith/metadata/commit/ab931712806791bfa9226d705f49bf6b00978fbc)
49 | - fixed spelling [`bc578ef`](https://github.com/metalsmith/metadata/commit/bc578ef521d1f14267acaf1d7ac6a54fc9e00f1d)
50 | - updated name to @metalsmith/metadata [`aabfe1c`](https://github.com/metalsmith/metadata/commit/aabfe1c9ea55405ce5a0b93738ebac31f6b911e6)
51 | - run changelog [`8d2ad44`](https://github.com/metalsmith/metadata/commit/8d2ad4483ca508d54b3aec8dcb2845d9bfcca657)
52 | - fixed path separator [`6375b00`](https://github.com/metalsmith/metadata/commit/6375b0087e1d3ddb9664213675b1b456274a165d)
53 | - added proper error reporting to done() [`6d21924`](https://github.com/metalsmith/metadata/commit/6d2192480167c58b5c77666bd55129188ea86da1)
54 | - run utilities [`ca4bdf6`](https://github.com/metalsmith/metadata/commit/ca4bdf6f8faac4272d60ede1e657c0ad1e716f9d)
55 | - edited readme [`299ec8a`](https://github.com/metalsmith/metadata/commit/299ec8a3c8309f607f3a26b9c18a9010d0891024)
56 | - Changed options file path [`e370e5a`](https://github.com/metalsmith/metadata/commit/e370e5a1a43211a665dac6affb884afdc965dbbd)
57 | - run format and changelog [`a761955`](https://github.com/metalsmith/metadata/commit/a7619551a87561833d239fdb2e1b9f9afee33692)
58 | - added tests [`e6ad84a`](https://github.com/metalsmith/metadata/commit/e6ad84a533c1658ac749b35887ad73fb9a5f6ac3)
59 | - Add tests for nested and deep-nested path [`e525a11`](https://github.com/metalsmith/metadata/commit/e525a11bce359214afab15d3fa5ddedea88eadcb)
60 | - Add regex for normalizing path [`c9cf34a`](https://github.com/metalsmith/metadata/commit/c9cf34a199a42f0449647729bb7f4010c2c2470f)
61 | - bump version number [`cc8b115`](https://github.com/metalsmith/metadata/commit/cc8b11597719811f8fd7043d75d549e6903d6eb1)
62 | - more tests [`6b0cf69`](https://github.com/metalsmith/metadata/commit/6b0cf696cea1321348fe5dd3d9c0b5f9f5043762)
63 | - don't try and parse if the key already exists in the metadata... [`c3353d8`](https://github.com/metalsmith/metadata/commit/c3353d83f8eed5645e9178e5556c6dabf9d23e4e)
64 | - fix test name [`965a9b5`](https://github.com/metalsmith/metadata/commit/965a9b5d6d5d1f0b7a54381c0719c0a404b2f3dc)
65 | - first commit [`48bcefc`](https://github.com/metalsmith/metadata/commit/48bcefcb049d896158aedd340149417a600d4c4c)
66 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright © 2021 webketje
4 | Copyright © 2014-2021 Segment
5 |
6 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
7 |
8 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
9 |
10 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # @metalsmith/metadata
2 |
3 | A metalsmith plugin to load global metadata from files and directories.
4 |
5 | [![metalsmith: plugin][metalsmith-badge]][metalsmith-url]
6 | [![npm: version][npm-badge]][npm-url]
7 | [![travis: build][ci-badge]][ci-url]
8 | [![code coverage][codecov-badge]][codecov-url]
9 | [![license: MIT][license-badge]][license-url]
10 |
11 | - Reads, parses and merges data files into global metadata and removes them from the build (when applicable).
12 | - Supports JSON, YAML and TOML files. [TOML parser](https://www.npmjs.com/package/toml) needs to be installed separately.
13 | - Supports dot-notated metadata keypath targets as option keys, eg `{'config.nav': 'src/config/nav.yml'}`
14 |
15 | ## Installation
16 |
17 | NPM:
18 |
19 | ```bash
20 | npm install @metalsmith/metadata
21 | ```
22 |
23 | Yarn:
24 |
25 | ```bash
26 | yarn add @metalsmith/metadata
27 | ```
28 |
29 | ## Usage
30 |
31 | Pass the options to `Metalsmith#use`. The options object is in the format `{ 'metadata.key': 'path/to/(file.ext|dir)' }`. Relative file/directory paths are resolved to `metalsmith.directory()`. Directory option keys will include direct children of the directory, see [Mapping nested metadata directories](#mapping-nested-metadata-directories) for creating nested directory structures.
32 |
33 | ```js
34 | import Metalsmith from 'metalsmith'
35 | import metadata from '@metalsmith/metadata'
36 |
37 | const __dirname = dirname(new URL(import.meta.url).pathname)
38 |
39 | Metalsmith(__dirname)
40 | .use(
41 | metadata({
42 | // in-source JSON file
43 | jsonFile: 'src/data/a-json-file.json',
44 | // out-of-source YAML file at nested keypath
45 | 'nested.yamlFile': 'some-directory/a-yaml-file.yaml',
46 | // out-of-source directory
47 | aDirectory: 'some-directory/a-directory'
48 | })
49 | )
50 | .build((err) => {
51 | console.log(metalsmith.metadata())
52 | // logs { jsonFile: {...}, nested: { yamlFile: {...}}, aDirectory: {...} }
53 | })
54 | ```
55 |
56 | Files inside `metalsmith.source()` will be considered metadata and thus removed from the build output.
57 |
58 | ### Plugin order
59 |
60 | Typically, you want to use this plugin somewhere at the start of the chain, before any rendering plugins run, like [@metalsmith/layouts](https://github.com/metalsmith/layouts) or [@metalsmith/in-place](https://github.com/metalsmith/in-place).
61 |
62 | ### Merging metadata files into objects
63 |
64 | You can merge metadata files into objects by making the root of the data file an object. For example, given the following 2 files:
65 |
66 |
67 | src/themes/red.json | src/themes/blue.json |
68 |
69 |
70 | {
71 | "red": {
72 | "primary-color": "#FF0000"
73 | }
74 | }
75 | |
76 |
77 | {
78 | "blue": {
79 | "primary-color": "#00FF00"
80 | }
81 | }
82 | |
83 |
84 |
85 |
86 | with a usage like `metalsmith.use(metadata({ themes: 'src/themes' }))`, `metalsmith.metadata().themes` will be `{ red: {"primary-color": #00FF00"}, blue: {"primary-color": "#00FF00"}}`.
87 |
88 | ### Merging metadata files into arrays
89 |
90 | You can merge metadata files into an array by making the root of the data file an array. For example, given the following 2 files:
91 |
92 |
93 | src/themes/red.json | src/themes/blue.json |
94 |
95 |
96 | [
97 | {
98 | "primary-color": "#FF0000"
99 | }
100 | ]
101 | |
102 |
103 | [
104 | {
105 | "primary-color": "#00FF00"
106 | }
107 | ]
108 | |
109 |
110 |
111 |
112 | with a usage like `metalsmith.use(metadata({ themes: 'src/themes' }))`, `metalsmith.metadata().themes` will be `[{"primary-color": #00FF00"}, {"primary-color": "#00FF00"}]`.
113 |
114 | ### Mapping nested metadata directories
115 |
116 | You can map nested metadata directories by specifying multiple options:
117 |
118 | ```js
119 | metalsmith.use(
120 | metadata({
121 | config: 'src/config',
122 | 'config.theme': 'src/config/theme',
123 | 'config.theme.summer': 'src/config/theme/summer',
124 | 'config.constants': 'src/config/constants.yaml'
125 | })
126 | )
127 | ```
128 |
129 | The resulting metadata will have a structure like:
130 |
131 | ```js
132 | {
133 | ...otherMetadata,
134 | config: {
135 | ...metadata_from_config_dir
136 | theme: {
137 | ...metadata_from_config_theme_dir
138 | summer: { ...metadata_from_config_theme_summer_dir }
139 | }
140 | },
141 | constants: {
142 | ...metadata_from_config_constants_file
143 | }
144 | }
145 | ```
146 |
147 | ## Debug
148 |
149 | To enable debug logs, set the `DEBUG` environment variable to `@metalsmith/metadata`:
150 |
151 | ```js
152 | metalsmith.env('DEBUG', '@metalsmith/metadata*')
153 | ```
154 |
155 | ## CLI Usage
156 |
157 | To use this plugin with the Metalsmith CLI,add the `@metalsmith/metadata` key to your `metalsmith.json` plugins. Each key in the dictionary of options will be the key for the global metadata object, like so:
158 |
159 | ```json
160 | {
161 | "plugins": {
162 | "@metalsmith/metadata": {
163 | "authors": "src/authors.json",
164 | "categories": "src/categories.yaml",
165 | "customers": "external_data/customers"
166 | }
167 | }
168 | }
169 | ```
170 |
171 | ## License
172 |
173 | [MIT][license-url]
174 |
175 | [npm-badge]: https://img.shields.io/npm/v/@metalsmith/metadata.svg
176 | [npm-url]: https://www.npmjs.com/package/@metalsmith/metadata
177 | [ci-badge]: https://github.com/metalsmith/metalsmith/actions/workflows/test.yml/badge.svg
178 | [ci-url]: https://github.com/metalsmith/metalsmith/actions/workflows/test.yml
179 | [metalsmith-badge]: https://img.shields.io/badge/metalsmith-core_plugin-green.svg?longCache=true
180 | [metalsmith-url]: https://metalsmith.io
181 | [codecov-badge]: https://img.shields.io/coveralls/github/metalsmith/metadata
182 | [codecov-url]: https://coveralls.io/github/metalsmith/metadata
183 | [license-badge]: https://img.shields.io/github/license/metalsmith/metadata
184 | [license-url]: LICENSE
185 |
--------------------------------------------------------------------------------
/lib/index.d.ts:
--------------------------------------------------------------------------------
1 | import Metalsmith from "metalsmith";
2 |
3 | export default metadata;
4 | export type Options = {
5 | [key: string]: string;
6 | };
7 | /**
8 | * A Metalsmith plugin to load global metadata from files
9 | *
10 | * @example
11 | * ```js
12 | * // inside metalsmith.source()
13 | * metalsmith.use(metadata({ 'config': 'src/config.json' }))
14 | * // inside metalsmith.directory()
15 | * metalsmith.use(metadata({ 'config': 'config.json' }))
16 | * // target a keypath
17 | * metalsmith.use(metadata({ 'config.nav.items': 'navitems.yaml' }))
18 | * ```
19 | */
20 | declare function metadata(options?: Options): Metalsmith.Plugin;
21 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@metalsmith/metadata",
3 | "version": "0.3.0",
4 | "description": "A metalsmith plugin to load global metadata from files and directories",
5 | "keywords": [
6 | "metalsmith-plugin",
7 | "metalsmith",
8 | "metadata",
9 | "yaml",
10 | "json",
11 | "toml",
12 | "static-site"
13 | ],
14 | "homepage": "https://github.com/metalsmith/metadata#readme",
15 | "repository": {
16 | "type": "git",
17 | "url": "git+https://github.com/metalsmith/metadata.git"
18 | },
19 | "license": "MIT",
20 | "source": "src/index.js",
21 | "main": "lib/index.cjs",
22 | "module": "lib/index.js",
23 | "type": "module",
24 | "exports": {
25 | "import": "./lib/index.js",
26 | "require": "./lib/index.cjs"
27 | },
28 | "directories": {
29 | "lib": "lib",
30 | "test": "test"
31 | },
32 | "files": [
33 | "lib"
34 | ],
35 | "types": "lib/index.d.ts",
36 | "scripts": {
37 | "changelog": "auto-changelog -u date --commit-limit false --ignore-commit-pattern '^((dev|chore|ci):|Release)'",
38 | "coverage": "npm test && c8 report --reporter=text-lcov > ./coverage.info",
39 | "format": "prettier --write \"**/*.{yml,md,js,json}\"",
40 | "format:check": "prettier --list-different \"**/*.{yml,md,js,json}\"",
41 | "lint": "eslint --cache --fix \"{src,test}/**/*.js\"",
42 | "lint:check": "eslint --cache --fix-dry-run \"{src,test}/**/*.js\"",
43 | "release": "release-it .",
44 | "build": "microbundle --target node -f cjs,esm --strict --generateTypes=false",
45 | "test": "c8 mocha -t 5000"
46 | },
47 | "dependencies": {
48 | "deepmerge": "^4.3.1",
49 | "js-yaml": "^4.1.0"
50 | },
51 | "devDependencies": {
52 | "auto-changelog": "^2.3.0",
53 | "c8": "^7.14.0",
54 | "eslint": "^8.42.0",
55 | "eslint-config-prettier": "^8.8.0",
56 | "eslint-plugin-import": "^2.27.5",
57 | "eslint-plugin-n": "^16.0.0",
58 | "metalsmith": "^2.6.0",
59 | "microbundle": "^0.15.1",
60 | "mocha": "^10.2.0",
61 | "prettier": "^2.8.8",
62 | "release-it": "^15.11.0"
63 | },
64 | "peerDependencies": {
65 | "metalsmith": "^2.5.0",
66 | "toml": "^3.0.0"
67 | },
68 | "peerDependenciesMeta": {
69 | "toml": {
70 | "optional": true
71 | }
72 | },
73 | "engines": {
74 | "node": ">=14.14.0"
75 | },
76 | "publishConfig": {
77 | "access": "public"
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import { promises } from 'fs'
2 | import merge from 'deepmerge'
3 | import yaml from 'js-yaml'
4 | import { relative, extname, basename, join } from 'path'
5 | import module, { createRequire } from 'module'
6 |
7 | const { readdir, readFile } = promises
8 | let toml
9 |
10 | // support for dynamic imports landed in Node 13.2.0, and was available with --experimental-modules flag in 12.0.0
11 | // ideally all the loaders should be refactored to be async, and loaded only when the plugin runs
12 | const req = module.require || createRequire(import.meta.url)
13 |
14 | try {
15 | toml = req('toml')
16 | } catch (err) {
17 | toml = () => {
18 | throw new Error('To use toml you must install it first, run "npm i toml"')
19 | }
20 | }
21 |
22 | /**
23 | * Supported metadata parsers.
24 | */
25 | const parsers = {
26 | '.json': JSON.parse,
27 | '.yaml': yaml.load,
28 | '.yml': yaml.load,
29 | '.toml': toml.parse
30 | }
31 | const extglob = `**/*{${Object.keys(parsers).join(',')}}`
32 |
33 | /**
34 | * @typedef Options
35 | * @property {String} key
36 | */
37 |
38 | /** @type {Options} */
39 | const defaults = {}
40 |
41 | /**
42 | * Normalize plugin options
43 | * @param {Options} [options]
44 | * @returns {Object}
45 | */
46 | function normalizeOptions(options) {
47 | return Object.assign({}, defaults, options || {})
48 | }
49 |
50 | /**
51 | * A Metalsmith plugin to load global metadata from files
52 | *
53 | * @example
54 | * ```js
55 | * // inside metalsmith.source()
56 | * metalsmith.use(metadata({ 'config': 'src/config.json' }))
57 | * // inside metalsmith.directory()
58 | * metalsmith.use(metadata({ 'config': 'config.json' }))
59 | * // target a keypath
60 | * metalsmith.use(metadata({ 'config.nav.items': 'navitems.yaml' }))
61 | * ```
62 | * @param {Options} options
63 | * @returns {import('metalsmith').Plugin}
64 | */
65 | function metadata(options = {}) {
66 | options = normalizeOptions(options)
67 | if (Object.keys(options).length === 0) {
68 | return function metadata(files, metalsmith, done) {
69 | const debug = metalsmith.debug('@metalsmith/metadata')
70 | debug.warn('No metadata files to process, skipping.')
71 | done()
72 | }
73 | }
74 |
75 | let meta
76 |
77 | return function metadata(files, metalsmith, done) {
78 | const filePromises = []
79 | const dirPromises = []
80 | const debug = metalsmith.debug('@metalsmith/metadata')
81 | debug('Running with options: %O', options)
82 |
83 | // the same metalsmith instance always returns the same metadata object,
84 | // if it is metalsmith.use'd twice, or the pipeline is rerun with metalsmith.watch
85 | // the data has already been loaded successfully
86 | if (meta === metalsmith.metadata()) {
87 | debug('Detected repeat run, skipping.')
88 | done()
89 | return
90 | }
91 |
92 | // get metalsmith source directory
93 | const relpath = (path, root) => relative(root || metalsmith.directory(), metalsmith.path(path))
94 |
95 | // fast in-source error handling first
96 | for (const filepath of Object.values(options)) {
97 | const ext = extname(basename(filepath))
98 | const srcPath = relpath(filepath, metalsmith.source())
99 |
100 | if (ext) {
101 | if (!metalsmith.match(extglob, filepath).length) {
102 | done(new Error(`Unsupported data format "${ext}" for entry "${filepath}"`))
103 | }
104 | if (!srcPath.startsWith('..') && !Object.prototype.hasOwnProperty.call(files, srcPath)) {
105 | done(new Error('No matching file found for entry "' + relpath(filepath) + '"'))
106 | }
107 | }
108 | }
109 |
110 | // create array with all option values relative to metalsmith directory
111 | Object.entries(options).forEach(([dest, filepath]) => {
112 | const srcPath = relpath(filepath, metalsmith.source())
113 | const absPath = metalsmith.path(filepath)
114 | const ext = extname(basename(srcPath))
115 |
116 | // it's local
117 | if (!srcPath.startsWith('..')) {
118 | // it's a single file
119 | if (ext) {
120 | filePromises.push(
121 | Promise.resolve({
122 | path: srcPath,
123 | key: dest,
124 | file: files[srcPath]
125 | })
126 | )
127 |
128 | // it's a directory
129 | } else {
130 | const matches = metalsmith.match(`${srcPath}/${extglob}`)
131 | if (!matches.length) {
132 | debug('No matching files found for entry "%s"', filepath)
133 | }
134 | matches.forEach((filepath) => {
135 | filePromises.push(
136 | Promise.resolve({
137 | path: filepath,
138 | key: dest,
139 | file: files[filepath]
140 | })
141 | )
142 | })
143 | }
144 | // it's external
145 | } else {
146 | // it's a single file
147 | if (extname(filepath)) {
148 | const fileread = readFile(absPath)
149 | .then((file) => ({
150 | path: relpath(filepath),
151 | key: dest,
152 | file: { contents: file }
153 | }))
154 | .catch(() =>
155 | done(new Error('No matching file found for entry "' + relpath(filepath) + '"'))
156 | )
157 | filePromises.push(fileread)
158 | // it's a directory
159 | } else {
160 | // for ext dirs, just push the file listings, flatten them afterwards
161 | dirPromises.push(
162 | readdir(absPath)
163 | .then((filelist) => {
164 | const matches = metalsmith.match(extglob, filelist)
165 | if (!matches.length) {
166 | debug('No matching files found for entry "%s"', relpath(filepath))
167 | }
168 | return matches.map((f) => ({
169 | path: join(relpath(absPath), f),
170 | key: dest
171 | }))
172 | })
173 | .catch((err) => done(err))
174 | )
175 | }
176 | }
177 | })
178 |
179 | // flatten file listings first, these are relatively inexpensive
180 | Promise.all(dirPromises)
181 | .then((filelists) => {
182 | filelists.forEach((filelist) => {
183 | const matches = metalsmith.match(
184 | extglob,
185 | filelist.map((f) => f.path)
186 | )
187 | filePromises.push(
188 | ...matches.map((filepath) =>
189 | readFile(metalsmith.path(filepath))
190 | .then((file) => ({
191 | path: filepath,
192 | key: filelist.find((f) => f.path === filepath).key,
193 | file: { contents: file }
194 | }))
195 | .catch((err) => done(err))
196 | )
197 | )
198 | })
199 | return Promise.all(filePromises)
200 | })
201 | .then((allFiles) => {
202 | let newMetadata = {}
203 | allFiles.forEach(({ key, file, path }) => {
204 | let parsed
205 | try {
206 | const parser = parsers[extname(path)]
207 | parsed = parser(file.contents.toString())
208 | } catch (err) {
209 | done(
210 | err.message.startsWith('To use toml')
211 | ? err
212 | : new Error('malformed data in "' + path + '"')
213 | )
214 | }
215 |
216 | const newMeta = {},
217 | keypath = key.split('.')
218 | let current = newMeta
219 | while (keypath.length) {
220 | const k = keypath.shift()
221 | current[k] = keypath.length ? {} : parsed
222 | current = current[k]
223 | }
224 | newMetadata = merge(newMetadata, newMeta)
225 | debug('Adding metadata from file "%s" at key "%s": %O', path, key, parsed)
226 |
227 | if (delete files[path]) {
228 | debug('Removed metadata file at "%s"', path)
229 | }
230 | })
231 |
232 | meta = metalsmith.metadata()
233 | const merged = merge(meta, newMetadata)
234 | metalsmith.metadata(merged)
235 | done()
236 | })
237 | .catch(done)
238 | }
239 | }
240 |
241 | export default metadata
242 |
--------------------------------------------------------------------------------
/test/fixtures/array-merge/src/metadata/first.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "id": 1
4 | }
5 | ]
--------------------------------------------------------------------------------
/test/fixtures/array-merge/src/metadata/second.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "id": 2
4 | }
5 | ]
--------------------------------------------------------------------------------
/test/fixtures/deep-nested/src/path/path/data.yaml:
--------------------------------------------------------------------------------
1 | string: 'string'
--------------------------------------------------------------------------------
/test/fixtures/duplicate/src/data.json:
--------------------------------------------------------------------------------
1 | {
2 | "string": "string"
3 | }
--------------------------------------------------------------------------------
/test/fixtures/duplicate/src/data2.json:
--------------------------------------------------------------------------------
1 | {
2 | "string": "string2"
3 | }
--------------------------------------------------------------------------------
/test/fixtures/external-file/data/test.json:
--------------------------------------------------------------------------------
1 | {
2 | "json": "string"
3 | }
--------------------------------------------------------------------------------
/test/fixtures/external-file/src/main.json:
--------------------------------------------------------------------------------
1 | {
2 | "string": "string"
3 | }
--------------------------------------------------------------------------------
/test/fixtures/external-folder/data/test.json:
--------------------------------------------------------------------------------
1 | {
2 | "json": "string"
3 | }
--------------------------------------------------------------------------------
/test/fixtures/external-folder/data/test2.yaml:
--------------------------------------------------------------------------------
1 | yaml:
2 | bool: true
--------------------------------------------------------------------------------
/test/fixtures/external-folder/src/main.json:
--------------------------------------------------------------------------------
1 | {
2 | "string": "string"
3 | }
--------------------------------------------------------------------------------
/test/fixtures/ignored/src/ignored.yaml:
--------------------------------------------------------------------------------
1 | loaded: true
--------------------------------------------------------------------------------
/test/fixtures/ignored/src/implicitly-ignored.yaml:
--------------------------------------------------------------------------------
1 | implicitly_loaded: true
--------------------------------------------------------------------------------
/test/fixtures/ignored/src/not-ignored.json:
--------------------------------------------------------------------------------
1 | {
2 | "not_ignored": true
3 | }
--------------------------------------------------------------------------------
/test/fixtures/incorrect-path/src/data.json:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/metalsmith/metadata/6cbf8ede60b29e17e9553264e8569281373eb089/test/fixtures/incorrect-path/src/data.json
--------------------------------------------------------------------------------
/test/fixtures/json/src/data.json:
--------------------------------------------------------------------------------
1 | {
2 | "string": "string"
3 | }
--------------------------------------------------------------------------------
/test/fixtures/malformed/src/data.json:
--------------------------------------------------------------------------------
1 | {
2 | "str
3 | }
--------------------------------------------------------------------------------
/test/fixtures/nested-directories/data/nested/test.yaml:
--------------------------------------------------------------------------------
1 | bool: true
--------------------------------------------------------------------------------
/test/fixtures/nested-directories/data/test.json:
--------------------------------------------------------------------------------
1 | {
2 | "json": "string"
3 | }
--------------------------------------------------------------------------------
/test/fixtures/nested-directories/src/main.json:
--------------------------------------------------------------------------------
1 | {
2 | "string": "string"
3 | }
--------------------------------------------------------------------------------
/test/fixtures/nested-keypath/src/metadata.yml:
--------------------------------------------------------------------------------
1 | string: string
--------------------------------------------------------------------------------
/test/fixtures/nested/src/path/data.yaml:
--------------------------------------------------------------------------------
1 | string: 'string'
--------------------------------------------------------------------------------
/test/fixtures/object-merge/src/metadata/metatags.json:
--------------------------------------------------------------------------------
1 | {
2 | "metatags": [
3 | {"name": "description", "value": "Hello world"}
4 | ]
5 | }
--------------------------------------------------------------------------------
/test/fixtures/object-merge/src/metadata/navitem.yml:
--------------------------------------------------------------------------------
1 | navitems:
2 | - uri: /
3 | label: Home
--------------------------------------------------------------------------------
/test/fixtures/object-merge/src/metadata/navitems.json:
--------------------------------------------------------------------------------
1 | {
2 | "navitems": [
3 | { "uri": "/about", "label": "About" }
4 | ]
5 | }
--------------------------------------------------------------------------------
/test/fixtures/toml/src/data.toml:
--------------------------------------------------------------------------------
1 | string = "string"
--------------------------------------------------------------------------------
/test/fixtures/unsupported-ext/src/data.txt:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/metalsmith/metadata/6cbf8ede60b29e17e9553264e8569281373eb089/test/fixtures/unsupported-ext/src/data.txt
--------------------------------------------------------------------------------
/test/fixtures/yaml/src/data.yaml:
--------------------------------------------------------------------------------
1 | string: 'string'
--------------------------------------------------------------------------------
/test/index.js:
--------------------------------------------------------------------------------
1 | /* eslint-env node, mocha */
2 |
3 | import assert from 'node:assert'
4 | import { resolve, dirname, join } from 'node:path'
5 | import { readFileSync } from 'node:fs'
6 | import { fileURLToPath } from 'node:url'
7 | import Metalsmith from 'metalsmith'
8 | import metadata from '../src/index.js'
9 |
10 | const __dirname = dirname(fileURLToPath(import.meta.url))
11 | const { name } = JSON.parse(readFileSync(resolve(__dirname, '../package.json'), 'utf-8'))
12 |
13 | describe('@metalsmith/metadata', function () {
14 | it('should export a named plugin function matching package.json name', function () {
15 | const namechars = name.split('/')[1]
16 | const camelCased = namechars.split('').reduce((str, char, i) => {
17 | str += namechars[i - 1] === '-' ? char.toUpperCase() : char === '-' ? '' : char
18 | return str
19 | }, '')
20 | assert.deepStrictEqual(metadata().name, camelCased)
21 | })
22 |
23 | it('should parse JSON', function (done) {
24 | const m = Metalsmith('test/fixtures/json')
25 | .env('DEBUG', process.env.DEBUG)
26 | .use(metadata({ file: 'src/data.json' }))
27 | m.build(function (err) {
28 | if (err) return done(err)
29 | assert.deepStrictEqual(m.metadata().file, { string: 'string' })
30 | done()
31 | })
32 | })
33 |
34 | it('should parse YAML', function (done) {
35 | const m = Metalsmith('test/fixtures/yaml')
36 | .env('DEBUG', process.env.DEBUG)
37 | .use(metadata({ file: 'src/data.yaml' }))
38 | m.build(function (err) {
39 | if (err) return done(err)
40 | assert.deepStrictEqual(m.metadata().file, { string: 'string' })
41 | done()
42 | })
43 | })
44 |
45 | it('should parse TOML', function (done) {
46 | // run this test locally after running "npm i toml" & removing this.skip
47 | this.skip()
48 | const m = Metalsmith('test/fixtures/toml')
49 | .env('DEBUG', process.env.DEBUG)
50 | .use(metadata({ file: 'src/data.toml' }))
51 | m.build(function (err) {
52 | if (err) return done(err)
53 | assert.deepStrictEqual(m.metadata().file, { string: 'string' })
54 | done()
55 | })
56 | })
57 |
58 | it('should resolve relative paths to metalsmith.directory()', function (done) {
59 | const m = Metalsmith('test/fixtures/yaml')
60 | .env('DEBUG', process.env.DEBUG)
61 | .use(metadata({ file: './src/data.yaml' }))
62 | m.build(function (err) {
63 | if (err) return done(err)
64 | assert.deepStrictEqual(m.metadata().file, { string: 'string' })
65 | done()
66 | })
67 | })
68 |
69 | it('should resolve absolute paths to metalsmith.directory()', function (done) {
70 | const m = Metalsmith('test/fixtures/yaml')
71 | m.env('DEBUG', process.env.DEBUG)
72 | m.use(metadata({ file: m.directory() + '/src/data.yaml' }))
73 | m.build(function (err) {
74 | if (err) return done(err)
75 | assert.deepStrictEqual(m.metadata().file, { string: 'string' })
76 | done()
77 | })
78 | })
79 |
80 | it("should ignore metalsmith.ignore'd files", function (done) {
81 | const m = Metalsmith('test/fixtures/ignored')
82 | const initialMeta = { ignored: { loaded: false }, implicitly_ignored: { loaded: false } }
83 |
84 | m.env('DEBUG', process.env.DEBUG)
85 | .ignore('*.yaml')
86 | .metadata(initialMeta)
87 | .use(
88 | metadata({
89 | ignored: 'src/ignored.yaml'
90 | })
91 | )
92 | .process((err) => {
93 | try {
94 | assert.deepStrictEqual(m.metadata(), initialMeta)
95 | assert(err instanceof Error)
96 | assert.strictEqual(
97 | err.message,
98 | `No matching file found for entry "${join('src', 'ignored.yaml')}"`
99 | )
100 | done()
101 | } catch (err) {
102 | done(err)
103 | }
104 | })
105 | })
106 |
107 | it('should parse a file even if the key exists if the file is in the bundle', function (done) {
108 | const m = Metalsmith('test/fixtures/duplicate')
109 | .env('DEBUG', process.env.DEBUG)
110 | .use(metadata({ file: 'src/data.json' }))
111 | .use(metadata({ file: 'src/data2.json' }))
112 | m.build(function (err) {
113 | if (err) return done(err)
114 | assert.deepStrictEqual(m.metadata().file, { string: 'string2' })
115 | done()
116 | })
117 | })
118 |
119 | it('should parse nested path', function (done) {
120 | const m = Metalsmith('test/fixtures/nested')
121 | .env('DEBUG', process.env.DEBUG)
122 | .use(metadata({ file: 'src/path/data.yaml' }))
123 | m.build(function (err) {
124 | if (err) return done(err)
125 | try {
126 | assert.deepStrictEqual(m.metadata().file, { string: 'string' })
127 | done()
128 | } catch (err) {
129 | done(err)
130 | }
131 | })
132 | })
133 |
134 | it('should parse deep nested path', function (done) {
135 | const m = Metalsmith('test/fixtures/deep-nested')
136 | .env('DEBUG', process.env.DEBUG)
137 | .use(metadata({ file: 'src/path/path/data.yaml' }))
138 | m.build(function (err) {
139 | if (err) return done(err)
140 | assert.deepStrictEqual(m.metadata().file, { string: 'string' })
141 | done()
142 | })
143 | })
144 |
145 | it('should allow merging files into an array', function (done) {
146 | const m = Metalsmith('test/fixtures/array-merge')
147 | .env('DEBUG', process.env.DEBUG)
148 | .use(metadata({ arr: 'src/metadata' }))
149 | m.build(function (err) {
150 | if (err) return done(err)
151 | assert.deepStrictEqual(m.metadata().arr, [{ id: 1 }, { id: 2 }])
152 | done()
153 | })
154 | })
155 |
156 | it('should allow merging files into a nested object', function (done) {
157 | const m = Metalsmith('test/fixtures/object-merge')
158 | .env('DEBUG', process.env.DEBUG)
159 | .use(metadata({ config: 'src/metadata' }))
160 | m.build(function (err) {
161 | if (err) return done(err)
162 | assert.deepStrictEqual(m.metadata().config, {
163 | metatags: [{ name: 'description', value: 'Hello world' }],
164 | navitems: [
165 | { uri: '/', label: 'Home' },
166 | { uri: '/about', label: 'About' }
167 | ]
168 | })
169 | done()
170 | })
171 | })
172 |
173 | it('should allow merging metadata into a nested keypath', function (done) {
174 | const m = Metalsmith('test/fixtures/nested-keypath')
175 | .env('DEBUG', process.env.DEBUG)
176 | .use(metadata({ 'config.metadata': 'src/metadata.yml' }))
177 | m.build(function (err) {
178 | if (err) return done(err)
179 | assert.deepStrictEqual(m.metadata().config, { metadata: { string: 'string' } })
180 | done()
181 | })
182 | })
183 |
184 | it('should support nested directories through multiple entries', function (done) {
185 | const m = Metalsmith('test/fixtures/nested-directories')
186 | .env('DEBUG', process.env.DEBUG)
187 | .use(
188 | metadata({
189 | 'config.metadata.extra': 'data',
190 | 'config.metadata.extra.yaml': 'data/nested'
191 | })
192 | )
193 | m.build(function (err) {
194 | if (err) return done(err)
195 | assert.deepStrictEqual(m.metadata().config, {
196 | metadata: { extra: { json: 'string', yaml: { bool: true } } }
197 | })
198 | done()
199 | })
200 | })
201 |
202 | it('should handle repeat runs on the same Metalsmith metadata (no duplicates)', function (done) {
203 | const m = Metalsmith('test/fixtures/object-merge')
204 | const plugin = metadata({ config: 'src/metadata' })
205 |
206 | m.metadata({
207 | config: {
208 | navitems: [{ uri: '/products', label: 'products' }]
209 | }
210 | })
211 | m.use(plugin)
212 | m.env('DEBUG', process.env.DEBUG)
213 | m.process()
214 | .then(() => m.process())
215 | .then(() => {
216 | assert.deepStrictEqual(m.metadata().config, {
217 | metatags: [{ name: 'description', value: 'Hello world' }],
218 | navitems: [
219 | { uri: '/products', label: 'products' },
220 | { uri: '/', label: 'Home' },
221 | { uri: '/about', label: 'About' }
222 | ]
223 | })
224 | done()
225 | })
226 | .catch(done)
227 | })
228 |
229 | it("should handle single runs on different Metalsmith instances' metadata", function (done) {
230 | const plugin = metadata({ config: 'src/metadata' })
231 |
232 | function singleRun() {
233 | const m = Metalsmith('test/fixtures/object-merge')
234 | m.metadata({
235 | config: {
236 | navitems: [{ uri: '/products', label: 'products' }]
237 | }
238 | })
239 | m.use(plugin)
240 | m.env('DEBUG', '@metalsmith/metadata')
241 | return m.process().then(() => m.metadata())
242 | }
243 |
244 | Promise.all([singleRun(), singleRun()])
245 | .then(([meta1, meta2]) => {
246 | const expected = {
247 | metatags: [{ name: 'description', value: 'Hello world' }],
248 | navitems: [
249 | { uri: '/products', label: 'products' },
250 | { uri: '/', label: 'Home' },
251 | { uri: '/about', label: 'About' }
252 | ]
253 | }
254 | assert.deepStrictEqual(meta1.config, expected, 'meta1')
255 | assert.deepStrictEqual(meta2.config, expected, 'meta2')
256 | done()
257 | })
258 | .catch(done)
259 | })
260 | })
261 |
262 | describe('External metadata', function () {
263 | it('should add an external file to metadata', function (done) {
264 | const m = Metalsmith('test/fixtures/external-file')
265 | .env('DEBUG', process.env.DEBUG)
266 | .use(metadata({ data: 'data/test.json' }))
267 | m.build(function (err) {
268 | if (err) return done(err)
269 | assert.deepStrictEqual(m.metadata().data, { json: 'string' })
270 | done()
271 | })
272 | })
273 |
274 | it('should add all external files in a folder', function (done) {
275 | const m = Metalsmith('test/fixtures/external-folder')
276 | .env('DEBUG', process.env.DEBUG)
277 | .use(metadata({ data: 'data' }))
278 | m.build(function (err) {
279 | if (err) return done(err)
280 | assert.deepStrictEqual(m.metadata().data, { json: 'string', yaml: { bool: true } })
281 | done()
282 | })
283 | })
284 | })
285 |
286 | describe('Error handling', function () {
287 | it('should error for unsupported extensions', function (done) {
288 | Metalsmith('test/fixtures/unsupported-ext')
289 | .env('DEBUG', process.env.DEBUG)
290 | .use(metadata({ file: 'src/data.txt' }))
291 | .build(function (err) {
292 | assert(err)
293 | assert(err.message.startsWith('Unsupported data format'))
294 | done()
295 | })
296 | })
297 |
298 | it('should error when TOML is used but not installed', function (done) {
299 | // run this test locally by removing this.skip & running "npm remove toml"
300 | this.skip()
301 | const Metalsmith = import('metalsmith').then(() => {
302 | Metalsmith('test/fixtures/toml')
303 | .env('DEBUG', process.env.DEBUG)
304 | .use(metadata({ file: 'src/data.toml' }))
305 | .build(function (err) {
306 | if (!err) done(new Error('No error was thrown'))
307 | assert(err)
308 | assert(err.message.startsWith('To use toml you must install it first'))
309 | done()
310 | })
311 | })
312 | })
313 |
314 | it('should error for malformed data', function (done) {
315 | Metalsmith('test/fixtures/malformed')
316 | .env('DEBUG', process.env.DEBUG)
317 | .use(metadata({ file: 'src/data.json' }))
318 | .build(function (err) {
319 | if (!err) done(new Error('No error was thrown'))
320 | assert(err.message.startsWith('malformed data'))
321 | done()
322 | })
323 | })
324 |
325 | it('should error for single-file entries that are not found', function (done) {
326 | Metalsmith('test/fixtures/incorrect-path')
327 | .env('DEBUG', process.env.DEBUG)
328 | .use(metadata({ file: 'src/data-incorrect.json' }))
329 | .build(function (err) {
330 | if (!err) done(new Error('No error was thrown'))
331 | assert(err.message.startsWith('No matching file found for entry'))
332 | done()
333 | })
334 | })
335 |
336 | it('should error for single-file entries that are not found (outside source dir)', function (done) {
337 | Metalsmith('test/fixtures/incorrect-path')
338 | .env('DEBUG', process.env.DEBUG)
339 | .use(metadata({ file: 'data-incorrect.json' }))
340 | .build(function (err) {
341 | if (!err) done(new Error('No error was thrown'))
342 | assert(err.message.startsWith('No matching file found for entry'))
343 | done()
344 | })
345 | })
346 | })
347 |
--------------------------------------------------------------------------------