├── .eslintrc.json ├── .github ├── renovate.json └── workflows │ ├── new-dependencies-advisor.yml │ └── nodejs.yml ├── .gitignore ├── .husky └── pre-commit ├── .npmrc ├── .prettierrc ├── .snyk ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── docs ├── NEW_DESIGN.MD └── OLD_DESIGN.MD ├── integration_test ├── createWindowlessAppUtils.test.ts └── index.test.ts ├── jest.config.ts ├── lsp └── conf │ └── xml.json ├── package-lock.json ├── package.json ├── resources └── docs │ ├── dist.png │ ├── interactive.gif │ ├── logo.gif │ └── usage.gif ├── src ├── checkNodeVersion.ts ├── cliParser.ts ├── consts.ts ├── createWindowlessApp.ts ├── createWindowlessAppUtils.ts ├── dependencies │ ├── dependenciesManager.ts │ └── index.ts ├── files │ ├── fileManager.ts │ ├── fileUtils.ts │ └── index.ts ├── index.ts ├── interactive.ts ├── launcherCompiler.ts ├── main.ts ├── nodeUtils.ts ├── packageJson │ ├── index.ts │ ├── packageJsonBuilder.ts │ └── packageJsonConsts.ts └── validation.ts ├── templates ├── .husky │ └── pre-commit ├── common │ └── sea-config.json ├── javascript │ ├── launcher │ │ └── launcherCompiler.js │ ├── src │ │ └── index.js │ ├── utils │ │ └── checkNodeVersion.js │ └── webpack.config.js ├── launcher │ ├── launcher.cs │ ├── launcher.csproj │ └── launcher.ico └── typescript │ ├── launcher │ └── launcherCompiler.ts │ ├── src │ └── index.ts │ ├── tsconfig.build.json │ ├── tsconfig.json │ ├── utils │ └── checkNodeVersion.ts │ └── webpack.config.ts ├── test ├── checkNodeVersion.test.ts ├── cliParser.test.ts ├── createWindowlessApp.test.ts ├── files │ └── fileUtils.test.ts ├── launcherCompiler.test.ts ├── packageJson │ ├── __snapshots__ │ │ └── packageJsonBuilder.test.ts.snap │ └── packageJsonBuilder.test.ts └── validation.test.ts ├── tsconfig.build.json └── tsconfig.json /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "es6": true, 4 | "node": true, 5 | "jest": true, 6 | "jest/globals": true 7 | }, 8 | "globals": { 9 | "Atomics": "readonly", 10 | "SharedArrayBuffer": "readonly" 11 | }, 12 | "overrides": [ 13 | { 14 | "files": [".js", ".ts"] 15 | }, 16 | { 17 | "files": ["**/*.ts"], 18 | "rules": { 19 | "@typescript-eslint/ban-ts-comment": 0 20 | } 21 | }, 22 | { 23 | "files": ["templates/**/*.js"], 24 | "rules": { 25 | "@typescript-eslint/no-var-requires": 0 26 | } 27 | }, 28 | { 29 | "files": ["test/**/*.ts"], 30 | "rules": { 31 | "max-lines-per-function": 0 32 | } 33 | } 34 | ], 35 | "parser": "@typescript-eslint/parser", 36 | "parserOptions": { 37 | "project": "tsconfig.json", 38 | "ecmaVersion": 2018, 39 | "sourceType": "module" 40 | }, 41 | "plugins": ["n", "@typescript-eslint", "import", "jest", "security"], 42 | "extends": [ 43 | "plugin:n/recommended", 44 | "plugin:import/errors", 45 | "plugin:import/warnings", 46 | "plugin:@typescript-eslint/eslint-recommended", 47 | "plugin:@typescript-eslint/recommended", 48 | "plugin:security/recommended" 49 | ], 50 | "rules": { 51 | // code 52 | "max-params": ["warn", 4], 53 | "max-depth": ["error", 3], 54 | "max-statements-per-line": ["error", { "max": 1 }], 55 | "max-lines": ["error", { "max": 1000, "skipBlankLines": true, "skipComments": true }], 56 | "max-lines-per-function": ["warn", { "max": 75, "skipBlankLines": true, "skipComments": true }], 57 | "@typescript-eslint/ban-ts-comment": ["warn"], 58 | "@typescript-eslint/interface-name-prefix": [0, "never"], 59 | "arrow-parens": ["error"], 60 | "quote-props": ["error", "consistent-as-needed", { "numbers": true }], 61 | "no-unused-vars": "warn", 62 | "no-useless-escape": "error", 63 | "no-empty-pattern": "error", 64 | "no-eval": "error", 65 | "no-implied-eval": "error", 66 | "no-prototype-builtins": "error", 67 | "prefer-const": 0, 68 | "no-process-exit": 0, 69 | "n/no-sync": 0, 70 | "n/exports-style": ["error", "module.exports"], 71 | "n/no-process-exit": 0, 72 | "n/no-unpublished-require": 0, 73 | "n/no-extraneous-import": 0, 74 | "n/no-deprecated-api": ["warn"], 75 | "n/no-missing-require": [ 76 | "error", 77 | { 78 | "tryExtensions": [".ts", ".js", ".d.ts", ".json", ".node"], 79 | "allowModules": ["clean-webpack-plugin", "copy-webpack-plugin"] 80 | } 81 | ], 82 | "n/no-missing-import": 0, 83 | "n/no-unpublished-import": 0, 84 | "n/no-unsupported-features/es-syntax": 0, 85 | "import/extensions": [ 86 | "error", 87 | { 88 | "ts": "never", 89 | "js": "never", 90 | "json": "always" 91 | } 92 | ], 93 | "import/named": "warn", 94 | "import/no-duplicates": "error", 95 | "import/no-unresolved": 0, 96 | "import/default": 0, 97 | "@typescript-eslint/no-unused-vars": "warn", 98 | "@typescript-eslint/no-inferrable-types": 0, 99 | "@typescript-eslint/no-empty-function": 0, 100 | "@typescript-eslint/no-use-before-define": "warn", 101 | "@typescript-eslint/no-var-requires": "warn", 102 | "@typescript-eslint/consistent-type-imports": ["error", { "prefer": "type-imports" }], 103 | // Style 104 | "max-len": ["error", { "code": 200 }], 105 | "indent": ["error", 4, { "SwitchCase": 1 }], 106 | "linebreak-style": ["error", "windows"], 107 | "quotes": ["error", "double"], 108 | "semi": ["error", "always"], 109 | "brace-style": ["error", "stroustrup"], 110 | "object-curly-spacing": ["error", "always"], 111 | "no-mixed-spaces-and-tabs": "error", 112 | "arrow-spacing": ["error"], 113 | "comma-dangle": ["error", "never"], 114 | "comma-style": ["error"], 115 | "no-extra-semi": "error", 116 | "comma-spacing": "error", 117 | "space-in-parens": ["error", "never"], 118 | "space-before-blocks": "error", 119 | "space-before-function-paren": ["error", { "anonymous": "never", "named": "never", "asyncArrow": "always" }], 120 | "keyword-spacing": "error", 121 | "one-var": ["error", "never"] 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["config:base"], 3 | "labels": ["renovate", "dependencies"], 4 | "packageRules": [ 5 | { 6 | "depTypeList": ["dependencies"], 7 | "labels": ["dependencies"], 8 | "updateTypes": ["minor", "patch"], 9 | "automerge": true 10 | }, 11 | { 12 | "depTypeList": ["devDependencies"], 13 | "labels": ["dev-dependencies"], 14 | "updateTypes": ["minor", "patch"], 15 | "automerge": true 16 | } 17 | ], 18 | "prConcurrentLimit": 5, 19 | "ignoreNpmrcFile": true 20 | } 21 | -------------------------------------------------------------------------------- /.github/workflows/new-dependencies-advisor.yml: -------------------------------------------------------------------------------- 1 | name: New dependencies advisor 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | new-dependencies-advisor: 10 | runs-on: windows-latest 11 | 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v4.2.2 15 | 16 | - name: New dependencies advisor 17 | uses: lirantal/github-action-new-dependencies-advisor@v1.2.0 18 | with: 19 | token: ${{ secrets.GITHUB_TOKEN }} 20 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | name: Node CI 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - main 7 | push: 8 | tags: 9 | - '*.*.*' 10 | 11 | jobs: 12 | build: 13 | runs-on: windows-latest 14 | strategy: 15 | matrix: 16 | node-version: [ '20', '22' ] 17 | 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@v4.2.2 21 | 22 | - name: Setup NodeJS 23 | uses: actions/setup-node@v4.4.0 24 | with: 25 | node-version: ${{ matrix.node-version }} 26 | cache: npm 27 | env: 28 | NPM_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }} 29 | 30 | - name: setup-msbuild 31 | uses: microsoft/setup-msbuild@v2.0.0 32 | 33 | - name: Dependencies Install 34 | run: npm ci 35 | env: 36 | CI: true 37 | NPM_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }} 38 | 39 | - name: Test 40 | run: npm run test 41 | env: 42 | CI: true 43 | NPM_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }} 44 | 45 | - name: Codecov 46 | if: matrix.node-version == '22' 47 | uses: codecov/codecov-action@v5.4.3 48 | with: 49 | fail_ci_if_error: true 50 | token: ${{ secrets.CODECOV_TOKEN }} 51 | 52 | publish: 53 | runs-on: windows-latest 54 | if: startsWith(github.ref, 'refs/tags/') 55 | needs: build 56 | strategy: 57 | matrix: 58 | node-version: [ '22' ] 59 | 60 | steps: 61 | - name: Checkout 62 | uses: actions/checkout@v4.2.2 63 | 64 | - name: Setup Windows SDK 65 | uses: GuillaumeFalourd/setup-windows10-sdk-action@v2.4 66 | 67 | - name: Add signtool.exe path to PATH environment variable 68 | uses: myci-actions/export-env-var-powershell@1 69 | with: 70 | name: PATH 71 | value: $env:PATH;C:\Program Files (x86)\Windows Kits\10\bin\10.0.17763.0\x86\ 72 | 73 | - name: Setup NodeJS 74 | uses: actions/setup-node@v4.4.0 75 | with: 76 | node-version: ${{ matrix.node-version }} 77 | cache: npm 78 | env: 79 | NPM_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }} 80 | 81 | - name: setup-msbuild 82 | uses: microsoft/setup-msbuild@v2.0.0 83 | 84 | - name: Dependencies Install 85 | run: npm ci 86 | env: 87 | CI: true 88 | NPM_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }} 89 | 90 | - name: Build 91 | run: npm run build:no-test:ci 92 | env: 93 | CI: true 94 | NPM_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }} 95 | 96 | - name: Pre-Publish - install package globally 97 | run: | 98 | FOR /F "tokens=*" %%g IN ('node -e "console.log(require(""./package.json"").version)"') do (SET VERSION=%%g) 99 | npm i -g create-windowless-app-%VERSION%.tgz 100 | shell: cmd 101 | env: 102 | NPM_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }} 103 | 104 | - name: Pre-Publish - test package globally 105 | run: | 106 | create-windowless-app pre-commit-test-app 107 | cd pre-commit-test-app 108 | npm run build 109 | shell: cmd 110 | env: 111 | NPM_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }} 112 | 113 | - name: Publish 114 | run: npm publish 115 | env: 116 | NPM_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }} 117 | 118 | - name: Release 119 | uses: softprops/action-gh-release@v2 120 | with: 121 | files: create-windowless-app-*.tgz 122 | env: 123 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 124 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | *.pid.lock 11 | 12 | # Directory for instrumented libs generated by jscoverage/JSCover 13 | lib-cov 14 | 15 | # Coverage directory used by tools like istanbul 16 | coverage 17 | coverage_integration 18 | 19 | # nyc test coverage 20 | .nyc_output 21 | 22 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 23 | .grunt 24 | 25 | # Bower dependency directory (https://bower.io/) 26 | bower_components 27 | 28 | # node-waf configuration 29 | .lock-wscript 30 | 31 | # Compiled binary addons (https://nodejs.org/api/addons.html) 32 | build/Release 33 | 34 | # Dependency directories 35 | node_modules/ 36 | jspm_packages/ 37 | 38 | # TypeScript v1 declaration files 39 | typings/ 40 | 41 | # Optional npm cache directory 42 | .npm 43 | 44 | # Optional eslint cache 45 | .eslintcache 46 | 47 | # Optional REPL history 48 | .node_repl_history 49 | 50 | # Output of 'npm pack' 51 | *.tgz 52 | 53 | # dotenv environment variables file 54 | .env 55 | 56 | # next.js build output 57 | .next 58 | /create-windowless-app.iml 59 | /.idea/ 60 | /dist/ 61 | /test-out/ 62 | /launcher-dist/ 63 | /.husky/_/husky.sh 64 | /npm-shrinkwrap.json 65 | /create-windowless-app-template/ 66 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | npm run pre-commit 2 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | //registry.npmjs.org/:_authToken=${NPM_AUTH_TOKEN} 2 | save-exact=true 3 | package-lock=true 4 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 200, 3 | "tabWidth": 4, 4 | "useTabs": false, 5 | "semi": true, 6 | "singleQuote": false, 7 | "trailingComma": "none", 8 | "bracketSpacing": true, 9 | "jsxBracketSameLine": false, 10 | "arrowParens": "always" 11 | } 12 | -------------------------------------------------------------------------------- /.snyk: -------------------------------------------------------------------------------- 1 | # Snyk (https://snyk.io) policy file, patches or ignores known vulnerabilities. 2 | version: v1.13.5 3 | # ignores vulnerabilities until expiry date; change duration by modifying expiry date 4 | ignore: 5 | SNYK-JS-LODASH-567746: 6 | - inquirer > lodash: 7 | reason: None available 8 | expires: '2020-06-03T20:18:33.054Z' 9 | patch: {} 10 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Contributor Covenant Code of Conduct 4 | 5 | ## Our Pledge 6 | 7 | We as members, contributors, and leaders pledge to make participation in our 8 | community a harassment-free experience for everyone, regardless of age, body 9 | size, visible or invisible disability, ethnicity, sex characteristics, gender 10 | identity and expression, level of experience, education, socio-economic status, 11 | nationality, personal appearance, race, caste, color, religion, or sexual identity 12 | and orientation. 13 | 14 | We pledge to act and interact in ways that contribute to an open, welcoming, 15 | diverse, inclusive, and healthy community. 16 | 17 | ## Our Standards 18 | 19 | Examples of behavior that contributes to a positive environment for our 20 | community include: 21 | 22 | * Demonstrating empathy and kindness toward other people 23 | * Being respectful of differing opinions, viewpoints, and experiences 24 | * Giving and gracefully accepting constructive feedback 25 | * Accepting responsibility and apologizing to those affected by our mistakes, 26 | and learning from the experience 27 | * Focusing on what is best not just for us as individuals, but for the 28 | overall community 29 | 30 | Examples of unacceptable behavior include: 31 | 32 | * The use of sexualized language or imagery, and sexual attention or 33 | advances of any kind 34 | * Trolling, insulting or derogatory comments, and personal or political attacks 35 | * Public or private harassment 36 | * Publishing others' private information, such as a physical or email 37 | address, without their explicit permission 38 | * Other conduct which could reasonably be considered inappropriate in a 39 | professional setting 40 | 41 | ## Enforcement Responsibilities 42 | 43 | Community leaders are responsible for clarifying and enforcing our standards of 44 | acceptable behavior and will take appropriate and fair corrective action in 45 | response to any behavior that they deem inappropriate, threatening, offensive, 46 | or harmful. 47 | 48 | Community leaders have the right and responsibility to remove, edit, or reject 49 | comments, commits, code, wiki edits, issues, and other contributions that are 50 | not aligned to this Code of Conduct, and will communicate reasons for moderation 51 | decisions when appropriate. 52 | 53 | ## Scope 54 | 55 | This Code of Conduct applies within all community spaces, and also applies when 56 | an individual is officially representing the community in public spaces. 57 | Examples of representing our community include using an official e-mail address, 58 | posting via an official social media account, or acting as an appointed 59 | representative at an online or offline event. 60 | 61 | ## Enforcement 62 | 63 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 64 | reported to the community leaders responsible for enforcement at 65 | [INSERT CONTACT METHOD]. 66 | All complaints will be reviewed and investigated promptly and fairly. 67 | 68 | All community leaders are obligated to respect the privacy and security of the 69 | reporter of any incident. 70 | 71 | ## Enforcement Guidelines 72 | 73 | Community leaders will follow these Community Impact Guidelines in determining 74 | the consequences for any action they deem in violation of this Code of Conduct: 75 | 76 | ### 1. Correction 77 | 78 | **Community Impact**: Use of inappropriate language or other behavior deemed 79 | unprofessional or unwelcome in the community. 80 | 81 | **Consequence**: A private, written warning from community leaders, providing 82 | clarity around the nature of the violation and an explanation of why the 83 | behavior was inappropriate. A public apology may be requested. 84 | 85 | ### 2. Warning 86 | 87 | **Community Impact**: A violation through a single incident or series 88 | of actions. 89 | 90 | **Consequence**: A warning with consequences for continued behavior. No 91 | interaction with the people involved, including unsolicited interaction with 92 | those enforcing the Code of Conduct, for a specified period of time. This 93 | includes avoiding interactions in community spaces as well as external channels 94 | like social media. Violating these terms may lead to a temporary or 95 | permanent ban. 96 | 97 | ### 3. Temporary Ban 98 | 99 | **Community Impact**: A serious violation of community standards, including 100 | sustained inappropriate behavior. 101 | 102 | **Consequence**: A temporary ban from any sort of interaction or public 103 | communication with the community for a specified period of time. No public or 104 | private interaction with the people involved, including unsolicited interaction 105 | with those enforcing the Code of Conduct, is allowed during this period. 106 | Violating these terms may lead to a permanent ban. 107 | 108 | ### 4. Permanent Ban 109 | 110 | **Community Impact**: Demonstrating a pattern of violation of community 111 | standards, including sustained inappropriate behavior, harassment of an 112 | individual, or aggression toward or disparagement of classes of individuals. 113 | 114 | **Consequence**: A permanent ban from any sort of public interaction within 115 | the community. 116 | 117 | ## Attribution 118 | 119 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 120 | version 2.1, available at 121 | [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. 122 | 123 | Community Impact Guidelines were inspired by 124 | [Mozilla's code of conduct enforcement ladder][Mozilla CoC]. 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | [https://www.contributor-covenant.org/faq][FAQ]. Translations are available 128 | at [https://www.contributor-covenant.org/translations][translations]. 129 | 130 | [homepage]: https://www.contributor-covenant.org 131 | [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html 132 | [Mozilla CoC]: https://github.com/mozilla/diversity 133 | [FAQ]: https://www.contributor-covenant.org/faq 134 | [translations]: https://www.contributor-covenant.org/translations 135 | 136 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | :+1::tada: First off, thanks for taking the time to contribute! :tada::+1: 4 | 5 | The following is a set of guidelines for contributing to create-windowless-app. 6 | These are mostly guidelines, not rules. Use your best judgment, and feel free to propose changes to this document in a pull request. 7 | 8 | ## Code of Conduct 9 | 10 | This project and everyone participating in it is governed by a [Code of Conduct](./CODE_OF_CONDUCT.md). By participating, you are expected to uphold this code. 11 | 12 | ## How to contribute to create-windowless-app 13 | 14 | 15 | 16 | ### Tests 17 | 18 | Make sure the code you're adding has decent test coverage. 19 | 20 | Running project tests and coverage: 21 | 22 | ```bash 23 | npm run test 24 | ``` 25 | 26 | ### Commit Guidelines 27 | 28 | The project uses ESLint for standardizing code style -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Yoav Vainrich 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![](https://raw.githubusercontent.com/yoavain/create-windowless-app/main/resources/docs/logo.gif) 2 | # Create Windowless App 3 | [![CodeQL](https://github.com/yoavain/create-windowless-app/workflows/CodeQL/badge.svg)](https://github.com/yoavain/create-windowless-app/actions?query=workflow%3ACodeQL) 4 | [![Actions Status](https://github.com/yoavain/create-windowless-app/workflows/Node%20CI/badge.svg)](https://github.com/yoavain/create-windowless-app/actions) 5 | ![node](https://img.shields.io/node/v/create-windowless-app.svg) 6 | ![types](https://img.shields.io/npm/types/typescript.svg) 7 | ![commit](https://img.shields.io/github/last-commit/yoavain/create-windowless-app.svg) 8 | ![license](https://img.shields.io/npm/l/create-windowless-app.svg) 9 | [![create-windowless-app](https://snyk.io/advisor/npm-package/create-windowless-app/badge.svg)](https://snyk.io/advisor/npm-package/create-windowless-app) 10 | [![Known Vulnerabilities](https://snyk.io/test/github/yoavain/create-windowless-app/badge.svg?targetFile=package.json)](https://snyk.io//test/github/yoavain/create-windowless-app?targetFile=package.json) 11 | [![codecov](https://codecov.io/gh/yoavain/create-windowless-app/branch/main/graph/badge.svg)](https://codecov.io/gh/yoavain/create-windowless-app) 12 | [![Renovate](https://img.shields.io/badge/renovate-enabled-brightgreen.svg)](https://renovatebot.com) 13 | ![visitors](https://visitor-badge.glitch.me/badge?page_id=yoavain.create-windowless-app) 14 | ![Downloads](https://img.shields.io/npm/dm/create-windowless-app.svg) 15 | 16 | Create a windowless Node.js app. 17 | 18 | You can also start from this template repository [create-windowless-app-template](https://github.com/yoavain/create-windowless-app-template) 19 | 20 | Create Windowless App works on Windows 64-bit only
21 | If something doesn't work, please [file an issue](https://github.com/yoavain/create-windowless-app/issues/new). 22 | 23 | 24 | Pre-Requisites for template to work: 25 | * `NodeJS` version `20.0.0` or higher 26 | * `MSBuild.exe` must be in the PATH environment variable 27 | * `signtool` must be in the PATH environment variable 28 | 29 | ## Quick Overview 30 | 31 | ```sh 32 | npx create-windowless-app my-app 33 | ``` 34 | Note: There's an open issue regarding running npx on Windows when the user folder path contains a space. 35 | For more info and a workaround: [npx#146](https://github.com/zkat/npx/issues/146) 36 | 37 |
Or with npm 38 |

39 | You can install create-windowless-app globally: 40 | 41 | ```sh 42 | npm install -g create-windowless-app 43 | ``` 44 | 45 | And then you can run: 46 | ```sh 47 | create-windowless-app my-app 48 | ``` 49 |

50 |
51 | 52 |

53 | npx create-windowless-app my-app 54 |

55 | 56 | Or in interactive mode: 57 | ```sh 58 | npx create-windowless-app --interactive 59 | ``` 60 | 61 |

62 | npx create-windowless-app --interactive 63 |

64 | 65 | 66 | ## Project Structure 67 | 68 | create-windowless-app creates the following files: 69 | ``` 70 | my-app 71 | ├── node_modules 72 | ├── package.json 73 | ├── sea-config.json 74 | ├── tsconfig.json 75 | ├── tsconfig.build.json 76 | ├── webpack.config.js 77 | ├── launcher 78 | │ ├── launcher.cs 79 | │ ├── launcher.csproj 80 | | ├── launcher.ico 81 | | └── launcherCompiler.ts 82 | ├── resources 83 | │ └── bin 84 | │ └── my-app-launcher.exe 85 | └───src 86 | └── index.js 87 | ``` 88 | 89 | No configuration or complicated folder structures, just the files you need to build your app.
90 | 91 | Once the installation is done, you can build the project 92 | ```sh 93 | cd my-app 94 | npm run build 95 | ``` 96 | 97 | Then you can find in your my-app\dist folder the following files: 98 |

99 | dist files 100 |

101 | 102 | * *my-app.exe* is the compiled app, with Node.js bundled (using [Single Executable Applications](https://nodejs.org/api/single-executable-applications.html)) 103 | * *my-app-launcher.exe* is the compiled launcher.cs file that executes my-app.exe without a console window 104 | * *snoretoast-x64.exe* allows windows notification (using [node-notifier](https://github.com/mikaelbr/node-notifier)) 105 | * *my-app.log* will be generated on first run (using [winston logger](https://github.com/winstonjs/winston)) 106 | 107 | ## create-windowless-app CLI 108 | 109 | ``` 110 | create-windowless-app [options] 111 | 112 | Options: 113 | --no-typescript use javascript rather than typescript 114 | --no-husky do not install husky pre-commit hook for building launcher 115 | --icon override default launcher icon file 116 | 117 | --interactive interactive mode 118 | 119 | Only is required. 120 | ``` 121 | 122 | ## Why? 123 | 124 | Node.js does not have a native windowless mode (like java has javaw). 125 | Sometimes, you want to run an app as a scheduled task that runs in the background, or run a long task (i.e. a server) but do not want a console that must always be open. 126 | Best solution I could find is using a script that executes the Node.js in windowless mode 127 | 128 | This package comes to do the following: 129 | 1) Compile a Node.js (javascript/typescript) project into an *.exe file bundled with Node.js, so no installation is needed 130 | 2) Compile a C# launcher that executes the compiled project, in windowless mode 131 | 132 | ## Template project 133 | 134 | The "Hello World" template is a POC containing 2 features you might want when running a windowless app: 135 | 1) Logger 136 | 2) Notifications 137 | 138 | The template project build script does the following things 139 | 1) Compiles TypeScript to JavaScript (if in a TypeScript template) 140 | 2) Runs WebPack to bundle all JavaScript into a single file, and copy binary files into the "dist" folder 141 | 3) Runs Node's single executable applications scripts to compile the single WebPack JavaScript output file to an exe file bundled with Node.js (Currently experimental in Node.js 20) 142 | 143 | -------------------------------------------------------------------------------- /docs/NEW_DESIGN.MD: -------------------------------------------------------------------------------- 1 | * `main`: 2 | * `checkNodeRuntimeVersion` 3 | * check node runtime version 4 | * `checkMsbuildInPath` 5 | * check if msbuild.exe is in the path 6 | * `createWindowlessApp` 7 | * parses command line arguments 8 | * `createApp` 9 | * check name 10 | * creates folder 11 | * writes package.json 12 | * writes sea-config.json 13 | * change dir 14 | * `buildPacakgeJson` 15 | * build package.json file 16 | * `savePacakgeJson` 17 | * save package.json file 18 | * `installDependencies` 19 | * install dependencies 20 | * `installDevDependencies` 21 | * install devDependencies 22 | * `copyCommonFiles` 23 | * copy common files 24 | * `copyTypeScriptFiles` 25 | * copy typeScript files 26 | * `copyJavaScriptFiles` 27 | * copy javaScript files 28 | * `copyOptional` 29 | * copy husky 30 | * copy icon 31 | * `createResourcesBin` 32 | * `compileLauncher` 33 | * runs compiler 34 | * change dir back 35 | 36 | 37 | ## MVP: 38 | * Adding a new file to template, does not require code change 39 | * Simple flow 40 | * Remove the "skipInstall" option 41 | 42 | TBD: 43 | * Should add package.json to template (without dependencies)? 44 | -------------------------------------------------------------------------------- /docs/OLD_DESIGN.MD: -------------------------------------------------------------------------------- 1 | * `main`: 2 | * `checkNodeRuntimeVersion` 3 | * check node runtime version 4 | * `checkMsbuildInPath` 5 | * check if msbuild.exe is in the path 6 | * `createWindowlessApp` 7 | * parses command line arguments 8 | * `createApp` 9 | * check name 10 | * creates folder 11 | * writes package.json 12 | * writes sea-config.json 13 | * change dir 14 | * `run` 15 | * builds dependencies & devDependencies 16 | * `installs` 17 | * install dependencies 18 | * `installs` 19 | * install devDependencies 20 | * `buildTypeScriptProject` 21 | * writes "tsconfig.build.json" 22 | * writes "tsconfig.json" 23 | * writes "webpack.config.ts" 24 | * creates "src" folder 25 | * writes "src/index.ts" 26 | * creates "utils" folder 27 | * writes "utils/checkNodeVersion.ts" 28 | * builds scripts 29 | * creates .husky folder 30 | * writes ".husky/pre-commit" 31 | * writes scripts to package.json 32 | * `buildJavaScriptProject` 33 | * writes "webpack.config.js" 34 | * creates "src" folder 35 | * writes "src/index.js" 36 | * creates "utils" folder 37 | * writes "utils/checkNodeVersion.js" 38 | * builds scripts 39 | * creates .husky folder 40 | * writes ".husky/pre-commit" 41 | * writes scripts to package.json 42 | * creates resources/bin folder 43 | * `buildLauncher` 44 | * creates "launcher" folder 45 | * writes "launcher/launcher.cs" 46 | * writes "launcher/launcher.csproj" 47 | * copy file "launcher/launcherCompiler.ts" or "launcher/launcherCompiler.js" 48 | * if there's an icon, copies "launcher/launcher.ico" 49 | * `compileLauncher` 50 | * runs compiler 51 | * change dir back 52 | -------------------------------------------------------------------------------- /integration_test/createWindowlessAppUtils.test.ts: -------------------------------------------------------------------------------- 1 | import { isSafeToCreateProjectIn } from "../src/createWindowlessAppUtils"; 2 | import { dir } from "tmp-promise"; 3 | import path from "path"; 4 | import { promises as fs } from "fs"; 5 | 6 | describe("Test createWindowlessAppUtils", () => { 7 | describe("Test isSafeToCreateProjectIn", () => { 8 | it ("Should return true on an empty folder", async () => { 9 | const d = await dir(); 10 | try { 11 | const safe: boolean = isSafeToCreateProjectIn(d.path, "test-1234"); 12 | expect(safe).toBe(true); 13 | } 14 | finally { 15 | if (d?.cleanup) { 16 | await d.cleanup(); 17 | } 18 | } 19 | }); 20 | it ("Should return true on a folder with previous log file", async () => { 21 | const d = await dir(); 22 | try { 23 | await fs.writeFile(path.join(d.path, "npm-debug.log"), "old log", { encoding: "utf-8" }); 24 | const safe: boolean = isSafeToCreateProjectIn(d.path, "test-1234"); 25 | expect(safe).toBe(true); 26 | } 27 | finally { 28 | if (d?.cleanup) { 29 | await d.cleanup(); 30 | } 31 | } 32 | }); 33 | it ("Should return true on a folder with a file in it", async () => { 34 | const d = await dir(); 35 | const existingFile = path.join(d.path, "package.json"); 36 | try { 37 | await fs.writeFile(existingFile, "{}", { encoding: "utf-8" }); 38 | const safe: boolean = isSafeToCreateProjectIn(d.path, "test-1234"); 39 | expect(safe).toBe(false); 40 | } 41 | finally { 42 | await fs.unlink(existingFile); 43 | if (d?.cleanup) { 44 | await d.cleanup(); 45 | } 46 | } 47 | }); 48 | }); 49 | }); -------------------------------------------------------------------------------- /integration_test/index.test.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import { randomUUID as uuid } from "crypto"; 3 | import type { ExecException } from "child_process"; 4 | import { exec } from "child_process"; 5 | import * as del from "del"; 6 | import { consts } from "../src/consts"; 7 | import { existsSync, pathExistsSync, readFileSync } from "fs-extra"; 8 | 9 | jest.setTimeout(300000); 10 | 11 | let SANDBOXES = new Set(); 12 | 13 | type CliResult = { 14 | code: number; 15 | stdout?: string; 16 | stderr?: string; 17 | error?: ExecException; 18 | }; 19 | 20 | // Remove fixed type in list, i.e. "node-notifier@9" => "node-notifier" 21 | const cleanExpectedDependencies = (expectedDependencies: string[]): string[] => expectedDependencies.map((dep) => dep.lastIndexOf("@") > 0 ? dep.substring(0, dep.lastIndexOf("@")) : dep); 22 | 23 | export const readJsonFile = (jsonFileName: string) => { 24 | return JSON.parse(readFileSync(jsonFileName, "utf8")); 25 | }; 26 | 27 | 28 | const testFilesExists = (root: string, typescript: boolean = true, husky: boolean = true): void => { 29 | // Files 30 | const scriptExt: string = typescript ? "ts" : "js"; 31 | expect(existsSync(path.resolve(root, "package.json"))).toBeTruthy(); 32 | expect(existsSync(path.resolve(root, "sea-config.json"))).toBeTruthy(); 33 | expect(existsSync(path.resolve(root, `webpack.config.${scriptExt}`))).toBeTruthy(); 34 | expect(existsSync(path.resolve(root, "tsconfig.build.json"))).toEqual(typescript); 35 | expect(existsSync(path.resolve(root, "tsconfig.json"))).toEqual(typescript); 36 | expect(existsSync(path.resolve(root, "src", `index.${scriptExt}`))).toBeTruthy(); 37 | expect(existsSync(path.resolve(root, "utils", `checkNodeVersion.${scriptExt}`))).toBeTruthy(); 38 | expect(existsSync(path.resolve(root, "launcher", "launcher.cs"))).toBeTruthy(); 39 | expect(existsSync(path.resolve(root, "launcher", "launcher.csproj"))).toBeTruthy(); 40 | expect(existsSync(path.resolve(root, "launcher", "launcher.ico"))).toBeTruthy(); 41 | expect(existsSync(path.resolve(root, "launcher", `launcherCompiler.${scriptExt}`))).toBeTruthy(); 42 | expect(existsSync(path.resolve(root, "resources", "bin", `${root}-launcher.exe`))).toBeTruthy(); 43 | expect(pathExistsSync(path.resolve(root, "node_modules"))).toEqual(true); 44 | if (husky) { 45 | expect(pathExistsSync(path.resolve(root, ".husky"))).toEqual(husky); 46 | expect(existsSync(path.resolve(root, ".husky", "pre-commit"))).toEqual(husky); 47 | } 48 | 49 | const packageJson = readJsonFile(path.resolve(root, "package.json")); 50 | 51 | // Dependencies 52 | let expectedDependencies = consts.dependencies; 53 | let expectedDevDependencies = consts.devDependencies; 54 | if (typescript) { 55 | expectedDevDependencies = expectedDevDependencies.concat(consts.tsDevDependencies); 56 | } 57 | if (husky) { 58 | expectedDevDependencies = expectedDevDependencies.concat(consts.huskyDependencies); 59 | } 60 | expect(Object.keys(packageJson.dependencies).sort()).toEqual(cleanExpectedDependencies(expectedDependencies).sort()); 61 | expect(Object.keys(packageJson.devDependencies).sort()).toEqual(cleanExpectedDependencies(expectedDevDependencies).sort()); 62 | }; 63 | 64 | const cli = (args: string[], cwd?: string): Promise => { 65 | return new Promise((resolve) => { 66 | const nycLocation: string = path.resolve("node_modules", ".bin", "nyc"); 67 | const indexLocation: string = `${path.resolve("src/index.ts")}`; 68 | const command: string = `${nycLocation} --reporter lcov --reporter json --report-dir coverage_integration -- node -r ts-node/register/transpile-only ${indexLocation} ${args.join(" ")}`; 69 | console.log(`Testing command: ${command}`); 70 | exec(command, { cwd }, (error, stdout, stderr) => { 71 | if (error) { 72 | console.log(JSON.stringify(error)); 73 | console.log(JSON.stringify({ stdout, stderr })); 74 | } 75 | resolve({ 76 | code: error && error.code ? error.code : 0, 77 | error, 78 | stdout, 79 | stderr 80 | }); 81 | }); 82 | }); 83 | }; 84 | 85 | describe("Test CLI", () => { 86 | afterAll(() => { 87 | SANDBOXES.forEach((sandbox) => { 88 | del.sync(sandbox); 89 | }); 90 | }); 91 | 92 | it("should create a prototype project with default flags", async () => { 93 | const sandbox: string = uuid(); 94 | SANDBOXES.add(sandbox); 95 | const result: CliResult = await cli([sandbox], "."); 96 | console.log(JSON.stringify(result, null, "\t")); 97 | expect(result.code).toBe(0); 98 | expect(result.error).toBeFalsy(); 99 | testFilesExists(sandbox); 100 | del.sync(sandbox); 101 | }); 102 | 103 | it("should create a prototype project with flags: --no-typescript", async () => { 104 | const sandbox: string = uuid(); 105 | SANDBOXES.add(sandbox); 106 | const result: CliResult = await cli([sandbox, "--no-typescript"], "."); 107 | console.log(JSON.stringify(result, null, "\t")); 108 | expect(result.code).toBe(0); 109 | expect(result.error).toBeFalsy(); 110 | testFilesExists(sandbox, false); 111 | del.sync(sandbox); 112 | }); 113 | 114 | it("should create a prototype project with flags: --no-husky", async () => { 115 | const sandbox: string = uuid(); 116 | SANDBOXES.add(sandbox); 117 | const result: CliResult = await cli([sandbox, "--no-husky"], "."); 118 | console.log(JSON.stringify(result, null, "\t")); 119 | expect(result.code).toBe(0); 120 | expect(result.error).toBeFalsy(); 121 | testFilesExists(sandbox, true, false); 122 | del.sync(sandbox); 123 | }); 124 | }); 125 | -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "@jest/types"; 2 | 3 | const config: Config.InitialOptions = { 4 | transform: { 5 | "^.+\\.ts$": "ts-jest" 6 | }, 7 | testEnvironment: "node", 8 | testRegex: "(test|integration_test)/.*.test.ts$", 9 | moduleFileExtensions: ["ts", "js", "json", "node"], 10 | maxWorkers: "50%", 11 | verbose: true, 12 | collectCoverage: true, 13 | coverageDirectory: "coverage", 14 | coverageReporters: [ 15 | "text", 16 | "text-summary", 17 | "json", 18 | "lcov", 19 | "clover" 20 | ], 21 | collectCoverageFrom: ["src/**/*.ts", "!**/node_modules/**"] 22 | }; 23 | 24 | export default config; 25 | -------------------------------------------------------------------------------- /lsp/conf/xml.json: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yoavain/create-windowless-app/7725de9044b570e8f5822cdbd781379fa05a5bc0/lsp/conf/xml.json -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "create-windowless-app", 3 | "version": "11.0.3", 4 | "description": "Create a windowless NodeJS app", 5 | "main": "dist/index.js", 6 | "types": "dist/index.d.ts", 7 | "bin": { 8 | "create-windowless-app": "./dist/index.js" 9 | }, 10 | "scripts": { 11 | "prepare": "npm run husky-install && npx fix-lockfile-integrity", 12 | "husky-install": "git config --get core.hookspath || husky", 13 | "prebuild": "npm run test", 14 | "build": "npm run build:no-test", 15 | "build:no-test": "npm run tsc && npm run add-shebang && npm run package", 16 | "build:no-test:ci": "npm run tsc && npm run add-shebang && del package-lock.json && npm prune --omit=dev && npm shrinkwrap && npm run package", 17 | "test": "npm run eslint && npm run type-check && npm run jest", 18 | "eslint": "eslint src/ test/ integration_test/ templates/", 19 | "eslint:fix": "npm run eslint -- --fix", 20 | "type-check": "tsc --build tsconfig.json", 21 | "jest": "cross-env FORCE_COLOR=0 jest", 22 | "jest:unit": "cross-env FORCE_COLOR=0 jest ./test/", 23 | "jest:integration": "cross-env FORCE_COLOR=0 jest ./integration_test/", 24 | "prettier": "prettier --write *.json templates/**/*.json", 25 | "pretsc": "rimraf dist", 26 | "tsc": "tsc --build tsconfig.build.json", 27 | "add-shebang": "add-shebang", 28 | "start": "ts-node src/index.ts", 29 | "start:help": "npm run start -- --help", 30 | "package": "npm pack", 31 | "pre-commit": "lint-staged" 32 | }, 33 | "repository": { 34 | "type": "git", 35 | "url": "git+https://github.com/yoavain/create-windowless-app.git" 36 | }, 37 | "keywords": [ 38 | "windowless", 39 | "no console", 40 | "nodew", 41 | "template", 42 | "typescript" 43 | ], 44 | "author": "yoavain", 45 | "license": "MIT", 46 | "engines": { 47 | "node": ">=20", 48 | "npm": ">=9" 49 | }, 50 | "files": [ 51 | "dist/**", 52 | "templates/**" 53 | ], 54 | "bugs": { 55 | "url": "https://github.com/yoavain/create-windowless-app/issues" 56 | }, 57 | "homepage": "https://github.com/yoavain/create-windowless-app#readme", 58 | "devDependencies": { 59 | "@tsconfig/node20": "20.1.5", 60 | "@types/cross-spawn": "6.0.6", 61 | "@types/fs-extra": "11.0.4", 62 | "@types/jest": "29.5.14", 63 | "@types/mock-fs": "4.13.4", 64 | "@types/node": "20.19.0", 65 | "@types/node-notifier": "8.0.5", 66 | "@types/validate-npm-package-name": "4.0.2", 67 | "@types/winston": "2.4.4", 68 | "@typescript-eslint/eslint-plugin": "7.18.0", 69 | "@typescript-eslint/parser": "7.18.0", 70 | "add-shebang": "0.1.0", 71 | "copy-webpack-plugin": "13.0.0", 72 | "cross-env": "7.0.3", 73 | "del": "6.1.1", 74 | "eslint": "8.57.1", 75 | "eslint-plugin-import": "2.31.0", 76 | "eslint-plugin-jest": "28.13.0", 77 | "eslint-plugin-n": "17.19.0", 78 | "eslint-plugin-security": "1.7.1", 79 | "global-npm": "0.5.0", 80 | "husky": "9.1.7", 81 | "jest": "29.7.0", 82 | "lint-staged": "15.5.2", 83 | "mock-fs": "5.5.0", 84 | "mocked-env": "1.3.5", 85 | "node-notifier": "10.0.1", 86 | "nyc": "17.1.0", 87 | "postject": "1.0.0-alpha.6", 88 | "prettier": "3.5.3", 89 | "rimraf": "6.0.1", 90 | "tmp-promise": "3.0.3", 91 | "ts-jest": "29.3.4", 92 | "ts-loader": "9.5.2", 93 | "ts-node": "10.9.2", 94 | "typescript": "5.8.3", 95 | "webpack": "5.99.9", 96 | "webpack-cli": "6.0.1", 97 | "winston": "3.17.0" 98 | }, 99 | "dependencies": { 100 | "chalk": "4.1.2", 101 | "cross-spawn": "7.0.6", 102 | "fs-extra": "11.3.0", 103 | "inquirer": "11.1.0", 104 | "validate-npm-package-name": "6.0.1", 105 | "yargs": "17.7.2" 106 | }, 107 | "lint-staged": { 108 | "*.(ts|js)": [ 109 | "eslint --fix" 110 | ], 111 | "*.json": [ 112 | "prettier --write" 113 | ] 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /resources/docs/dist.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yoavain/create-windowless-app/7725de9044b570e8f5822cdbd781379fa05a5bc0/resources/docs/dist.png -------------------------------------------------------------------------------- /resources/docs/interactive.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yoavain/create-windowless-app/7725de9044b570e8f5822cdbd781379fa05a5bc0/resources/docs/interactive.gif -------------------------------------------------------------------------------- /resources/docs/logo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yoavain/create-windowless-app/7725de9044b570e8f5822cdbd781379fa05a5bc0/resources/docs/logo.gif -------------------------------------------------------------------------------- /resources/docs/usage.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yoavain/create-windowless-app/7725de9044b570e8f5822cdbd781379fa05a5bc0/resources/docs/usage.gif -------------------------------------------------------------------------------- /src/checkNodeVersion.ts: -------------------------------------------------------------------------------- 1 | const MIN_MAJOR_VERSION = 20; 2 | const MIN_MINOR_VERSION = 0; 3 | 4 | export const checkNodeRuntimeVersion = () => { 5 | const currentNodeVersion: string = process.versions.node; 6 | const semver: string[] = currentNodeVersion.split("."); 7 | const major: number = Number(semver[0]); 8 | const minor: number = Number(semver[1]); 9 | 10 | if (Number.isNaN(major) || Number.isNaN(minor) || major < MIN_MAJOR_VERSION || (major === MIN_MAJOR_VERSION && minor < MIN_MINOR_VERSION)) { 11 | console.error(`You are running NodeJS ${currentNodeVersion}.\nCreate Windowless App requires NodeJS ${MIN_MAJOR_VERSION}.${MIN_MINOR_VERSION} or higher.\nPlease update your version of Node.`); 12 | process.exit(1); 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /src/cliParser.ts: -------------------------------------------------------------------------------- 1 | import chalk from "chalk"; 2 | import { PACKAGE_JSON_FILENAME } from "./createWindowlessAppUtils"; 3 | import { pathExistsSync } from "fs-extra"; 4 | import yargs from "yargs"; 5 | import { hideBin } from "yargs/helpers"; 6 | import { interactiveMode } from "./interactive"; 7 | import { validateProjectNameInput } from "./validation"; 8 | 9 | // eslint-disable-next-line @typescript-eslint/no-var-requires 10 | const packageJson = require(`../${PACKAGE_JSON_FILENAME}`); 11 | 12 | export type ProgramConfig = { 13 | projectName: string; 14 | icon?: string; 15 | typescript: boolean; 16 | husky: boolean; 17 | verbose: boolean; 18 | }; 19 | 20 | const validateInput = (argv) => { 21 | if (argv.icon && !pathExistsSync(argv.icon)) { 22 | console.warn(`Cannot find icon in ${chalk.red(argv.icon)}. Switching to ${chalk.green("default")} icon.`); 23 | delete argv.icon; 24 | } 25 | 26 | return argv; 27 | }; 28 | 29 | export const parseCommand = async (argv: string[]): Promise => { 30 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 31 | const command: any = yargs(hideBin(argv)) 32 | .command("* [projectName]", "project name", 33 | (yargs) => { 34 | return yargs.positional("projectName", { 35 | describe: "project name", 36 | type: "string" 37 | }); 38 | }, 39 | () => {}) 40 | .option("verbose", { 41 | alias: "v", 42 | type: "boolean", 43 | description: "print additional logs" 44 | }) 45 | .option("interactive", { 46 | type: "boolean", 47 | alias: "i", 48 | description: "interactive mode" 49 | }) 50 | .option("typescript", { 51 | alias: "t", 52 | type: "boolean", 53 | description: "use typescript", 54 | default: true 55 | }) 56 | .option("husky", { 57 | alias: "h", 58 | type: "boolean", 59 | description: "install husky pre-commit hook for building launcher", 60 | default: true 61 | }) 62 | .option("icon", { 63 | alias: "c", 64 | type: "string", 65 | description: "override default launcher icon file" 66 | }) 67 | .check(({ projectName, interactive, help }) => { 68 | if (projectName && typeof validateProjectNameInput(projectName as string) === "string") { 69 | throw new Error("Invalid project name"); 70 | } 71 | else if (!projectName && !interactive && !help) { 72 | throw new Error("Missing project name"); 73 | } 74 | return true; 75 | }) 76 | .version("version", packageJson.version) 77 | .help() 78 | .middleware(validateInput) 79 | .strict() 80 | .argv; 81 | 82 | let programConfig: ProgramConfig; 83 | if (command.interactive) { 84 | programConfig = await interactiveMode(); 85 | } 86 | else { 87 | programConfig = { 88 | projectName : command.projectName, 89 | verbose: command.verbose, 90 | typescript: command.typescript, 91 | husky: command.husky, 92 | icon: command.icon 93 | }; 94 | } 95 | 96 | return programConfig; 97 | }; 98 | -------------------------------------------------------------------------------- /src/consts.ts: -------------------------------------------------------------------------------- 1 | export const consts = { 2 | dependencies: [ 3 | "node-notifier", 4 | "winston" 5 | ], 6 | devDependencies: [ 7 | "fs-extra", 8 | "jest", 9 | "webpack", 10 | "webpack-cli", 11 | "copy-webpack-plugin", 12 | "rimraf", 13 | "cross-spawn", 14 | "postject" 15 | ], 16 | huskyDependencies: [ 17 | "husky" 18 | ], 19 | tsDevDependencies: [ 20 | "@types/jest", 21 | "@types/node", 22 | "@tsconfig/node20", 23 | "@types/node-notifier", 24 | "@types/winston", 25 | "ts-loader", 26 | "ts-node", 27 | "typescript", 28 | "@types/cross-spawn" 29 | ], 30 | errorLogFilePatterns: [ 31 | "npm-debug.log" 32 | ], 33 | validFiles: [ 34 | ".DS_Store", 35 | "Thumbs.db", 36 | ".git", 37 | ".gitignore", 38 | ".idea", 39 | "README.md", 40 | "LICENSE", 41 | ".hg", 42 | ".hgignore", 43 | ".hgcheck", 44 | ".npmignore", 45 | "mkdocs.yml", 46 | "docs", 47 | ".travis.yml", 48 | ".gitlab-ci.yml", 49 | ".gitattributes" 50 | ], 51 | knownGeneratedFiles: [ 52 | "package.json", 53 | "sea-config.json", 54 | "webpack.config.js", 55 | "tsconfig.json", 56 | "tsconfig.build.json", 57 | "src", 58 | "resources", 59 | "launcher", 60 | "node_modules" 61 | ] 62 | }; 63 | -------------------------------------------------------------------------------- /src/createWindowlessApp.ts: -------------------------------------------------------------------------------- 1 | import chalk from "chalk"; 2 | import * as path from "path"; 3 | import { ensureDirSync, readdirSync, removeSync } from "fs-extra"; 4 | import { compileLauncher } from "./launcherCompiler"; 5 | import { consts } from "./consts"; 6 | import { checkAppName, isSafeToCreateProjectIn } from "./createWindowlessAppUtils"; 7 | import { checkThatNpmCanReadCwd } from "./nodeUtils"; 8 | import type { ProgramConfig } from "./cliParser"; 9 | import { parseCommand } from "./cliParser"; 10 | import { PackageJsonBuilder } from "./packageJson"; 11 | import { DependenciesManager } from "./dependencies"; 12 | import { FileManager, writeJson } from "./files"; 13 | 14 | const run = async (root: string, appName: string, originalDirectory: string, programConfig: ProgramConfig): Promise => { 15 | const { typescript, husky, icon, verbose } = programConfig; 16 | 17 | try { 18 | const dependenciesManager: DependenciesManager = new DependenciesManager(typescript, husky); 19 | await dependenciesManager.installAll(verbose); 20 | 21 | const fileManager: FileManager = new FileManager(root, appName, typescript, husky, icon); 22 | await fileManager.copyTemplate(); 23 | 24 | // Launcher 25 | ensureDirSync(path.resolve(root, "resources", "bin")); 26 | await compileLauncher(); 27 | 28 | console.log("Done"); 29 | } 30 | catch (reason) { 31 | console.log(); 32 | console.log("Aborting installation."); 33 | if (reason.command) { 34 | console.log(` ${chalk.cyan(reason.command)} has failed.`); 35 | } 36 | else { 37 | console.log(chalk.red("Unexpected error. Please report it as a bug:")); 38 | console.log(reason); 39 | } 40 | console.log(); 41 | 42 | // On 'exit' we will delete these files from target directory. 43 | const knownGeneratedFiles = [...consts.knownGeneratedFiles]; 44 | const currentFiles = readdirSync(path.join(root)); 45 | currentFiles.forEach((file) => { 46 | knownGeneratedFiles.forEach((fileToMatch) => { 47 | // This removes all knownGeneratedFiles. 48 | if (file === fileToMatch) { 49 | console.log(`Deleting generated file... ${chalk.cyan(file)}`); 50 | removeSync(path.join(root, file)); 51 | } 52 | }); 53 | }); 54 | const remainingFiles = readdirSync(path.join(root)); 55 | if (!remainingFiles.length) { 56 | // Delete target folder if empty 57 | console.log(`Deleting ${chalk.cyan(`${appName}/`)} from ${chalk.cyan(path.resolve(root, ".."))}`); 58 | process.chdir(path.resolve(root, "..")); 59 | removeSync(path.join(root)); 60 | } 61 | console.log("Done (with errors)."); 62 | process.exit(1); 63 | } 64 | }; 65 | 66 | const createApp = async (programConfig: ProgramConfig): Promise => { 67 | const { projectName, typescript, husky } = programConfig; 68 | const root: string = path.resolve(projectName); 69 | const appName: string = path.basename(root); 70 | 71 | checkAppName(appName); 72 | ensureDirSync(projectName); 73 | if (!isSafeToCreateProjectIn(root, projectName)) { 74 | process.exit(1); 75 | } 76 | 77 | console.log(`Creating a new windowless app in ${chalk.green(root)}.`); 78 | console.log(); 79 | 80 | const originalDirectory = process.cwd(); 81 | process.chdir(root); 82 | if (!checkThatNpmCanReadCwd()) { 83 | process.chdir(originalDirectory); 84 | process.exit(1); 85 | } 86 | 87 | try { 88 | // package.json 89 | const packageJson = new PackageJsonBuilder(appName); 90 | if (!typescript) { 91 | packageJson.withJavaScript(); 92 | } 93 | if (husky) { 94 | packageJson.withHusky(); 95 | } 96 | writeJson(path.join(root, "package.json"), packageJson.build()); 97 | 98 | await run(root, appName, originalDirectory, programConfig); 99 | } 100 | finally { 101 | process.chdir(originalDirectory); 102 | } 103 | }; 104 | 105 | export const createWindowlessApp = async (argv: string[]): Promise => { 106 | const programConfig: ProgramConfig = await parseCommand(argv); 107 | 108 | if (programConfig.projectName) { 109 | return createApp(programConfig); 110 | } 111 | }; 112 | -------------------------------------------------------------------------------- /src/createWindowlessAppUtils.ts: -------------------------------------------------------------------------------- 1 | import type { InvalidNames, LegacyNames } from "validate-npm-package-name"; 2 | import validateProjectName from "validate-npm-package-name"; 3 | import chalk from "chalk"; 4 | import path from "path"; 5 | import { consts } from "./consts"; 6 | import { readdirSync, removeSync } from "fs-extra"; 7 | 8 | // These files should be allowed to remain on a failed install, but then silently removed during the next create. 9 | const errorLogFilePatterns = consts.errorLogFilePatterns; 10 | 11 | export const PACKAGE_JSON_FILENAME = "package.json"; 12 | 13 | // If project only contains files generated by GH, it’s safe. 14 | // Also, if project contains remnant error logs from a previous installation, lets remove them now. 15 | // We also special case IJ-based products .idea because it integrates with CRA: 16 | // https://github.com/facebook/create-react-app/pull/368#issuecomment-243446094 17 | export const isSafeToCreateProjectIn = (root: string, name: string): boolean => { 18 | const validFiles: string[] = consts.validFiles; 19 | console.log(); 20 | 21 | const conflicts = readdirSync(root) 22 | .filter((file) => !validFiles.includes(file)) 23 | // IntelliJ IDEA creates module files before CRA is launched 24 | .filter((file) => !/\.iml$/.test(file)) 25 | // Don't treat log files from previous installation as conflicts 26 | .filter((file) => !errorLogFilePatterns.some((pattern) => file.indexOf(pattern) === 0)); 27 | 28 | if (conflicts.length > 0) { 29 | console.log(`The directory ${chalk.green(name)} contains files that could conflict:`); 30 | console.log(); 31 | for (const file of conflicts) { 32 | console.log(` ${file}`); 33 | } 34 | console.log(); 35 | console.log("Either try using a new directory name, or remove the files listed above."); 36 | 37 | return false; 38 | } 39 | 40 | // Remove any remnant files from a previous installation 41 | const currentFiles = readdirSync(path.join(root)); 42 | currentFiles.forEach((file) => { 43 | errorLogFilePatterns.forEach((errorLogFilePattern) => { 44 | // This will catch `npm-debug.log*` files 45 | if (file.indexOf(errorLogFilePattern) === 0) { 46 | removeSync(path.join(root, file)); 47 | } 48 | }); 49 | }); 50 | return true; 51 | }; 52 | 53 | const printValidationResults = (results) => { 54 | if (typeof results !== "undefined") { 55 | results.forEach((error) => { 56 | console.error(chalk.red(` * ${error}`)); 57 | }); 58 | } 59 | }; 60 | 61 | export const checkAppName = (appName) => { 62 | const validationResult = validateProjectName(appName); 63 | if (!validationResult.validForNewPackages) { 64 | console.error(`Could not create a project called ${chalk.red(`"${appName}"`)} because of npm naming restrictions:`); 65 | printValidationResults((validationResult as InvalidNames).errors); 66 | printValidationResults((validationResult as LegacyNames).warnings); 67 | process.exit(1); 68 | } 69 | 70 | const dependencies = [...consts.dependencies, ...consts.devDependencies].sort(); 71 | if (dependencies.indexOf(appName) >= 0) { 72 | console.error( 73 | chalk.red( 74 | `We cannot create a project called ${chalk.green(appName)} because a dependency with the same name exists.\n` + "Due to the way npm works, the following names are not allowed:\n\n" 75 | ) + 76 | chalk.cyan(dependencies.map((depName) => ` ${depName}`).join("\n")) + 77 | chalk.red("\n\nPlease choose a different project name.") 78 | ); 79 | process.exit(1); 80 | } 81 | }; 82 | 83 | export const replaceAppNamePlaceholder = (appName: string, str: string): string => { 84 | return str.replace(/##APPNAME##/g, `${appName}`); 85 | }; 86 | -------------------------------------------------------------------------------- /src/dependencies/dependenciesManager.ts: -------------------------------------------------------------------------------- 1 | import chalk from "chalk"; 2 | import spawn from "cross-spawn"; 3 | 4 | const dependencies = [ 5 | "node-notifier", 6 | "winston" 7 | ]; 8 | 9 | const devDependencies = [ 10 | "fs-extra", 11 | "jest", 12 | "webpack", 13 | "webpack-cli", 14 | "copy-webpack-plugin", 15 | "rimraf", 16 | "cross-spawn", 17 | "postject" 18 | ]; 19 | 20 | const tsDevDependencies =[ 21 | "@types/jest", 22 | "@types/node", 23 | "@tsconfig/node20", 24 | "@types/node-notifier", 25 | "@types/winston", 26 | "ts-loader", 27 | "ts-node", 28 | "typescript", 29 | "@types/cross-spawn" 30 | ]; 31 | 32 | const huskyDependencies = [ 33 | "husky" 34 | ]; 35 | 36 | const install = async (dependencies: string[], isDev: boolean, verbose?: boolean): Promise => { 37 | const command = "npm"; 38 | const args = ["install", isDev ? "--save-dev" : "--save", "--save-exact", "--loglevel", "error"].concat(dependencies); 39 | if (verbose) { 40 | args.push("--verbose"); 41 | } 42 | console.log(`Installing ${chalk.green(isDev ? "dev dependencies" : "dependencies")}.`); 43 | console.log(); 44 | 45 | spawn.sync(command, args, { stdio: "inherit" }); 46 | }; 47 | 48 | 49 | export class DependenciesManager { 50 | #dependencies: string[] = []; 51 | #devDependencies: string[] = []; 52 | 53 | constructor(typescript: boolean, husky: boolean) { 54 | this.#dependencies = dependencies; 55 | this.#devDependencies = devDependencies; 56 | if (typescript) { 57 | this.#devDependencies = this.#devDependencies.concat(tsDevDependencies); 58 | } 59 | if (husky) { 60 | this.#devDependencies = this.#devDependencies.concat(huskyDependencies); 61 | } 62 | } 63 | 64 | async installAll(verbose?: boolean) { 65 | await install(this.#dependencies, false, verbose); 66 | await install(this.#devDependencies, true, verbose); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/dependencies/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./dependenciesManager"; -------------------------------------------------------------------------------- /src/files/fileManager.ts: -------------------------------------------------------------------------------- 1 | import type { Formatter } from "."; 2 | import { copyFolderRecursiveSync } from "."; 3 | import { replaceAppNamePlaceholder } from "../createWindowlessAppUtils"; 4 | import { copyFileSync } from "fs-extra"; 5 | import path from "path"; 6 | 7 | export class FileManager { 8 | #templatesRoot: string; 9 | #targetRoot: string; 10 | #typeScript: boolean; 11 | #husky: boolean; 12 | #icon: string; 13 | #formatter: Formatter; 14 | 15 | 16 | constructor(targetRoot: string, appName: string, typeScript: boolean, husky: boolean, icon: string) { 17 | this.#templatesRoot = path.resolve(__dirname, "..", "..", "templates"); 18 | this.#targetRoot = targetRoot; 19 | 20 | this.#typeScript = typeScript; 21 | this.#husky = husky; 22 | this.#icon = icon; 23 | 24 | this.#formatter = (str: string) => replaceAppNamePlaceholder(appName, str); 25 | } 26 | 27 | async copyTemplate() { 28 | // common files 29 | copyFolderRecursiveSync(path.resolve(this.#templatesRoot, "common"), path.resolve(this.#targetRoot), this.#formatter); 30 | 31 | // TypeScript or JavaScript 32 | if (this.#typeScript) { 33 | copyFolderRecursiveSync(path.resolve(this.#templatesRoot, "typescript"), path.resolve(this.#targetRoot), this.#formatter); 34 | } 35 | else { 36 | copyFolderRecursiveSync(path.resolve(this.#templatesRoot, "javascript"), path.resolve(this.#targetRoot), this.#formatter); 37 | } 38 | 39 | // Husky 40 | if (this.#husky) { 41 | copyFolderRecursiveSync(path.resolve(this.#templatesRoot, ".husky"), path.resolve(this.#targetRoot, ".husky"), this.#formatter); 42 | } 43 | 44 | // Launcher 45 | copyFolderRecursiveSync(path.resolve(this.#templatesRoot, "launcher"), path.resolve(this.#targetRoot, "launcher"), this.#formatter); 46 | 47 | // Icon 48 | if (this.#icon) { 49 | copyFileSync(path.resolve(this.#icon), path.resolve(this.#targetRoot, "launcher", "launcher.ico")); 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/files/fileUtils.ts: -------------------------------------------------------------------------------- 1 | import { existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "fs-extra"; 2 | import path from "path"; 3 | import os from "os"; 4 | 5 | export const writeJson = (fileName: string, json: object): void => { 6 | writeFileSync(fileName, JSON.stringify(json, null, 2).replace(/\n/g, os.EOL) + os.EOL); 7 | }; 8 | 9 | const TEXT_FORMAT_EXTENSIONS = new Set([".ts", ".js", ".cs", ".json", ".csproj"]); 10 | 11 | export type Formatter = (s: string) => string; 12 | 13 | function copyFileSyncWithFormatter(sourceFile, targetFile, formatter?: Formatter) { 14 | console.log(`copyFileSyncWithFormatter from ${sourceFile} to ${targetFile}`); 15 | 16 | if (existsSync(targetFile)) { 17 | throw new Error(`Target file already exists: ${targetFile}`); 18 | } 19 | 20 | const ext: string = path.extname(sourceFile); 21 | if (typeof formatter === "function" && TEXT_FORMAT_EXTENSIONS.has(ext.toLowerCase())) { 22 | console.log(`modifying ${sourceFile}`); 23 | const data = readFileSync(sourceFile, { encoding: "utf8" }); 24 | writeFileSync(targetFile, formatter(data), { encoding: "utf8" }); 25 | } 26 | else { 27 | const data = readFileSync(sourceFile); 28 | writeFileSync(targetFile, data); 29 | } 30 | } 31 | 32 | export const copyFolderRecursiveSync = (sourceFolder, targetFolder, formatter?: Formatter) => { 33 | console.log(`copyFolderRecursiveSync from ${sourceFolder} to ${targetFolder}`); 34 | 35 | if (!existsSync(targetFolder)) { 36 | console.log(`mkdir ${targetFolder}`); 37 | mkdirSync(targetFolder); 38 | } 39 | else if (!lstatSync(targetFolder).isDirectory()) { 40 | throw new Error("Target exists and is not a directory."); 41 | } 42 | 43 | // Copy 44 | if (lstatSync(sourceFolder).isDirectory()) { 45 | readdirSync(sourceFolder).forEach((child: string) => { 46 | const curSource: string = path.join(sourceFolder, child); 47 | const curTarget: string = path.join(targetFolder, child); 48 | if (lstatSync(curSource).isDirectory()) { 49 | copyFolderRecursiveSync(curSource, curTarget, formatter); 50 | } 51 | else { 52 | copyFileSyncWithFormatter(curSource, curTarget, formatter); 53 | } 54 | }); 55 | } 56 | }; 57 | -------------------------------------------------------------------------------- /src/files/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./fileManager"; 2 | export * from "./fileUtils"; 3 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { main } from "./main"; 2 | 3 | // Run main 4 | main().catch(console.error); 5 | -------------------------------------------------------------------------------- /src/interactive.ts: -------------------------------------------------------------------------------- 1 | import inquirer from "inquirer"; 2 | import type { ProgramConfig } from "./cliParser"; 3 | import { validateProjectNameInput } from "./validation"; 4 | 5 | export const interactiveMode = (): Promise => { 6 | return inquirer.prompt([ 7 | { 8 | type: "input", 9 | message: "Project Name:", 10 | name: "projectName", 11 | validate: validateProjectNameInput 12 | }, 13 | { 14 | type: "input", 15 | message: "Icon:", 16 | name: "icon" 17 | }, 18 | { 19 | type: "confirm", 20 | message: "TypeScript:", 21 | name: "typescript", 22 | default: true 23 | }, 24 | { 25 | type: "confirm", 26 | message: "Install Husky:", 27 | name: "husky", 28 | default: true 29 | }, 30 | { 31 | type: "confirm", 32 | message: "Verbose:", 33 | name: "verbose", 34 | default: false 35 | } 36 | ]); 37 | }; -------------------------------------------------------------------------------- /src/launcherCompiler.ts: -------------------------------------------------------------------------------- 1 | import { pathExists } from "fs-extra"; 2 | import * as path from "path"; 3 | import spawn from "cross-spawn"; 4 | import type { SpawnSyncReturns } from "child_process"; 5 | 6 | const COMPILER: string = "msbuild.exe"; 7 | 8 | export const checkMsbuildInPath = async (exit?: boolean): Promise => { 9 | // Check for compiler in %PATH% 10 | const promises = process.env.path.split(";").map((p) => pathExists(path.resolve(p, COMPILER))); 11 | const results: boolean[] = await Promise.all(promises); 12 | const compilerFound: boolean = results.find((result) => !!result); 13 | 14 | if (exit && !compilerFound) { 15 | console.error(`You need "${COMPILER}" in your %PATH% in order to compile the launcher executable.`); 16 | process.exit(1); 17 | } 18 | else { 19 | return compilerFound; 20 | } 21 | }; 22 | 23 | export const compileLauncher = async (): Promise => { 24 | const args: string[] = ["./launcher/launcher.csproj"]; 25 | 26 | const spawnResult: SpawnSyncReturns = spawn.sync(COMPILER, args, { stdio: "inherit" }); 27 | if (spawnResult.status !== 0) { 28 | return Promise.reject({ command: `${COMPILER} ${args.join(" ")}` }); 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { checkNodeRuntimeVersion } from "./checkNodeVersion"; 2 | import { checkMsbuildInPath } from "./launcherCompiler"; 3 | import { createWindowlessApp } from "./createWindowlessApp"; 4 | 5 | export const main = async () => { 6 | // check minimum node version 7 | checkNodeRuntimeVersion(); 8 | 9 | // Check for msbuild.exe in %PATH% 10 | await checkMsbuildInPath(true); 11 | 12 | await createWindowlessApp(process.argv); 13 | }; 14 | -------------------------------------------------------------------------------- /src/nodeUtils.ts: -------------------------------------------------------------------------------- 1 | import chalk from "chalk"; 2 | import spawn from "cross-spawn"; 3 | import type { SpawnSyncReturns } from "child_process"; 4 | 5 | export const checkThatNpmCanReadCwd = (): boolean => { 6 | const cwd = process.cwd(); 7 | let childOutput: string = null; 8 | try { 9 | const spawnResult: SpawnSyncReturns = spawn.sync("npm", ["config", "list"]); 10 | if (spawnResult.status !== 0) { 11 | return false; 12 | } 13 | childOutput = spawnResult.output.toString(); 14 | } 15 | catch (err) { 16 | return false; 17 | } 18 | 19 | const lines: string[] = childOutput.split("\n"); 20 | // `npm config list` output includes the following line: 21 | // "; cwd = C:\path\to\current\dir" (unquoted) 22 | // I couldn't find an easier way to get it. 23 | const prefix = "; cwd = "; 24 | const line = lines.find((line) => line.indexOf(prefix) === 0); 25 | if (typeof line !== "string") { 26 | // Fail gracefully. They could remove it. 27 | return true; 28 | } 29 | const npmCWD = line.substring(prefix.length); 30 | if (npmCWD === cwd) { 31 | return true; 32 | } 33 | console.error( 34 | chalk.red( 35 | "Could not start an npm process in the right directory.\n\n" + 36 | `The current directory is: ${chalk.bold(cwd)}\n` + 37 | `However, a newly started npm process runs in: ${chalk.bold(npmCWD)}\n\n` + 38 | "This is probably caused by a misconfigured system terminal shell." 39 | ) 40 | ); 41 | if (process.platform === "win32") { 42 | console.error( 43 | chalk.red("On Windows, this can usually be fixed by running:\n\n") + 44 | ` ${chalk.cyan("reg")} delete "HKCU\\Software\\Microsoft\\Command Processor" /v AutoRun /f\n` + 45 | ` ${chalk.cyan("reg")} delete "HKLM\\Software\\Microsoft\\Command Processor" /v AutoRun /f\n\n` + 46 | chalk.red("Try to run the above two lines in the terminal.\n") + 47 | chalk.red("To learn more about this problem, read: https://blogs.msdn.microsoft.com/oldnewthing/20071121-00/?p=24433/") 48 | ); 49 | } 50 | return false; 51 | }; 52 | -------------------------------------------------------------------------------- /src/packageJson/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./packageJsonBuilder"; 2 | export * from "./packageJsonConsts"; -------------------------------------------------------------------------------- /src/packageJson/packageJsonBuilder.ts: -------------------------------------------------------------------------------- 1 | import { getHuskyScripts, getJsScripts, getPackageJsonBase, getTsScripts } from "./packageJsonConsts"; 2 | 3 | export class PackageJsonBuilder { 4 | #appName: string; 5 | #typescript: boolean = true; 6 | #husky: boolean = false; 7 | 8 | constructor(appName: string) { 9 | this.#appName = appName; 10 | } 11 | 12 | withJavaScript(): this { 13 | this.#typescript = false; 14 | return this; 15 | } 16 | 17 | withHusky(): this { 18 | this.#husky = true; 19 | return this; 20 | } 21 | 22 | build(): object { 23 | let packageJson = getPackageJsonBase(this.#appName); 24 | if (this.#typescript) { 25 | packageJson = { ...packageJson, scripts: { ...packageJson.scripts, ...getTsScripts(this.#appName) } }; 26 | } 27 | else { 28 | packageJson = { ...packageJson, scripts: { ...packageJson.scripts, ...getJsScripts(this.#appName) } }; 29 | } 30 | if (this.#husky) { 31 | packageJson = { ...packageJson, scripts: { ...packageJson.scripts, ...getHuskyScripts(this.#appName) } }; 32 | } 33 | return packageJson; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/packageJson/packageJsonConsts.ts: -------------------------------------------------------------------------------- 1 | export type Scripts = Record 2 | 3 | export const getPackageJsonBase = (appName) => ({ 4 | name: appName, 5 | version: "0.1.0", 6 | private: true, 7 | main: "_build/index.js", 8 | scripts: {} 9 | }); 10 | 11 | const getSingleExecutableApplicationsScripts = (appName: string): Scripts => ({ 12 | "prenode-sea:build-blob": "rimraf _blob && mkdir _blob", 13 | "node-sea:build-blob": "node --experimental-sea-config sea-config.json", 14 | "node-sea:copy-node": `node -e "require('fs').copyFileSync(process.execPath, 'dist/${appName}.exe')"`, 15 | "node-sea:unsign": `signtool remove /s dist/${appName}.exe || echo Warning: signtool not found in "Path"`, 16 | "node-sea:inject-blob": `postject dist/${appName}.exe NODE_SEA_BLOB _blob\\sea-prep.blob --sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2`, 17 | "node-sea:sign": `signtool sign /fd SHA256 dist/${appName}.exe`, 18 | "node-sea": "npm run node-sea:build-blob && npm run node-sea:copy-node && npm run node-sea:unsign && npm run node-sea:inject-blob" 19 | }); 20 | 21 | export const getTsScripts = (appName: string): Scripts => ({ 22 | "start": "ts-node src/index.ts", 23 | "type-check": "tsc --build tsconfig.json", 24 | "prewebpack": "rimraf build && rimraf dist", 25 | "webpack": "webpack", 26 | "prebuild": "npm run check-node-version", 27 | "build": "npm run type-check && npm run webpack && npm run node-sea", 28 | "check-node-version": "ts-node -e \"require(\"\"./utils/checkNodeVersion\"\").checkNodeRuntimeVersion()\"", 29 | "check-msbuild": "ts-node -e \"require(\"\"./launcher/launcherCompiler\"\").checkMsbuildInPath(true)\"", 30 | "rebuild-launcher": "msbuild launcher/launcher.csproj", 31 | ...getSingleExecutableApplicationsScripts(appName) 32 | }); 33 | 34 | export const getJsScripts = (appName: string): Scripts => ({ 35 | "start": "node src/index.js", 36 | "prewebpack": "rimraf build && rimraf dist", 37 | "webpack": "webpack", 38 | "prebuild": "npm run check-node-version", 39 | "build": "npm run webpack && npm run node-sea", 40 | "check-node-version": "node -e \"require(\"\"./utils/checkNodeVersion\"\").checkNodeRuntimeVersion()\"", 41 | "check-msbuild": "node -e \"require(\"\"./launcher/launcherCompiler\"\").checkMsbuildInPath(true)\"", 42 | "rebuild-launcher": "msbuild launcher/launcher.csproj", 43 | ...getSingleExecutableApplicationsScripts(appName) 44 | }); 45 | 46 | export const getHuskyScripts = (appName: string): Scripts => ({ 47 | "prepare":"git config --get core.hookspath || husky", 48 | "pre-commit": `git diff HEAD --exit-code --stat launcher/* || npm run check-msbuild && npm run rebuild-launcher && git add resources/bin/${appName}-launcher.exe` 49 | }); 50 | -------------------------------------------------------------------------------- /src/validation.ts: -------------------------------------------------------------------------------- 1 | import type { InvalidNames, LegacyNames, ValidNames } from "validate-npm-package-name"; 2 | import validateProjectName from "validate-npm-package-name"; 3 | 4 | export const validateProjectNameInput = (value: string): string | boolean => { 5 | const result: ValidNames | InvalidNames | LegacyNames = validateProjectName(value); 6 | return result.validForNewPackages || ((result as InvalidNames)?.errors?.[0]) || ((result as LegacyNames)?.warnings?.[0]) || "Invalid project name"; 7 | }; 8 | -------------------------------------------------------------------------------- /templates/.husky/pre-commit: -------------------------------------------------------------------------------- 1 | npm run pre-commit 2 | -------------------------------------------------------------------------------- /templates/common/sea-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": "_build/index.js", 3 | "output": "_blob/sea-prep.blob", 4 | "disableExperimentalSEAWarning": true 5 | } 6 | -------------------------------------------------------------------------------- /templates/javascript/launcher/launcherCompiler.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs-extra"); 2 | const path = require("path"); 3 | const spawn = require("cross-spawn"); 4 | 5 | const COMPILER = "msbuild.exe"; 6 | 7 | const checkCscInPath = async (exit) => { 8 | // Check for compiler in %PATH% 9 | const promises = process.env.path.split(";").map((p) => fs.pathExists(path.resolve(p, COMPILER))); 10 | const results = await Promise.all(promises); 11 | const compilerFound = await results.find((result) => !!result); 12 | 13 | if (exit && !compilerFound) { 14 | console.error(`You need "${COMPILER}" in your %PATH% in order to compile the launcher executable.`); 15 | process.exit(1); 16 | } 17 | else { 18 | return compilerFound; 19 | } 20 | }; 21 | 22 | const compileLauncher = async () => { 23 | const args = ["./launcher/launcher.csproj"]; 24 | 25 | const spawnResult = spawn.sync(COMPILER, args, { stdio: "inherit" }); 26 | if (spawnResult.status !== 0) { 27 | return Promise.reject({ command: `${COMPILER} ${args.join(" ")}` }); 28 | } 29 | }; 30 | 31 | module.exports = { checkCscInPath, compileLauncher }; 32 | -------------------------------------------------------------------------------- /templates/javascript/src/index.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const winston = require("winston"); 3 | const { WindowsToaster } = require("node-notifier"); 4 | const { execFile } = require("child_process"); 5 | 6 | // App Name 7 | const AppName = "##APPNAME##"; 8 | 9 | const executable = process.argv[0]; 10 | 11 | // Args (ignore exe + js) 12 | const argv = process.argv.slice(2); 13 | 14 | // Logger init 15 | const { combine, timestamp, printf, label } = winston.format; 16 | const filename = `${AppName}.log`; 17 | const transports = { 18 | file: new winston.transports.File({ filename }) 19 | }; 20 | transports.file.level = "debug"; 21 | const logger = winston.createLogger({ 22 | level: "debug", 23 | format: combine( 24 | label({ label: "[my-label]" }), 25 | timestamp({ format: "YYYY-MM-DD HH:mm:ss.SSS" }), 26 | printf((info) => `${info.timestamp} [${info.level.toUpperCase()}] ${info.message}`) 27 | ), 28 | transports: [transports.file] 29 | }); 30 | 31 | // Notifier init 32 | const snoreToastPath = executable.endsWith(".exe") ? path.resolve(executable, "../", "snoretoast-x64.exe") : null; 33 | let notifierOptions = { withFallback: false, customPath: snoreToastPath }; 34 | const notifier = new WindowsToaster(notifierOptions); 35 | 36 | // Log message 37 | logger.log("info", `"${AppName}" started with ${argv ? argv.join("; ") : "no args"}`); 38 | logger.log("info", `Notifier started with options ${JSON.stringify(notifierOptions)}`); 39 | 40 | // Notify 41 | const notification = { title: `${AppName}`, message: "Hello World", actions: ["Log", "Close"] }; 42 | notifier.notify(notification); 43 | notifier.on("log", () => { 44 | const file = path.join(__dirname, "..", filename); 45 | execFile(file, { shell: "powershell" }); 46 | }); 47 | -------------------------------------------------------------------------------- /templates/javascript/utils/checkNodeVersion.js: -------------------------------------------------------------------------------- 1 | const MIN_MAJOR_VERSION = 20; 2 | const MIN_MINOR_VERSION = 0; 3 | 4 | export const checkNodeRuntimeVersion = () => { 5 | const currentNodeVersion = process.versions.node; 6 | const semver = currentNodeVersion.split("."); 7 | const major = Number(semver[0]); 8 | const minor = Number(semver[1]); 9 | 10 | if (Number.isNaN(major) || Number.isNaN(minor) || major < MIN_MAJOR_VERSION || (major === MIN_MAJOR_VERSION && minor < MIN_MINOR_VERSION)) { 11 | console.error(`You are running Node.js ${currentNodeVersion}.\nYou need at ${MIN_MAJOR_VERSION}.${MIN_MINOR_VERSION} or higher.\nPlease update your version of Node.`); 12 | process.exit(1); 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /templates/javascript/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const CopyWebpackPlugin = require("copy-webpack-plugin"); 3 | 4 | module.exports = { 5 | mode: "production", 6 | entry: "./src/index.js", 7 | target: "node", 8 | output: { 9 | path: path.join(__dirname, "_build"), 10 | filename: "index.js" 11 | }, 12 | plugins: [ 13 | new CopyWebpackPlugin({ 14 | patterns: [ 15 | { 16 | from: "node_modules/node-notifier/vendor/snoreToast/snoretoast-x64.exe", 17 | to: "../dist/snoretoast-x64.exe", 18 | toType: "file" 19 | }, 20 | { 21 | from: "resources/bin/##APPNAME##-launcher.exe", 22 | to: "../dist/##APPNAME##-launcher.exe", 23 | toType: "file" 24 | } 25 | ] 26 | }) 27 | ], 28 | devtool: "source-map" 29 | }; 30 | -------------------------------------------------------------------------------- /templates/launcher/launcher.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Diagnostics; 4 | using System.ComponentModel; 5 | 6 | namespace MyProcessSample { 7 | class MyProcess { 8 | public static void Main(string[] args) { 9 | // App Name 10 | const string AppName = "##APPNAME##"; 11 | 12 | try { 13 | using (Process myProcess = new Process()) { 14 | myProcess.StartInfo.FileName = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, AppName + ".exe"); 15 | 16 | for (int i=0; i < args.Length; i++) { 17 | args[i] = "\"" + args[i] + "\""; 18 | } 19 | myProcess.StartInfo.Arguments = String.Join(" ", args); 20 | 21 | // WorkingDirectory same as BaseDirectory 22 | myProcess.StartInfo.WorkingDirectory = AppDomain.CurrentDomain.BaseDirectory; 23 | 24 | // Stop the process from opening a new window 25 | myProcess.StartInfo.WindowStyle = ProcessWindowStyle.Hidden; 26 | 27 | myProcess.Start(); 28 | } 29 | } 30 | catch (Exception e) { 31 | Console.WriteLine(e.Message); 32 | } 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /templates/launcher/launcher.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | ##APPNAME## 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /templates/launcher/launcher.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yoavain/create-windowless-app/7725de9044b570e8f5822cdbd781379fa05a5bc0/templates/launcher/launcher.ico -------------------------------------------------------------------------------- /templates/typescript/launcher/launcherCompiler.ts: -------------------------------------------------------------------------------- 1 | import { pathExists } from "fs-extra"; 2 | import * as path from "path"; 3 | import spawn from "cross-spawn"; 4 | import type { SpawnSyncReturns } from "child_process"; 5 | 6 | const COMPILER: string = "msbuild.exe"; 7 | 8 | export const checkMsbuildInPath = async (exit?: boolean): Promise => { 9 | // Check for compiler in %PATH% 10 | const promises = process.env.path.split(";").map((p) => pathExists(path.resolve(p, COMPILER))); 11 | const results: boolean[] = await Promise.all(promises); 12 | const compilerFound: boolean = results.find((result) => !!result); 13 | 14 | if (exit && !compilerFound) { 15 | console.error(`You need "${COMPILER}" in your %PATH% in order to compile the launcher executable.`); 16 | process.exit(1); 17 | } 18 | else { 19 | return compilerFound; 20 | } 21 | }; 22 | 23 | export const compileLauncher = async (): Promise => { 24 | const args: string[] = ["./launcher/launcher.csproj"]; 25 | 26 | const spawnResult: SpawnSyncReturns = spawn.sync(COMPILER, args, { stdio: "inherit" }); 27 | if (spawnResult.status !== 0) { 28 | return Promise.reject({ command: `${COMPILER} ${args.join(" ")}` }); 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /templates/typescript/src/index.ts: -------------------------------------------------------------------------------- 1 | import * as path from "path"; 2 | import * as winston from "winston"; 3 | import { WindowsToaster } from "node-notifier"; 4 | import { execFile } from "child_process"; 5 | import type { Option } from "node-notifier"; 6 | 7 | // App Name 8 | const AppName: string = "##APPNAME##"; 9 | 10 | const executable: string = process.argv[0]; 11 | 12 | // Args (ignore exe + js) 13 | const argv: string[] = process.argv.slice(2); 14 | 15 | // Logger init 16 | const { combine, timestamp, printf, label } = winston.format; 17 | const filename: string = `${AppName}.log`; 18 | const transports = { 19 | file: new winston.transports.File({ filename: filename }) 20 | }; 21 | transports.file.level = "debug"; 22 | const logger: winston.Logger = winston.createLogger({ 23 | level: "debug", 24 | format: combine( 25 | label({ label: "[my-label]" }), 26 | timestamp({ format: "YYYY-MM-DD HH:mm:ss.SSS" }), 27 | printf((info) => `${info.timestamp} [${info.level.toUpperCase()}] ${info.message}`) 28 | ), 29 | transports: [transports.file] 30 | }); 31 | 32 | // Notifier init 33 | const snoreToastPath: string = executable.endsWith(".exe") ? path.resolve(executable, "../", "snoretoast-x64.exe") : null; 34 | let notifierOptions: Option = { withFallback: false, customPath: snoreToastPath }; 35 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 36 | const notifier: any = new WindowsToaster(notifierOptions); 37 | 38 | // Log message 39 | logger.log("info", `"${AppName}" started with ${argv ? argv.join("; ") : "no args"}`); 40 | logger.log("info", `Notifier started with options ${JSON.stringify(notifierOptions)}`); 41 | 42 | // Notify 43 | const notification = { title: `${AppName}`, message: "Hello World", actions: ["Log", "Close"] }; 44 | notifier.notify(notification); 45 | notifier.on("log", () => { 46 | const file: string = path.join(__dirname, "..", filename); 47 | execFile(file, { shell: "powershell" }); 48 | }); 49 | -------------------------------------------------------------------------------- /templates/typescript/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "noEmit": false, 5 | "outDir": "./_compile" 6 | }, 7 | "include": ["./src/**/*"] 8 | } 9 | -------------------------------------------------------------------------------- /templates/typescript/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/node20", 3 | "compilerOptions": { 4 | "strict": false, 5 | "esModuleInterop": true, 6 | "allowJs": true, 7 | "target": "es6", 8 | "noEmit": true, 9 | "verbatimModuleSyntax": false 10 | }, 11 | "include": ["./src/**/*.ts", "./launcher/**/*.ts", "./utils/**/*.ts", "./webpack.config.ts"] 12 | } 13 | -------------------------------------------------------------------------------- /templates/typescript/utils/checkNodeVersion.ts: -------------------------------------------------------------------------------- 1 | const MIN_MAJOR_VERSION = 20; 2 | const MIN_MINOR_VERSION = 0; 3 | 4 | export const checkNodeRuntimeVersion = () => { 5 | const currentNodeVersion: string = process.versions.node; 6 | const semver: string[] = currentNodeVersion.split("."); 7 | const major: number = Number(semver[0]); 8 | const minor: number = Number(semver[1]); 9 | 10 | if (Number.isNaN(major) || Number.isNaN(minor) || major < MIN_MAJOR_VERSION || (major === MIN_MAJOR_VERSION && minor < MIN_MINOR_VERSION)) { 11 | console.error(`You are running Node.js ${currentNodeVersion}.\nYou need at ${MIN_MAJOR_VERSION}.${MIN_MINOR_VERSION} or higher.\nPlease update your version of Node.`); 12 | process.exit(1); 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /templates/typescript/webpack.config.ts: -------------------------------------------------------------------------------- 1 | import type webpack from "webpack"; 2 | import CopyWebpackPlugin from "copy-webpack-plugin"; 3 | import path from "path"; 4 | 5 | export const webpackConfig: webpack.Configuration = { 6 | mode: "production", 7 | entry: "./src/index.ts", 8 | target: "node", 9 | output: { 10 | path: path.join(__dirname, "_build"), 11 | filename: "index.js" 12 | }, 13 | module: { 14 | rules: [ 15 | { 16 | test: /\.ts$/, 17 | use: [{ 18 | loader: "ts-loader", 19 | options: { 20 | configFile: "tsconfig.build.json", 21 | transpileOnly: true 22 | } 23 | }], 24 | exclude: /node_modules/ 25 | } 26 | ] 27 | }, 28 | plugins: [ 29 | new CopyWebpackPlugin({ 30 | patterns: [ 31 | { 32 | from: "node_modules/node-notifier/vendor/snoreToast/snoretoast-x64.exe", 33 | to: "../dist/snoretoast-x64.exe", 34 | toType: "file" 35 | }, 36 | { 37 | from: "resources/bin/##APPNAME##-launcher.exe", 38 | to: "../dist/##APPNAME##-launcher.exe", 39 | toType: "file" 40 | } 41 | ] 42 | }) 43 | ], 44 | devtool: "source-map" 45 | }; 46 | 47 | export default webpackConfig; 48 | -------------------------------------------------------------------------------- /test/checkNodeVersion.test.ts: -------------------------------------------------------------------------------- 1 | import { checkNodeRuntimeVersion } from "../src/checkNodeVersion"; 2 | 3 | describe("Test checkNodeRuntimeVersion", () => { 4 | let p; 5 | beforeEach(() => { 6 | p = process; 7 | }); 8 | afterEach(() => { 9 | jest.restoreAllMocks(); 10 | process = p; 11 | }); 12 | 13 | 14 | describe("Test checkNodeRuntimeVersion", () => { 15 | it("should succeed on allowed version", async () => { 16 | process = { ...process, versions: { ...process.versions, node :"20.0.0" } }; 17 | checkNodeRuntimeVersion(); 18 | }); 19 | 20 | it("should exit with code 1 when version does not meet requirements", async () => { 21 | // @ts-ignore 22 | jest.spyOn(process, "exit").mockImplementation(() => {}); 23 | 24 | process = { ...process, versions: { ...process.versions, node :"10.0.0" } }; 25 | checkNodeRuntimeVersion(); 26 | expect(process.exit).toHaveBeenCalledTimes(1); 27 | expect(process.exit).toHaveBeenCalledWith(1); 28 | }); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /test/cliParser.test.ts: -------------------------------------------------------------------------------- 1 | import type { ProgramConfig } from "../src/cliParser"; 2 | import { parseCommand } from "../src/cliParser"; 3 | import * as interactive from "../src/interactive"; 4 | import { randomUUID as uuid } from "crypto"; 5 | import inquirer from "inquirer"; 6 | import * as path from "path"; 7 | 8 | describe("Test cliParser", () => { 9 | afterEach(() => { 10 | jest.restoreAllMocks(); 11 | }); 12 | 13 | describe("Test parseCommand", () => { 14 | it("should parse default flags", async () => { 15 | const sandbox: string = uuid(); 16 | 17 | const { projectName, verbose, typescript, husky, icon } = await parseCommand(["node.exe", "dummy.ts", sandbox]); 18 | 19 | expect(projectName).toEqual(sandbox); 20 | expect(verbose).toBeUndefined(); 21 | expect(typescript).toEqual(true); 22 | expect(husky).toEqual(true); 23 | expect(icon).toBeUndefined(); 24 | }); 25 | 26 | it("should parse flags: --no-typescript", async () => { 27 | const sandbox: string = uuid(); 28 | 29 | const { projectName, verbose, typescript, husky, icon } = await parseCommand(["node.exe", "dummy.ts", sandbox, "--no-typescript"]); 30 | 31 | expect(projectName).toEqual(sandbox); 32 | expect(verbose).toBeUndefined(); 33 | expect(typescript).toEqual(false); 34 | expect(husky).toEqual(true); 35 | expect(icon).toBeUndefined(); 36 | }); 37 | 38 | it("should parse flags: --no-husky", async () => { 39 | const sandbox: string = uuid(); 40 | 41 | const { projectName, verbose, typescript, husky, icon } = await parseCommand(["node.exe", "dummy.ts", sandbox, "--no-husky"]); 42 | 43 | expect(projectName).toEqual(sandbox); 44 | expect(verbose).toBeUndefined(); 45 | expect(typescript).toEqual(true); 46 | expect(husky).toEqual(false); 47 | expect(icon).toBeUndefined(); 48 | }); 49 | 50 | it("should parse flags: --verbose", async () => { 51 | const sandbox: string = uuid(); 52 | 53 | const { projectName, verbose, typescript, husky, icon } = await parseCommand(["node.exe", "dummy.ts", sandbox, "--verbose"]); 54 | 55 | expect(projectName).toEqual(sandbox); 56 | expect(verbose).toEqual(true); 57 | expect(typescript).toEqual(true); 58 | expect(husky).toEqual(true); 59 | expect(icon).toBeUndefined(); 60 | }); 61 | 62 | it("should parse flags: --icon", async () => { 63 | const sandbox: string = uuid(); 64 | 65 | const iconLocation: string = path.join(__dirname, "..", "templates", "launcher", "launcher.ico"); 66 | const { projectName, verbose, typescript, husky, icon } = await parseCommand(["node.exe", "dummy.ts", sandbox, "--icon", iconLocation]); 67 | 68 | expect(projectName).toEqual(sandbox); 69 | expect(verbose).toBeUndefined(); 70 | expect(typescript).toEqual(true); 71 | expect(husky).toEqual(true); 72 | expect(icon).toEqual(iconLocation); 73 | }); 74 | 75 | it("should error on non existing icon", async () => { 76 | // @ts-ignore 77 | jest.spyOn(process, "exit").mockImplementation((code: number) => { 78 | expect(code).toEqual(1); 79 | }); 80 | // @ts-ignore 81 | const mockStdout = jest.spyOn(process.stdout, "write").mockImplementation(() => { /* do nothing */ }); 82 | 83 | const sandbox: string = uuid(); 84 | 85 | const iconLocation: string = path.join(__dirname, "..", "templates", "common", "resources", "not-exists.ico"); 86 | const { projectName, verbose, typescript, husky, icon } = await parseCommand(["node.exe", "dummy.ts", sandbox, "--icon", iconLocation]); 87 | 88 | expect(projectName).toEqual(sandbox); 89 | expect(verbose).toBeUndefined(); 90 | expect(typescript).toEqual(true); 91 | expect(husky).toEqual(true); 92 | expect(icon).toBeUndefined(); 93 | expect(mockStdout.mock.calls[0][0]).toContain("Cannot find icon in"); 94 | }); 95 | 96 | it("should print help with flags: --help", async () => { 97 | // @ts-ignore 98 | jest.spyOn(process, "exit").mockImplementation((code: number) => { 99 | expect(code).toEqual(0); 100 | }); 101 | // @ts-ignore 102 | const mockStdout = jest.spyOn(process.stdout, "write").mockImplementation(() => { /* do nothing */ }); 103 | const sandbox: string = uuid(); 104 | 105 | await parseCommand(["node.exe", "dummy.ts", sandbox, "--help"]); 106 | 107 | expect(mockStdout.mock.calls[0][0] as string).toContain("Show help"); 108 | }); 109 | 110 | it("should error on missing project name", async () => { 111 | // @ts-ignore 112 | jest.spyOn(process, "exit").mockImplementation((code: number) => { 113 | expect(code).toEqual(1); 114 | }); 115 | // @ts-ignore 116 | const mockStdout = jest.spyOn(process.stdout, "write").mockImplementation(() => { /* do nothing */ }); 117 | 118 | await parseCommand(["node.exe", "dummy.ts"]); 119 | expect(mockStdout.mock.calls[0][0] as string).toContain("Show help"); 120 | }); 121 | 122 | it("should not fail if no projectName and interactive", async () => { 123 | jest.spyOn(interactive, "interactiveMode").mockImplementation(async () => { 124 | return { 125 | projectName: "Test1234", 126 | typescript: true, 127 | husky: true 128 | } as ProgramConfig; 129 | }); 130 | 131 | const { projectName, verbose, typescript, husky, icon } = await parseCommand(["node.exe", "dummy.ts", "--interactive"]); 132 | 133 | expect(interactive.interactiveMode).toHaveBeenCalled(); 134 | expect(projectName).toEqual("Test1234"); 135 | expect(verbose).toBeUndefined(); 136 | expect(typescript).toEqual(true); 137 | expect(husky).toEqual(true); 138 | expect(icon).toBeUndefined(); 139 | }); 140 | 141 | it("should call inquirer in interactive mode", async () => { 142 | // @ts-ignore 143 | jest.spyOn(inquirer, "prompt").mockImplementation(async () => ({})); 144 | 145 | await parseCommand(["node.exe", "dummy.ts", "--interactive"]); 146 | 147 | expect(inquirer.prompt).toHaveBeenCalled(); 148 | }); 149 | }); 150 | }); 151 | -------------------------------------------------------------------------------- /test/createWindowlessApp.test.ts: -------------------------------------------------------------------------------- 1 | // Mocks Should be first 2 | jest.mock("cross-spawn", () => ({ 3 | sync: (command: string, args?: ReadonlyArray) => { 4 | if (command === "npm" && args.length === 2 && args[0] === "config" && args[1] === "list") { 5 | return { status: 0, output: [`; cwd = ${process.cwd()}`] }; 6 | } 7 | else { 8 | return { status: 0 }; 9 | } 10 | } 11 | })); 12 | jest.mock("fs-extra", () => { 13 | return { 14 | existsSync: jest.fn(() => true), 15 | lstatSync: jest.fn(() => ({ isDirectory: () => true })), 16 | mkdirSync: jest.fn(), 17 | ensureDirSync: jest.fn(), 18 | readdirSync: jest.fn(() => []), 19 | writeFileSync: jest.fn(), 20 | readFileSync: jest.fn(() => "{}"), 21 | copyFileSync: jest.fn(), 22 | pathExistsSync: jest.fn() 23 | }; 24 | }); 25 | 26 | // Imports should be after mocks 27 | import { createWindowlessApp } from "../src/createWindowlessApp"; 28 | import { randomUUID as uuid } from "crypto"; 29 | 30 | process.chdir = jest.fn(); 31 | 32 | jest.setTimeout(15000); 33 | 34 | describe("Test createWindowlessApp", () => { 35 | it("should create a prototype project with default flags", async () => { 36 | const sandbox: string = uuid(); 37 | await createWindowlessApp(["node.exe", "dummy.ts", sandbox]); 38 | }); 39 | 40 | it("should create a prototype project with flags: --no-typescript", async () => { 41 | const sandbox: string = uuid(); 42 | await createWindowlessApp(["node.exe", "dummy.ts", sandbox, "--no-typescript"]); 43 | }); 44 | 45 | it("should create a prototype project with flags: --no-husky", async () => { 46 | const sandbox: string = uuid(); 47 | await createWindowlessApp(["node.exe", "dummy.ts", sandbox, "--no-husky"]); 48 | }); 49 | 50 | it("should create a prototype project with flags: --verbose", async () => { 51 | const sandbox: string = uuid(); 52 | await createWindowlessApp(["node.exe", "dummy.ts", sandbox, "--verbose"]); 53 | }); 54 | 55 | it("should create a prototype project with flags: --icon", async () => { 56 | const sandbox: string = uuid(); 57 | await createWindowlessApp(["node.exe", "dummy.ts", sandbox, "--icon", "someIcon"]); 58 | }); 59 | 60 | it("should print help with flags: --help", async () => { 61 | // @ts-ignore 62 | jest.spyOn(process, "exit").mockImplementation((code: number) => { 63 | expect(code).toEqual(0); 64 | }); 65 | const sandbox: string = uuid(); 66 | await createWindowlessApp(["node.exe", "dummy.ts", sandbox, "--help"]); 67 | }); 68 | 69 | it("should error with missing project name", async () => { 70 | // @ts-ignore 71 | jest.spyOn(process, "exit").mockImplementation((code: number) => { 72 | expect(code).toEqual(1); 73 | }); 74 | await createWindowlessApp(["node.exe", "dummy.ts"]); 75 | }); 76 | }); 77 | -------------------------------------------------------------------------------- /test/files/fileUtils.test.ts: -------------------------------------------------------------------------------- 1 | import * as path from "path"; 2 | 3 | const mockExistsSync = jest.fn((p) => { 4 | if (p === "target" || p === `target${path.sep}inner1` || p === `target${path.sep}inner2`) { 5 | return false; 6 | } 7 | }); 8 | const mockLstatSync = jest.fn((p) => { 9 | const isDirectory: boolean = p === "source" || p === `source${path.sep}inner1` || p === `source${path.sep}inner2`; 10 | return { 11 | isDirectory: () => isDirectory 12 | } as unknown as StatsBase; 13 | }); 14 | const mockReaddirSync = jest.fn((p) => { 15 | if (p === "source") { 16 | return ["inner1", "inner2", "root.json"]; 17 | } 18 | if (p === `source${path.sep}inner1`) { 19 | return ["inner.png"]; 20 | } 21 | return []; 22 | }); 23 | const mockReadFileSync = jest.fn(() => "data"); 24 | const mockMkdirSync = jest.fn(() => ""); 25 | const mockWriteFileSync = jest.fn(() => ""); 26 | 27 | jest.mock("fs-extra", () => { 28 | 29 | return { 30 | existsSync: mockExistsSync, 31 | lstatSync: mockLstatSync, 32 | mkdirSync: mockMkdirSync, 33 | writeFileSync: mockWriteFileSync, 34 | readFileSync: mockReadFileSync, 35 | readdirSync: mockReaddirSync 36 | }; 37 | }); 38 | 39 | 40 | import { copyFolderRecursiveSync } from "../../src/files"; 41 | import type { StatsBase } from "fs"; 42 | 43 | 44 | describe("Test file utils", () => { 45 | it("Should copy folders recursively", () => { 46 | 47 | const formatter = jest.fn((data) => `###${data}###`); 48 | copyFolderRecursiveSync("source", "target", formatter); 49 | 50 | expect(mockExistsSync).toHaveBeenCalledTimes(5); 51 | expect(mockExistsSync.mock.calls[0]).toEqual(["target"]); 52 | expect(mockExistsSync.mock.calls[1]).toEqual([`target${path.sep}inner1`]); 53 | expect(mockExistsSync.mock.calls[2]).toEqual([`target${path.sep}inner1${path.sep}inner.png`]); 54 | expect(mockExistsSync.mock.calls[3]).toEqual([`target${path.sep}inner2`]); 55 | expect(mockExistsSync.mock.calls[4]).toEqual([`target${path.sep}root.json`]); 56 | 57 | expect(mockLstatSync).toHaveBeenCalledTimes(7); 58 | expect(mockLstatSync.mock.calls[0]).toEqual(["source"]); 59 | expect(mockLstatSync.mock.calls[1]).toEqual([`source${path.sep}inner1`]); 60 | expect(mockLstatSync.mock.calls[2]).toEqual([`source${path.sep}inner1`]); 61 | expect(mockLstatSync.mock.calls[3]).toEqual([`source${path.sep}inner1${path.sep}inner.png`]); 62 | expect(mockLstatSync.mock.calls[4]).toEqual([`source${path.sep}inner2`]); 63 | expect(mockLstatSync.mock.calls[5]).toEqual([`source${path.sep}inner2`]); 64 | expect(mockLstatSync.mock.calls[6]).toEqual([`source${path.sep}root.json`]); 65 | 66 | // 3 folders read 67 | expect(mockReaddirSync).toHaveBeenCalledTimes(3); 68 | expect(mockReaddirSync.mock.calls[0]).toEqual(["source"]); 69 | expect(mockReaddirSync.mock.calls[1]).toEqual([`source${path.sep}inner1`]); 70 | expect(mockReaddirSync.mock.calls[2]).toEqual([`source${path.sep}inner2`]); 71 | 72 | // 3 folders created 73 | expect(mockMkdirSync).toHaveBeenCalledTimes(3); 74 | expect(mockMkdirSync.mock.calls[0]).toEqual(["target"]); 75 | expect(mockMkdirSync.mock.calls[1]).toEqual([`target${path.sep}inner1`]); 76 | expect(mockMkdirSync.mock.calls[2]).toEqual([`target${path.sep}inner2`]); 77 | 78 | // 2 files read 79 | expect(mockReadFileSync).toHaveBeenCalledTimes(2); 80 | expect(mockReadFileSync.mock.calls[0]).toEqual([`source${path.sep}inner1${path.sep}inner.png`]); 81 | expect(mockReadFileSync.mock.calls[1]).toEqual([`source${path.sep}root.json`, { encoding: "utf8" }]); 82 | 83 | // 2 files copied 84 | expect(mockWriteFileSync).toHaveBeenCalledTimes(2); 85 | expect(mockWriteFileSync.mock.calls[0]).toEqual([`target${path.sep}inner1${path.sep}inner.png`, "data"]); 86 | expect(mockWriteFileSync.mock.calls[1]).toEqual([`target${path.sep}root.json`, "###data###", { encoding: "utf8" }]); 87 | 88 | expect(formatter).toHaveBeenCalledTimes(1); 89 | expect(formatter.mock.calls[0]).toEqual(["data"]); 90 | }); 91 | }); 92 | -------------------------------------------------------------------------------- /test/launcherCompiler.test.ts: -------------------------------------------------------------------------------- 1 | import { checkMsbuildInPath } from "../src/launcherCompiler"; 2 | import mockedEnv from "mocked-env"; 3 | import mockFs from "mock-fs"; 4 | 5 | describe("Test checkMsbuildInPath", () => { 6 | let p; 7 | beforeEach(() => { 8 | p = process; 9 | }); 10 | afterEach(() => { 11 | mockFs.restore(); 12 | jest.restoreAllMocks(); 13 | process = p; 14 | }); 15 | 16 | it("Test MSBuild found", async () => { 17 | const restoreEnv = mockedEnv({ 18 | PATH: "fakePath" 19 | }); 20 | mockFs({ 21 | "fakePath/msbuild.exe": "exist" 22 | }); 23 | 24 | const result: boolean = await checkMsbuildInPath(false); 25 | expect(result).toBeTruthy(); 26 | restoreEnv(); 27 | 28 | }); 29 | it("Test MSBuild not found", async () => { 30 | const restoreEnv = mockedEnv({ 31 | PATH: "fakePath" 32 | }); 33 | 34 | const result: boolean = await checkMsbuildInPath(false); 35 | expect(result).toBeFalsy(); 36 | restoreEnv(); 37 | }); 38 | it("Test MSBuild not found, with exit", async () => { 39 | const restoreEnv = mockedEnv({ 40 | PATH: "fakePath" 41 | }); 42 | // @ts-ignore 43 | jest.spyOn(process, "exit").mockImplementation(() => {}); 44 | 45 | const result: boolean = await checkMsbuildInPath(true); 46 | expect(result).toBeFalsy(); 47 | expect(process.exit).toHaveBeenCalledTimes(1); 48 | expect(process.exit).toHaveBeenCalledWith(1); 49 | restoreEnv(); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /test/packageJson/__snapshots__/packageJsonBuilder.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Test PackageJsonBuilder Test default package.json 1`] = ` 4 | { 5 | "main": "_build/index.js", 6 | "name": "test-app", 7 | "private": true, 8 | "scripts": { 9 | "build": "npm run type-check && npm run webpack && npm run node-sea", 10 | "check-msbuild": "ts-node -e "require(""./launcher/launcherCompiler"").checkMsbuildInPath(true)"", 11 | "check-node-version": "ts-node -e "require(""./utils/checkNodeVersion"").checkNodeRuntimeVersion()"", 12 | "node-sea": "npm run node-sea:build-blob && npm run node-sea:copy-node && npm run node-sea:unsign && npm run node-sea:inject-blob", 13 | "node-sea:build-blob": "node --experimental-sea-config sea-config.json", 14 | "node-sea:copy-node": "node -e "require('fs').copyFileSync(process.execPath, 'dist/test-app.exe')"", 15 | "node-sea:inject-blob": "postject dist/test-app.exe NODE_SEA_BLOB _blob\\sea-prep.blob --sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2", 16 | "node-sea:sign": "signtool sign /fd SHA256 dist/test-app.exe", 17 | "node-sea:unsign": "signtool remove /s dist/test-app.exe || echo Warning: signtool not found in "Path"", 18 | "prebuild": "npm run check-node-version", 19 | "prenode-sea:build-blob": "rimraf _blob && mkdir _blob", 20 | "prewebpack": "rimraf build && rimraf dist", 21 | "rebuild-launcher": "msbuild launcher/launcher.csproj", 22 | "start": "ts-node src/index.ts", 23 | "type-check": "tsc --build tsconfig.json", 24 | "webpack": "webpack", 25 | }, 26 | "version": "0.1.0", 27 | } 28 | `; 29 | 30 | exports[`Test PackageJsonBuilder Test package.json with husky 1`] = ` 31 | { 32 | "main": "_build/index.js", 33 | "name": "test-app", 34 | "private": true, 35 | "scripts": { 36 | "build": "npm run type-check && npm run webpack && npm run node-sea", 37 | "check-msbuild": "ts-node -e "require(""./launcher/launcherCompiler"").checkMsbuildInPath(true)"", 38 | "check-node-version": "ts-node -e "require(""./utils/checkNodeVersion"").checkNodeRuntimeVersion()"", 39 | "node-sea": "npm run node-sea:build-blob && npm run node-sea:copy-node && npm run node-sea:unsign && npm run node-sea:inject-blob", 40 | "node-sea:build-blob": "node --experimental-sea-config sea-config.json", 41 | "node-sea:copy-node": "node -e "require('fs').copyFileSync(process.execPath, 'dist/test-app.exe')"", 42 | "node-sea:inject-blob": "postject dist/test-app.exe NODE_SEA_BLOB _blob\\sea-prep.blob --sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2", 43 | "node-sea:sign": "signtool sign /fd SHA256 dist/test-app.exe", 44 | "node-sea:unsign": "signtool remove /s dist/test-app.exe || echo Warning: signtool not found in "Path"", 45 | "pre-commit": "git diff HEAD --exit-code --stat launcher/* || npm run check-msbuild && npm run rebuild-launcher && git add resources/bin/test-app-launcher.exe", 46 | "prebuild": "npm run check-node-version", 47 | "prenode-sea:build-blob": "rimraf _blob && mkdir _blob", 48 | "prepare": "git config --get core.hookspath || husky", 49 | "prewebpack": "rimraf build && rimraf dist", 50 | "rebuild-launcher": "msbuild launcher/launcher.csproj", 51 | "start": "ts-node src/index.ts", 52 | "type-check": "tsc --build tsconfig.json", 53 | "webpack": "webpack", 54 | }, 55 | "version": "0.1.0", 56 | } 57 | `; 58 | 59 | exports[`Test PackageJsonBuilder Test package.json with husky after javascript 1`] = ` 60 | { 61 | "main": "_build/index.js", 62 | "name": "test-app", 63 | "private": true, 64 | "scripts": { 65 | "build": "npm run webpack && npm run node-sea", 66 | "check-msbuild": "node -e "require(""./launcher/launcherCompiler"").checkMsbuildInPath(true)"", 67 | "check-node-version": "node -e "require(""./utils/checkNodeVersion"").checkNodeRuntimeVersion()"", 68 | "node-sea": "npm run node-sea:build-blob && npm run node-sea:copy-node && npm run node-sea:unsign && npm run node-sea:inject-blob", 69 | "node-sea:build-blob": "node --experimental-sea-config sea-config.json", 70 | "node-sea:copy-node": "node -e "require('fs').copyFileSync(process.execPath, 'dist/test-app.exe')"", 71 | "node-sea:inject-blob": "postject dist/test-app.exe NODE_SEA_BLOB _blob\\sea-prep.blob --sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2", 72 | "node-sea:sign": "signtool sign /fd SHA256 dist/test-app.exe", 73 | "node-sea:unsign": "signtool remove /s dist/test-app.exe || echo Warning: signtool not found in "Path"", 74 | "pre-commit": "git diff HEAD --exit-code --stat launcher/* || npm run check-msbuild && npm run rebuild-launcher && git add resources/bin/test-app-launcher.exe", 75 | "prebuild": "npm run check-node-version", 76 | "prenode-sea:build-blob": "rimraf _blob && mkdir _blob", 77 | "prepare": "git config --get core.hookspath || husky", 78 | "prewebpack": "rimraf build && rimraf dist", 79 | "rebuild-launcher": "msbuild launcher/launcher.csproj", 80 | "start": "node src/index.js", 81 | "webpack": "webpack", 82 | }, 83 | "version": "0.1.0", 84 | } 85 | `; 86 | 87 | exports[`Test PackageJsonBuilder Test package.json with javascript 1`] = ` 88 | { 89 | "main": "_build/index.js", 90 | "name": "test-app", 91 | "private": true, 92 | "scripts": { 93 | "build": "npm run webpack && npm run node-sea", 94 | "check-msbuild": "node -e "require(""./launcher/launcherCompiler"").checkMsbuildInPath(true)"", 95 | "check-node-version": "node -e "require(""./utils/checkNodeVersion"").checkNodeRuntimeVersion()"", 96 | "node-sea": "npm run node-sea:build-blob && npm run node-sea:copy-node && npm run node-sea:unsign && npm run node-sea:inject-blob", 97 | "node-sea:build-blob": "node --experimental-sea-config sea-config.json", 98 | "node-sea:copy-node": "node -e "require('fs').copyFileSync(process.execPath, 'dist/test-app.exe')"", 99 | "node-sea:inject-blob": "postject dist/test-app.exe NODE_SEA_BLOB _blob\\sea-prep.blob --sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2", 100 | "node-sea:sign": "signtool sign /fd SHA256 dist/test-app.exe", 101 | "node-sea:unsign": "signtool remove /s dist/test-app.exe || echo Warning: signtool not found in "Path"", 102 | "prebuild": "npm run check-node-version", 103 | "prenode-sea:build-blob": "rimraf _blob && mkdir _blob", 104 | "prewebpack": "rimraf build && rimraf dist", 105 | "rebuild-launcher": "msbuild launcher/launcher.csproj", 106 | "start": "node src/index.js", 107 | "webpack": "webpack", 108 | }, 109 | "version": "0.1.0", 110 | } 111 | `; 112 | 113 | exports[`Test PackageJsonBuilder Test package.json with javascript after husky 1`] = ` 114 | { 115 | "main": "_build/index.js", 116 | "name": "test-app", 117 | "private": true, 118 | "scripts": { 119 | "build": "npm run webpack && npm run node-sea", 120 | "check-msbuild": "node -e "require(""./launcher/launcherCompiler"").checkMsbuildInPath(true)"", 121 | "check-node-version": "node -e "require(""./utils/checkNodeVersion"").checkNodeRuntimeVersion()"", 122 | "node-sea": "npm run node-sea:build-blob && npm run node-sea:copy-node && npm run node-sea:unsign && npm run node-sea:inject-blob", 123 | "node-sea:build-blob": "node --experimental-sea-config sea-config.json", 124 | "node-sea:copy-node": "node -e "require('fs').copyFileSync(process.execPath, 'dist/test-app.exe')"", 125 | "node-sea:inject-blob": "postject dist/test-app.exe NODE_SEA_BLOB _blob\\sea-prep.blob --sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2", 126 | "node-sea:sign": "signtool sign /fd SHA256 dist/test-app.exe", 127 | "node-sea:unsign": "signtool remove /s dist/test-app.exe || echo Warning: signtool not found in "Path"", 128 | "pre-commit": "git diff HEAD --exit-code --stat launcher/* || npm run check-msbuild && npm run rebuild-launcher && git add resources/bin/test-app-launcher.exe", 129 | "prebuild": "npm run check-node-version", 130 | "prenode-sea:build-blob": "rimraf _blob && mkdir _blob", 131 | "prepare": "git config --get core.hookspath || husky", 132 | "prewebpack": "rimraf build && rimraf dist", 133 | "rebuild-launcher": "msbuild launcher/launcher.csproj", 134 | "start": "node src/index.js", 135 | "webpack": "webpack", 136 | }, 137 | "version": "0.1.0", 138 | } 139 | `; 140 | -------------------------------------------------------------------------------- /test/packageJson/packageJsonBuilder.test.ts: -------------------------------------------------------------------------------- 1 | import { PackageJsonBuilder } from "../../src/packageJson"; 2 | 3 | describe("Test PackageJsonBuilder", () => { 4 | it("Test default package.json", () => { 5 | const builder = new PackageJsonBuilder("test-app"); 6 | expect(builder.build()).toMatchSnapshot(); 7 | }); 8 | it("Test package.json with javascript", () => { 9 | const builder = new PackageJsonBuilder("test-app").withJavaScript(); 10 | expect(builder.build()).toMatchSnapshot(); 11 | }); 12 | it("Test package.json with husky", () => { 13 | const builder = new PackageJsonBuilder("test-app").withHusky(); 14 | expect(builder.build()).toMatchSnapshot(); 15 | }); 16 | it("Test package.json with javascript after husky", () => { 17 | const builder = new PackageJsonBuilder("test-app").withHusky().withJavaScript(); 18 | expect(builder.build()).toMatchSnapshot(); 19 | }); 20 | it("Test package.json with husky after javascript", () => { 21 | const builder = new PackageJsonBuilder("test-app").withJavaScript().withHusky(); 22 | expect(builder.build()).toMatchSnapshot(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /test/validation.test.ts: -------------------------------------------------------------------------------- 1 | import { validateProjectNameInput } from "../src/validation"; 2 | 3 | describe("Test validateProjectNameInput", () => { 4 | it("Should return true on valid name", () => { 5 | expect(validateProjectNameInput("test-1234")).toBe(true); 6 | }); 7 | it("Should return error message on invalid name", () => { 8 | expect(validateProjectNameInput("Test-1234")).toBe("name can no longer contain capital letters"); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "noEmit": false, 5 | "outDir": "./dist" 6 | }, 7 | "include": ["./src/**/*"] 8 | } 9 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/node20", 3 | "compilerOptions": { 4 | "strict": false, 5 | "esModuleInterop": true, 6 | "allowJs": true, 7 | "target": "es6", 8 | "noEmit": true, 9 | "verbatimModuleSyntax": false 10 | }, 11 | "include": ["./src/**/*", "./test/**/*", "./integration_test/**/*", "./templates/**/*", "jest.config.ts"] 12 | } 13 | --------------------------------------------------------------------------------