@di-framework/tsc 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +117 -0
- package/bin/dtsc.cjs +43 -0
- package/package.json +69 -0
- package/plugin/go.mod +10 -0
- package/plugin/main.go +457 -0
- package/plugin.cjs +9 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 DI Framework Contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# @di-framework/tsc
|
|
2
|
+
|
|
3
|
+
[`ttsc`](https://ttsc.dev) transform plugin that injects **runtime parameter checks** from your TypeScript types.
|
|
4
|
+
|
|
5
|
+
Source stays plain TypeScript — no `assert()`, schemas, or decorators. On emit, function bodies get `typeof` / shape guards synthesized from parameter types.
|
|
6
|
+
|
|
7
|
+
> MVP: function declarations with required identifier params; primitives + plain object/interface props. See [Limitations](#limitations).
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm i -D ttsc typescript @di-framework/tsc
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Consumers supply `ttsc` and TypeScript 7+; this package is the transform only.
|
|
16
|
+
|
|
17
|
+
First build compiles the Go sidecar (cached afterward). Needs a Go toolchain (`go` 1.26+ recommended; `ttsc` can pin via `TTSC_GO_BINARY`).
|
|
18
|
+
|
|
19
|
+
## Setup
|
|
20
|
+
|
|
21
|
+
`tsconfig.json`:
|
|
22
|
+
|
|
23
|
+
```json
|
|
24
|
+
{
|
|
25
|
+
"compilerOptions": {
|
|
26
|
+
"strict": true,
|
|
27
|
+
"plugins": [{ "transform": "@di-framework/tsc" }]
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
The package also declares `ttsc.plugin` for auto-discovery when listed in `devDependencies`. Explicit `plugins[]` is recommended so wiring is obvious.
|
|
33
|
+
|
|
34
|
+
Build with `ttsc`, not stock `tsc`:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
npx ttsc --emit
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Example
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
interface User {
|
|
44
|
+
id: number;
|
|
45
|
+
name: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function greet(user: User): string {
|
|
49
|
+
return `hello ${user.name}`;
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Emitted JS (simplified):
|
|
54
|
+
|
|
55
|
+
```js
|
|
56
|
+
function greet(user) {
|
|
57
|
+
if (typeof user !== "object" || user === null)
|
|
58
|
+
throw new TypeError("Expected user to be an object");
|
|
59
|
+
if (typeof user.id !== "number")
|
|
60
|
+
throw new TypeError("Expected user.id to be a number");
|
|
61
|
+
if (typeof user.name !== "string")
|
|
62
|
+
throw new TypeError("Expected user.name to be a string");
|
|
63
|
+
return `hello ${user.name}`;
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## How it works
|
|
68
|
+
|
|
69
|
+
1. `ttsc` loads the program (parse + typecheck).
|
|
70
|
+
2. This plugin walks `FunctionDeclaration` nodes.
|
|
71
|
+
3. For each required parameter, it reads the type (syntax keywords, else checker).
|
|
72
|
+
4. It synthesizes `if` / `throw` AST nodes with typescript-go `NodeFactory` and prepends them to the body.
|
|
73
|
+
5. `EmitAllRaw` prints JavaScript from the mutated AST.
|
|
74
|
+
|
|
75
|
+
## Limitations
|
|
76
|
+
|
|
77
|
+
Skipped today:
|
|
78
|
+
|
|
79
|
+
- methods, arrow functions, function expressions
|
|
80
|
+
- optional / default / rest / destructured parameters
|
|
81
|
+
- unions, arrays, tuples, classes, branded types, typia-style tags
|
|
82
|
+
|
|
83
|
+
## Monorepo
|
|
84
|
+
|
|
85
|
+
Workspace package at `packages/di-framework-tsc`. It is listed in the CLI build/publish allowlists; `build` is a no-op (plugin.cjs + Go sidecar, not the TS5 `tsc` graph). Peers `ttsc` and TypeScript 7+ stay out of root/`@di-framework/*` core packages.
|
|
86
|
+
|
|
87
|
+
Isolated smoke fixture (installs its own ttsc + TS7):
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
cd packages/di-framework-tsc/fixture
|
|
91
|
+
bun install
|
|
92
|
+
bun run smoke # ttsc --emit + check injected guards
|
|
93
|
+
# or: bun run smoke:dtsc
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Publish (maintainers)
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
cd packages/di-framework-tsc
|
|
100
|
+
bun run pack:check # npm pack --dry-run
|
|
101
|
+
npm login # once; scoped package needs org access
|
|
102
|
+
npm publish # publishConfig.access=public
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Smoke-test a packed tarball in another project:
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
npm pack
|
|
109
|
+
cd /path/to/other-project
|
|
110
|
+
npm i -D ttsc typescript /path/to/di-framework-tsc-0.1.0.tgz
|
|
111
|
+
# add plugins: [{ "transform": "@di-framework/tsc" }]
|
|
112
|
+
npx ttsc --emit
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## License
|
|
116
|
+
|
|
117
|
+
MIT
|
package/bin/dtsc.cjs
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const { spawnSync } = require('node:child_process');
|
|
5
|
+
const fs = require('node:fs');
|
|
6
|
+
const path = require('node:path');
|
|
7
|
+
|
|
8
|
+
function findTtscJs(startDir) {
|
|
9
|
+
let dir = path.resolve(startDir);
|
|
10
|
+
for (;;) {
|
|
11
|
+
const candidate = path.join(dir, 'node_modules', 'ttsc', 'lib', 'launcher', 'ttsc.js');
|
|
12
|
+
if (fs.existsSync(candidate)) return candidate;
|
|
13
|
+
const parent = path.dirname(dir);
|
|
14
|
+
if (parent === dir) return null;
|
|
15
|
+
dir = parent;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const args = process.argv.slice(2);
|
|
20
|
+
const ttscJs = findTtscJs(process.cwd());
|
|
21
|
+
|
|
22
|
+
let result;
|
|
23
|
+
if (ttscJs) {
|
|
24
|
+
result = spawnSync(process.execPath, [ttscJs, ...args], {
|
|
25
|
+
stdio: 'inherit',
|
|
26
|
+
env: process.env,
|
|
27
|
+
});
|
|
28
|
+
} else {
|
|
29
|
+
result = spawnSync('ttsc', args, {
|
|
30
|
+
stdio: 'inherit',
|
|
31
|
+
env: process.env,
|
|
32
|
+
shell: process.platform === 'win32',
|
|
33
|
+
});
|
|
34
|
+
if (result.error && result.error.code === 'ENOENT') {
|
|
35
|
+
console.error(
|
|
36
|
+
'dtsc: could not find ttsc. Install it in this project (`npm i -D ttsc`) or on PATH.',
|
|
37
|
+
);
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (result.signal) process.kill(process.pid, result.signal);
|
|
43
|
+
process.exit(result.status ?? 1);
|
package/package.json
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@di-framework/tsc",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "ttsc transform that injects runtime parameter checks from TypeScript types (assertAll)",
|
|
5
|
+
"author": "@di-framework Contributors",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"private": false,
|
|
8
|
+
"type": "commonjs",
|
|
9
|
+
"main": "plugin.cjs",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": "./plugin.cjs"
|
|
12
|
+
},
|
|
13
|
+
"bin": {
|
|
14
|
+
"dtsc": "bin/dtsc.cjs"
|
|
15
|
+
},
|
|
16
|
+
"ttsc": {
|
|
17
|
+
"plugin": {
|
|
18
|
+
"transform": "@di-framework/tsc"
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"bin",
|
|
23
|
+
"plugin.cjs",
|
|
24
|
+
"plugin",
|
|
25
|
+
"README.md",
|
|
26
|
+
"LICENSE"
|
|
27
|
+
],
|
|
28
|
+
"engines": {
|
|
29
|
+
"node": ">=18"
|
|
30
|
+
},
|
|
31
|
+
"keywords": [
|
|
32
|
+
"ttsc",
|
|
33
|
+
"ttsc-plugin",
|
|
34
|
+
"tsgo",
|
|
35
|
+
"typescript",
|
|
36
|
+
"runtime-checks",
|
|
37
|
+
"assertAll"
|
|
38
|
+
],
|
|
39
|
+
"repository": {
|
|
40
|
+
"type": "git",
|
|
41
|
+
"url": "https://github.com/di-framework/di-framework",
|
|
42
|
+
"directory": "packages/di-framework-tsc"
|
|
43
|
+
},
|
|
44
|
+
"bugs": {
|
|
45
|
+
"url": "https://github.com/di-framework/di-framework/issues"
|
|
46
|
+
},
|
|
47
|
+
"homepage": "https://github.com/di-framework/di-framework#readme",
|
|
48
|
+
"peerDependencies": {
|
|
49
|
+
"ttsc": ">=0.25.0",
|
|
50
|
+
"typescript": ">=7.0.0"
|
|
51
|
+
},
|
|
52
|
+
"peerDependenciesMeta": {
|
|
53
|
+
"ttsc": {
|
|
54
|
+
"optional": false
|
|
55
|
+
},
|
|
56
|
+
"typescript": {
|
|
57
|
+
"optional": false
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
"publishConfig": {
|
|
61
|
+
"access": "public"
|
|
62
|
+
},
|
|
63
|
+
"scripts": {
|
|
64
|
+
"build": "bun -e \"console.log('@di-framework/tsc: plugin package (plugin.cjs + Go sidecar), no TS5 compile step')\"",
|
|
65
|
+
"pack:check": "npm pack --dry-run",
|
|
66
|
+
"test": "bun test",
|
|
67
|
+
"smoke": "bun run --cwd fixture smoke"
|
|
68
|
+
}
|
|
69
|
+
}
|
package/plugin/go.mod
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
module github.com/di-framework/tsc
|
|
2
|
+
|
|
3
|
+
go 1.26
|
|
4
|
+
|
|
5
|
+
require (
|
|
6
|
+
github.com/samchon/ttsc/packages/ttsc v0.0.0
|
|
7
|
+
github.com/microsoft/typescript-go/shim/ast v0.0.0
|
|
8
|
+
github.com/microsoft/typescript-go/shim/checker v0.0.0
|
|
9
|
+
github.com/microsoft/typescript-go/shim/printer v0.0.0
|
|
10
|
+
)
|
package/plugin/main.go
ADDED
|
@@ -0,0 +1,457 @@
|
|
|
1
|
+
// @di-framework/tsc injects runtime typeof/shape checks into function
|
|
2
|
+
// bodies from parameter types (assertAll). Transparent: no source markers.
|
|
3
|
+
// Checks are synthesized as AST nodes (NodeFactory), not text edits.
|
|
4
|
+
package main
|
|
5
|
+
|
|
6
|
+
import (
|
|
7
|
+
"encoding/json"
|
|
8
|
+
"flag"
|
|
9
|
+
"fmt"
|
|
10
|
+
"os"
|
|
11
|
+
"path/filepath"
|
|
12
|
+
"strings"
|
|
13
|
+
|
|
14
|
+
shimast "github.com/microsoft/typescript-go/shim/ast"
|
|
15
|
+
shimchecker "github.com/microsoft/typescript-go/shim/checker"
|
|
16
|
+
shimprinter "github.com/microsoft/typescript-go/shim/printer"
|
|
17
|
+
|
|
18
|
+
"github.com/samchon/ttsc/packages/ttsc/driver"
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
type options struct {
|
|
22
|
+
cwd string
|
|
23
|
+
emit bool
|
|
24
|
+
noEmit bool
|
|
25
|
+
outDir string
|
|
26
|
+
tsconfig string
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
type transformResult struct {
|
|
30
|
+
Diagnostics []any `json:"diagnostics,omitempty"`
|
|
31
|
+
TypeScript map[string]string `json:"typescript"`
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
func main() {
|
|
35
|
+
os.Exit(run(os.Args[1:]))
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
func run(args []string) int {
|
|
39
|
+
if len(args) == 0 {
|
|
40
|
+
fmt.Fprintln(os.Stderr, "@di-framework/tsc: command required")
|
|
41
|
+
return 2
|
|
42
|
+
}
|
|
43
|
+
switch args[0] {
|
|
44
|
+
case "version", "-v", "--version":
|
|
45
|
+
fmt.Fprintln(os.Stdout, "@di-framework/tsc 0.1.0")
|
|
46
|
+
return 0
|
|
47
|
+
case "check":
|
|
48
|
+
return 0
|
|
49
|
+
case "transform":
|
|
50
|
+
return runTransform(args[1:])
|
|
51
|
+
case "build":
|
|
52
|
+
return runBuild(args[1:])
|
|
53
|
+
default:
|
|
54
|
+
fmt.Fprintf(os.Stderr, "@di-framework/tsc: unknown command %q\n", args[0])
|
|
55
|
+
return 2
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
func runTransform(args []string) int {
|
|
60
|
+
opts, ok := parseOptions("transform", args)
|
|
61
|
+
if !ok {
|
|
62
|
+
return 2
|
|
63
|
+
}
|
|
64
|
+
prog, ok := loadProgram(opts)
|
|
65
|
+
if !ok {
|
|
66
|
+
return 2
|
|
67
|
+
}
|
|
68
|
+
defer prog.Close()
|
|
69
|
+
|
|
70
|
+
injectProgram(prog)
|
|
71
|
+
|
|
72
|
+
// Host treats typescript[path] as opaque text — print the mutated AST.
|
|
73
|
+
printer := shimprinter.NewPrinter(
|
|
74
|
+
shimprinter.PrinterOptions{},
|
|
75
|
+
shimprinter.PrintHandlers{},
|
|
76
|
+
nil,
|
|
77
|
+
)
|
|
78
|
+
out := transformResult{TypeScript: map[string]string{}}
|
|
79
|
+
for _, file := range prog.SourceFiles() {
|
|
80
|
+
if file == nil || file.IsDeclarationFile {
|
|
81
|
+
continue
|
|
82
|
+
}
|
|
83
|
+
out.TypeScript[outputKey(opts.cwd, file.FileName())] = shimprinter.EmitSourceFile(printer, file)
|
|
84
|
+
}
|
|
85
|
+
data, err := json.Marshal(out)
|
|
86
|
+
if err != nil {
|
|
87
|
+
fmt.Fprintf(os.Stderr, "@di-framework/tsc: marshal failed: %v\n", err)
|
|
88
|
+
return 3
|
|
89
|
+
}
|
|
90
|
+
fmt.Fprintln(os.Stdout, string(data))
|
|
91
|
+
return 0
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
func runBuild(args []string) int {
|
|
95
|
+
opts, ok := parseOptions("build", args)
|
|
96
|
+
if !ok {
|
|
97
|
+
return 2
|
|
98
|
+
}
|
|
99
|
+
prog, ok := loadProgram(opts)
|
|
100
|
+
if !ok {
|
|
101
|
+
return 2
|
|
102
|
+
}
|
|
103
|
+
defer prog.Close()
|
|
104
|
+
|
|
105
|
+
injectProgram(prog)
|
|
106
|
+
if opts.noEmit {
|
|
107
|
+
return 0
|
|
108
|
+
}
|
|
109
|
+
_, emitDiags, err := prog.EmitAllRaw(nil)
|
|
110
|
+
if err != nil {
|
|
111
|
+
fmt.Fprintf(os.Stderr, "@di-framework/tsc: emit failed: %v\n", err)
|
|
112
|
+
return 3
|
|
113
|
+
}
|
|
114
|
+
for _, d := range emitDiags {
|
|
115
|
+
fmt.Fprintln(os.Stderr, d.String())
|
|
116
|
+
}
|
|
117
|
+
if len(emitDiags) > 0 {
|
|
118
|
+
return 2
|
|
119
|
+
}
|
|
120
|
+
return 0
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
func loadProgram(opts options) (*driver.Program, bool) {
|
|
124
|
+
prog, parseDiags, err := driver.LoadProgram(opts.cwd, opts.tsconfig, driver.LoadProgramOptions{
|
|
125
|
+
ForceEmit: opts.emit,
|
|
126
|
+
ForceNoEmit: opts.noEmit,
|
|
127
|
+
OutDir: opts.outDir,
|
|
128
|
+
})
|
|
129
|
+
if err != nil {
|
|
130
|
+
fmt.Fprintf(os.Stderr, "@di-framework/tsc: %v\n", err)
|
|
131
|
+
return nil, false
|
|
132
|
+
}
|
|
133
|
+
if len(parseDiags) > 0 {
|
|
134
|
+
driver.WritePrettyDiagnostics(os.Stderr, parseDiags, opts.cwd)
|
|
135
|
+
prog.Close()
|
|
136
|
+
return nil, false
|
|
137
|
+
}
|
|
138
|
+
if diags := prog.Diagnostics(); len(diags) > 0 {
|
|
139
|
+
driver.WritePrettyDiagnostics(os.Stderr, diags, opts.cwd)
|
|
140
|
+
prog.Close()
|
|
141
|
+
return nil, false
|
|
142
|
+
}
|
|
143
|
+
return prog, true
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
func injectProgram(prog *driver.Program) {
|
|
147
|
+
factory := shimast.NewNodeFactory(shimast.NodeFactoryHooks{})
|
|
148
|
+
for _, file := range prog.SourceFiles() {
|
|
149
|
+
if file == nil || file.IsDeclarationFile {
|
|
150
|
+
continue
|
|
151
|
+
}
|
|
152
|
+
injectFile(factory, file, prog.Checker)
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
func injectFile(factory *shimast.NodeFactory, file *shimast.SourceFile, checker *shimchecker.Checker) {
|
|
157
|
+
var walk func(node *shimast.Node)
|
|
158
|
+
walk = func(node *shimast.Node) {
|
|
159
|
+
if node == nil {
|
|
160
|
+
return
|
|
161
|
+
}
|
|
162
|
+
if node.Kind == shimast.KindFunctionDeclaration {
|
|
163
|
+
injectFunction(factory, checker, node)
|
|
164
|
+
}
|
|
165
|
+
node.ForEachChild(func(child *shimast.Node) bool {
|
|
166
|
+
walk(child)
|
|
167
|
+
return false
|
|
168
|
+
})
|
|
169
|
+
}
|
|
170
|
+
if file.Statements != nil {
|
|
171
|
+
for _, stmt := range file.Statements.Nodes {
|
|
172
|
+
walk(stmt)
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
func injectFunction(factory *shimast.NodeFactory, checker *shimchecker.Checker, node *shimast.Node) {
|
|
178
|
+
fn := node.AsFunctionDeclaration()
|
|
179
|
+
if fn == nil || fn.Body == nil || fn.Parameters == nil {
|
|
180
|
+
return
|
|
181
|
+
}
|
|
182
|
+
block := fn.Body.AsBlock()
|
|
183
|
+
if block == nil || block.Statements == nil {
|
|
184
|
+
return
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
var checks []*shimast.Node
|
|
188
|
+
for _, param := range fn.Parameters.Nodes {
|
|
189
|
+
if param == nil || param.Kind != shimast.KindParameter {
|
|
190
|
+
continue
|
|
191
|
+
}
|
|
192
|
+
p := param.AsParameterDeclaration()
|
|
193
|
+
if p == nil || p.Name() == nil || p.Name().Kind != shimast.KindIdentifier {
|
|
194
|
+
continue
|
|
195
|
+
}
|
|
196
|
+
if p.DotDotDotToken != nil || p.QuestionToken != nil || p.Initializer != nil {
|
|
197
|
+
continue
|
|
198
|
+
}
|
|
199
|
+
name := p.Name().Text()
|
|
200
|
+
checks = append(checks, checksForParam(factory, checker, param, name)...)
|
|
201
|
+
}
|
|
202
|
+
if len(checks) == 0 {
|
|
203
|
+
return
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Structural prepend — printer walks Statements.Nodes directly.
|
|
207
|
+
out := make([]*shimast.Node, 0, len(checks)+len(block.Statements.Nodes))
|
|
208
|
+
out = append(out, checks...)
|
|
209
|
+
out = append(out, block.Statements.Nodes...)
|
|
210
|
+
block.Statements.Nodes = out
|
|
211
|
+
// Emit walks parents; factory nodes start detached.
|
|
212
|
+
shimast.SetParentInChildren(fn.Body)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
func checksForParam(
|
|
216
|
+
factory *shimast.NodeFactory,
|
|
217
|
+
checker *shimchecker.Checker,
|
|
218
|
+
param *shimast.Node,
|
|
219
|
+
path string,
|
|
220
|
+
) []*shimast.Node {
|
|
221
|
+
p := param.AsParameterDeclaration()
|
|
222
|
+
if p != nil && p.Type != nil {
|
|
223
|
+
if stmts := stmtsFromTypeNode(factory, path, p.Type); len(stmts) > 0 {
|
|
224
|
+
return stmts
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
if checker == nil {
|
|
228
|
+
return nil
|
|
229
|
+
}
|
|
230
|
+
t := checker.GetTypeAtLocation(param)
|
|
231
|
+
if t == nil {
|
|
232
|
+
return nil
|
|
233
|
+
}
|
|
234
|
+
return stmtsFromCheckerType(factory, checker, path, t)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
func stmtsFromTypeNode(
|
|
238
|
+
factory *shimast.NodeFactory,
|
|
239
|
+
path string,
|
|
240
|
+
typeNode *shimast.Node,
|
|
241
|
+
) []*shimast.Node {
|
|
242
|
+
if typeNode == nil {
|
|
243
|
+
return nil
|
|
244
|
+
}
|
|
245
|
+
switch typeNode.Kind {
|
|
246
|
+
case shimast.KindStringKeyword:
|
|
247
|
+
return []*shimast.Node{typeofCheck(factory, path, "string")}
|
|
248
|
+
case shimast.KindNumberKeyword:
|
|
249
|
+
return []*shimast.Node{typeofCheck(factory, path, "number")}
|
|
250
|
+
case shimast.KindBooleanKeyword:
|
|
251
|
+
return []*shimast.Node{typeofCheck(factory, path, "boolean")}
|
|
252
|
+
case shimast.KindBigIntKeyword:
|
|
253
|
+
return []*shimast.Node{typeofCheck(factory, path, "bigint")}
|
|
254
|
+
default:
|
|
255
|
+
return nil
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
func stmtsFromCheckerType(
|
|
260
|
+
factory *shimast.NodeFactory,
|
|
261
|
+
checker *shimchecker.Checker,
|
|
262
|
+
path string,
|
|
263
|
+
t *shimchecker.Type,
|
|
264
|
+
) []*shimast.Node {
|
|
265
|
+
flags := t.Flags()
|
|
266
|
+
switch {
|
|
267
|
+
case flags&shimchecker.TypeFlagsString != 0:
|
|
268
|
+
return []*shimast.Node{typeofCheck(factory, path, "string")}
|
|
269
|
+
case flags&shimchecker.TypeFlagsNumber != 0:
|
|
270
|
+
return []*shimast.Node{typeofCheck(factory, path, "number")}
|
|
271
|
+
case flags&shimchecker.TypeFlagsBoolean != 0:
|
|
272
|
+
return []*shimast.Node{typeofCheck(factory, path, "boolean")}
|
|
273
|
+
case flags&shimchecker.TypeFlagsBigInt != 0:
|
|
274
|
+
return []*shimast.Node{typeofCheck(factory, path, "bigint")}
|
|
275
|
+
case flags&shimchecker.TypeFlagsObject != 0:
|
|
276
|
+
props := shimchecker.Checker_getApparentProperties(checker, t)
|
|
277
|
+
if len(props) == 0 {
|
|
278
|
+
return nil
|
|
279
|
+
}
|
|
280
|
+
out := []*shimast.Node{objectCheck(factory, path)}
|
|
281
|
+
for _, sym := range props {
|
|
282
|
+
if sym == nil {
|
|
283
|
+
continue
|
|
284
|
+
}
|
|
285
|
+
propName := sym.Name
|
|
286
|
+
if propName == "" || strings.HasPrefix(propName, "__") {
|
|
287
|
+
continue
|
|
288
|
+
}
|
|
289
|
+
if sym.Flags&shimast.SymbolFlagsOptional != 0 {
|
|
290
|
+
continue
|
|
291
|
+
}
|
|
292
|
+
propType := shimchecker.Checker_getTypeOfPropertyOfType(checker, t, propName)
|
|
293
|
+
if propType == nil {
|
|
294
|
+
continue
|
|
295
|
+
}
|
|
296
|
+
out = append(out, stmtsFromCheckerType(
|
|
297
|
+
factory,
|
|
298
|
+
checker,
|
|
299
|
+
path+"."+propName,
|
|
300
|
+
propType,
|
|
301
|
+
)...)
|
|
302
|
+
}
|
|
303
|
+
return out
|
|
304
|
+
default:
|
|
305
|
+
return nil
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// pathExpr builds a fresh expression tree for a dotted path (e.g. "user.id").
|
|
310
|
+
// Each call allocates new nodes — AST nodes are not shareable across parents.
|
|
311
|
+
func pathExpr(factory *shimast.NodeFactory, path string) *shimast.Expression {
|
|
312
|
+
parts := strings.Split(path, ".")
|
|
313
|
+
var expr *shimast.Expression = factory.NewIdentifier(parts[0])
|
|
314
|
+
for _, part := range parts[1:] {
|
|
315
|
+
expr = factory.NewPropertyAccessExpression(
|
|
316
|
+
expr,
|
|
317
|
+
nil,
|
|
318
|
+
factory.NewIdentifier(part),
|
|
319
|
+
0,
|
|
320
|
+
)
|
|
321
|
+
}
|
|
322
|
+
return expr
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
func typeofCheck(factory *shimast.NodeFactory, path, kind string) *shimast.Node {
|
|
326
|
+
cond := binary(
|
|
327
|
+
factory,
|
|
328
|
+
factory.NewTypeOfExpression(pathExpr(factory, path)),
|
|
329
|
+
shimast.KindExclamationEqualsEqualsToken,
|
|
330
|
+
factory.NewStringLiteral(kind, shimast.TokenFlagsNone),
|
|
331
|
+
)
|
|
332
|
+
return throwIf(factory, cond, "Expected "+path+" to be a "+kind)
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
func objectCheck(factory *shimast.NodeFactory, path string) *shimast.Node {
|
|
336
|
+
notObject := binary(
|
|
337
|
+
factory,
|
|
338
|
+
factory.NewTypeOfExpression(pathExpr(factory, path)),
|
|
339
|
+
shimast.KindExclamationEqualsEqualsToken,
|
|
340
|
+
factory.NewStringLiteral("object", shimast.TokenFlagsNone),
|
|
341
|
+
)
|
|
342
|
+
isNull := binary(
|
|
343
|
+
factory,
|
|
344
|
+
pathExpr(factory, path),
|
|
345
|
+
shimast.KindEqualsEqualsEqualsToken,
|
|
346
|
+
factory.NewToken(shimast.KindNullKeyword),
|
|
347
|
+
)
|
|
348
|
+
cond := binary(factory, notObject, shimast.KindBarBarToken, isNull)
|
|
349
|
+
return throwIf(factory, cond, "Expected "+path+" to be an object")
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
func throwIf(factory *shimast.NodeFactory, condition *shimast.Expression, message string) *shimast.Node {
|
|
353
|
+
msg := factory.NewStringLiteral(message, shimast.TokenFlagsNone)
|
|
354
|
+
ctor := factory.NewIdentifier("TypeError")
|
|
355
|
+
args := factory.NewNodeList([]*shimast.Node{msg})
|
|
356
|
+
newExpr := factory.NewNewExpression(ctor, nil, args)
|
|
357
|
+
throwStmt := factory.NewThrowStatement(newExpr)
|
|
358
|
+
return factory.NewIfStatement(condition, throwStmt, nil)
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
func binary(
|
|
362
|
+
factory *shimast.NodeFactory,
|
|
363
|
+
left *shimast.Expression,
|
|
364
|
+
op shimast.Kind,
|
|
365
|
+
right *shimast.Expression,
|
|
366
|
+
) *shimast.Expression {
|
|
367
|
+
return factory.NewBinaryExpression(
|
|
368
|
+
nil,
|
|
369
|
+
left,
|
|
370
|
+
nil,
|
|
371
|
+
factory.NewToken(op),
|
|
372
|
+
right,
|
|
373
|
+
)
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
func parseOptions(command string, args []string) (options, bool) {
|
|
377
|
+
fs := flag.NewFlagSet(command, flag.ContinueOnError)
|
|
378
|
+
fs.SetOutput(os.Stderr)
|
|
379
|
+
cwd := fs.String("cwd", "", "project directory")
|
|
380
|
+
emit := fs.Bool("emit", false, "force emit")
|
|
381
|
+
noEmit := fs.Bool("noEmit", false, "force no emit")
|
|
382
|
+
outDir := fs.String("outDir", "", "emit directory override")
|
|
383
|
+
tsconfig := fs.String("tsconfig", "tsconfig.json", "project tsconfig")
|
|
384
|
+
_ = fs.String("plugins-json", "", "ttsc plugin metadata")
|
|
385
|
+
_ = fs.Bool("quiet", true, "suppress summary")
|
|
386
|
+
_ = fs.Bool("verbose", false, "print summary")
|
|
387
|
+
if err := fs.Parse(filterHostArgs(args, fs)); err != nil {
|
|
388
|
+
return options{}, false
|
|
389
|
+
}
|
|
390
|
+
root := *cwd
|
|
391
|
+
if root == "" {
|
|
392
|
+
var err error
|
|
393
|
+
root, err = os.Getwd()
|
|
394
|
+
if err != nil {
|
|
395
|
+
fmt.Fprintln(os.Stderr, err)
|
|
396
|
+
return options{}, false
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
if !filepath.IsAbs(root) {
|
|
400
|
+
abs, err := filepath.Abs(root)
|
|
401
|
+
if err != nil {
|
|
402
|
+
fmt.Fprintln(os.Stderr, err)
|
|
403
|
+
return options{}, false
|
|
404
|
+
}
|
|
405
|
+
root = abs
|
|
406
|
+
}
|
|
407
|
+
cfg := *tsconfig
|
|
408
|
+
if !filepath.IsAbs(cfg) {
|
|
409
|
+
cfg = filepath.Join(root, cfg)
|
|
410
|
+
}
|
|
411
|
+
return options{
|
|
412
|
+
cwd: filepath.Clean(root),
|
|
413
|
+
emit: *emit,
|
|
414
|
+
noEmit: *noEmit,
|
|
415
|
+
outDir: *outDir,
|
|
416
|
+
tsconfig: cfg,
|
|
417
|
+
}, true
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
var valueFlags = map[string]bool{
|
|
421
|
+
"--cwd": true,
|
|
422
|
+
"--tsconfig": true,
|
|
423
|
+
"--outDir": true,
|
|
424
|
+
"--plugins-json": true,
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
func filterHostArgs(args []string, fs *flag.FlagSet) []string {
|
|
428
|
+
out := make([]string, 0, len(args))
|
|
429
|
+
for i := 0; i < len(args); i++ {
|
|
430
|
+
a := args[i]
|
|
431
|
+
if !strings.HasPrefix(a, "-") {
|
|
432
|
+
out = append(out, a)
|
|
433
|
+
continue
|
|
434
|
+
}
|
|
435
|
+
name, _, hasEq := strings.Cut(a, "=")
|
|
436
|
+
if fs.Lookup(strings.TrimLeft(name, "-")) != nil {
|
|
437
|
+
out = append(out, a)
|
|
438
|
+
if !hasEq && valueFlags[name] && i+1 < len(args) {
|
|
439
|
+
out = append(out, args[i+1])
|
|
440
|
+
i++
|
|
441
|
+
}
|
|
442
|
+
continue
|
|
443
|
+
}
|
|
444
|
+
if !hasEq && valueFlags[name] && i+1 < len(args) {
|
|
445
|
+
i++
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
return out
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
func outputKey(cwd, fileName string) string {
|
|
452
|
+
rel, err := filepath.Rel(cwd, fileName)
|
|
453
|
+
if err != nil || rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) {
|
|
454
|
+
return filepath.ToSlash(fileName)
|
|
455
|
+
}
|
|
456
|
+
return filepath.ToSlash(rel)
|
|
457
|
+
}
|