├── .husky ├── .gitignore └── pre-commit ├── .prettierrc.json ├── integs ├── test-files │ ├── not_a_config │ ├── test_main.js │ ├── invalid_config.json │ ├── test_main.ts │ ├── test_lib.ts │ └── other_config.json └── e2e.test.ts ├── .gitignore ├── .prettierignore ├── src ├── meow.ts ├── config.test.ts ├── constants.ts ├── main.ts ├── validate.ts ├── validate.test.ts └── config.ts ├── jest.config.js ├── rollup.config.js ├── .github ├── dependabot.yml └── workflows │ ├── node.js.yml │ └── codeql-analysis.yml ├── tsconfig.json ├── LICENSE ├── package.json ├── README.md └── dist └── cli.js /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /integs/test-files/not_a_config: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | integs/tmp/* 3 | coverage 4 | -------------------------------------------------------------------------------- /integs/test-files/test_main.js: -------------------------------------------------------------------------------- 1 | console.log("Test"); 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Ignore artifacts: 2 | build 3 | coverage 4 | dist/* 5 | -------------------------------------------------------------------------------- /integs/test-files/invalid_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "invalid": "key" 3 | } 4 | -------------------------------------------------------------------------------- /src/meow.ts: -------------------------------------------------------------------------------- 1 | import M from "meow"; 2 | 3 | export const meow = M; 4 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /integs/test-files/test_main.ts: -------------------------------------------------------------------------------- 1 | import { greet } from "./test_lib"; 2 | 3 | greet("This is a greeting"); 4 | -------------------------------------------------------------------------------- /integs/test-files/test_lib.ts: -------------------------------------------------------------------------------- 1 | export function greet(name: string) { 2 | console.log(`Hello, ${name}!`); 3 | } 4 | -------------------------------------------------------------------------------- /integs/test-files/other_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Other test script", 3 | "namespace": "http://tampermonkey.net/", 4 | "version": "0.1", 5 | "description": "Gorilla-built, rock-solid, Monkey script", 6 | "author": "You" 7 | } 8 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: "ts-jest", 3 | testEnvironment: "node", 4 | coverageThreshold: { 5 | global: { 6 | branches: 85, 7 | functions: 90, 8 | lines: 90, 9 | }, 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | // Rollup config for CLI package 2 | import typescript from "rollup-plugin-typescript"; 3 | export default { 4 | input: "src/main.ts", 5 | output: { 6 | file: "dist/cli.js", 7 | format: "cjs", 8 | banner: "#!/usr/bin/env node", //Required for node commands 9 | }, 10 | plugins: [typescript()], 11 | }; 12 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "npm" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "daily" 12 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "moduleResolution": "node", 4 | "target": "es2017", 5 | "module": "commonjs", 6 | "lib": ["esnext"], 7 | "strict": true, 8 | "sourceMap": true, 9 | "declaration": true, 10 | "esModuleInterop": true, 11 | "allowSyntheticDefaultImports": true, 12 | "experimentalDecorators": true, 13 | "emitDecoratorMetadata": true, 14 | "declarationDir": "./dist", 15 | "outDir": "./dist", 16 | "typeRoots": ["node_modules/@types"] 17 | }, 18 | "include": ["src/**/*.ts"], 19 | "exclude": ["node_modules"] 20 | } 21 | -------------------------------------------------------------------------------- /src/config.test.ts: -------------------------------------------------------------------------------- 1 | import { getBanner } from "./config"; 2 | import { ERROR_MSG } from "./constants"; 3 | 4 | test("handles empty config", () => { 5 | const config = {}; 6 | 7 | const output = getBanner(config); 8 | expect(output).toEqual(` 9 | // ==UserScript== 10 | 11 | // 12 | // Created with love using Gorilla 13 | // ==/UserScript== 14 | `); 15 | }); 16 | 17 | test("handles invalid config key", () => { 18 | const config = { 19 | invalid: "key", 20 | }; 21 | 22 | try { 23 | getBanner(config, true); 24 | } catch (err) { 25 | expect(err).toContain(ERROR_MSG.EXPECT_VALID_KEY); 26 | } 27 | }); 28 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | export const HELP_MENU = ` 2 | Usage 3 | $ gorilla 4 | 5 | Options 6 | --config, -c Custom GreaseMonkey config 7 | --input, -i (required) Input filename 8 | --output, -o (required) Output filename 9 | 10 | Examples 11 | $ gorilla --input ./my-script.ts --output ./my-script.user.js 12 | `; 13 | 14 | export const ERROR_MSG = { 15 | EXPECT_JSON_FILE: "Gorilla configs must be a JSON file", 16 | EXPECT_VALID_KEY: "Invalid gorilla config key(s):", 17 | }; 18 | 19 | export const WARN_MSG = { 20 | EXPECT_TYPESCRIPT: 21 | "Gorilla recommends that your input files be written in TypeScript", 22 | EXPECT_GM_EXTENSION: 23 | "GreaseMonkey scripts must end in '.user.js'. Consider renaming your output file.", 24 | EXPECT_GM_KEYS: 25 | "GreaseMonkey script includes keys that GreaseMonkey does not support: ", 26 | }; 27 | -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: [ mainline ] 9 | pull_request: 10 | branches: [ mainline ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | matrix: 19 | node-version: [12.x, 14.x, 16.x] 20 | 21 | steps: 22 | - uses: actions/checkout@v2 23 | - name: Use Node.js ${{ matrix.node-version }} 24 | uses: actions/setup-node@v1 25 | with: 26 | node-version: ${{ matrix.node-version }} 27 | - run: npm ci 28 | - run: npm run build --if-present 29 | - run: npm test 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright © Alexander King 2 | 3 | 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: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | 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. 8 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | const typescript = require("rollup-plugin-typescript"); 2 | import { rollup, RollupOptions, OutputOptions } from "rollup"; 3 | import { nodeResolve } from "@rollup/plugin-node-resolve"; 4 | import validate from "./validate"; 5 | import { getConfig, getBanner } from "./config"; 6 | 7 | //Validate config input 8 | const { input, output, config, minify } = validate(); 9 | 10 | // Get config values 11 | const configJSON = getConfig(config); 12 | 13 | // Create banner text from config 14 | const banner = getBanner(configJSON); 15 | 16 | // Create Rollup config 17 | const outputConfig: OutputOptions = { 18 | file: output, 19 | banner: banner, 20 | format: "iife", 21 | compact: minify, 22 | }; 23 | const rollupConfig: RollupOptions = { 24 | input, 25 | output: outputConfig, 26 | plugins: [typescript(), nodeResolve()], 27 | }; 28 | 29 | // Call rollup 30 | rollup(rollupConfig) 31 | .then(async (bundle) => { 32 | await bundle.generate(outputConfig); 33 | return await bundle.write(outputConfig); 34 | }) 35 | .then(() => console.log("Gorilla smash complete!")); 36 | -------------------------------------------------------------------------------- /src/validate.ts: -------------------------------------------------------------------------------- 1 | import { meow } from "./meow"; 2 | import { HELP_MENU, ERROR_MSG, WARN_MSG } from "./constants"; 3 | 4 | const validate = () => { 5 | //Use Meow for arg parsing and validation 6 | const cli = meow(HELP_MENU, { 7 | flags: { 8 | config: { 9 | type: "string", 10 | alias: "c", 11 | }, 12 | quiet: { 13 | type: "boolean", 14 | alias: "q", 15 | default: false, 16 | }, 17 | minify: { 18 | type: "boolean", 19 | alias: "m", 20 | default: false, 21 | }, 22 | input: { 23 | type: "string", 24 | alias: "i", 25 | isRequired: true, 26 | }, 27 | output: { 28 | type: "string", 29 | alias: "o", 30 | isRequired: true, 31 | }, 32 | }, 33 | }); 34 | 35 | const { input, output, config, quiet } = cli.flags; 36 | 37 | // Validate expected filetypes 38 | if (config && !config.endsWith(".json")) { 39 | throw ERROR_MSG.EXPECT_JSON_FILE; 40 | } 41 | 42 | if (!quiet && !input.endsWith(".ts")) { 43 | console.warn(WARN_MSG.EXPECT_TYPESCRIPT); 44 | } 45 | 46 | //Provide warning on output 47 | if (!quiet && !output.endsWith("user.js")) { 48 | console.warn(WARN_MSG.EXPECT_GM_EXTENSION); 49 | } 50 | 51 | return cli.flags; 52 | }; 53 | 54 | export default validate; 55 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gorilla-build", 3 | "version": "0.1.16", 4 | "description": "Gorilla: Stop monkeying around and build better scripts.", 5 | "bin": { 6 | "gorilla": "dist/cli.js" 7 | }, 8 | "scripts": { 9 | "build": "rollup --config", 10 | "test": "npx jest ./src", 11 | "test:c": "npx jest --coverage ./src", 12 | "test:w": "npx jest --watchAll ./src", 13 | "integ": "npx jest ./integs", 14 | "integ:w": "npx jest --watchAll ./integs", 15 | "prepare": "husky install" 16 | }, 17 | "author": "Alex King", 18 | "license": "MIT", 19 | "dependencies": { 20 | "@rollup/plugin-node-resolve": "^15.0.1", 21 | "meow": "^9.0.0", 22 | "rollup": "^2.33.3", 23 | "rollup-plugin-typescript": "^1.0.1", 24 | "tslib": "^2.0.3", 25 | "typescript": "^4.0.5" 26 | }, 27 | "devDependencies": { 28 | "@rollup/plugin-typescript": "^11.0.0", 29 | "@types/jest": "^27.0.1", 30 | "@types/node": "^18.11.18", 31 | "@types/npm": "^7.19.0", 32 | "@types/sinon": "^10.0.2", 33 | "husky": "^8.0.1", 34 | "jest": "^27.5.1", 35 | "lint-staged": "^13.0.0", 36 | "npm": "^8.5.2", 37 | "prettier": "2.8.4", 38 | "sinon": "^14.0.0", 39 | "ts-jest": "^27.1.3" 40 | }, 41 | "repository": { 42 | "type": "git", 43 | "url": "git+https://github.com/apsking/gorilla.git" 44 | }, 45 | "keywords": [ 46 | "greasemonkey", 47 | "tampermonkey", 48 | "violentmonkey", 49 | "script", 50 | "userscript", 51 | "typescript", 52 | "build", 53 | "tool-chain", 54 | "cli" 55 | ], 56 | "bugs": { 57 | "url": "https://github.com/apsking/gorilla/issues" 58 | }, 59 | "homepage": "https://github.com/apsking/gorilla#readme", 60 | "lint-staged": { 61 | "*.{ts,js,css,md}": "prettier --write" 62 | }, 63 | "coveragePathIgnorePatterns": [ 64 | "/node_modules/", 65 | "/integs/tmp/**" 66 | ] 67 | } 68 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ mainline ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ mainline ] 20 | schedule: 21 | - cron: '30 15 * * 5' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'javascript' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Learn more about CodeQL language support at https://git.io/codeql-language-support 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v2 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v1 46 | with: 47 | languages: ${{ matrix.language }} 48 | # If you wish to specify custom queries, you can do so here or in a config file. 49 | # By default, queries listed here will override any specified in a config file. 50 | # Prefix the list here with "+" to use these queries and those in the config file. 51 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 52 | 53 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 54 | # If this step fails, then you should remove it and run the build manually (see below) 55 | - name: Autobuild 56 | uses: github/codeql-action/autobuild@v1 57 | 58 | # ℹ️ Command-line programs to run using the OS shell. 59 | # 📚 https://git.io/JvXDl 60 | 61 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 62 | # and modify them (or add more) to build your code if your project 63 | # uses a compiled language 64 | 65 | #- run: | 66 | # make bootstrap 67 | # make release 68 | 69 | - name: Perform CodeQL Analysis 70 | uses: github/codeql-action/analyze@v1 71 | -------------------------------------------------------------------------------- /src/validate.test.ts: -------------------------------------------------------------------------------- 1 | import * as M from "./meow"; // = require('meow'); 2 | import validate from "./validate"; 3 | import sinon from "sinon"; 4 | import { ERROR_MSG, WARN_MSG } from "./constants"; 5 | 6 | const sandbox = sinon.createSandbox(); 7 | let stubMeow: sinon.SinonStub; 8 | let spyConsole: sinon.SinonStub; 9 | 10 | beforeEach(() => { 11 | stubMeow = sandbox.stub(M, "meow"); 12 | spyConsole = sandbox.stub(console, "warn"); 13 | }); 14 | 15 | afterEach(() => { 16 | sandbox.restore(); 17 | }); 18 | 19 | test("validates input is a ts file", () => { 20 | stubMeow.returns({ 21 | flags: { 22 | input: "test.js", 23 | output: "test.user.js", 24 | }, 25 | input: [], 26 | unnormalizedFlags: {}, 27 | pkg: {}, 28 | help: "", 29 | showHelp: () => {}, 30 | showVersion: () => {}, 31 | }); 32 | 33 | validate(); 34 | 35 | expect(spyConsole.args[0][0]).toEqual(WARN_MSG.EXPECT_TYPESCRIPT); 36 | }); 37 | 38 | test("validates output is is a GreaseMonkey extension", () => { 39 | stubMeow.returns({ 40 | flags: { 41 | input: "test.ts", 42 | output: "test.other.extension", 43 | }, 44 | input: [], 45 | unnormalizedFlags: {}, 46 | pkg: {}, 47 | help: "", 48 | showHelp: () => {}, 49 | showVersion: () => {}, 50 | }); 51 | 52 | validate(); 53 | 54 | expect(spyConsole.args[0][0]).toEqual(WARN_MSG.EXPECT_GM_EXTENSION); 55 | }); 56 | 57 | test("throws exception with invalid config", () => { 58 | stubMeow.returns({ 59 | flags: { 60 | input: "test.ts", 61 | output: "test.user.js", 62 | config: "test.other.extension", 63 | }, 64 | input: [], 65 | unnormalizedFlags: {}, 66 | pkg: {}, 67 | help: "", 68 | showHelp: () => {}, 69 | showVersion: () => {}, 70 | }); 71 | 72 | try { 73 | validate(); 74 | } catch (e) { 75 | expect(e).toEqual(ERROR_MSG.EXPECT_JSON_FILE); 76 | } 77 | }); 78 | 79 | test("validates quietly", () => { 80 | stubMeow.returns({ 81 | flags: { 82 | input: "test.js", 83 | output: "test.other.extension", 84 | quiet: true, 85 | }, 86 | input: [], 87 | unnormalizedFlags: {}, 88 | pkg: {}, 89 | help: "", 90 | showHelp: () => {}, 91 | showVersion: () => {}, 92 | }); 93 | 94 | validate(); 95 | 96 | expect(spyConsole.called).toEqual(false); 97 | }); 98 | 99 | test("validates output w/o any warnings", () => { 100 | stubMeow.returns({ 101 | flags: { 102 | input: "test.ts", 103 | output: "test.user.js", 104 | }, 105 | input: [], 106 | unnormalizedFlags: {}, 107 | pkg: {}, 108 | help: "", 109 | showHelp: () => {}, 110 | showVersion: () => {}, 111 | }); 112 | 113 | validate(); 114 | 115 | expect(spyConsole.called).toEqual(false); 116 | }); 117 | -------------------------------------------------------------------------------- /integs/e2e.test.ts: -------------------------------------------------------------------------------- 1 | const { exec } = require("child_process"); 2 | import * as fs from "fs"; 3 | import { ERROR_MSG, WARN_MSG } from "../src/constants"; 4 | 5 | const execPromise = ( 6 | execStr: string 7 | ): Promise<{ 8 | err: string; 9 | stdout: string; 10 | stderr: string; 11 | resolve: any; 12 | reject: any; 13 | }> => { 14 | return new Promise((resolve, reject) => { 15 | exec(execStr, (err: string, stdout: string, stderr: string) => { 16 | resolve({ err, stdout, stderr, resolve, reject }); 17 | }); 18 | }); 19 | }; 20 | 21 | // Remove all temp files before each run 22 | beforeEach(async () => { 23 | await execPromise("rm ./integs/tmp/*"); 24 | }); 25 | 26 | test("should show help menu", async () => { 27 | const { stdout } = await execPromise("gorilla --help"); 28 | 29 | expect(stdout).toContain( 30 | "Gorilla: Stop monkeying around and build better scripts." 31 | ); 32 | }); 33 | 34 | test("should throw error on unknown file", async () => { 35 | const { stderr } = await execPromise( 36 | "gorilla --input ./integs/test-files/not_a_file.ts --output ./integs/tmp/out.js" 37 | ); 38 | expect(stderr).toContain("Could not resolve entry module"); 39 | }); 40 | 41 | test("should throw error on bad config JSON filetype", async () => { 42 | const { stderr } = await execPromise( 43 | "gorilla --input ./integs/test-files/not_a_file.ts --output ./integs/tmp/out.js --config ./integs/test-files/not_a_config" 44 | ); 45 | expect(stderr).toContain(ERROR_MSG.EXPECT_JSON_FILE); 46 | }); 47 | 48 | test("should warn on bad gorilla config key", async () => { 49 | const { stderr } = await execPromise( 50 | "gorilla --input ./integs/test-files/test_main.ts --output ./integs/tmp/out.js --config ./integs/test-files/invalid_config.json" 51 | ); 52 | expect(stderr).toContain(WARN_MSG.EXPECT_GM_KEYS); 53 | }); 54 | 55 | test("should show warning for output filename", async () => { 56 | const { stderr } = await execPromise( 57 | "gorilla --input ./integs/test-files/test_main.ts --output ./integs/tmp/out.js" 58 | ); 59 | expect(stderr).toContain(WARN_MSG.EXPECT_GM_EXTENSION); 60 | }); 61 | 62 | test("should show warning for non-TypeScript input", async () => { 63 | const { stderr } = await execPromise( 64 | "gorilla --input ./integs/test-files/test_main.js --output ./integs/tmp/out.js" 65 | ); 66 | expect(stderr).toContain(WARN_MSG.EXPECT_TYPESCRIPT); 67 | }); 68 | 69 | test("should create script with package.json info", async () => { 70 | await execPromise( 71 | "gorilla --input ./integs/test-files/test_main.ts --output ./integs/tmp/out.js" 72 | ); 73 | const file = fs.readFileSync("./integs/tmp/out.js", "utf8"); 74 | expect(file).toContain("gorilla-build"); 75 | expect(file).toContain("Alex King"); 76 | expect(file).toContain("MIT"); 77 | }); 78 | 79 | test("should create script with custom config", async () => { 80 | await execPromise( 81 | "gorilla --config ./integs/test-files/other_config.json --input ./integs/test-files/test_main.ts --output ./integs/tmp/out.js" 82 | ); 83 | const file = fs.readFileSync("./integs/tmp/out.js", "utf8"); 84 | expect(file).toContain("Other test script"); //Just assert name of config 85 | }); 86 | -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "fs"; 2 | import { WARN_MSG } from "./constants"; 3 | 4 | const PACKAGE_JSON_LOCATION = "./package.json"; 5 | const PACKAGE_JSON_GORILLA_KEY = "gorilla"; 6 | const PACKAGE_JSON_KEYS = [ 7 | "name", 8 | "version", 9 | "description", 10 | "author", 11 | "homepage", 12 | "copyright", 13 | "license", 14 | ]; 15 | 16 | /* 17 | * Attributes for all Metadata Block items: 18 | * https://wiki.greasespot.net/Metadata_Block 19 | */ 20 | export type GorillaConfig = { 21 | author?: string; 22 | description?: string; 23 | exclude?: string[]; 24 | grant?: string[]; 25 | icon?: string; 26 | include?: string[]; 27 | match?: string[]; 28 | name?: string; 29 | namespace?: string; 30 | noframes?: string; 31 | require?: string[]; 32 | resource?: string[]; 33 | updateURL?: string; 34 | downloadURL?: string; 35 | version?: string; 36 | [key: string]: undefined | string | string[]; //needed for lookup 37 | }; 38 | 39 | export const VALID_GORILLA_CONFIG_KEYS = [ 40 | "author", 41 | "description", 42 | "exclude", 43 | "grant", 44 | "icon", 45 | "include", 46 | "match", 47 | "name", 48 | "namespace", 49 | "noframes", 50 | "require", 51 | "resource", 52 | "version", 53 | "updateURL", 54 | "downloadURL", 55 | ]; 56 | 57 | /** 58 | * Priority: 59 | * 1. Input config 60 | * 2. package.json 61 | * @returns current GorillaConfig 62 | */ 63 | export const getConfig = (inputConfigLocation?: string): GorillaConfig => { 64 | const tmpConfig: GorillaConfig = {}; 65 | 66 | // Config passed in by input 67 | if (inputConfigLocation && inputConfigLocation !== "") { 68 | try { 69 | const inputConfig = JSON.parse( 70 | fs.readFileSync(inputConfigLocation, "utf8") 71 | ); 72 | Object.keys(inputConfig).forEach((key) => { 73 | tmpConfig[key] = inputConfig[key]; 74 | }); 75 | } catch (err) { 76 | console.error("Failed to parse input config", err); 77 | } 78 | } 79 | 80 | // Read `package.json` 81 | if (fs.existsSync(PACKAGE_JSON_LOCATION)) { 82 | const packageJSON = JSON.parse( 83 | fs.readFileSync(PACKAGE_JSON_LOCATION, "utf8") 84 | ); 85 | 86 | // Read common keys 87 | PACKAGE_JSON_KEYS.forEach((key) => { 88 | if (packageJSON[key]) { 89 | if (!tmpConfig[key]) { 90 | tmpConfig[key] = packageJSON[key]; 91 | } 92 | } 93 | }); 94 | 95 | // Read valid Gorilla keys 96 | if (packageJSON[PACKAGE_JSON_GORILLA_KEY]) { 97 | VALID_GORILLA_CONFIG_KEYS.forEach((key) => { 98 | if (packageJSON[PACKAGE_JSON_GORILLA_KEY][key]) { 99 | tmpConfig[key] = packageJSON[PACKAGE_JSON_GORILLA_KEY][key]; 100 | } 101 | }); 102 | } 103 | } 104 | 105 | return tmpConfig; 106 | }; 107 | 108 | /* 109 | * Fetch a GreaseMonkey-formatted banner text, which will 110 | * prepend the script itself. 111 | */ 112 | export const getBanner = ( 113 | config: GorillaConfig, 114 | quiet: boolean = false 115 | ): string => { 116 | const invalidItems = Object.keys(config).filter( 117 | (key) => !VALID_GORILLA_CONFIG_KEYS.includes(key) 118 | ); 119 | 120 | if (invalidItems.length > 0 && !quiet) { 121 | const msg = `${WARN_MSG.EXPECT_GM_KEYS} ${invalidItems.join(", ")}`; 122 | console.warn(msg); 123 | } 124 | 125 | const items = Object.keys(config) 126 | .map((key) => ({ key, value: config[key] })) 127 | .map((item) => 128 | Array.isArray(item.value) 129 | ? item.value.map((inner) => ({ key: item.key, value: inner })) 130 | : item 131 | ) 132 | .flatMap((i) => i); 133 | 134 | const scriptLines = items 135 | .map(({ key, value }) => { 136 | const tabs = key.length < 8 ? "\t\t\t" : "\t\t"; 137 | return `// @${key}${value ? `${tabs}${value}` : ""}`; 138 | }) 139 | .join("\n"); 140 | return ` 141 | // ==UserScript== 142 | ${scriptLines} 143 | // 144 | // Created with love using Gorilla 145 | // ==/UserScript== 146 | `; 147 | }; 148 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

