├── .changeset
├── README.md
└── config.json
├── .gitignore
├── .travis.yml
├── .vscode
└── settings.example.json
├── LICENSE
├── README.md
├── biome.json
├── documentation
├── about-typeonly.md
└── tutorial.md
├── package-lock.json
├── package.json
└── packages
├── loader
├── .gitignore
├── .npmignore
├── .prettierignore
├── CHANGELOG.md
├── LICENSE
├── README.md
├── bundle-tsd
│ └── bundle-tsd.js
├── package.json
├── scripts
│ └── bundle-tsd.js
├── src
│ ├── api.ts
│ ├── helpers
│ │ └── module-path-helpers.ts
│ ├── loader
│ │ ├── ModuleFactory.ts
│ │ └── Project.ts
│ └── typeonly-loader.d.ts
├── tests
│ ├── loader-types.spec.ts
│ └── tsconfig.json
├── tsconfig.json
└── vitest.config.ts
├── typeonly
├── .gitignore
├── .npmignore
├── .prettierignore
├── CHANGELOG.md
├── LICENSE
├── README.md
├── TODO.md
├── TypeOnlyLexer.g4
├── TypeOnlyParser.g4
├── package.json
├── scripts
│ └── bundle-tsd.js
├── src
│ ├── api.ts
│ ├── ast.d.ts
│ ├── cli.ts
│ ├── helpers
│ │ ├── fs-utils.ts
│ │ ├── js-lib.ts
│ │ └── module-path-helpers.ts
│ ├── parser
│ │ ├── AstExtractor.ts
│ │ ├── CommentGrabber.ts
│ │ ├── antlr4-defs.d.ts
│ │ └── parse-typeonly.ts
│ ├── rto-factory
│ │ ├── AstImportTool.ts
│ │ ├── InlineImportScanner.ts
│ │ ├── ProjectInputOutput.ts
│ │ ├── RtoModuleFactory.ts
│ │ ├── RtoProject.ts
│ │ └── internal-types.d.ts
│ └── rto.d.ts
├── tests
│ ├── ast-tests
│ │ ├── ast-array.spec.ts
│ │ ├── ast-comment.spec.ts
│ │ ├── ast-composite.spec.ts
│ │ ├── ast-function.spec.ts
│ │ ├── ast-generic.spec.ts
│ │ ├── ast-index-signature.spec.ts
│ │ ├── ast-inline-import.spec.ts
│ │ ├── ast-interface.spec.ts
│ │ ├── ast-interface2.spec.ts
│ │ ├── ast-key-of.spec.ts
│ │ ├── ast-literal.spec.ts
│ │ ├── ast-member-type.spec.ts
│ │ ├── ast-named-import.spec.ts
│ │ ├── ast-named-type.spec.ts
│ │ ├── ast-nested-types.spec.ts
│ │ ├── ast-precedence.spec.ts
│ │ ├── ast-tuple.spec.ts
│ │ └── ast-whitespaces.spec.ts
│ ├── import-tests
│ │ ├── import-tests.spec.ts
│ │ └── test-proj01
│ │ │ ├── node_modules
│ │ │ ├── @hello
│ │ │ │ └── world
│ │ │ │ │ ├── package.json
│ │ │ │ │ └── types
│ │ │ │ │ ├── main.d.ts
│ │ │ │ │ └── other.d.ts
│ │ │ └── simple-package
│ │ │ │ ├── package.json
│ │ │ │ └── types
│ │ │ │ ├── main.d.ts
│ │ │ │ └── simple-other.d.ts
│ │ │ ├── package.json
│ │ │ └── types
│ │ │ └── proj01-types.d.ts
│ ├── rto-tests
│ │ ├── rto-comment.spec.ts
│ │ ├── rto-import.spec.ts
│ │ └── rto-types.spec.ts
│ └── tsconfig.json
├── tsconfig.json
├── typeonly-language.md
└── vitest.config.ts
├── validator-cli
├── .gitignore
├── .prettierignore
├── CHANGELOG.md
├── LICENSE
├── README.md
├── package.json
├── src
│ └── cli.ts
└── tsconfig.json
└── validator
├── .gitignore
├── .npmignore
├── .prettierignore
├── CHANGELOG.md
├── LICENSE
├── README.md
├── bundle-tsd
└── bundle-tsd.js
├── package.json
├── scripts
└── bundle-tsd.js
├── src
├── Validator.ts
├── api.ts
├── error-message.ts
└── helpers.ts
├── tests
├── interface.spec.ts
├── tsconfig.json
└── types.spec.ts
├── tsconfig.json
└── vitest.config.ts
/.changeset/README.md:
--------------------------------------------------------------------------------
1 | # Changesets
2 |
3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
4 | with multi-package repos, or single-package repos to help you version and publish your code. You can
5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets)
6 |
7 | We have a quick list of common questions to get you started engaging with this project in
8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)
9 |
--------------------------------------------------------------------------------
/.changeset/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://unpkg.com/@changesets/config@3.0.5/schema.json",
3 | "changelog": "@changesets/cli/changelog",
4 | "commit": false,
5 | "fixed": [],
6 | "linked": [],
7 | "access": "public",
8 | "baseBranch": "master",
9 | "updateInternalDependencies": "patch",
10 | "ignore": [],
11 | "___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH": {
12 | "onlyUpdatePeerDependentsWhenOutOfRange": true
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dist
3 | .npmrc
4 | .vscode/*
5 | !.vscode/settings.example.json
6 | /.DS_Store
7 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - 10
4 | - 12
5 | - 14
6 | cache:
7 | directories:
8 | - "node_modules"
9 | before_install:
10 | - wget https://www.antlr.org/download/antlr-4.13.2-complete.jar --directory-prefix typeonly
11 | # before_script:
12 | # - npm run build
13 | env:
14 | - SUB_PROJECT=typeonly
15 | - SUB_PROJECT=loader
16 | - SUB_PROJECT=validator
17 | - SUB_PROJECT=validator-cli
18 | script: cd $SUB_PROJECT && npm i && npm run prepublishOnly
19 |
--------------------------------------------------------------------------------
/.vscode/settings.example.json:
--------------------------------------------------------------------------------
1 | {
2 | "editor.insertSpaces": true,
3 | "editor.rulers": [100],
4 | "editor.tabSize": 2,
5 | "editor.wordWrap": "wordWrapColumn",
6 | "editor.wordWrapColumn": 100,
7 | "files.encoding": "utf8",
8 | "files.trimTrailingWhitespace": true,
9 | "javascript.preferences.quoteStyle": "double",
10 | "search.useIgnoreFiles": true,
11 | "javascript.format.semicolons": "insert",
12 | "typescript.locale": "en",
13 | "typescript.format.semicolons": "insert",
14 | "typescript.preferences.importModuleSpecifier": "relative",
15 | "typescript.preferences.quoteStyle": "double",
16 | "search.exclude": {
17 | "**/node_modules": true,
18 | "**/package-lock.json": true,
19 | "**/dist": true,
20 | "**/CHANGELOG.md": true
21 | },
22 | "[typescript]": {
23 | "editor.codeActionsOnSave": {
24 | "source.fixAll": "explicit",
25 | "source.organizeImports": "explicit"
26 | },
27 | "editor.defaultFormatter": "biomejs.biome",
28 | "editor.formatOnSave": true
29 | },
30 | "[json]": {
31 | "editor.defaultFormatter": "biomejs.biome"
32 | },
33 | "antlr4.format": {
34 | "continuationIndentWidth": 2,
35 | "indentWidth": 2,
36 | "tabWidth": 2,
37 | "useTab": false
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Creative Commons Legal Code
2 |
3 | CC0 1.0 Universal
4 |
5 | CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
6 | LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN
7 | ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
8 | INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES
9 | REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS
10 | PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM
11 | THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED
12 | HEREUNDER.
13 |
14 | Statement of Purpose
15 |
16 | The laws of most jurisdictions throughout the world automatically confer
17 | exclusive Copyright and Related Rights (defined below) upon the creator
18 | and subsequent owner(s) (each and all, an "owner") of an original work of
19 | authorship and/or a database (each, a "Work").
20 |
21 | Certain owners wish to permanently relinquish those rights to a Work for
22 | the purpose of contributing to a commons of creative, cultural and
23 | scientific works ("Commons") that the public can reliably and without fear
24 | of later claims of infringement build upon, modify, incorporate in other
25 | works, reuse and redistribute as freely as possible in any form whatsoever
26 | and for any purposes, including without limitation commercial purposes.
27 | These owners may contribute to the Commons to promote the ideal of a free
28 | culture and the further production of creative, cultural and scientific
29 | works, or to gain reputation or greater distribution for their Work in
30 | part through the use and efforts of others.
31 |
32 | For these and/or other purposes and motivations, and without any
33 | expectation of additional consideration or compensation, the person
34 | associating CC0 with a Work (the "Affirmer"), to the extent that he or she
35 | is an owner of Copyright and Related Rights in the Work, voluntarily
36 | elects to apply CC0 to the Work and publicly distribute the Work under its
37 | terms, with knowledge of his or her Copyright and Related Rights in the
38 | Work and the meaning and intended legal effect of CC0 on those rights.
39 |
40 | 1. Copyright and Related Rights. A Work made available under CC0 may be
41 | protected by copyright and related or neighboring rights ("Copyright and
42 | Related Rights"). Copyright and Related Rights include, but are not
43 | limited to, the following:
44 |
45 | i. the right to reproduce, adapt, distribute, perform, display,
46 | communicate, and translate a Work;
47 | ii. moral rights retained by the original author(s) and/or performer(s);
48 | iii. publicity and privacy rights pertaining to a person's image or
49 | likeness depicted in a Work;
50 | iv. rights protecting against unfair competition in regards to a Work,
51 | subject to the limitations in paragraph 4(a), below;
52 | v. rights protecting the extraction, dissemination, use and reuse of data
53 | in a Work;
54 | vi. database rights (such as those arising under Directive 96/9/EC of the
55 | European Parliament and of the Council of 11 March 1996 on the legal
56 | protection of databases, and under any national implementation
57 | thereof, including any amended or successor version of such
58 | directive); and
59 | vii. other similar, equivalent or corresponding rights throughout the
60 | world based on applicable law or treaty, and any national
61 | implementations thereof.
62 |
63 | 2. Waiver. To the greatest extent permitted by, but not in contravention
64 | of, applicable law, Affirmer hereby overtly, fully, permanently,
65 | irrevocably and unconditionally waives, abandons, and surrenders all of
66 | Affirmer's Copyright and Related Rights and associated claims and causes
67 | of action, whether now known or unknown (including existing as well as
68 | future claims and causes of action), in the Work (i) in all territories
69 | worldwide, (ii) for the maximum duration provided by applicable law or
70 | treaty (including future time extensions), (iii) in any current or future
71 | medium and for any number of copies, and (iv) for any purpose whatsoever,
72 | including without limitation commercial, advertising or promotional
73 | purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each
74 | member of the public at large and to the detriment of Affirmer's heirs and
75 | successors, fully intending that such Waiver shall not be subject to
76 | revocation, rescission, cancellation, termination, or any other legal or
77 | equitable action to disrupt the quiet enjoyment of the Work by the public
78 | as contemplated by Affirmer's express Statement of Purpose.
79 |
80 | 3. Public License Fallback. Should any part of the Waiver for any reason
81 | be judged legally invalid or ineffective under applicable law, then the
82 | Waiver shall be preserved to the maximum extent permitted taking into
83 | account Affirmer's express Statement of Purpose. In addition, to the
84 | extent the Waiver is so judged Affirmer hereby grants to each affected
85 | person a royalty-free, non transferable, non sublicensable, non exclusive,
86 | irrevocable and unconditional license to exercise Affirmer's Copyright and
87 | Related Rights in the Work (i) in all territories worldwide, (ii) for the
88 | maximum duration provided by applicable law or treaty (including future
89 | time extensions), (iii) in any current or future medium and for any number
90 | of copies, and (iv) for any purpose whatsoever, including without
91 | limitation commercial, advertising or promotional purposes (the
92 | "License"). The License shall be deemed effective as of the date CC0 was
93 | applied by Affirmer to the Work. Should any part of the License for any
94 | reason be judged legally invalid or ineffective under applicable law, such
95 | partial invalidity or ineffectiveness shall not invalidate the remainder
96 | of the License, and in such case Affirmer hereby affirms that he or she
97 | will not (i) exercise any of his or her remaining Copyright and Related
98 | Rights in the Work or (ii) assert any associated claims and causes of
99 | action with respect to the Work, in either case contrary to Affirmer's
100 | express Statement of Purpose.
101 |
102 | 4. Limitations and Disclaimers.
103 |
104 | a. No trademark or patent rights held by Affirmer are waived, abandoned,
105 | surrendered, licensed or otherwise affected by this document.
106 | b. Affirmer offers the Work as-is and makes no representations or
107 | warranties of any kind concerning the Work, express, implied,
108 | statutory or otherwise, including without limitation warranties of
109 | title, merchantability, fitness for a particular purpose, non
110 | infringement, or the absence of latent or other defects, accuracy, or
111 | the present or absence of errors, whether or not discoverable, all to
112 | the greatest extent permissible under applicable law.
113 | c. Affirmer disclaims responsibility for clearing rights of other persons
114 | that may apply to the Work or any use thereof, including without
115 | limitation any person's Copyright and Related Rights in the Work.
116 | Further, Affirmer disclaims responsibility for obtaining any necessary
117 | consents, permissions or other rights required for any use of the
118 | Work.
119 | d. Affirmer understands and acknowledges that Creative Commons is not a
120 | party to this document and has no duty or obligation with respect to
121 | this CC0 or use of the Work.
122 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Monorepo for TypeOnly
2 |
3 | [](https://travis-ci.com/paroi-tech/typeonly)
4 |
5 | **TypeOnly** is a lightweight validation library that uses TypeScript type definitions to validate JSON data. **[Learn more about TypeOnly here](https://github.com/paroi-tech/typeonly/tree/master/packages/typeonly)**.
6 |
7 | ## Projects
8 |
9 | * [typeonly](https://github.com/paroi-tech/typeonly/tree/master/packages/typeonly): Parses types and interfaces from TypeScript and stores them as JSON files;
10 | * [@typeonly/loader](https://github.com/paroi-tech/typeonly/tree/master/packages/loader): Brings types and interfaces from TypeScript at runtime;
11 | * [@typeonly/validator](https://github.com/paroi-tech/typeonly/tree/master/packages/validator): An API to validate JSON data or JavaScript objects, using TypeScript typing definitions;
12 | * [@typeonly/validator-cli](https://github.com/paroi-tech/typeonly/tree/master/packages/validator-cli): A CLI to validate JSON files, using TypeScript typing definitions.
13 |
14 | ## Contribute
15 |
16 | ### Install and Build
17 |
18 | We need a JVM (Java Virtual Machine) to build the parser because we use [ANTLR](https://www.antlr.org/), which is a Java program. So, at first, install a JVM on your system.
19 |
20 | In a terminal, open the cloned `typeonly/typeonly/` repository. Then:
21 |
22 | ```sh
23 | # Download once the ANTLR JAR file in the project's root directory
24 | wget https://www.antlr.org/download/antlr-4.13.2-complete.jar
25 |
26 | # Install once all Node.js dependencies
27 | npm install
28 | ```
29 |
30 | ### Development environment
31 |
32 | With VS Code, our recommanded plugins are:
33 |
34 | - **Biome** from biomejs (biomejs.dev)
35 | - **ANTLR4 grammar syntax support** from Mike Lischke (`mike-lischke.vscode-antlr4`)
36 |
--------------------------------------------------------------------------------
/biome.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "./node_modules/@biomejs/biome/configuration_schema.json",
3 | "javascript": {
4 | "parser": {
5 | "unsafeParameterDecoratorsEnabled": true
6 | }
7 | },
8 | "linter": {
9 | "enabled": true,
10 | "rules": {
11 | "recommended": true,
12 | "suspicious": {
13 | "noExplicitAny": "off",
14 | "useValidTypeof": "off"
15 | },
16 | "complexity": {
17 | "noForEach": "off",
18 | "useSimplifiedLogicExpression": "off"
19 | },
20 | "a11y": {
21 | "useKeyWithClickEvents": "warn",
22 | "noLabelWithoutControl": "off"
23 | },
24 | "security": {
25 | "noDangerouslySetInnerHtml": "off"
26 | },
27 | "correctness": {
28 | "noConstantCondition": "off",
29 | "noSwitchDeclarations": "off",
30 | "useExhaustiveDependencies": "off",
31 | "useImportExtensions": "error"
32 | }
33 | }
34 | },
35 | "formatter": {
36 | "enabled": true,
37 | "formatWithErrors": false,
38 | "indentStyle": "space",
39 | "indentWidth": 2,
40 | "lineWidth": 100
41 | },
42 | "files": {
43 | "include": ["*.ts", "*.json"],
44 | "ignore": ["dist", "node_modules", "scripts/declarations"]
45 | },
46 | "organizeImports": {
47 | "enabled": true
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/documentation/about-typeonly.md:
--------------------------------------------------------------------------------
1 | # About TypeOnly
2 |
3 | ## What is TypeOnly?
4 |
5 | TypeOnly aims to be the pure typing part of TypeScript. See also: a [detailed description of the language](https://github.com/paroi-tech/typeonly/blob/master/typeonly/typeonly-language.md).
6 |
7 | TypeOnly is a new language but not a new syntax. TypeOnly aims to be and remain a strict subset of TypeScript: any code that compiles with TypeOnly will also compile with TypeScript. It is the "pure typing" part of TypeScript: only `interface` and `type` definitions.
8 |
9 | With TypeScript, types definitions are not available at runtime. Sometime this forces us to repeat ourselves, as in the following example:
10 |
11 | ```ts
12 | type ColorName = "red" | "green" | "blue";
13 |
14 | function isColorName(name: string): name is ColorName {
15 | return ["red", "green", "blue"].includes(name);
16 | }
17 | ```
18 |
19 | This kind of code is not ideal. There is an [issue](https://github.com/microsoft/TypeScript/issues/3628) on Github related to this subject, and the TypeScript team is not ready to provide a solution.
20 |
21 | The TypeOnly parser is implemented from scratch and does not require TypeScript as a dependency. It can be used outside a TypeScript project, such as in a JavaScript project, or to validate JSON data with a command line tool.
22 |
23 | ## How to use TypeOnly
24 |
25 | There are three packages built on top of TypeOnly.
26 |
27 | How to **load typing metadata at runtime**: use the package [@typeonly/loader](https://github.com/paroi-tech/typeonly/tree/master/packages/loader).
28 |
29 | How to **validate JSON data from the command line**: use the package [@typeonly/validator-cli](https://github.com/paroi-tech/typeonly/tree/master/packages/validator-cli).
30 |
31 | How to **validate JSON data or a JavaScript object using an API**: use the package [@typeonly/validator](https://github.com/paroi-tech/typeonly/tree/master/packages/validator).
32 |
33 | ## Known Limitations
34 |
35 | Generics are not implemented.
36 |
37 | There is some kind of source code that can currently be parsed without error with TypeOnly, although it is invalid in TypeScript. This is a temporary limitation of our implementation. Do not use it! TypeOnly will always remain a strict subset of TypeScript. If you write some code that is incompatible with TypeScript, then future versions of TypeOnly could break your code.
38 |
39 | An example of invalid TypeScript code that mistakenly can be parsed by the current version of TypeOnly:
40 |
41 | ```ts
42 | interface I1 {
43 | [name: string]: boolean;
44 | p1: number; // TS Error: Property 'p1' of type 'number' is not assignable to string index type 'boolean'.
45 | }
46 | ```
47 |
--------------------------------------------------------------------------------
/documentation/tutorial.md:
--------------------------------------------------------------------------------
1 | # Tutorials for TypeOnly
2 |
3 | ## Parse TypeScript definitions with the CLI
4 |
5 | In a new directory, install `typeonly` as a dependency:
6 |
7 | ```sh
8 | npm init
9 | npm install typeonly --save-dev
10 | ```
11 |
12 | Edit the file `package.json` and add the following entry in the section `"scripts"`:
13 |
14 | ```json
15 | "scripts": {
16 | "typeonly": "typeonly --bundle dist/types.to.json --source-dir types"
17 | },
18 | ```
19 |
20 | Create a subdirectory `types/`, then create a file _"types/drawing.d.ts"_ with the following code:
21 |
22 | ```ts
23 | // types/drawing.d.ts
24 |
25 | export interface Drawing {
26 | color: ColorName;
27 | dashed?: boolean;
28 | shape: Rectangle | Circle;
29 | }
30 |
31 | export type ColorName = "red" | "green" | "blue";
32 |
33 | export interface Rectangle {
34 | kind: "rectangle";
35 | x: number;
36 | y: number;
37 | width: number;
38 | height: number;
39 | }
40 |
41 | export interface Circle {
42 | kind: "circle";
43 | x: number;
44 | y: number;
45 | radius: number;
46 | }
47 | ```
48 |
49 | Now we can execute the TypeOnly parser via our script:
50 |
51 | ```sh
52 | npm run typeonly
53 | ```
54 |
55 | This command creates a file `dist/types.to.json`. A file with the `.to.json` extension is a bundle that contains metadata extracted from several `.d.ts` typing file.
56 |
57 | ## How to programmatically validate JSON data
58 |
59 | Now, add `@typeonly/validator` to the project:
60 |
61 | ```sh
62 | npm install @typeonly/validator
63 | ```
64 |
65 | Create a file `src/validate-main.js` with the following content:
66 |
67 | ```ts
68 | // src/validate-main.js
69 | import { readFileSync } from "node:fs"
70 | import { createValidator } from "@typeonly/validator";
71 |
72 | const data = {
73 | color: "green",
74 | shape: {
75 | kind: "circle",
76 | x: 100,
77 | y: 100,
78 | radius: 50
79 | }
80 | };
81 |
82 | const validator = createValidator({
83 | bundle: JSON.parse(readFileSync(`./types.to.json`));
84 | });
85 | const result = validator.validate("./drawing", "Drawing", data);
86 | console.log(result);
87 | ```
88 |
89 | This code validates the `data` object using RTO files generated by the TypeOnly parser.
90 |
91 | Execute it:
92 |
93 | ```sh
94 | $ node src/validate-main.js
95 | { valid: true }
96 | ```
97 |
98 | ## Tutorial: Load typing definitions at runtime
99 |
100 | Now, add `@typeonly/loader` to the project:
101 |
102 | ```sh
103 | npm install @typeonly/loader
104 | ```
105 |
106 | Create a file `src/main.js` with the following content:
107 |
108 | ```ts
109 | // src/main.js
110 | import { readFileSync } from "node:fs"
111 | import { loadModules, literals } from "@typeonly/loader";
112 |
113 | const modules = loadModules({
114 | bundle: JSON.parse(readFileSync(`./types.to.json`));
115 | });
116 |
117 | const { ColorName } = modules["./drawing"].namedTypes;
118 | console.log("Color names:", literals(ColorName, "string"));
119 | ```
120 |
121 | If you write this code in a TypeScript source file, simply replace the `require` syntax with a standard `import`.
122 |
123 | We can execute our program:
124 |
125 | ```sh
126 | $ node src/main.js
127 | Color names: [ 'red', 'green', 'blue' ]
128 | ```
129 |
130 | Yes, it’s as easy as it seems: the list of color names is now available at runtime.
131 |
132 | Notice: The TypeOnly parser is used at build time. At runtime, our code only use `@typeonly/loader` which is a lightweight wrapper for `.to.json` files.
133 |
134 | ## How to validate the conformity of a JSON file using the CLI
135 |
136 | Create a file _"drawing.d.ts"_ with the following code:
137 |
138 | ```ts
139 | // drawing.d.ts
140 |
141 | export interface Drawing {
142 | color: ColorName
143 | dashed?: boolean
144 | shape: Rectangle | Circle
145 | }
146 |
147 | export type ColorName = "red" | "green" | "blue"
148 |
149 | export interface Rectangle {
150 | kind: "rectangle",
151 | x: number
152 | y: number
153 | width: number
154 | height: number
155 | }
156 |
157 | export interface Circle {
158 | kind: "circle",
159 | x: number
160 | y: number
161 | radius: number
162 | }
163 | ```
164 |
165 | Then, create a JSON file _"drawing.json"_ that must be of type `Drawing`:
166 |
167 | ```json
168 | {
169 | "color": "green",
170 | "shape": {
171 | "kind": "circle",
172 | "x": 100,
173 | "y": 100,
174 | "radius": "wrong value"
175 | }
176 | }
177 | ```
178 |
179 | We are ready to validate the JSON file:
180 |
181 | ```sh
182 | $ npx @typeonly/validator-cli -s drawing.d.ts -t "Drawing" drawing.json
183 | In property 'radius', value '"wrong value"' is not conform to number.
184 | ```
185 |
186 | A mistake is detected in the JSON file. Fix it by replacing the value of the property `"radius"` with a valid number. For example: `"radius": 50`. And run the command again:
187 |
188 | ```sh
189 | $ npx @typeonly/validator-cli -s drawing.d.ts -t "Drawing" drawing.json
190 | ```
191 |
192 | Good. The validator no longer complain.
193 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "typeonly-monorepo",
3 | "version": "0.0.0",
4 | "author": "Paroi Team",
5 | "private": true,
6 | "scripts": {
7 | "clear": "npm run clear --workspaces --if-present",
8 | "build": "npm run build --workspaces --if-present",
9 | "test": "npm run test --workspaces --if-present",
10 | "lint": "npm run lint --workspaces --if-present",
11 | "changeset": "changeset",
12 | "changeset:version": "changeset version",
13 | "changeset:publish": "changeset publish"
14 | },
15 | "devDependencies": {
16 | "@biomejs/biome": "~1.9.4",
17 | "@changesets/cli": "~2.27.12"
18 | },
19 | "workspaces": [
20 | "packages/typeonly",
21 | "packages/loader",
22 | "packages/validator",
23 | "packages/validator-cli"
24 | ]
25 | }
26 |
--------------------------------------------------------------------------------
/packages/loader/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | ._*
3 |
4 | node_modules
5 | package-lock.json
6 | .npmrc
7 |
8 | /dist
9 | /scripts/declarations
10 |
--------------------------------------------------------------------------------
/packages/loader/.npmignore:
--------------------------------------------------------------------------------
1 | /scripts/declarations
2 |
--------------------------------------------------------------------------------
/packages/loader/.prettierignore:
--------------------------------------------------------------------------------
1 | *
--------------------------------------------------------------------------------
/packages/loader/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # @typeonly/loader
2 |
3 | ## 1.0.1
4 |
5 | ### Patch Changes
6 |
7 | - 9759c6a: Documentation
8 |
9 | ## 1.0.0
10 |
11 | ### Major Changes
12 |
13 | - ESM modules
14 |
15 | ## 0.6.0
16 |
17 | ### Minor Changes
18 |
19 | - upgrade dependencies & syntax
20 |
--------------------------------------------------------------------------------
/packages/loader/LICENSE:
--------------------------------------------------------------------------------
1 | CC0 1.0 Universal
2 |
3 | Statement of Purpose
4 |
5 | The laws of most jurisdictions throughout the world automatically confer
6 | exclusive Copyright and Related Rights (defined below) upon the creator and
7 | subsequent owner(s) (each and all, an "owner") of an original work of
8 | authorship and/or a database (each, a "Work").
9 |
10 | Certain owners wish to permanently relinquish those rights to a Work for the
11 | purpose of contributing to a commons of creative, cultural and scientific
12 | works ("Commons") that the public can reliably and without fear of later
13 | claims of infringement build upon, modify, incorporate in other works, reuse
14 | and redistribute as freely as possible in any form whatsoever and for any
15 | purposes, including without limitation commercial purposes. These owners may
16 | contribute to the Commons to promote the ideal of a free culture and the
17 | further production of creative, cultural and scientific works, or to gain
18 | reputation or greater distribution for their Work in part through the use and
19 | efforts of others.
20 |
21 | For these and/or other purposes and motivations, and without any expectation
22 | of additional consideration or compensation, the person associating CC0 with a
23 | Work (the "Affirmer"), to the extent that he or she is an owner of Copyright
24 | and Related Rights in the Work, voluntarily elects to apply CC0 to the Work
25 | and publicly distribute the Work under its terms, with knowledge of his or her
26 | Copyright and Related Rights in the Work and the meaning and intended legal
27 | effect of CC0 on those rights.
28 |
29 | 1. Copyright and Related Rights. A Work made available under CC0 may be
30 | protected by copyright and related or neighboring rights ("Copyright and
31 | Related Rights"). Copyright and Related Rights include, but are not limited
32 | to, the following:
33 |
34 | i. the right to reproduce, adapt, distribute, perform, display, communicate,
35 | and translate a Work;
36 |
37 | ii. moral rights retained by the original author(s) and/or performer(s);
38 |
39 | iii. publicity and privacy rights pertaining to a person's image or likeness
40 | depicted in a Work;
41 |
42 | iv. rights protecting against unfair competition in regards to a Work,
43 | subject to the limitations in paragraph 4(a), below;
44 |
45 | v. rights protecting the extraction, dissemination, use and reuse of data in
46 | a Work;
47 |
48 | vi. database rights (such as those arising under Directive 96/9/EC of the
49 | European Parliament and of the Council of 11 March 1996 on the legal
50 | protection of databases, and under any national implementation thereof,
51 | including any amended or successor version of such directive); and
52 |
53 | vii. other similar, equivalent or corresponding rights throughout the world
54 | based on applicable law or treaty, and any national implementations thereof.
55 |
56 | 2. Waiver. To the greatest extent permitted by, but not in contravention of,
57 | applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and
58 | unconditionally waives, abandons, and surrenders all of Affirmer's Copyright
59 | and Related Rights and associated claims and causes of action, whether now
60 | known or unknown (including existing as well as future claims and causes of
61 | action), in the Work (i) in all territories worldwide, (ii) for the maximum
62 | duration provided by applicable law or treaty (including future time
63 | extensions), (iii) in any current or future medium and for any number of
64 | copies, and (iv) for any purpose whatsoever, including without limitation
65 | commercial, advertising or promotional purposes (the "Waiver"). Affirmer makes
66 | the Waiver for the benefit of each member of the public at large and to the
67 | detriment of Affirmer's heirs and successors, fully intending that such Waiver
68 | shall not be subject to revocation, rescission, cancellation, termination, or
69 | any other legal or equitable action to disrupt the quiet enjoyment of the Work
70 | by the public as contemplated by Affirmer's express Statement of Purpose.
71 |
72 | 3. Public License Fallback. Should any part of the Waiver for any reason be
73 | judged legally invalid or ineffective under applicable law, then the Waiver
74 | shall be preserved to the maximum extent permitted taking into account
75 | Affirmer's express Statement of Purpose. In addition, to the extent the Waiver
76 | is so judged Affirmer hereby grants to each affected person a royalty-free,
77 | non transferable, non sublicensable, non exclusive, irrevocable and
78 | unconditional license to exercise Affirmer's Copyright and Related Rights in
79 | the Work (i) in all territories worldwide, (ii) for the maximum duration
80 | provided by applicable law or treaty (including future time extensions), (iii)
81 | in any current or future medium and for any number of copies, and (iv) for any
82 | purpose whatsoever, including without limitation commercial, advertising or
83 | promotional purposes (the "License"). The License shall be deemed effective as
84 | of the date CC0 was applied by Affirmer to the Work. Should any part of the
85 | License for any reason be judged legally invalid or ineffective under
86 | applicable law, such partial invalidity or ineffectiveness shall not
87 | invalidate the remainder of the License, and in such case Affirmer hereby
88 | affirms that he or she will not (i) exercise any of his or her remaining
89 | Copyright and Related Rights in the Work or (ii) assert any associated claims
90 | and causes of action with respect to the Work, in either case contrary to
91 | Affirmer's express Statement of Purpose.
92 |
93 | 4. Limitations and Disclaimers.
94 |
95 | a. No trademark or patent rights held by Affirmer are waived, abandoned,
96 | surrendered, licensed or otherwise affected by this document.
97 |
98 | b. Affirmer offers the Work as-is and makes no representations or warranties
99 | of any kind concerning the Work, express, implied, statutory or otherwise,
100 | including without limitation warranties of title, merchantability, fitness
101 | for a particular purpose, non infringement, or the absence of latent or
102 | other defects, accuracy, or the present or absence of errors, whether or not
103 | discoverable, all to the greatest extent permissible under applicable law.
104 |
105 | c. Affirmer disclaims responsibility for clearing rights of other persons
106 | that may apply to the Work or any use thereof, including without limitation
107 | any person's Copyright and Related Rights in the Work. Further, Affirmer
108 | disclaims responsibility for obtaining any necessary consents, permissions
109 | or other rights required for any use of the Work.
110 |
111 | d. Affirmer understands and acknowledges that Creative Commons is not a
112 | party to this document and has no duty or obligation with respect to this
113 | CC0 or use of the Work.
114 |
115 | For more information, please see
116 |
117 |
--------------------------------------------------------------------------------
/packages/loader/README.md:
--------------------------------------------------------------------------------
1 | # @typeonly/loader
2 |
3 | [](https://travis-ci.com/paroi-tech/typeonly)
4 | [](https://www.npmjs.com/package/@typeonly/loader)
5 | 
6 | 
7 |
8 | This package is part of **TypeOnly**, a lightweight validation library that uses TypeScript type definitions to validate JSON data. **[Learn more about TypeOnly here](https://www.npmjs.com/package/typeonly)**.
9 |
--------------------------------------------------------------------------------
/packages/loader/bundle-tsd/bundle-tsd.js:
--------------------------------------------------------------------------------
1 | import { readFileSync, writeFileSync } from "node:fs"
2 | import { join } from "node:path"
3 | import { fileURLToPath } from 'node:url';
4 | import { dirname } from 'node:path';
5 |
6 | const __filename = fileURLToPath(import.meta.url);
7 | const __dirname = dirname(__filename);
8 |
9 | const bundleName = "loader"
10 | const srcDir = join(__dirname, "..", "src")
11 | const compiledDir = join(__dirname, "declarations")
12 | const packageDir = join(__dirname, "..")
13 |
14 | try {
15 | writeFileSync(join(packageDir, "dist", `${bundleName}.d.ts`), makeDefinitionsCode())
16 | } catch (err) {
17 | console.log(err.message, err.stack)
18 | }
19 |
20 | function makeDefinitionsCode() {
21 | const defs = [
22 | "// -- API Definitions --",
23 | cleanGeneratedCode(
24 | removeLocalImportsExports((readFileSync(join(compiledDir, "api.d.ts"), "utf-8")).trim()),
25 | ),
26 | "// -- TypeOnly Loader Definitions --",
27 | removeLocalImportsExports((readFileSync(join(srcDir, "typeonly-loader.d.ts"), "utf-8")).trim()),
28 | ]
29 | return defs.join("\n\n")
30 | }
31 |
32 | function removeLocalImportsExports(code) {
33 | const localImportExport = /^\s*(import|export) .* from "\.\/.*"\s*;?\s*$/
34 | return code.split("\n").filter(line => {
35 | return !localImportExport.test(line)
36 | }).join("\n").trim()
37 | }
38 |
39 | function cleanGeneratedCode(code) {
40 | return code.replace(/;/g, "").replace(/ /g, " ")
41 | }
42 |
--------------------------------------------------------------------------------
/packages/loader/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@typeonly/loader",
3 | "version": "1.0.1",
4 | "description": "Brings types and interfaces from TypeScript at runtime.",
5 | "author": "Paroi Team",
6 | "scripts": {
7 | "prepublishOnly": "npm run build && npm run test",
8 | "clear": "rimraf dist/* scripts/declarations/*",
9 | "tsc": "tsc",
10 | "tsc:watch": "tsc --watch",
11 | "bundle-tsd": "node scripts/bundle-tsd.js",
12 | "build": "npm run clear && npm run tsc && npm run bundle-tsd",
13 | "lint": "biome check . --json-formatter-enabled=false --organize-imports-enabled=false",
14 | "test:watch": "vitest",
15 | "test": "vitest run"
16 | },
17 | "devDependencies": {
18 | "@types/node": "22",
19 | "rimraf": "^6.0.1",
20 | "typeonly": "^1.0.1",
21 | "typescript": "^5.7.3",
22 | "vitest": "^3.0.5"
23 | },
24 | "type": "module",
25 | "main": "dist/api.js",
26 | "types": "dist/loader.d.ts",
27 | "repository": {
28 | "type": "git",
29 | "url": "git+https://github.com/paroi-tech/typeonly.git"
30 | },
31 | "homepage": "https://github.com/paroi-tech/typeonly/tree/master/packages/loader",
32 | "license": "CC0-1.0",
33 | "keywords": [
34 | "typescript",
35 | "type",
36 | "interface",
37 | "runtime"
38 | ]
39 | }
--------------------------------------------------------------------------------
/packages/loader/scripts/bundle-tsd.js:
--------------------------------------------------------------------------------
1 | import { readFileSync, writeFileSync } from "node:fs"
2 | import { join } from "node:path"
3 | import { fileURLToPath } from 'node:url';
4 | import { dirname } from 'node:path';
5 |
6 | const __filename = fileURLToPath(import.meta.url);
7 | const __dirname = dirname(__filename);
8 |
9 | const bundleName = "loader"
10 | const srcDir = join(__dirname, "..", "src")
11 | const compiledDir = join(__dirname, "declarations")
12 | const packageDir = join(__dirname, "..")
13 |
14 | try {
15 | writeFileSync(join(packageDir, "dist", `${bundleName}.d.ts`), makeDefinitionsCode())
16 | } catch (err) {
17 | console.log(err.message, err.stack)
18 | }
19 |
20 | function makeDefinitionsCode() {
21 | const defs = [
22 | "// -- API Definitions --",
23 | cleanGeneratedCode(
24 | removeLocalImportsExports((readFileSync(join(compiledDir, "api.d.ts"), "utf-8")).trim()),
25 | ),
26 | "// -- TypeOnly Loader Definitions --",
27 | removeLocalImportsExports((readFileSync(join(srcDir, "typeonly-loader.d.ts"), "utf-8")).trim()),
28 | ]
29 | return defs.join("\n\n")
30 | }
31 |
32 | function removeLocalImportsExports(code) {
33 | const localImportExport = /^\s*(import|export) .* from "\.\/.*"\s*;?\s*$/
34 | return code.split("\n").filter(line => {
35 | return !localImportExport.test(line)
36 | }).join("\n").trim()
37 | }
38 |
39 | function cleanGeneratedCode(code) {
40 | return code.replace(/;/g, "").replace(/ /g, " ")
41 | }
42 |
--------------------------------------------------------------------------------
/packages/loader/src/api.ts:
--------------------------------------------------------------------------------
1 | import { readFile, readdir } from "node:fs/promises";
2 | import { join } from "node:path";
3 | import Project from "./loader/Project.js";
4 | import type { Modules, Type } from "./typeonly-loader.d.ts";
5 |
6 | export type ReadModulesOptions = SyncReadModulesOptions | AsyncReadModulesOptions;
7 |
8 | export interface SyncReadModulesOptions {
9 | /**
10 | * Optional when `"modules"` is defined.
11 | */
12 | modulePaths?: string[];
13 | /**
14 | * Of type: `RtoModules`.
15 | */
16 | bundle: any;
17 | }
18 |
19 | export interface AsyncReadModulesOptions {
20 | /**
21 | * Optional when `"modules"` is defined.
22 | */
23 | modulePaths: string[];
24 | baseDir?: string;
25 | encoding?: BufferEncoding;
26 | rtoModuleProvider?: RtoModuleProvider;
27 | }
28 |
29 | /**
30 | * The returned type is `Promise | RtoModule`.
31 | */
32 | export type RtoModuleProvider = (modulePath: string) => Promise | any;
33 |
34 | export function loadModules(options: SyncReadModulesOptions): Modules;
35 | export function loadModules(options: AsyncReadModulesOptions): Promise;
36 | export function loadModules(options: ReadModulesOptions): any {
37 | if (isSyncReadModulesOptions(options)) return loadModulesSync(options);
38 | return loadModulesAsync(options);
39 | }
40 |
41 | export function isSyncReadModulesOptions(
42 | options: ReadModulesOptions,
43 | ): options is SyncReadModulesOptions {
44 | return !!(options as any).bundle;
45 | }
46 |
47 | function loadModulesSync(options: SyncReadModulesOptions): Modules {
48 | let { modulePaths, bundle } = options;
49 | const rtoModuleProvider = (modulePath: string) => {
50 | const rtoModule = bundle[modulePath];
51 | if (!rtoModule) throw new Error(`Unknown module: ${modulePath}`);
52 | return rtoModule;
53 | };
54 | if (!modulePaths) modulePaths = Object.keys(bundle);
55 | const project = new Project({ rtoModuleProvider });
56 | return project.parseModulesSync(modulePaths);
57 | }
58 |
59 | async function loadModulesAsync(options: AsyncReadModulesOptions): Promise {
60 | let { modulePaths, rtoModuleProvider } = options;
61 | if (rtoModuleProvider) {
62 | if (options.baseDir)
63 | throw new Error("Do not use 'baseDir' with 'rtoModuleProvider' or 'modules'");
64 | if (!modulePaths) throw new Error("Missing parameter 'modulePaths'");
65 | } else {
66 | if (!options.baseDir)
67 | throw new Error("An option 'baseDir', 'rtoModuleProvider' or 'modules' is required");
68 | rtoModuleProvider = makeReadSourceFileRtoModuleProvider({
69 | baseDir: options.baseDir,
70 | encoding: options.encoding || "utf8",
71 | });
72 | if (!modulePaths) modulePaths = await getModulePathsInDir(options.baseDir);
73 | }
74 | const project = new Project({ rtoModuleProvider });
75 | return await project.parseModulesAsync(modulePaths);
76 | }
77 |
78 | async function getModulePathsInDir(dir: string): Promise {
79 | const files = await readdir(dir);
80 | return files
81 | .filter((fileName) => fileName.endsWith(".rto.json"))
82 | .map((fileName) => `./${fileName}`);
83 | }
84 |
85 | function makeReadSourceFileRtoModuleProvider(options: {
86 | baseDir: string;
87 | encoding: BufferEncoding;
88 | }): RtoModuleProvider {
89 | return async (modulePath: string) => {
90 | const { baseDir, encoding } = options;
91 | const data = await readRtoFile(baseDir, modulePath, encoding);
92 | return JSON.parse(data);
93 | };
94 | }
95 |
96 | async function readRtoFile(baseDir: string, modulePath: string, encoding: BufferEncoding) {
97 | const path = join(baseDir, modulePath);
98 | try {
99 | return await readFile(`${path}.rto.json`, encoding);
100 | } catch {
101 | throw new Error(`Cannot open module file: ${path}.rto.json`);
102 | }
103 | }
104 |
105 | export function literals(type: Type, only: "string"): string[];
106 | export function literals(type: Type, only: "number"): number[];
107 | export function literals(type: Type, only: "bigint"): Array;
108 | export function literals(type: Type, only: "boolean"): boolean[];
109 | export function literals(type: Type): Array;
110 | export function literals(type: Type, only?: string): any[] {
111 | let children: Type[];
112 | if (type.kind !== "composite" || type.op !== "union") {
113 | if (type.kind === "literal") children = [type];
114 | else throw new Error("Should be a union");
115 | } else children = type.types;
116 | return children.map((child) => {
117 | if (child.kind !== "literal") throw new Error(`Should be a 'literal': '${type.kind}'`);
118 | if (only && typeof child.literal !== only)
119 | throw new Error(`Literal should be a '${only}': '${typeof child.literal}'`);
120 | return child.literal;
121 | });
122 | }
123 |
--------------------------------------------------------------------------------
/packages/loader/src/helpers/module-path-helpers.ts:
--------------------------------------------------------------------------------
1 | import { dirname } from "node:path";
2 |
3 | export interface RelativeModulePath {
4 | from: string;
5 | relativeToModule?: string;
6 | }
7 |
8 | export interface ToModulePathOptions extends RelativeModulePath {
9 | removeExtensions?: string[];
10 | }
11 |
12 | export function toModulePath(options: ToModulePathOptions): string {
13 | let from = options.removeExtensions
14 | ? removeExtensions(options.from, options.removeExtensions)
15 | : options.from;
16 | if (!from.startsWith("./") && !from.startsWith("../"))
17 | throw new Error("Module path must start with './' or '../'");
18 | let parentDir = options.relativeToModule ? dirname(options.relativeToModule) : ".";
19 | while (true) {
20 | if (from.startsWith("./")) from = from.substr(2);
21 | else if (from.startsWith("../")) {
22 | const newDir = dirname(parentDir);
23 | if (newDir === parentDir) break;
24 | parentDir = newDir;
25 | from = from.substr(3);
26 | } else break;
27 | }
28 | if (from.startsWith("../")) return from;
29 | return `${parentDir}/${from}`;
30 | }
31 |
32 | function removeExtensions(path: string, extensions: string[]): string {
33 | for (const extension of extensions) {
34 | if (path.endsWith(extension)) return path.substr(0, path.length - extension.length);
35 | }
36 | return path;
37 | }
38 |
--------------------------------------------------------------------------------
/packages/loader/src/loader/Project.ts:
--------------------------------------------------------------------------------
1 | import type { RtoModuleProvider } from "../api.js";
2 | import { type RelativeModulePath, toModulePath } from "../helpers/module-path-helpers.js";
3 | import type { Modules } from "../typeonly-loader.d.ts";
4 | import ModuleFactory from "./ModuleFactory.js";
5 |
6 | export type GetModuleFactory = (modulePath: RelativeModulePath) => ModuleFactory;
7 |
8 | export interface ProjectOptions {
9 | rtoModuleProvider: RtoModuleProvider;
10 | }
11 |
12 | export default class Project {
13 | private factories = new Map();
14 |
15 | constructor(private options: ProjectOptions) {}
16 |
17 | parseModulesSync(paths: string[]): Modules {
18 | this.checkPaths(paths);
19 | for (const from of paths) this.loadRtoModuleSync({ from });
20 | return this.createModules();
21 | }
22 |
23 | async parseModulesAsync(paths: string[]): Promise {
24 | this.checkPaths(paths);
25 | for (const from of paths) await this.loadRtoModule({ from });
26 | return this.createModules();
27 | }
28 |
29 | private checkPaths(paths: string[]) {
30 | paths.forEach((path) => {
31 | if (!path.startsWith("./") && !path.startsWith("../"))
32 | throw new Error("A relative path is required for RTO module");
33 | });
34 | }
35 |
36 | private createModules() {
37 | const modules: Modules = {};
38 | for (const factory of this.factories.values()) {
39 | const module = factory.createModule((modulePath) => this.getModuleFactory(modulePath));
40 | if (!module.path) throw new Error("Missing path in module");
41 | modules[module.path] = module;
42 | }
43 | return modules;
44 | }
45 |
46 | private getModuleFactory(relPath: RelativeModulePath): ModuleFactory {
47 | const modulePath = toModulePath({
48 | ...relPath,
49 | removeExtensions: [".rto.json"],
50 | });
51 | const factory = this.factories.get(modulePath);
52 | if (!factory) throw new Error(`Unknown module: ${modulePath}`);
53 | return factory;
54 | }
55 |
56 | private async loadRtoModule(relPath: RelativeModulePath) {
57 | const modulePath = toModulePath({
58 | ...relPath,
59 | removeExtensions: [".rto.json"],
60 | });
61 | let factory = this.factories.get(modulePath);
62 | if (!factory) {
63 | const rtoModule = await this.options.rtoModuleProvider(modulePath);
64 | factory = new ModuleFactory(rtoModule, modulePath);
65 | this.factories.set(modulePath, factory);
66 | await this.loadImports(factory, modulePath);
67 | }
68 | }
69 |
70 | private async loadImports(factory: ModuleFactory, modulePath: string) {
71 | if (factory.rtoModule.imports) {
72 | for (const { from } of factory.rtoModule.imports)
73 | await this.loadRtoModule({ from, relativeToModule: modulePath });
74 | }
75 | if (factory.rtoModule.namespacedImports) {
76 | for (const { from } of factory.rtoModule.namespacedImports)
77 | await this.loadRtoModule({ from, relativeToModule: modulePath });
78 | }
79 | }
80 |
81 | private loadRtoModuleSync(relPath: RelativeModulePath) {
82 | const modulePath = toModulePath({
83 | ...relPath,
84 | removeExtensions: [".rto.json"],
85 | });
86 | let factory = this.factories.get(modulePath);
87 | if (!factory) {
88 | const rtoModule = this.options.rtoModuleProvider(modulePath);
89 | if (rtoModule.then) throw new Error(`Cannot load module '${modulePath}' synchronously`);
90 | factory = new ModuleFactory(rtoModule, modulePath);
91 | this.factories.set(modulePath, factory);
92 | this.loadImportsSync(factory, modulePath);
93 | }
94 | }
95 |
96 | private loadImportsSync(factory: ModuleFactory, modulePath: string) {
97 | if (factory.rtoModule.imports) {
98 | for (const { from } of factory.rtoModule.imports)
99 | this.loadRtoModuleSync({ from, relativeToModule: modulePath });
100 | }
101 | if (factory.rtoModule.namespacedImports) {
102 | for (const { from } of factory.rtoModule.namespacedImports)
103 | this.loadRtoModuleSync({ from, relativeToModule: modulePath });
104 | }
105 | }
106 | }
107 |
--------------------------------------------------------------------------------
/packages/loader/src/typeonly-loader.d.ts:
--------------------------------------------------------------------------------
1 | export interface Modules {
2 | [modulePath: string]: Module;
3 | }
4 |
5 | /**
6 | * TypeOnly Module
7 | */
8 | export interface Module {
9 | path?: string;
10 | imports?: Import[];
11 | namespacedImports?: NamespacedImport[];
12 | namedTypes: NamedTypes;
13 | }
14 |
15 | export interface Import {
16 | from: Module;
17 | namedMembers?: ImportNamedMembers;
18 | }
19 |
20 | export interface ImportNamedMembers {
21 | [as: string]: NamedType;
22 | }
23 |
24 | export interface NamespacedImport {
25 | from: Module;
26 | asNamespace: string;
27 | }
28 |
29 | export interface NamedTypes {
30 | [name: string]: NamedType;
31 | }
32 |
33 | export type NamedType = Type & BaseNamedType;
34 |
35 | export type NamedTypeName = TypeName & BaseNamedType;
36 | export type NamedGenericParameterName = GenericParameterName & BaseNamedType;
37 | export type NamedLocalTypeRef = LocalTypeRef & BaseNamedType;
38 | export type NamedImportedTypeRef = ImportedTypeRef & BaseNamedType;
39 | export type NamedLiteralType = LiteralType & BaseNamedType;
40 | export type NamedCompositeType = CompositeType & BaseNamedType;
41 | export type NamedTupleType = TupleType & BaseNamedType;
42 | export type NamedArrayType = ArrayType & BaseNamedType;
43 | export type NamedGenericInstance = GenericInstance & BaseNamedType;
44 | export type NamedFunctionType = FunctionType & BaseNamedType;
45 | export type NamedKeyofType = KeyofType & BaseNamedType;
46 | export type NamedMemberType = MemberType & BaseNamedType;
47 | export type NamedInterface = Interface & BaseNamedType;
48 |
49 | export type Type =
50 | | TypeName
51 | | GenericParameterName
52 | | LocalTypeRef
53 | | ImportedTypeRef
54 | | LiteralType
55 | | CompositeType
56 | | TupleType
57 | | ArrayType
58 | | GenericInstance
59 | | FunctionType
60 | | KeyofType
61 | | MemberType
62 | | Interface;
63 |
64 | export interface BaseNamedType extends Commentable {
65 | module: Module;
66 | exported: boolean;
67 | name: string;
68 | generic?: GenericParameter[];
69 | }
70 |
71 | export interface GenericParameter {
72 | name: string;
73 | extendsType?: Type;
74 | defaultType?: Type;
75 | }
76 |
77 | export interface TypeName {
78 | kind: "name";
79 | group: "ts" | "primitive" | "global";
80 | refName: SpecialTypeName | PrimitiveTypeName | string;
81 | }
82 |
83 | export type SpecialTypeName = "any" | "unknown" | "object" | "void" | "never";
84 | export type PrimitiveTypeName =
85 | | "string"
86 | | "number"
87 | | "bigint"
88 | | "boolean"
89 | | "undefined"
90 | | "null"
91 | | "symbol";
92 |
93 | export interface GenericParameterName {
94 | kind: "genericParameterName";
95 | genericParameterName: string;
96 | }
97 |
98 | export interface LocalTypeRef {
99 | kind: "localRef";
100 | refName: string;
101 | ref: NamedType;
102 | }
103 |
104 | export interface ImportedTypeRef {
105 | kind: "importedRef";
106 | refName: string;
107 | namespace?: string;
108 | ref: NamedType;
109 | }
110 |
111 | export interface LiteralType {
112 | kind: "literal";
113 | literal: string | number | bigint | boolean;
114 | }
115 |
116 | export interface CompositeType {
117 | kind: "composite";
118 | op: "union" | "intersection";
119 | types: Type[];
120 | }
121 |
122 | export interface TupleType {
123 | kind: "tuple";
124 | itemTypes: Type[];
125 | }
126 |
127 | export interface ArrayType {
128 | kind: "array";
129 | itemType: Type;
130 | }
131 |
132 | export interface GenericInstance {
133 | kind: "genericInstance";
134 | genericName: string;
135 | parameterTypes: Type[];
136 | }
137 |
138 | export interface FunctionType {
139 | kind: "function";
140 | parameters: FunctionParameter[];
141 | returnType: Type;
142 | generic?: GenericParameter[];
143 | }
144 |
145 | export interface FunctionParameter {
146 | name: string;
147 | type: Type;
148 | optional: boolean;
149 | }
150 |
151 | export interface KeyofType {
152 | kind: "keyof";
153 | type: Type;
154 | }
155 |
156 | export interface MemberType {
157 | kind: "member";
158 | parentType: Type;
159 | memberName: string | MemberNameLiteral;
160 | }
161 |
162 | export interface MemberNameLiteral {
163 | literal: string | number;
164 | }
165 |
166 | export interface Interface {
167 | kind: "interface";
168 | indexSignature?: IndexSignature;
169 | mappedIndexSignature?: MappedIndexSignature;
170 | properties?: Properties;
171 | }
172 |
173 | export interface Properties {
174 | [name: string]: Property;
175 | }
176 |
177 | export interface Property extends Commentable {
178 | of: Interface;
179 | name: string;
180 | type: Type;
181 | optional: boolean;
182 | readonly: boolean;
183 | }
184 |
185 | export interface IndexSignature extends Commentable {
186 | of: Interface;
187 | keyName: string;
188 | keyType: "string" | "number";
189 | type: Type;
190 | optional: boolean;
191 | readonly: boolean;
192 | }
193 |
194 | export interface MappedIndexSignature extends Commentable {
195 | of: Interface;
196 | keyName: string;
197 | keyInType: Type;
198 | type: Type;
199 | optional: boolean;
200 | readonly: boolean;
201 | }
202 |
203 | export interface Commentable {
204 | /**
205 | * A multiline string.
206 | */
207 | docComment?: string;
208 | }
209 |
--------------------------------------------------------------------------------
/packages/loader/tests/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "strict": true,
4 | "target": "ES2022",
5 | "module": "NodeNext",
6 | "moduleResolution": "nodenext",
7 | "sourceMap": false,
8 | "lib": ["esnext"],
9 | "noEmit": true
10 | },
11 | "include": ["."]
12 | }
13 |
--------------------------------------------------------------------------------
/packages/loader/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "strict": true,
4 | "target": "ES2022",
5 | "module": "NodeNext",
6 | "moduleResolution": "nodenext",
7 | "sourceMap": false,
8 | "lib": ["esnext"],
9 | "outDir": "dist",
10 | "noEmitOnError": true,
11 | "declaration": true,
12 | "declarationDir": "scripts/declarations"
13 | },
14 | "include": ["src"],
15 | "exclude": ["node_modules", "dist"]
16 | }
17 |
--------------------------------------------------------------------------------
/packages/loader/vitest.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "vitest/config";
2 |
3 | export default defineConfig({
4 | test: {
5 | include: ["**/src/**/*.spec.ts", "**/tests/**/*.spec.ts"],
6 | environment: "node",
7 | },
8 | });
9 |
--------------------------------------------------------------------------------
/packages/typeonly/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | ._*
3 |
4 | node_modules
5 | package-lock.json
6 | .npmrc
7 |
8 | /dist
9 | /scripts/declarations
10 |
11 | /.antlr
12 | /antlr-*.jar
13 | antlr-parser
14 |
15 | !/tests/import-tests/**/node_modules
--------------------------------------------------------------------------------
/packages/typeonly/.npmignore:
--------------------------------------------------------------------------------
1 | /scripts/declarations
2 |
3 | /.antlr
4 | /antlr-*.jar
--------------------------------------------------------------------------------
/packages/typeonly/.prettierignore:
--------------------------------------------------------------------------------
1 | *
--------------------------------------------------------------------------------
/packages/typeonly/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # typeonly
2 |
3 | ## 1.0.1
4 |
5 | ### Patch Changes
6 |
7 | - 9759c6a: Documentation
8 |
9 | ## 1.0.0
10 |
11 | ### Major Changes
12 |
13 | - ESM modules
14 |
15 | ## 0.5.0
16 |
17 | ### Minor Changes
18 |
19 | - upgrade dependencies & syntax
20 |
--------------------------------------------------------------------------------
/packages/typeonly/LICENSE:
--------------------------------------------------------------------------------
1 | CC0 1.0 Universal
2 |
3 | Statement of Purpose
4 |
5 | The laws of most jurisdictions throughout the world automatically confer
6 | exclusive Copyright and Related Rights (defined below) upon the creator and
7 | subsequent owner(s) (each and all, an "owner") of an original work of
8 | authorship and/or a database (each, a "Work").
9 |
10 | Certain owners wish to permanently relinquish those rights to a Work for the
11 | purpose of contributing to a commons of creative, cultural and scientific
12 | works ("Commons") that the public can reliably and without fear of later
13 | claims of infringement build upon, modify, incorporate in other works, reuse
14 | and redistribute as freely as possible in any form whatsoever and for any
15 | purposes, including without limitation commercial purposes. These owners may
16 | contribute to the Commons to promote the ideal of a free culture and the
17 | further production of creative, cultural and scientific works, or to gain
18 | reputation or greater distribution for their Work in part through the use and
19 | efforts of others.
20 |
21 | For these and/or other purposes and motivations, and without any expectation
22 | of additional consideration or compensation, the person associating CC0 with a
23 | Work (the "Affirmer"), to the extent that he or she is an owner of Copyright
24 | and Related Rights in the Work, voluntarily elects to apply CC0 to the Work
25 | and publicly distribute the Work under its terms, with knowledge of his or her
26 | Copyright and Related Rights in the Work and the meaning and intended legal
27 | effect of CC0 on those rights.
28 |
29 | 1. Copyright and Related Rights. A Work made available under CC0 may be
30 | protected by copyright and related or neighboring rights ("Copyright and
31 | Related Rights"). Copyright and Related Rights include, but are not limited
32 | to, the following:
33 |
34 | i. the right to reproduce, adapt, distribute, perform, display, communicate,
35 | and translate a Work;
36 |
37 | ii. moral rights retained by the original author(s) and/or performer(s);
38 |
39 | iii. publicity and privacy rights pertaining to a person's image or likeness
40 | depicted in a Work;
41 |
42 | iv. rights protecting against unfair competition in regards to a Work,
43 | subject to the limitations in paragraph 4(a), below;
44 |
45 | v. rights protecting the extraction, dissemination, use and reuse of data in
46 | a Work;
47 |
48 | vi. database rights (such as those arising under Directive 96/9/EC of the
49 | European Parliament and of the Council of 11 March 1996 on the legal
50 | protection of databases, and under any national implementation thereof,
51 | including any amended or successor version of such directive); and
52 |
53 | vii. other similar, equivalent or corresponding rights throughout the world
54 | based on applicable law or treaty, and any national implementations thereof.
55 |
56 | 2. Waiver. To the greatest extent permitted by, but not in contravention of,
57 | applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and
58 | unconditionally waives, abandons, and surrenders all of Affirmer's Copyright
59 | and Related Rights and associated claims and causes of action, whether now
60 | known or unknown (including existing as well as future claims and causes of
61 | action), in the Work (i) in all territories worldwide, (ii) for the maximum
62 | duration provided by applicable law or treaty (including future time
63 | extensions), (iii) in any current or future medium and for any number of
64 | copies, and (iv) for any purpose whatsoever, including without limitation
65 | commercial, advertising or promotional purposes (the "Waiver"). Affirmer makes
66 | the Waiver for the benefit of each member of the public at large and to the
67 | detriment of Affirmer's heirs and successors, fully intending that such Waiver
68 | shall not be subject to revocation, rescission, cancellation, termination, or
69 | any other legal or equitable action to disrupt the quiet enjoyment of the Work
70 | by the public as contemplated by Affirmer's express Statement of Purpose.
71 |
72 | 3. Public License Fallback. Should any part of the Waiver for any reason be
73 | judged legally invalid or ineffective under applicable law, then the Waiver
74 | shall be preserved to the maximum extent permitted taking into account
75 | Affirmer's express Statement of Purpose. In addition, to the extent the Waiver
76 | is so judged Affirmer hereby grants to each affected person a royalty-free,
77 | non transferable, non sublicensable, non exclusive, irrevocable and
78 | unconditional license to exercise Affirmer's Copyright and Related Rights in
79 | the Work (i) in all territories worldwide, (ii) for the maximum duration
80 | provided by applicable law or treaty (including future time extensions), (iii)
81 | in any current or future medium and for any number of copies, and (iv) for any
82 | purpose whatsoever, including without limitation commercial, advertising or
83 | promotional purposes (the "License"). The License shall be deemed effective as
84 | of the date CC0 was applied by Affirmer to the Work. Should any part of the
85 | License for any reason be judged legally invalid or ineffective under
86 | applicable law, such partial invalidity or ineffectiveness shall not
87 | invalidate the remainder of the License, and in such case Affirmer hereby
88 | affirms that he or she will not (i) exercise any of his or her remaining
89 | Copyright and Related Rights in the Work or (ii) assert any associated claims
90 | and causes of action with respect to the Work, in either case contrary to
91 | Affirmer's express Statement of Purpose.
92 |
93 | 4. Limitations and Disclaimers.
94 |
95 | a. No trademark or patent rights held by Affirmer are waived, abandoned,
96 | surrendered, licensed or otherwise affected by this document.
97 |
98 | b. Affirmer offers the Work as-is and makes no representations or warranties
99 | of any kind concerning the Work, express, implied, statutory or otherwise,
100 | including without limitation warranties of title, merchantability, fitness
101 | for a particular purpose, non infringement, or the absence of latent or
102 | other defects, accuracy, or the present or absence of errors, whether or not
103 | discoverable, all to the greatest extent permissible under applicable law.
104 |
105 | c. Affirmer disclaims responsibility for clearing rights of other persons
106 | that may apply to the Work or any use thereof, including without limitation
107 | any person's Copyright and Related Rights in the Work. Further, Affirmer
108 | disclaims responsibility for obtaining any necessary consents, permissions
109 | or other rights required for any use of the Work.
110 |
111 | d. Affirmer understands and acknowledges that Creative Commons is not a
112 | party to this document and has no duty or obligation with respect to this
113 | CC0 or use of the Work.
114 |
115 | For more information, please see
116 |
117 |
--------------------------------------------------------------------------------
/packages/typeonly/README.md:
--------------------------------------------------------------------------------
1 | # TypeOnly
2 |
3 | [](https://travis-ci.com/paroi-tech/typeonly)
4 | [](https://www.npmjs.com/package/typeonly)
5 | 
6 | [](https://github.com/paroi-tech/typeonly)
7 |
8 | A lightweight library for validating JSON data using TypeScript type definitions.
9 |
10 | ## Getting Started
11 |
12 | ```sh
13 | npm install typeonly --save-dev
14 | npm install @typeonly/validator
15 | ```
16 |
17 | Add an entry to the `"scripts"` section of your `package.json` (this example uses a `typeonly/` directory for input and a `dist/` directory for output):
18 |
19 | ```json
20 | "typeonly": "typeonly --bundle dist/types.to.json -s ./typeonly"
21 | ```
22 |
23 | Create a type file with a `.d.ts` extension:
24 |
25 | ```ts
26 | // typeonly/my-types.d.ts
27 |
28 | export type Task = RedTask | BlueTask;
29 |
30 | export interface RedTask {
31 | color: "red";
32 | priority: number | "max";
33 | label: string;
34 | }
35 |
36 | export interface BlueTask {
37 | color: "blue";
38 | label: string;
39 | }
40 | ```
41 |
42 | _Important: TypeOnly uses its own parser and implements only a subset of TypeScript. Specifically, generics and template string types are not supported._
43 |
44 | Generate the type bundle by running:
45 |
46 | ```sh
47 | npm run typeonly
48 | ```
49 |
50 | A new file `dist/types.to.json` will be generated containing all the type definitions from the input directory. Being a JSON file, the parser is not needed at runtime.
51 |
52 | ### Validating JSON Data (ESM Version)
53 |
54 | ```js
55 | import { readFileSync } from "node:fs";
56 | import { createValidator } from "@typeonly/validator";
57 |
58 | const typeValidator = createValidator({
59 | bundle: JSON.parse(readFileSync("./dist/types.to.json", "utf-8")),
60 | });
61 |
62 | const result1 = typeValidator.validate("Task", {
63 | color: "red",
64 | priority: 2,
65 | label: "My urgent task"
66 | });
67 |
68 | console.log(result1); // { valid: true }
69 |
70 | const result2 = typeValidator.validate("Task", {
71 | color: "red",
72 | priority: "abc",
73 | label: "My urgent task"
74 | });
75 |
76 | console.log(result2); // { valid: false, error: 'Value...' }
77 | ```
78 |
79 | The error message looks like:
80 |
81 | ```
82 | Value {color: "red", priority: "abc", label: "My urgent ta…"} is not conform to Task: no matching type in: RedTask | BlueTask.
83 | In type 'RedTask', value {color: "red", priority: "abc", label: "My urgent ta…"} is not conform to RedTask.
84 | In property 'priority', value '"abc"' is not conform to union: no matching type in: number | "max".
85 | ```
86 |
87 | ### Validating JSON Data (CommonJS Version)
88 |
89 | ```js
90 | const { readFileSync } = require("node:fs");
91 |
92 | async function main() {
93 | const { createValidator } = await import("@typeonly/validator");
94 |
95 | // … same code as in the ESM version …
96 | }
97 |
98 | main();
99 | ```
100 |
101 | ## Command Line Interface
102 |
103 | Compile a typing source file:
104 |
105 | ```sh
106 | npx typeonly --bundle dist/types.to.json --source-dir types
107 | ```
108 |
109 | This command generates a compiled file `dist/types.to.json`.
110 |
111 | Available options:
112 |
113 | ```
114 | -h, --help Print this help message.
115 | -o, --output-dir directory The output directory (optional).
116 | -s, --source-dir directory The source directory (optional when used with option --ast or with a single source file).
117 | -e, --encoding string Encoding for input and output file(s) (default is utf8).
118 | -b, --bundle string Generate a bundle file for RTO data (optional).
119 | --prettify Prettify RTO files (optional).
120 | --ast Generate AST files instead of RTO files (optional).
121 | --src file ... Input files to process (by default at last position).
122 | ```
123 |
124 | ## Using as a Library
125 |
126 | Install as a dependency:
127 |
128 | ```sh
129 | npm install typeonly --save-dev
130 | ```
131 |
132 | Then, use it:
133 |
134 | ```js
135 | import { generateRtoModules } from "typeonly";
136 |
137 | const bundle = await generateRtoModules({
138 | modulePaths: ["./file-name"],
139 | readFiles: {
140 | sourceDir: `../types`
141 | },
142 | returnRtoModules: true
143 | }).catch(console.log);
144 | ```
145 |
--------------------------------------------------------------------------------
/packages/typeonly/TODO.md:
--------------------------------------------------------------------------------
1 |
2 | * Additional properties with an index signature must be conform to the index signature.
3 |
--------------------------------------------------------------------------------
/packages/typeonly/TypeOnlyParser.g4:
--------------------------------------------------------------------------------
1 | parser grammar TypeOnlyParser;
2 |
3 | options {
4 | tokenVocab = TypeOnlyLexer;
5 | }
6 |
7 | declarations: typeSep? declaration* EOF;
8 |
9 | typeSep: (NL | SEMI_COLON)+;
10 |
11 | declaration:
12 | importDecl (typeSep | EOF)
13 | | namedInterface typeSep?
14 | | namedType (typeSep | EOF);
15 |
16 | /*
17 | * Import
18 | */
19 | importDecl: classicImport | namespacedImport;
20 |
21 | classicImport:
22 | IMPORT TYPE? NL* (namedImportContent NL* FROM NL*)? STRING_LITERAL;
23 |
24 | namedImportContent:
25 | OPEN_BRACE NL* namedMember (NL* COMMA NL* namedMember)* NL* CLOSE_BRACE;
26 |
27 | namedMember: IDENTIFIER (NL* AS NL* IDENTIFIER)?;
28 |
29 | namespacedImport:
30 | IMPORT NL* STAR NL* AS NL* IDENTIFIER NL* FROM NL* STRING_LITERAL;
31 |
32 | /*
33 | * NamedInterface
34 | */
35 | namedInterface:
36 | (EXPORT NL*)? INTERFACE IDENTIFIER (NL* genericParameters)? (
37 | NL* interfaceExtends
38 | )? NL* anonymousInterface;
39 |
40 | interfaceExtends:
41 | EXTENDS NL* typeName (NL* COMMA NL* typeName)*;
42 |
43 | anonymousInterface:
44 | OPEN_BRACE (NL* interfaceEntries)? CLOSE_BRACE;
45 |
46 | interfaceEntries:
47 | interfaceEntry (propertySeparator interfaceEntry)* propertySeparator?;
48 |
49 | interfaceEntry:
50 | indexSignature
51 | | property
52 | | functionProperty
53 | | mappedIndexSignature;
54 |
55 | property:
56 | (READONLY NL*)? propertyName (NL* QUESTION_MARK)? NL* COLON NL* aType;
57 |
58 | functionProperty:
59 | (READONLY NL*)? propertyName (NL* QUESTION_MARK)? NL* (
60 | NL* genericParameters
61 | )? OPEN_PARENTHESE (
62 | NL* functionParameter (NL* COMMA NL* functionParameter)*
63 | )? NL* CLOSE_PARENTHESE (NL* COLON NL* aType)?;
64 |
65 | /*
66 | * IndexSignature and MappedIndexSignature
67 | */
68 | indexSignature:
69 | (READONLY NL*)? OPEN_BRACKET IDENTIFIER COLON signatureType CLOSE_BRACKET (
70 | NL* QUESTION_MARK
71 | )? COLON aType;
72 | signatureType: STRING | NUMBER;
73 | mappedIndexSignature:
74 | (READONLY NL*)? OPEN_BRACKET IDENTIFIER IN aType CLOSE_BRACKET (
75 | NL* QUESTION_MARK
76 | )? COLON aType;
77 |
78 | propertySeparator: (NL+ (propertyExplicitSeparator NL*)?) | (propertyExplicitSeparator NL*);
79 | propertyExplicitSeparator: (SEMI_COLON (NL* SEMI_COLON)*) | COMMA;
80 |
81 | propertyName: IDENTIFIER | JS_KEYWORD | typeOnlyKeywords;
82 | typeOnlyKeywords:
83 | INTERFACE
84 | | TYPE
85 | | EXPORT
86 | | EXTENDS
87 | | READONLY
88 | | KEYOF
89 | | STRING
90 | | NUMBER
91 | | IN
92 | | AS
93 | | FROM
94 | | IMPORT;
95 |
96 | typeName: IDENTIFIER | IDENTIFIER DOT IDENTIFIER;
97 |
98 | /*
99 | * NamedType
100 | */
101 | namedType: (EXPORT NL*)? TYPE IDENTIFIER NL* (
102 | NL* genericParameters
103 | )? ASSIGN NL* aType;
104 |
105 | /*
106 | * Common rules for NamedInterface and NamedType
107 | */
108 | aType:
109 | inlineImportType
110 | | typeName
111 | | signatureType
112 | | literal
113 | | tupleType
114 | | memberParentType = aType OPEN_BRACKET memberName CLOSE_BRACKET
115 | | arrayItemType = aType NL* OPEN_BRACKET NL* CLOSE_BRACKET
116 | | KEYOF aType
117 | | anonymousInterface
118 | | typeWithParenthesis
119 | | aType NL* INTERSECTION NL* aType
120 | | INTERSECTION NL* aType
121 | | aType NL* UNION NL* aType
122 | | UNION NL* aType
123 | | genericInstance
124 | | (NL* genericParameters)? OPEN_PARENTHESE (
125 | NL* functionParameter (NL* COMMA NL* functionParameter)*
126 | )? NL* CLOSE_PARENTHESE NL* ARROW NL* returnType = aType;
127 |
128 | memberName: STRING_LITERAL | INTEGER_LITERAL | IDENTIFIER;
129 |
130 | genericParameters: LESS_THAN genericParameter+ MORE_THAN;
131 | genericParameter:
132 | IDENTIFIER (EXTENDS extendsType = aType)? (
133 | ASSIGN defaultType = aType
134 | )?;
135 | genericInstance:
136 | typeName NL* LESS_THAN NL* aType (NL* COMMA NL* aType)* NL* MORE_THAN;
137 | inlineImportType:
138 | IMPORT OPEN_PARENTHESE stringLiteral CLOSE_PARENTHESE DOT IDENTIFIER;
139 |
140 | stringLiteral: STRING_LITERAL; // | TEMPLATE_STRING_LITERAL
141 |
142 | tupleType:
143 | OPEN_BRACKET (NL* aType (NL* COMMA NL* aType)*)? NL* CLOSE_BRACKET;
144 | typeWithParenthesis:
145 | OPEN_PARENTHESE NL* aType NL* CLOSE_PARENTHESE;
146 | functionParameter: IDENTIFIER (NL* QUESTION_MARK)? (NL* COLON NL* aType)?;
147 |
148 | /*
149 | * Literal
150 | */
151 | literal:
152 | STRING_LITERAL
153 | // | TEMPLATE_STRING_LITERAL
154 | | BOOLEAN_LITERAL
155 | | BIG_INT_LITERAL
156 | | INTEGER_LITERAL
157 | | DECIMAL_LITERAL
158 | | HEX_INTEGER_LITERAL
159 | | OCTAL_INTEGER_LITERAL
160 | | BINARY_INTEGER_LITERAL;
161 |
--------------------------------------------------------------------------------
/packages/typeonly/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "typeonly",
3 | "version": "1.0.1",
4 | "description": "Parses types and interfaces from TypeScript and stores them as JSON files.",
5 | "author": "Paroi",
6 | "scripts": {
7 | "prepublishOnly": "npm run lint && npm run build && npm run test",
8 | "antlr": "java -cp \"antlr-4.13.2-complete.jar\" org.antlr.v4.Tool -Dlanguage=JavaScript -o antlr-parser TypeOnlyParser.g4 TypeOnlyLexer.g4",
9 | "clear": "rimraf dist/* scripts/declarations/* antlr-parser/*",
10 | "tsc": "tsc",
11 | "tsc:watch": "tsc --watch",
12 | "bundle-tsd": "node scripts/bundle-tsd",
13 | "build": "npm run clear && npm run antlr && npm run tsc && npm run bundle-tsd",
14 | "lint": "biome check . --json-formatter-enabled=false --organize-imports-enabled=false",
15 | "test:watch": "vitest",
16 | "test": "vitest run"
17 | },
18 | "dependencies": {
19 | "antlr4": "4.13.2",
20 | "command-line-args": "^6.0.1",
21 | "command-line-usage": "^7.0.3"
22 | },
23 | "devDependencies": {
24 | "@types/command-line-args": "^5.2.3",
25 | "@types/command-line-usage": "^5.0.4",
26 | "@types/node": "22",
27 | "rimraf": "^6.0.1",
28 | "typescript": "^5.7.3",
29 | "vitest": "^3.0.5"
30 | },
31 | "type": "module",
32 | "main": "dist/api.js",
33 | "types": "dist/typeonly.d.ts",
34 | "bin": "./dist/cli.js",
35 | "repository": {
36 | "type": "git",
37 | "url": "git+https://github.com/paroi-tech/typeonly.git"
38 | },
39 | "homepage": "https://github.com/paroi-tech/typeonly/tree/master/packages/typeonly",
40 | "license": "CC0-1.0",
41 | "keywords": [
42 | "typescript",
43 | "type",
44 | "interface",
45 | "runtime"
46 | ]
47 | }
48 |
--------------------------------------------------------------------------------
/packages/typeonly/scripts/bundle-tsd.js:
--------------------------------------------------------------------------------
1 | import { readFileSync, writeFileSync } from "node:fs"
2 | import { join } from "node:path"
3 | import { fileURLToPath } from 'node:url';
4 | import { dirname } from 'node:path';
5 |
6 | const __filename = fileURLToPath(import.meta.url);
7 | const __dirname = dirname(__filename);
8 |
9 | const bundleName = "typeonly"
10 | const srcDir = join(__dirname, "..", "src")
11 | const compiledDir = join(__dirname, "declarations")
12 | const packageDir = join(__dirname, "..")
13 |
14 | try {
15 | writeFileSync(join(packageDir, "dist", `${bundleName}.d.ts`), makeDefinitionsCode())
16 | } catch (err) {
17 | console.log(err.message, err.stack)
18 | }
19 |
20 | function makeDefinitionsCode() {
21 | const defs = [
22 | "// -- API definitions --",
23 | cleanGeneratedCode(
24 | removeLocalImportsExports((readFileSync(join(compiledDir, "api.d.ts"), "utf-8")).trim()),
25 | ),
26 | "// -- RTO definitions --",
27 | removeLocalImportsExports((readFileSync(join(srcDir, "rto.d.ts"), "utf-8")).trim()),
28 | "// -- AST definitions --",
29 | removeLocalImportsExports((readFileSync(join(srcDir, "ast.d.ts"), "utf-8")).trim()),
30 | ]
31 | return defs.join("\n\n")
32 | }
33 |
34 | function removeLocalImportsExports(code) {
35 | const localImportExport = /^\s*(import|export) .* from "\.\/.*"\s*;?\s*$/
36 | return code.split("\n").filter(line => {
37 | return !localImportExport.test(line)
38 | }).join("\n").trim()
39 | }
40 |
41 | function cleanGeneratedCode(code) {
42 | return code.replace(/;/g, "").replace(/ /g, " ")
43 | }
44 |
--------------------------------------------------------------------------------
/packages/typeonly/src/api.ts:
--------------------------------------------------------------------------------
1 | import type { TypeOnlyAst } from "./ast.d.ts";
2 | import { parseTypeOnlyToAst } from "./parser/parse-typeonly.js";
3 | import {
4 | RtoProjectOutput,
5 | type WriteRtoFilesOptions,
6 | makeReadSourceFileAstProvider,
7 | } from "./rto-factory/ProjectInputOutput.js";
8 | import RtoModuleFactory from "./rto-factory/RtoModuleFactory.js";
9 | import RtoProject from "./rto-factory/RtoProject.js";
10 | import type { RtoModule, RtoModules } from "./rto.d.ts";
11 |
12 | export interface ParseTypeOnlyOptions {
13 | source: string;
14 | return?: {
15 | /**
16 | * Default value is `false`.
17 | */
18 | freeze?: boolean;
19 | };
20 | }
21 |
22 | export function parseTypeOnly(options: ParseTypeOnlyOptions): TypeOnlyAst {
23 | const ast = parseTypeOnlyToAst(options.source);
24 | if (options.return?.freeze) deepFreezePojo(ast);
25 | return ast;
26 | }
27 |
28 | export interface GenerateRtoModulesOptions {
29 | modulePaths: string[];
30 | readFiles?: {
31 | sourceDir: string;
32 | encoding?: BufferEncoding;
33 | };
34 | astProvider?: TypeOnlyAstProvider;
35 | defineGlobals?: (globals: Set) => Set;
36 | writeFiles?:
37 | | boolean
38 | | {
39 | encoding?: BufferEncoding;
40 | outputDir?: string;
41 | /**
42 | * The indentation parameter of `JSON.stringify`.
43 | */
44 | prettify?: number | string;
45 | };
46 | returnRtoModules?:
47 | | boolean
48 | | {
49 | /**
50 | * Default value is `false`.
51 | */
52 | freeze?: boolean;
53 | };
54 | }
55 |
56 | export type TypeOnlyAstProvider = (modulePath: string) => Promise | TypeOnlyAst;
57 |
58 | export async function generateRtoModules(
59 | options: GenerateRtoModulesOptions,
60 | ): Promise {
61 | let astProvider = options.astProvider;
62 | if (!astProvider) {
63 | if (!options.readFiles)
64 | throw new Error("A parameter 'readFiles' or 'astProvider' is required.");
65 | astProvider = makeReadSourceFileAstProvider(
66 | options.readFiles.sourceDir,
67 | options.readFiles.encoding ?? "utf8",
68 | );
69 | }
70 | let wfOpt2: WriteRtoFilesOptions | undefined;
71 | if (options.writeFiles) {
72 | const wfOpt1 = options.writeFiles === true ? {} : options.writeFiles;
73 | const encoding = wfOpt1.encoding || "utf8";
74 | const outputDir =
75 | wfOpt1.outputDir ||
76 | (!options.astProvider && options.readFiles && options.readFiles.sourceDir);
77 | if (!outputDir)
78 | throw new Error(
79 | "Option 'writeFiles.outputDir' is required when 'readFiles.sourceDir' is not provided.",
80 | );
81 | wfOpt2 = {
82 | encoding,
83 | outputDir,
84 | prettify: wfOpt1.prettify,
85 | };
86 | }
87 | const output = new RtoProjectOutput({
88 | returnRtoModules: !!options.returnRtoModules,
89 | writeFiles: wfOpt2,
90 | });
91 | const project = new RtoProject({
92 | astProvider,
93 | rtoModuleListener: output.listener,
94 | moduleFactoryOptions: {
95 | defineGlobals: options.defineGlobals,
96 | },
97 | });
98 | await project.addModules(options.modulePaths);
99 | if (options.returnRtoModules) {
100 | const rtoModules = output.getRtoModules();
101 | if (options.returnRtoModules !== true && options.returnRtoModules.freeze)
102 | deepFreezePojo(rtoModules);
103 | return rtoModules;
104 | }
105 | }
106 |
107 | export interface CreateStandaloneRtoModuleOptions {
108 | ast: TypeOnlyAst;
109 | defineGlobals?: (globals: Set) => Set;
110 | /**
111 | * Default value is `false`.
112 | */
113 | freeze?: boolean;
114 | }
115 |
116 | export function createStandaloneRtoModule(options: CreateStandaloneRtoModuleOptions): RtoModule {
117 | const factory = new RtoModuleFactory(options.ast, undefined, {
118 | defineGlobals: options.defineGlobals,
119 | });
120 | const rtoModule = factory.createRtoModule();
121 | if (options.freeze) deepFreezePojo(rtoModule);
122 | return rtoModule;
123 | }
124 |
125 | function deepFreezePojo(object: T): T {
126 | if (Object.isFrozen(object)) return object;
127 | Object.freeze(object);
128 | for (const key of Object.keys(object)) {
129 | const value = (object as any)[key];
130 | if (value && typeof value === "object") deepFreezePojo(value);
131 | }
132 | return object;
133 | }
134 |
--------------------------------------------------------------------------------
/packages/typeonly/src/ast.d.ts:
--------------------------------------------------------------------------------
1 | export interface TypeOnlyAst {
2 | declarations?: AstDeclaration[];
3 | }
4 |
5 | export type AstDeclaration = AstImport | AstNamedInterface | AstNamedType | AstStandaloneComment;
6 |
7 | export type AstImport = AstClassicImport | AstNamespacedImport;
8 |
9 | export interface AstClassicImport extends AstCommentable {
10 | whichDeclaration: "import";
11 | whichImport: "classic";
12 | from: string;
13 | namedMembers?: AstImportNamedMember[];
14 | }
15 |
16 | export interface AstImportNamedMember {
17 | name: string;
18 | as?: string;
19 | }
20 |
21 | export interface AstNamespacedImport extends AstCommentable {
22 | whichDeclaration: "import";
23 | whichImport: "namespaced";
24 | from: string;
25 | asNamespace: string;
26 | }
27 |
28 | export interface AstInterface {
29 | whichType: "interface";
30 | entries?: AstInterfaceEntry[];
31 | }
32 |
33 | export interface AstNamedInterface extends AstInterface, AstCommentable {
34 | whichDeclaration: "interface";
35 | name: string;
36 | exported?: boolean;
37 | extends?: string[];
38 | generic?: AstGenericParameter[];
39 | }
40 |
41 | export type AstInterfaceEntry =
42 | | AstProperty
43 | | AstFunctionProperty
44 | | AstIndexSignature
45 | | AstMappedIndexSignature
46 | | AstStandaloneInterfaceComment;
47 |
48 | export interface AstProperty extends AstCommentable {
49 | whichEntry: "property";
50 | optional?: boolean;
51 | readonly?: boolean;
52 | name: string;
53 | type: AstType;
54 | }
55 |
56 | export interface AstFunctionProperty extends AstCommentable {
57 | whichEntry: "functionProperty";
58 | optional?: boolean;
59 | readonly?: boolean;
60 | name: string;
61 | parameters?: AstFunctionParameter[];
62 | returnType?: AstType;
63 | generic?: AstGenericParameter[];
64 | }
65 |
66 | export interface AstIndexSignature extends AstCommentable {
67 | whichEntry: "indexSignature";
68 | keyName: string;
69 | keyType: "string" | "number";
70 | type: AstType;
71 | optional?: boolean;
72 | readonly?: boolean;
73 | }
74 |
75 | export interface AstMappedIndexSignature extends AstCommentable {
76 | whichEntry: "mappedIndexSignature";
77 | keyName: string;
78 | keyInType: AstType;
79 | type: AstType;
80 | optional?: boolean;
81 | readonly?: boolean;
82 | }
83 |
84 | export interface AstNamedType extends AstCommentable {
85 | whichDeclaration: "type";
86 | name: string;
87 | type: AstType;
88 | exported?: boolean;
89 | generic?: AstGenericParameter[];
90 | }
91 |
92 | export type AstType =
93 | | string
94 | | AstLiteralType
95 | | AstInterface
96 | | AstCompositeType
97 | | AstTupleType
98 | | AstArrayType
99 | | AstGenericInstance
100 | | AstFunctionType
101 | | AstKeyofType
102 | | AstMemberType
103 | | AstInlineImportType;
104 |
105 | export interface AstLiteralType {
106 | whichType: "literal";
107 | literal: string | number | boolean | bigint;
108 | stringDelim?: '"' | "'";
109 | }
110 |
111 | export interface AstCompositeType {
112 | whichType: "composite";
113 | op: "union" | "intersection";
114 | types: AstType[];
115 | }
116 |
117 | export interface AstTupleType {
118 | whichType: "tuple";
119 | itemTypes?: AstType[];
120 | }
121 |
122 | export interface AstArrayType {
123 | whichType: "array";
124 | itemType: AstType;
125 | genericSyntax?: boolean;
126 | }
127 |
128 | export interface AstGenericInstance {
129 | whichType: "genericInstance";
130 | genericName: string;
131 | parameterTypes: AstType[];
132 | }
133 |
134 | export interface AstFunctionType {
135 | whichType: "function";
136 | parameters?: AstFunctionParameter[];
137 | returnType: AstType;
138 | generic?: AstGenericParameter[];
139 | }
140 |
141 | export interface AstFunctionParameter {
142 | name: string;
143 | optional?: boolean;
144 | type?: AstType;
145 | }
146 |
147 | export interface AstKeyofType {
148 | whichType: "keyof";
149 | type: AstType;
150 | }
151 |
152 | export interface AstMemberType {
153 | whichType: "member";
154 | parentType: AstType;
155 | memberName: string | AstMemberNameLiteral;
156 | }
157 |
158 | export interface AstMemberNameLiteral {
159 | literal: string | number;
160 | stringDelim?: '"' | "'";
161 | }
162 |
163 | export interface AstInlineImportType {
164 | whichType: "inlineImport";
165 | from: string;
166 | exportedName: string;
167 | }
168 |
169 | export interface AstGenericParameter {
170 | name: string;
171 | extendsType?: AstType;
172 | defaultType?: AstType;
173 | }
174 |
175 | export interface AstStandaloneComment {
176 | whichDeclaration: "comment";
177 | /**
178 | * A multiline string.
179 | */
180 | text: string;
181 | syntax: "inline" | "classic";
182 | }
183 |
184 | export interface AstStandaloneInterfaceComment {
185 | whichEntry: "comment";
186 | /**
187 | * A multiline string.
188 | */
189 | text: string;
190 | syntax: "inline" | "classic";
191 | }
192 |
193 | export interface AstCommentable {
194 | /**
195 | * A multiline string.
196 | */
197 | docComment?: string;
198 | inlineComments?: AstInlineComment[];
199 | }
200 |
201 | export interface AstInlineComment {
202 | /**
203 | * A single line string.
204 | */
205 | text: string;
206 | syntax: "inline" | "classic";
207 | }
208 |
--------------------------------------------------------------------------------
/packages/typeonly/src/cli.ts:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 | import { readFileSync, readdirSync, writeFileSync } from "node:fs";
3 | import { basename, dirname, join, parse } from "node:path";
4 | import commandLineArgs from "command-line-args";
5 | import commandLineUsage from "command-line-usage";
6 | import { generateRtoModules, parseTypeOnly } from "./api.js";
7 | import type { TypeOnlyAst } from "./ast.d.ts";
8 | import { ensureDirectory } from "./rto-factory/ProjectInputOutput.js";
9 | import type { RtoModules } from "./rto.d.ts";
10 |
11 | process.on("uncaughtException", (err) => {
12 | console.error("uncaughtException", err);
13 | process.exit(1);
14 | });
15 |
16 | process.on("unhandledRejection", (err) => {
17 | console.trace("unhandledRejection", err);
18 | process.exit(1);
19 | });
20 |
21 | class InvalidArgumentError extends Error {
22 | readonly causeCode = "invalidArgument";
23 | }
24 |
25 | type OptionDefinition = commandLineUsage.OptionDefinition & commandLineArgs.OptionDefinition;
26 |
27 | const optionDefinitions: OptionDefinition[] = [
28 | {
29 | name: "help",
30 | alias: "h",
31 | type: Boolean,
32 | description: "Print this help message.",
33 | },
34 | {
35 | name: "output-dir",
36 | alias: "o",
37 | type: String,
38 | description: "The output directory (optional).",
39 | typeLabel: "{underline directory}",
40 | },
41 | {
42 | name: "source-dir",
43 | alias: "s",
44 | type: String,
45 | description:
46 | "The source directory (optional when used with option {underline --ast} or with a single source file).",
47 | typeLabel: "{underline directory}",
48 | },
49 | {
50 | name: "encoding",
51 | alias: "e",
52 | type: String,
53 | description: "Encoding for input and output file(s) (default is {underline utf8}).",
54 | },
55 | {
56 | name: "bundle",
57 | alias: "b",
58 | type: String,
59 | description: "Generate a bundle file for RTO data (optional).",
60 | },
61 | {
62 | name: "prettify",
63 | type: Boolean,
64 | description: "Prettify RTO files (optional).",
65 | },
66 | {
67 | name: "ast",
68 | type: Boolean,
69 | description: "Generate AST files instead of RTO files (optional).",
70 | },
71 | {
72 | name: "src",
73 | description: "Input files to process (by default at last position).",
74 | type: String,
75 | multiple: true,
76 | defaultOption: true,
77 | typeLabel: "{underline file} ...",
78 | },
79 | ];
80 |
81 | cli().catch((error) => {
82 | console.error(`Error: ${error.message}`, error);
83 | });
84 |
85 | async function cli() {
86 | const options = parseOptions();
87 | if (!options) return;
88 | if (options.help) {
89 | printHelp();
90 | return;
91 | }
92 |
93 | try {
94 | await processFiles(options);
95 | } catch (error: any) {
96 | if (error.causeCode === "invalidArgument") {
97 | console.error(`Error: ${error.message}`);
98 | printHelp();
99 | } else throw error;
100 | }
101 | }
102 |
103 | function printHelp() {
104 | const sections = [
105 | {
106 | header: "TypeOnly",
107 | content: "TypeOnly Parser.",
108 | },
109 | {
110 | header: "Synopsis",
111 | content: [
112 | "$ npx typeonly {bold --source-dir} {underline src/} {underline file-name.d.ts}",
113 | "$ npx typeonly {bold --help}",
114 | ],
115 | },
116 | {
117 | header: "Options",
118 | optionList: optionDefinitions,
119 | },
120 | {
121 | content: "Project home: {underline https://github.com/paroi-tech/typeonly}",
122 | },
123 | ];
124 | const usage = commandLineUsage(sections);
125 | console.log(usage);
126 | }
127 |
128 | interface OptionsObject {
129 | [name: string]: unknown;
130 | }
131 |
132 | function parseOptions(): OptionsObject | undefined {
133 | try {
134 | return commandLineArgs(optionDefinitions);
135 | } catch (error: any) {
136 | console.log(`Error: ${error.message}`);
137 | printHelp();
138 | }
139 | }
140 |
141 | async function processFiles(options: OptionsObject) {
142 | if (options.ast) {
143 | if (!options.src || !Array.isArray(options.src))
144 | throw new InvalidArgumentError("Missing source file(s).");
145 | options.src.map((file: string) => createAstJsonFile(file, options));
146 | } else await createRtoJsonFiles(options);
147 | }
148 |
149 | function createAstJsonFile(file: string, options: OptionsObject) {
150 | const f = options["source-dir"] ? join(options["source-dir"] as string, file) : file;
151 | const encoding: BufferEncoding = (options.encoding as BufferEncoding) ?? "utf8";
152 | let source: string;
153 | try {
154 | source = readFileSync(f, { encoding });
155 | } catch (err) {
156 | throw new InvalidArgumentError(`Cannot read file: ${f}`);
157 | }
158 |
159 | let fileName = basename(f);
160 | if (fileName.endsWith(".ts"))
161 | fileName = fileName.substring(0, fileName.length - (fileName.endsWith(".d.ts") ? 5 : 3));
162 |
163 | const ast: TypeOnlyAst = parseTypeOnly({ source });
164 | const outFile = join(
165 | (options["output-dir"] as string | undefined) ?? dirname(f),
166 | `${fileName}.ast.json`,
167 | );
168 | writeFileSync(outFile, JSON.stringify(ast, undefined, "\t"), encoding);
169 | }
170 |
171 | async function createRtoJsonFiles(options: OptionsObject) {
172 | let srcList = (options.src as string[] | undefined) ?? [];
173 | let sourceDir: string;
174 | if (!options["source-dir"]) {
175 | if (srcList.length === 1) sourceDir = dirname(srcList[0]);
176 | else throw new Error("Missing 'source-dir' option.");
177 | } else sourceDir = options["source-dir"] as string;
178 | sourceDir = normalizeDir(sourceDir);
179 |
180 | if (srcList.length === 0) srcList = getTypingFilesInDir(sourceDir);
181 |
182 | const modulePaths = normalizeModulePaths(srcList, sourceDir);
183 | const encoding: BufferEncoding = (options.encoding as BufferEncoding | undefined) ?? "utf8";
184 | const prettify = options.prettify ? "\t" : undefined;
185 | let bundleName = options.bundle as string | undefined;
186 |
187 | if (bundleName) {
188 | if (!bundleName.endsWith(".to.json")) bundleName += ".to.json";
189 | let outputDir = options["output-dir"]
190 | ? normalizeDir(options["output-dir"] as string)
191 | : undefined;
192 | const parsed = parse(bundleName);
193 | if (parsed.dir) {
194 | outputDir = outputDir ? join(outputDir, parsed.dir) : parsed.dir;
195 | bundleName = parsed.base;
196 | }
197 | if (outputDir) {
198 | await ensureDirectory(outputDir);
199 | bundleName = join(outputDir, bundleName);
200 | }
201 | const rtoModules = (await generateRtoModules({
202 | modulePaths,
203 | readFiles: {
204 | sourceDir,
205 | encoding,
206 | },
207 | returnRtoModules: true,
208 | })) as RtoModules;
209 | writeFileSync(bundleName, JSON.stringify(rtoModules, undefined, prettify), { encoding });
210 | } else {
211 | const outputDir = normalizeDir((options["output-dir"] as string | undefined) ?? sourceDir);
212 | await generateRtoModules({
213 | modulePaths,
214 | readFiles: {
215 | sourceDir,
216 | encoding,
217 | },
218 | writeFiles: {
219 | outputDir,
220 | prettify,
221 | },
222 | });
223 | }
224 | }
225 |
226 | function getTypingFilesInDir(dir: string): string[] {
227 | const files = readdirSync(dir);
228 | return files.filter((fileName) => fileName.endsWith(".d.ts"));
229 | }
230 |
231 | function normalizeModulePaths(files: string[], sourceDir: string): string[] {
232 | const prefix = `${sourceDir}/`.replace(/\\/g, "/");
233 | return files.map((file) => {
234 | let f = file.replace(/\\/g, "/");
235 | if (f.startsWith(prefix)) f = `./${f.substring(prefix.length)}`;
236 | else if (!f.startsWith("./") && !f.startsWith("../")) f = `./${f}`;
237 | return f;
238 | });
239 | }
240 |
241 | function normalizeDir(path: string): string {
242 | return path.replace(/\\/g, "/").replace(/\/+$/, "");
243 | }
244 |
--------------------------------------------------------------------------------
/packages/typeonly/src/helpers/fs-utils.ts:
--------------------------------------------------------------------------------
1 | import { type MakeDirectoryOptions, existsSync, mkdirSync, promises } from "node:fs";
2 |
3 | const { access } = promises;
4 |
5 | export async function pathExists(path: string) {
6 | try {
7 | await access(path);
8 | return true;
9 | } catch {
10 | return false;
11 | }
12 | }
13 |
14 | export function ensureDirSync(dir: string, options: MakeDirectoryOptions = {}) {
15 | if (!existsSync(dir)) mkdirSync(dir, options);
16 | }
17 |
--------------------------------------------------------------------------------
/packages/typeonly/src/helpers/js-lib.ts:
--------------------------------------------------------------------------------
1 | export function assertExists(val: T | undefined): asserts val is T {
2 | if (val === undefined) throw new Error("missing value");
3 | }
4 |
--------------------------------------------------------------------------------
/packages/typeonly/src/helpers/module-path-helpers.ts:
--------------------------------------------------------------------------------
1 | import { readFile } from "node:fs/promises";
2 | import { dirname, join, normalize, resolve } from "node:path";
3 | import { pathExists } from "./fs-utils.js";
4 |
5 | export interface RelativeModulePath {
6 | from: string;
7 | relativeToModule?: string;
8 | }
9 |
10 | export interface ToModulePathOptions extends RelativeModulePath {
11 | removeExtensions?: string[];
12 | }
13 |
14 | export function toModulePath(options: ToModulePathOptions): string {
15 | if (parseExternalModuleName(options.from)) return options.from;
16 | let from = options.removeExtensions
17 | ? removeExtensions(options.from, options.removeExtensions)
18 | : options.from;
19 | if (!from.startsWith("./") && !from.startsWith("../"))
20 | throw new Error("Module path must start with './' or '../'");
21 | let parentDir = options.relativeToModule ? dirname(options.relativeToModule) : ".";
22 | while (true) {
23 | if (from.startsWith("./")) from = from.substr(2);
24 | else if (from.startsWith("../")) {
25 | const newDir = dirname(parentDir);
26 | if (newDir === parentDir) break;
27 | parentDir = newDir;
28 | from = from.substr(3);
29 | } else break;
30 | }
31 | if (from.startsWith("../")) return normalize(from);
32 | const result = `${parentDir}/${from}`;
33 | if (result.startsWith("./")) return `./${normalize(result)}`;
34 | return normalize(result);
35 | }
36 |
37 | export interface ParsedExternalModuleName {
38 | packageName: string;
39 | additionalPath?: string;
40 | }
41 |
42 | export function parseExternalModuleName(modulePath: string): ParsedExternalModuleName | undefined {
43 | const result = /^((?:@[a-z0-9-~][a-z0-9-\._~]*\/)?[a-z0-9-~][a-z0-9-\._~]*)(\/.+)?$/.exec(
44 | modulePath,
45 | );
46 | if (!result) return;
47 | return {
48 | packageName: result[1],
49 | additionalPath: result[2],
50 | };
51 | }
52 |
53 | export async function getExternalModulePath(
54 | parsed: ParsedExternalModuleName,
55 | sourceDir: string,
56 | ): Promise {
57 | // console.log("==>", parsed, sourceDir)
58 | const mainPackageDir = await findParentPackageDir(sourceDir);
59 | // console.log("==. packageDir", mainPackageDir)
60 | if (!mainPackageDir) throw new Error(`Cannot find the package directory of '${sourceDir}'`);
61 |
62 | const depPackageDir = join(mainPackageDir, "node_modules", parsed.packageName);
63 |
64 | if (parsed.additionalPath) return join(depPackageDir, parsed.additionalPath);
65 |
66 | const content = JSON.parse(await readFile(join(depPackageDir, "package.json"), "utf8"));
67 | const additionalPath = content.types ?? content.typing;
68 | if (additionalPath) return join(depPackageDir, removeModuleNameExtension(additionalPath));
69 | return join(depPackageDir, "index");
70 | }
71 |
72 | function removeModuleNameExtension(fileName: string): string {
73 | if (!fileName.endsWith(".ts")) return fileName;
74 | return fileName.substring(0, fileName.length - (fileName.endsWith(".d.ts") ? 5 : 3));
75 | }
76 |
77 | async function findParentPackageDir(dir: string): Promise {
78 | let resolvedDir = resolve(dir);
79 | while (true) {
80 | if (await pathExists(join(resolvedDir, "package.json"))) return resolvedDir;
81 | if (resolvedDir === "/" || resolvedDir === ".") return;
82 | resolvedDir = dirname(resolvedDir);
83 | }
84 | }
85 |
86 | function removeExtensions(path: string, extensions: string[]): string {
87 | for (const extension of extensions) {
88 | if (path.endsWith(extension)) return path.substr(0, path.length - extension.length);
89 | }
90 | return path;
91 | }
92 |
--------------------------------------------------------------------------------
/packages/typeonly/src/parser/antlr4-defs.d.ts:
--------------------------------------------------------------------------------
1 | export interface AntlrParser {
2 | ruleNames: { [type: number]: string };
3 | symbolicNames: { [type: number]: string };
4 | }
5 |
6 | export interface AntlrTokenStream {
7 | tokens: AntlrToken[];
8 | // getHiddenTokensToLeft(tokenIndex: number, channel: number): AntlrToken[] | null
9 | // getHiddenTokensToRight(tokenIndex: number, channel: number): AntlrToken[] | null
10 | }
11 |
12 | export type ParseTree = AntlrRuleContext | AntlrTerminalNode;
13 |
14 | export interface AntlrRuleContext {
15 | [childName: string]: any;
16 | parentCtx: AntlrRuleContext;
17 | parser: AntlrParser;
18 | ruleIndex: number;
19 | start: AntlrToken;
20 | stop: AntlrToken;
21 | getText(): string;
22 | getChild(index: number): ParseTree | null;
23 | getChildCount(): number;
24 | }
25 |
26 | export type AntlrAnyRuleContext = AntlrRuleContext & {
27 | [childrenName: string]: any;
28 | };
29 |
30 | export interface AntlrTerminalNode {
31 | parentCtx: AntlrRuleContext;
32 | symbol: AntlrToken;
33 | getText(): string;
34 | }
35 |
36 | export interface AntlrToken {
37 | tokenIndex: number;
38 | channel: number;
39 | type: number;
40 | start: number;
41 | stop: number;
42 | line: number;
43 | column: number;
44 | }
45 |
--------------------------------------------------------------------------------
/packages/typeonly/src/parser/parse-typeonly.ts:
--------------------------------------------------------------------------------
1 | import antlr4, { CommonTokenStream, InputStream, type Recognizer } from "antlr4";
2 | import AstExtractor from "./AstExtractor.js";
3 |
4 | const TypeOnlyLexer = (await import("../../antlr-parser/TypeOnlyLexer.js" as string)).default;
5 | const TypeOnlyParser = (await import("../../antlr-parser/TypeOnlyParser.js" as string)).default;
6 |
7 | export function parseTypeOnlyToAst(source: string) {
8 | const chars = new InputStream(source);
9 | const lexer = new TypeOnlyLexer(chars);
10 | const tokenStream = new CommonTokenStream(lexer);
11 | const parser = new TypeOnlyParser(tokenStream);
12 |
13 | parser.buildParseTrees = true;
14 |
15 | // console.log(debugTokensToText(tokenStream.tokens))
16 | // function debugTokensToText(tokens) {
17 | // if (!tokens)
18 | // return "-no-tokens-"
19 | // return tokens.map(({ tokenIndex, type, start, stop }) => {
20 | // return `[${tokenIndex}] ${type}: ${source.substring(start, stop + 1).replace(/\n/g, "\u23ce")}`
21 | // }).join("\n")
22 | // }
23 |
24 | const errors: string[] = [];
25 | const errorListener = {
26 | syntaxError(
27 | _recognizer: any,
28 | _offendingSymbol: any,
29 | line: number,
30 | column: number,
31 | msg: string,
32 | _e: any,
33 | ) {
34 | errors.push(`Syntax error at line ${line}:${column}, ${msg}`);
35 | },
36 |
37 | reportAmbiguity(
38 | recognizer: Recognizer,
39 | dfa: any,
40 | startIndex: number,
41 | stopIndex: number,
42 | exact: boolean,
43 | ambigAlts: any,
44 | configs: any,
45 | ) {},
46 |
47 | reportAttemptingFullContext(
48 | recognizer: Recognizer,
49 | dfa: any,
50 | startIndex: number,
51 | stopIndex: number,
52 | conflictingAlts: any,
53 | configs: any,
54 | ) {},
55 |
56 | reportContextSensitivity(
57 | recognizer: Recognizer,
58 | dfa: any,
59 | startIndex: number,
60 | stopIndex: number,
61 | prediction: number,
62 | configs: any,
63 | ) {},
64 | };
65 | lexer.removeErrorListeners();
66 | lexer.addErrorListener(errorListener);
67 | parser.removeErrorListeners();
68 | parser.addErrorListener(errorListener);
69 |
70 | const treeRoot = parser.declarations();
71 |
72 | if (errors.length > 0) throw new Error(errors.join("\n"));
73 |
74 | const extractor = new AstExtractor({
75 | source,
76 | tokenStream,
77 | tokenTypes: {
78 | SEMICOLON: TypeOnlyParser.SEMI_COLON,
79 | COMMA: TypeOnlyParser.COMMA,
80 | MULTILINE_COMMENT: TypeOnlyParser.MULTILINE_COMMENT,
81 | SINGLE_LINE_COMMENT: TypeOnlyParser.SINGLE_LINE_COMMENT,
82 | NEWLINE: TypeOnlyParser.NL,
83 | },
84 | });
85 |
86 | (antlr4 as any).tree.ParseTreeWalker.DEFAULT.walk(extractor, treeRoot);
87 |
88 | if (!extractor.ast) throw new Error("missing AST");
89 |
90 | return extractor.ast;
91 | }
92 |
--------------------------------------------------------------------------------
/packages/typeonly/src/rto-factory/AstImportTool.ts:
--------------------------------------------------------------------------------
1 | import type { AstImport, AstInlineImportType } from "../ast.d.ts";
2 | import type { RtoImport, RtoNamespacedImport } from "../rto.d.ts";
3 | import type RtoModuleFactory from "./RtoModuleFactory.js";
4 | import type { RtoModuleLoader } from "./internal-types.js";
5 |
6 | export interface ImportRef {
7 | refName: string;
8 | namespace?: string;
9 | }
10 |
11 | interface ImportedFromModule {
12 | namedMembers: Array<{ name: string; as?: string }>;
13 | namespaces: string[];
14 | inlineMembers: Set;
15 | }
16 |
17 | export default class AstImportTool {
18 | private fromModules = new Map();
19 | private importedIdentifiers = new Set();
20 | private importedNamespaces = new Set();
21 |
22 | constructor(
23 | readonly path: string,
24 | private moduleLoader: RtoModuleLoader,
25 | ) {}
26 |
27 | addImport(astNode: AstImport) {
28 | const ifm = this.getImportedFromModule(astNode.from);
29 | if (astNode.whichImport === "namespaced") this.addNamespacedImport(astNode.asNamespace, ifm);
30 | else {
31 | if (astNode.namedMembers) {
32 | for (const member of astNode.namedMembers) this.addNamedImport({ ...member }, ifm);
33 | }
34 | }
35 | }
36 |
37 | addInlineImport(astNode: AstInlineImportType) {
38 | const ifm = this.getImportedFromModule(astNode.from);
39 | ifm.inlineMembers.add(astNode.exportedName);
40 | }
41 |
42 | async load() {
43 | for (const [from, ifm] of this.fromModules.entries()) {
44 | const factory = await this.moduleLoader({ from, relativeToModule: this.path });
45 | this.checkExportedNames(ifm, factory);
46 | }
47 | }
48 |
49 | createRtoImports(): { imports?: RtoImport[]; namespacedImports?: RtoNamespacedImport[] } {
50 | const imports: RtoImport[] = [];
51 | const namespacedImports: RtoNamespacedImport[] = [];
52 | for (const [from, ifm] of this.fromModules.entries()) {
53 | const { namedMembers, namespaces } = ifm;
54 | if (namedMembers.length > 0) {
55 | imports.push({
56 | from,
57 | namedMembers,
58 | });
59 | }
60 | namespaces.forEach((asNamespace) => namespacedImports.push({ from, asNamespace }));
61 | }
62 | const result: { imports?: RtoImport[]; namespacedImports?: RtoNamespacedImport[] } = {};
63 | if (imports.length > 0) result.imports = imports;
64 | if (namespacedImports.length > 0) result.namespacedImports = namespacedImports;
65 | return result;
66 | }
67 |
68 | findImportedMember(fullName: string): ImportRef | undefined {
69 | const dotIndex = fullName.indexOf(".");
70 | if (dotIndex !== -1) {
71 | const refName = fullName.slice(0, dotIndex);
72 | const namespace = fullName.slice(dotIndex + 1);
73 | if (!this.importedNamespaces.has(namespace))
74 | throw new Error(`Unknown namespace: ${namespace}`);
75 | return { refName, namespace };
76 | }
77 | return this.importedIdentifiers.has(fullName) ? { refName: fullName } : undefined;
78 | }
79 |
80 | inlineImport({ exportedName }: AstInlineImportType): ImportRef {
81 | return { refName: exportedName };
82 | }
83 |
84 | private addNamespacedImport(asNamespace: string, ifm: ImportedFromModule) {
85 | if (this.importedNamespaces.has(asNamespace) || this.importedIdentifiers.has(asNamespace))
86 | throw new Error(`Duplicate identifier '${asNamespace}'`);
87 | this.importedNamespaces.add(asNamespace);
88 | ifm.namespaces.push(asNamespace);
89 | }
90 |
91 | private addNamedImport(member: { name: string; as?: string }, ifm: ImportedFromModule) {
92 | const name = member.as || member.name;
93 | if (this.importedNamespaces.has(name) || this.importedIdentifiers.has(name))
94 | throw new Error(`Duplicate identifier '${name}'`);
95 | this.importedIdentifiers.add(name);
96 | ifm.namedMembers.push({ ...member });
97 | }
98 |
99 | private getImportedFromModule(from: string): ImportedFromModule {
100 | let ifm = this.fromModules.get(from);
101 | if (!ifm) {
102 | ifm = {
103 | namedMembers: [],
104 | namespaces: [],
105 | inlineMembers: new Set(),
106 | };
107 | this.fromModules.set(from, ifm);
108 | }
109 | return ifm;
110 | }
111 |
112 | private checkExportedNames(ifm: ImportedFromModule, factory: RtoModuleFactory) {
113 | for (const { name } of ifm.namedMembers) {
114 | if (!factory.hasExportedNamedType(name))
115 | throw new Error(`Module '${factory.getModulePath()}' has no exported member '${name}'.`);
116 | }
117 | for (const name of ifm.inlineMembers) {
118 | if (!factory.hasExportedNamedType(name))
119 | throw new Error(`Module '${factory.getModulePath()}' has no exported member '${name}'.`);
120 | }
121 | }
122 | }
123 |
--------------------------------------------------------------------------------
/packages/typeonly/src/rto-factory/InlineImportScanner.ts:
--------------------------------------------------------------------------------
1 | import type {
2 | AstArrayType,
3 | AstCompositeType,
4 | AstFunctionParameter,
5 | AstFunctionProperty,
6 | AstFunctionType,
7 | AstGenericInstance,
8 | AstGenericParameter,
9 | AstIndexSignature,
10 | AstInlineImportType,
11 | AstInterface,
12 | AstKeyofType,
13 | AstMappedIndexSignature,
14 | AstMemberType,
15 | AstProperty,
16 | AstTupleType,
17 | AstType,
18 | } from "../ast.d.ts";
19 | import type AstImportTool from "./AstImportTool.js";
20 |
21 | export default class InlineImportScanner {
22 | private typeScans: {
23 | [K in Exclude["whichType"]]: (astNode: any) => void;
24 | } = {
25 | inlineImport: (astNode) => this.registerInlineImport(astNode),
26 | literal: () => {},
27 | array: (astNode) => this.scanArrayType(astNode),
28 | tuple: (astNode) => this.scanTupleType(astNode),
29 | keyof: (astNode) => this.scanKeyofType(astNode),
30 | member: (astNode) => this.scanMemberType(astNode),
31 | composite: (astNode) => this.scanCompositeType(astNode),
32 | genericInstance: (astNode) => this.scanGenericInstance(astNode),
33 | function: (astNode) => this.scanFunctionType(astNode),
34 | interface: (astNode) => this.scanInterface(astNode),
35 | };
36 |
37 | constructor(private importTool: AstImportTool) {}
38 |
39 | scan(astNode: AstType): void {
40 | if (typeof astNode === "string") return;
41 | const scan = this.typeScans[astNode.whichType];
42 | if (!scan) throw new Error(`Unexpected whichType to scan: ${astNode.whichType}`);
43 | scan(astNode);
44 | }
45 |
46 | private registerInlineImport(astNode: AstInlineImportType) {
47 | this.importTool.addInlineImport(astNode);
48 | }
49 |
50 | private scanArrayType(astNode: AstArrayType) {
51 | this.scan(astNode.itemType);
52 | }
53 |
54 | private scanCompositeType(astNode: AstCompositeType) {
55 | astNode.types.forEach((child) => this.scan(child));
56 | }
57 |
58 | private scanGenericInstance(astNode: AstGenericInstance) {
59 | astNode.parameterTypes.forEach((child) => this.scan(child));
60 | }
61 |
62 | private scanKeyofType(astNode: AstKeyofType) {
63 | this.scan(astNode.type);
64 | }
65 |
66 | private scanMemberType(astNode: AstMemberType) {
67 | this.scan(astNode.parentType);
68 | }
69 |
70 | private scanTupleType(astNode: AstTupleType) {
71 | if (astNode.itemTypes) astNode.itemTypes.forEach((child) => this.scan(child));
72 | }
73 |
74 | private scanFunctionType(astNode: AstFunctionType) {
75 | this.scan(astNode.returnType);
76 | if (astNode.parameters) this.scanFunctionParameters(astNode.parameters);
77 | if (astNode.generic) this.scanGenericParameters(astNode.generic);
78 | }
79 |
80 | private scanFunctionParameters(astNodes: AstFunctionParameter[]) {
81 | return astNodes.forEach(({ type }) => {
82 | if (type) this.scan(type);
83 | });
84 | }
85 |
86 | private scanGenericParameters(astNodes: AstGenericParameter[]) {
87 | return astNodes.forEach(({ extendsType, defaultType }) => {
88 | if (extendsType) this.scan(extendsType);
89 | if (defaultType) this.scan(defaultType);
90 | });
91 | }
92 |
93 | private scanInterface(astNode: AstInterface) {
94 | if (!astNode.entries) return;
95 | for (const entry of astNode.entries) {
96 | if (entry.whichEntry === "indexSignature") this.scanIndexSignature(entry);
97 | else if (entry.whichEntry === "mappedIndexSignature") this.scanMappedIndexSignature(entry);
98 | else if (entry.whichEntry === "property") this.scanProperty(entry);
99 | else if (entry.whichEntry === "functionProperty")
100 | this.scanPropertyFromFunctionProperty(entry);
101 | }
102 | }
103 |
104 | private scanProperty(entry: AstProperty) {
105 | this.scan(entry.type);
106 | }
107 |
108 | private scanPropertyFromFunctionProperty(entry: AstFunctionProperty) {
109 | if (entry.parameters) this.scanFunctionParameters(entry.parameters);
110 | if (entry.returnType) this.scan(entry.returnType);
111 | if (entry.generic) this.scanGenericParameters(entry.generic);
112 | }
113 |
114 | private scanIndexSignature(entry: AstIndexSignature) {
115 | this.scan(entry.type);
116 | }
117 |
118 | private scanMappedIndexSignature(entry: AstMappedIndexSignature) {
119 | this.scan(entry.keyInType);
120 | this.scan(entry.type);
121 | }
122 | }
123 |
--------------------------------------------------------------------------------
/packages/typeonly/src/rto-factory/ProjectInputOutput.ts:
--------------------------------------------------------------------------------
1 | import { stat } from "node:fs";
2 | import { mkdir, readFile, writeFile } from "node:fs/promises";
3 | import { dirname, join } from "node:path";
4 | import type { TypeOnlyAstProvider } from "../api.js";
5 | import { getExternalModulePath, parseExternalModuleName } from "../helpers/module-path-helpers.js";
6 | import { parseTypeOnlyToAst } from "../parser/parse-typeonly.js";
7 | import type { RtoModule, RtoModules } from "../rto.js";
8 | import type { RtoModuleListener } from "./RtoProject.js";
9 |
10 | export interface RtoProjectOutputOptions {
11 | writeFiles?: WriteRtoFilesOptions;
12 | returnRtoModules: boolean;
13 | }
14 |
15 | export interface WriteRtoFilesOptions {
16 | encoding: BufferEncoding;
17 | outputDir: string;
18 | /**
19 | * The indentation parameter of `JSON.stringify`.
20 | */
21 | prettify?: number | string;
22 | }
23 |
24 | export class RtoProjectOutput {
25 | readonly listener: RtoModuleListener;
26 |
27 | private modules = new Map();
28 |
29 | constructor(options: RtoProjectOutputOptions) {
30 | this.listener = async (module: RtoModule, modulePath: string) => {
31 | if (options.writeFiles) await this.writeModuleFile(module, modulePath, options.writeFiles);
32 | if (options.returnRtoModules) this.modules.set(modulePath, module);
33 | };
34 | }
35 |
36 | getRtoModules(): RtoModules {
37 | const result: RtoModules = {};
38 | for (const [modulePath, module] of this.modules) result[modulePath] = module;
39 | return result;
40 | }
41 |
42 | private async writeModuleFile(
43 | module: RtoModule,
44 | modulePath: string,
45 | options: WriteRtoFilesOptions,
46 | ) {
47 | const data = JSON.stringify(module, undefined, options.prettify);
48 | const outputFile = `${join(options.outputDir, modulePath)}.rto.json`;
49 | await ensureDirectory(options.outputDir);
50 | await ensureDirectory(dirname(outputFile), { createIntermediate: true });
51 | await writeFile(outputFile, data, { encoding: options.encoding });
52 | }
53 | }
54 |
55 | export function makeReadSourceFileAstProvider(
56 | sourceDir: string,
57 | encoding: BufferEncoding,
58 | ): TypeOnlyAstProvider {
59 | return async (modulePath: string) => {
60 | const source = await readModuleFile(sourceDir, modulePath, encoding);
61 | return parseTypeOnlyToAst(source);
62 | };
63 | }
64 |
65 | async function readModuleFile(sourceDir: string, modulePath: string, encoding: BufferEncoding) {
66 | const parsedExternalModule = parseExternalModuleName(modulePath);
67 | const path = parsedExternalModule
68 | ? await getExternalModulePath(parsedExternalModule, sourceDir)
69 | : join(sourceDir, modulePath);
70 | try {
71 | return await readFile(`${path}.d.ts`, encoding);
72 | } catch {}
73 | try {
74 | return await readFile(`${path}.ts`, encoding);
75 | } catch {}
76 | throw new Error(`Cannot open module file: ${path}.d.ts`);
77 | }
78 |
79 | export async function ensureDirectory(path: string, { createIntermediate = false } = {}) {
80 | if (!(await fsExists(path))) await mkdir(path, { recursive: createIntermediate });
81 | }
82 |
83 | function fsExists(path: string): Promise {
84 | return new Promise((resolve) => {
85 | stat(path, (error) => resolve(!error));
86 | });
87 | }
88 |
--------------------------------------------------------------------------------
/packages/typeonly/src/rto-factory/RtoProject.ts:
--------------------------------------------------------------------------------
1 | import type { TypeOnlyAstProvider } from "../api.js";
2 | import { type RelativeModulePath, toModulePath } from "../helpers/module-path-helpers.js";
3 | import type { RtoModule } from "../rto.d.ts";
4 | import RtoModuleFactory, { type RtoModuleFactoryOptions } from "./RtoModuleFactory.js";
5 |
6 | export interface RtoProjectOptions {
7 | astProvider: TypeOnlyAstProvider;
8 | rtoModuleListener: RtoModuleListener;
9 | moduleFactoryOptions?: RtoModuleFactoryOptions;
10 | }
11 |
12 | export type RtoModuleListener = (module: RtoModule, modulePath: string) => Promise | void;
13 |
14 | export default class RtoProject {
15 | private factories = new Map();
16 |
17 | constructor(private options: RtoProjectOptions) {}
18 |
19 | async addModules(modulePaths: string[]) {
20 | for (const from of modulePaths) await this.importModule({ from });
21 | for (const factory of this.factories.values())
22 | await factory.loadImports((modulePath) => this.importModule(modulePath));
23 | for (const factory of this.factories.values()) {
24 | const module = factory.createRtoModule();
25 | await this.options.rtoModuleListener(module, factory.getModulePath());
26 | }
27 | }
28 |
29 | private async importModule(relPath: RelativeModulePath): Promise {
30 | const modulePath = toModulePath({
31 | ...relPath,
32 | removeExtensions: [".d.ts", ".ts"],
33 | });
34 | let factory = this.factories.get(modulePath);
35 | if (!factory) {
36 | const ast = await this.options.astProvider(modulePath);
37 | factory = new RtoModuleFactory(ast, modulePath, this.options.moduleFactoryOptions);
38 | this.factories.set(modulePath, factory);
39 | }
40 | return factory;
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/packages/typeonly/src/rto-factory/internal-types.d.ts:
--------------------------------------------------------------------------------
1 | import type { RelativeModulePath } from "../helpers/module-path-helpers.js";
2 | import type RtoModuleFactory from "./RtoModuleFactory.d.ts";
3 |
4 | export type RtoModuleLoader = (modulePath: RelativeModulePath) => Promise;
5 |
--------------------------------------------------------------------------------
/packages/typeonly/src/rto.d.ts:
--------------------------------------------------------------------------------
1 | export interface RtoModules {
2 | [modulePath: string]: RtoModule;
3 | }
4 |
5 | /**
6 | * Raw TypeOnly Module
7 | */
8 | export interface RtoModule {
9 | imports?: RtoImport[];
10 | namespacedImports?: RtoNamespacedImport[];
11 | namedTypes?: RtoNamedType[];
12 | }
13 |
14 | export interface RtoImport {
15 | from: string;
16 | namedMembers?: RtoImportNamedMember[];
17 | }
18 |
19 | export interface RtoImportNamedMember {
20 | name: string;
21 | as?: string;
22 | }
23 |
24 | export interface RtoNamespacedImport {
25 | from: string;
26 | asNamespace: string;
27 | }
28 |
29 | export type RtoNamedType = RtoType & RtoBaseNamedType;
30 |
31 | export type RtoNamedTypeName = RtoTypeName & RtoBaseNamedType;
32 | export type RtoNamedGenericParameterName = RtoGenericParameterName & RtoBaseNamedType;
33 | export type RtoNamedLocalTypeRef = RtoLocalTypeRef & RtoBaseNamedType;
34 | export type RtoNamedImportedTypeRef = RtoImportedTypeRef & RtoBaseNamedType;
35 | export type RtoNamedLiteralType = RtoLiteralType & RtoBaseNamedType;
36 | export type RtoNamedCompositeType = RtoCompositeType & RtoBaseNamedType;
37 | export type RtoNamedTupleType = RtoTupleType & RtoBaseNamedType;
38 | export type RtoNamedArrayType = RtoArrayType & RtoBaseNamedType;
39 | export type RtoNamedGenericInstance = RtoGenericInstance & RtoBaseNamedType;
40 | export type RtoNamedFunctionType = RtoFunctionType & RtoBaseNamedType;
41 | export type RtoNamedKeyofType = RtoKeyofType & RtoBaseNamedType;
42 | export type RtoNamedMemberType = RtoMemberType & RtoBaseNamedType;
43 | export type RtoNamedInterface = RtoInterface & RtoBaseNamedType;
44 |
45 | export type RtoType =
46 | | RtoTypeName
47 | | RtoGenericParameterName
48 | | RtoLocalTypeRef
49 | | RtoImportedTypeRef
50 | | RtoLiteralType
51 | | RtoCompositeType
52 | | RtoTupleType
53 | | RtoArrayType
54 | | RtoGenericInstance
55 | | RtoFunctionType
56 | | RtoKeyofType
57 | | RtoMemberType
58 | | RtoInterface;
59 |
60 | export interface RtoBaseNamedType extends RtoCommentable {
61 | exported?: boolean;
62 | name: string;
63 | generic?: RtoGenericParameter[];
64 | }
65 |
66 | export interface RtoGenericParameter {
67 | name: string;
68 | extendsType?: RtoType;
69 | defaultType?: RtoType;
70 | }
71 |
72 | export interface RtoTypeName {
73 | kind: "name";
74 | group: "ts" | "primitive" | "global";
75 | refName: RtoSpecialTypeName | RtoPrimitiveTypeName | string;
76 | }
77 |
78 | export type RtoSpecialTypeName = "any" | "unknown" | "object" | "void" | "never";
79 | export type RtoPrimitiveTypeName =
80 | | "string"
81 | | "number"
82 | | "bigint"
83 | | "boolean"
84 | | "symbol"
85 | | "undefined"
86 | | "null";
87 |
88 | export interface RtoGenericParameterName {
89 | kind: "genericParameterName";
90 | genericParameterName: string;
91 | }
92 |
93 | export interface RtoLocalTypeRef {
94 | kind: "localRef";
95 | refName: string;
96 | }
97 |
98 | export interface RtoImportedTypeRef {
99 | kind: "importedRef";
100 | refName: string;
101 | namespace?: string;
102 | }
103 |
104 | export interface RtoLiteralType {
105 | kind: "literal";
106 | literal: string | number | bigint | boolean;
107 | }
108 |
109 | export interface RtoCompositeType {
110 | kind: "composite";
111 | op: "union" | "intersection";
112 | types: RtoType[];
113 | }
114 |
115 | export interface RtoTupleType {
116 | kind: "tuple";
117 | itemTypes?: RtoType[];
118 | }
119 |
120 | export interface RtoArrayType {
121 | kind: "array";
122 | itemType: RtoType;
123 | }
124 |
125 | export interface RtoGenericInstance {
126 | kind: "genericInstance";
127 | genericName: string;
128 | parameterTypes: RtoType[];
129 | }
130 |
131 | export interface RtoFunctionType {
132 | kind: "function";
133 | parameters?: RtoFunctionParameter[];
134 | returnType: RtoType;
135 | generic?: RtoGenericParameter[];
136 | }
137 |
138 | export interface RtoFunctionParameter {
139 | name: string;
140 | type?: RtoType;
141 | optional?: boolean;
142 | }
143 |
144 | export interface RtoKeyofType {
145 | kind: "keyof";
146 | type: RtoType;
147 | }
148 |
149 | export interface RtoMemberType {
150 | kind: "member";
151 | parentType: RtoType;
152 | memberName: string | RtoMemberNameLiteral;
153 | }
154 |
155 | export interface RtoMemberNameLiteral {
156 | literal: string | number;
157 | }
158 |
159 | export interface RtoInterface {
160 | kind: "interface";
161 | indexSignature?: RtoIndexSignature;
162 | mappedIndexSignature?: RtoMappedIndexSignature;
163 | properties?: RtoProperty[];
164 | }
165 |
166 | export interface RtoProperty extends RtoCommentable {
167 | name: string;
168 | type: RtoType;
169 | optional?: boolean;
170 | readonly?: boolean;
171 | }
172 |
173 | export interface RtoIndexSignature extends RtoCommentable {
174 | keyName: string;
175 | keyType: "string" | "number";
176 | type: RtoType;
177 | optional?: boolean;
178 | readonly?: boolean;
179 | }
180 |
181 | export interface RtoMappedIndexSignature extends RtoCommentable {
182 | keyName: string;
183 | keyInType: RtoType;
184 | type: RtoType;
185 | optional?: boolean;
186 | readonly?: boolean;
187 | }
188 |
189 | export interface RtoCommentable {
190 | /**
191 | * A multiline string.
192 | */
193 | docComment?: string;
194 | }
195 |
--------------------------------------------------------------------------------
/packages/typeonly/tests/ast-tests/ast-array.spec.ts:
--------------------------------------------------------------------------------
1 | import { describe, expect, test } from "vitest";
2 | import { parseTypeOnly } from "../../src/api.js";
3 | import type {
4 | AstArrayType,
5 | AstFunctionType,
6 | AstInterface,
7 | AstNamedType,
8 | AstProperty,
9 | } from "../../src/ast.d.ts";
10 |
11 | describe("AST Specification for Array", () => {
12 | test("array with an identifier", () => {
13 | const source = `
14 | type T1 = number[]
15 | `;
16 | const ast = parseTypeOnly({ source });
17 | const namedType = ast.declarations?.[0] as AstNamedType;
18 | expect(namedType.type).toEqual({
19 | whichType: "array",
20 | itemType: "number",
21 | } as AstArrayType);
22 | });
23 |
24 | test("array with an interface", () => {
25 | const source = `
26 | type T1 = {a: A}[]
27 | `;
28 | const ast = parseTypeOnly({ source });
29 | const namedType = ast.declarations?.[0] as AstNamedType;
30 | const type = namedType.type as AstArrayType;
31 | expect(type.whichType).toBe("array");
32 | expect(type.genericSyntax).toBe(undefined);
33 | const itemType = type.itemType as AstInterface;
34 | expect(itemType.whichType).toBe("interface");
35 | expect((itemType.entries?.[0] as AstProperty).name).toBe("a");
36 | });
37 |
38 | test("array with a function", () => {
39 | const source = `
40 | type T1 = (() => A)[]
41 | `;
42 | const ast = parseTypeOnly({ source });
43 | const namedType = ast.declarations?.[0] as AstNamedType;
44 | const type = namedType.type as AstArrayType;
45 | expect(type.whichType).toBe("array");
46 | expect(type.genericSyntax).toBe(undefined);
47 | const itemType = type.itemType as AstFunctionType;
48 | expect(itemType.whichType).toBe("function");
49 | });
50 | });
51 |
--------------------------------------------------------------------------------
/packages/typeonly/tests/ast-tests/ast-comment.spec.ts:
--------------------------------------------------------------------------------
1 | import { describe, expect, test } from "vitest";
2 | import { parseTypeOnly } from "../../src/api.js";
3 | import type {
4 | AstInlineComment,
5 | AstNamedInterface,
6 | AstNamedType,
7 | AstStandaloneComment,
8 | } from "../../src/ast.d.ts";
9 |
10 | describe("AST Specification for Comments", () => {
11 | function testStandaloneComment(source: string, text: string, syntax: "inline" | "classic") {
12 | test(`standalone comment: ${JSON.stringify(source).replace(/\\n/g, "\u23ce")}`, () => {
13 | const ast = parseTypeOnly({ source });
14 | expect(ast.declarations?.[0]).toEqual({
15 | whichDeclaration: "comment",
16 | text,
17 | syntax,
18 | } as AstStandaloneComment);
19 | });
20 | }
21 |
22 | testStandaloneComment("// com 1", "com 1", "inline");
23 | testStandaloneComment("//line 1\n// line 2", "line 1\nline 2", "inline");
24 | testStandaloneComment("// line 1\n// line 2", " line 1\n line 2", "inline");
25 | testStandaloneComment(" \n\n // com 1 \n \n ", "com 1", "inline");
26 | testStandaloneComment("//\n// com 1\n//", "\ncom 1\n", "inline");
27 |
28 | testStandaloneComment("/* com 1 */", "com 1", "classic");
29 | testStandaloneComment("/* line 1\n line 2 */", " line 1\n line 2", "classic");
30 | testStandaloneComment("/*\n * com 1\n */", "com 1", "classic");
31 | testStandaloneComment(" \n\n /* com 1 */ \n \n ", "com 1", "classic");
32 | testStandaloneComment("/*\ncom 1\n*/", "\ncom 1\n", "classic");
33 | testStandaloneComment("/*\n *\n * com 1\n *\n */", "\ncom 1\n", "classic");
34 |
35 | function testInlineComment(source: string, text: string, syntax: "inline" | "classic") {
36 | test(`inline comment: ${JSON.stringify(source).replace(/\\n/g, "\u23ce")}`, () => {
37 | const ast = parseTypeOnly({ source });
38 | const namedType = ast.declarations?.[0] as AstNamedType;
39 | expect(namedType.inlineComments).toEqual([
40 | {
41 | syntax,
42 | text,
43 | },
44 | ] as AstInlineComment[]);
45 | });
46 | }
47 |
48 | testInlineComment("type T1 = string // com 1", "com 1", "inline");
49 | testInlineComment("type T1 = string /* com 1 */", "com 1", "classic");
50 | testInlineComment("type T1 = /* com 1 */ string", "com 1", "classic");
51 | testInlineComment("type T1 /* com 1 */ = string", "com 1", "classic");
52 | testInlineComment("type /* com 1 */ T1 = string", "com 1", "classic");
53 |
54 | function testDocCommentOnDeclaration(source: string, text: string) {
55 | test(`doc comment in declaration: ${JSON.stringify(source).replace(/\\n/g, "\u23ce")}`, () => {
56 | const ast = parseTypeOnly({ source });
57 | const decl = ast.declarations?.[0] as AstNamedType | AstNamedInterface;
58 | expect(decl.docComment).toBe(text);
59 | });
60 | }
61 |
62 | function testDocCommentsOnDeclaration(declaration: string) {
63 | testDocCommentOnDeclaration(`/** com 1 */${declaration}`, "com 1");
64 | testDocCommentOnDeclaration(`/** com 1 */ ${declaration}`, "com 1");
65 | testDocCommentOnDeclaration(`/** com 1 */\n${declaration}`, "com 1");
66 | testDocCommentOnDeclaration(`/** line 1\n line 2 */\n${declaration}`, " line 1\n line 2");
67 | testDocCommentOnDeclaration(`/**\n * line 1\n * line 2\n */\n${declaration}`, "line 1\nline 2");
68 | }
69 |
70 | testDocCommentsOnDeclaration("type T1 = string");
71 | testDocCommentsOnDeclaration("interface I1 {}");
72 |
73 | test("multiple comments", () => {
74 | const source = `
75 | // standalone 1, line 1
76 | // standalone 1, line 2
77 |
78 | // standalone 2
79 |
80 | /**
81 | * doc T1
82 | */
83 | type T1 = A // inline T1
84 |
85 | // standalone 3
86 | `;
87 | const ast = parseTypeOnly({ source });
88 | expect(ast.declarations?.[0]).toEqual({
89 | whichDeclaration: "comment",
90 | text: "standalone 1, line 1\nstandalone 1, line 2",
91 | syntax: "inline",
92 | } as AstStandaloneComment);
93 | expect(ast.declarations?.[1]).toEqual({
94 | whichDeclaration: "comment",
95 | text: "standalone 2",
96 | syntax: "inline",
97 | } as AstStandaloneComment);
98 | const namedType = ast.declarations?.[2] as AstNamedType;
99 | expect(namedType.whichDeclaration).toBe("type");
100 | expect(namedType.docComment).toBe("doc T1");
101 | expect(namedType.inlineComments).toEqual([
102 | {
103 | syntax: "inline",
104 | text: "inline T1",
105 | },
106 | ] as AstInlineComment[]);
107 | expect(ast.declarations?.[3]).toEqual({
108 | whichDeclaration: "comment",
109 | text: "standalone 3",
110 | syntax: "inline",
111 | } as AstStandaloneComment);
112 | });
113 | });
114 |
--------------------------------------------------------------------------------
/packages/typeonly/tests/ast-tests/ast-composite.spec.ts:
--------------------------------------------------------------------------------
1 | import { describe, expect, test } from "vitest";
2 | import { parseTypeOnly } from "../../src/api.js";
3 | import type { AstCompositeType, AstInterface, AstNamedType, AstProperty } from "../../src/ast.d.ts";
4 |
5 | describe("AST Specification for Composite Types", () => {
6 | test("composite type with identifiers", () => {
7 | const source = "type T1 = number | string | undefined | null";
8 | const ast = parseTypeOnly({ source });
9 | const namedType = ast.declarations?.[0] as AstNamedType;
10 | const composite = namedType.type as AstCompositeType;
11 | expect(composite).toEqual({
12 | whichType: "composite",
13 | op: "union",
14 | types: ["number", "string", "undefined", "null"],
15 | } as AstCompositeType);
16 | });
17 |
18 | test("composite type with nested types", () => {
19 | const source = "type T1 = { a: string | number } & { b: boolean }";
20 | const ast = parseTypeOnly({ source });
21 | const namedType = ast.declarations?.[0] as AstNamedType;
22 | const composite = namedType.type as AstCompositeType;
23 | expect(composite.op).toBe("intersection");
24 |
25 | const t1 = composite.types[0] as AstInterface;
26 | expect(t1.whichType).toBe("interface");
27 | const t1Prop = (t1.entries?.[0] as AstProperty).type as AstCompositeType;
28 | expect(t1Prop).toEqual({
29 | whichType: "composite",
30 | op: "union",
31 | types: ["string", "number"],
32 | } as AstCompositeType);
33 |
34 | const t2 = composite.types?.[1] as AstInterface;
35 | expect(t2.whichType).toBe("interface");
36 | });
37 |
38 | const withParenthesis = [
39 | "A | B & C",
40 | "A | (B & C)",
41 | "(A) | ((B & C))",
42 | "(((A) | ((B & C))))",
43 | "A | B \n & \n C",
44 | ];
45 | withParenthesis.forEach((inputType, index) => {
46 | test(`composite type with parenthesis #${index + 1}`, () => {
47 | const ast = parseTypeOnly({ source: `type T1 = ${inputType}` });
48 | const namedType = ast.declarations?.[0] as AstNamedType;
49 | const composite = namedType.type as AstCompositeType;
50 | expect(composite.op).toBe("union");
51 | expect(composite.types[0]).toBe("A");
52 |
53 | const t2 = composite.types[1] as AstCompositeType;
54 | expect(t2).toEqual({
55 | whichType: "composite",
56 | op: "intersection",
57 | types: ["B", "C"],
58 | } as AstCompositeType);
59 | });
60 | });
61 | });
62 |
--------------------------------------------------------------------------------
/packages/typeonly/tests/ast-tests/ast-function.spec.ts:
--------------------------------------------------------------------------------
1 | import { describe, expect, test } from "vitest";
2 | import { parseTypeOnly } from "../../src/api.js";
3 | import type { AstFunctionType, AstNamedType } from "../../src/ast.d.ts";
4 |
5 | describe("AST Specification for Function Types", () => {
6 | test("empty function", () => {
7 | const source = "type T1 = () => void";
8 | const ast = parseTypeOnly({ source });
9 | const namedType = ast.declarations?.[0] as AstNamedType;
10 | expect(namedType.type).toEqual({
11 | whichType: "function",
12 | returnType: "void",
13 | } as AstFunctionType);
14 | });
15 |
16 | test("function with parameters", () => {
17 | const source = "type T1 = (p1: string, p2?: number) => void";
18 | const ast = parseTypeOnly({ source });
19 | const namedType = ast.declarations?.[0] as AstNamedType;
20 | expect(namedType.type).toEqual({
21 | whichType: "function",
22 | parameters: [
23 | {
24 | name: "p1",
25 | type: "string",
26 | optional: false,
27 | },
28 | {
29 | name: "p2",
30 | type: "number",
31 | optional: true,
32 | },
33 | ],
34 | returnType: "void",
35 | } as AstFunctionType);
36 | });
37 |
38 | test("function with nested types", () => {
39 | const source = "type T1 = (p1: () => void) => () => void";
40 | const ast = parseTypeOnly({ source });
41 | const namedType = ast.declarations?.[0] as AstNamedType;
42 | const fnType = namedType.type as AstFunctionType;
43 | const emptyFnType: AstFunctionType = {
44 | whichType: "function",
45 | returnType: "void",
46 | };
47 | expect(fnType.returnType).toEqual(emptyFnType);
48 | expect(fnType.parameters?.[0].type).toEqual(emptyFnType);
49 | });
50 |
51 | test("function with nested types and parenthesis", () => {
52 | const source = "type T1 = (((p1: (() => void)) => (() => void)))";
53 | const ast = parseTypeOnly({ source });
54 | const namedType = ast.declarations?.[0] as AstNamedType;
55 | const fnType = namedType.type as AstFunctionType;
56 | const emptyFnType: AstFunctionType = {
57 | whichType: "function",
58 | returnType: "void",
59 | };
60 | expect(fnType.returnType).toEqual(emptyFnType);
61 | expect(fnType.parameters?.[0].type).toEqual(emptyFnType);
62 | });
63 | });
64 |
--------------------------------------------------------------------------------
/packages/typeonly/tests/ast-tests/ast-generic.spec.ts:
--------------------------------------------------------------------------------
1 | import { describe, expect, test } from "vitest";
2 | import { parseTypeOnly } from "../../src/api.js";
3 | import type { AstGenericInstance, AstNamedType } from "../../src/ast.d.ts";
4 |
5 | describe("AST Specification for Generic", () => {
6 | test("a generic instance with identifier", () => {
7 | const source = `
8 | type T1 = G
9 | `;
10 | const ast = parseTypeOnly({ source });
11 | const namedType = ast.declarations?.[0] as AstNamedType;
12 | expect(namedType.name).toBe("T1");
13 | expect(namedType.type).toEqual({
14 | whichType: "genericInstance",
15 | genericName: "G",
16 | parameterTypes: ["number", "string"],
17 | } as AstGenericInstance);
18 | });
19 | });
20 |
--------------------------------------------------------------------------------
/packages/typeonly/tests/ast-tests/ast-index-signature.spec.ts:
--------------------------------------------------------------------------------
1 | import { describe, expect, test } from "vitest";
2 | import { parseTypeOnly } from "../../src/api.js";
3 | import type { AstInterface, AstNamedInterface, AstNamedType } from "../../src/ast.d.ts";
4 |
5 | describe("AST Specification for Index Signature", () => {
6 | test("a index signature", () => {
7 | const source = `interface I1 {
8 | [index1: string] : {a: number}
9 | }`;
10 | const ast = parseTypeOnly({ source });
11 | const namedInterface = ast.declarations?.[0] as AstNamedInterface;
12 | expect(namedInterface.whichDeclaration).toBe("interface");
13 | expect(namedInterface.whichType).toBe("interface");
14 | expect(namedInterface.name).toBe("I1");
15 | expect(namedInterface.entries).toEqual([
16 | {
17 | whichEntry: "indexSignature",
18 | keyName: "index1",
19 | keyType: "string",
20 | optional: false,
21 | readonly: false,
22 | type: {
23 | whichType: "interface",
24 | entries: [
25 | {
26 | whichEntry: "property",
27 | name: "a",
28 | optional: false,
29 | readonly: false,
30 | type: "number",
31 | },
32 | ],
33 | },
34 | },
35 | ]);
36 | });
37 |
38 | test("a mapped index signature", () => {
39 | const source = `type T1 = {
40 | [index1 in string] : {a: number}
41 | }`;
42 | const ast = parseTypeOnly({ source });
43 | const namedType = ast.declarations?.[0] as AstNamedType;
44 | expect(namedType.whichDeclaration).toBe("type");
45 | expect(namedType.name).toBe("T1");
46 | const type = namedType.type as AstInterface;
47 | expect(type.whichType).toBe("interface");
48 | expect(type.entries).toEqual([
49 | {
50 | whichEntry: "mappedIndexSignature",
51 | keyName: "index1",
52 | optional: false,
53 | readonly: false,
54 | keyInType: "string",
55 | type: {
56 | whichType: "interface",
57 | entries: [
58 | {
59 | whichEntry: "property",
60 | name: "a",
61 | optional: false,
62 | readonly: false,
63 | type: "number",
64 | },
65 | ],
66 | },
67 | },
68 | ]);
69 | });
70 | });
71 |
--------------------------------------------------------------------------------
/packages/typeonly/tests/ast-tests/ast-inline-import.spec.ts:
--------------------------------------------------------------------------------
1 | import { describe, expect, test } from "vitest";
2 | import { parseTypeOnly } from "../../src/api.js";
3 | import type { AstNamedType } from "../../src/ast.d.ts";
4 |
5 | describe("AST Specification for InlineImport", () => {
6 | test("a InlineImport", () => {
7 | const source = `
8 | type T1 = import("./abc").Test
9 | `;
10 | const ast = parseTypeOnly({ source });
11 | const namedType = ast.declarations?.[0] as AstNamedType;
12 | expect(namedType.name).toBe("T1");
13 | expect(namedType.type).toEqual({
14 | whichType: "inlineImport",
15 | from: "./abc",
16 | exportedName: "Test",
17 | });
18 | });
19 | });
20 |
--------------------------------------------------------------------------------
/packages/typeonly/tests/ast-tests/ast-interface.spec.ts:
--------------------------------------------------------------------------------
1 | import { describe, expect, test } from "vitest";
2 | import { parseTypeOnly } from "../../src/api.js";
3 | import type { AstNamedInterface, AstProperty } from "../../src/ast.d.ts";
4 |
5 | describe("AST Specification for Interfaces", () => {
6 | const validIdentifiers = ["Abc12", "$_ab12", "_", "$", "əe"];
7 | validIdentifiers.forEach((identifier) => {
8 | test(`valid identifier: ${identifier}`, () => {
9 | const source = `interface ${identifier} {}`;
10 | const ast = parseTypeOnly({ source });
11 | const namedInterface = ast.declarations?.[0] as AstNamedInterface;
12 | expect(namedInterface.name).toBe(identifier);
13 | });
14 | });
15 |
16 | const invalidIdentifiers = ["2b", "a-b", "if", "for", "while", "do", "break", "continue"];
17 | invalidIdentifiers.forEach((identifier) => {
18 | test(`invalid identifier: ${identifier}`, () => {
19 | const source = `interface ${identifier} {}`;
20 | expect(() => parseTypeOnly({ source })).toThrow();
21 | });
22 | });
23 |
24 | test("declarations can be separated with semicolon or newline or nothing", () => {
25 | const source = `
26 | interface I1 {}interface I2{} interface I3{}
27 | interface I4{}; interface I5{};
28 | interface I6{}
29 | `;
30 | const ast = parseTypeOnly({ source });
31 | expect(ast.declarations?.length).toBe(6);
32 | });
33 |
34 | test("property separator can be a coma, a semicolon or a new line", () => {
35 | const source = `
36 | interface I1 {
37 | a: string,aa: boolean,
38 | b: string;bb: boolean;
39 | ;
40 | c: string
41 |
42 | , d: string
43 | }
44 | `;
45 | const ast = parseTypeOnly({ source });
46 | const namedInterface = ast.declarations?.[0] as AstNamedInterface;
47 | expect(namedInterface.entries?.length).toBe(6);
48 | });
49 |
50 | test("inline interface", () => {
51 | const source = `
52 | interface I1 { a: string, b: string; c: string }
53 | `;
54 | const ast = parseTypeOnly({ source });
55 | const namedInterface = ast.declarations?.[0] as AstNamedInterface;
56 | expect(namedInterface.entries?.length).toBe(3);
57 | });
58 |
59 | const validTypeNames = {
60 | primitive: ["number", "string", "boolean", "bigint", "symbol"],
61 | global: ["Number", "String", "Boolean", "Bigint", "Date", "Symbol"],
62 | typescript: ["any", "void", "object", "unknown", "never"],
63 | identifier: validIdentifiers,
64 | };
65 | Object.entries(validTypeNames).forEach(([category, typeNames]) => {
66 | for (const typeName of typeNames) {
67 | test(`${category} type: ${typeName}`, () => {
68 | const source = `interface I1 {
69 | a: ${typeName}
70 | }`;
71 | const ast = parseTypeOnly({ source });
72 | const namedInterface = ast.declarations?.[0] as AstNamedInterface;
73 | const property = namedInterface.entries?.[0] as AstProperty;
74 | expect(property.type).toBe(typeName);
75 | });
76 | }
77 | });
78 |
79 | test("extends", () => {
80 | const source = `
81 | interface I1 extends I2, I3 {
82 | a: number
83 | }
84 | `;
85 | const ast = parseTypeOnly({ source });
86 | const namedInterface = ast.declarations?.[0] as AstNamedInterface;
87 | expect(namedInterface.extends).toEqual(["I2", "I3"]);
88 | });
89 |
90 | test("interface with optional property", () => {
91 | const source = `
92 | interface I1 {
93 | a?: number
94 | }
95 | `;
96 | const ast = parseTypeOnly({ source });
97 | const namedInterface = ast.declarations?.[0] as AstNamedInterface;
98 | const prop = namedInterface.entries?.[0] as AstProperty;
99 | expect(prop.optional).toBe(true);
100 | });
101 |
102 | test("semicolons", () => {
103 | const source = `
104 | interface I1 {
105 | a: number;
106 | b: number;
107 | }
108 | `;
109 | const ast = parseTypeOnly({ source });
110 | const namedInterface = ast.declarations?.[0] as AstNamedInterface;
111 | expect(namedInterface.entries?.length).toBe(2);
112 | });
113 |
114 | test("comas", () => {
115 | const source = `
116 | interface I1 {
117 | a: number,
118 | b: number,
119 | }
120 | `;
121 | const ast = parseTypeOnly({ source });
122 | const namedInterface = ast.declarations?.[0] as AstNamedInterface;
123 | expect(namedInterface.entries?.length).toBe(2);
124 | });
125 | });
126 |
--------------------------------------------------------------------------------
/packages/typeonly/tests/ast-tests/ast-interface2.spec.ts:
--------------------------------------------------------------------------------
1 | import { describe, expect, test } from "vitest";
2 | import { parseTypeOnly } from "../../src/api.js";
3 | import type {
4 | AstFunctionProperty,
5 | AstFunctionType,
6 | AstNamedInterface,
7 | AstProperty,
8 | } from "../../src/ast.d.ts";
9 |
10 | describe("AST Specification for Interfaces (part 2)", () => {
11 | const testFunctionAsProperty = (source: string, parameters: any[], returnType: string) => {
12 | test(`a function as property, ${parameters.length === 0 ? "no parameter" : `with ${parameters.length} parameters`}`, () => {
13 | const ast = parseTypeOnly({ source });
14 | const namedInterface = ast.declarations?.[0] as AstNamedInterface;
15 | const prop = namedInterface.entries?.[0] as AstProperty;
16 | expect(prop.whichEntry).toBe("property");
17 | expect(prop.name).toBe("a");
18 | const propType = prop.type as AstFunctionType;
19 | expect(propType.whichType).toBe("function");
20 | expect(propType.parameters).toEqual(parameters.length === 0 ? undefined : parameters);
21 | expect(propType.returnType).toBe(returnType);
22 | });
23 | };
24 |
25 | const testFunctionProperty = (source: string, parameters: any[], returnType: string) => {
26 | test(`a function as functionProperty, ${parameters.length === 0 ? "no parameter" : `with ${parameters.length} parameters`}`, () => {
27 | const ast = parseTypeOnly({ source });
28 | const namedInterface = ast.declarations?.[0] as AstNamedInterface;
29 | const prop = namedInterface.entries?.[0] as AstFunctionProperty;
30 | expect(prop.whichEntry).toBe("functionProperty");
31 | expect(prop.name).toBe("a");
32 | expect(prop.parameters).toEqual(parameters.length === 0 ? undefined : parameters);
33 | expect(prop.returnType).toBe(returnType);
34 | });
35 | };
36 |
37 | testFunctionProperty(
38 | `
39 | interface I1 {
40 | a(): number
41 | }`,
42 | [],
43 | "number",
44 | );
45 |
46 | testFunctionAsProperty(
47 | `
48 | interface I1 {
49 | a: () => number
50 | }`,
51 | [],
52 | "number",
53 | );
54 |
55 | const parameters = [
56 | {
57 | name: "p1",
58 | type: "T1",
59 | optional: false,
60 | },
61 | {
62 | name: "p2",
63 | type: "T2",
64 | optional: false,
65 | },
66 | ];
67 |
68 | testFunctionProperty(
69 | `
70 | interface I1 {
71 | a(p1: T1, p2: T2): void
72 | }`,
73 | parameters,
74 | "void",
75 | );
76 |
77 | testFunctionAsProperty(
78 | `
79 | interface I1 {
80 | a: (p1: T1, p2: T2) => void
81 | }`,
82 | parameters,
83 | "void",
84 | );
85 | });
86 |
--------------------------------------------------------------------------------
/packages/typeonly/tests/ast-tests/ast-key-of.spec.ts:
--------------------------------------------------------------------------------
1 | import { describe, expect, test } from "vitest";
2 | import { parseTypeOnly } from "../../src/api.js";
3 | import type { AstNamedType } from "../../src/ast.d.ts";
4 |
5 | describe("AST Specification for KeyOf", () => {
6 | test("a keyOf with interface", () => {
7 | const source = `
8 | type T1 = keyof {a:number}
9 | `;
10 | const ast = parseTypeOnly({ source });
11 | const namedType = ast.declarations?.[0] as AstNamedType;
12 | expect(namedType.name).toBe("T1");
13 | expect(namedType.type).toEqual({
14 | whichType: "keyof",
15 | type: {
16 | whichType: "interface",
17 | entries: [
18 | {
19 | whichEntry: "property",
20 | name: "a",
21 | optional: false,
22 | readonly: false,
23 | type: "number",
24 | },
25 | ],
26 | },
27 | });
28 | });
29 | });
30 |
--------------------------------------------------------------------------------
/packages/typeonly/tests/ast-tests/ast-literal.spec.ts:
--------------------------------------------------------------------------------
1 | import { describe, expect, test } from "vitest";
2 | import { parseTypeOnly } from "../../src/api.js";
3 | import type { AstLiteralType, AstNamedType } from "../../src/ast.js";
4 |
5 | describe("AST Specification for Literal Types", () => {
6 | const validStringLiterals = [
7 | { literal: `"abc"`, result: "abc" },
8 | { literal: `"a\\"b"`, result: 'a"b' },
9 | { literal: `'a\\'b'`, result: "a'b" },
10 | ];
11 |
12 | validStringLiterals.forEach(({ literal, result }) => {
13 | test(`valid string literal: ${literal}`, () => {
14 | const source = `type T1 = ${literal}`;
15 | const ast = parseTypeOnly({ source });
16 | const namedType = ast.declarations?.[0] as AstNamedType;
17 | expect(namedType.type).toEqual({
18 | whichType: "literal",
19 | literal: result,
20 | stringDelim: literal[0],
21 | } as AstLiteralType);
22 | });
23 | });
24 |
25 | const validLiterals = [
26 | { literal: "23n", result: 23n },
27 | { literal: "12", result: 12 },
28 | { literal: "2.3", result: 2.3 },
29 | { literal: "false", result: false },
30 | { literal: "true", result: true },
31 | ];
32 | validLiterals.forEach(({ literal, result }) => {
33 | test(`valid literal: ${literal}`, () => {
34 | const source = `type T1 = ${literal}`;
35 | const ast = parseTypeOnly({ source });
36 | const namedType = ast.declarations?.[0] as AstNamedType;
37 | expect(namedType.type).toEqual({
38 | whichType: "literal",
39 | literal: result,
40 | } as AstLiteralType);
41 | });
42 | });
43 | });
44 |
--------------------------------------------------------------------------------
/packages/typeonly/tests/ast-tests/ast-member-type.spec.ts:
--------------------------------------------------------------------------------
1 | import { describe, expect, test } from "vitest";
2 | import { parseTypeOnly } from "../../src/api.js";
3 | import type { AstMemberType, AstNamedType } from "../../src/ast.d.ts";
4 |
5 | describe("AST Specification for MemberType", () => {
6 | test("a member type with identifier", () => {
7 | const source = `
8 | type T1 = Add[cv]
9 | `;
10 | const ast = parseTypeOnly({ source });
11 | const namedType = ast.declarations?.[0] as AstNamedType;
12 | expect(namedType.name).toBe("T1");
13 | expect(namedType.type).toEqual({
14 | whichType: "member",
15 | memberName: "cv",
16 | parentType: "Add",
17 | } as AstMemberType);
18 | });
19 | });
20 |
--------------------------------------------------------------------------------
/packages/typeonly/tests/ast-tests/ast-named-import.spec.ts:
--------------------------------------------------------------------------------
1 | import { describe, expect, test } from "vitest";
2 | import { parseTypeOnly } from "../../src/api.js";
3 | import type { AstClassicImport, AstNamespacedImport } from "../../src/ast.d.ts";
4 |
5 | describe("AST Specification for Named Import", () => {
6 | test("a classic import", () => {
7 | const source = `import { A as B, C as D } from "./abc.js"`;
8 | const ast = parseTypeOnly({ source });
9 | const classicImport = ast.declarations?.[0] as AstClassicImport;
10 | expect(classicImport.whichDeclaration).toBe("import");
11 | expect(classicImport.whichImport).toBe("classic");
12 | expect(classicImport.from).toBe("./abc.js");
13 | expect(classicImport.namedMembers).toEqual([
14 | {
15 | name: "A",
16 | as: "B",
17 | },
18 | {
19 | name: "C",
20 | as: "D",
21 | },
22 | ]);
23 | });
24 |
25 | test("a namespaced import", () => {
26 | const source = `import * as ns from "./abc.js"`;
27 | const ast = parseTypeOnly({ source });
28 | const namespacedImport = ast.declarations?.[0] as AstNamespacedImport;
29 | expect(namespacedImport.whichDeclaration).toBe("import");
30 | expect(namespacedImport.whichImport).toBe("namespaced");
31 | expect(namespacedImport.from).toBe("./abc.js");
32 | expect(namespacedImport.asNamespace).toBe("ns");
33 | });
34 | });
35 |
--------------------------------------------------------------------------------
/packages/typeonly/tests/ast-tests/ast-named-type.spec.ts:
--------------------------------------------------------------------------------
1 | import { describe, expect, test } from "vitest";
2 | import { parseTypeOnly } from "../../src/api.js";
3 | import type { AstInterface, AstNamedType } from "../../src/ast.d.ts";
4 |
5 | describe("AST Specification for Named Types", () => {
6 | test("type as alias", () => {
7 | const source = `
8 | type T1 = string
9 | `;
10 | const ast = parseTypeOnly({ source });
11 | const namedType = ast.declarations?.[0] as AstNamedType;
12 | expect(namedType.name).toBe("T1");
13 | expect(namedType.type).toBe("string");
14 | });
15 |
16 | test("type assign with interface", () => {
17 | const source = `
18 | type T1 = { a: number }
19 | `;
20 | const ast = parseTypeOnly({ source });
21 | const namedType = ast.declarations?.[0] as AstNamedType;
22 | const type = namedType.type as AstInterface;
23 | expect(type.whichType).toBe("interface");
24 | });
25 | });
26 |
--------------------------------------------------------------------------------
/packages/typeonly/tests/ast-tests/ast-nested-types.spec.ts:
--------------------------------------------------------------------------------
1 | import { describe, expect, test } from "vitest";
2 | import { parseTypeOnly } from "../../src/api.js";
3 | import type { AstInterface, AstNamedInterface, AstProperty } from "../../src/ast.d.ts";
4 |
5 | describe("AST Specification for Nested Types", () => {
6 | test("an anonymous interface nested in a interface", () => {
7 | const source = `
8 | interface I1 {
9 | a: {
10 | b: string
11 | }
12 | }
13 | `;
14 | const ast = parseTypeOnly({ source });
15 | expect(ast.declarations?.length).toBe(1);
16 | const namedInterface = ast.declarations?.[0] as AstNamedInterface;
17 | expect(namedInterface.whichType).toBe("interface");
18 | expect(namedInterface.entries?.length).toBe(1);
19 | const property = namedInterface.entries?.[0] as AstProperty;
20 | expect(property.whichEntry).toBe("property");
21 | expect(property.name).toBe("a");
22 | const subType = property.type as AstInterface;
23 | expect(subType.whichType).toBe("interface");
24 | expect(subType.entries?.length).toBe(1);
25 | const subProperty = subType.entries?.[0] as AstProperty;
26 | expect(subProperty.whichEntry).toBe("property");
27 | expect(subProperty.name).toBe("b");
28 | expect(subProperty.type).toBe("string");
29 | });
30 |
31 | const deep = Math.round(Math.random() * 20) + 1;
32 | test(`a random number of nested interfaces (${deep})`, () => {
33 | const makeInterface = (deep: number): string => {
34 | if (deep <= 0) return "number";
35 | return `{ a: ${makeInterface(deep - 1)} }`;
36 | };
37 | const source = `interface I1 ${makeInterface(deep)}`;
38 | const ast = parseTypeOnly({ source });
39 | let parent = ast.declarations?.[0] as AstInterface;
40 | for (let i = 0; i < deep - 1; ++i) {
41 | const child = parent.entries?.[0] as AstProperty;
42 | parent = child.type as AstInterface;
43 | expect(typeof parent).toBe("object");
44 | expect(parent.whichType).toBe("interface");
45 | }
46 | const child = parent.entries?.[0] as AstProperty;
47 | expect(child.type).toBe("number");
48 | });
49 | });
50 |
--------------------------------------------------------------------------------
/packages/typeonly/tests/ast-tests/ast-precedence.spec.ts:
--------------------------------------------------------------------------------
1 | import { describe, expect, test } from "vitest";
2 | import { parseTypeOnly } from "../../src/api.js";
3 | import type {
4 | AstArrayType,
5 | AstCompositeType,
6 | AstFunctionType,
7 | AstNamedType,
8 | } from "../../src/ast.d.ts";
9 |
10 | describe("AST Specification for Precedence", () => {
11 | test("function with an array as return value", () => {
12 | const source = `
13 | type T1 = () => A[]
14 | `;
15 | const ast = parseTypeOnly({ source });
16 | const namedType = ast.declarations?.[0] as AstNamedType;
17 | const type = namedType.type as AstFunctionType;
18 | expect(type.whichType).toBe("function");
19 | const returnType = type.returnType as AstArrayType;
20 | expect(returnType.whichType).toBe("array");
21 | expect(returnType.itemType).toBe("A");
22 | });
23 |
24 | test("function with a composite type as return value", () => {
25 | const source = `
26 | type T1 = () => A | B[]
27 | `;
28 | const ast = parseTypeOnly({ source });
29 | const namedType = ast.declarations?.[0] as AstNamedType;
30 | const type = namedType.type as AstFunctionType;
31 | expect(type.whichType).toBe("function");
32 | const returnType = type.returnType as AstCompositeType;
33 | expect(returnType.whichType).toBe("composite");
34 | expect(returnType.types.length).toBe(2);
35 | expect(returnType.types[0]).toBe("A");
36 | const arrayType = returnType.types[1] as AstArrayType;
37 | expect(arrayType.whichType).toBe("array");
38 | expect(arrayType.itemType).toBe("B");
39 | });
40 | });
41 |
--------------------------------------------------------------------------------
/packages/typeonly/tests/ast-tests/ast-tuple.spec.ts:
--------------------------------------------------------------------------------
1 | import { describe, expect, test } from "vitest";
2 | import { parseTypeOnly } from "../../src/api.js";
3 | import type { AstNamedType } from "../../src/ast.d.ts";
4 |
5 | describe("AST Specification for Tuples", () => {
6 | test("empty tuple", () => {
7 | const source = `
8 | type T1 = []
9 | `;
10 | const ast = parseTypeOnly({ source });
11 | const namedType = ast.declarations?.[0] as AstNamedType;
12 | expect(namedType.name).toBe("T1");
13 | expect(namedType.type).toEqual({
14 | whichType: "tuple",
15 | });
16 | });
17 |
18 | test("a tuple with identifiers", () => {
19 | const source = `
20 | type T1 = [string, number, boolean]
21 | `;
22 | const ast = parseTypeOnly({ source });
23 | const namedType = ast.declarations?.[0] as AstNamedType;
24 | expect(namedType.name).toBe("T1");
25 | expect(namedType.type).toEqual({
26 | whichType: "tuple",
27 | itemTypes: ["string", "number", "boolean"],
28 | });
29 | });
30 |
31 | test("a tuple with identifiers and an interface", () => {
32 | const source = `
33 | type T2 = [string, {a: number, b: string}, boolean]
34 | `;
35 | const ast = parseTypeOnly({ source });
36 | const namedType = ast.declarations?.[0] as AstNamedType;
37 | expect(namedType.name).toBe("T2");
38 | expect(namedType.type).toEqual({
39 | whichType: "tuple",
40 | itemTypes: [
41 | "string",
42 | {
43 | whichType: "interface",
44 | entries: [
45 | {
46 | whichEntry: "property",
47 | name: "a",
48 | optional: false,
49 | readonly: false,
50 | type: "number",
51 | },
52 | {
53 | whichEntry: "property",
54 | name: "b",
55 | optional: false,
56 | readonly: false,
57 | type: "string",
58 | },
59 | ],
60 | },
61 | "boolean",
62 | ],
63 | });
64 | });
65 |
66 | test("a tuple with a newline", () => {
67 | const source = `
68 | type T3 = [string, number,boolean
69 | ,
70 | dfdfd]
71 | `;
72 | const ast = parseTypeOnly({ source });
73 | const namedType = ast.declarations?.[0] as AstNamedType;
74 | expect(namedType.name).toBe("T3");
75 | expect(namedType.type).toEqual({
76 | whichType: "tuple",
77 | itemTypes: ["string", "number", "boolean", "dfdfd"],
78 | });
79 | });
80 | });
81 |
--------------------------------------------------------------------------------
/packages/typeonly/tests/ast-tests/ast-whitespaces.spec.ts:
--------------------------------------------------------------------------------
1 | import { describe, expect, test } from "vitest";
2 | import { parseTypeOnly } from "../../src/api.js";
3 |
4 | describe("AST Specification about White Spaces", () => {
5 | test("weird spaces do not matter", () => {
6 | const source = `
7 |
8 | interface I1
9 |
10 | {
11 |
12 | a : string
13 |
14 | b:string
15 | }
16 |
17 | `;
18 | const ast = parseTypeOnly({ source });
19 | expect(ast.declarations?.length).toBe(1);
20 | });
21 |
22 | test("new lines in a named type", () => {
23 | parseTypeOnly({
24 | source: `
25 | type T1
26 | =
27 | number
28 | [
29 |
30 | ]
31 | `,
32 | });
33 | });
34 |
35 | test("new lines in a named interface", () => {
36 | parseTypeOnly({
37 | source: `
38 | export
39 | interface I1 {
40 | readonly
41 | a
42 | :
43 | string
44 | }
45 | `,
46 | });
47 | });
48 | });
49 |
--------------------------------------------------------------------------------
/packages/typeonly/tests/import-tests/import-tests.spec.ts:
--------------------------------------------------------------------------------
1 | import { dirname, join } from "node:path";
2 | import { fileURLToPath } from "node:url";
3 | import { describe, expect, test } from "vitest";
4 | import { generateRtoModules } from "../../src/api.js";
5 |
6 | const __filename = fileURLToPath(import.meta.url);
7 | const __dirname = dirname(__filename);
8 |
9 | describe("Imports", () => {
10 | test("imports external packages", async () => {
11 | const result = await generateRtoModules({
12 | modulePaths: ["./proj01-types"],
13 | readFiles: {
14 | sourceDir: join(__dirname, "test-proj01", "types"),
15 | },
16 | returnRtoModules: true,
17 | });
18 | // console.log("result of imports", JSON.stringify(result, null, 2))
19 | expect(result).toBeDefined();
20 | });
21 | });
22 |
--------------------------------------------------------------------------------
/packages/typeonly/tests/import-tests/test-proj01/node_modules/@hello/world/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "types": "types/main.d.ts"
3 | }
--------------------------------------------------------------------------------
/packages/typeonly/tests/import-tests/test-proj01/node_modules/@hello/world/types/main.d.ts:
--------------------------------------------------------------------------------
1 | export interface Main {
2 | main: string
3 | }
--------------------------------------------------------------------------------
/packages/typeonly/tests/import-tests/test-proj01/node_modules/@hello/world/types/other.d.ts:
--------------------------------------------------------------------------------
1 | export interface Other {
2 | other: string
3 | }
--------------------------------------------------------------------------------
/packages/typeonly/tests/import-tests/test-proj01/node_modules/simple-package/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "types": "types/main.d.ts"
3 | }
--------------------------------------------------------------------------------
/packages/typeonly/tests/import-tests/test-proj01/node_modules/simple-package/types/main.d.ts:
--------------------------------------------------------------------------------
1 | export interface SimplePackage {
2 | simplePackage: string
3 | }
--------------------------------------------------------------------------------
/packages/typeonly/tests/import-tests/test-proj01/node_modules/simple-package/types/simple-other.d.ts:
--------------------------------------------------------------------------------
1 | export interface SimpleOther {
2 | simpleOther: string
3 | }
--------------------------------------------------------------------------------
/packages/typeonly/tests/import-tests/test-proj01/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "test-proj01"
3 | }
4 |
--------------------------------------------------------------------------------
/packages/typeonly/tests/import-tests/test-proj01/types/proj01-types.d.ts:
--------------------------------------------------------------------------------
1 | import type { Main } from "@hello/world";
2 | import type { Other } from "@hello/world/types/other";
3 | import type { SimplePackage } from "simple-package";
4 | import type { SimpleOther } from "simple-package/types/simple-other";
5 |
6 | export interface Proj01 {
7 | main: Main;
8 | other: Other;
9 | simplePackage: SimplePackage;
10 | simpleOther: SimpleOther;
11 | }
12 |
--------------------------------------------------------------------------------
/packages/typeonly/tests/rto-tests/rto-comment.spec.ts:
--------------------------------------------------------------------------------
1 | import { describe, expect, test } from "vitest";
2 | import { createStandaloneRtoModule, parseTypeOnly } from "../../src/api.js";
3 | import type { RtoBaseNamedType, RtoInterface, RtoTypeName } from "../../src/rto.d.ts";
4 |
5 | describe("RTO Specification for Comment", () => {
6 | test("Comment with type", async () => {
7 | const source = `
8 | /**
9 | * something
10 | */
11 | export type T1 = number
12 | `;
13 | const rtoModule = createStandaloneRtoModule({
14 | ast: parseTypeOnly({ source }),
15 | });
16 |
17 | expect(rtoModule.namedTypes?.length).toBe(1);
18 | const rtoNamedType = rtoModule.namedTypes?.[0] as RtoTypeName & RtoBaseNamedType;
19 | expect(rtoNamedType).toEqual({
20 | name: "T1",
21 | exported: true,
22 | docComment: "something",
23 | kind: "name",
24 | group: "primitive",
25 | refName: "number",
26 | });
27 | });
28 |
29 | test("Comment with interface", async () => {
30 | const source = `
31 | /**
32 | * something
33 | */
34 | export interface T1 {
35 | a: number
36 | }
37 | `;
38 | const rtoModule = createStandaloneRtoModule({
39 | ast: parseTypeOnly({ source }),
40 | });
41 |
42 | expect(rtoModule.namedTypes?.length).toBe(1);
43 | const rtoNamedType = rtoModule.namedTypes?.[0] as RtoInterface & RtoBaseNamedType;
44 | expect(rtoNamedType.docComment).toBe("something");
45 | expect(rtoNamedType.exported).toBe(true);
46 | expect(rtoNamedType.generic).toBeUndefined();
47 | expect(rtoNamedType.kind).toBe("interface");
48 | expect(rtoNamedType.name).toBe("T1");
49 | expect(rtoNamedType.properties).toEqual([
50 | {
51 | name: "a",
52 | type: { kind: "name", group: "primitive", refName: "number" },
53 | },
54 | ]);
55 | });
56 |
57 | test("Comment with interface property", async () => {
58 | const source = `
59 | export interface T1 {
60 | a: number
61 | /**
62 | * something
63 | */
64 | b: string
65 | }
66 | `;
67 | const rtoModule = createStandaloneRtoModule({
68 | ast: parseTypeOnly({ source }),
69 | });
70 |
71 | expect(rtoModule.namedTypes?.length).toBe(1);
72 | const rtoNamedType = rtoModule.namedTypes?.[0] as RtoInterface & RtoBaseNamedType;
73 | expect(rtoNamedType.docComment).toBeUndefined();
74 | expect(rtoNamedType.exported).toBe(true);
75 | expect(rtoNamedType.generic).toBeUndefined();
76 | expect(rtoNamedType.kind).toBe("interface");
77 | expect(rtoNamedType.name).toBe("T1");
78 | expect(rtoNamedType.properties).toEqual([
79 | {
80 | name: "a",
81 | type: { kind: "name", group: "primitive", refName: "number" },
82 | },
83 | {
84 | name: "b",
85 | type: { kind: "name", group: "primitive", refName: "string" },
86 | docComment: "something",
87 | },
88 | ]);
89 | });
90 | });
91 |
--------------------------------------------------------------------------------
/packages/typeonly/tests/rto-tests/rto-import.spec.ts:
--------------------------------------------------------------------------------
1 | import { describe, expect, test } from "vitest";
2 | import { generateRtoModules, parseTypeOnly } from "../../src/api.js";
3 | import type { RtoBaseNamedType, RtoImportedTypeRef } from "../../src/rto.d.ts";
4 |
5 | describe("RTO Specification for Import", () => {
6 | test("RtoImportedTypeRef", async () => {
7 | const source1 = `
8 | import { T2 } from "./source2"
9 | export type T1 = T2
10 | `;
11 | const source2 = `
12 | export type T2 = boolean
13 | `;
14 | const rtoModules = await generateRtoModules({
15 | modulePaths: ["./source1"],
16 | astProvider: (modulePath) => {
17 | if (modulePath === "./source1") return parseTypeOnly({ source: source1 });
18 | if (modulePath === "./source2") return parseTypeOnly({ source: source2 });
19 | throw new Error(`Unknown module: ${modulePath}`);
20 | },
21 | returnRtoModules: true,
22 | });
23 | const rtoModule = rtoModules?.["./source1"];
24 |
25 | expect(rtoModule?.namedTypes?.length).toBe(1);
26 | const rtoNamedType = rtoModule?.namedTypes?.[0] as RtoImportedTypeRef & RtoBaseNamedType;
27 | expect(rtoNamedType).toEqual({
28 | name: "T1",
29 | exported: true,
30 | kind: "importedRef",
31 | refName: "T2",
32 | });
33 | });
34 | });
35 |
--------------------------------------------------------------------------------
/packages/typeonly/tests/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "strict": true,
4 | "target": "ES2022",
5 | "module": "NodeNext",
6 | "moduleResolution": "nodenext",
7 | "sourceMap": false,
8 | "lib": ["esnext"],
9 | "noEmit": true
10 | },
11 | "include": ["."]
12 | }
13 |
--------------------------------------------------------------------------------
/packages/typeonly/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "strict": true,
4 | "target": "ES2022",
5 | "module": "NodeNext",
6 | "moduleResolution": "nodenext",
7 | "sourceMap": false,
8 | "lib": ["esnext"],
9 | "esModuleInterop": true,
10 | "outDir": "dist",
11 | "noEmitOnError": true,
12 | "declaration": true,
13 | "declarationDir": "scripts/declarations",
14 | "allowJs": true,
15 | "skipLibCheck": true,
16 | "rootDir": "src"
17 | },
18 | "include": ["src"]
19 | }
20 |
--------------------------------------------------------------------------------
/packages/typeonly/typeonly-language.md:
--------------------------------------------------------------------------------
1 | # TypeOnly Language
2 |
3 | TypeOnly is a strict subset of TypeScript: any code that compiles with TypeOnly will also compile with TypeScript.
4 |
5 | An example of TypeOnly source files:
6 |
7 | ```ts
8 | // pencil-json-def.d.ts
9 | import { Color } from "./constants"
10 |
11 | export interface JsonDef {
12 | pencils: Pencil[]
13 | }
14 |
15 | export interface Pencil {
16 | color: Color
17 | size: "normal" | "small"
18 | }
19 | ```
20 |
21 | ```ts
22 | // constants.d.ts
23 |
24 | export type Color = "red" | "green" | "blue"
25 | ```
26 |
27 | # Allowed declarations
28 |
29 | A TypeOnly _module_ is a set of declarations. There are three sort of declarations: _imports_, _named types_ and _named interfaces_.
30 |
31 | Notice: Our naming slightly differs from that of TypeScript, especially on what we call an "interface".
32 |
33 | # Module, export, import
34 |
35 | A TypeOnly source file is called a _module_. Exported members can be imported the same way as ECMAScript modules, except that default imports or exports are not allowed.
36 |
37 | The `export` keyword on named types or named interfaces is optional. Only the module members with an `export` keyword can be imported from outside the module.
38 |
39 | # Named Type
40 |
41 | A named type starts with the optional keyword `export` followed by the `type` keyword, then a type name as an identifier, the `=` character, and a type definition. Example:
42 |
43 | ```ts
44 | type T1 = boolean
45 | ```
46 |
47 | # Named Interface
48 |
49 | A named interface is a named type that starts with the optional keyword `export` followed by the `interface` keyword, then an interface name as an identifier, and the interface definition. Example:
50 |
51 | ```ts
52 | interface I1 {
53 | prop1: boolean
54 | }
55 | ```
56 |
57 | # Types
58 |
59 | ## Interface
60 |
61 | An interface definition is surrounded by curly brackets `{` and `}`. It can contain three sort of entries: a list of properties, an index signature, and a mapped index signature.
62 |
63 | A **property** definition is composed with a name and a type. An example of interface with two properties:
64 |
65 | ```ts
66 | {
67 | prop1: boolean
68 | prop2: number
69 | }
70 | ```
71 |
72 | An example of interface with an **index signature**:
73 |
74 | ```ts
75 | {
76 | [propName: string]: boolean
77 | }
78 | ```
79 |
80 | An example of interface with a **mapped index signature**:
81 |
82 | ```ts
83 | {
84 | [K in "prop1" | "prop2"]: boolean
85 | }
86 | ```
87 |
88 | Notice: An index signature can be mixed with properties in accordance with TypeScript rules. But a mapped index signature is incompatible with an index signature or properties. Additionally, a named interface cannot contains a mapped index signature.
89 |
90 | ## Type Name
91 |
92 | Here are the accepted type names:
93 |
94 | * **Primitive type names:** `string`, `number`, `bigint`, `boolean`, `symbol`, `null`, `undefined`;
95 | * **TypeScript type names:** `any`, `unknown`, `object`, `void`, `never`;
96 | * **JavaScript type names:** `String`, `Number`, `Bigint`, `Boolean`, `Symbol`, `Date`;
97 | * **Global type names:** A list of global types can be provided as an option;
98 | * **Local type names:** Names of named types in the same module;
99 | * **Imported type names:** Names of imported named types that are exported from other modules;
100 | * **Generic parameter names:** Names of generic parameters in declared parent scopes.
101 |
102 | ## Literal Type
103 |
104 | A literal type is a literal value in one of the following primitive types: `string`, `number`, `bigint`, `boolean`. An example:
105 |
106 | ```ts
107 | "a"
108 | ```
109 |
110 | ## Composite Type
111 |
112 | A composite type is a union (`|`) or an intersection (`&`) of types. An example:
113 |
114 | ```ts
115 | "a" | "b" | undefined
116 | ```
117 |
118 | ## Array Type
119 |
120 | An array type represents an array of items with the same type. Two syntaxes are accepted.
121 |
122 | An example with the short syntax:
123 |
124 | ```ts
125 | string[]
126 | ```
127 |
128 | The same example with the generic syntax:
129 |
130 | ```ts
131 | Array
132 | ```
133 |
134 | ## Tuple Type
135 |
136 | A tuple type is a fixed list of types. An example:
137 |
138 | ```ts
139 | [boolean, string, number]
140 | ```
141 |
142 | ## Keyof Type
143 |
144 | A keyof type represents an union of property names. An example:
145 |
146 | ```ts
147 | keyof T1
148 | ```
149 |
150 | ## Member Type
151 |
152 | A member type is the type of a property in a parent type. Example:
153 |
154 | ```ts
155 | T1.prop1
156 | ```
157 |
158 | ## Generic Instance
159 |
160 | A generic instance is a type which is an instance of a generic type. An example:
161 |
162 | ```ts
163 | T1
164 | ```
165 |
166 |
167 |
168 |
--------------------------------------------------------------------------------
/packages/typeonly/vitest.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "vitest/config";
2 |
3 | export default defineConfig({
4 | test: {
5 | include: ["**/src/**/*.spec.ts", "**/tests/**/*.spec.ts"],
6 | environment: "node",
7 | },
8 | });
9 |
--------------------------------------------------------------------------------
/packages/validator-cli/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | ._*
3 |
4 | node_modules
5 | package-lock.json
6 | .npmrc
7 |
8 | /dist
9 |
--------------------------------------------------------------------------------
/packages/validator-cli/.prettierignore:
--------------------------------------------------------------------------------
1 | *
--------------------------------------------------------------------------------
/packages/validator-cli/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # @typeonly/validator-cli
2 |
3 | ## 1.0.1
4 |
5 | ### Patch Changes
6 |
7 | - 9759c6a: Documentation
8 | - Updated dependencies [9759c6a]
9 | - @typeonly/validator@1.0.1
10 | - typeonly@1.0.1
11 |
12 | ## 1.0.0
13 |
14 | ### Major Changes
15 |
16 | - ESM modules
17 |
18 | ## 0.6.0
19 |
20 | ### Minor Changes
21 |
22 | - upgrade dependencies & syntax
23 |
24 | ### Patch Changes
25 |
26 | - Updated dependencies
27 | - typeonly@1.0.0
28 | - @typeonly/validator@1.0.0
29 | - @typeonly/validator@0.6.0
30 | - typeonly@0.5.0
31 |
--------------------------------------------------------------------------------
/packages/validator-cli/LICENSE:
--------------------------------------------------------------------------------
1 | CC0 1.0 Universal
2 |
3 | Statement of Purpose
4 |
5 | The laws of most jurisdictions throughout the world automatically confer
6 | exclusive Copyright and Related Rights (defined below) upon the creator and
7 | subsequent owner(s) (each and all, an "owner") of an original work of
8 | authorship and/or a database (each, a "Work").
9 |
10 | Certain owners wish to permanently relinquish those rights to a Work for the
11 | purpose of contributing to a commons of creative, cultural and scientific
12 | works ("Commons") that the public can reliably and without fear of later
13 | claims of infringement build upon, modify, incorporate in other works, reuse
14 | and redistribute as freely as possible in any form whatsoever and for any
15 | purposes, including without limitation commercial purposes. These owners may
16 | contribute to the Commons to promote the ideal of a free culture and the
17 | further production of creative, cultural and scientific works, or to gain
18 | reputation or greater distribution for their Work in part through the use and
19 | efforts of others.
20 |
21 | For these and/or other purposes and motivations, and without any expectation
22 | of additional consideration or compensation, the person associating CC0 with a
23 | Work (the "Affirmer"), to the extent that he or she is an owner of Copyright
24 | and Related Rights in the Work, voluntarily elects to apply CC0 to the Work
25 | and publicly distribute the Work under its terms, with knowledge of his or her
26 | Copyright and Related Rights in the Work and the meaning and intended legal
27 | effect of CC0 on those rights.
28 |
29 | 1. Copyright and Related Rights. A Work made available under CC0 may be
30 | protected by copyright and related or neighboring rights ("Copyright and
31 | Related Rights"). Copyright and Related Rights include, but are not limited
32 | to, the following:
33 |
34 | i. the right to reproduce, adapt, distribute, perform, display, communicate,
35 | and translate a Work;
36 |
37 | ii. moral rights retained by the original author(s) and/or performer(s);
38 |
39 | iii. publicity and privacy rights pertaining to a person's image or likeness
40 | depicted in a Work;
41 |
42 | iv. rights protecting against unfair competition in regards to a Work,
43 | subject to the limitations in paragraph 4(a), below;
44 |
45 | v. rights protecting the extraction, dissemination, use and reuse of data in
46 | a Work;
47 |
48 | vi. database rights (such as those arising under Directive 96/9/EC of the
49 | European Parliament and of the Council of 11 March 1996 on the legal
50 | protection of databases, and under any national implementation thereof,
51 | including any amended or successor version of such directive); and
52 |
53 | vii. other similar, equivalent or corresponding rights throughout the world
54 | based on applicable law or treaty, and any national implementations thereof.
55 |
56 | 2. Waiver. To the greatest extent permitted by, but not in contravention of,
57 | applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and
58 | unconditionally waives, abandons, and surrenders all of Affirmer's Copyright
59 | and Related Rights and associated claims and causes of action, whether now
60 | known or unknown (including existing as well as future claims and causes of
61 | action), in the Work (i) in all territories worldwide, (ii) for the maximum
62 | duration provided by applicable law or treaty (including future time
63 | extensions), (iii) in any current or future medium and for any number of
64 | copies, and (iv) for any purpose whatsoever, including without limitation
65 | commercial, advertising or promotional purposes (the "Waiver"). Affirmer makes
66 | the Waiver for the benefit of each member of the public at large and to the
67 | detriment of Affirmer's heirs and successors, fully intending that such Waiver
68 | shall not be subject to revocation, rescission, cancellation, termination, or
69 | any other legal or equitable action to disrupt the quiet enjoyment of the Work
70 | by the public as contemplated by Affirmer's express Statement of Purpose.
71 |
72 | 3. Public License Fallback. Should any part of the Waiver for any reason be
73 | judged legally invalid or ineffective under applicable law, then the Waiver
74 | shall be preserved to the maximum extent permitted taking into account
75 | Affirmer's express Statement of Purpose. In addition, to the extent the Waiver
76 | is so judged Affirmer hereby grants to each affected person a royalty-free,
77 | non transferable, non sublicensable, non exclusive, irrevocable and
78 | unconditional license to exercise Affirmer's Copyright and Related Rights in
79 | the Work (i) in all territories worldwide, (ii) for the maximum duration
80 | provided by applicable law or treaty (including future time extensions), (iii)
81 | in any current or future medium and for any number of copies, and (iv) for any
82 | purpose whatsoever, including without limitation commercial, advertising or
83 | promotional purposes (the "License"). The License shall be deemed effective as
84 | of the date CC0 was applied by Affirmer to the Work. Should any part of the
85 | License for any reason be judged legally invalid or ineffective under
86 | applicable law, such partial invalidity or ineffectiveness shall not
87 | invalidate the remainder of the License, and in such case Affirmer hereby
88 | affirms that he or she will not (i) exercise any of his or her remaining
89 | Copyright and Related Rights in the Work or (ii) assert any associated claims
90 | and causes of action with respect to the Work, in either case contrary to
91 | Affirmer's express Statement of Purpose.
92 |
93 | 4. Limitations and Disclaimers.
94 |
95 | a. No trademark or patent rights held by Affirmer are waived, abandoned,
96 | surrendered, licensed or otherwise affected by this document.
97 |
98 | b. Affirmer offers the Work as-is and makes no representations or warranties
99 | of any kind concerning the Work, express, implied, statutory or otherwise,
100 | including without limitation warranties of title, merchantability, fitness
101 | for a particular purpose, non infringement, or the absence of latent or
102 | other defects, accuracy, or the present or absence of errors, whether or not
103 | discoverable, all to the greatest extent permissible under applicable law.
104 |
105 | c. Affirmer disclaims responsibility for clearing rights of other persons
106 | that may apply to the Work or any use thereof, including without limitation
107 | any person's Copyright and Related Rights in the Work. Further, Affirmer
108 | disclaims responsibility for obtaining any necessary consents, permissions
109 | or other rights required for any use of the Work.
110 |
111 | d. Affirmer understands and acknowledges that Creative Commons is not a
112 | party to this document and has no duty or obligation with respect to this
113 | CC0 or use of the Work.
114 |
115 | For more information, please see
116 |
117 |
--------------------------------------------------------------------------------
/packages/validator-cli/README.md:
--------------------------------------------------------------------------------
1 | # @typeonly/validator-cli
2 |
3 | [](https://travis-ci.com/paroi-tech/typeonly)
4 | [](https://www.npmjs.com/package/@typeonly/validator-cli)
5 | 
6 | 
7 |
8 | This package is part of **TypeOnly**, a lightweight validation library that uses TypeScript type definitions to validate JSON data. **[Learn more about TypeOnly here](https://www.npmjs.com/package/typeonly)**.
9 |
10 | ## Command Line Interface of the Validator
11 |
12 | Example:
13 |
14 | ```sh
15 | npx @typeonly/validator-cli -s src/file-name.d.ts -t RootTypeName data.json
16 | ```
17 |
18 | Available options:
19 |
20 | ```
21 | -h, --help Print this help message.
22 | -s, --source file.d.ts The typing file (one file allowed).
23 | --source-encoding string Encoding for typing files (default is utf8).
24 | --source-dir directory The source directory that contains typing files (optional).
25 | --rto-module file.rto.json The rto.json file to process (one file allowed).
26 | --rto-dir directory The source directory for rto.json file (optional).
27 | -t, --type string The type name of the root element in JSON.
28 | --non-strict Enable non-strict mode (accept extra properties).
29 | -e, --json-encoding string Encoding for JSON file to validate (default is utf8).
30 | --json file.json The JSON file to validate (by default at last position, one file allowed).
31 | ```
32 |
--------------------------------------------------------------------------------
/packages/validator-cli/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@typeonly/validator-cli",
3 | "version": "1.0.1",
4 | "description": "A CLI to validate JSON files, using TypeScript typing definitions.",
5 | "author": "Paroi",
6 | "type": "module",
7 | "scripts": {
8 | "prepublishOnly": "npm run build",
9 | "clear": "rimraf dist/*",
10 | "tsc": "tsc",
11 | "tsc:watch": "tsc --watch",
12 | "build": "npm run clear && npm run tsc",
13 | "lint": "biome check . --json-formatter-enabled=false --organize-imports-enabled=false"
14 | },
15 | "dependencies": {
16 | "@typeonly/validator": "^1.0.1",
17 | "command-line-args": "^6.0.1",
18 | "command-line-usage": "^7.0.3",
19 | "typeonly": "^1.0.1"
20 | },
21 | "devDependencies": {
22 | "@types/command-line-args": "^5.2.3",
23 | "@types/command-line-usage": "^5.0.4",
24 | "@types/node": "22",
25 | "rimraf": "^6.0.1",
26 | "typescript": "^5.7.3"
27 | },
28 | "bin": "dist/cli.js",
29 | "repository": {
30 | "type": "git",
31 | "url": "git+https://github.com/paroi-tech/typeonly.git"
32 | },
33 | "homepage": "https://github.com/paroi-tech/typeonly/tree/master/packages/validator-cli",
34 | "license": "CC0-1.0",
35 | "keywords": [
36 | "typescript",
37 | "json",
38 | "validation"
39 | ]
40 | }
--------------------------------------------------------------------------------
/packages/validator-cli/src/cli.ts:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 | import { readFileSync } from "node:fs";
3 | import { basename, dirname } from "node:path";
4 | import { createValidator } from "@typeonly/validator";
5 | import { type RtoModules, generateRtoModules } from "typeonly";
6 |
7 | import commandLineArgs from "command-line-args";
8 | import commandLineUsage from "command-line-usage";
9 |
10 | process.on("uncaughtException", (err) => {
11 | console.error("uncaughtException", err);
12 | process.exit(1);
13 | });
14 |
15 | process.on("unhandledRejection", (err) => {
16 | console.trace("unhandledRejection", err);
17 | process.exit(1);
18 | });
19 |
20 | class InvalidArgumentError extends Error {
21 | readonly causeCode = "invalidArgument";
22 | }
23 |
24 | type OptionDefinition = commandLineUsage.OptionDefinition & commandLineArgs.OptionDefinition;
25 |
26 | const optionDefinitions: OptionDefinition[] = [
27 | {
28 | name: "help",
29 | alias: "h",
30 | type: Boolean,
31 | description: "Print this help message.",
32 | },
33 | {
34 | name: "source",
35 | alias: "s",
36 | description: "The typing file (one file allowed).",
37 | type: String,
38 | multiple: false,
39 | typeLabel: "{underline file.d.ts}",
40 | },
41 | {
42 | name: "source-encoding",
43 | type: String,
44 | description: "Encoding for typing files (default is {underline utf8}).",
45 | },
46 | {
47 | name: "source-dir",
48 | type: String,
49 | description: "The source directory that contains typing files (optional).",
50 | typeLabel: "{underline directory}",
51 | },
52 | {
53 | name: "rto-module",
54 | description: "The rto.json file to process (one file allowed).",
55 | type: String,
56 | multiple: false,
57 | typeLabel: "{underline file.rto.json}",
58 | },
59 | {
60 | name: "rto-dir",
61 | type: String,
62 | description: "The source directory for rto.json file (optional).",
63 | typeLabel: "{underline directory}",
64 | },
65 | {
66 | name: "type",
67 | alias: "t",
68 | type: String,
69 | description: "The type name of the root element in JSON.",
70 | },
71 | {
72 | name: "non-strict",
73 | type: Boolean,
74 | description: "Enable non-strict mode (accept extra properties).",
75 | },
76 | {
77 | name: "json-encoding",
78 | alias: "e",
79 | type: String,
80 | description: "Encoding for JSON file to validate (default is {underline utf8}).",
81 | },
82 | {
83 | name: "json",
84 | description: "The JSON file to validate (by default at last position, one file allowed).",
85 | type: String,
86 | multiple: false,
87 | defaultOption: true,
88 | typeLabel: "{underline file.json}",
89 | },
90 | ];
91 |
92 | cli().catch((error) => {
93 | console.error(`Error: ${error.message}`);
94 | });
95 |
96 | async function cli() {
97 | const options = parseOptions();
98 | if (!options) {
99 | printHelp();
100 | return;
101 | }
102 |
103 | if (options.help) {
104 | printHelp();
105 | return;
106 | }
107 |
108 | try {
109 | await processFile(options);
110 | } catch (error: any) {
111 | if (error.causeCode === "invalidArgument") {
112 | console.error(`Error: ${error.message}`);
113 | printHelp();
114 | } else throw error;
115 | }
116 | }
117 |
118 | function printHelp() {
119 | const sections = [
120 | {
121 | header: "TypeOnly Validator CLI",
122 | content: "A CLI to validate JSON files conformity with typing.",
123 | },
124 | {
125 | header: "Synopsis",
126 | content: [
127 | "$ npx @typeonly/validator-cli {bold -s} {underline src/file-name.d.ts} {bold -t} {underline RootTypeName} {underline dir/data.json}",
128 | "$ npx @typeonly/validator-cli {bold --help}",
129 | ],
130 | },
131 | {
132 | header: "Options",
133 | optionList: optionDefinitions,
134 | },
135 | {
136 | content: "Project home: {underline https://github.com/paroi-tech/typeonly}",
137 | },
138 | ];
139 | const usage = commandLineUsage(sections);
140 | console.log(usage);
141 | }
142 |
143 | interface OptionsObject {
144 | [name: string]: unknown;
145 | }
146 |
147 | function parseOptions(): OptionsObject | undefined {
148 | try {
149 | return commandLineArgs(optionDefinitions);
150 | } catch (error: any) {
151 | console.log(`Error: ${error.message}`);
152 | printHelp();
153 | }
154 | }
155 |
156 | async function processFile(options: OptionsObject) {
157 | if (!options.source && !options["rto-module"])
158 | throw new InvalidArgumentError("Missing typing file or rto.json file.");
159 | if (options.source && options["rto-module"])
160 | throw new InvalidArgumentError("You must provide a typing file or a rto.json file not both.");
161 | if (!options.json) throw new InvalidArgumentError("Missing input JSON file to validate.");
162 |
163 | if (options.source) await validateFromTypingFile(options);
164 | else await validateFromRtoFile(options);
165 | }
166 |
167 | async function validateFromRtoFile(options: OptionsObject) {
168 | const moduleFile = options["rto-module"] as string;
169 | const bnad = baseNameAndDir(moduleFile);
170 | const baseDir = normalizeDir((options["rto-dir"] as string | undefined) ?? bnad.directory);
171 | const typeName = options.type as string;
172 | const data = readJsonFileSync(options);
173 |
174 | let modulePath = normalizeModulePath(moduleFile, baseDir);
175 | if (modulePath.endsWith(".rto.json")) modulePath = modulePath.slice(0, -9);
176 |
177 | const validator = await createValidator({
178 | modulePaths: [modulePath],
179 | baseDir,
180 | acceptAdditionalProperties: !!options["non-strict"],
181 | });
182 |
183 | const result = validator.validate(typeName, data, modulePath);
184 |
185 | if (!result.valid) {
186 | console.error(result.error);
187 | process.exit(1);
188 | }
189 | }
190 |
191 | async function validateFromTypingFile(options: OptionsObject) {
192 | let typingFile = options.source as string;
193 | const bnad = baseNameAndDir(typingFile);
194 | const sourceDir = normalizeDir((options["source-dir"] as string | undefined) ?? bnad.directory);
195 |
196 | if (typingFile.startsWith(sourceDir)) typingFile = typingFile.substring(sourceDir.length + 1);
197 |
198 | const typeName = options.type as string;
199 |
200 | const jsonData = readJsonFileSync(options);
201 |
202 | let sourceModulePath = normalizeModulePath(typingFile, sourceDir);
203 | if (!sourceModulePath.endsWith(".ts"))
204 | throw new InvalidArgumentError("Parameter 'source' must end with '.d.ts' or '.ts'");
205 | sourceModulePath = sourceModulePath.substring(
206 | 0,
207 | sourceModulePath.length - (sourceModulePath.endsWith(".d.ts") ? 5 : 3),
208 | );
209 |
210 | const encoding = (options["source-encoding"] ?? undefined) as string | undefined;
211 | validateBufferEncoding(encoding);
212 |
213 | const bundle = (await generateRtoModules({
214 | modulePaths: [sourceModulePath],
215 | readFiles: {
216 | sourceDir,
217 | encoding,
218 | },
219 | returnRtoModules: true,
220 | })) as RtoModules;
221 |
222 | const validator = createValidator({
223 | bundle,
224 | acceptAdditionalProperties: !!options["non-strict"],
225 | });
226 |
227 | const result = validator.validate(typeName, jsonData, sourceModulePath);
228 |
229 | if (!result.valid) {
230 | console.error(result.error);
231 | process.exit(1);
232 | }
233 | }
234 |
235 | function readJsonFileSync(options: OptionsObject): unknown {
236 | const fileToValidate = options.json as string;
237 | const encoding = (options["json-encoding"] as string | undefined) ?? "utf8";
238 | validateBufferEncoding(encoding);
239 | try {
240 | const data = readFileSync(fileToValidate, encoding);
241 | return JSON.parse(data);
242 | } catch (err) {
243 | throw new InvalidArgumentError(`Cannot read file: ${fileToValidate}`);
244 | }
245 | }
246 |
247 | function normalizeModulePath(file: string, sourceDir: string): string {
248 | const prefix = `${sourceDir}/`.replace(/\\/g, "/");
249 | let f = file.replace(/\\/g, "/");
250 | if (f.startsWith(prefix)) f = `./${f.substr(prefix.length)}`;
251 | else if (!f.startsWith("./") && !f.startsWith("../")) f = `./${f}`;
252 | return f;
253 | }
254 |
255 | function normalizeDir(path: string): string {
256 | return path.replace(/\\/g, "/").replace(/\/+$/, "");
257 | }
258 |
259 | interface BaseNameAndDir {
260 | directory: string;
261 | fileName: string;
262 | }
263 |
264 | function baseNameAndDir(file: string): BaseNameAndDir {
265 | return {
266 | directory: dirname(file),
267 | fileName: basename(file),
268 | };
269 | }
270 |
271 | const bufferEncodingValues = new Set([
272 | "ascii",
273 | "utf8",
274 | "utf-8",
275 | "utf16le",
276 | "ucs2",
277 | "ucs-2",
278 | "base64",
279 | "base64url",
280 | "latin1",
281 | "binary",
282 | "hex",
283 | ]);
284 | function validateBufferEncoding(s: string | undefined): asserts s is BufferEncoding | undefined {
285 | if (!s) return;
286 | if (!bufferEncodingValues.has(s)) throw new Error(`Invalid encoding value '${s}'`);
287 | }
288 |
--------------------------------------------------------------------------------
/packages/validator-cli/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "strict": true,
4 | "target": "ES2022",
5 | "module": "NodeNext",
6 | "moduleResolution": "nodenext",
7 | "sourceMap": false,
8 | "lib": ["esnext"],
9 | "esModuleInterop": true,
10 | "outDir": "dist",
11 | "noEmitOnError": true,
12 | "declaration": false
13 | },
14 | "include": ["src"],
15 | "exclude": ["node_modules", "dist"]
16 | }
17 |
--------------------------------------------------------------------------------
/packages/validator/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | ._*
3 |
4 | node_modules
5 | package-lock.json
6 | .npmrc
7 |
8 | /dist
9 | /scripts/declarations
10 |
--------------------------------------------------------------------------------
/packages/validator/.npmignore:
--------------------------------------------------------------------------------
1 | /scripts/declarations
2 |
--------------------------------------------------------------------------------
/packages/validator/.prettierignore:
--------------------------------------------------------------------------------
1 | *
--------------------------------------------------------------------------------
/packages/validator/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # @typeonly/validator
2 |
3 | ## 1.0.1
4 |
5 | ### Patch Changes
6 |
7 | - 9759c6a: Documentation
8 | - Updated dependencies [9759c6a]
9 | - @typeonly/loader@1.0.1
10 |
11 | ## 1.0.0
12 |
13 | ### Major Changes
14 |
15 | - ESM modules
16 |
17 | ## 0.6.0
18 |
19 | ### Minor Changes
20 |
21 | - upgrade dependencies & syntax
22 |
23 | ### Patch Changes
24 |
25 | - Updated dependencies
26 | - @typeonly/loader@1.0.0
27 |
28 | ## 0.5.3
29 |
30 | ### Patch Changes
31 |
32 | - Improve validation error message (fix)
33 | - @typeonly/loader@0.6.0
34 |
--------------------------------------------------------------------------------
/packages/validator/LICENSE:
--------------------------------------------------------------------------------
1 | CC0 1.0 Universal
2 |
3 | Statement of Purpose
4 |
5 | The laws of most jurisdictions throughout the world automatically confer
6 | exclusive Copyright and Related Rights (defined below) upon the creator and
7 | subsequent owner(s) (each and all, an "owner") of an original work of
8 | authorship and/or a database (each, a "Work").
9 |
10 | Certain owners wish to permanently relinquish those rights to a Work for the
11 | purpose of contributing to a commons of creative, cultural and scientific
12 | works ("Commons") that the public can reliably and without fear of later
13 | claims of infringement build upon, modify, incorporate in other works, reuse
14 | and redistribute as freely as possible in any form whatsoever and for any
15 | purposes, including without limitation commercial purposes. These owners may
16 | contribute to the Commons to promote the ideal of a free culture and the
17 | further production of creative, cultural and scientific works, or to gain
18 | reputation or greater distribution for their Work in part through the use and
19 | efforts of others.
20 |
21 | For these and/or other purposes and motivations, and without any expectation
22 | of additional consideration or compensation, the person associating CC0 with a
23 | Work (the "Affirmer"), to the extent that he or she is an owner of Copyright
24 | and Related Rights in the Work, voluntarily elects to apply CC0 to the Work
25 | and publicly distribute the Work under its terms, with knowledge of his or her
26 | Copyright and Related Rights in the Work and the meaning and intended legal
27 | effect of CC0 on those rights.
28 |
29 | 1. Copyright and Related Rights. A Work made available under CC0 may be
30 | protected by copyright and related or neighboring rights ("Copyright and
31 | Related Rights"). Copyright and Related Rights include, but are not limited
32 | to, the following:
33 |
34 | i. the right to reproduce, adapt, distribute, perform, display, communicate,
35 | and translate a Work;
36 |
37 | ii. moral rights retained by the original author(s) and/or performer(s);
38 |
39 | iii. publicity and privacy rights pertaining to a person's image or likeness
40 | depicted in a Work;
41 |
42 | iv. rights protecting against unfair competition in regards to a Work,
43 | subject to the limitations in paragraph 4(a), below;
44 |
45 | v. rights protecting the extraction, dissemination, use and reuse of data in
46 | a Work;
47 |
48 | vi. database rights (such as those arising under Directive 96/9/EC of the
49 | European Parliament and of the Council of 11 March 1996 on the legal
50 | protection of databases, and under any national implementation thereof,
51 | including any amended or successor version of such directive); and
52 |
53 | vii. other similar, equivalent or corresponding rights throughout the world
54 | based on applicable law or treaty, and any national implementations thereof.
55 |
56 | 2. Waiver. To the greatest extent permitted by, but not in contravention of,
57 | applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and
58 | unconditionally waives, abandons, and surrenders all of Affirmer's Copyright
59 | and Related Rights and associated claims and causes of action, whether now
60 | known or unknown (including existing as well as future claims and causes of
61 | action), in the Work (i) in all territories worldwide, (ii) for the maximum
62 | duration provided by applicable law or treaty (including future time
63 | extensions), (iii) in any current or future medium and for any number of
64 | copies, and (iv) for any purpose whatsoever, including without limitation
65 | commercial, advertising or promotional purposes (the "Waiver"). Affirmer makes
66 | the Waiver for the benefit of each member of the public at large and to the
67 | detriment of Affirmer's heirs and successors, fully intending that such Waiver
68 | shall not be subject to revocation, rescission, cancellation, termination, or
69 | any other legal or equitable action to disrupt the quiet enjoyment of the Work
70 | by the public as contemplated by Affirmer's express Statement of Purpose.
71 |
72 | 3. Public License Fallback. Should any part of the Waiver for any reason be
73 | judged legally invalid or ineffective under applicable law, then the Waiver
74 | shall be preserved to the maximum extent permitted taking into account
75 | Affirmer's express Statement of Purpose. In addition, to the extent the Waiver
76 | is so judged Affirmer hereby grants to each affected person a royalty-free,
77 | non transferable, non sublicensable, non exclusive, irrevocable and
78 | unconditional license to exercise Affirmer's Copyright and Related Rights in
79 | the Work (i) in all territories worldwide, (ii) for the maximum duration
80 | provided by applicable law or treaty (including future time extensions), (iii)
81 | in any current or future medium and for any number of copies, and (iv) for any
82 | purpose whatsoever, including without limitation commercial, advertising or
83 | promotional purposes (the "License"). The License shall be deemed effective as
84 | of the date CC0 was applied by Affirmer to the Work. Should any part of the
85 | License for any reason be judged legally invalid or ineffective under
86 | applicable law, such partial invalidity or ineffectiveness shall not
87 | invalidate the remainder of the License, and in such case Affirmer hereby
88 | affirms that he or she will not (i) exercise any of his or her remaining
89 | Copyright and Related Rights in the Work or (ii) assert any associated claims
90 | and causes of action with respect to the Work, in either case contrary to
91 | Affirmer's express Statement of Purpose.
92 |
93 | 4. Limitations and Disclaimers.
94 |
95 | a. No trademark or patent rights held by Affirmer are waived, abandoned,
96 | surrendered, licensed or otherwise affected by this document.
97 |
98 | b. Affirmer offers the Work as-is and makes no representations or warranties
99 | of any kind concerning the Work, express, implied, statutory or otherwise,
100 | including without limitation warranties of title, merchantability, fitness
101 | for a particular purpose, non infringement, or the absence of latent or
102 | other defects, accuracy, or the present or absence of errors, whether or not
103 | discoverable, all to the greatest extent permissible under applicable law.
104 |
105 | c. Affirmer disclaims responsibility for clearing rights of other persons
106 | that may apply to the Work or any use thereof, including without limitation
107 | any person's Copyright and Related Rights in the Work. Further, Affirmer
108 | disclaims responsibility for obtaining any necessary consents, permissions
109 | or other rights required for any use of the Work.
110 |
111 | d. Affirmer understands and acknowledges that Creative Commons is not a
112 | party to this document and has no duty or obligation with respect to this
113 | CC0 or use of the Work.
114 |
115 | For more information, please see
116 |
117 |
--------------------------------------------------------------------------------
/packages/validator/README.md:
--------------------------------------------------------------------------------
1 | # @typeonly/validator
2 |
3 | [](https://travis-ci.com/paroi-tech/typeonly)
4 | [](https://www.npmjs.com/package/@typeonly/validator)
5 | 
6 | 
7 |
8 | This package is part of **TypeOnly**, a lightweight validation library that uses TypeScript type definitions to validate JSON data. **[Learn more about TypeOnly here](https://www.npmjs.com/package/typeonly)**.
9 |
--------------------------------------------------------------------------------
/packages/validator/bundle-tsd/bundle-tsd.js:
--------------------------------------------------------------------------------
1 | import { readFileSync, writeFileSync } from "node:fs"
2 | import { join } from "node:path"
3 | import { fileURLToPath } from 'node:url';
4 | import { dirname } from 'node:path';
5 |
6 | const __filename = fileURLToPath(import.meta.url);
7 | const __dirname = dirname(__filename);
8 |
9 | const bundleName = "validator"
10 | const compiledDir = join(__dirname, "declarations")
11 | const packageDir = join(__dirname, "..")
12 |
13 | try {
14 | writeFileSync(join(packageDir, "dist", `${bundleName}.d.ts`), makeDefinitionsCode())
15 | } catch (err) {
16 | console.log(err.message, err.stack)
17 | }
18 |
19 | function makeDefinitionsCode() {
20 | const defs = [
21 | "// -- API Definitions --",
22 | cleanGeneratedCode(
23 | removeLocalImportsExports((readFileSync(join(compiledDir, "api.d.ts"), "utf-8")).trim()),
24 | ),
25 | ]
26 | return defs.join("\n\n")
27 | }
28 |
29 | function removeLocalImportsExports(code) {
30 | const localImportExport = /^\s*(import|export) .* from "\.\/.*"\s*;?\s*$/
31 | return code.split("\n").filter(line => {
32 | return !localImportExport.test(line)
33 | }).join("\n").trim()
34 | }
35 |
36 | function cleanGeneratedCode(code) {
37 | return code.replace(/;/g, "").replace(/ /g, " ")
38 | }
39 |
--------------------------------------------------------------------------------
/packages/validator/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@typeonly/validator",
3 | "version": "1.0.1",
4 | "description": "An API to validate JSON data or JavaScript objects, using TypeScript typing definitions.",
5 | "author": "Paroi Team",
6 | "scripts": {
7 | "prepublishOnly": "npm run build && npm run test",
8 | "clear": "rimraf dist/* scripts/declarations/*",
9 | "tsc": "tsc",
10 | "tsc:watch": "tsc --watch",
11 | "bundle-tsd": "node scripts/bundle-tsd",
12 | "build": "npm run clear && npm run tsc && npm run bundle-tsd",
13 | "lint": "biome check . --json-formatter-enabled=false --organize-imports-enabled=false",
14 | "test:watch": "vitest",
15 | "test": "vitest run"
16 | },
17 | "dependencies": {
18 | "@typeonly/loader": "^1.0.1"
19 | },
20 | "devDependencies": {
21 | "@types/node": "22",
22 | "rimraf": "^6.0.1",
23 | "typeonly": "^1.0.1",
24 | "typescript": "^5.7.3",
25 | "vitest": "^3.0.5"
26 | },
27 | "type": "module",
28 | "main": "dist/api.js",
29 | "types": "dist/validator.d.ts",
30 | "repository": {
31 | "type": "git",
32 | "url": "git+https://github.com/paroi-tech/typeonly.git"
33 | },
34 | "homepage": "https://github.com/paroi-tech/typeonly/tree/master/packages/validator",
35 | "license": "CC0-1.0",
36 | "keywords": [
37 | "typescript",
38 | "json",
39 | "validation"
40 | ]
41 | }
--------------------------------------------------------------------------------
/packages/validator/scripts/bundle-tsd.js:
--------------------------------------------------------------------------------
1 | import { readFileSync, writeFileSync } from "node:fs"
2 | import { join } from "node:path"
3 | import { fileURLToPath } from 'node:url';
4 | import { dirname } from 'node:path';
5 |
6 | const __filename = fileURLToPath(import.meta.url);
7 | const __dirname = dirname(__filename);
8 |
9 | const bundleName = "validator"
10 | const compiledDir = join(__dirname, "declarations")
11 | const packageDir = join(__dirname, "..")
12 |
13 | try {
14 | writeFileSync(join(packageDir, "dist", `${bundleName}.d.ts`), makeDefinitionsCode())
15 | } catch (err) {
16 | console.log(err.message, err.stack)
17 | }
18 |
19 | function makeDefinitionsCode() {
20 | const defs = [
21 | "// -- API Definitions --",
22 | cleanGeneratedCode(
23 | removeLocalImportsExports((readFileSync(join(compiledDir, "api.d.ts"), "utf-8")).trim()),
24 | ),
25 | ]
26 | return defs.join("\n\n")
27 | }
28 |
29 | function removeLocalImportsExports(code) {
30 | const localImportExport = /^\s*(import|export) .* from "\.\/.*"\s*;?\s*$/
31 | return code.split("\n").filter(line => {
32 | return !localImportExport.test(line)
33 | }).join("\n").trim()
34 | }
35 |
36 | function cleanGeneratedCode(code) {
37 | return code.replace(/;/g, "").replace(/ /g, " ")
38 | }
39 |
--------------------------------------------------------------------------------
/packages/validator/src/api.ts:
--------------------------------------------------------------------------------
1 | import {
2 | type AsyncReadModulesOptions,
3 | type Modules,
4 | type SyncReadModulesOptions,
5 | isSyncReadModulesOptions,
6 | loadModules,
7 | } from "@typeonly/loader";
8 | import Validator from "./Validator.js";
9 |
10 | export interface ValidatorOptions {
11 | acceptAdditionalProperties?: boolean;
12 | }
13 |
14 | export interface SyncReadModulesValidatorOptions extends ValidatorOptions, SyncReadModulesOptions {}
15 |
16 | export interface AsyncReadModulesValidatorOptions
17 | extends ValidatorOptions,
18 | AsyncReadModulesOptions {}
19 |
20 | export type CreateValidatorOptions =
21 | | SyncReadModulesValidatorOptions
22 | | AsyncReadModulesValidatorOptions;
23 |
24 | export function createValidator(options: SyncReadModulesValidatorOptions): TypeOnlyValidator;
25 | export function createValidator(
26 | options: AsyncReadModulesValidatorOptions,
27 | ): Promise;
28 | export function createValidator(options: CreateValidatorOptions): any {
29 | if (isSyncReadModulesOptions(options)) return createValidatorSync(options);
30 | return createValidatorAsync(options);
31 | }
32 |
33 | function createValidatorSync(options: SyncReadModulesValidatorOptions): TypeOnlyValidator {
34 | return createValidatorFromModules(loadModules(options), options);
35 | }
36 |
37 | async function createValidatorAsync(
38 | options: AsyncReadModulesValidatorOptions,
39 | ): Promise {
40 | return createValidatorFromModules(await loadModules(options), options);
41 | }
42 |
43 | export function createValidatorFromModules(
44 | modules: Modules,
45 | options?: ValidatorOptions,
46 | ): TypeOnlyValidator {
47 | const validator = new Validator(modules, options);
48 | return {
49 | validate: (typeName: string, val: unknown, moduleName?: string) => {
50 | return validator.validate(
51 | moduleName ?? getDefaultModuleName(modules, typeName),
52 | typeName,
53 | val,
54 | );
55 | },
56 | };
57 | }
58 |
59 | function getDefaultModuleName(modules: Modules, typeName: string): string {
60 | const candidates = [];
61 | for (const moduleName of Object.keys(modules)) {
62 | const module = modules[moduleName];
63 | if (module.namedTypes[typeName]) candidates.push(moduleName);
64 | }
65 | if (candidates.length === 0) throw new Error(`Cannot find type '${typeName}' in modules.`);
66 | if (candidates.length > 1)
67 | throw new Error(
68 | `There are several module candidates for type '${typeName}': '${candidates.join("', '")}'.`,
69 | );
70 | return candidates[0];
71 | }
72 |
73 | export interface TypeOnlyValidator {
74 | validate(typeName: string, val: unknown, moduleName?: string): ValidateResult;
75 | }
76 |
77 | export interface ValidateResult {
78 | valid: boolean;
79 | error?: string;
80 | }
81 |
--------------------------------------------------------------------------------
/packages/validator/src/error-message.ts:
--------------------------------------------------------------------------------
1 | import type { Type } from "@typeonly/loader";
2 | import type { Unmatch } from "./Validator.js";
3 |
4 | export function makeErrorMessage(unmatchs: Unmatch[]): string {
5 | const messages: string[] = [];
6 | for (const { val, type, cause, parentContextMsg } of unmatchs) {
7 | let message = `${valueAsString(val)} is not conform to ${typeAsString(type)}`;
8 | if (cause) message += `: ${cause}`;
9 | if (parentContextMsg) message = `In ${parentContextMsg}, value ${message}`;
10 | else message = `Value ${message}`;
11 | message += ".";
12 | messages.push(message);
13 | }
14 | messages.reverse();
15 | return messages.join("\n");
16 | }
17 |
18 | function valueAsString(val: unknown): string {
19 | if (Array.isArray(val)) return `[array of ${val.length}]`;
20 | switch (typeof val) {
21 | case "string":
22 | case "number":
23 | case "boolean":
24 | case "bigint":
25 | return `'${primitiveValueAsString(val)}'`;
26 | case "object":
27 | if (val === null) return "null";
28 | return objectAsString(val as object);
29 | case "function":
30 | case "symbol":
31 | case "undefined":
32 | return typeof val;
33 | default:
34 | throw new Error(`Unexpected typeof val: ${typeof val}`);
35 | }
36 | }
37 |
38 | function primitiveValueAsString(val: string | number | bigint | boolean) {
39 | switch (typeof val) {
40 | case "string":
41 | return JSON.stringify(val.length <= 12 ? val : `${val.substring(0, 12)}…`);
42 | case "number":
43 | case "boolean":
44 | return String(val);
45 | case "bigint":
46 | return `${val}n`;
47 | default:
48 | throw new Error(`Unexpected primitive type: ${typeof val}`);
49 | }
50 | }
51 |
52 | function objectAsString(obj: object) {
53 | return `{${Object.entries(obj)
54 | .map(([key, val]) => {
55 | const t = typeof val;
56 | if (t === "string" || t === "number" || t === "bigint" || t === "boolean")
57 | return `${key}: ${primitiveValueAsString(val)}`;
58 | return key;
59 | })
60 | .join(", ")}}`;
61 | }
62 |
63 | export function typeAsString(type: Type): string {
64 | if ((type as any).name) return (type as any).name;
65 | switch (type.kind) {
66 | case "name":
67 | return type.refName;
68 | case "localRef":
69 | return type.refName;
70 | case "importedRef":
71 | return type.refName;
72 | case "array":
73 | return `${typeAsString(type.itemType)}[]`;
74 | case "tuple":
75 | return `[${type.itemTypes.map(typeAsString).join(", ")}]`;
76 | case "composite":
77 | return type.op;
78 | case "function":
79 | case "genericInstance":
80 | case "interface":
81 | return type.kind;
82 | case "genericParameterName":
83 | return type.genericParameterName;
84 | case "keyof":
85 | return `keyof ${typeAsString(type.type)}`;
86 | case "literal":
87 | return JSON.stringify(type.literal);
88 | case "member":
89 | const propName =
90 | typeof type.memberName !== "string"
91 | ? JSON.stringify(type.memberName.literal)
92 | : type.memberName;
93 | return `${typeAsString(type.parentType)}[${propName}]`;
94 | default:
95 | throw new Error(`Unexpected type: ${(type as Type).kind}`);
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/packages/validator/src/helpers.ts:
--------------------------------------------------------------------------------
1 | const primitiveTypeNames = new Set([
2 | "boolean",
3 | "number",
4 | "bigint",
5 | "string",
6 | "undefined",
7 | "symbol",
8 | ]);
9 |
10 | export function hasAncestor(val: unknown, ancestorName: string): boolean {
11 | let obj: object;
12 | if (primitiveTypeNames.has(typeof val) || val === null) obj = Object(val);
13 | else if (typeof val === "function") {
14 | const name: string = val.name;
15 | if (name === ancestorName) return true;
16 | obj = val.prototype;
17 | } else obj = val as object;
18 |
19 | do {
20 | if (obj.constructor.name === ancestorName) return true;
21 | obj = Object.getPrototypeOf(obj);
22 | } while (obj);
23 |
24 | return false;
25 | }
26 |
--------------------------------------------------------------------------------
/packages/validator/tests/interface.spec.ts:
--------------------------------------------------------------------------------
1 | import { createStandaloneRtoModule, parseTypeOnly } from "typeonly";
2 | import { describe, expect, test } from "vitest";
3 | import { createValidator } from "../src/api.js";
4 |
5 | describe("Validate Interface", () => {
6 | test("interface with primitive types", async () => {
7 | const source = `
8 | export interface A {
9 | a: number,
10 | b: string
11 | }
12 | `;
13 |
14 | const validator = await createValidator({
15 | modulePaths: ["./mod1"],
16 | rtoModuleProvider: async () =>
17 | createStandaloneRtoModule({
18 | ast: parseTypeOnly({ source }),
19 | }),
20 | });
21 |
22 | const result = validator.validate(
23 | "A",
24 | {
25 | a: 12,
26 | b: 22,
27 | },
28 | "./mod1",
29 | );
30 |
31 | expect(result.valid).toBe(false);
32 | expect(result.error).toBeDefined();
33 | });
34 |
35 | // test("interface with array type", async () => {
36 | // const source = `
37 | // export type A = {
38 | // a: number[],
39 | // b: string
40 | // }
41 | // `
42 | // const validator = await createValidator({
43 | // modulePaths: ["./mod1"],
44 | // rtoModuleProvider: async () => createStandaloneRtoModule({
45 | // ast: parseTypeOnly({ source })
46 | // })
47 | // })
48 |
49 | // const result = validator.validate(
50 | // "A",
51 | // {
52 | // a: [12, "23"],
53 | // b: "ab"
54 | // },
55 | // "./mod1"
56 | // )
57 |
58 | // expect(result.valid).toBe(false)
59 | // expect(result.error).toBeDefined()
60 | // })
61 |
62 | // test("interface with literal type", async () => {
63 | // const source = `
64 | // export type A = {
65 | // a: "test",
66 | // b: string
67 | // }
68 | // `
69 |
70 | // const validator = await createValidator({
71 | // modulePaths: ["./mod1"],
72 | // rtoModuleProvider: async () => createStandaloneRtoModule({
73 | // ast: parseTypeOnly({ source })
74 | // })
75 | // })
76 |
77 | // const result = validator.validate(
78 | // "A",
79 | // {
80 | // a: "test1",
81 | // b: "ab"
82 | // },
83 | // "./mod1"
84 | // )
85 |
86 | // expect(result.valid).toBe(false)
87 | // expect(result.error).toBeDefined()
88 | // })
89 |
90 | // test("interface with tuple", async () => {
91 | // const source = `
92 | // export interface A {
93 | // a: [string, number],
94 | // b: string
95 | // }
96 | // `
97 | // const validator = await createValidator({
98 | // modulePaths: ["./mod1"],
99 | // rtoModuleProvider: async () => createStandaloneRtoModule({
100 | // ast: parseTypeOnly({ source })
101 | // })
102 | // })
103 |
104 | // const result = validator.validate(
105 | // "A",
106 | // {
107 | // a: ["sd", "23"],
108 | // b: "ab"
109 | // },
110 | // "./mod1"
111 | // )
112 |
113 | // expect(result.valid).toBe(false)
114 | // expect(result.error).toBeDefined()
115 | // })
116 |
117 | // test("validate interface with depth", async () => {
118 | // const source = `
119 | // export type A = {
120 | // a: {
121 | // c: { d: boolean }[]
122 | // },
123 | // b: string
124 | // }
125 | // `
126 | // const validator = await createValidator({
127 | // modulePaths: ["./mod1"],
128 | // rtoModuleProvider: async () => createStandaloneRtoModule({
129 | // ast: parseTypeOnly({ source })
130 | // })
131 | // })
132 |
133 | // const result = validator.validate(
134 | // "A",
135 | // {
136 | // a: {
137 | // c: [
138 | // {
139 | // d: false
140 | // }
141 | // ]
142 | // },
143 | // b: "ab"
144 | // },
145 | // "./mod1"
146 | // )
147 |
148 | // expect(result.valid).toBe(true)
149 | // expect(result.error).toBeUndefined()
150 | // })
151 | });
152 |
--------------------------------------------------------------------------------
/packages/validator/tests/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "strict": true,
4 | "target": "ES2022",
5 | "module": "NodeNext",
6 | "moduleResolution": "nodenext",
7 | "sourceMap": false,
8 | "lib": ["esnext"],
9 | "noEmit": true
10 | },
11 | "include": ["."]
12 | }
13 |
--------------------------------------------------------------------------------
/packages/validator/tests/types.spec.ts:
--------------------------------------------------------------------------------
1 | import { loadModules } from "@typeonly/loader";
2 | import { createStandaloneRtoModule, parseTypeOnly } from "typeonly";
3 | import { describe, expect, test } from "vitest";
4 | import { createValidator, createValidatorFromModules } from "../src/api.js";
5 |
6 | describe("Validate Types", () => {
7 | test("TypeName", async () => {
8 | const source = `
9 | export type A = B
10 | type B = number
11 | `;
12 | const modules = await loadModules({
13 | modulePaths: ["./mod1"],
14 | rtoModuleProvider: async () =>
15 | createStandaloneRtoModule({
16 | ast: parseTypeOnly({ source }),
17 | }),
18 | });
19 | const validator = createValidatorFromModules(modules);
20 |
21 | const response = validator.validate("A", "12", "./mod1");
22 |
23 | expect(response.valid).toBe(false);
24 | expect(response.error).not.toBeUndefined();
25 | });
26 |
27 | test("ArrayType", async () => {
28 | const source = `
29 | export type A = number[]
30 | `;
31 | const validator = await createValidator({
32 | modulePaths: ["./mod1"],
33 | rtoModuleProvider: async () =>
34 | createStandaloneRtoModule({
35 | ast: parseTypeOnly({ source }),
36 | }),
37 | });
38 |
39 | const result = validator.validate("A", [12, "90"], "./mod1");
40 |
41 | expect(result.valid).toBe(false);
42 | expect(result.error).toBeDefined();
43 | });
44 |
45 | test("TupleType", async () => {
46 | const source = `
47 | export type A = [number, string]
48 | `;
49 | const validator = await createValidator({
50 | modulePaths: ["./mod1"],
51 | rtoModuleProvider: async () =>
52 | createStandaloneRtoModule({
53 | ast: parseTypeOnly({ source }),
54 | }),
55 | });
56 |
57 | const result = validator.validate("A", [12, "90", 23], "./mod1");
58 |
59 | expect(result.valid).toBe(false);
60 | expect(result.error).toBeDefined();
61 | });
62 |
63 | test("KeyofType", async () => {
64 | const source = `
65 | export type A = keyof B
66 | interface B {
67 | [A: number]: boolean
68 | a: string
69 | }
70 | `;
71 |
72 | const validator = await createValidator({
73 | modulePaths: ["./mod1"],
74 | rtoModuleProvider: async () =>
75 | createStandaloneRtoModule({
76 | ast: parseTypeOnly({ source }),
77 | }),
78 | });
79 |
80 | const result = validator.validate("A", 12, "./mod1");
81 |
82 | expect(result.valid).toBe(true);
83 | expect(result.error).toBeUndefined();
84 | });
85 |
86 | test("MemberType", async () => {
87 | const source = `
88 | export type A = B["a"]
89 | interface B {
90 | a: string
91 | }
92 | `;
93 |
94 | const validator = await createValidator({
95 | modulePaths: ["./mod1"],
96 | rtoModuleProvider: async () =>
97 | createStandaloneRtoModule({
98 | ast: parseTypeOnly({ source }),
99 | }),
100 | });
101 |
102 | const result = validator.validate("A", "sdds", "./mod1");
103 |
104 | expect(result.valid).toBe(true);
105 | expect(result.error).toBeUndefined();
106 | });
107 |
108 | test("FunctionType", async () => {
109 | const source = `
110 | export type A = () => number
111 | `;
112 |
113 | const validator = await createValidator({
114 | modulePaths: ["./mod1"],
115 | rtoModuleProvider: async () =>
116 | createStandaloneRtoModule({
117 | ast: parseTypeOnly({ source }),
118 | }),
119 | });
120 |
121 | const result = validator.validate("A", () => "12", "./mod1");
122 |
123 | expect(result.valid).toBe(true);
124 | expect(result.error).toBeUndefined();
125 | });
126 |
127 | test("CompositeType", async () => {
128 | const source = `
129 | export type A = "12" | 12
130 | `;
131 |
132 | const validator = await createValidator({
133 | modulePaths: ["./mod1"],
134 | rtoModuleProvider: async () =>
135 | createStandaloneRtoModule({
136 | ast: parseTypeOnly({ source }),
137 | }),
138 | });
139 |
140 | const result = validator.validate("A", 12, "./mod1");
141 |
142 | expect(result.valid).toBe(true);
143 | expect(result.error).toBeUndefined();
144 | });
145 |
146 | test("CompositeType with null type", async () => {
147 | const source = `
148 | export type A = string | null
149 | `;
150 |
151 | const validator = await createValidator({
152 | modulePaths: ["./mod1"],
153 | rtoModuleProvider: async () =>
154 | createStandaloneRtoModule({
155 | ast: parseTypeOnly({ source }),
156 | }),
157 | });
158 |
159 | const result = validator.validate("A", null, "./mod1");
160 | expect(result.valid).toBe(true);
161 | expect(result.error).toBeUndefined();
162 | });
163 |
164 | test("LiteralType", async () => {
165 | const source = `
166 | export type A = "ff"
167 | `;
168 |
169 | const validator = await createValidator({
170 | modulePaths: ["./mod1"],
171 | rtoModuleProvider: async () =>
172 | createStandaloneRtoModule({
173 | ast: parseTypeOnly({ source }),
174 | }),
175 | });
176 |
177 | const result = validator.validate("A", "ff", "./mod1");
178 |
179 | expect(result.valid).toBe(true);
180 | expect(result.error).toBeUndefined();
181 | });
182 |
183 | test("LocalTypeRef", async () => {
184 | const source = `
185 | export type A = B
186 | type B = number
187 | `;
188 |
189 | const validator = await createValidator({
190 | modulePaths: ["./mod1"],
191 | rtoModuleProvider: async () =>
192 | createStandaloneRtoModule({
193 | ast: parseTypeOnly({ source }),
194 | }),
195 | });
196 |
197 | const result = validator.validate("A", 12, "./mod1");
198 |
199 | expect(result.valid).toBe(true);
200 | expect(result.error).toBeUndefined();
201 | });
202 |
203 | test("Use default module name", async () => {
204 | const source = `
205 | export type A = B
206 | type B = number
207 | `;
208 | const modules = await loadModules({
209 | modulePaths: ["./mod1"],
210 | rtoModuleProvider: async () =>
211 | createStandaloneRtoModule({
212 | ast: parseTypeOnly({ source }),
213 | }),
214 | });
215 | const validator = createValidatorFromModules(modules);
216 |
217 | const response = validator.validate("A", "12");
218 |
219 | expect(response.valid).toBe(false);
220 | expect(response.error).not.toBeUndefined();
221 | });
222 |
223 | // test("GenericInstance", async () => {
224 | // const source = `
225 | // export type A = B
226 | // type B = {a: number}
227 | // `
228 |
229 | // // type A = B
230 | // // type B = G
231 | // // type G = Array
232 |
233 | // const validator = await createValidator({
234 | // modulePaths: ["./mod1"],
235 | // rtoModuleProvider: async () => createStandaloneRtoModule({
236 | // ast: parseTypeOnly({ source })
237 | // })
238 | // })
239 |
240 | // const result = validator.validate("A", 12, "./mod1")
241 |
242 | // expect(result.valid).toBe(true)
243 | // expect(result.error).toBeUndefined()
244 | // })
245 | });
246 |
--------------------------------------------------------------------------------
/packages/validator/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "strict": true,
4 | "target": "ES2022",
5 | "module": "NodeNext",
6 | "moduleResolution": "nodenext",
7 | "sourceMap": false,
8 | "lib": ["esnext"],
9 | "outDir": "dist",
10 | "noEmitOnError": true,
11 | "declaration": true,
12 | "declarationDir": "scripts/declarations"
13 | },
14 | "include": ["src"],
15 | "exclude": ["node_modules", "dist"]
16 | }
17 |
--------------------------------------------------------------------------------
/packages/validator/vitest.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "vitest/config";
2 |
3 | export default defineConfig({
4 | test: {
5 | include: ["**/src/**/*.spec.ts", "**/tests/**/*.spec.ts"],
6 | environment: "node",
7 | },
8 | });
9 |
--------------------------------------------------------------------------------