├── .gitignore ├── .eslintignore ├── .prettierignore ├── .prettierrc.js ├── release.config.js ├── commitlint.config.js ├── .eslintrc ├── tsconfig.json ├── .editorconfig ├── src ├── util.ts ├── bin.ts └── lib.ts ├── .gitattributes ├── .github └── workflows │ └── ci.yml ├── LICENSE ├── package.json └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .vscode 4 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | /dist 4 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .vscode 3 | dist 4 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | ...require('@gamesdonequick/prettier-config'), 3 | }; 4 | -------------------------------------------------------------------------------- /release.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | release: { 3 | branch: 'master', 4 | }, 5 | }; 6 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@commitlint/config-conventional'], 3 | }; 4 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "extends": [ 4 | "xo", 5 | "xo-typescript", 6 | "prettier" 7 | ], 8 | "ignorePatterns": ["commitlint.config.js", "release.config.js"], 9 | "rules": { 10 | "@typescript-eslint/camelcase": [ 11 | 0 12 | ] 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@supportclass/tsconfig-base", 3 | "compilerOptions": { 4 | "esModuleInterop": true, 5 | "rootDir": "src", 6 | "outDir": "dist", 7 | "lib": ["es2017"], 8 | "module": "commonjs", 9 | "types": ["node"], 10 | "sourceMap": true 11 | }, 12 | "include": ["src/**/*.ts"] 13 | } 14 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | charset = utf-8 4 | 5 | [*] 6 | indent_style = tab 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | indent_size = 4 12 | 13 | [{package.json,*.yml}] 14 | indent_style = space 15 | indent_size = 2 16 | 17 | # Don't remove trailing whitespace from Markdown 18 | [*.md] 19 | trim_trailing_whitespace = false 20 | -------------------------------------------------------------------------------- /src/util.ts: -------------------------------------------------------------------------------- 1 | /** Changes the extension of an existing filename, and returns the new filename */ 2 | export function changeExtension(filename: string, newExtension: string): string { 3 | const extStartIndex = filename.lastIndexOf('.'); 4 | 5 | // Handles files with no extension. 6 | if (extStartIndex < 0) { 7 | return `${filename}.${newExtension}`; 8 | } 9 | 10 | return `${filename.slice(0, extStartIndex)}.${newExtension}`; 11 | } 12 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Set the default behavior, in case people don't have core.autocrlf set. 2 | * text=auto 3 | 4 | # Explicitly declare text files you want to always be normalized and converted 5 | # to native line endings on checkout. 6 | *.css text eol=lf 7 | *.html text eol=lf 8 | *.js text eol=lf 9 | *.jsx text eol=lf 10 | *.ts text eol=lf 11 | *.json text eol=lf 12 | *.md text eol=lf 13 | *.yml text eol=lf 14 | 15 | # Force images/fonts to be handled as binaries 16 | *.jpg binary 17 | *.jpeg binary 18 | *.gif binary 19 | *.png binary 20 | *.t3x binary 21 | *.t3d binary 22 | *.exe binary 23 | *.data binary 24 | *.ttf binary 25 | *.eof binary 26 | *.eot binary 27 | *.woff binary 28 | *.swf binary 29 | *.mov binary 30 | *.mp4 binary 31 | *.mp3 binary 32 | *.ogg binary 33 | *.flv binary 34 | *.webm binary 35 | *.ico binary 36 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | test: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v2 16 | - uses: actions/cache@v2 17 | with: 18 | path: | 19 | ~/.dts 20 | ~/.npm 21 | key: node${{ matrix.node-version }} 22 | - uses: actions/setup-node@v1 23 | with: 24 | node-version: '14' 25 | - name: install dependencies 26 | run: npm ci 27 | - name: run test 28 | run: npm t 29 | - name: release (if necessary) 30 | if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' }} 31 | run: npx semantic-release 32 | env: 33 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 34 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Games Done Quick, LLC 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/bin.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | // Native 4 | import * as fs from 'fs'; 5 | import * as path from 'path'; 6 | 7 | // Packages 8 | import * as yargs from 'yargs'; 9 | 10 | // Ours 11 | import { changeExtension } from './util'; 12 | import { generateTypes, CodeCase, SceneCollection } from './lib'; 13 | 14 | const { argv } = yargs.options({ 15 | input: { 16 | type: 'string', 17 | describe: 'the OBS Scene Collection to generate typedefs from', 18 | demandOption: true, 19 | }, 20 | output: { 21 | type: 'string', 22 | describe: 'the location to write the generated typedefs to', 23 | }, 24 | case: { 25 | choices: ['camel', 'snake'], 26 | describe: 'the casing to use in the enum key names', 27 | default: 'snake', 28 | }, 29 | }); 30 | 31 | const outputPath = path.resolve(process.cwd(), argv.output ? argv.output : changeExtension(argv.input, 'd.ts')); 32 | const sceneCollection = JSON.parse( 33 | fs.readFileSync(argv.input, { 34 | encoding: 'utf8', 35 | }), 36 | ) as SceneCollection; 37 | const types = generateTypes(sceneCollection, argv.case as CodeCase); 38 | fs.writeFileSync(outputPath, types); 39 | 40 | console.log(`Types written to ${outputPath}`); 41 | -------------------------------------------------------------------------------- /src/lib.ts: -------------------------------------------------------------------------------- 1 | // Packages 2 | import { uniqBy, camelCase, snakeCase } from 'lodash'; 3 | import * as prettier from 'prettier'; 4 | 5 | interface SceneItem { 6 | id: string; 7 | name: string; 8 | settings?: Record; 9 | } 10 | 11 | export type CodeCase = 'camel' | 'snake'; 12 | type Source = SceneItem; 13 | type Transition = SceneItem; 14 | type Group = SceneItem; 15 | export type SceneCollection = { 16 | groups: Group[]; 17 | sources: Source[]; 18 | transitions: Transition[]; 19 | }; 20 | 21 | export function generateTypes(sceneCollection: SceneCollection, codeCase: CodeCase): string { 22 | const groups = generateEnum('Group', sceneCollection.groups, codeCase); 23 | const sources = generateEnum( 24 | 'Source', 25 | sceneCollection.sources.filter((source) => source.id !== 'scene'), 26 | codeCase, 27 | ); 28 | const scenes = generateEnum( 29 | 'Scene', 30 | sceneCollection.sources.filter((source) => source.id === 'scene'), 31 | codeCase, 32 | ); 33 | const transitions = generateEnum('Transition', sceneCollection.transitions, codeCase); 34 | return prettier.format( 35 | ` 36 | export ${groups} 37 | 38 | export ${sources} 39 | 40 | export ${scenes} 41 | 42 | export ${transitions} 43 | `, 44 | { 45 | parser: 'typescript', 46 | singleQuote: true, 47 | quoteProps: 'as-needed', 48 | }, 49 | ); 50 | } 51 | 52 | function capitalCamelCase(str: string): string { 53 | const casedKey = `${camelCase(str)}`; 54 | return `${casedKey.charAt(0).toUpperCase()}${casedKey.slice(1)}`; 55 | } 56 | 57 | function generateEnum(enumName: string, items: Array, codeCase: CodeCase): string { 58 | const casingFn = codeCase === 'camel' ? capitalCamelCase : snakeCase; 59 | items = uniqBy(items, (item) => casingFn(item.name)); 60 | const lines: string[] = items.map((item) => `'${casingFn(item.name)}' = '${item.name}',`); 61 | return `const enum ${enumName} { 62 | ${lines.join('\n')} 63 | }`; 64 | } 65 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@gamesdonequick/typed-obs-scenes", 3 | "version": "0.0.0-development", 4 | "description": "Generate a d.ts file full of enums describing an OBS Scene Collection.", 5 | "main": "dist/lib.js", 6 | "scripts": { 7 | "lint": "npm-run-all -s lint:*", 8 | "lint:prettier": "prettier --list-different \"**/*.{ts,json,md,yml,html,js}\"", 9 | "lint:eslint": "eslint ./src --ext .ts", 10 | "build": "npx tsc -p tsconfig.json", 11 | "fix": "npm-run-all -s fix:*", 12 | "fix:prettier": "prettier --write \"**/*.{ts,json,md,yml,html,js}\"", 13 | "fix:eslint": "npm run lint:eslint -- --fix ", 14 | "test": "npm run lint && npm run build" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git://github.com/GamesDoneQuick/typed-obs-scenes.git" 19 | }, 20 | "keywords": [ 21 | "obs", 22 | "typescript", 23 | "types", 24 | "typedef", 25 | "enum" 26 | ], 27 | "author": "Games Done Quick LLC ", 28 | "license": "MIT", 29 | "bugs": { 30 | "url": "https://github.com/GamesDoneQuick/typed-obs-scenes/issues" 31 | }, 32 | "homepage": "https://github.com/GamesDoneQuick/typed-obs-scenes#readme", 33 | "devDependencies": { 34 | "@commitlint/cli": "^13.1.0", 35 | "@commitlint/config-conventional": "^13.1.0", 36 | "@gamesdonequick/prettier-config": "^2.1.1", 37 | "@supportclass/tsconfig-base": "^1.0.4", 38 | "@types/lodash": "^4.14.173", 39 | "@types/node": "^14.17.6", 40 | "@types/prettier": "^2.4.0", 41 | "@types/yargs": "^13.0.2", 42 | "@typescript-eslint/eslint-plugin": "^4.31.2", 43 | "@typescript-eslint/parser": "^4.31.2", 44 | "eslint": "^7.32.0", 45 | "eslint-config-prettier": "^8.3.0", 46 | "eslint-config-xo": "^0.38.0", 47 | "eslint-config-xo-typescript": "^0.44.0", 48 | "husky": "^3.0.5", 49 | "lint-staged": "^9.2.5", 50 | "npm-run-all": "^4.1.5", 51 | "semantic-release": "^18.0.0", 52 | "typescript": "^4.4.3" 53 | }, 54 | "dependencies": { 55 | "lodash": "^4.17.21", 56 | "prettier": "^2.4.1", 57 | "tslib": "^2.3.1", 58 | "yargs": "^14.0.0" 59 | }, 60 | "husky": { 61 | "hooks": { 62 | "commit-msg": "commitlint -E HUSKY_GIT_PARAMS", 63 | "pre-commit": "lint-staged" 64 | } 65 | }, 66 | "lint-staged": { 67 | "*.{ts,json,md,yml,html,js}": [ 68 | "prettier --write", 69 | "git add" 70 | ], 71 | "*.{ts,html,js}": [ 72 | "eslint --fix", 73 | "git add" 74 | ] 75 | }, 76 | "publishConfig": { 77 | "access": "public" 78 | }, 79 | "files": [ 80 | "dist" 81 | ], 82 | "bin": { 83 | "typed-obs-scenes": "./dist/bin.js" 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # typed-obs-scenes [![npm](https://img.shields.io/npm/v/@gamesdonequick/typed-obs-scenes.svg)](https://www.npmjs.com/package/@gamesdonequick/typed-obs-scenes) [![Build Status](https://dev.azure.com/gamesdonequick/typed-obs-scenes/_apis/build/status/GamesDoneQuick.typed-obs-scenes?branchName=master)](https://dev.azure.com/gamesdonequick/typed-obs-scenes/_build/latest?definitionId=9&branchName=master) 2 | 3 | > Generate a d.ts file full of enums describing an OBS Scene Collection. 4 | 5 | ## Table of Contents 6 | 7 | - [Motivation](#motivation) 8 | - [Installation](#installation) 9 | - [Usage](#usage) 10 | 11 | ## Motivation 12 | 13 | We often need to write code which remotely controls one or more OBS Studio instances via `obs-websocket`. When doing this, it is common to need to reference specific Scene and Source names. This can be really dangerous, as it's easy to typo and these typos might go unnoticed until it is too late. 14 | 15 | That's where this CLI tool comes in. It takes an OBS Scene Collection JSON file as input, and outputs a TypeScript `d.ts` typedef file with enums describing the Scenes, Sources, Groups, and Transitions present in that Collection. It looks like this (don't worry, you can use `UpperCamelCase` if you don't like `snake_case`): 16 | 17 | ```ts 18 | export const enum Group { 19 | 'camera_interview' = 'Camera (Interview)', 20 | 'camera_widescreen_2' = 'Camera (Widescreen 2)', 21 | 'camera_widescreen_1' = 'Camera (Widescreen 1)', 22 | } 23 | 24 | export const enum Source { 25 | 'layout_widescreen_3' = 'Layout Widescreen 3', 26 | 'layout_standard_4_ff_4_fe' = 'Layout Standard 4 FF4FE', 27 | 'layout_standard_2_sms' = 'Layout Standard 2 SMS', 28 | 'countdown_anim' = 'Countdown Anim', 29 | 'layout_ds_vertical' = 'Layout DS Vertical', 30 | 'closing_slate' = 'Closing Slate', 31 | } 32 | 33 | export const enum Scene { 34 | 'main_stage_full_camera' = 'Main Stage Full Camera', 35 | 'widescreen_3' = 'Widescreen 3', 36 | 'standard_4_ff_4_fe' = 'Standard 4 FF4FE', 37 | 'standard_2_sms' = 'Standard 2 SMS', 38 | } 39 | 40 | export const enum Transition { 41 | 'fade_to_color' = 'Fade to Color', 42 | 'blank_stinger' = 'Blank Stinger', 43 | } 44 | ``` 45 | 46 | ## Installation 47 | 48 | This is a CLI program, so you don't need to write any code to use it. Just install and run. 49 | 50 | ``` 51 | npm i -g @gamesdonequick/typed-obs-scenes 52 | ``` 53 | 54 | ## Usage 55 | 56 | 1. Export your Scene Collection from OBS (this is done via the `Scene Collection > Export` dropdown menu). 57 | 2. Run `typed-obs-scenes` on it: 58 | 59 | ```bash 60 | typed-obs-scenes --input path/to/your/scene/collection.json 61 | ``` 62 | 63 | 3. Import the resulting `d.ts` file in your TypeScript project and start using it! 64 | 65 | ```ts 66 | import * as SceneCollection from 'path/to/your/scene/collection.d.ts'; 67 | import * as ObsWebsocketJs from 'obs-websocket-js'; 68 | 69 | const obs = new OBSWebSocket(); 70 | obs.connect({ address: 'localhost:4444', password: '$up3rSecretP@ssw0rd' }).then(() => { 71 | obs.send('SetCurrentScene', { 72 | 'scene-name': SceneCollection.Scene.main_stage_full_camera, 73 | }); 74 | }); 75 | ``` 76 | 77 | You can also run `typed-obs-scenes` (with no arguments) at any time, and it will print a list of its options and how to use them: 78 | 79 | ``` 80 | Options: 81 | --help Show help [boolean] 82 | --version Show version number [boolean] 83 | --input the OBS Scene Collection to generate typedefs from 84 | [string] [required] 85 | --output the location to write the generated typedefs to [string] 86 | --case the casing to use in the enum key names 87 | [choices: "camel", "snake"] [default: "snake"] 88 | 89 | Missing required argument: input 90 | ``` 91 | 92 | # License 93 | 94 | [MIT](LICENSE) 95 | --------------------------------------------------------------------------------