@nemmtor/ts-databuilders 0.0.1-alpha.10 → 0.0.1-alpha.11

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 +42 -13
  2. package/dist/main.js +24 -0
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -15,33 +15,57 @@ yarn add -D @nemmtor/ts-databuilders
15
15
  ```
16
16
 
17
17
  ## Configuration
18
- All configuration is optional with sensible defaults.
18
+ Configuration is optional - it fallbacks to sensible defaults.
19
19
 
20
- ### Initialize Config File (optional)
21
- Generate a default `ts-databuilders.json` configuration file:
20
+ ### Configure via CLI flags (optional):
22
21
  ```bash
23
- pnpm ts-databuilders init
22
+ pnpm ts-databuilders --include "src/**/*.ts{,x}" --output-dir src/__generated__ --jsdoc-tag DataBuilder
24
23
  ```
25
- You can also generate configuration file by providing values step by step in an interactive wizard:
24
+ You can also provide configuration by going through interactive wizard:
26
25
  ```bash
27
- pnpm ts-databuilders init --wizard
26
+ pnpm ts-databuilders --wizard
28
27
  ```
29
28
 
30
- ### Configure via CLI flags (optional):
29
+ ### Configure via config file (optional)
30
+ Ts-databuilders will try to find config file `ts-databuilders.json` in the root of your repository.
31
+ Config file is optional.
32
+
33
+ Example of default config file:
34
+ ```json
35
+ {
36
+ "$schema": "https://raw.githubusercontent.com/nemmtor/ts-databuilders/refs/heads/main/schema.json",
37
+ "jsdocTag": "DataBuilder",
38
+ "inlineDefaultJsdocTag": "DataBuilderDefault",
39
+ "withNestedBuilders": true,
40
+ "outputDir": "generated/builders",
41
+ "include": "src/**/*.ts{,x}",
42
+ "fileSuffix": ".builder",
43
+ "builderSuffix": "Builder",
44
+ "defaults": {
45
+ "string": "",
46
+ "number": 0,
47
+ "boolean": false
48
+ }
49
+ }
50
+ ```
51
+
52
+ You can generate a default configuration file by running `init` command:
31
53
  ```bash
32
- pnpm ts-databuilders --output-dir="src/__generated__" --jsdoc-tag=MyBuilder
54
+ pnpm ts-databuilders init
33
55
  ```
34
- You can also provide configuration by going through interactive wizard:
56
+ You can also generate it by providing values step by step in an interactive wizard:
35
57
  ```bash
36
- pnpm ts-databuilders --wizard
58
+ pnpm ts-databuilders init --wizard
37
59
  ```
38
60
 
61
+
39
62
  ### Options Reference
40
63
 
41
- | Name | Flag | Description | Default |
64
+ | Name (in config file) | Flag (cli flags) | Description | Default |
42
65
  |---------------|-------------------------------------------------------|-----------------------------------------|----------------------|
43
66
  | jsdocTag | `--jsdoc-tag` | JSDoc tag to mark types for generation | `DataBuilder` |
44
- | inlineDefaultJsdocTag | `--inline-default-jsdoc-tag` | JSDoc tag used to set default value of given field. | `DataBuilderDefault` |
67
+ | inlineDefaultJsdocTag | `--inline-default-jsdoc-tag` | JSDoc tag used to set default value of given field | `DataBuilderDefault` |
68
+ | withNestedBuilders | `--with-nested-builders` | When set to true ts-databuilders will use nested builders approach | `true` |
45
69
  | outputDir | `--output-dir -o` | Output directory for generated builders | `generated/builders` |
46
70
  | include | `--include -i` | Glob pattern for source files | `src/**/*.ts{,x}` |
47
71
  | fileSuffix | `--file-suffix` | File suffix for builder files | `.builder` |
@@ -200,6 +224,9 @@ This not only makes the test code less verbose but also highlights what is reall
200
224
  [Read more about data builders.](http://www.natpryce.com/articles/000714.html)
201
225
 
202
226
  ## Nested Builders
227
+ > [!NOTE]
228
+ > Nested builders can be turned off by using withNestedBuilders option. Check configuration section for more details.
229
+
203
230
  When your types contain complex nested objects, you can annotate their type definitions and TS DataBuilders will automatically generate nested builders, allowing you to compose them fluently.
204
231
  ### Example
205
232
 
@@ -291,6 +318,8 @@ const userWithCity = new UserBuilder()
291
318
  ```
292
319
 
293
320
  ## Inline Default Values
321
+ > [!NOTE]
322
+ > It's your responsibility to provide inline default value that satisfies expected type.
294
323
 
295
324
  While global defaults work well for most cases, sometimes you need field-specific default values. This is especially important for specialized string types like ISO dates, UUIDs etc.
296
325
 
