@yunarch/config-web 0.4.0 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +89 -71
- package/dist/biome.config.json +1 -0
- package/dist/cli/bun-run-all/{index.js → bun-run-all.cli.js} +5 -2
- package/dist/cli/chunk-3QWYGBKZ.js +1 -0
- package/dist/cli/openapi-sync/openapi-sync-lint-msw-handlers.cli.js +5 -0
- package/dist/cli/openapi-sync/openapi-sync.cli.js +133 -0
- package/dist/cli/turbo-select/{index.js → turbo-select.cli.js} +5 -2
- package/dist/{linters/eslint.config.d.ts → eslint.config.d.ts} +240 -187
- package/dist/eslint.config.js +1 -0
- package/dist/{linters/oxlint.config.json → oxlint.config.json} +1 -1
- package/package.json +58 -44
- package/dist/cli/chunk-PWSW557X.js +0 -1
- package/dist/cli/openapi-sync/index.js +0 -135
- package/dist/formatters/biome.config.json +0 -1
- package/dist/linters/eslint.config.js +0 -1
- /package/dist/{formatters/prettier.config.js → prettier.config.js} +0 -0
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/@yunarch/config-web)
|
|
4
4
|
|
|
5
|
-
> A curated set of
|
|
5
|
+
> A curated set of configurations and useful CLI tools for web projects.
|
|
6
6
|
|
|
7
7
|
> [!NOTE]
|
|
8
8
|
> This package is pure [ESM](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules). This means you need to ensure to use an **ESM-Compatible Environment** (Your runtime or bundler must support ESM) and enable **Package Type module** by adding the following to you `package.json`:
|
|
@@ -14,16 +14,18 @@
|
|
|
14
14
|
- [📖 Why use this?](#-why-use-this)
|
|
15
15
|
- [📦 What’s included?](#-whats-included)
|
|
16
16
|
- [⚙️ Installation](#️-installation)
|
|
17
|
-
- [
|
|
18
|
-
- [
|
|
19
|
-
- [
|
|
20
|
-
- [
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
- [Oxlint](#oxlint)
|
|
26
|
-
- [
|
|
17
|
+
- [⚠️ Caveats](#️-caveats)
|
|
18
|
+
- [Code Formatting](#code-formatting)
|
|
19
|
+
- [Linting](#linting)
|
|
20
|
+
- [Prettier](#prettier)
|
|
21
|
+
- [ESlint](#eslint)
|
|
22
|
+
- [Override configuration](#override-configuration)
|
|
23
|
+
- [Typescript Type aware rules](#typescript-type-aware-rules)
|
|
24
|
+
- [Oxlint](#oxlint)
|
|
25
|
+
- [Enabling ESLint and Oxlint Simultaneously](#enabling-eslint-and-oxlint-simultaneously)
|
|
26
|
+
- [Biome](#biome)
|
|
27
|
+
- [Enabling ESLint and Biome Simultaneously](#enabling-eslint-and-biome-simultaneously)
|
|
28
|
+
- [Typescript](#typescript)
|
|
27
29
|
- [ts-reset](#ts-reset)
|
|
28
30
|
- [Utilities](#utilities)
|
|
29
31
|
- [🔧 CLI Tools](#-cli-tools)
|
|
@@ -42,8 +44,7 @@ Even experienced developers can waste valuable time configuring tools from scrat
|
|
|
42
44
|
|
|
43
45
|
This package provides ready-to-use configurations for:
|
|
44
46
|
|
|
45
|
-
- **Code
|
|
46
|
-
- **Linting:** ESLint, Oxlint
|
|
47
|
+
- **Shared configs for Code Style & Linting:** Pre-configured yet extensible setups for Prettier, ESLint, Oxlint, and Biome.
|
|
47
48
|
- **TypeScript:** Best-practice default config and utilities.
|
|
48
49
|
- **CLI Tools:** Useful command-line tools for streamlining workflows
|
|
49
50
|
|
|
@@ -74,19 +75,27 @@ npm install --save-dev eslint
|
|
|
74
75
|
npm install --save-dev oxlint
|
|
75
76
|
```
|
|
76
77
|
|
|
77
|
-
##
|
|
78
|
+
## ⚠️ Caveats
|
|
78
79
|
|
|
79
|
-
|
|
80
|
+
### Code Formatting
|
|
80
81
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
> Language support for [Prettier](https://prettier.io/docs/) and [Biome](https://biomejs.dev/internals/language-support/).
|
|
82
|
+
While both `Prettier` and `Biome` are configured to format code in the same way, there are some [differences](https://biomejs.dev/formatter/differences-with-prettier/) between the two.
|
|
83
|
+
|
|
84
|
+
Language support for [Prettier](https://prettier.io/docs/) and [Biome](https://biomejs.dev/internals/language-support/).
|
|
85
85
|
|
|
86
86
|
> [!WARNING]
|
|
87
87
|
> While it's technically possible to use both tools in the same project, **each file should be formatted by only one formatter** to avoid conflicts. This repository uses this hybrid setup, but for simplicity and consistency, **we recommend choosing a single formatter** for your own project.
|
|
88
88
|
|
|
89
|
-
###
|
|
89
|
+
### Linting
|
|
90
|
+
|
|
91
|
+
We offer a strict yet configurable `ESLint` setup with autocomplete support. Additionally, since the `ESLint` ecosystem is extensive but can sometimes be slow, this configuration allows leveraging `Oxlint` or `Biome` for certain rules, boosting speed without compromising flexibility.
|
|
92
|
+
|
|
93
|
+
For small projects, `Oxlint` or `Biome` should be sufficient. However, for big projects or if you want to maintain consistent code style across multiple projects. I recommend `ESlint` and if need it a performance boost then combining `ESLint` with either `Oxlint` or `Biome`.
|
|
94
|
+
|
|
95
|
+
> [!NOTE]
|
|
96
|
+
> Avoid using all three tools (`ESLint`, `Oxlint`, and `Biome`) simultaneously, as this may lead to conflicts between `Oxlint` and `Biome` that you'll need to manually resolve.
|
|
97
|
+
|
|
98
|
+
## Prettier
|
|
90
99
|
|
|
91
100
|
The easiest way to use the prettier configuration as-is is to set it directly in your `package.json`:
|
|
92
101
|
|
|
@@ -106,32 +115,10 @@ export default {
|
|
|
106
115
|
};
|
|
107
116
|
```
|
|
108
117
|
|
|
118
|
+
> [!TIP]
|
|
109
119
|
> Add a `.prettierignore` file to ignore certain files and folder completly or use the CLI option [--ignore-path](https://prettier.io/docs/cli#--ignore-path) to indicate a path to a file containing patterns that describe files to ignore. By default, Prettier looks for `./.gitignore` and `./.prettierignore`.
|
|
110
120
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
To use the Biome formatter, create a `biome.json` [configuration file](https://biomejs.dev/reference/configuration/):
|
|
114
|
-
|
|
115
|
-
```jsonc
|
|
116
|
-
{
|
|
117
|
-
"$schema": "./node_modules/@biomejs/biome/configuration_schema.json",
|
|
118
|
-
"extends": ["@yunarch/config-web/biome-formatter"],
|
|
119
|
-
// Add your overrides here...
|
|
120
|
-
}
|
|
121
|
-
```
|
|
122
|
-
|
|
123
|
-
> [!IMPORTANT]
|
|
124
|
-
> We disable the `organizeImports` options as we want that to be manage by the linter configuration that we offer. feel free to enable it if you prefer to use Biome for these tasks. **Remember to disable it on ESlint configuration** if you use it.
|
|
125
|
-
|
|
126
|
-
> Enable [vcs.useIgnoreFile](https://biomejs.dev/guides/integrate-in-vcs/#use-the-ignore-file), to allow Biome to ignore all the files and directories listed in your VCS ignore file.
|
|
127
|
-
|
|
128
|
-
## 🧹 Linting
|
|
129
|
-
|
|
130
|
-
We offer a strict yet configurable ESLint setup with autocomplete support. Additionally, since the ESLint ecosystem is extensive but can sometimes be slow, this configuration allows leveraging Oxlint for certain rules, boosting speed without compromising flexibility.
|
|
131
|
-
|
|
132
|
-
For small projects, `Oxlint` or enabling the linter in `Biome` should be sufficient. However, for big projects or if you want to maintain consistent code style across multiple projects, we recommend ESlint and for performance boost ESlint + Oxlint.
|
|
133
|
-
|
|
134
|
-
### ESlint
|
|
121
|
+
## ESlint
|
|
135
122
|
|
|
136
123
|
To use the ESlint linter, create a [ESlint configuration file](https://eslint.org/docs/latest/use/configure/configuration-files):
|
|
137
124
|
|
|
@@ -180,7 +167,7 @@ export default config(
|
|
|
180
167
|
|
|
181
168
|
> Thanks to [antfu/eslint-config](https://github.com/antfu/eslint-config) for the inspiration, reference, and developed tools.
|
|
182
169
|
|
|
183
|
-
|
|
170
|
+
### Override configuration
|
|
184
171
|
|
|
185
172
|
Thanks to [eslint-flat-config-utils](https://github.com/antfu/eslint-flat-config-utils) we returns a flat config composer where you can chain methods and compose the configuration in different ways.
|
|
186
173
|
|
|
@@ -221,7 +208,7 @@ export default config()
|
|
|
221
208
|
> [!TIP]
|
|
222
209
|
> There are other methods such as `remove`, `removeRules`, `append`, `insertBefore`, etc. These methods help you configure the linter to suit your specific needs.
|
|
223
210
|
|
|
224
|
-
|
|
211
|
+
### Typescript Type aware rules
|
|
225
212
|
|
|
226
213
|
By providing the `tsconfigPath` in the `typescript` configuration it will automatically enable [type aware rules](https://typescript-eslint.io/getting-started/typed-linting/) which may/will impact the linter's performance.
|
|
227
214
|
|
|
@@ -238,24 +225,7 @@ export default config({
|
|
|
238
225
|
> [!NOTE]
|
|
239
226
|
> You can pass `disableTypeAware: true` to disable type-aware rules while keeping the TypeScript parser configuration which will allow you to manually enable the type-aware rules you want.
|
|
240
227
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
If you want to offload certain rules to Oxlint, which will reduce linting time, you can configure it as follows:
|
|
244
|
-
|
|
245
|
-
```js
|
|
246
|
-
import { config } from '@yunarch/config-web/eslint';
|
|
247
|
-
|
|
248
|
-
export default config({
|
|
249
|
-
oxlint: {
|
|
250
|
-
oxlintConfigPath: './.oxlintrc.json',
|
|
251
|
-
},
|
|
252
|
-
});
|
|
253
|
-
```
|
|
254
|
-
|
|
255
|
-
> [!NOTE]
|
|
256
|
-
> This setup automatically detects which rules are specified in the Oxlint configuration and disables them in ESLint accordingly.
|
|
257
|
-
|
|
258
|
-
### Oxlint
|
|
228
|
+
## Oxlint
|
|
259
229
|
|
|
260
230
|
To use the oxlint linter, create a `.oxlintrc.json` [configuration file](https://oxc.rs/docs/guide/usage/linter/config.html):
|
|
261
231
|
|
|
@@ -280,7 +250,57 @@ To use the oxlint linter, create a `.oxlintrc.json` [configuration file](https:/
|
|
|
280
250
|
> Currently, `Oxlint` does not resolve configuration file paths automatically. To extend a config, you must explicitly provide the full path, like so:
|
|
281
251
|
> `"extends": ["./node_modules/@yunarch/config-web/dist/linters/oxlint.config.json"]`
|
|
282
252
|
|
|
283
|
-
|
|
253
|
+
### Enabling ESLint and Oxlint Simultaneously
|
|
254
|
+
|
|
255
|
+
If you want to offload certain rules to Oxlint, which will reduce linting time, you can configure `ESlint` as follows:
|
|
256
|
+
|
|
257
|
+
```js
|
|
258
|
+
import { config } from '@yunarch/config-web/eslint';
|
|
259
|
+
|
|
260
|
+
export default config({
|
|
261
|
+
oxlint: {
|
|
262
|
+
oxlintConfigPath: './.oxlintrc.json',
|
|
263
|
+
},
|
|
264
|
+
});
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
## Biome
|
|
268
|
+
|
|
269
|
+
To use the Biome, create a `biome.json` [configuration file](https://biomejs.dev/reference/configuration/):
|
|
270
|
+
|
|
271
|
+
```jsonc
|
|
272
|
+
{
|
|
273
|
+
"$schema": "./node_modules/@biomejs/biome/configuration_schema.json",
|
|
274
|
+
"extends": ["@yunarch/config-web/biome"],
|
|
275
|
+
"linter": {
|
|
276
|
+
"enabled": true,
|
|
277
|
+
},
|
|
278
|
+
}
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
That’s it! Biome will now use the shared config to lint and format your code. However:
|
|
282
|
+
|
|
283
|
+
- If you prefer not to use Biome as a linter, simply remove the `"linter"` section. Linting is disabled by default unless explicitly enabled.
|
|
284
|
+
- If you prefer to use Biome only as a linter, disable the formatter by: `"formatter": { "enabled": false }`.
|
|
285
|
+
|
|
286
|
+
> [!TIP]
|
|
287
|
+
> Enable [vcs.useIgnoreFile](https://biomejs.dev/guides/integrate-in-vcs/#use-the-ignore-file), to allow Biome to ignore all the files and directories listed in your VCS ignore file.
|
|
288
|
+
|
|
289
|
+
### Enabling ESLint and Biome Simultaneously
|
|
290
|
+
|
|
291
|
+
If you want to offload certain rules to `Biome`, which will reduce linting time, you can configure `ESlint` as follows:
|
|
292
|
+
|
|
293
|
+
```js
|
|
294
|
+
import { config } from '@yunarch/config-web/eslint';
|
|
295
|
+
|
|
296
|
+
export default config({
|
|
297
|
+
biome: {
|
|
298
|
+
biomeConfigPath: './biome.json',
|
|
299
|
+
},
|
|
300
|
+
});
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
## Typescript
|
|
284
304
|
|
|
285
305
|
Create the `tsconfig.json` file with the following content:
|
|
286
306
|
|
|
@@ -346,12 +366,10 @@ const x2 = typedObjectFromEntries([['a', 1]] as const);
|
|
|
346
366
|
|
|
347
367
|
This package ships with useful command-line tools to streamline your workflow.
|
|
348
368
|
|
|
349
|
-
-
|
|
350
|
-
-
|
|
351
|
-
-
|
|
352
|
-
|
|
353
|
-
> [!NOTE]
|
|
354
|
-
> All the CLI tools include a `--help` flag, which provides detailed information on usage and available options.
|
|
369
|
+
- **[`bun-run-all`](./src/cli/__docs__/bun-run-all.md)**: CLI tool for running npm package scripts in parallel or sequential by using bun.
|
|
370
|
+
- **[`openapi-sync`](./src/cli/__docs__/openapi-sync.md)**: CLI tool designed to convert OpenAPI 3.0/3.1 schemas to TypeScript types and create type-safe fetching based on a openapi schema file and keep them in sync.
|
|
371
|
+
- **[`openapi-sync-lint-msw-handlers`](./src/cli/__docs__/openapi-sync-lint-msw-handlers.md)**: CLI tool for linting and identifying missing MSW (Mock Service Worker) handlers based on OpenAPI generated services. It analyzes your codebase to find where service methods are used and suggests appropriate handlers with detailed reporting.
|
|
372
|
+
- **[`turbo-select`](./src/cli/__docs__/turbo-select.md)**: CLI tool for filtering and selecting a single package from your Turborepo package list and executing a script command. Additionally, it can prompt you to select an environment mode (development, staging, production) — useful for adjusting settings based on the environment (e.g., when using Vite).
|
|
355
373
|
|
|
356
374
|
> [!IMPORTANT]
|
|
357
375
|
> These tools are **a personal configuration** with a lot of opinions. They might not work for everyone or every use case. Additionally, tools can be added or removed without being considered a breaking change.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"formatter":{"enabled":true,"includes":["**","!**/node_modules/","!**/dist/","!**/out/","!**/output","!**/.output","!**/build/","!**/*.min.*","!**/package-lock.json","!**/yarn.lock","!**/.yarn/","!**/.yarnrc.yml","!**/.pnp.*","!**/.pnp","!**/.pnp.js","!**/.pnp.cjs","!**/bun.lock","!**/bun.lockb","!**/pnpm-lock.yaml","!**/.vite-inspect","!**/.vitepress/cache","!**/vite.config.*.timestamp-*","!**/*.log","!**/npm-debug.log*","!**/yarn-debug.log*","!**/yarn-error.log*","!**/coverage/","!**/.nyc_output/","!**/__snapshots__","!**/.vscode/","!**/.idea/","!**/.cache","!**/.nuxt","!**/.next","!**/.svelte-kit","!**/.vercel","!**/.changeset","!**/.turbo/","!**/.DS_Store","!**/Thumbs.db","!**/temp","!**/.temp","!**/tmp","!**/.tmp","!**/.history","!**/mockServiceWorker.js","!**/CHANGELOG*","!**/LICENSE*"],"useEditorconfig":true,"formatWithErrors":false,"indentStyle":"space","indentWidth":2,"lineEnding":"lf","lineWidth":80,"attributePosition":"auto","bracketSpacing":true},"javascript":{"formatter":{"jsxQuoteStyle":"double","quoteProperties":"asNeeded","trailingCommas":"es5","semicolons":"always","arrowParentheses":"always","bracketSameLine":false,"quoteStyle":"single","attributePosition":"auto","bracketSpacing":true}},"linter":{"enabled":false,"includes":["!**/node_modules/","!**/dist/","!**/out/","!**/output","!**/.output","!**/build/","!**/*.min.*","!**/package-lock.json","!**/yarn.lock","!**/.yarn/","!**/.yarnrc.yml","!**/.pnp.*","!**/.pnp","!**/.pnp.js","!**/.pnp.cjs","!**/bun.lock","!**/bun.lockb","!**/pnpm-lock.yaml","!**/.vite-inspect","!**/.vitepress/cache","!**/vite.config.*.timestamp-*","!**/*.log","!**/npm-debug.log*","!**/yarn-debug.log*","!**/yarn-error.log*","!**/coverage/","!**/.nyc_output/","!**/__snapshots__","!**/.vscode/","!**/.idea/","!**/.cache","!**/.nuxt","!**/.next","!**/.svelte-kit","!**/.vercel","!**/.changeset","!**/.turbo/","!**/.DS_Store","!**/Thumbs.db","!**/temp","!**/.temp","!**/tmp","!**/.tmp","!**/.history","!**/mockServiceWorker.js","!**/CHANGELOG*","!**/LICENSE*"]},"overrides":[{"includes":["**/package.json"],"formatter":{"indentStyle":"space"}}],"assist":{"actions":{"source":{"organizeImports":"off"}}}}
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
-
import{c as p}from"../chunk-
|
|
2
|
+
import{c as p}from"../chunk-3QWYGBKZ.js";import{styleText as h}from"util";import{styleText as l}from"util";var d=["blue","green","yellow","grey","white","cyan"],m=o=>`${(Number(Bun.nanoseconds()-o)/1e6/1e3).toFixed(2)}s`;function b({start:o,tasks:e,failedTasks:n}){let c=e-n,a=m(o),t=n>0?l("red",`${n} failed`):"",i=c>0?l("green",`${c} successful`):"";console.log(""),console.log(l(["white","bold"],"Tasks: "),`${t}${t&&i?"|":""}${i}`,l("gray",`-- ${e} total`)),console.log(l(["white","bold"],"Time: "),l("gray",`${a}
|
|
3
3
|
`))}function g({index:o,script:e,continueOnError:n,reportTime:c}){let a=d[o%d.length],t=Bun.nanoseconds(),i=Bun.spawn(["bun","run",e],{stdout:"pipe",stderr:"pipe",env:{...Bun.env,FORCE_COLOR:"1"},onExit(r,s){s===1&&!n&&process.exit(1)}});return i.stdout.pipeTo(new WritableStream({write(r){let s=new TextDecoder().decode(r).split(`
|
|
4
4
|
`);for(let u of s)console.log(l([a,"bold"],`${e}:`),u)}})),i.stderr.pipeTo(new WritableStream({write(r){let s=new TextDecoder().decode(r).split(`
|
|
5
|
-
`);for(let u of s)console.log(l([a,"bold"],`${e}:`),u)}})),i.exited.then(r=>{r===0&&c&&console.log(l([a,"bold"],`${e}:`),l(["gray"],"Finished in"),l(["white","bold"],m(t)))}),i}async function f(o,e){let{continueOnError:n,reportTime:c}=e,a=Bun.nanoseconds(),t=o.map((s,u)=>g({index:u,script:s,continueOnError:n,reportTime:c})),r=(await Promise.allSettled(t.map(s=>s.exited))).filter(s=>s.status==="rejected"||s.value!==0).length;return b({start:a,tasks:o.length,failedTasks:r}),r>0?1:0}async function x(o,e){let{continueOnError:n,reportTime:c}=e,a=Bun.nanoseconds(),t=0;for(let[i,r]of o.entries())await g({index:i,script:r,continueOnError:n,reportTime:c}).exited!==0&&t++;return b({start:a,tasks:o.length,failedTasks:t}),t>0?1:0}p().name("bun-run-all").description("Run given package scripts in parallel or sequential by using bun.").argument("<scripts...>","A list of package scripts' names.").option("-c, --continue-on-error","Continue executing other/subsequent tasks even if a task threw an error").option("-p, --parallel","Run a group of tasks in parallel.").option("-s, --sequential","Run a group of tasks sequentially.").option("-t, --time","Report execution time for each task.").
|
|
5
|
+
`);for(let u of s)console.log(l([a,"bold"],`${e}:`),u)}})),i.exited.then(r=>{r===0&&c&&console.log(l([a,"bold"],`${e}:`),l(["gray"],"Finished in"),l(["white","bold"],m(t)))}),i}async function f(o,e){let{continueOnError:n,reportTime:c}=e,a=Bun.nanoseconds(),t=o.map((s,u)=>g({index:u,script:s,continueOnError:n,reportTime:c})),r=(await Promise.allSettled(t.map(s=>s.exited))).filter(s=>s.status==="rejected"||s.value!==0).length;return b({start:a,tasks:o.length,failedTasks:r}),r>0?1:0}async function x(o,e){let{continueOnError:n,reportTime:c}=e,a=Bun.nanoseconds(),t=0;for(let[i,r]of o.entries())await g({index:i,script:r,continueOnError:n,reportTime:c}).exited!==0&&t++;return b({start:a,tasks:o.length,failedTasks:t}),t>0?1:0}p().name("bun-run-all").description("Run given package scripts in parallel or sequential by using bun.").argument("<scripts...>","A list of package scripts' names.").option("-c, --continue-on-error","Continue executing other/subsequent tasks even if a task threw an error").option("-p, --parallel","Run a group of tasks in parallel.").option("-s, --sequential","Run a group of tasks sequentially.").option("-t, --time","Report execution time for each task.").addHelpText("after",`
|
|
6
|
+
Example usage:
|
|
7
|
+
|
|
8
|
+
$ bun-run-all script1 script2`).action(async(o,e)=>{try{console.log(h("magenta",`
|
|
6
9
|
\u{1F680} bun-run-all
|
|
7
10
|
`));let n=e.sequential??!1,c=e.parallel??!n,a=e.continueOnError??!1,t=e.time??!1;if(c===n&&(console.error("You cannot use both --parallel and --sequential options at the same time."),process.exit(1)),n){let r=await x(o,{continueOnError:a,reportTime:t});process.exit(r)}let i=await f(o,{continueOnError:a,reportTime:t});process.exit(i)}catch(n){console.error(n),process.exit(1)}}).parseAsync(process.argv);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{exec as c}from"child_process";import{promisify as p,styleText as e,types as u}from"util";import{Command as l}from"commander";import y from"ora";var d=p(c);async function h(n){let{command:t,name:r,options:a}=n,s=y(r);s.spinner=a?.spinner??"aesthetic";let m=Date.now();s.start();try{let o=typeof t=="string"?await d(t):u.isPromise(t)?await t:await t();return s.succeed(a?.showTime?`${e("dim",`${Date.now()-m}ms`)} ${r}`:void 0),await new Promise(i=>{setTimeout(i,0)}),typeof o=="object"&&o&&"stdout"in o?o.stdout:o}catch(o){let i=o;throw s.fail(e("red",i.stderr??i.message??"")),o}}function P(){let n=new l;return n.configureHelp({styleTitle:t=>e("bold",t),styleCommandText:t=>e("cyan",t),styleCommandDescription:t=>e("magenta",t),styleDescriptionText:t=>e("italic",t),styleOptionText:t=>e("green",t),styleArgumentText:t=>e("yellow",t),styleSubcommandText:t=>e("blue",t)}).configureOutput({outputError:(t,r)=>{r(e("red",t))}}),n}export{d as a,h as b,P as c};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import{c as M}from"../chunk-3QWYGBKZ.js";import{existsSync as _,statSync as b}from"fs";import y from"path";import{styleText as g}from"util";function $(d){let c=new Map;for(let i of d)c.has(i.service.name)||c.set(i.service.name,{service:i.service,handlers:[]}),c.get(i.service.name)?.handlers.push(i);if(c.size===0){console.log(g("green","\u2714 No missing handlers found"));return}for(let{service:i,handlers:n}of c.values()){console.log(`${g("underline",i.name)}${g("gray",` (${i.path})`)}`);for(let[o,e]of n.entries()){let s=o===n.length-1;console.log(` ${s?"\u2514\u2500":"\u251C\u2500"} ${g("cyan",e.service.toHandleHttpMethod)} ${g("yellow",e.service.toHandleUrl)}`),console.log(` ${s?" ":"\u2502"} \u251C\u2500 ${g("gray","Used in:")}`);for(let[l,a]of e.usedIn.entries()){let t=l===e.usedIn.length-1;console.log(` ${s?" ":"\u2502"} ${t?"\u2502 \u2514\u2500":"\u2502 \u251C\u2500"} ${a}`)}console.log(` ${s?" ":"\u2502"} \u2514\u2500 ${g("green","Suggested handler:")}`),console.log(` ${s?" ":"\u2502"} ${g("dim","\u2192")} ${e.suggestedPath}`),o<n.length-1&&console.log(` ${s?" ":"\u2502"}`)}console.log("")}console.log(g("red",`\u2718 ${d.length} missing ${d.length===1?"handler":"handlers"}`))}async function H({mswSetupFilePath:d,mswSetupConst:c}){let i=new Map,n=await import(d);if(!Object.hasOwn(n,c))throw new TypeError("MSW setup constant not found in the setup file");let o=n[c];if(!o||typeof o.listHandlers!="function")throw new TypeError("MSW setup constant does not have a listHandlers() method");let e=o.listHandlers();for(let s of e){if(!("info"in s)||!s.info?.path||!s.info.method)continue;let l=String(s.info.path),a=String(s.info.method).toUpperCase(),t=l.replaceAll(/:(?<temp1>[^/]+)/g,"{$1}"),p=`${a}:${t}`;i.set(p,{path:l,httpMethod:a,url:t})}return i}import{existsSync as F,readFileSync as I}from"fs";import E from"path";import U from"fast-glob";import r from"typescript";async function C(d){let c=[],i=E.join(d,"services");if(!F(i))throw new Error(`Services directory not found: ${i}`);let n=await U("**/*Service.ts",{cwd:i,absolute:!0,ignore:["**/node_modules/**"]});for(let o of n){let e=E.basename(o,".ts"),s=I(o,"utf8"),l=r.createSourceFile(o,s,r.ScriptTarget.Latest,!0);r.forEachChild(l,a=>{if(r.isClassDeclaration(a)&&a.name?.text===e){for(let t of a.members)if(r.isMethodDeclaration(t)&&r.isIdentifier(t.name)&&t.modifiers&&t.modifiers.some(p=>p.kind===r.SyntaxKind.PublicKeyword)&&t.modifiers.some(p=>p.kind===r.SyntaxKind.StaticKeyword)){let p=t.name.text,u,m,S=h=>{if(r.isReturnStatement(h)&&h.expression&&r.isCallExpression(h.expression)&&r.isIdentifier(h.expression.expression)&&h.expression.expression.text==="__request"){let w=h.expression.arguments[1];if(r.isObjectLiteralExpression(w))for(let f of w.properties)r.isPropertyAssignment(f)&&r.isIdentifier(f.name)&&f.name.text==="url"&&r.isStringLiteral(f.initializer)?u=f.initializer.text:r.isPropertyAssignment(f)&&r.isIdentifier(f.name)&&f.name.text==="method"&&r.isStringLiteral(f.initializer)&&(m=f.initializer.text.toUpperCase())}r.forEachChild(h,S)};t.body&&r.forEachChild(t.body,S),u&&m?c.push({path:o,name:e,methodName:p,toHandleUrl:u,toHandleHttpMethod:m}):(u||console.warn(`No URL found for ${p} request in service ${e} (${o})`),m||console.warn(`No HTTP method found for ${p} request in service ${e} (${o})`))}}})}return c}async function P({genPath:d,srcPath:c}){let i=await C(d),n=new Map;for(let e of i)n.has(e.name)||n.set(e.name,new Map),n.get(e.name)?.set(e.methodName,{serviceInfo:e,files:new Set});let o=await U("**/*.{ts,tsx}",{cwd:c,absolute:!0,ignore:["**/node_modules/**","**/__tests__/**"]});for(let e of o)try{let s=I(e,"utf8"),l=r.createSourceFile(e,s,r.ScriptTarget.Latest,!0),a=t=>{if(r.isCallExpression(t)&&r.isPropertyAccessExpression(t.expression)&&r.isIdentifier(t.expression.expression)&&t.expression.expression.text.endsWith("Service")){let p=t.expression.expression.text,u=t.expression.name.text,m=n.get(p)?.get(u);if(!m)return;m.files.add(e)}r.forEachChild(t,a)};a(l)}catch(s){throw s instanceof Error?new TypeError(`Error parsing ${e}: ${s.message}`):new TypeError(`Error parsing ${e}: Unknown error`)}for(let[e,s]of n.entries()){for(let[l,a]of s.entries())a.files.size===0&&s.delete(l);s.size===0&&n.delete(e)}return n}import T from"path";function N(d,c,i){let n=[];for(let[o,e]of d.entries())for(let[s,l]of e.entries()){let{serviceInfo:a}=l,t=a.toHandleHttpMethod,p=a.toHandleUrl;!c.has(`${t}:${p}`)&&!c.has(`${t}:*${p}`)&&n.push({type:"missing_handler",service:a,usedIn:[...l.files],suggestedPath:T.join(i,`handlers/services/${o}/${s}.ts`)})}return n}M().name("openapi-sync-lint-msw-handlers").description("Lint MSW handlers against OpenAPI generated services from `openapi-sync`.\nIt checks for missing handlers based on generated services and your MSW setup.").requiredOption("--gen <path>","The output folder from `openapi-sync` script. Where the generated models and openapi schema and type definitions are saved.").requiredOption("--msw-setup-file <path>","Path to the MSW setup file (file that configures MSW setupServer or setupWorker).").requiredOption("--msw-setup-const <const>","Name of the constant that holds the MSW setup (e.g., server or worker).").addHelpText("after",`
|
|
3
|
+
Example usage:
|
|
4
|
+
|
|
5
|
+
$ openapi-sync-lint-msw-handlers --gen ./src/api/gen --msw-setup-file ./src/api/__tests__/node.ts --msw-setup-const server`).action(async({gen:d,mswSetupFile:c,mswSetupConst:i})=>{try{let n=process.cwd(),o=y.resolve(n,d),e=y.resolve(n,"."),s=y.resolve(n,c);if(!_(o)||!b(o).isDirectory())throw new Error("Generated API folder does not exist or is not a directory");if(!_(s)||!b(s).isFile())throw new Error("MSW setup file does not exist or is not a file");let l=await P({genPath:o,srcPath:e}),a=await H({mswSetupFilePath:s,mswSetupConst:i}),t=N(l,a,y.dirname(s));$(t),process.exit(t.length>0?1:0)}catch(n){console.error(n),process.exit(1)}}).parseAsync(process.argv);
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import{a as f,b as o,c as y}from"../chunk-3QWYGBKZ.js";import{existsSync as u}from"fs";import{mkdir as E,readFile as T,writeFile as M}from"fs/promises";import d from"path";import{styleText as c}from"util";import v from"@inquirer/confirm";async function g(e,t){await o({name:"Generating models",command:`npx openapi-typescript-codegen --input ${e} --output ${t} --client fetch`})}import{writeFile as O}from"fs/promises";var w=`
|
|
3
|
+
import {
|
|
4
|
+
http as mswHttp,
|
|
5
|
+
type DefaultBodyType,
|
|
6
|
+
type HttpHandler,
|
|
7
|
+
type HttpResponseResolver,
|
|
8
|
+
type PathParams,
|
|
9
|
+
type RequestHandlerOptions,
|
|
10
|
+
} from 'msw';
|
|
11
|
+
import type { paths as ImportedPaths } from './schema';
|
|
12
|
+
|
|
13
|
+
// Type definitions
|
|
14
|
+
type Paths = ImportedPaths;
|
|
15
|
+
type HttpMethod =
|
|
16
|
+
| 'get'
|
|
17
|
+
| 'put'
|
|
18
|
+
| 'post'
|
|
19
|
+
| 'delete'
|
|
20
|
+
| 'options'
|
|
21
|
+
| 'head'
|
|
22
|
+
| 'patch'
|
|
23
|
+
| 'trace';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Type guard to get the http methods available for a given path.
|
|
27
|
+
*/
|
|
28
|
+
type Methods<Path extends keyof Paths> = Extract<keyof Paths[Path], HttpMethod>;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Type guard to get the content type 'application/json' or 'multipart/form-data' of a type.
|
|
32
|
+
*/
|
|
33
|
+
type ExtractContent<T> = T extends { content?: infer C }
|
|
34
|
+
? undefined extends C
|
|
35
|
+
? DefaultBodyType
|
|
36
|
+
: 'application/json' extends keyof C
|
|
37
|
+
? C['application/json']
|
|
38
|
+
: 'multipart/form-data' extends keyof C
|
|
39
|
+
? C['multipart/form-data']
|
|
40
|
+
: DefaultBodyType
|
|
41
|
+
: DefaultBodyType;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Type guard to get the parameters of a path.
|
|
45
|
+
*/
|
|
46
|
+
export type OpenapiPathParams<
|
|
47
|
+
P extends keyof Paths,
|
|
48
|
+
M extends keyof Paths[P],
|
|
49
|
+
> = 'parameters' extends keyof Paths[P][M]
|
|
50
|
+
? 'path' extends keyof Paths[P][M]['parameters']
|
|
51
|
+
? PathParams<keyof Paths[P][M]['parameters']['path']>
|
|
52
|
+
: PathParams
|
|
53
|
+
: PathParams;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Type guard to get the request body of a path.
|
|
57
|
+
*/
|
|
58
|
+
export type OpenapiPathRequestBody<
|
|
59
|
+
P extends keyof Paths,
|
|
60
|
+
M extends keyof Paths[P],
|
|
61
|
+
> = Paths[P][M] extends { requestBody?: infer RB }
|
|
62
|
+
? undefined extends RB
|
|
63
|
+
? DefaultBodyType
|
|
64
|
+
: ExtractContent<RB>
|
|
65
|
+
: DefaultBodyType;
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Type guard to get the response body of a path.
|
|
69
|
+
*/
|
|
70
|
+
export type OpenapiPathResponseBody<
|
|
71
|
+
P extends keyof Paths,
|
|
72
|
+
M extends keyof Paths[P],
|
|
73
|
+
> = Paths[P][M] extends { responses?: infer R }
|
|
74
|
+
? undefined extends R
|
|
75
|
+
? DefaultBodyType
|
|
76
|
+
: 200 extends keyof R
|
|
77
|
+
? ExtractContent<R[200]>
|
|
78
|
+
: 201 extends keyof R
|
|
79
|
+
? ExtractContent<R[201]>
|
|
80
|
+
: DefaultBodyType
|
|
81
|
+
: DefaultBodyType;
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Wrapper around MSW http function so we can have "typesafe" handlers against an openapi schema.
|
|
85
|
+
*
|
|
86
|
+
* @param path - The path to use from the openapi definition.
|
|
87
|
+
* @param method - The method to use on the handler.
|
|
88
|
+
* @param resolver - The MSW resolver function.
|
|
89
|
+
* @param options - The MSW http request handler options.
|
|
90
|
+
* @returns a typesafe wrapper for MSW http function.
|
|
91
|
+
*
|
|
92
|
+
* @throws Error if the method is not supported.
|
|
93
|
+
*/
|
|
94
|
+
export function http<P extends keyof Paths, M extends Methods<P>>(
|
|
95
|
+
path: P,
|
|
96
|
+
method: M,
|
|
97
|
+
resolver: HttpResponseResolver<
|
|
98
|
+
OpenapiPathParams<P, M>,
|
|
99
|
+
OpenapiPathRequestBody<P, M>,
|
|
100
|
+
OpenapiPathResponseBody<P, M>
|
|
101
|
+
>,
|
|
102
|
+
options?: RequestHandlerOptions
|
|
103
|
+
): HttpHandler {
|
|
104
|
+
const uri = \`*\${path.toString().replaceAll(/{(?<temp1>[^}]+)}/g, ':$1')}\`;
|
|
105
|
+
const handlers = {
|
|
106
|
+
head: mswHttp.head,
|
|
107
|
+
get: mswHttp.get,
|
|
108
|
+
post: mswHttp.post,
|
|
109
|
+
put: mswHttp.put,
|
|
110
|
+
delete: mswHttp.delete,
|
|
111
|
+
patch: mswHttp.patch,
|
|
112
|
+
options: mswHttp.options,
|
|
113
|
+
} as const;
|
|
114
|
+
if (typeof method !== 'string' || !Object.hasOwn(handlers, method)) {
|
|
115
|
+
throw new Error('Unsupported Http Method');
|
|
116
|
+
}
|
|
117
|
+
return handlers[method as keyof typeof handlers](uri, resolver, options);
|
|
118
|
+
}
|
|
119
|
+
`;async function P(e){await o({name:"Generating openapi MSW utils",command:async()=>{await O(`${e}/openapi-msw-http.ts`,w)}})}import{readFile as R,writeFile as H}from"fs/promises";async function x(e,t){await o({name:"Generating schema types",command:async()=>{await f(`npx openapi-typescript ${e} -o ${t}`);let n=await R(t,"utf8");await H(t,`/* eslint-disable -- Autogenerated file */
|
|
120
|
+
${n}`)}})}async function B(e){if(d.extname(e)!=="")throw new Error("Output must be a directory.");let t=process.cwd(),n=d.resolve(e),a=n.startsWith(t)?n:d.resolve(t,d.relative(d.parse(e).root,e));return u(a)||await o({name:"Generating output directory",command:async()=>{await E(a,{recursive:!0})}}),a}async function $(e,t){let[n,r]=await Promise.all([o({name:"Reading input openapi schema",command:async()=>{if(!e.endsWith(".json"))throw new Error(`Input file must be a JSON file: ${e}`);if(e.startsWith("http"))try{let{stdout:a}=await f(`curl -s ${e} --fail`);return a}catch{throw new Error(`Failed to fetch remote OpenAPI file: ${e}`)}if(!u(e))throw new Error(`Input file does not exist: ${e}`);return await T(e,"utf8")}}),o({name:"Reading output openapi schema",command:async()=>{if(!t.endsWith(".json"))throw new Error(`Output file must be a JSON file: ${t}`);return u(t)?await T(t,"utf8"):!1}})]);return[JSON.stringify(JSON.parse(n)),r?JSON.stringify(JSON.parse(r)):!1]}y().name("openapi-sync").description("A CLI tool to convert OpenAPI 3.0/3.1 schemas to TypeScript types and create type-safe fetching based on a openapi file and keep them in sync.").requiredOption("-i, --input <path>","The input (local or remote) openapi schema (JSON).").requiredOption("-o, --output <folder>","The output folder to save the generated models and openapi schema and type definitions.").option("-f, --force-gen","Force generation of typescript schemas and fetching code even if the input and output schemas are identical.").option("--include-msw-utils","Include MSW mocking utilities based on the generated typescript types.").option("--post-script <script>","A package.json script to run after the code generation.").addHelpText("after",`
|
|
121
|
+
Example usage:
|
|
122
|
+
|
|
123
|
+
$ openapi-sync -i ./input.json -o ./src/api/gen --include-msw-utils`).action(async({input:e,output:t,forceGen:n,includeMswUtils:r,postScript:a})=>{try{console.log(c("magenta",`
|
|
124
|
+
\u{1F680} openapi-sync
|
|
125
|
+
`));let s=await B(t),p=`${s}/openapi.json`,k=`${s}/schema.d.ts`,[m,i]=await $(e,p);i&&m===i&&!n?(console.log(c("blue",`
|
|
126
|
+
No updates required.
|
|
127
|
+
`)),process.exit(0)):i?i&&m!==i&&(console.log(c("yellow",`
|
|
128
|
+
\u26A0\uFE0F Local and remote schemas does not match!
|
|
129
|
+
`)),await v({message:"Do you want to use the remote schema? (y/n)?"})?await o({name:"Replacing local schema with input schema",command:M(p,m)}):(console.log(c("yellow",`
|
|
130
|
+
\u26A0\uFE0F Sync remote schemas skipped.
|
|
131
|
+
`)),n||process.exit(0))):await o({name:"Creating local schema",command:M(p,m)}),await Promise.all([x(p,k),g(p,s)]),r&&await P(s),a&&await o({name:"Running post script",command:`node --run ${a}`}),console.log(c("green",`
|
|
132
|
+
\u2705 openapi-sync process completed!
|
|
133
|
+
`))}catch(s){console.error(s),process.exit(1)}}).parseAsync(process.argv);
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import{c as r}from"../chunk-
|
|
2
|
+
import{c as r}from"../chunk-3QWYGBKZ.js";import{execSync as p}from"child_process";import{styleText as u}from"util";import{execSync as m}from"child_process";import a from"@inquirer/select";async function i(){let t=m("npx turbo ls",{encoding:"utf8",stdio:"pipe"}).split(`
|
|
3
3
|
`).slice(1).map(e=>e.trim()).filter(Boolean).map(e=>e.split(" ")[0]);return await a({message:"Select a package to run the script:",choices:t.map(e=>({name:e,value:e}))})}async function c(){return await a({message:"Select a mode to load different env files:",choices:[{name:"development",value:"development"},{name:"staging",value:"staging"},{name:"production",value:"production"}]})}r().name("turbo-select").description(`A CLI tool to filter and select a single package from the Turborepo package list and run a script command.
|
|
4
|
-
Additionally, allow to prompt environment mode (development, staging, production), for example, when using Vite.`).requiredOption("--run <script>","The package script command to execute (e.g., --run=dev).").option("--select-env","An environment mode (development, staging, production) If using for example vite.").
|
|
4
|
+
Additionally, allow to prompt environment mode (development, staging, production), for example, when using Vite.`).requiredOption("--run <script>","The package script command to execute (e.g., --run=dev).").option("--select-env","An environment mode (development, staging, production) If using for example vite.").addHelpText("after",`
|
|
5
|
+
Example usage:
|
|
6
|
+
|
|
7
|
+
$ turbo-select --run dev --select-env`).action(async({run:n,selectEnv:t})=>{try{console.log(u("magenta",`
|
|
5
8
|
\u{1F680} Turbo-Select
|
|
6
9
|
`));let e=await i(),o=t?await c():void 0;p(`turbo run ${n} --ui stream ${e?`--filter=${e}`:""} ${o?`-- --mode ${o}`:""}`,{encoding:"utf8",stdio:"inherit"})}catch(e){console.error(e),process.exit(1)}}).parseAsync(process.argv);
|