@nemmtor/ts-databuilders 0.0.1-alpha.8 → 0.0.1-alpha.9

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.
Files changed (3) hide show
  1. package/README.md +24 -8
  2. package/dist/main.js +6 -3
  3. package/package.json +25 -12
package/README.md CHANGED
@@ -27,7 +27,7 @@ You can also generate configuration file by providing values step by step in an
27
27
  pnpm ts-databuilders init --wizard
28
28
  ```
29
29
 
30
- ### Configure via CLI flags (optional:
30
+ ### Configure via CLI flags (optional):
31
31
  ```bash
32
32
  pnpm ts-databuilders --output-dir="src/__generated__" --jsdoc-tag=MyBuilder
33
33
  ```
@@ -49,6 +49,9 @@ pnpm ts-databuilders --wizard
49
49
 
50
50
  **Priority:** CLI flags > Config file > Built-in defaults
51
51
 
52
+ #### Debugging
53
+ In order to turn on debug logs pass a flag: `--log-level debug`.
54
+
52
55
  ## Quick Start
53
56
  **1. Annotate your types with JSDoc:**
54
57
  ```ts
@@ -133,7 +136,7 @@ Where in reality the only thing specific to this single test is the fact that so
133
136
  Imagine even more complex scenario:
134
137
  ```tsx
135
138
  it('should show validation error when email is invalid', async () => {
136
- render(<ProfileForm defaultValues={
139
+ render(<ProfileForm defaultValues={{
137
140
  firstName: '',
138
141
  lastName: '',
139
142
  age: 0,
@@ -152,7 +155,7 @@ it('should show validation error when email is invalid', async () => {
152
155
  skills: [],
153
156
  bio: '',
154
157
  email: 'invalid-email'
155
- }
158
+ }}
156
159
  />)
157
160
 
158
161
  await submitForm();
@@ -165,9 +168,13 @@ Again - in reality you should only be worried about email, not about whole form
165
168
  Here's how above tests could be written with databuilders:
166
169
  ```ts
167
170
  it('should emit a ContentUpdatedEvent', () => {
168
- const aggregate = DocumentAggregate.create(new CreateDocumentAggregatedPayloadBuilder().build());
171
+ const aggregate = DocumentAggregate.create(
172
+ new CreateDocumentAggregatedPayloadBuilder().build()
173
+ );
169
174
 
170
- aggregate.updateContent(new UpdateDocumentContentPayloadBuilder().withContent('new-content').build());
175
+ aggregate.updateContent(
176
+ new UpdateDocumentContentPayloadBuilder().withContent('new-content').build()
177
+ );
171
178
 
172
179
  expect(...);
173
180
  })
