├── .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 | 
2 | # Create Windowless App
3 | [](https://github.com/yoavain/create-windowless-app/actions?query=workflow%3ACodeQL)
4 | [](https://github.com/yoavain/create-windowless-app/actions)
5 | 
6 | 
7 | 
8 | 
9 | [](https://snyk.io/advisor/npm-package/create-windowless-app)
10 | [](https://snyk.io//test/github/yoavain/create-windowless-app?targetFile=package.json)
11 | [](https://codecov.io/gh/yoavain/create-windowless-app)
12 | [](https://renovatebot.com)
13 | 
14 | 
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 |
54 |
55 |
56 | Or in interactive mode:
57 | ```sh
58 | npx create-windowless-app --interactive
59 | ```
60 |
61 |
62 |
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 |
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 |
--------------------------------------------------------------------------------