@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 CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  [![NPM version](https://img.shields.io/npm/v/@yunarch/config-web?color=3eb910&label=)](https://www.npmjs.com/package/@yunarch/config-web)
4
4
 
5
- > A curated set of linters (ESLint, Oxlint), formatters (Prettier, Biome), TypeScript configurations for web projects, and useful CLI tools.
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
- - [📐 Code Formatting](#-code-formatting)
18
- - [Prettier](#prettier)
19
- - [Biome](#biome)
20
- - [🧹 Linting](#-linting)
21
- - [ESlint](#eslint)
22
- - [Override configuration](#override-configuration)
23
- - [Typescript Type aware rules](#typescript-type-aware-rules)
24
- - [Enabling ESLint and Oxlint Simultaneously](#enabling-eslint-and-oxlint-simultaneously)
25
- - [Oxlint](#oxlint)
26
- - [🔵 Typescript](#-typescript)
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 Formatting:** Prettier, Biome
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
- ## 📐 Code Formatting
78
+ ## ⚠️ Caveats
78
79
 
79
- This package comes with configurations for both `Prettier` and `Biome` to ensure consistent formatting across your codebase.
80
+ ### Code Formatting
80
81
 
81
- > [!NOTE]
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/).
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
- ### Prettier
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
- ### Biome
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
- #### Override configuration
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
- #### Typescript Type aware rules
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
- #### Enabling ESLint and Oxlint Simultaneously
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
- ## 🔵 Typescript
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
- - **`bun-run-all`**: CLI tool for running npm package scripts in parallel or sequential by using bun.
350
- - **`openapi-sync`**: 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.
351
- - **`turbo-select`**: 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).
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-PWSW557X.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}
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.").action(async(o,e)=>{try{console.log(h("magenta",`
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-PWSW557X.js";import{execSync as p}from"child_process";import{styleText as u}from"util";import{execSync as l}from"child_process";import a from"@inquirer/select";async function i(){let t=l("npx turbo ls",{encoding:"utf8",stdio:"pipe"}).split(`
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.").action(async({run:n,selectEnv:t})=>{try{console.log(u("magenta",`
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);