@@ -175,7 +182,9 @@ it('should emit a ContentUpdatedEvent', () => {
175
182
 
176
183
  ```tsx
177
184
  it('should show validation error when email is invalid', async () => {
178
- render(<ProfileForm defaultValues={new ProfileFormInputBuilder.withEmail('invalid-email').build()} />)
185
+ render(<ProfileForm defaultValues={
186
+ new ProfileFormInputBuilder.withEmail('invalid-email').build()} />
187
+ )
179
188
 
180
189
  await submitForm();
181
190
 
@@ -185,6 +194,8 @@ it('should show validation error when email is invalid', async () => {
185
194
 
186
195
  This not only makes the test code less verbose but also highlights what is really being tested.
187
196
 
197
+ **Why not use AI for that?** While AI can generate test data, ts-databuilders is fast, free and deterministic.
198
+
188
199
  [Read more about data builders.](http://www.natpryce.com/articles/000714.html)
189
200
 
190
201
  ## Nested Builders
@@ -333,7 +344,7 @@ export type User = { name: string };
333
344
  ```
334
345
 
335
346
  ### Type Aliases Only
336
- Currently, only **type aliases** are supported as root builder types. Interfaces, classes, and enums are not supported:
347
+ Currently, only **type aliases** are supported as root builder types. Interfaces, classes etc. are not supported:
337
348
  ```ts
338
349
  // ❌ Not supported
339
350
  /** @DataBuilder */
@@ -354,7 +365,7 @@ export type User = {
354
365
  };
355
366
  ```
356
367
 
357
- ### Unsupported Type Features
368
+ ### Unsupported TypeScript Features
358
369
 
359
370
  Some TypeScript features are not yet supported and will cause generation errors:
360
371
 
@@ -385,12 +396,17 @@ Some TypeScript features are not yet supported and will cause generation errors:
385
396
  - Test thoroughly before using in production
386
397
 
387
398
  **Found an issue?** Please [report it on GitHub](https://github.com/nemmtor/ts-databuilders/issues) with:
399
+ - A minimal reproducible example (if possible)
388
400
  - The type definition causing the issue
389
401
  - The error message received
390
402
  - Your `ts-databuilders.json` config and any provided CLI flags (if applicable)
391
403
 
404
+ You can also turn on debug logs by passing `--log-level debug` flag.
405
+
392
406
  Your feedback helps improve the library for everyone! 🙏
393
407
 
408
+ ## Similar Projects
409
+ - [effect-builder](https://github.com/slashlifeai/effect-builder) - a runtime library for building objects with Effect Schema validation.
394
410
 
395
411
  ## Contributing
396
412
 
package/dist/main.js CHANGED
@@ -1,8 +1,10 @@
1
1
  #!/usr/bin/env node
2
- import*as it from"@effect/platform-node/NodeContext";import*as rt from"@effect/platform-node/NodeRuntime";import{Effect as Ut}from"effect";import*as ot from"effect/Layer";import*as nt from"effect/Logger";import*as at from"effect/LogLevel";import*as U from"@effect/cli/Command";import*as X from"effect/Layer";import*as $e from"@effect/platform/FileSystem";import*as we from"@effect/platform/Path";import*as B from"effect/Effect";import*as pe from"@effect/platform/FileSystem";import*as Te from"@effect/platform/Path";import*as xe from"effect/Context";import*as C from"effect/Effect";import*as T from"effect/Option";import*as r from"effect/Schema";var D={jsdocTag:"JSDoc tag used to mark types for data building generation.",outputDir:"Output directory for generated builders.",include:"Glob pattern for files included while searching for jsdoc tag.",fileSuffix:"File suffix for created builder files.",builderSuffix:"Suffix for generated classes.",defaults:"Default values to be used in data builder constructor.",defaultString:"Default string value to be used in data builder constructor.",defaultNumber:"Default number value to be used in data builder constructor.",defaultBoolean:"Default boolean value to be used in data builder constructor."};import*as oe from"effect/Effect";class $ extends oe.Service()("@TSDataBuilders/Process",{succeed:{cwd:oe.sync(()=>process.cwd())}}){}var De=(t)=>{return t!==void 0};var ee=(t)=>{return Object.fromEntries(Object.entries(t).filter(([i,c])=>De(c)))};var ge="ts-databuilders.json",st=r.Struct({jsdocTag:r.NonEmptyTrimmedString,outputDir:r.NonEmptyTrimmedString,include:r.NonEmptyTrimmedString,fileSuffix:r.NonEmptyTrimmedString,builderSuffix:r.NonEmptyTrimmedString,defaults:r.Struct({string:r.String,number:r.Number,boolean:r.Boolean})}),M=st.make({jsdocTag:"DataBuilder",outputDir:"generated/builders",include:"src/**/*.ts{,x}",fileSuffix:".builder",builderSuffix:"Builder",defaults:{string:"",number:0,boolean:!1}}),he=r.Struct({$schema:r.optional(r.String),jsdocTag:r.String.pipe(r.annotations({description:D.jsdocTag})),outputDir:r.String.pipe(r.annotations({description:D.outputDir})),include:r.String.pipe(r.annotations({description:D.include})),fileSuffix:r.String.pipe(r.annotations({description:D.fileSuffix})),builderSuffix:r.String.pipe(r.annotations({description:D.builderSuffix})),defaults:r.Struct({string:r.String.pipe(r.annotations({description:D.defaultString})),number:r.Number.pipe(r.annotations({description:D.defaultNumber})),boolean:r.Boolean.pipe(r.annotations({description:D.defaultBoolean}))}).pipe(r.partial,r.annotations({description:D.defaults}))}).pipe(r.partial);class N extends xe.Tag("Configuration")(){}var R=r.Struct({jsdocTag:r.NonEmptyTrimmedString,outputDir:r.NonEmptyTrimmedString,include:r.NonEmptyTrimmedString,fileSuffix:r.NonEmptyTrimmedString,builderSuffix:r.NonEmptyTrimmedString,defaultString:r.String,defaultNumber:r.NumberFromString,defaultBoolean:r.BooleanFromString}),Fe=(t)=>C.gen(function*(){yield*C.logDebug("[Configuration]: Loading configuration");let c=yield*(yield*$).cwd,g=(yield*Te.Path).join(c,ge),s=yield*lt(g),o=yield*ct({fromCLI:t,fromConfigFile:s});return N.of(o)}),ct=(t)=>C.gen(function*(){let i=ft(t),c=T.flatMap(t.fromConfigFile,(o)=>T.fromNullable(o.defaults)).pipe(T.map((o)=>ee(o)),T.getOrElse(()=>({}))),e=ee({string:t.fromCLI.defaultString.pipe(T.getOrUndefined),number:t.fromCLI.defaultNumber.pipe(T.getOrUndefined),boolean:t.fromCLI.defaultBoolean.pipe(T.getOrUndefined)}),g={...c,...e},s={builderSuffix:yield*i("builderSuffix"),include:yield*i("include"),fileSuffix:yield*i("fileSuffix"),jsdocTag:yield*i("jsdocTag"),outputDir:yield*i("outputDir"),defaults:{...M.defaults,...g}};return yield*C.logDebug(`[Configuration]: Resolving config with value: ${JSON.stringify(s,null,4)}`),s}),ft=(t)=>(i)=>t.fromCLI[i].pipe(C.orElse(()=>T.flatMap(t.fromConfigFile,(c)=>T.fromNullable(c[i]))),C.orElseSucceed(()=>M[i])),lt=(t)=>C.gen(function*(){let i=yield*pe.FileSystem;if(yield*C.orDie(i.exists(t))){yield*C.logDebug("[Configuration]: Found config file - attempting to read it");let e=yield*dt(t),g=yield*r.decodeUnknown(he)(e);return T.some(g)}else return yield*C.logDebug("[Configuration]: No config file found"),T.none()}),dt=(t)=>C.gen(function*(){let i=yield*pe.FileSystem,c=yield*C.orDie(i.readFileString(t));return yield*C.try({try:()=>JSON.parse(c),catch:(e)=>`[FileSystem] Unable to read and parse JSON file from '${t}': ${String(e)}`})}).pipe(C.orDie);import*as Ne from"@effect/platform/FileSystem";import*as Oe from"@effect/platform/Path";import*as k from"effect/Effect";import*as x from"effect/Match";import{Project as mt}from"ts-morph";var ne=(t)=>t.replace(/([a-z0-9])([A-Z])/g,"$1-$2").replace(/([A-Z])([A-Z][a-z])/g,"$1-$2").replace(/_/g,"-").toLowerCase(),Se=(t)=>{return t.split(/[-_\s]+/).flatMap((i)=>i.split(/(?=[A-Z])/)).filter(Boolean).map((i)=>i.charAt(0).toUpperCase()+i.slice(1).toLowerCase()).join("")},ye=(t)=>{return t.split(/[-_\s]+/).flatMap((c)=>c.split(/(?=[A-Z])/)).filter(Boolean).map((c,e)=>{let g=c.toLowerCase();return e===0?g:g.charAt(0).toUpperCase()+g.slice(1)}).join("")};var Be=(t)=>{let{fieldName:i,optional:c,typeName:e,isNestedBuilder:g}=t,s=i.replaceAll("'","").replaceAll('"',""),o=ye(s),m=`with${Se(s)}`,d=[`return this.with({ ${i}: ${o} });`],u=[`return this.with({ ${i}: ${o}.build() });`],a=g?u:d,n=[`if (!${o}) {`,` const { "${s}": _unused, ...rest } = this.build();`," return this.with(rest);","}"],S=c?[...n,...a]:a,h=`${e}['${s}']`;return{name:m,isPublic:!0,parameters:[{name:o,type:g?`DataBuilder<${h}>`:h}],statements:S}};class ae extends k.Service()("@TSDataBuilders/BuilderGenerator",{effect:k.gen(function*(){let t=yield*Ne.FileSystem,i=yield*Oe.Path,c=yield*$,e=yield*N,{fileSuffix:g,builderSuffix:s,defaults:o}=e,m=(d)=>x.value(d).pipe(x.when({kind:"STRING"},()=>`"${o.string}"`),x.when({kind:"NUMBER"},()=>o.number),x.when({kind:"BOOLEAN"},()=>o.boolean),x.when({kind:"UNDEFINED"},()=>"undefined"),x.when({kind:"DATE"},()=>"new Date()"),x.when({kind:"ARRAY"},()=>"[]"),x.when({kind:"LITERAL"},(u)=>u.literalValue),x.when({kind:"TYPE_CAST"},(u)=>m(u.baseTypeMetadata)),x.when({kind:"TUPLE"},(u)=>{return`[${u.members.map((n)=>m(n)).map((n)=>`${n}`).join(", ")}]`}),x.when({kind:"TYPE_LITERAL"},(u)=>{return`{${Object.entries(u.metadata).filter(([n,{optional:S}])=>!S).map(([n,S])=>`${n}: ${m(S)}`).join(", ")}}`}),x.when({kind:"RECORD"},(u)=>{if(u.keyType.kind==="STRING"||u.keyType.kind==="NUMBER")return"{}";let a=m(u.keyType),n=m(u.valueType);return`{${a}: ${n}}`}),x.when({kind:"UNION"},(u)=>{let n=u.members.slice().sort((S,h)=>{let b=ke.indexOf(S.kind),O=ke.indexOf(h.kind);return(b===-1?1/0:b)-(O===-1?1/0:O)})[0];if(!n)return"never";return m(n)}),x.when({kind:"BUILDER"},(u)=>{return`new ${u.name}${s}().build()`}),x.exhaustive);return{generateBaseBuilder:k.fnUntraced(function*(){let d=i.join(yield*c.cwd,e.outputDir),u=i.resolve(d,"data-builder.ts");yield*k.logDebug(`[Builders]: Creating base builder at ${u}`),yield*k.orDie(t.writeFileString(u,pt))}),generateBuilder:k.fnUntraced(function*(d){let u=new mt,a=d.name;yield*k.logDebug(`[Builders]: Creating builder for ${a}`);let n=i.join(yield*c.cwd,e.outputDir),S=i.resolve(n,`${ne(a)}${g}.ts`);yield*k.logDebug(`[Builders]: Creating builder file at ${S}`);let h=u.createSourceFile(S,"",{overwrite:!0}),b=i.resolve(d.path),O=i.relative(i.dirname(S),b).replace(/\.ts$/,"");if(h.addImportDeclaration({namedImports:[a],isTypeOnly:!0,moduleSpecifier:O}),h.addImportDeclaration({namedImports:["DataBuilder"],moduleSpecifier:"./data-builder"}),d.shape.kind!=="TYPE_LITERAL")return yield*k.dieMessage("[BuilderGenerator]: Expected root type to be type literal");[...new Set(gt(d.shape.metadata))].forEach((v)=>{h.addImportDeclaration({namedImports:[`${v}${s}`],moduleSpecifier:`./${ne(v)}${g}`})});let Y=Object.entries(d.shape.metadata).filter(([v,{optional:j}])=>!j).map(([v,j])=>`${v}: ${j.kind==="TYPE_CAST"?`${m(j)} as ${a}['${v}']`:m(j)}`),K=Object.entries(d.shape.metadata).map(([v,{optional:j,kind:me}])=>Be({fieldName:v,optional:j,typeName:a,isNestedBuilder:me==="BUILDER"})),J=`{
2
+ import*as at from"@effect/platform-node/NodeContext";import*as st from"@effect/platform-node/NodeRuntime";import*as ct from"effect/Effect";import*as dt from"effect/Layer";import*as lt from"effect/Logger";import*as ft from"effect/LogLevel";import*as L from"@effect/cli/Command";import*as u from"@effect/cli/Options";import*as X from"effect/Layer";import{Project as Nt}from"ts-morph";import*as De from"@effect/platform/FileSystem";import*as Oe from"@effect/platform/Path";import*as g from"effect/Effect";import*as b from"effect/Match";import*as Ge from"effect/Option";import*as oe from"effect/Schema";import*as he from"@effect/platform/FileSystem";import*as Be from"@effect/platform/Path";import*as Re from"effect/Context";import*as D from"effect/Effect";import*as x from"effect/Option";import*as r from"effect/Schema";var O={jsdocTag:"JSDoc tag used to mark types for data building generation.",outputDir:"Output directory for generated builders.",include:"Glob pattern for files included while searching for jsdoc tag.",fileSuffix:"File suffix for created builder files.",builderSuffix:"Suffix for generated classes.",defaults:"Default values to be used in data builder constructor.",defaultString:"Default string value to be used in data builder constructor.",defaultNumber:"Default number value to be used in data builder constructor.",defaultBoolean:"Default boolean value to be used in data builder constructor."};import*as le from"effect/Effect";var F=class extends le.Service()("@TSDataBuilders/Process",{succeed:{cwd:le.sync(()=>process.cwd())}}){};var mt=e=>e!==void 0,ie=e=>Object.fromEntries(Object.entries(e).filter(([i,s])=>mt(s)));var Ee="ts-databuilders.json",gt=r.Struct({jsdocTag:r.NonEmptyTrimmedString,outputDir:r.NonEmptyTrimmedString,include:r.NonEmptyTrimmedString,fileSuffix:r.NonEmptyTrimmedString,builderSuffix:r.NonEmptyTrimmedString,defaults:r.Struct({string:r.String,number:r.Number,boolean:r.Boolean})}),_=gt.make({jsdocTag:"DataBuilder",outputDir:"generated/builders",include:"src/**/*.ts{,x}",fileSuffix:".builder",builderSuffix:"Builder",defaults:{string:"",number:0,boolean:!1}}),Ce=r.Struct({$schema:r.optional(r.String),jsdocTag:r.String.pipe(r.annotations({description:O.jsdocTag})),outputDir:r.String.pipe(r.annotations({description:O.outputDir})),include:r.String.pipe(r.annotations({description:O.include})),fileSuffix:r.String.pipe(r.annotations({description:O.fileSuffix})),builderSuffix:r.String.pipe(r.annotations({description:O.builderSuffix})),defaults:r.Struct({string:r.String.pipe(r.annotations({description:O.defaultString})),number:r.Number.pipe(r.annotations({description:O.defaultNumber})),boolean:r.Boolean.pipe(r.annotations({description:O.defaultBoolean}))}).pipe(r.partial,r.annotations({description:O.defaults}))}).pipe(r.partial),M=class extends Re.Tag("Configuration")(){},K=r.Struct({jsdocTag:r.NonEmptyTrimmedString,outputDir:r.NonEmptyTrimmedString,include:r.NonEmptyTrimmedString,fileSuffix:r.NonEmptyTrimmedString,builderSuffix:r.NonEmptyTrimmedString,defaultString:r.String,defaultNumber:r.NumberFromString,defaultBoolean:r.BooleanFromString}),Le=e=>D.gen(function*(){yield*D.logDebug("[Configuration]: Loading configuration");let s=yield*(yield*F).cwd,n=(yield*Be.Path).join(s,Ee),o=yield*Tt(n),p=yield*yt({fromCLI:e,fromConfigFile:o});return M.of(p)}),yt=e=>D.gen(function*(){let i=St(e),s=x.flatMap(e.fromConfigFile,p=>x.fromNullable(p.defaults)).pipe(x.map(p=>ie(p)),x.getOrElse(()=>({}))),l=ie({string:e.fromCLI.defaultString.pipe(x.getOrUndefined),number:e.fromCLI.defaultNumber.pipe(x.getOrUndefined),boolean:e.fromCLI.defaultBoolean.pipe(x.getOrUndefined)}),n={...s,...l},o={builderSuffix:yield*i("builderSuffix"),include:yield*i("include"),fileSuffix:yield*i("fileSuffix"),jsdocTag:yield*i("jsdocTag"),outputDir:yield*i("outputDir"),defaults:{..._.defaults,...n}};return yield*D.logDebug(`[Configuration]: Resolving config with value: ${JSON.stringify(o,null,4)}`),o}),St=e=>i=>e.fromCLI[i].pipe(D.orElse(()=>x.flatMap(e.fromConfigFile,s=>x.fromNullable(s[i]))),D.orElseSucceed(()=>_[i])),Tt=e=>D.gen(function*(){let i=yield*he.FileSystem;if(yield*D.orDie(i.exists(e))){yield*D.logDebug("[Configuration]: Found config file - attempting to read it");let l=yield*ht(e),n=yield*r.decodeUnknown(Ce)(l);return x.some(n)}else return yield*D.logDebug("[Configuration]: No config file found"),x.none()}),ht=e=>D.gen(function*(){let i=yield*he.FileSystem,s=yield*D.orDie(i.readFileString(e));return yield*D.try({try:()=>JSON.parse(s),catch:l=>`[FileSystem] Unable to read and parse JSON file from '${e}': ${String(l)}`})}).pipe(D.orDie);import*as P from"effect/Schema";var Ne=P.transform(P.String,P.String.pipe(P.brand("KebabCase")),{decode:e=>e.replace(/([a-z0-9])([A-Z])/g,"$1-$2").replace(/([A-Z])([A-Z][a-z])/g,"$1-$2").replace(/_/g,"-").toLowerCase(),encode:e=>e}),$e=P.transform(P.String,P.String.pipe(P.brand("PascalCase")),{decode:e=>e.split(/[-_\s]+/).flatMap(i=>i.split(/(?=[A-Z])/)).filter(Boolean).map(i=>i.charAt(0).toUpperCase()+i.slice(1).toLowerCase()).join(""),encode:e=>e}),Ue=P.transform(P.String,P.String.pipe(P.brand("CamelCase")),{decode:e=>e.split(/[-_\s]+/).flatMap(i=>i.split(/(?=[A-Z])/)).filter(Boolean).map((i,s)=>{let l=i.toLowerCase();return s===0?l:l.charAt(0).toUpperCase()+l.slice(1)}).join(""),encode:e=>e});import{randomUUID as Ct}from"crypto";import*as fe from"effect/Effect";var J=class extends fe.Service()("@TSDataBuilders/IdGenerator",{succeed:{generateUuid:fe.sync(()=>Ct())}}){};var pe=class extends g.Service()("@TSDataBuilders/BuilderGenerator",{effect:g.gen(function*(){let i=yield*De.FileSystem,s=yield*Oe.Path,l=yield*F,n=yield*M,o=yield*J,{fileSuffix:p,builderSuffix:c,defaults:d}=n,m=T=>Ge.getOrUndefined(T.inlineDefault)??b.value(T).pipe(b.when({kind:"STRING"},()=>`"${d.string}"`),b.when({kind:"NUMBER"},()=>d.number),b.when({kind:"BOOLEAN"},()=>d.boolean),b.when({kind:"UNDEFINED"},()=>"undefined"),b.when({kind:"NULL"},()=>"null"),b.when({kind:"DATE"},()=>"new Date()"),b.when({kind:"ARRAY"},()=>"[]"),b.when({kind:"LITERAL"},y=>y.literalValue),b.when({kind:"TYPE_CAST"},y=>m(y.baseTypeMetadata)),b.when({kind:"TUPLE"},y=>`[${y.members.map(f=>m(f)).map(f=>`${f}`).join(", ")}]`),b.when({kind:"TYPE_LITERAL"},y=>`{${Object.entries(y.metadata).filter(([f,{optional:h}])=>!h).map(([f,h])=>`${f}: ${m(h)}`).join(", ")}}`),b.when({kind:"RECORD"},y=>{if(y.keyType.kind==="STRING"||y.keyType.kind==="NUMBER")return"{}";let a=m(y.keyType),f=m(y.valueType);return`{${a}: ${f}}`}),b.when({kind:"UNION"},y=>{let f=y.members.slice().sort((h,C)=>{let N=je.indexOf(h.kind),I=je.indexOf(C.kind);return(N===-1?1/0:N)-(I===-1?1/0:I)})[0];return f?m(f):"never"}),b.when({kind:"BUILDER"},y=>`new ${y.name}${c}().build()`),b.exhaustive);return{generateBaseBuilder:g.fnUntraced(function*(){let T=s.join(yield*l.cwd,n.outputDir),y=s.resolve(T,"data-builder.ts");yield*g.logDebug(`[Builders]: Creating base builder at ${y}`),yield*g.orDie(i.writeFileString(y,`${Dt}
3
+ `))}),generateBuilder:g.fnUntraced(function*(T){let y=new Nt,a=T.name;yield*g.logDebug(`[Builders]: Creating builder for ${a}`);let f=s.join(yield*l.cwd,n.outputDir),h=s.resolve(f,`${yield*Ne.pipe(oe.decode)(a)}${p}.ts`);yield*g.logDebug(`[Builders]: Creating builder content for ${a}`);let C=y.createSourceFile(`__temp_${yield*o.generateUuid}.ts`,"",{overwrite:!0}),N=s.resolve(T.path),I=s.relative(s.dirname(h),N).replace(/\.ts$/,"");if(C.addImportDeclaration({namedImports:[a],isTypeOnly:!0,moduleSpecifier:I}),C.addImportDeclaration({namedImports:["DataBuilder"],moduleSpecifier:"./data-builder"}),T.shape.kind!=="TYPE_LITERAL")return yield*g.dieMessage("[BuilderGenerator]: Expected root type to be type literal");let v=[...new Set(Ot(T.shape.metadata))];yield*g.forEach(v,$=>Ne.pipe(oe.decode)($).pipe(g.andThen(k=>C.addImportDeclaration({namedImports:[`${$}${c}`],moduleSpecifier:`./${k}${p}`}))),{concurrency:"unbounded"});let Y=Object.entries(T.shape.metadata).filter(([$,{optional:k}])=>!k).map(([$,k])=>`${$}: ${k.kind==="TYPE_CAST"?`${m(k)} as ${a}['${$}']`:m(k)}`),W=yield*g.all(Object.entries(T.shape.metadata).map(([$,{optional:k,kind:de}])=>bt({fieldName:$,optional:k,typeName:a,isNestedBuilder:de==="BUILDER"})),{concurrency:"unbounded"}),ee=`{
3
4
  ${Y.join(`,
4
5
  `)}
5
- }`;h.addClass({name:`${a}${s}`,isExported:!0,extends:`DataBuilder<${a}>`,methods:[{name:"constructor",statements:[`super(${J});`]},...K]}),yield*k.logDebug(`[Builders]: Saving builder content at ${S}`),h.saveSync()})}})}){}var ke=["UNDEFINED","BOOLEAN","NUMBER","STRING","DATE","LITERAL","TYPE_LITERAL","ARRAY","TUPLE","RECORD"],pt=`export abstract class DataBuilder<T> {
6
+ }`;C.addClass({name:`${a}${c}`,isExported:!0,extends:`DataBuilder<${a}>`,methods:[{name:"constructor",statements:[`super(${ee});`]},...W]}),yield*g.logDebug(`[Builders]: Saving builder content at ${h}`),yield*i.writeFileString(h,`${C.getText()}
7
+ `)})}}),dependencies:[J.Default]}){},je=["UNDEFINED","BOOLEAN","NUMBER","STRING","DATE","LITERAL","TYPE_LITERAL","ARRAY","TUPLE","RECORD"],Dt=`export abstract class DataBuilder<T> {
6
8
  private data: T;
7
9
 
8
10
  constructor(initialData: T) {
@@ -18,4 +20,5 @@ import*as it from"@effect/platform-node/NodeContext";import*as rt from"@effect/p
18
20
  return this;
19
21
  }
20
22
  }
21
- `;function gt(t){let i=[];function c(e){switch(e.kind){case"BUILDER":i.push(e.name);break;case"TYPE_LITERAL":Object.values(e.metadata).forEach(c);break;case"UNION":case"TUPLE":e.members.forEach(c);break;case"RECORD":c(e.keyType),c(e.valueType);break}}return Object.values(t).forEach(c),i}class z extends B.Service()("@TSDataBuilders/Builders",{effect:B.gen(function*(){let t=yield*$e.FileSystem,i=yield*ae,c=yield*$,e=yield*we.Path,g=yield*N;return{create:B.fnUntraced(function*(s){let o=e.join(yield*c.cwd,g.outputDir);if(yield*B.orDie(t.exists(o)))yield*B.logDebug(`[Builders]: Removing already existing output directory at ${o}`),yield*B.orDie(t.remove(o,{recursive:!0}));yield*B.logDebug(`[Builders]: Creating output directory at ${o}`),yield*B.orDie(t.makeDirectory(o,{recursive:!0})),yield*i.generateBaseBuilder();let d=s.map((n)=>n.name),u=d.filter((n,S)=>d.indexOf(n)!==S),a=[...new Set(u)];if(u.length>0)return yield*B.dieMessage(`Duplicated builders: ${a.join(", ")}`);yield*B.all(s.map((n)=>i.generateBuilder(n)),{concurrency:"unbounded"})})}}),dependencies:[ae.Default]}){}import*as l from"@effect/cli/Options";var St=l.text("jsdoc-tag").pipe(l.withDescription(D.jsdocTag),l.withSchema(R.fields.jsdocTag),l.optional),yt=l.text("output-dir").pipe(l.withAlias("o"),l.withDescription(D.outputDir),l.withSchema(R.fields.outputDir),l.optional),Et=l.text("include").pipe(l.withAlias("i"),l.withDescription(D.include),l.withSchema(R.fields.include),l.optional),bt=l.text("file-suffix").pipe(l.withDescription(D.fileSuffix),l.withSchema(R.fields.fileSuffix),l.optional),Ct=l.text("builder-suffix").pipe(l.withDescription(D.builderSuffix),l.withSchema(R.fields.builderSuffix),l.optional),Dt=l.text("default-string").pipe(l.withDescription(D.defaultString),l.withSchema(R.fields.defaultString),l.optional),Tt=l.text("default-number").pipe(l.withDescription(D.defaultNumber),l.withSchema(R.fields.defaultNumber),l.optional),xt=l.text("default-boolean").pipe(l.withDescription(D.defaultBoolean),l.withSchema(R.fields.defaultBoolean),l.optional),Ee={jsdocTag:St,outputDir:yt,include:Et,fileSuffix:bt,builderSuffix:Ct,defaultString:Dt,defaultNumber:Tt,defaultBoolean:xt};import*as Pe from"@effect/platform/FileSystem";import*as Ue from"@effect/platform/Path";import*as ve from"effect/Effect";import*as L from"effect/Option";import*as Ae from"effect/Schema";var Ie=(t)=>ve.gen(function*(){let i=yield*$,c=yield*Pe.FileSystem,e=yield*i.cwd,s=(yield*Ue.Path).join(e,ge),o=ee({string:t.defaultString.pipe(L.getOrUndefined),number:t.defaultNumber.pipe(L.getOrUndefined),boolean:t.defaultBoolean.pipe(L.getOrUndefined)}),m=yield*Ae.decode(he)({$schema:"https://raw.githubusercontent.com/nemmtor/ts-databuilders/refs/heads/main/schema.json",builderSuffix:t.builderSuffix.pipe(L.getOrElse(()=>M.builderSuffix)),fileSuffix:t.fileSuffix.pipe(L.getOrElse(()=>M.fileSuffix)),include:t.include.pipe(L.getOrElse(()=>M.include)),jsdocTag:t.jsdocTag.pipe(L.getOrElse(()=>M.jsdocTag)),outputDir:t.outputDir.pipe(L.getOrElse(()=>M.outputDir)),defaults:{...M.defaults,...o}});yield*c.writeFileString(s,JSON.stringify(m,null,2))});import*as _ from"effect/Chunk";import*as F from"effect/Effect";import*as _e from"effect/Function";import*as V from"effect/Option";import*as ce from"effect/Stream";import*as Le from"effect/Data";import*as G from"effect/Effect";import*as je from"effect/Stream";import{glob as Ft}from"glob";class te extends G.Service()("@TSDataBuilders/TreeWalker",{effect:G.gen(function*(){let t=yield*$;return{walk:G.fnUntraced(function*(i){let c=yield*t.cwd;return yield*G.logDebug(`[TreeWalker]: Walking path: ${c}/${i}`),je.fromAsyncIterable(Ft.stream(i,{cwd:c,nodir:!0}),(e)=>new Me({cause:e}))})}})}){}class Me extends Le.TaggedError("TreeWalkerError"){}import*as Re from"@effect/platform/FileSystem";import*as A from"effect/Effect";import*as P from"effect/Stream";class se extends A.Service()("@TSDataBuilders/FileContentChecker",{effect:A.gen(function*(){let t=yield*Re.FileSystem,i=new TextDecoder;return{check:A.fnUntraced(function*(c){let{content:e,filePath:g}=c;return yield*A.logDebug(`[FileContentChecker](${g}): Checking file content`),yield*P.orDie(t.stream(g,{chunkSize:16384})).pipe(P.map((m)=>i.decode(m,{stream:!0})),P.mapAccum("",(m,d)=>{let u=m+d;return[u.slice(-e.length+1),u.includes(e)]}),P.find(Boolean),P.tap((m)=>m?A.logDebug(`[FileContentChecker](${g}): found expected content`):A.void),P.runCollect)})}})}){}class H extends F.Service()("@TSDataBuilders/Finder",{effect:F.gen(function*(){let t=yield*se,i=yield*te,{include:c,jsdocTag:e}=yield*N,g=`@${e}`;return{find:F.fnUntraced(function*(){yield*F.logDebug("[Finder]: Attempting to find files with builders");let o=yield*(yield*i.walk(c)).pipe(ce.mapEffect((m)=>t.check({filePath:m,content:g}).pipe(F.map(_.map((d)=>d?V.some(m):V.none()))),{concurrency:"unbounded"}),ce.runCollect,F.map(_.flatMap(_e.identity)),F.map(_.filter((m)=>V.isSome(m))),F.map(_.map((m)=>m.value)));return yield*F.logDebug(`[Finder]: Found builders in files: ${o.pipe(_.toArray).join(", ")}`),o},F.catchTag("TreeWalkerError",(s)=>F.die(s)))}}),dependencies:[te.Default,se.Default]}){}import*as Je from"@effect/platform/FileSystem";import*as q from"effect/Data";import*as p from"effect/Effect";import*as re from"effect/Either";import{Project as Ot,SyntaxKind as ue}from"ts-morph";import*as W from"effect/Data";import*as f from"effect/Effect";import*as y from"effect/Match";import*as Ge from"effect/Option";import{Node as Nt,SyntaxKind as E}from"ts-morph";import{randomUUID as kt}from"node:crypto";import*as fe from"effect/Effect";class le extends fe.Service()("@TSDataBuilders/RandomUUID",{succeed:{generate:fe.sync(()=>kt())}}){}class de extends f.Service()("@TSDataBuilders/TypeNodeParser",{effect:f.gen(function*(){let{jsdocTag:t}=yield*N,i=yield*le,c=(g)=>f.gen(function*(){let{type:s,contextNode:o,optional:m}=g,d=s.getProperties();if(!s.isObject()||d.length===0)return yield*new Ke({raw:s.getText(),kind:o.getKind()});let u={};for(let a of d){let n=a.getName(),S=a.getTypeAtLocation(o),h=a.isOptional(),b=o.getProject().createSourceFile(`__temp_${yield*i.generate}.ts`,`type __T = ${S.getText()}`,{overwrite:!0}),O=b.getTypeAliasOrThrow("__T").getTypeNodeOrThrow(),I=yield*f.suspend(()=>e({typeNode:O,optional:h}));u[n]=I,o.getProject().removeSourceFile(b)}return{kind:"TYPE_LITERAL",metadata:u,optional:m}}),e=(g)=>f.gen(function*(){let{typeNode:s,optional:o}=g,m=s.getKind(),d=y.value(m).pipe(y.when(y.is(E.StringKeyword),()=>f.succeed({kind:"STRING",optional:o})),y.when(y.is(E.NumberKeyword),()=>f.succeed({kind:"NUMBER",optional:o})),y.when(y.is(E.BooleanKeyword),()=>f.succeed({kind:"BOOLEAN",optional:o})),y.when(y.is(E.UndefinedKeyword),()=>f.succeed({kind:"UNDEFINED",optional:o})),y.when(y.is(E.ArrayType),()=>f.succeed({kind:"ARRAY",optional:o})),y.when(y.is(E.LiteralType),()=>f.succeed({kind:"LITERAL",literalValue:s.asKindOrThrow(E.LiteralType).getLiteral().getText(),optional:o})),y.when(y.is(E.TypeLiteral),()=>f.gen(function*(){let n=s.asKindOrThrow(E.TypeLiteral).getMembers();return{kind:"TYPE_LITERAL",metadata:yield*f.reduce(n,{},(h,b)=>f.gen(function*(){if(!b.isKind(E.PropertySignature))return h;let O=b.getTypeNode();if(!O)return h;let I=b.getNameNode().getText(),Y=b.hasQuestionToken(),K=yield*f.suspend(()=>e({typeNode:O,optional:Y}));return{...h,[I]:K}})),optional:o}})),y.when(y.is(E.ImportType),()=>f.gen(function*(){let a=s.asKindOrThrow(E.ImportType),n=a.getType(),S=n.getSymbol();if(!S)return yield*f.die(new We);let h=S.getDeclarations();if(h&&h.length>1)return yield*f.die(new be);let[b]=h;if(!b)return yield*f.die(new Ce);return yield*c({type:n,contextNode:a,optional:o})})),y.when(y.is(E.TupleType),()=>f.gen(function*(){let n=s.asKindOrThrow(E.TupleType).getElements(),S=yield*f.all(n.map((h)=>f.suspend(()=>e({typeNode:h,optional:!1}))),{concurrency:"unbounded"});return{kind:"TUPLE",optional:o,members:S}})),y.when(y.is(E.TypeReference),()=>f.gen(function*(){let a=s.asKindOrThrow(E.TypeReference),n=a.getTypeName().getText();if(n==="Date")return{kind:"DATE",optional:o};if(n==="Array")return{kind:"ARRAY",optional:o};let S=a.getTypeArguments();if(n==="Record"){let[J,v]=S;if(!J||!v)return yield*new ie({kind:m,raw:s.getText()});let j=yield*f.suspend(()=>e({typeNode:J,optional:!1})),me=yield*f.suspend(()=>e({typeNode:v,optional:!1}));return{kind:"RECORD",keyType:j,valueType:me,optional:o}}if(["Pick","Omit","Partial","Required","Readonly","Extract","NonNullable"].includes(n))return yield*c({type:a.getType(),contextNode:a,optional:o});let b=a.getType().getAliasSymbol();if(!b)return yield*c({type:a.getType(),contextNode:a,optional:o});let O=b.getDeclarations();if(O&&O.length>1)return yield*f.die(new be);let[I]=O;if(!I)return yield*f.die(new Ce);let Y=b?.getJsDocTags().map((J)=>J.getName()).includes(t);if(!Nt.isTypeAliasDeclaration(I))return yield*f.die(new Ye);let K=I.getTypeNode();if(!K)return yield*new ie({kind:m,raw:s.getText()});if(!Y)return yield*f.suspend(()=>e({typeNode:K,optional:o}));return{kind:"BUILDER",name:I.getName(),optional:o}})),y.when(y.is(E.UnionType),()=>f.gen(function*(){let a=yield*f.all(s.asKindOrThrow(E.UnionType).getTypeNodes().map((n)=>f.suspend(()=>e({typeNode:n,optional:!1}))),{concurrency:"unbounded"});return{kind:"UNION",optional:o,members:a}})),y.when(y.is(E.IntersectionType),()=>f.gen(function*(){let n=s.asKindOrThrow(E.IntersectionType).getTypeNodes(),S=[E.StringKeyword,E.NumberKeyword,E.BooleanKeyword],h=n.find((b)=>S.includes(b.getKind()));if(h&&n.length>1)return{kind:"TYPE_CAST",baseTypeMetadata:yield*f.suspend(()=>e({typeNode:h,optional:!1})),optional:o};return yield*new ie({kind:m,raw:s.getText()})})),y.option);if(Ge.isNone(d))return yield*new ie({kind:m,raw:s.getText()});return yield*d.value});return{generateMetadata:e}}),dependencies:[le.Default]}){}class ie extends W.TaggedError("UnsupportedSyntaxKindError"){}class be extends W.TaggedError("MultipleSymbolDeclarationsError"){}class Ce extends W.TaggedError("MissingSymbolDeclarationError"){}class We extends W.TaggedError("MissingSymbolError"){}class Ye extends W.TaggedError("UnsupportedTypeAliasDeclaration"){}class Ke extends W.TaggedError("CannotBuildTypeReferenceMetadata"){}class Q extends p.Service()("@TSDataBuilders/Parser",{effect:p.gen(function*(){let t=yield*Je.FileSystem,i=yield*de,{jsdocTag:c}=yield*N;return{generateBuildersMetadata:p.fnUntraced(function*(e){yield*p.logDebug(`[Parser](${e}): Generating builder metadata`),yield*p.logDebug(`[Parser](${e}): Reading source code`);let g=yield*p.orDie(t.readFileString(e)),s=yield*p.try({try:()=>{return new Ot().createSourceFile(e,g,{overwrite:!0}).getTypeAliases().filter((n)=>n.getJsDocs().flatMap((S)=>S.getTags().flatMap((h)=>h.getTagName())).includes(c)).map((n)=>{let S=n.getName();if(!n.isExported())return re.left(new ze({path:e,typeName:S}));let h=n.getTypeNode();if(!(h?.isKind(ue.TypeLiteral)||h?.isKind(ue.TypeReference)))return re.left(new Ve({path:e,typeName:n.getName()}));return re.right({name:n.getName(),node:h})}).filter(Boolean)},catch:(d)=>new Ze({cause:d})}),o=yield*p.all(s.map((d)=>d),{concurrency:"unbounded"});return yield*p.logDebug(`[Parser](${e}): Generating metadata for types: ${o.map(({name:d})=>d).join(", ")}`),yield*p.all(o.map(({name:d,node:u})=>i.generateMetadata({typeNode:u,optional:!1}).pipe(p.tap(()=>p.logDebug(`[Parser](${e}): Finished generating metadata for type: ${d}`)),p.map((a)=>({name:d,shape:a,path:e})),p.catchTags({UnsupportedSyntaxKindError:(a)=>p.fail(new He({kind:a.kind,raw:a.raw,path:e,typeName:d})),CannotBuildTypeReferenceMetadata:(a)=>p.fail(new qe({kind:a.kind,raw:a.raw,path:e,typeName:d}))}))),{concurrency:"unbounded"})},p.catchTags({ParserError:(e)=>p.die(e),UnexportedDatabuilderError:(e)=>p.dieMessage(`[Parser](${e.path}): Unexported databuilder ${e.typeName}`),RichUnsupportedSyntaxKindError:(e)=>p.dieMessage(`[Parser](${e.path}): Unsupported syntax kind of id: ${e.kind} with raw type: ${e.raw} found in type ${e.typeName}`),RichCannotBuildTypeReferenceMetadata:(e)=>p.dieMessage(`[Parser](${e.path}): Cannot build type reference metadata with kind of id: ${e.kind} with raw type: ${e.raw} found in type ${e.typeName}. Is it a root of databuilder?`),UnsupportedBuilderTypeError:(e)=>p.dieMessage(`[Parser](${e.path}): Unsupported builder type ${e.typeName}`)}))}}),dependencies:[de.Default]}){}class Ze extends q.TaggedError("ParserError"){}class ze extends q.TaggedError("UnexportedDatabuilderError"){}class Ve extends q.TaggedError("UnsupportedBuilderTypeError"){}class He extends q.TaggedError("RichUnsupportedSyntaxKindError"){}class qe extends q.TaggedError("RichCannotBuildTypeReferenceMetadata"){}import*as Qe from"effect/Chunk";import*as w from"effect/Effect";import*as Xe from"effect/Function";var et=w.gen(function*(){let t=yield*H,i=yield*Q,c=yield*z;yield*w.logInfo("[TSDatabuilders]: Generating builders for your project.");let e=yield*t.find();yield*w.logInfo(`[TSDatabuilders]: Found builders in ${e.length} file(s).`),yield*w.logDebug("[TSDatabuilders]: Attempting to generate builders metadata");let g=yield*w.all(Qe.map(e,(s)=>i.generateBuildersMetadata(s)),{concurrency:"unbounded"}).pipe(w.map((s)=>s.flatMap(Xe.identity)));if(g.length===0)return;yield*w.logDebug("[TSDatabuilders]: Attempting to create builders files"),yield*c.create(g),yield*w.logInfo(`[TSDatabuilders]: Created ${g.length} builder(s).`)});var wt=U.make("init",Ee).pipe(U.withHandler(Ie)),Pt=U.make("ts-databuilders",Ee),tt=Pt.pipe(U.withHandler(()=>et),U.withSubcommands([wt]),U.provide((t)=>X.mergeAll(H.Default,Q.Default,z.Default).pipe(X.provide(X.effect(N,Fe(t))))),U.run({name:"Typescript Databuilders generator",version:"v0.0.1"}));var vt=ot.mergeAll(nt.minimumLogLevel(at.Debug),$.Default,it.layer);tt(process.argv).pipe(Ut.provide(vt),rt.runMain);
23
+ `;function Ot(e){let i=[];function s(l){switch(l.kind){case"BUILDER":i.push(l.name);break;case"TYPE_LITERAL":Object.values(l.metadata).forEach(s);break;case"UNION":case"TUPLE":l.members.forEach(s);break;case"RECORD":s(l.keyType),s(l.valueType);break}}return Object.values(e).forEach(s),i}var bt=e=>g.gen(function*(){let{fieldName:i,optional:s,typeName:l,isNestedBuilder:n}=e,o=i.replaceAll("'","").replaceAll('"',""),p=yield*Ue.pipe(oe.decode)(o),c=`with${yield*$e.pipe(oe.decode)(o)}`,d=[`return this.with({ ${i}: ${p} });`],m=[`return this.with({ ${i}: ${p}.build() });`],T=n?m:d,y=[`if (!${p}) {`,` const { "${o}": _unused, ...rest } = this.build();`," return this.with(rest);","}"],a=s?[...y,...T]:T,f=`${l}['${o}']`;return{name:c,isPublic:!0,parameters:[{name:p,type:n?`DataBuilder<${f}>`:f}],statements:a}}),Z=class extends g.Service()("@TSDataBuilders/BuildersGenerator",{effect:g.gen(function*(){let i=yield*De.FileSystem,s=yield*pe,l=yield*F,n=yield*Oe.Path,o=yield*M;return{create:g.fnUntraced(function*(p){let c=n.join(yield*l.cwd,o.outputDir);(yield*g.orDie(i.exists(c)))&&(yield*g.logDebug(`[Builders]: Removing already existing output directory at ${c}`),yield*g.orDie(i.remove(c,{recursive:!0}))),yield*g.logDebug(`[Builders]: Creating output directory at ${c}`),yield*g.orDie(i.makeDirectory(c,{recursive:!0})),yield*s.generateBaseBuilder();let m=p.map(a=>a.name),T=m.filter((a,f)=>m.indexOf(a)!==f),y=[...new Set(T)];if(T.length>0)return yield*g.dieMessage(`Duplicated builders: ${y.join(", ")}`);yield*g.all(p.map(a=>s.generateBuilder(a)),{concurrency:"unbounded"})})}}),dependencies:[pe.Default]}){};import*as Ke from"@effect/platform/FileSystem";import*as Ye from"@effect/platform/Path";import*as We from"effect/Effect";import*as j from"effect/Option";import*as Je from"effect/Schema";var Ve=e=>We.gen(function*(){let i=yield*F,s=yield*Ke.FileSystem,l=yield*i.cwd,o=(yield*Ye.Path).join(l,Ee),p=ie({string:e.defaultString.pipe(j.getOrUndefined),number:e.defaultNumber.pipe(j.getOrUndefined),boolean:e.defaultBoolean.pipe(j.getOrUndefined)}),c=yield*Je.decode(Ce)({$schema:"https://raw.githubusercontent.com/nemmtor/ts-databuilders/refs/heads/main/schema.json",builderSuffix:e.builderSuffix.pipe(j.getOrElse(()=>_.builderSuffix)),fileSuffix:e.fileSuffix.pipe(j.getOrElse(()=>_.fileSuffix)),include:e.include.pipe(j.getOrElse(()=>_.include)),jsdocTag:e.jsdocTag.pipe(j.getOrElse(()=>_.jsdocTag)),outputDir:e.outputDir.pipe(j.getOrElse(()=>_.outputDir)),defaults:{..._.defaults,...p}});yield*s.writeFileString(o,`${JSON.stringify(c,null,2)}
24
+ `)});import*as q from"effect/Chunk";import*as w from"effect/Effect";import*as me from"effect/Option";import*as ge from"effect/Stream";import*as Ze from"@effect/platform/FileSystem";import*as ze from"effect/Chunk";import*as U from"effect/Effect";import*as B from"effect/Stream";var re=class extends U.Service()("@TSDataBuilders/FileContentChecker",{effect:U.gen(function*(){let i=yield*Ze.FileSystem,s=new TextDecoder;return{check:U.fnUntraced(function*(l){let{content:n,filePath:o}=l;return yield*U.logDebug(`[FileContentChecker](${o}): Checking file content`),yield*B.orDie(i.stream(o,{chunkSize:16*1024})).pipe(B.map(d=>s.decode(d,{stream:!0})),B.mapAccum("",(d,m)=>{let T=d+m;return[T.slice(-n.length+1),T.includes(n)]}),B.find(d=>!!d),B.tap(()=>U.logDebug(`[FileContentChecker](${o}): found expected content`)),B.runCollect,U.map(d=>d.pipe(ze.get(0))))})}})}){};import*as qe from"effect/Data";import*as V from"effect/Effect";import*as He from"effect/Stream";import{glob as wt}from"glob";import*as ue from"effect/Effect";var ae=class extends ue.Service()("@TSDataBuilders/Glob",{succeed:{iterate:i=>ue.sync(()=>wt.iterate(i.path,{cwd:i.cwd,nodir:!0}))}}){};var se=class extends V.Service()("@TSDataBuilders/TreeWalker",{effect:V.gen(function*(){let i=yield*ae,s=yield*F;return{walk:V.fnUntraced(function*(l){let n=yield*s.cwd;return yield*V.logDebug(`[TreeWalker]: Walking path: ${n}/${l}`),He.fromAsyncIterable(yield*i.iterate({path:l,cwd:n}),o=>new be({cause:o}))})}}),dependencies:[ae.Default]}){},be=class extends qe.TaggedError("TreeWalkerError"){};var z=class extends w.Service()("@TSDataBuilders/Finder",{effect:w.gen(function*(){let i=yield*re,s=yield*se,{include:l,jsdocTag:n}=yield*M,o=`@${n}`;return{find:w.gen(function*(){yield*w.logDebug("[Finder]: Attempting to find files with builders");let c=yield*(yield*s.walk(l)).pipe(ge.mapEffect(d=>i.check({filePath:d,content:o}).pipe(w.map(m=>m.pipe(me.map(()=>d)))),{concurrency:"unbounded"}),ge.runCollect,w.map(q.filter(d=>me.isSome(d))),w.map(q.map(d=>d.value)));return yield*w.logDebug(`[Finder]: Found builders in files: ${c.pipe(q.toArray).join(", ")}`),c}).pipe(w.catchTag("TreeWalkerError",p=>w.die(p)))}}),dependencies:[se.Default,re.Default]}){};import{Node as kt,Project as Ft,SyntaxKind as E}from"ts-morph";import*as Xe from"@effect/platform/FileSystem";import*as G from"effect/Data";import*as t from"effect/Effect";import*as ce from"effect/Either";import*as S from"effect/Match";import*as R from"effect/Option";var ye=class extends t.Service()("@TSDataBuilders/TypeNodeParser",{effect:t.gen(function*(){let{jsdocTag:i}=yield*M,s=yield*J,l=o=>t.gen(function*(){let{type:p,contextNode:c,optional:d,inlineDefault:m}=o,T=p.getProperties();if(!p.isObject()||T.length===0)return yield*new we({raw:p.getText(),kind:c.getKind()});let y={};for(let a of T){let f=a.getName(),h=a.getTypeAtLocation(c),C=a.isOptional(),N=c.getProject().createSourceFile(`__temp_${yield*s.generateUuid}.ts`,`type __T = ${h.getText()}`,{overwrite:!0}),I=N.getTypeAliasOrThrow("__T").getTypeNodeOrThrow(),v=yield*t.suspend(()=>n({typeNode:I,optional:C,inlineDefault:R.none()}));y[f]=v,c.getProject().removeSourceFile(N)}return{kind:"TYPE_LITERAL",metadata:y,inlineDefault:m,optional:d}}),n=o=>t.gen(function*(){let{typeNode:p,optional:c,inlineDefault:d}=o,m=p.getKind(),T=S.value(m).pipe(S.when(S.is(E.StringKeyword),()=>t.succeed({kind:"STRING",inlineDefault:d,optional:c})),S.when(S.is(E.NumberKeyword),()=>t.succeed({kind:"NUMBER",inlineDefault:d,optional:c})),S.when(S.is(E.BooleanKeyword),()=>t.succeed({kind:"BOOLEAN",inlineDefault:d,optional:c})),S.when(S.is(E.UndefinedKeyword),()=>t.succeed({kind:"UNDEFINED",inlineDefault:d,optional:c})),S.when(S.is(E.ArrayType),()=>t.succeed({kind:"ARRAY",inlineDefault:d,optional:c})),S.when(S.is(E.LiteralType),()=>{let a=p.asKindOrThrow(E.LiteralType).getLiteral().getText();return a==="null"?t.succeed({kind:"NULL",inlineDefault:d,optional:c}):t.succeed({kind:"LITERAL",inlineDefault:d,literalValue:a,optional:c})}),S.when(S.is(E.TypeLiteral),()=>t.gen(function*(){let f=p.asKindOrThrow(E.TypeLiteral).getMembers(),h=yield*t.reduce(f,{},(C,N)=>t.gen(function*(){if(!N.isKind(E.PropertySignature))return C;let I=N.getTypeNode();if(!I)return C;let v=N.getNameNode().getText(),Y=N.hasQuestionToken(),W=yield*Mt(N),ee=yield*t.suspend(()=>n({typeNode:I,optional:Y,inlineDefault:W}));return{...C,[v]:ee}}));return{kind:"TYPE_LITERAL",inlineDefault:d,metadata:h,optional:c}})),S.when(S.is(E.ImportType),()=>t.gen(function*(){let a=p.asKindOrThrow(E.ImportType),f=a.getType(),h=f.getSymbol(),C=f.getText();if(!h)return yield*new Fe({raw:C});let N=h.getDeclarations();if(N&&N.length>1)return yield*new Te({raw:C});let[I]=N;return I?yield*l({type:f,contextNode:a,inlineDefault:d,optional:c}):yield*new Se({raw:C})})),S.when(S.is(E.TupleType),()=>t.gen(function*(){let f=p.asKindOrThrow(E.TupleType).getElements(),h=yield*t.all(f.map(C=>t.suspend(()=>n({typeNode:C,optional:!1,inlineDefault:R.none()}))),{concurrency:"unbounded"});return{kind:"TUPLE",inlineDefault:d,optional:c,members:h}})),S.when(S.is(E.TypeReference),()=>t.gen(function*(){let a=p.asKindOrThrow(E.TypeReference),f=a.getTypeName().getText();if(f==="Date")return{kind:"DATE",optional:c,inlineDefault:d};if(f==="Array")return{kind:"ARRAY",optional:c,inlineDefault:d};let h=a.getTypeArguments();if(f==="Record"){let[k,de]=h;if(!k||!de)return yield*new H({kind:m,raw:p.getText()});let pt=yield*t.suspend(()=>n({typeNode:k,optional:!1,inlineDefault:R.none()})),ut=yield*t.suspend(()=>n({typeNode:de,optional:!1,inlineDefault:R.none()}));return{kind:"RECORD",keyType:pt,valueType:ut,optional:c,inlineDefault:d}}if(["Pick","Omit","Partial","Required","Readonly","Extract","NonNullable"].includes(f))return yield*l({type:a.getType(),contextNode:a,optional:c,inlineDefault:d});let N=a.getType(),I=N.getText(),v=N.getAliasSymbol();if(!v)return yield*l({type:N,contextNode:a,optional:c,inlineDefault:d});let Y=v.getDeclarations();if(Y&&Y.length>1)return yield*new Te({raw:I});let[W]=Y;if(!W)return yield*new Se({raw:I});let ee=v?.getJsDocTags().map(k=>k.getName()).includes(i);if(!kt.isTypeAliasDeclaration(W))return yield*new xe;let $=W.getTypeNode();return $?ee?{kind:"BUILDER",name:W.getName(),inlineDefault:d,optional:c}:yield*t.suspend(()=>n({typeNode:$,optional:c,inlineDefault:d})):yield*new H({kind:m,raw:I})})),S.when(S.is(E.UnionType),()=>t.gen(function*(){let a=yield*t.all(p.asKindOrThrow(E.UnionType).getTypeNodes().map(f=>t.suspend(()=>n({typeNode:f,optional:!1,inlineDefault:R.none()}))),{concurrency:"unbounded"});return{kind:"UNION",optional:c,members:a,inlineDefault:d}})),S.when(S.is(E.IntersectionType),()=>t.gen(function*(){let f=p.asKindOrThrow(E.IntersectionType).getTypeNodes(),h=[E.StringKeyword,E.NumberKeyword,E.BooleanKeyword],C=f.find(N=>h.includes(N.getKind()));return C&&f.length>1?{kind:"TYPE_CAST",baseTypeMetadata:yield*t.suspend(()=>n({typeNode:C,optional:!1,inlineDefault:d})),inlineDefault:d,optional:c}:yield*new H({kind:m,raw:p.getText()})})),S.option);return R.isNone(T)?yield*new H({kind:m,raw:p.getText()}):yield*T.value});return{generateMetadata:n}}),dependencies:[J.Default]}){},Q=class extends t.Service()("@TSDataBuilders/Parser",{effect:t.gen(function*(){let i=yield*Xe.FileSystem,s=yield*ye,{jsdocTag:l}=yield*M;return{generateBuildersMetadata:n=>t.gen(function*(){yield*t.logDebug(`[Parser](${n}): Generating builder metadata`),yield*t.logDebug(`[Parser](${n}): Reading source code`);let o=yield*t.orDie(i.readFileString(n)),p=yield*t.try({try:()=>new Ft().createSourceFile(n,o,{overwrite:!0}).getTypeAliases().filter(a=>a.getJsDocs().flatMap(f=>f.getTags().flatMap(h=>h.getTagName())).includes(l)).map(a=>{let f=a.getName();if(!a.isExported())return ce.left(new Pe({typeName:f}));let h=a.getTypeNode();return h?.isKind(E.TypeLiteral)||h?.isKind(E.TypeReference)?ce.right({name:a.getName(),node:h}):ce.left(new ke({typeName:a.getName()}))}).filter(Boolean),catch:m=>new Ie({cause:m})}),c=yield*t.all(p.map(m=>m),{concurrency:"unbounded"});return yield*t.logDebug(`[Parser](${n}): Generating metadata for types: ${c.map(({name:m})=>m).join(", ")}`),yield*t.all(c.map(({name:m,node:T})=>s.generateMetadata({typeNode:T,optional:!1,inlineDefault:R.none()}).pipe(t.tap(()=>t.logDebug(`[Parser](${n}): Finished generating metadata for type: ${m}`)),t.map(y=>({name:m,shape:y,path:n})))),{concurrency:"unbounded"})}).pipe(t.catchTags({ParserError:o=>t.die(o),MissingSymbolDeclarationError:o=>t.dieMessage(`[Parser](${n}): Missing symbol declaration for type: ${o.raw}`),UnsupportedTypeAliasDeclarationError:()=>t.dieMessage(`[Parser](${n}): Unsupported type alias declaration`),MultipleSymbolDeclarationsError:o=>t.dieMessage(`[Parser](${n}): Missing symbol declaration error for type: ${o.raw}`),MissingSymbolError:o=>t.dieMessage(`[Parser](${n}): Missing symbol error for type: ${o.raw}`),UnexportedDatabuilderError:o=>t.dieMessage(`[Parser](${n}): Unexported databuilder ${o.typeName}`),UnsupportedSyntaxKindError:o=>t.dieMessage(`[Parser](${n}): Unsupported syntax kind of id: ${o.kind} for type: ${o.raw}`),CannotBuildTypeReferenceMetadataError:o=>t.dieMessage(`[Parser](${n}): Cannot build type reference metadata with kind of id: ${o.kind} for type: ${o.raw}. Is it a root of databuilder?`),UnsupportedBuilderTypeError:o=>t.dieMessage(`[Parser](${n}): Unsupported builder type ${o.typeName}`)}))}}),dependencies:[ye.Default]}){},H=class extends G.TaggedError("UnsupportedSyntaxKindError"){},xe=class extends G.TaggedError("UnsupportedTypeAliasDeclarationError"){},we=class extends G.TaggedError("CannotBuildTypeReferenceMetadataError"){},Ie=class extends G.TaggedError("ParserError"){},Pe=class extends G.TaggedError("UnexportedDatabuilderError"){},ke=class extends G.TaggedError("UnsupportedBuilderTypeError"){},Fe=class extends G.TaggedError("MissingSymbolError"){},Se=class extends G.TaggedError("MissingSymbolDeclarationError"){},Te=class extends G.TaggedError("MultipleSymbolDeclarationsError"){},Mt=t.fnUntraced(function*(e){let i=e.getJsDocs();for(let s of i){let n=s.getTags().find(o=>o.getTagName()==="DataBuilderDefault");if(n){let o=n.getComment();if(typeof o=="string")return R.some(o.trim())}}return R.none()});import*as tt from"effect/Chunk";import*as A from"effect/Effect";import*as it from"effect/Function";var nt=A.gen(function*(){let e=yield*z,i=yield*Q,s=yield*Z;yield*A.logInfo("[TSDatabuilders]: Generating builders for your project.");let l=yield*e.find;yield*A.logInfo(`[TSDatabuilders]: Found builders in ${l.length} file(s).`),yield*A.logDebug("[TSDatabuilders]: Attempting to generate builders metadata");let n=yield*A.all(tt.map(l,o=>i.generateBuildersMetadata(o)),{concurrency:"unbounded"}).pipe(A.map(o=>o.flatMap(it.identity)));n.length!==0&&(yield*A.logDebug("[TSDatabuilders]: Attempting to create builders files"),yield*s.create(n),yield*A.logInfo(`[TSDatabuilders]: Created ${n.length} builder(s).`))});var At=u.text("jsdoc-tag").pipe(u.withDescription(O.jsdocTag),u.withSchema(K.fields.jsdocTag),u.optional),Bt=u.text("output-dir").pipe(u.withAlias("o"),u.withDescription(O.outputDir),u.withSchema(K.fields.outputDir),u.optional),Rt=u.text("include").pipe(u.withAlias("i"),u.withDescription(O.include),u.withSchema(K.fields.include),u.optional),Lt=u.text("file-suffix").pipe(u.withDescription(O.fileSuffix),u.withSchema(K.fields.fileSuffix),u.optional),$t=u.text("builder-suffix").pipe(u.withDescription(O.builderSuffix),u.withSchema(K.fields.builderSuffix),u.optional),Ut=u.text("default-string").pipe(u.withDescription(O.defaultString),u.withSchema(K.fields.defaultString),u.optional),vt=u.text("default-number").pipe(u.withDescription(O.defaultNumber),u.withSchema(K.fields.defaultNumber),u.optional),jt=u.text("default-boolean").pipe(u.withDescription(O.defaultBoolean),u.withSchema(K.fields.defaultBoolean),u.optional),ot={jsdocTag:At,outputDir:Bt,include:Rt,fileSuffix:Lt,builderSuffix:$t,defaultString:Ut,defaultNumber:vt,defaultBoolean:jt},Gt=L.make("init",ot).pipe(L.withHandler(Ve)),_t=L.make("ts-databuilders",ot),rt=_t.pipe(L.withHandler(()=>nt),L.withSubcommands([Gt]),L.provide(e=>X.mergeAll(z.Default,Q.Default,Z.Default).pipe(X.provide(X.effect(M,Le(e))))),L.run({name:"Typescript Databuilders generator",version:"v0.0.1"}));var Kt=dt.mergeAll(lt.minimumLogLevel(ft.Info),F.Default,at.layer);rt(process.argv).pipe(ct.provide(Kt),st.runMain);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
3
  "name": "@nemmtor/ts-databuilders",
4
- "version": "0.0.1-alpha.8",
4
+ "version": "0.0.1-alpha.9",
5
5
  "type": "module",
6
6
  "private": false,
7
7
  "description": "CLI tool that automatically generates builder classes from annotated TypeScript types.",
@@ -27,8 +27,11 @@
27
27
  "dist"
28
28
  ],
29
29
  "scripts": {
30
- "build:node": "bun build src/main.ts --target node --outfile dist/main.js --production --packages external",
31
- "start": "bun src/main.ts",
30
+ "build": "tsup",
31
+ "test": "vitest run",
32
+ "test:watch": "vitest",
33
+ "test:coverage": "vitest run --coverage",
34
+ "start": "tsx src/main.ts",
32
35
  "format": "biome format",
33
36
  "format:fix": "biome format --write",
34
37
  "lint": "biome lint",
@@ -38,25 +41,35 @@
38
41
  "check-types": "tsc -b tsconfig.json",
39
42
  "check-knip": "knip",
40
43
  "check-dep": "depcruise --output-type err-long src",
41
- "gen:schema": "bun scripts/generate-schema.ts"
44
+ "gen:schema": "tsx scripts/generate-schema.ts"
42
45
  },
43
46
  "devDependencies": {
44
- "@biomejs/biome": "^2.2.7",
45
- "@effect/language-service": "^0.47.3",
47
+ "@biomejs/biome": "^2.3.2",
48
+ "@effect/language-service": "^0.55.0",
49
+ "@effect/vitest": "^0.27.0",
46
50
  "@total-typescript/ts-reset": "^0.6.1",
47
- "@types/bun": "^1.3.0",
48
- "dependency-cruiser": "^17.1.0",
49
- "knip": "^5.66.2"
51
+ "@types/node": "^24.0.0",
52
+ "@vitest/coverage-v8": "4.0.7",
53
+ "dependency-cruiser": "^17.2.0",
54
+ "knip": "^5.66.4",
55
+ "lefthook": "^2.0.2",
56
+ "tsup": "^8.5.0",
57
+ "tsx": "^4.20.6",
58
+ "vitest": "^4.0.6"
50
59
  },
51
60
  "peerDependencies": {
52
61
  "typescript": "^5.9.3"
53
62
  },
54
63
  "dependencies": {
55
- "@effect/cli": "^0.71.0",
56
- "@effect/platform": "^0.92.1",
57
- "@effect/platform-node": "^0.98.4",
64
+ "@effect/cli": "^0.72.0",
65
+ "@effect/platform": "^0.93.0",
66
+ "@effect/platform-node": "^0.100.0",
58
67
  "effect": "^3.18.4",
59
68
  "glob": "^11.0.3",
60
69
  "ts-morph": "^27.0.2"
70
+ },
71
+ "packageManager": "pnpm@10.20.0+sha512.cf9998222162dd85864d0a8102e7892e7ba4ceadebbf5a31f9c2fce48dfce317a9c53b9f6464d1ef9042cba2e02ae02a9f7c143a2b438cd93c91840f0192b9dd",
72
+ "engines": {
73
+ "node": ">=20.0.0"
61
74
  }
62
75
  }