├── .gitignore ├── .npmignore ├── .c8rc.json ├── babel.config.json ├── tests ├── fixtures │ ├── output.json │ ├── files │ │ ├── _fonts.scss │ │ └── app.js │ └── options.js └── unit │ └── test.js ├── eslint.config.js ├── .github └── workflows │ ├── deploy.yml │ └── verify.yml ├── LICENSE.md ├── package.json ├── README.md └── lib └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | .npm-debug.log 2 | reports 3 | tmp 4 | node_modules 5 | .jshint* 6 | bundle.js 7 | *.tgz 8 | package 9 | dist 10 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .travis.yml 3 | *.tgz 4 | .c8* 5 | .eslint* 6 | .git* 7 | babel* 8 | reports 9 | tests 10 | tmp 11 | package 12 | .jshint* 13 | -------------------------------------------------------------------------------- /.c8rc.json: -------------------------------------------------------------------------------- 1 | { 2 | "reporter": ["lcov", "json"], 3 | "reports-dir": "reports/coverage", 4 | "exclude": ["tmp", "reports", "node_modules", "tests", ".github"] 5 | } -------------------------------------------------------------------------------- /babel.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | "targets": { 7 | "node": "18" 8 | } 9 | } 10 | ] 11 | ] 12 | } -------------------------------------------------------------------------------- /tests/fixtures/output.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest": { 3 | "id": 123, 4 | "version": "0.1.0", 5 | "whatever": "string I want" 6 | }, 7 | "assets": [ 8 | "//fonts.gstatic.com/s/sourcesanspro/v9/ODelI1aHBYDBqgeIAH2zlNV_2ngZ8dMf8fLgjYEouxg.woff2", 9 | "//fonts.gstatic.com/s/sourcesanspro/v9/ODelI1aHBYDBqgeIAH2zlBM0YzuT7MdOe03otPbuUS0.woff", 10 | "//fonts.gstatic.com/s/sourcesanspro/v9/ODelI1aHBYDBqgeIAH2zlEY6Fu39Tt9XkmtSosaMoEA.ttf" 11 | ], 12 | "apis": [ 13 | "/_api" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import js from '@eslint/js'; 2 | import globals from 'globals'; 3 | 4 | export default [{ 5 | ignores: [ 6 | 'tmp/**', 7 | 'dist/**', 8 | 'node_modules/**', 9 | 'reports/**', 10 | 'tests/fixtures/**' 11 | ] 12 | }, { 13 | name: 'node', 14 | ...js.configs.recommended, 15 | languageOptions: { 16 | globals: { 17 | ...globals.node 18 | } 19 | } 20 | }, { 21 | name: 'tests', 22 | files: [ 23 | 'tests/**' 24 | ], 25 | languageOptions: { 26 | globals: { 27 | ...globals.mocha 28 | } 29 | } 30 | }] -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy 2 | on: 3 | push: 4 | branches: [ master ] 5 | 6 | jobs: 7 | deploy: 8 | runs-on: ubuntu-24.04 9 | permissions: 10 | contents: read 11 | id-token: write 12 | steps: 13 | - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # 6.0.1 14 | - uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # 6.1.0 15 | with: 16 | node-version: '24.x' 17 | registry-url: 'https://registry.npmjs.org' 18 | - name: Update npm 19 | run: npm install -g npm@latest # ensure npm 11.5.1 or later 20 | - run: npm ci 21 | - name: Verify 22 | run: npm run lint && npm test 23 | - name: Publish 24 | if: ${{ success() }} 25 | run: npm publish --access public -------------------------------------------------------------------------------- /.github/workflows/verify.yml: -------------------------------------------------------------------------------- 1 | name: Verify 2 | on: 3 | pull_request: 4 | branches: [ master ] 5 | 6 | jobs: 7 | verify: 8 | 9 | runs-on: ubuntu-24.04 10 | 11 | strategy: 12 | matrix: 13 | node-version: [20, 22, 24] 14 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 15 | 16 | steps: 17 | - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # 6.0.1 18 | - name: Use Node.js ${{ matrix.node-version }} 19 | uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # 6.1.0 20 | with: 21 | node-version: ${{ matrix.node-version }} 22 | - run: npm ci 23 | - name: Lint, Test, And Coverage 24 | run: npm run lint && npm run cover 25 | - name: Coverage Upload 26 | if: ${{ success() }} 27 | uses: coverallsapp/github-action@master 28 | with: 29 | github-token: ${{ secrets.GITHUB_TOKEN }} 30 | path-to-lcov: ./reports/coverage/lcov.info -------------------------------------------------------------------------------- /tests/fixtures/files/_fonts.scss: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2016 - 2025 Alex Grant (@localnerve), LocalNerve LLC 3 | // Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms. 4 | // 5 | // font styles 6 | // ------------------------- 7 | 8 | @font-face { 9 | font-family: "Source Sans Pro"; 10 | src: local("Source Sans Pro"), local("SourceSansPro-Regular"), 11 | url("//fonts.gstatic.com/s/sourcesanspro/v9/ODelI1aHBYDBqgeIAH2zlNV_2ngZ8dMf8fLgjYEouxg.woff2") format("woff2"), 12 | url("//fonts.gstatic.com/s/sourcesanspro/v9/ODelI1aHBYDBqgeIAH2zlBM0YzuT7MdOe03otPbuUS0.woff") format("woff"), 13 | url("//fonts.gstatic.com/s/sourcesanspro/v9/ODelI1aHBYDBqgeIAH2zlEY6Fu39Tt9XkmtSosaMoEA.ttf") format("truetype"); 14 | /* unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215, U+E0FF, U+EFFD, U+F000; */ 15 | font-weight: 400; 16 | font-style: normal; 17 | } 18 | 19 | .fonts-loaded body { 20 | font-family: "Source Sans Pro", sans-serif; 21 | } 22 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2016 - 2025 Alex Grant (@localnerve), LocalNerve, LLC 3 | 4 | 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: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | 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. 9 | -------------------------------------------------------------------------------- /tests/fixtures/options.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2016 - 2025 Alex Grant (@localnerve), LocalNerve LLC 3 | * Copyrights licensed under the MIT License. See the accompanying LICENSE file 4 | * for terms. 5 | */ 6 | /*eslint no-console:0 */ 7 | import url from 'node:url'; 8 | import path from 'node:path'; 9 | 10 | const thisDirname = url.fileURLToPath(new URL('.', import.meta.url)); 11 | 12 | export default { 13 | output: { 14 | manifest: { 15 | id: 123, 16 | version: '0.1.0', 17 | whatever: 'string I want' 18 | }, 19 | file: path.join(thisDirname, './tmp/test_out.json') 20 | }, 21 | input: { 22 | assets: [{ 23 | file: path.join(thisDirname, './files/_fonts.scss'), 24 | captures: [{ 25 | global: true, 26 | matchIndex: 1, 27 | re: /url\(([^)]+)\)/ig 28 | }] 29 | }], 30 | apis: [{ 31 | file: path.join(thisDirname, './files/app.js'), 32 | captures: [{ 33 | global: false, 34 | matchIndex: 1, 35 | re: /xhrPath\s*:\s*(?:'|")([^'"]+)/ 36 | }] 37 | }] 38 | }, 39 | logger: console.log 40 | } 41 | -------------------------------------------------------------------------------- /tests/fixtures/files/app.js: -------------------------------------------------------------------------------- 1 | /*** 2 | * Copyright (c) 2016 - 2025 Alex Grant (@localnerve), LocalNerve LLC 3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms. 4 | * 5 | * Assemble the Fluxible app. 6 | */ 7 | 'use strict'; 8 | 9 | var debug = require('debug')('Example:App'); 10 | var FluxibleApp = require('fluxible'); 11 | var fetchrPlugin = require('fluxible-plugin-fetchr'); 12 | var ApplicationStore = require('./stores/ApplicationStore'); 13 | var ContentStore = require('./stores/ContentStore'); 14 | var ContactStore = require('./stores/ContactStore'); 15 | var BackgroundStore = require('./stores/BackgroundStore'); 16 | var RouteStore = require('./stores/RouteStore'); 17 | var ModalStore = require('./stores/ModalStore'); 18 | 19 | debug('Creating FluxibleApp'); 20 | var app = new FluxibleApp({ 21 | component: require('./components/Application.jsx') 22 | }); 23 | 24 | debug('Adding Plugins'); 25 | app.plug(fetchrPlugin({ xhrPath: '/_api' })); 26 | 27 | debug('Registering Stores'); 28 | app.registerStore(ApplicationStore); 29 | app.registerStore(ContentStore); 30 | app.registerStore(ContactStore); 31 | app.registerStore(BackgroundStore); 32 | app.registerStore(RouteStore); 33 | app.registerStore(ModalStore); 34 | 35 | module.exports = app; 36 | -------------------------------------------------------------------------------- /tests/unit/test.js: -------------------------------------------------------------------------------- 1 | /*** 2 | * Copyright (c) 2016 - 2025 Alex Grant (@localnerve), LocalNerve LLC 3 | * Copyrights licensed under the MIT License. See the accompanying LICENSE file 4 | * for terms. 5 | */ 6 | 7 | import fs from 'node:fs'; 8 | import path from 'node:path'; 9 | import { expect } from 'chai'; 10 | import inputFixture from '../fixtures/options.js'; 11 | import cannibalizr from '../../lib/index.js'; 12 | 13 | describe('cannibalizr', () => { 14 | const outputJson = path.resolve('tests/fixtures/output.json'); 15 | const outputFixture = JSON.parse(fs.readFileSync(outputJson, { encoding: 'utf8' })); 16 | 17 | before('cannibalizr', () => { 18 | const outputDir = path.dirname(inputFixture.output.file); 19 | fs.mkdirSync(outputDir, { recursive: true }); 20 | }); 21 | 22 | beforeEach(() => { 23 | try { 24 | fs.unlinkSync(inputFixture.output.file); 25 | } catch (e) {} // eslint-disable-line 26 | }); 27 | 28 | it('should produce the expected output', done => { 29 | cannibalizr(inputFixture); 30 | 31 | fs.readFile(inputFixture.output.file, { 32 | encoding: 'utf8' 33 | }, (err, data) => { 34 | expect(data).to.eql(JSON.stringify(outputFixture)); 35 | done(err); 36 | }); 37 | }); 38 | 39 | it('should not run if bad input', () => { 40 | const badOptions = JSON.parse(JSON.stringify(inputFixture)); 41 | delete badOptions.output.file; 42 | 43 | expect(() => { 44 | cannibalizr(badOptions); 45 | }).to.throw(Error); 46 | 47 | expect(() => { 48 | fs.accessSync(inputFixture.output.file) 49 | }).to.throw(Error); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cannibalizr", 3 | "version": "5.0.1", 4 | "description": "Turns an old pile of rusty files into a new json data source", 5 | "main": "dist/index.js", 6 | "type": "module", 7 | "exports": { 8 | "import": "./dist/index.js", 9 | "require": "./dist/index.cjs", 10 | "default": "./dist/index.js" 11 | }, 12 | "scripts": { 13 | "prepublishOnly": "rimraf ./dist && babel ./lib/index.js -o ./dist/index.cjs && node -e 'require(\"fs\").copyFileSync(\"./lib/index.js\", \"./dist/index.js\");'", 14 | "test": "mocha ./tests/unit --recursive --reporter spec", 15 | "cover": "c8 -- npm test", 16 | "lint": "eslint .", 17 | "validate": "npm ls" 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "git+https://github.com/localnerve/cannibalizr.git" 22 | }, 23 | "keywords": [ 24 | "data", 25 | "capture", 26 | "regex", 27 | "strings", 28 | "multiple", 29 | "files", 30 | "json" 31 | ], 32 | "author": "Alex Grant (@localnerve)", 33 | "license": "MIT", 34 | "bugs": { 35 | "url": "https://github.com/localnerve/cannibalizr/issues" 36 | }, 37 | "homepage": "https://github.com/localnerve/cannibalizr#readme", 38 | "engines": { 39 | "node": ">=20" 40 | }, 41 | "devDependencies": { 42 | "@babel/cli": "^7.28.3", 43 | "@babel/core": "^7.28.5", 44 | "@babel/preset-env": "^7.28.5", 45 | "@eslint/js": "^9.39.1", 46 | "c8": "^10.1.3", 47 | "chai": "^6.2.1", 48 | "eslint": "^9.39.1", 49 | "globals": "^16.5.0", 50 | "mocha": "^11.7.5", 51 | "precommit-hook": "^3.0.0", 52 | "rimraf": "^6.1.2" 53 | }, 54 | "pre-commit": [ 55 | "lint", 56 | "test" 57 | ] 58 | } 59 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Cannibalizr 2 | 3 | [![npm version](https://badge.fury.io/js/cannibalizr.svg)](http://badge.fury.io/js/cannibalizr) 4 | ![Verify](https://github.com/localnerve/cannibalizr/workflows/Verify/badge.svg) 5 | [![Coverage Status](https://coveralls.io/repos/localnerve/cannibalizr/badge.svg?branch=master)](https://coveralls.io/r/localnerve/cannibalizr?branch=master) 6 | 7 | 8 | > A tool to pull strings from multiple files into a json file using regexes. 9 | 10 | ## What? 11 | > "Turns an old pile of rusty files into a shiny, new json data source!" 12 | 13 | This tool pulls select strings from multiple files and writes them to a 14 | structured json file. 15 | So, it allows you to use those files as input to something else, making them the 16 | single source of truth. 17 | 18 | *This is not the tool you are looking for.* 19 | 20 | **Use this as a last resort.** 21 | 22 | By nature, this is a brittle solution as the input files will (presumably) change 23 | over time, and inevitably evade your capturing regexes at some point. 24 | The usefulness of this tool depends on your regexes and the nature of the input 25 | they capture against (and other viable options available to you in your timeframe). 26 | 27 | Ideally, you should never have to do this, but sometimes, things are not ideal. 28 | However, if there are strings in your code that are NOT easy available in any 29 | other data form (**I'm looking at you, asset references in CSS**), this could be 30 | a useful way to keep DRY without overcomplicating things. 31 | 32 | ## Example using all options 33 | 34 | ```javascript 35 | import cannibalizr from 'cannibalizr'; 36 | 37 | cannibalizr({ 38 | output: { 39 | // manifest is an optional object with arbitrary data to store in the output 40 | manifest: { 41 | id: 123, 42 | version: '0.1.0', 43 | whatever: 'data I want here' 44 | } 45 | file: 'data.json' // the json output file path, always overwritten. 46 | }, 47 | input: { 48 | someIdentifier: [{ 49 | file: 'somefile.css', 50 | captures: [{ 51 | global: true, // true if the regex is global (/g) 52 | matchIndex: 1, // the match index to pull data from 53 | re: /url\(([^\)]+)\)/ig // the actual regex 54 | }] 55 | }], 56 | anotherIdentifier: [{ 57 | file: 'somefile.js', 58 | captures: [{ 59 | global: false, 60 | matchIndex: 1, 61 | re: /xhrPath\s*\:\s*(?:'|")([^'"]+)/ 62 | }] 63 | }] 64 | }, 65 | // A logging function. If omitted, nothing is logged. 66 | logger: console.log 67 | }); 68 | 69 | // data.json: 70 | { 71 | "manifest": { 72 | "id": 123, 73 | "version": "0.1.0", 74 | "whatever": "data I want here" 75 | }, 76 | "someIdentifier": [ 77 | "//fonts.gstatic.com/s/sourcesanspro/v9/guid.woff2", 78 | "//fonts.gstatic.com/s/sourcesanspro/v9/guid.woff", 79 | "//fonts.gstatic.com/s/sourcesanspro/v9/guid.ttf" 80 | ] 81 | "anotherIdentifier": [ 82 | "/api" 83 | ] 84 | } 85 | ``` 86 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | /*** 2 | * Copyright (c) 2016 - 2025 Alex Grant (@localnerve), LocalNerve LLC 3 | * Copyrights licensed under the MIT License. See the accompanying LICENSE file 4 | * for terms. 5 | */ 6 | import fs from 'node:fs'; 7 | export default cannibalizr; 8 | /** 9 | * Pull strings from multiple input files, write to structured json. 10 | * 11 | * json output file structure: 12 | * { 13 | * manifest: { 14 | * // any output manifest data supplied in options 15 | * }, 16 | * anyname: [ 17 | * 'some captured string', 18 | * 'another captured string' 19 | * ] 20 | * } 21 | * 22 | * @param {Object} options - Cannibalizr options. 23 | * @param {Object} options.input - input options. 24 | * @param {Object} options.input.anyname - At least one input is required, 25 | * use a name that makes sense for what is being captured. 26 | * @param {String} options.input.anyname.file - The path of the input file. 27 | * @param {Array} options.input.anyname.captures - The captures to run against 28 | * the input file. 29 | * @param {Boolean} options.input.anyname.captures.n.global - true if the regex 30 | * is global, false otherwise. 31 | * @param {Number} options.input.anyname.captures.n.matchIndex - The match index 32 | * to use to capture data for the regex. 33 | * @param {Regex} options.input.anyname.captures.n.re - The capturing regex to 34 | * use to get data. 35 | * @param {Object} options.output - output options. 36 | * @param {String} options.output.file - The path of the json output file. 37 | * @param {Object} [options.output.manifest] - An arbitrary manifest to write 38 | * to the json output file. 39 | * @param {Function} [options.logger] - A logger to use. 40 | * @returns {Undefined} Nothing. 41 | */ 42 | export function cannibalizr (options) { 43 | const output = {}; 44 | 45 | // used to trim "|' from the beginning and end of a captured result. 46 | const reClean = /^(?:\s+|"|')|(?:\s+|"|')$/g; 47 | 48 | // basic input check 49 | const inputOk = options.input && Object.keys(options.input).length && 50 | options.output && options.output.file; 51 | 52 | if (!inputOk) { 53 | throw new Error( 54 | 'Cannibalizr received invalid options. Please review readme' 55 | ); 56 | } 57 | 58 | if (options.output.manifest) { 59 | output.manifest = options.output.manifest; 60 | } 61 | 62 | /** 63 | * Report output results 64 | * 65 | * @param {Object} items - An array of output values. 66 | * @param {String} name - The name assocaited with the output. 67 | * @returns {Undefined} Nothing. 68 | */ 69 | const report = function (items, name) { 70 | if (options.logger) { 71 | if (items && items.length) { 72 | items.forEach(str => { 73 | options.logger(`"${name}": ${str}`); 74 | }); 75 | } else { 76 | options.logger(`nothing found for "${name}"`); 77 | } 78 | } 79 | }; 80 | 81 | Object.keys(options.input).forEach(item => { 82 | options.input[item].forEach(input => { 83 | const contents = fs.readFileSync(input.file, { encoding: 'utf8' }); 84 | 85 | input.captures.forEach(capSpec => { 86 | let m; 87 | 88 | output[item] || (output[item] = []); 89 | 90 | if (capSpec.global) { 91 | while ((m = capSpec.re.exec(contents)) !== null) { 92 | output[item].push(m[capSpec.matchIndex].replace(reClean, '')); 93 | } 94 | } else { 95 | m = capSpec.re.exec(contents); 96 | if (m) { 97 | output[item].push(m[capSpec.matchIndex].replace(reClean, '')); 98 | } 99 | } 100 | }); 101 | }); 102 | report(output[item], item); 103 | }); 104 | 105 | fs.writeFileSync(options.output.file, JSON.stringify( 106 | output 107 | )); 108 | } 109 | --------------------------------------------------------------------------------