🦍 Gorilla 🦍

2 |

Stop monkeying around and write better scripts

3 | 4 |

5 | 6 | 🙈 🙉 🙊 7 |
8 | 9 | 🍌 GreaseMonkey · TamperMonkey 🍌 10 | 11 |

12 | 13 |

14 | 15 | Blazing Fast 16 | 17 | 18 |
19 | 20 | npm version 21 | 22 | code style: prettier 23 | GitHub Workflow Status 24 |
25 |
26 | 27 | buy me a coffee 28 | 29 |

30 | 31 | ## Intro 32 | 33 | Gorilla is a blazing fast, TypeScript build tool for creating better 34 | GreaseMonkey scripts. It handles the complex build chain, so you don't 35 | have to. 36 | 37 | ## Get started 38 | 39 | ### Input 40 | 41 | `helper.ts` 42 | 43 | 44 | ```js 45 | export const hello = (name:string) => { 46 | console.log(`Hello ${name}!`); 47 | } 48 | ``` 49 | 50 | `main.ts` 51 | 52 | 53 | ```js 54 | import { hello } from './helper'; 55 | 56 | hello('world'); 57 | ``` 58 | 59 | `package.json` 60 | 61 | 62 | ```js 63 | ... 64 | "scripts": { 65 | "build": "gorilla --input ./main.ts --output ./script.user.js" 66 | }, 67 | ... 68 | ``` 69 | 70 | ### Output 71 | 72 | `script.user.js` 73 | 74 | 75 | ```js 76 | // ==UserScript== 77 | // @name New Userscript 78 | // @namespace http://tampermonkey.net/ 79 | // @version 0.1 80 | // @description Gorilla-built, rock-solid, Monkey script 81 | // @updateURL 82 | // @downloadURL 83 | // @author You 84 | // @include https://** 85 | // 86 | // Created with love using Gorilla 87 | // ==/UserScript== 88 | 89 | (function () { 90 | 'use strict'; 91 | 92 | function greet(name) { 93 | console.log(`Hello, ${name}!`); 94 | } 95 | 96 | greet("This is a greeting"); 97 | 98 | }()); 99 | ``` 100 | 101 | ## Samples 102 | 103 | You can find a [collection of samples, here](https://github.com/apsking/gorilla-samples). 104 | 105 | ## Options 106 | 107 | ### Help (`--help`) 108 | 109 | Display help menu. 110 | 111 | eg. 112 | 113 | ``` 114 | gorilla --help 115 | ``` 116 | 117 | ### Input (`--input, -i`) 118 | 119 | The input handler for your script. 120 | 121 | **Note:** While not required, Gorilla recommends writing your scripts in `TypeScript`. 122 | 123 | eg. 124 | 125 | ``` 126 | gorilla --input ./my-input-file.ts ... 127 | ``` 128 | 129 | ### Output (`--output, -o`) 130 | 131 | The input handler for your script. 132 | 133 | **Note:** While not required, GreaseMonkey scripts should end with `.user.js`. 134 | 135 | eg. 136 | 137 | ``` 138 | gorilla --output ./my-script.user.js ... 139 | ``` 140 | 141 | ### Config (`--config, -c`) 142 | 143 | JSON input Gorilla config including GreaseMonkey metadata block data. 144 | 145 | eg. 146 | 147 | ``` 148 | gorilla --config ./my-config.json ... 149 | ``` 150 | 151 | ### Quiet (`--quiet, -q`) 152 | 153 | Hide all warning messages. 154 | 155 | eg. 156 | 157 | ``` 158 | gorilla --quiet true ... 159 | ``` 160 | 161 | ### Minify (`--minify, -m`) 162 | 163 | Minify the output code. 164 | 165 | eg. 166 | 167 | ``` 168 | gorilla --minify ... 169 | ``` 170 | 171 | ## Config 172 | 173 | The config is based off of the officially supported Metadata Block items found here: https://wiki.greasespot.net/Metadata_Block 174 | 175 | The following JSON keys are supported by GreaseMonkey: 176 | 177 | - `author` - (`string`) - Author of the script 178 | - `description` - (`string`) - Description of the script 179 | - `exclude` - (`string[]`) - URLs to exclude the script from 180 | - `grant` - (`string[]`) - Permissions to grant to the script 181 | - `icon` - (`string`) - Icon for the script 182 | - `include` - (`string[]`) - URLs to include the script in 183 | - `match` - (`string[]`) - URLs to match the script in 184 | - `name` - (`string`) - Name of the script 185 | - `namespace` - (`string`) - Namespace of the script 186 | - `noframes` - (`string`) - Whether or not to run in frames 187 | - `require` - (`string[]`) - Scripts to include within the script 188 | - `resource` - (`string[]`) - Resources to include within the script 189 | - `version` - (`string`) - Version number of the script 190 | - `updateURL` - (`string`) - URL location for script updates 191 | - `downloadURL` - (`string`) - URL location for script download 192 | 193 | The config will be constructed by both the optional `config` argument and with information from the `package.json` file for 194 | your current project. Some information will be take from the root of your `package.json` (eg. `author`, `name`, etc.). Other information can be defined in a `gorilla` key in your `package.json`. For example: 195 | 196 | ``` 197 | ... 198 | "name": "This is my awesome script package.json!", 199 | ... 200 | "gorilla": { 201 | "include": ["this_key", "and this one"], 202 | "updateURL": "this_url" 203 | } 204 | ``` 205 | 206 | NOTE - any valid keys in the `gorilla` will override anything else from the root `package.json`! 207 | -------------------------------------------------------------------------------- /dist/cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 'use strict'; 3 | 4 | var rollup = require('rollup'); 5 | var pluginNodeResolve = require('@rollup/plugin-node-resolve'); 6 | var M = require('meow'); 7 | var fs = require('fs'); 8 | 9 | function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; } 10 | 11 | function _interopNamespace(e) { 12 | if (e && e.__esModule) return e; 13 | var n = Object.create(null); 14 | if (e) { 15 | Object.keys(e).forEach(function (k) { 16 | if (k !== 'default') { 17 | var d = Object.getOwnPropertyDescriptor(e, k); 18 | Object.defineProperty(n, k, d.get ? d : { 19 | enumerable: true, 20 | get: function () { return e[k]; } 21 | }); 22 | } 23 | }); 24 | } 25 | n["default"] = e; 26 | return Object.freeze(n); 27 | } 28 | 29 | var M__default = /*#__PURE__*/_interopDefaultLegacy(M); 30 | var fs__namespace = /*#__PURE__*/_interopNamespace(fs); 31 | 32 | const meow = M__default["default"]; 33 | 34 | const HELP_MENU = ` 35 | Usage 36 | $ gorilla 37 | 38 | Options 39 | --config, -c Custom GreaseMonkey config 40 | --input, -i (required) Input filename 41 | --output, -o (required) Output filename 42 | 43 | Examples 44 | $ gorilla --input ./my-script.ts --output ./my-script.user.js 45 | `; 46 | const ERROR_MSG = { 47 | EXPECT_JSON_FILE: "Gorilla configs must be a JSON file", 48 | EXPECT_VALID_KEY: "Invalid gorilla config key(s):", 49 | }; 50 | const WARN_MSG = { 51 | EXPECT_TYPESCRIPT: "Gorilla recommends that your input files be written in TypeScript", 52 | EXPECT_GM_EXTENSION: "GreaseMonkey scripts must end in '.user.js'. Consider renaming your output file.", 53 | EXPECT_GM_KEYS: "GreaseMonkey script includes keys that GreaseMonkey does not support: ", 54 | }; 55 | 56 | const validate = () => { 57 | //Use Meow for arg parsing and validation 58 | const cli = meow(HELP_MENU, { 59 | flags: { 60 | config: { 61 | type: "string", 62 | alias: "c", 63 | }, 64 | quiet: { 65 | type: "boolean", 66 | alias: "q", 67 | default: false, 68 | }, 69 | minify: { 70 | type: "boolean", 71 | alias: "m", 72 | default: false, 73 | }, 74 | input: { 75 | type: "string", 76 | alias: "i", 77 | isRequired: true, 78 | }, 79 | output: { 80 | type: "string", 81 | alias: "o", 82 | isRequired: true, 83 | }, 84 | }, 85 | }); 86 | const { input, output, config, quiet } = cli.flags; 87 | // Validate expected filetypes 88 | if (config && !config.endsWith(".json")) { 89 | throw ERROR_MSG.EXPECT_JSON_FILE; 90 | } 91 | if (!quiet && !input.endsWith(".ts")) { 92 | console.warn(WARN_MSG.EXPECT_TYPESCRIPT); 93 | } 94 | //Provide warning on output 95 | if (!quiet && !output.endsWith("user.js")) { 96 | console.warn(WARN_MSG.EXPECT_GM_EXTENSION); 97 | } 98 | return cli.flags; 99 | }; 100 | 101 | const PACKAGE_JSON_LOCATION = "./package.json"; 102 | const PACKAGE_JSON_GORILLA_KEY = "gorilla"; 103 | const PACKAGE_JSON_KEYS = [ 104 | "name", 105 | "version", 106 | "description", 107 | "author", 108 | "homepage", 109 | "copyright", 110 | "license", 111 | ]; 112 | const VALID_GORILLA_CONFIG_KEYS = [ 113 | "author", 114 | "description", 115 | "exclude", 116 | "grant", 117 | "icon", 118 | "include", 119 | "match", 120 | "name", 121 | "namespace", 122 | "noframes", 123 | "require", 124 | "resource", 125 | "version", 126 | "updateURL", 127 | "downloadURL", 128 | ]; 129 | /** 130 | * Priority: 131 | * 1. Input config 132 | * 2. package.json 133 | * @returns current GorillaConfig 134 | */ 135 | const getConfig = (inputConfigLocation) => { 136 | const tmpConfig = {}; 137 | // Config passed in by input 138 | if (inputConfigLocation && inputConfigLocation !== "") { 139 | try { 140 | const inputConfig = JSON.parse(fs__namespace.readFileSync(inputConfigLocation, "utf8")); 141 | Object.keys(inputConfig).forEach((key) => { 142 | tmpConfig[key] = inputConfig[key]; 143 | }); 144 | } 145 | catch (err) { 146 | console.error("Failed to parse input config", err); 147 | } 148 | } 149 | // Read `package.json` 150 | if (fs__namespace.existsSync(PACKAGE_JSON_LOCATION)) { 151 | const packageJSON = JSON.parse(fs__namespace.readFileSync(PACKAGE_JSON_LOCATION, "utf8")); 152 | // Read common keys 153 | PACKAGE_JSON_KEYS.forEach((key) => { 154 | if (packageJSON[key]) { 155 | if (!tmpConfig[key]) { 156 | tmpConfig[key] = packageJSON[key]; 157 | } 158 | } 159 | }); 160 | // Read valid Gorilla keys 161 | if (packageJSON[PACKAGE_JSON_GORILLA_KEY]) { 162 | VALID_GORILLA_CONFIG_KEYS.forEach((key) => { 163 | if (packageJSON[PACKAGE_JSON_GORILLA_KEY][key]) { 164 | tmpConfig[key] = packageJSON[PACKAGE_JSON_GORILLA_KEY][key]; 165 | } 166 | }); 167 | } 168 | } 169 | return tmpConfig; 170 | }; 171 | /* 172 | * Fetch a GreaseMonkey-formatted banner text, which will 173 | * prepend the script itself. 174 | */ 175 | const getBanner = (config, quiet = false) => { 176 | const invalidItems = Object.keys(config).filter((key) => !VALID_GORILLA_CONFIG_KEYS.includes(key)); 177 | if (invalidItems.length > 0 && !quiet) { 178 | const msg = `${WARN_MSG.EXPECT_GM_KEYS} ${invalidItems.join(", ")}`; 179 | console.warn(msg); 180 | } 181 | const items = Object.keys(config) 182 | .map((key) => ({ key, value: config[key] })) 183 | .map((item) => Array.isArray(item.value) 184 | ? item.value.map((inner) => ({ key: item.key, value: inner })) 185 | : item) 186 | .flatMap((i) => i); 187 | const scriptLines = items 188 | .map(({ key, value }) => { 189 | const tabs = key.length < 8 ? "\t\t\t" : "\t\t"; 190 | return `// @${key}${value ? `${tabs}${value}` : ""}`; 191 | }) 192 | .join("\n"); 193 | return ` 194 | // ==UserScript== 195 | ${scriptLines} 196 | // 197 | // Created with love using Gorilla 198 | // ==/UserScript== 199 | `; 200 | }; 201 | 202 | const typescript = require("rollup-plugin-typescript"); 203 | //Validate config input 204 | const { input, output, config, minify } = validate(); 205 | // Get config values 206 | const configJSON = getConfig(config); 207 | // Create banner text from config 208 | const banner = getBanner(configJSON); 209 | // Create Rollup config 210 | const outputConfig = { 211 | file: output, 212 | banner: banner, 213 | format: "iife", 214 | compact: minify, 215 | }; 216 | const rollupConfig = { 217 | input, 218 | output: outputConfig, 219 | plugins: [typescript(), pluginNodeResolve.nodeResolve()], 220 | }; 221 | // Call rollup 222 | rollup.rollup(rollupConfig) 223 | .then(async (bundle) => { 224 | await bundle.generate(outputConfig); 225 | return await bundle.write(outputConfig); 226 | }) 227 | .then(() => console.log("Gorilla smash complete!")); 228 | --------------------------------------------------------------------------------