@@ -316,7 +345,7 @@ Use `@DataBuilderDefault` JSDoc tag to override defaults per field:
316
345
  type Order = {
317
346
  /** @DataBuilderDefault '550e8400-e29b-41d4-a716-446655440000' */
318
347
  id: string;
319
-
348
+
320
349
  /** @DataBuilderDefault '2025-11-05T15:32:58.727Z' */
321
350
  createdAt: string;
322
351
  }
package/dist/main.js ADDED
@@ -0,0 +1,24 @@
1
+ #!/usr/bin/env node
2
+ import*as dt from"@effect/platform-node/NodeContext";import*as lt from"@effect/platform-node/NodeRuntime";import*as ft from"effect/Effect";import*as ut from"effect/Layer";import*as pt from"effect/Logger";import*as mt from"effect/LogLevel";import*as G from"@effect/cli/Command";import*as s from"@effect/cli/Options";import*as ee from"effect/Layer";import{Project as bt}from"ts-morph";import*as Oe from"@effect/platform/FileSystem";import*as be from"@effect/platform/Path";import*as m from"effect/Effect";import*as w from"effect/Match";import*as Je from"effect/Option";import*as re from"effect/Schema";import*as Ee from"@effect/platform/FileSystem";import*as $e from"@effect/platform/Path";import*as Ue from"effect/Context";import*as O from"effect/Effect";import*as I from"effect/Option";import*as i from"effect/Schema";var C={jsdocTag:"JSDoc tag used to mark types for data building generation.",inlineDefaultJsdocTag:"JSDoc tag used to set default value of given field.",withNestedbuilders:"When set to true ts-databuilders will use nested builders approach.",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 fe from"effect/Effect";var B=class extends fe.Service()("@TSDataBuilders/Process",{succeed:{cwd:fe.sync(()=>process.cwd())}}){};var yt=e=>e!==void 0,ne=e=>Object.fromEntries(Object.entries(e).filter(([n,c])=>yt(c)));var Ne="ts-databuilders.json",Tt=i.Struct({jsdocTag:i.NonEmptyTrimmedString,inlineDefaultJsdocTag:i.NonEmptyTrimmedString,withNestedBuilders:i.Boolean,outputDir:i.NonEmptyTrimmedString,include:i.NonEmptyTrimmedString,fileSuffix:i.NonEmptyTrimmedString,builderSuffix:i.NonEmptyTrimmedString,defaults:i.Struct({string:i.String,number:i.Number,boolean:i.Boolean})}),_=Tt.make({jsdocTag:"DataBuilder",inlineDefaultJsdocTag:"DataBuilderDefault",withNestedBuilders:!0,outputDir:"generated/builders",include:"src/**/*.ts{,x}",fileSuffix:".builder",builderSuffix:"Builder",defaults:{string:"",number:0,boolean:!1}}),Ce=i.Struct({$schema:i.optional(i.String),jsdocTag:i.String.pipe(i.annotations({description:C.jsdocTag})),inlineDefaultJsdocTag:i.String.pipe(i.annotations({description:C.inlineDefaultJsdocTag})),withNestedBuilders:i.Boolean.pipe(i.annotations({description:C.withNestedbuilders})),outputDir:i.String.pipe(i.annotations({description:C.outputDir})),include:i.String.pipe(i.annotations({description:C.include})),fileSuffix:i.String.pipe(i.annotations({description:C.fileSuffix})),builderSuffix:i.String.pipe(i.annotations({description:C.builderSuffix})),defaults:i.Struct({string:i.String.pipe(i.annotations({description:C.defaultString})),number:i.Number.pipe(i.annotations({description:C.defaultNumber})),boolean:i.Boolean.pipe(i.annotations({description:C.defaultBoolean}))}).pipe(i.partial,i.annotations({description:C.defaults}))}).pipe(i.partial),R=class extends Ue.Tag("Configuration")(){},K=i.Struct({jsdocTag:i.NonEmptyTrimmedString,inlineDefaultJsdocTag:i.NonEmptyTrimmedString,withNestedBuilders:i.BooleanFromString,outputDir:i.NonEmptyTrimmedString,include:i.NonEmptyTrimmedString,fileSuffix:i.NonEmptyTrimmedString,builderSuffix:i.NonEmptyTrimmedString,defaultString:i.String,defaultNumber:i.NumberFromString,defaultBoolean:i.BooleanFromString}),ve=e=>O.gen(function*(){yield*O.logDebug("[Configuration]: Loading configuration");let c=yield*(yield*B).cwd,d=(yield*$e.Path).join(c,Ne),a=yield*Nt(d),T=yield*ht({fromCLI:e,fromConfigFile:a});return R.of(T)}),ht=e=>O.gen(function*(){let n=Et(e),c=I.flatMap(e.fromConfigFile,T=>I.fromNullable(T.defaults)).pipe(I.map(T=>ne(T)),I.getOrElse(()=>({}))),f=ne({string:e.fromCLI.defaultString.pipe(I.getOrUndefined),number:e.fromCLI.defaultNumber.pipe(I.getOrUndefined),boolean:e.fromCLI.defaultBoolean.pipe(I.getOrUndefined)}),d={...c,...f},a={builderSuffix:yield*n("builderSuffix"),include:yield*n("include"),withNestedBuilders:yield*n("withNestedBuilders"),fileSuffix:yield*n("fileSuffix"),jsdocTag:yield*n("jsdocTag"),inlineDefaultJsdocTag:yield*n("inlineDefaultJsdocTag"),outputDir:yield*n("outputDir"),defaults:{..._.defaults,...d}};return yield*O.logDebug(`[Configuration]: Resolving config with value: ${JSON.stringify(a,null,4)}`),a}),Et=e=>n=>e.fromCLI[n].pipe(O.orElse(()=>I.flatMap(e.fromConfigFile,c=>I.fromNullable(c[n]))),O.orElseSucceed(()=>_[n])),Nt=e=>O.gen(function*(){let n=yield*Ee.FileSystem;if(yield*O.orDie(n.exists(e))){yield*O.logDebug("[Configuration]: Found config file - attempting to read it");let f=yield*Ct(e),d=yield*i.decodeUnknown(Ce)(f);return I.some(d)}else return yield*O.logDebug("[Configuration]: No config file found"),I.none()}),Ct=e=>O.gen(function*(){let n=yield*Ee.FileSystem,c=yield*O.orDie(n.readFileString(e));return yield*O.try({try:()=>JSON.parse(c),catch:f=>`[FileSystem] Unable to read and parse JSON file from '${e}': ${String(f)}`})}).pipe(O.orDie);import*as M from"effect/Schema";var De=M.transform(M.String,M.String.pipe(M.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}),je=M.transform(M.String,M.String.pipe(M.brand("PascalCase")),{decode:e=>e.split(/[-_\s]+/).flatMap(n=>n.split(/(?=[A-Z])/)).filter(Boolean).map(n=>n.charAt(0).toUpperCase()+n.slice(1).toLowerCase()).join(""),encode:e=>e}),Ge=M.transform(M.String,M.String.pipe(M.brand("CamelCase")),{decode:e=>e.split(/[-_\s]+/).flatMap(n=>n.split(/(?=[A-Z])/)).filter(Boolean).map((n,c)=>{let f=n.toLowerCase();return c===0?f:f.charAt(0).toUpperCase()+f.slice(1)}).join(""),encode:e=>e});import{randomUUID as Ot}from"crypto";import*as ue from"effect/Effect";var V=class extends ue.Service()("@TSDataBuilders/IdGenerator",{succeed:{generateUuid:ue.sync(()=>Ot())}}){};var pe=class extends m.Service()("@TSDataBuilders/BuilderGenerator",{effect:m.gen(function*(){let n=yield*Oe.FileSystem,c=yield*be.Path,f=yield*B,d=yield*R,a=yield*V,{fileSuffix:T,builderSuffix:g,defaults:h}=d,l=r=>Je.getOrUndefined(r.inlineDefault)??w.value(r).pipe(w.when({kind:"STRING"},()=>`"${h.string}"`),w.when({kind:"NUMBER"},()=>h.number),w.when({kind:"BOOLEAN"},()=>h.boolean),w.when({kind:"UNDEFINED"},()=>"undefined"),w.when({kind:"NULL"},()=>"null"),w.when({kind:"DATE"},()=>"new Date()"),w.when({kind:"ARRAY"},()=>"[]"),w.when({kind:"LITERAL"},o=>o.literalValue),w.when({kind:"TYPE_CAST"},o=>l(o.baseTypeMetadata)),w.when({kind:"TUPLE"},o=>`[${o.members.map(p=>l(p)).map(p=>`${p}`).join(", ")}]`),w.when({kind:"TYPE_LITERAL"},o=>`{${Object.entries(o.metadata).filter(([p,{optional:b}])=>!b).map(([p,b])=>`${p}: ${l(b)}`).join(", ")}}`),w.when({kind:"RECORD"},o=>{if(o.keyType.kind==="STRING"||o.keyType.kind==="NUMBER")return"{}";let u=l(o.keyType),p=l(o.valueType);return`{${u}: ${p}}`}),w.when({kind:"UNION"},o=>{let p=o.members.slice().sort((b,S)=>{let N=Ke.indexOf(b.kind),P=Ke.indexOf(S.kind);return(N===-1?1/0:N)-(P===-1?1/0:P)})[0];return p?l(p):"never"}),w.when({kind:"BUILDER"},o=>`new ${o.name}${g}().build()`),w.exhaustive);return{generateBaseBuilder:m.fnUntraced(function*(){let r=c.join(yield*f.cwd,d.outputDir),o=c.resolve(r,"data-builder.ts");yield*m.logDebug(`[Builders]: Creating base builder at ${o}`),yield*m.orDie(n.writeFileString(o,`${wt}
3
+ `))}),generateBuilder:m.fnUntraced(function*(r){let o=new bt,u=r.name;yield*m.logDebug(`[Builders]: Creating builder for ${u}`);let p=c.join(yield*f.cwd,d.outputDir),b=c.resolve(p,`${yield*De.pipe(re.decode)(u)}${T}.ts`);yield*m.logDebug(`[Builders]: Creating builder content for ${u}`);let S=o.createSourceFile(`__temp_${yield*a.generateUuid}.ts`,"",{overwrite:!0}),N=c.resolve(r.path),P=c.relative(c.dirname(b),N).replace(/\.ts$/,"");if(S.addImportDeclaration({namedImports:[u],isTypeOnly:!0,moduleSpecifier:P}),S.addImportDeclaration({namedImports:["DataBuilder"],moduleSpecifier:"./data-builder"}),r.shape.kind!=="TYPE_LITERAL")return yield*m.dieMessage("[BuilderGenerator]: Expected root type to be type literal");let x=[...new Set(xt(r.shape.metadata))];yield*m.forEach(x,A=>De.pipe(re.decode)(A).pipe(m.andThen(F=>S.addImportDeclaration({namedImports:[`${A}${g}`],moduleSpecifier:`./${F}${T}`}))),{concurrency:"unbounded"});let D=Object.entries(r.shape.metadata).filter(([A,{optional:F}])=>!F).map(([A,F])=>`${A}: ${F.kind==="TYPE_CAST"?`${l(F)} as ${u}['${A}']`:l(F)}`),$=yield*m.all(Object.entries(r.shape.metadata).map(([A,{optional:F,kind:te}])=>It({fieldName:A,optional:F,typeName:u,isNestedBuilder:te==="BUILDER"})),{concurrency:"unbounded"}),W=`{
4
+ ${D.join(`,
5
+ `)}
6
+ }`;S.addClass({name:`${u}${g}`,isExported:!0,extends:`DataBuilder<${u}>`,methods:[{name:"constructor",statements:[`super(${W});`]},...$]}),yield*m.logDebug(`[Builders]: Saving builder content at ${b}`),yield*n.writeFileString(b,`${S.getText()}
7
+ `)})}}),dependencies:[V.Default]}){},Ke=["UNDEFINED","BOOLEAN","NUMBER","STRING","DATE","LITERAL","TYPE_LITERAL","ARRAY","TUPLE","RECORD"],wt=`export abstract class DataBuilder<T> {
8
+ private data: T;
9
+
10
+ constructor(initialData: T) {
11
+ this.data = initialData;
12
+ }
13
+
14
+ public build(): Readonly<T> {
15
+ return structuredClone(this.data);
16
+ }
17
+
18
+ protected with(partial: Partial<T>): this {
19
+ this.data = { ...this.data, ...partial };
20
+ return this;
21
+ }
22
+ }
23
+ `;function xt(e){let n=[];function c(f){switch(f.kind){case"BUILDER":n.push(f.name);break;case"TYPE_LITERAL":Object.values(f.metadata).forEach(c);break;case"UNION":case"TUPLE":f.members.forEach(c);break;case"RECORD":c(f.keyType),c(f.valueType);break}}return Object.values(e).forEach(c),n}var It=e=>m.gen(function*(){let{fieldName:n,optional:c,typeName:f,isNestedBuilder:d}=e,a=n.replaceAll("'","").replaceAll('"',""),T=yield*Ge.pipe(re.decode)(a),g=`with${yield*je.pipe(re.decode)(a)}`,h=[`return this.with({ ${n}: ${T} });`],l=[`return this.with({ ${n}: ${T}.build() });`],r=d?l:h,o=[`if (!${T}) {`,` const { "${a}": _unused, ...rest } = this.build();`," return this.with(rest);","}"],u=c?[...o,...r]:r,p=`${f}['${a}']`;return{name:g,isPublic:!0,parameters:[{name:T,type:d?`DataBuilder<${p}>`:p}],statements:u}}),z=class extends m.Service()("@TSDataBuilders/BuildersGenerator",{effect:m.gen(function*(){let n=yield*Oe.FileSystem,c=yield*pe,f=yield*B,d=yield*be.Path,a=yield*R;return{create:m.fnUntraced(function*(T){let g=d.join(yield*f.cwd,a.outputDir);(yield*m.orDie(n.exists(g)))&&(yield*m.logDebug(`[Builders]: Removing already existing output directory at ${g}`),yield*m.orDie(n.remove(g,{recursive:!0}))),yield*m.logDebug(`[Builders]: Creating output directory at ${g}`),yield*m.orDie(n.makeDirectory(g,{recursive:!0})),yield*c.generateBaseBuilder();let l=T.map(u=>u.name),r=l.filter((u,p)=>l.indexOf(u)!==p),o=[...new Set(r)];if(r.length>0)return yield*m.dieMessage(`Duplicated builders: ${o.join(", ")}`);yield*m.all(T.map(u=>c.generateBuilder(u)),{concurrency:"unbounded"})})}}),dependencies:[pe.Default]}){};import*as We from"@effect/platform/FileSystem";import*as Ve from"@effect/platform/Path";import*as Ze from"effect/Effect";import*as U from"effect/Option";import*as ze from"effect/Schema";var qe=e=>Ze.gen(function*(){let n=yield*B,c=yield*We.FileSystem,f=yield*n.cwd,a=(yield*Ve.Path).join(f,Ne),T=ne({string:e.defaultString.pipe(U.getOrUndefined),number:e.defaultNumber.pipe(U.getOrUndefined),boolean:e.defaultBoolean.pipe(U.getOrUndefined)}),g=yield*ze.decode(Ce)({$schema:"https://raw.githubusercontent.com/nemmtor/ts-databuilders/refs/heads/main/schema.json",builderSuffix:e.builderSuffix.pipe(U.getOrElse(()=>_.builderSuffix)),fileSuffix:e.fileSuffix.pipe(U.getOrElse(()=>_.fileSuffix)),include:e.include.pipe(U.getOrElse(()=>_.include)),jsdocTag:e.jsdocTag.pipe(U.getOrElse(()=>_.jsdocTag)),inlineDefaultJsdocTag:e.inlineDefaultJsdocTag.pipe(U.getOrElse(()=>_.inlineDefaultJsdocTag)),withNestedBuilders:e.withNestedBuilders.pipe(U.getOrElse(()=>_.withNestedBuilders)),outputDir:e.outputDir.pipe(U.getOrElse(()=>_.outputDir)),defaults:{..._.defaults,...T}});yield*c.writeFileString(a,`${JSON.stringify(g,null,2)}
24
+ `)});import*as H from"effect/Chunk";import*as k from"effect/Effect";import*as ge from"effect/Option";import*as Se from"effect/Stream";import*as He from"@effect/platform/FileSystem";import*as Qe from"effect/Chunk";import*as J from"effect/Effect";import*as v from"effect/Stream";var ae=class extends J.Service()("@TSDataBuilders/FileContentChecker",{effect:J.gen(function*(){let n=yield*He.FileSystem,c=new TextDecoder;return{check:J.fnUntraced(function*(f){let{content:d,filePath:a}=f;return yield*J.logDebug(`[FileContentChecker](${a}): Checking file content`),yield*v.orDie(n.stream(a,{chunkSize:16*1024})).pipe(v.map(h=>c.decode(h,{stream:!0})),v.mapAccum("",(h,l)=>{let r=h+l;return[r.slice(-d.length+1),r.includes(d)]}),v.find(h=>!!h),v.tap(()=>J.logDebug(`[FileContentChecker](${a}): found expected content`)),v.runCollect,J.map(h=>h.pipe(Qe.get(0))))})}})}){};import*as Xe from"effect/Data";import*as Z from"effect/Effect";import*as et from"effect/Stream";import{glob as Ft}from"glob";import*as me from"effect/Effect";var se=class extends me.Service()("@TSDataBuilders/Glob",{succeed:{iterate:n=>me.sync(()=>Ft.iterate(n.path,{cwd:n.cwd,nodir:!0}))}}){};var ce=class extends Z.Service()("@TSDataBuilders/TreeWalker",{effect:Z.gen(function*(){let n=yield*se,c=yield*B;return{walk:Z.fnUntraced(function*(f){let d=yield*c.cwd;return yield*Z.logDebug(`[TreeWalker]: Walking path: ${d}/${f}`),et.fromAsyncIterable(yield*n.iterate({path:f,cwd:d}),a=>new we({cause:a}))})}}),dependencies:[se.Default]}){},we=class extends Xe.TaggedError("TreeWalkerError"){};var q=class extends k.Service()("@TSDataBuilders/Finder",{effect:k.gen(function*(){let n=yield*ae,c=yield*ce,{include:f,jsdocTag:d}=yield*R,a=`@${d}`;return{find:k.gen(function*(){yield*k.logDebug("[Finder]: Attempting to find files with builders");let g=yield*(yield*c.walk(f)).pipe(Se.mapEffect(h=>n.check({filePath:h,content:a}).pipe(k.map(l=>l.pipe(ge.map(()=>h)))),{concurrency:"unbounded"}),Se.runCollect,k.map(H.filter(h=>ge.isSome(h))),k.map(H.map(h=>h.value)));return yield*k.logDebug(`[Finder]: Found builders in files: ${g.pipe(H.toArray).join(", ")}`),g}).pipe(k.catchTag("TreeWalkerError",T=>k.die(T)))}}),dependencies:[ce.Default,ae.Default]}){};import{Node as At,Project as Bt,SyntaxKind as E}from"ts-morph";import*as it from"@effect/platform/FileSystem";import*as Y from"effect/Data";import*as t from"effect/Effect";import*as de from"effect/Either";import*as y from"effect/Match";import*as j from"effect/Option";var ye=class extends t.Service()("@TSDataBuilders/TypeNodeParser",{effect:t.gen(function*(){let{jsdocTag:n,inlineDefaultJsdocTag:c,withNestedBuilders:f}=yield*R,d=yield*V,a=t.fnUntraced(function*(h){let l=h.getJsDocs();for(let r of l){let u=r.getTags().find(p=>p.getTagName()===c);if(u){let p=u.getComment();if(typeof p=="string")return j.some(p.trim())}}return j.none()}),T=t.fnUntraced(function*(h){let{type:l,contextNode:r,optional:o,inlineDefault:u}=h,p=l.getProperties();if(!l.isObject()||p.length===0)return yield*new Ie({raw:l.getText(),kind:r.getKind()});let b={};for(let S of p){let N=S.getName(),P=S.getTypeAtLocation(r),x=S.isOptional(),D=r.getProject().createSourceFile(`__temp_${yield*d.generateUuid}.ts`,`type __T = ${P.getText()}`,{overwrite:!0}),$=D.getTypeAliasOrThrow("__T").getTypeNodeOrThrow(),W=yield*t.suspend(()=>g({typeNode:$,optional:x,inlineDefault:j.none()}));b[N]=W,r.getProject().removeSourceFile(D)}return{kind:"TYPE_LITERAL",metadata:b,inlineDefault:u,optional:o}}),g=h=>t.gen(function*(){let{typeNode:l,optional:r,inlineDefault:o}=h,u=l.getKind(),p=y.value(u).pipe(y.when(y.is(E.StringKeyword),()=>t.succeed({kind:"STRING",inlineDefault:o,optional:r})),y.when(y.is(E.NumberKeyword),()=>t.succeed({kind:"NUMBER",inlineDefault:o,optional:r})),y.when(y.is(E.BooleanKeyword),()=>t.succeed({kind:"BOOLEAN",inlineDefault:o,optional:r})),y.when(y.is(E.UndefinedKeyword),()=>t.succeed({kind:"UNDEFINED",inlineDefault:o,optional:r})),y.when(y.is(E.ArrayType),()=>t.succeed({kind:"ARRAY",inlineDefault:o,optional:r})),y.when(y.is(E.LiteralType),()=>{let S=l.asKindOrThrow(E.LiteralType).getLiteral().getText();return S==="null"?t.succeed({kind:"NULL",inlineDefault:o,optional:r}):t.succeed({kind:"LITERAL",inlineDefault:o,literalValue:S,optional:r})}),y.when(y.is(E.TypeLiteral),()=>t.gen(function*(){let N=l.asKindOrThrow(E.TypeLiteral).getMembers(),P=yield*t.reduce(N,{},(x,D)=>t.gen(function*(){if(!D.isKind(E.PropertySignature))return x;let $=D.getTypeNode();if(!$)return x;let W=D.getNameNode().getText(),A=D.hasQuestionToken(),F=yield*a(D),te=yield*t.suspend(()=>g({typeNode:$,optional:A,inlineDefault:F}));return{...x,[W]:te}}));return{kind:"TYPE_LITERAL",inlineDefault:o,metadata:P,optional:r}})),y.when(y.is(E.ImportType),()=>t.gen(function*(){let S=l.asKindOrThrow(E.ImportType),N=S.getType(),P=N.getSymbol(),x=N.getText();if(!P)return yield*new Me({raw:x});let D=P.getDeclarations();if(D&&D.length>1)return yield*new he({raw:x});let[$]=D;return $?yield*T({type:N,contextNode:S,inlineDefault:o,optional:r}):yield*new Te({raw:x})})),y.when(y.is(E.TupleType),()=>t.gen(function*(){let N=l.asKindOrThrow(E.TupleType).getElements(),P=yield*t.all(N.map(x=>t.suspend(()=>g({typeNode:x,optional:!1,inlineDefault:j.none()}))),{concurrency:"unbounded"});return{kind:"TUPLE",inlineDefault:o,optional:r,members:P}})),y.when(y.is(E.TypeReference),()=>t.gen(function*(){let S=l.asKindOrThrow(E.TypeReference),N=S.getTypeName().getText();if(N==="Date")return{kind:"DATE",optional:r,inlineDefault:o};if(N==="Array")return{kind:"ARRAY",optional:r,inlineDefault:o};let P=S.getTypeArguments();if(N==="Record"){let[le,Be]=P;if(!le||!Be)return yield*new Q({kind:u,raw:l.getText()});let gt=yield*t.suspend(()=>g({typeNode:le,optional:!1,inlineDefault:j.none()})),St=yield*t.suspend(()=>g({typeNode:Be,optional:!1,inlineDefault:j.none()}));return{kind:"RECORD",keyType:gt,valueType:St,optional:r,inlineDefault:o}}if(["Pick","Omit","Partial","Required","Readonly","Extract","NonNullable"].includes(N))return yield*T({type:S.getType(),contextNode:S,optional:r,inlineDefault:o});let D=S.getType(),$=D.getText(),W=D.getAliasSymbol();if(!W)return yield*T({type:D,contextNode:S,optional:r,inlineDefault:o});let A=W.getDeclarations();if(A&&A.length>1)return yield*new he({raw:$});let[F]=A;if(!F)return yield*new Te({raw:$});let te=W?.getJsDocTags().map(le=>le.getName()).includes(n);if(!At.isTypeAliasDeclaration(F))return yield*new xe;let Ae=F.getTypeNode();return Ae?!te||!f?yield*t.suspend(()=>g({typeNode:Ae,optional:r,inlineDefault:o})):{kind:"BUILDER",name:F.getName(),inlineDefault:o,optional:r}:yield*new Q({kind:u,raw:$})})),y.when(y.is(E.UnionType),()=>t.gen(function*(){let S=yield*t.all(l.asKindOrThrow(E.UnionType).getTypeNodes().map(N=>t.suspend(()=>g({typeNode:N,optional:!1,inlineDefault:j.none()}))),{concurrency:"unbounded"});return{kind:"UNION",optional:r,members:S,inlineDefault:o}})),y.when(y.is(E.IntersectionType),()=>t.gen(function*(){let N=l.asKindOrThrow(E.IntersectionType).getTypeNodes(),P=[E.StringKeyword,E.NumberKeyword,E.BooleanKeyword],x=N.find(D=>P.includes(D.getKind()));return x&&N.length>1?{kind:"TYPE_CAST",baseTypeMetadata:yield*t.suspend(()=>g({typeNode:x,optional:!1,inlineDefault:o})),inlineDefault:o,optional:r}:yield*new Q({kind:u,raw:l.getText()})})),y.option);return j.isNone(p)?yield*new Q({kind:u,raw:l.getText()}):yield*p.value});return{generateMetadata:g}}),dependencies:[V.Default]}){},X=class extends t.Service()("@TSDataBuilders/Parser",{effect:t.gen(function*(){let n=yield*it.FileSystem,c=yield*ye,{jsdocTag:f}=yield*R;return{generateBuildersMetadata:d=>t.gen(function*(){yield*t.logDebug(`[Parser](${d}): Generating builder metadata`),yield*t.logDebug(`[Parser](${d}): Reading source code`);let a=yield*t.orDie(n.readFileString(d)),T=yield*t.try({try:()=>new Bt().createSourceFile(d,a,{overwrite:!0}).getTypeAliases().filter(u=>u.getJsDocs().flatMap(p=>p.getTags().flatMap(b=>b.getTagName())).includes(f)).map(u=>{let p=u.getName();if(!u.isExported())return de.left(new Fe({typeName:p}));let b=u.getTypeNode();return b?.isKind(E.TypeLiteral)||b?.isKind(E.TypeReference)?de.right({name:u.getName(),node:b}):de.left(new ke({typeName:u.getName()}))}).filter(Boolean),catch:l=>new Pe({cause:l})}),g=yield*t.all(T.map(l=>l),{concurrency:"unbounded"});return yield*t.logDebug(`[Parser](${d}): Generating metadata for types: ${g.map(({name:l})=>l).join(", ")}`),yield*t.all(g.map(({name:l,node:r})=>c.generateMetadata({typeNode:r,optional:!1,inlineDefault:j.none()}).pipe(t.tap(()=>t.logDebug(`[Parser](${d}): Finished generating metadata for type: ${l}`)),t.map(o=>({name:l,shape:o,path:d})))),{concurrency:"unbounded"})}).pipe(t.catchTags({ParserError:a=>t.die(a),MissingSymbolDeclarationError:a=>t.dieMessage(`[Parser](${d}): Missing symbol declaration for type: ${a.raw}`),UnsupportedTypeAliasDeclarationError:()=>t.dieMessage(`[Parser](${d}): Unsupported type alias declaration`),MultipleSymbolDeclarationsError:a=>t.dieMessage(`[Parser](${d}): Missing symbol declaration error for type: ${a.raw}`),MissingSymbolError:a=>t.dieMessage(`[Parser](${d}): Missing symbol error for type: ${a.raw}`),UnexportedDatabuilderError:a=>t.dieMessage(`[Parser](${d}): Unexported databuilder ${a.typeName}`),UnsupportedSyntaxKindError:a=>t.dieMessage(`[Parser](${d}): Unsupported syntax kind of id: ${a.kind} for type: ${a.raw}`),CannotBuildTypeReferenceMetadataError:a=>t.dieMessage(`[Parser](${d}): Cannot build type reference metadata with kind of id: ${a.kind} for type: ${a.raw}. Is it a root of databuilder?`),UnsupportedBuilderTypeError:a=>t.dieMessage(`[Parser](${d}): Unsupported builder type ${a.typeName}`)}))}}),dependencies:[ye.Default]}){},Q=class extends Y.TaggedError("UnsupportedSyntaxKindError"){},xe=class extends Y.TaggedError("UnsupportedTypeAliasDeclarationError"){},Ie=class extends Y.TaggedError("CannotBuildTypeReferenceMetadataError"){},Pe=class extends Y.TaggedError("ParserError"){},Fe=class extends Y.TaggedError("UnexportedDatabuilderError"){},ke=class extends Y.TaggedError("UnsupportedBuilderTypeError"){},Me=class extends Y.TaggedError("MissingSymbolError"){},Te=class extends Y.TaggedError("MissingSymbolDeclarationError"){},he=class extends Y.TaggedError("MultipleSymbolDeclarationsError"){};import*as ot from"effect/Chunk";import*as L from"effect/Effect";import*as rt from"effect/Function";var at=L.gen(function*(){let e=yield*q,n=yield*X,c=yield*z;yield*L.logInfo("[TSDatabuilders]: Generating builders for your project.");let f=yield*e.find;yield*L.logInfo(`[TSDatabuilders]: Found builders in ${f.length} file(s).`),yield*L.logDebug("[TSDatabuilders]: Attempting to generate builders metadata");let d=yield*L.all(ot.map(f,a=>n.generateBuildersMetadata(a)),{concurrency:"unbounded"}).pipe(L.map(a=>a.flatMap(rt.identity)));d.length!==0&&(yield*L.logDebug("[TSDatabuilders]: Attempting to create builders files"),yield*c.create(d),yield*L.logInfo(`[TSDatabuilders]: Created ${d.length} builder(s).`))});var Rt=s.text("jsdoc-tag").pipe(s.withDescription(C.jsdocTag),s.withSchema(K.fields.jsdocTag),s.optional),Lt=s.text("inline-default-jsdoc-tag").pipe(s.withDescription(C.inlineDefaultJsdocTag),s.withSchema(K.fields.inlineDefaultJsdocTag),s.optional),$t=s.text("with-nested-builders").pipe(s.withDescription(C.withNestedbuilders),s.withSchema(K.fields.withNestedBuilders),s.optional),Ut=s.text("output-dir").pipe(s.withAlias("o"),s.withDescription(C.outputDir),s.withSchema(K.fields.outputDir),s.optional),vt=s.text("include").pipe(s.withAlias("i"),s.withDescription(C.include),s.withSchema(K.fields.include),s.optional),jt=s.text("file-suffix").pipe(s.withDescription(C.fileSuffix),s.withSchema(K.fields.fileSuffix),s.optional),Gt=s.text("builder-suffix").pipe(s.withDescription(C.builderSuffix),s.withSchema(K.fields.builderSuffix),s.optional),_t=s.text("default-string").pipe(s.withDescription(C.defaultString),s.withSchema(K.fields.defaultString),s.optional),Kt=s.text("default-number").pipe(s.withDescription(C.defaultNumber),s.withSchema(K.fields.defaultNumber),s.optional),Jt=s.text("default-boolean").pipe(s.withDescription(C.defaultBoolean),s.withSchema(K.fields.defaultBoolean),s.optional),st={jsdocTag:Rt,outputDir:Ut,withNestedBuilders:$t,include:vt,fileSuffix:jt,builderSuffix:Gt,defaultString:_t,defaultNumber:Kt,defaultBoolean:Jt,inlineDefaultJsdocTag:Lt},Yt=G.make("init",st).pipe(G.withHandler(qe)),Wt=G.make("ts-databuilders",st),ct=Wt.pipe(G.withHandler(()=>at),G.withSubcommands([Yt]),G.provide(e=>ee.mergeAll(q.Default,X.Default,z.Default).pipe(ee.provide(ee.effect(R,ve(e))))),G.run({name:"Typescript Databuilders generator",version:"v0.0.1"}));var Vt=ut.mergeAll(pt.minimumLogLevel(mt.Info),B.Default,dt.layer);ct(process.argv).pipe(ft.provide(Vt),lt.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.10",
4
+ "version": "0.0.1-alpha.11",
5
5
  "type": "module",
6
6
  "private": false,
7
7
  "description": "CLI tool that automatically generates builder classes from annotated TypeScript types.",
@@ -49,7 +49,7 @@
49
49
  "@effect/vitest": "^0.27.0",
50
50
  "@total-typescript/ts-reset": "^0.6.1",
51
51
  "@types/node": "^24.0.0",
52
- "@vitest/coverage-v8": "4.0.7",
52
+ "@vitest/coverage-v8": "4.0.8",
53
53
  "dependency-cruiser": "^17.2.0",
54
54
  "knip": "^5.66.4",
55
55
  "lefthook": "^2.0.2",