@effect-app/eslint-codegen-model 0.7.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/dist/.tsbuildinfo +1 -0
- package/dist/compiler.d.ts +3 -0
- package/dist/compiler.d.ts.map +1 -0
- package/dist/compiler.js +244 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +19 -0
- package/dist/presets/barrel.d.ts +39 -0
- package/dist/presets/barrel.d.ts.map +1 -0
- package/dist/presets/barrel.js +143 -0
- package/dist/presets/index.d.ts +3 -0
- package/dist/presets/index.d.ts.map +1 -0
- package/dist/presets/index.js +20 -0
- package/dist/presets/model.d.ts +5 -0
- package/dist/presets/model.d.ts.map +1 -0
- package/dist/presets/model.js +126 -0
- package/index.js +2 -0
- package/package.json +38 -0
- package/src/compiler.ts +235 -0
- package/src/index.ts +3 -0
- package/src/presets/barrel.ts +150 -0
- package/src/presets/index.ts +4 -0
- package/src/presets/model.ts +114 -0
- package/tsconfig.json +94 -0
- package/tsconfig.json.bak +16 -0
package/src/compiler.ts
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import ts, { SyntaxKind } from "typescript"
|
|
2
|
+
|
|
3
|
+
const sortUnion = (a: string, b: string) => {
|
|
4
|
+
if (a !== "null" && a!== "undefined" && (b === "null" || b === "undefined")) {
|
|
5
|
+
return -1
|
|
6
|
+
}
|
|
7
|
+
if (b !== "null" && b !== "undefined" && (a === "null" || a === "undefined")) {
|
|
8
|
+
return 1
|
|
9
|
+
}
|
|
10
|
+
if(a < b) { return -1; }
|
|
11
|
+
if(a > b) { return 1; }
|
|
12
|
+
return 0;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const sortAlpha = (a: string, b: string) => {
|
|
16
|
+
if(a < b) { return -1; }
|
|
17
|
+
if(a > b) { return 1; }
|
|
18
|
+
return 0;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// TODO: we don't support string literals with spaces in them currently.
|
|
22
|
+
const rx = /(([^\s\<\>\,\[\(]+)? \| ([^\s\<\>\,\]\)]+))+/
|
|
23
|
+
|
|
24
|
+
function sortIt(str: string) {
|
|
25
|
+
return str.split(" | ").sort(sortUnion).join(" | ")
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const debug = false // true
|
|
29
|
+
|
|
30
|
+
export function processNode(tc: ts.TypeChecker, root: ts.Node) {
|
|
31
|
+
return (n: ts.Node) => {
|
|
32
|
+
if (/*ts.isClassDeclaration(n) || ts.isTypeAliasDeclaration(n)*/ true) {
|
|
33
|
+
const constructorName = (n as any).name?.escapedText
|
|
34
|
+
|
|
35
|
+
if (!constructorName?.endsWith("Constructor")) {
|
|
36
|
+
//console.log("$$$constructorName doesnt end with Constructor", constructorName)
|
|
37
|
+
return
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
//console.log("$$$ constructorName", constructorName)
|
|
41
|
+
|
|
42
|
+
const t = tc.getTypeAtLocation(n)
|
|
43
|
+
|
|
44
|
+
const result = { encoded: [] as string[], parsed: [] as string[] }
|
|
45
|
+
const unions: Record<string, string> = {}
|
|
46
|
+
|
|
47
|
+
//console.log("$$$ props", t.getProperties().map(x => x.escapedName))
|
|
48
|
+
t.getProperties().forEach((c) => {
|
|
49
|
+
const method = c.name
|
|
50
|
+
if (method === "encoded" || method === "parsed") {
|
|
51
|
+
//console.log("$$$ method", method)
|
|
52
|
+
//console.log(c.members)
|
|
53
|
+
const tt = tc.getTypeOfSymbolAtLocation(c, n)
|
|
54
|
+
// const s = tc.getReturnTypeOfSignature(tt.getCallSignatures()[0])
|
|
55
|
+
|
|
56
|
+
// const type = tc.getReturnTypeOfSignature(s! as any /* TODO */)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
tt.getProperties().forEach(p => {
|
|
60
|
+
const isLookup = debug && p.escapedName === "carrier"
|
|
61
|
+
|
|
62
|
+
//kind = 207,
|
|
63
|
+
//arguments[0].escapedText === "HosterRole"
|
|
64
|
+
//console.log("$$$p", p.escapedName)
|
|
65
|
+
//if (p.escapedName === "opposite") {
|
|
66
|
+
//console.log("$$$ a union!", p.declarations?.map(x => x.forEachChild(c => {
|
|
67
|
+
|
|
68
|
+
// TODO: have to find nullable, array, set, map, etc.
|
|
69
|
+
// TODO: "Encoded"
|
|
70
|
+
// but also should find fully custom sets like PurchaseOrderModulesSet - we should be able to just use those directly, incl PurchaseOrderModulesSet.Encoded
|
|
71
|
+
// for now just skip them?
|
|
72
|
+
p.declarations?.map(x => x.forEachChild(c => {
|
|
73
|
+
if (isLookup) {
|
|
74
|
+
console.log("$$$ lookup", c.kind, c)
|
|
75
|
+
}
|
|
76
|
+
if (c.kind === SyntaxKind.CallExpression) { // 207 -- SyntaxKind.ElementAccessExpression) {
|
|
77
|
+
let it = (c as any).arguments[0]
|
|
78
|
+
//const isState = p.escapedName === "state"
|
|
79
|
+
if (isLookup) {
|
|
80
|
+
console.log("$$$ state it", it)
|
|
81
|
+
}
|
|
82
|
+
const isNullable = it.expression?.escapedText === "nullable"
|
|
83
|
+
const isIt = it.arguments && it.arguments[0] //it.expression?.escapedText === "nullable"
|
|
84
|
+
if (isIt) {
|
|
85
|
+
//console.log("$$ nullable", it.arguments[0])
|
|
86
|
+
// TODO: usually the union is on the last input, we need to support all elements individually however
|
|
87
|
+
it = it.arguments[it.arguments.length - 1]
|
|
88
|
+
}
|
|
89
|
+
//console.log("$args", it)
|
|
90
|
+
//tc.getTypeAtLocation(it)
|
|
91
|
+
const tt = tc.getTypeAtLocation(c) //tc.getTypeOfSymbolAtLocation(it.parent, n)
|
|
92
|
+
const typeDecl = tc.typeToString(
|
|
93
|
+
tt,
|
|
94
|
+
root,
|
|
95
|
+
ts.TypeFormatFlags.NoTruncation
|
|
96
|
+
//ts.TypeFormatFlags.None
|
|
97
|
+
//ts.TypeFormatFlags.AddUndefined |
|
|
98
|
+
// | ts.TypeFormatFlags.NoTypeReduction
|
|
99
|
+
// | ts.TypeFormatFlags.MultilineObjectLiterals
|
|
100
|
+
//| ts.TypeFormatFlags.InTypeAlias
|
|
101
|
+
| ts.TypeFormatFlags.UseAliasDefinedOutsideCurrentScope // prevents import(*)
|
|
102
|
+
// | ts.TypeFormatFlags.UseStructuralFallback
|
|
103
|
+
)
|
|
104
|
+
if (isLookup) {
|
|
105
|
+
console.log("$$ type", typeDecl)
|
|
106
|
+
}
|
|
107
|
+
const matches = typeDecl.match(rx)
|
|
108
|
+
if (isLookup) {
|
|
109
|
+
console.log("$$ matches", matches)
|
|
110
|
+
}
|
|
111
|
+
const isOptional = typeDecl.match(/\>, "optional"/)
|
|
112
|
+
if (matches) {
|
|
113
|
+
let replaced = matches[0]!.replace(rx, (match) => sortIt(match))
|
|
114
|
+
replaced = sortIt(isOptional ? isNullable ? replaced.replace(" | null", " | undefined | null") : replaced + " | undefined" : replaced)
|
|
115
|
+
//console.log("$$ replaced", replaced, it.escapedText, matches)
|
|
116
|
+
// if (it.escapedText === "TaskState") {
|
|
117
|
+
// console.log("Help", it)
|
|
118
|
+
// }
|
|
119
|
+
if (isLookup) {
|
|
120
|
+
console.log("$$$ replaced", it.escapedText, replaced)
|
|
121
|
+
}
|
|
122
|
+
if (it.escapedText && !it.escapedText.endsWith("Set") /* skip the "Set" problem */ && replaced.replace(" | null", "").includes("|")) {
|
|
123
|
+
const replacement = it.escapedText + (isNullable ? " | null" : "") + (isOptional ? " | undefined" : "")
|
|
124
|
+
// if (it.escapedText === "TaskState") {
|
|
125
|
+
// console.log("$$$", { replaced, replacement })
|
|
126
|
+
// unions[replaced] = replacement
|
|
127
|
+
// } else {
|
|
128
|
+
unions[replaced] = replacement
|
|
129
|
+
if (isLookup) {
|
|
130
|
+
console.log("$$ repl", { replaced, replacement})
|
|
131
|
+
}
|
|
132
|
+
//}
|
|
133
|
+
} else {
|
|
134
|
+
// if (isIt) {
|
|
135
|
+
// console.log("$$ no name found", it.escapedText)
|
|
136
|
+
// }
|
|
137
|
+
// console.log("$$ no name found??", it)
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
}
|
|
142
|
+
//c.kind === 346 ? console.log(c) : null
|
|
143
|
+
//console.log((c as any).flowNode?.node?.name)
|
|
144
|
+
}))
|
|
145
|
+
//}
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
if (debug && Object.keys(unions).length) {
|
|
149
|
+
console.log("$$$ unions to replace", unions)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const typeDecl = tc.typeToString(
|
|
153
|
+
tt,
|
|
154
|
+
root,
|
|
155
|
+
ts.TypeFormatFlags.NoTruncation
|
|
156
|
+
//ts.TypeFormatFlags.None
|
|
157
|
+
//ts.TypeFormatFlags.AddUndefined |
|
|
158
|
+
// | ts.TypeFormatFlags.NoTypeReduction
|
|
159
|
+
// | ts.TypeFormatFlags.MultilineObjectLiterals
|
|
160
|
+
//| ts.TypeFormatFlags.InTypeAlias
|
|
161
|
+
| ts.TypeFormatFlags.UseAliasDefinedOutsideCurrentScope // prevents import(*)
|
|
162
|
+
// | ts.TypeFormatFlags.UseStructuralFallback
|
|
163
|
+
)
|
|
164
|
+
const str = typeDecl === "{}" ? [] :
|
|
165
|
+
// drop leading { and trailing }
|
|
166
|
+
typeDecl.substring(2, typeDecl.length - 2)
|
|
167
|
+
.split(";")
|
|
168
|
+
.map(l => l.trim())
|
|
169
|
+
// todo; skip the first split, as its the property
|
|
170
|
+
.map(l => l.replace(rx, (match) => {
|
|
171
|
+
const rpl = sortIt(match)
|
|
172
|
+
//if (debug) { console.log("Searching for", rpl, { unions}) }
|
|
173
|
+
if (rpl.endsWith(" | undefined")) {
|
|
174
|
+
const sub = unions[rpl.replace(" | undefined", "")]
|
|
175
|
+
return sub ? sub + " | undefined" : unions[rpl] ?? rpl
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const sub = unions[rpl]
|
|
179
|
+
return (sub ? sub : rpl)
|
|
180
|
+
})
|
|
181
|
+
.replaceAll(" Array<", " ReadonlyArray<") // .replaceAll(/(Array|Set|Map)\</", "ReadonlyArray<") //
|
|
182
|
+
.replaceAll(" Set<", " ROSet<")
|
|
183
|
+
.replaceAll(" Map<", " ROMap<")
|
|
184
|
+
.replaceAll("(Array<", "(ReadonlyArray<") // .replaceAll(/(Array|Set|Map)\</", "ReadonlyArray<") //
|
|
185
|
+
.replaceAll("(Set<", "(ROSet<")
|
|
186
|
+
.replaceAll("(Map<", "(ROMap<")
|
|
187
|
+
.replaceAll(" Array.Array<", " ReadonlyArray<") // .replaceAll(/(Array|Set|Map)\</", "ReadonlyArray<") //
|
|
188
|
+
.replaceAll(" Set.Set<", " ROSet<")
|
|
189
|
+
.replaceAll(" Map.Map<", " ROMap<")
|
|
190
|
+
)
|
|
191
|
+
// we sort for now, because otherwise we have sometimes multiple times changing back and forth between editor and console.
|
|
192
|
+
.sort(sortAlpha)
|
|
193
|
+
// Taken care of by "ts.TypeFormatFlags.UseAliasDefinedOutsideCurrentScope"
|
|
194
|
+
//.replaceAll(/import\("[^"]+"\)\./g, "")
|
|
195
|
+
|
|
196
|
+
result[method] = str
|
|
197
|
+
}
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
if (!("parsed" in result)) {
|
|
201
|
+
throw new Error("No parsed result")
|
|
202
|
+
}
|
|
203
|
+
if (!("encoded" in result)) {
|
|
204
|
+
throw new Error("No encoded result")
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const modelName = constructorName.replace("Constructor", "")
|
|
208
|
+
|
|
209
|
+
const encoded = result.encoded.filter(x => !!x)
|
|
210
|
+
const parsed = result.parsed.filter(x => !!x)
|
|
211
|
+
|
|
212
|
+
return [
|
|
213
|
+
`export interface ${modelName} {${parsed.length ? "\n" + parsed.map(l => " " + l).join("\n") + "\n" : ""}}`,
|
|
214
|
+
`export namespace ${modelName} {`,
|
|
215
|
+
` /**`,
|
|
216
|
+
` * @tsplus type ${modelName}.Encoded`,
|
|
217
|
+
` */`,
|
|
218
|
+
` export interface Encoded {${encoded.length ? "\n" + encoded.map(l => " " + l).join("\n") + "\n " : ""}}`,
|
|
219
|
+
` export const Encoded: EncodedOps = { $: {} }`,
|
|
220
|
+
` /**`,
|
|
221
|
+
` * @tsplus type ${modelName}.Encoded/Aspects`,
|
|
222
|
+
` */`,
|
|
223
|
+
` export interface EncodedAspects {}`,
|
|
224
|
+
` /**`,
|
|
225
|
+
` * @tsplus type ${modelName}.Encoded/Ops`,
|
|
226
|
+
` */`,
|
|
227
|
+
` export interface EncodedOps { $: EncodedAspects }`,
|
|
228
|
+
" export interface ConstructorInput",
|
|
229
|
+
` extends ConstructorInputFromApi<typeof ${modelName}> {}`,
|
|
230
|
+
` export interface Props extends GetProvidedProps<typeof ${modelName}> {}`,
|
|
231
|
+
"}",
|
|
232
|
+
]
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import generate from '@babel/generator';
|
|
2
|
+
import { parse } from '@babel/parser';
|
|
3
|
+
import type { Preset } from 'eslint-plugin-codegen';
|
|
4
|
+
import * as glob from 'glob';
|
|
5
|
+
import { match } from 'io-ts-extra';
|
|
6
|
+
import * as lodash from 'lodash';
|
|
7
|
+
import * as path from 'path';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Bundle several modules into a single convenient one.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* // codegen:start {preset: barrel, include: some/path/*.ts, exclude: some/path/*util.ts}
|
|
14
|
+
* export * from './some/path/module-a'
|
|
15
|
+
* export * from './some/path/module-b'
|
|
16
|
+
* export * from './some/path/module-c'
|
|
17
|
+
* // codegen:end
|
|
18
|
+
*
|
|
19
|
+
* @param include
|
|
20
|
+
* [optional] If specified, the barrel will only include file paths that match this glob pattern
|
|
21
|
+
* @param exclude
|
|
22
|
+
* [optional] If specified, the barrel will exclude file paths that match these glob patterns
|
|
23
|
+
* @param import
|
|
24
|
+
* [optional] If specified, matching files will be imported and re-exported rather than directly exported
|
|
25
|
+
* with `export * from './xyz'`. Use `import: star` for `import * as xyz from './xyz'` style imports.
|
|
26
|
+
* Use `import: default` for `import xyz from './xyz'` style imports.
|
|
27
|
+
* @param export
|
|
28
|
+
* [optional] Only valid if the import style has been specified (either `import: star` or `import: default`).
|
|
29
|
+
* If specified, matching modules will be bundled into a const or default export based on this name. If set
|
|
30
|
+
* to `{name: someName, keys: path}` the relative file paths will be used as keys. Otherwise the file paths
|
|
31
|
+
* will be camel-cased to make them valid js identifiers.
|
|
32
|
+
*/
|
|
33
|
+
export const barrel: Preset<{
|
|
34
|
+
include?: string;
|
|
35
|
+
exclude?: string | string[];
|
|
36
|
+
import?: 'default' | 'star';
|
|
37
|
+
export?:
|
|
38
|
+
| string
|
|
39
|
+
| { name: string; keys: 'path' | 'camelCase' }
|
|
40
|
+
| { as: 'PascalCase', postfix?: string };
|
|
41
|
+
nodir?: boolean;
|
|
42
|
+
}> = ({ meta, options: opts }) => {
|
|
43
|
+
const cwd = path.dirname(meta.filename);
|
|
44
|
+
const nodir = opts.nodir ?? true;
|
|
45
|
+
|
|
46
|
+
const ext = meta.filename.split('.').slice(-1)[0];
|
|
47
|
+
const pattern = opts.include || `*.${ext}`;
|
|
48
|
+
|
|
49
|
+
const relativeFiles = glob
|
|
50
|
+
.sync(pattern, { cwd, ignore: opts.exclude, nodir })
|
|
51
|
+
.filter((f) => path.resolve(cwd, f) !== path.resolve(meta.filename))
|
|
52
|
+
.map((f) => `./${f}`.replace(/(\.\/)+\./g, '.'))
|
|
53
|
+
.filter((file) =>
|
|
54
|
+
nodir
|
|
55
|
+
? ['.js', '.mjs', '.ts', '.tsx'].includes(path.extname(file))
|
|
56
|
+
: true,
|
|
57
|
+
)
|
|
58
|
+
.map((f) => f.replace(/\.\w+$/, '').replace(/\/$/, ''));
|
|
59
|
+
|
|
60
|
+
function last<T>(list: readonly T[]) {
|
|
61
|
+
return list[list.length - 1]
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const expectedContent = match(opts.import)
|
|
65
|
+
.case(undefined, () =>
|
|
66
|
+
match(opts.export)
|
|
67
|
+
.case({ as: 'PascalCase' as const }, (v) =>
|
|
68
|
+
lodash
|
|
69
|
+
.chain(relativeFiles)
|
|
70
|
+
.map(
|
|
71
|
+
(f) =>
|
|
72
|
+
`export * as ${lodash
|
|
73
|
+
.startCase(lodash.camelCase(last(f.split("/"))))
|
|
74
|
+
.replace(/ /g, "") // why?
|
|
75
|
+
.replace(/\//, '')}${"postfix" in v ? v.postfix : ''} from '${f}.js'`,
|
|
76
|
+
)
|
|
77
|
+
.value()
|
|
78
|
+
.join('\n'),
|
|
79
|
+
)
|
|
80
|
+
.default(() => {
|
|
81
|
+
return relativeFiles.map((f) => `export * from '${f}.js'`).join('\n');
|
|
82
|
+
})
|
|
83
|
+
.get(),
|
|
84
|
+
)
|
|
85
|
+
.case(String, (s) => {
|
|
86
|
+
const importPrefix = s === 'default' ? '' : '* as ';
|
|
87
|
+
const withIdentifiers = lodash
|
|
88
|
+
.chain(relativeFiles)
|
|
89
|
+
.map((f) => ({
|
|
90
|
+
file: f,
|
|
91
|
+
identifier: lodash
|
|
92
|
+
.camelCase(f)
|
|
93
|
+
.replace(/^([^a-z])/, '_$1')
|
|
94
|
+
.replace(/Index$/, ''),
|
|
95
|
+
}))
|
|
96
|
+
.groupBy((info) => info.identifier)
|
|
97
|
+
.values()
|
|
98
|
+
.flatMap((group) =>
|
|
99
|
+
group.length === 1
|
|
100
|
+
? group
|
|
101
|
+
: group.map((info, i) => ({
|
|
102
|
+
...info,
|
|
103
|
+
identifier: `${info.identifier}_${i + 1}`,
|
|
104
|
+
})),
|
|
105
|
+
)
|
|
106
|
+
.value();
|
|
107
|
+
|
|
108
|
+
const imports = withIdentifiers
|
|
109
|
+
.map((i) => `import ${importPrefix}${i.identifier} from '${i.file}.js'`)
|
|
110
|
+
.join('\n');
|
|
111
|
+
const exportProps = match(opts.export)
|
|
112
|
+
.case({ name: String, keys: 'path' }, () =>
|
|
113
|
+
withIdentifiers.map(
|
|
114
|
+
(i) => `${JSON.stringify(i.file)}: ${i.identifier}`,
|
|
115
|
+
),
|
|
116
|
+
)
|
|
117
|
+
.default(() => withIdentifiers.map((i) => i.identifier))
|
|
118
|
+
.get();
|
|
119
|
+
|
|
120
|
+
const exportPrefix = match(opts.export)
|
|
121
|
+
.case(undefined, () => 'export')
|
|
122
|
+
.case('default', () => 'export default')
|
|
123
|
+
.case({ name: 'default' }, () => 'export default')
|
|
124
|
+
.case(String, (name) => `export const ${name} =`)
|
|
125
|
+
.case({ name: String }, ({ name }) => `export const ${name} =`)
|
|
126
|
+
.default(() => '')
|
|
127
|
+
.get();
|
|
128
|
+
|
|
129
|
+
const exports = exportProps.join(',\n ');
|
|
130
|
+
|
|
131
|
+
return `${imports}\n\n${exportPrefix} {\n ${exports}\n}\n`;
|
|
132
|
+
})
|
|
133
|
+
.get();
|
|
134
|
+
|
|
135
|
+
// ignore stylistic differences. babel generate deals with most
|
|
136
|
+
const normalise = (str: string) =>
|
|
137
|
+
generate(
|
|
138
|
+
parse(str, { sourceType: 'module', plugins: ['typescript'] }) as any,
|
|
139
|
+
)
|
|
140
|
+
.code.replace(/'/g, `"`)
|
|
141
|
+
.replace(/\/index/g, '');
|
|
142
|
+
|
|
143
|
+
try {
|
|
144
|
+
if (normalise(expectedContent) === normalise(meta.existingContent)) {
|
|
145
|
+
return meta.existingContent;
|
|
146
|
+
}
|
|
147
|
+
} catch {}
|
|
148
|
+
|
|
149
|
+
return expectedContent;
|
|
150
|
+
};
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import generate from "@babel/generator"
|
|
2
|
+
import { parse } from "@babel/parser"
|
|
3
|
+
import type { Preset } from "eslint-plugin-codegen"
|
|
4
|
+
import * as fs from "fs"
|
|
5
|
+
import { processNode } from "../compiler"
|
|
6
|
+
function normalise(str: string) {
|
|
7
|
+
try {
|
|
8
|
+
return generate(
|
|
9
|
+
parse(str, { sourceType: "module", plugins: ["typescript"] }) as any
|
|
10
|
+
)
|
|
11
|
+
.code
|
|
12
|
+
// .replace(/'/g, `"`)
|
|
13
|
+
// .replace(/\/index/g, "")
|
|
14
|
+
//.replace(/([\n\s]+ \|)/g, " |").replaceAll(": |", ":")
|
|
15
|
+
//.replaceAll(/[\s\n]+\|/g, " |")
|
|
16
|
+
//.replaceAll("\n", ";")
|
|
17
|
+
//.replaceAll(" ", "")
|
|
18
|
+
// TODO: remove all \n and whitespace?
|
|
19
|
+
} catch (e) {
|
|
20
|
+
return str
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
// TODO: get shared compiler host...
|
|
24
|
+
import { ESLintUtils } from "@typescript-eslint/utils"
|
|
25
|
+
export const model: Preset<{
|
|
26
|
+
exclude?: string
|
|
27
|
+
}> = ({ meta }, context: any) => {
|
|
28
|
+
if (!context.parserOptions.project) {
|
|
29
|
+
console.warn(`${meta.filename}: Cannot run ESLint Model plugin, because no TS Compiler is enabled`)
|
|
30
|
+
return meta.existingContent
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
// option to exclude some methods
|
|
35
|
+
//const exclude = (options.exclude || "").split(",")
|
|
36
|
+
|
|
37
|
+
// checks and reads the file
|
|
38
|
+
const sourcePath = meta.filename
|
|
39
|
+
if (!fs.existsSync(sourcePath) || !fs.statSync(sourcePath).isFile()) {
|
|
40
|
+
throw Error(`Source path is not a file: ${sourcePath}`)
|
|
41
|
+
}
|
|
42
|
+
// const cfgFile = ts.findConfigFile(sourcePath, (fn) => fs.existsSync(fn))
|
|
43
|
+
// if (!cfgFile) {
|
|
44
|
+
// throw new Error("No TS config file found")
|
|
45
|
+
// }
|
|
46
|
+
|
|
47
|
+
// const cfg = ts.readConfigFile(cfgFile, (fn) => fs.readFileSync(fn, "utf-8"))
|
|
48
|
+
// const basePath = path.dirname(cfgFile); // equal to "getDirectoryPath" from ts, at least in our case.
|
|
49
|
+
// const parsedConfig = ts.parseJsonConfigFileContent(cfg.config, ts.sys, basePath);
|
|
50
|
+
|
|
51
|
+
// const program = ts.createProgram([sourcePath], parsedConfig.options)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
const { program } = ESLintUtils.getParserServices(context);
|
|
55
|
+
|
|
56
|
+
//console.log("$$ processing", sourcePath)
|
|
57
|
+
|
|
58
|
+
// create and parse the AST
|
|
59
|
+
const sourceFile = program.getSourceFile(
|
|
60
|
+
sourcePath,
|
|
61
|
+
)!
|
|
62
|
+
|
|
63
|
+
// collect data-first declarations
|
|
64
|
+
// const dataFirstDeclarations = sourceFile.statements
|
|
65
|
+
// .filter(ts.isFunctionDeclaration)
|
|
66
|
+
// // .filter(
|
|
67
|
+
// // (node) =>
|
|
68
|
+
// // node.modifiers &&
|
|
69
|
+
// // node.modifiers.filter(
|
|
70
|
+
// // (modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword
|
|
71
|
+
// // ).length > 0
|
|
72
|
+
// // )
|
|
73
|
+
// // .filter((node) => !!node.name)
|
|
74
|
+
// // .filter((node) => node.parameters.length >= 2)
|
|
75
|
+
// // .filter((node) => node.name!.getText(sourceFile).endsWith("_"))
|
|
76
|
+
// // .map((node) => ({
|
|
77
|
+
// // functionName: node.name!.getText(sourceFile),
|
|
78
|
+
// // typeParameters: node.typeParameters || ts.factory.createNodeArray(),
|
|
79
|
+
// // parameters: node.parameters || ts.factory.createNodeArray(),
|
|
80
|
+
// // type: node.type!,
|
|
81
|
+
// // implemented: !!node.body,
|
|
82
|
+
// // jsDoc: getJSDoc(node)
|
|
83
|
+
// // }))
|
|
84
|
+
// // .filter((decl) => exclude.indexOf(decl.functionName) === -1)
|
|
85
|
+
|
|
86
|
+
// // create the actual AST nodes
|
|
87
|
+
// const nodes = dataFirstDeclarations.map(createPipeableFunctionDeclaration)
|
|
88
|
+
// const expectedContent = nodes.map((node) => printNode(node, sourceFile)).join("\n")
|
|
89
|
+
|
|
90
|
+
const pn = processNode(program.getTypeChecker(), sourceFile)
|
|
91
|
+
let abc: (string[] | undefined)[] = []
|
|
92
|
+
// TODO: must return void, cannot use getChildren() etc, or it wont work, no idea why!
|
|
93
|
+
sourceFile.forEachChild(c => {abc = abc.concat(pn(c))})
|
|
94
|
+
const expectedContent = [
|
|
95
|
+
"//",
|
|
96
|
+
`/* eslint-disable */`,
|
|
97
|
+
...abc.filter((x): x is string[] => !!x),
|
|
98
|
+
`/* eslint-enable */`,
|
|
99
|
+
"//"
|
|
100
|
+
].join("\n")
|
|
101
|
+
|
|
102
|
+
// do not re-emit in a different style, or a loop will occur
|
|
103
|
+
if (normalise(meta.existingContent) === normalise(expectedContent))
|
|
104
|
+
return meta.existingContent
|
|
105
|
+
return expectedContent
|
|
106
|
+
} catch (e) {
|
|
107
|
+
return (
|
|
108
|
+
"/** Got exception: " +
|
|
109
|
+
("stack" in (e as any) ? (e as any).stack : "") +
|
|
110
|
+
JSON.stringify(e) +
|
|
111
|
+
"*/"
|
|
112
|
+
)
|
|
113
|
+
}
|
|
114
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"strict": true,
|
|
4
|
+
"allowUnusedLabels": false,
|
|
5
|
+
"allowUnreachableCode": false,
|
|
6
|
+
"exactOptionalPropertyTypes": false,
|
|
7
|
+
"noFallthroughCasesInSwitch": true,
|
|
8
|
+
"noImplicitOverride": true,
|
|
9
|
+
"noImplicitReturns": false,
|
|
10
|
+
"noPropertyAccessFromIndexSignature": true,
|
|
11
|
+
"noUncheckedIndexedAccess": true,
|
|
12
|
+
"noUnusedLocals": true,
|
|
13
|
+
"noUnusedParameters": true,
|
|
14
|
+
"importsNotUsedAsValues": "error",
|
|
15
|
+
"checkJs": true,
|
|
16
|
+
"esModuleInterop": true,
|
|
17
|
+
"skipLibCheck": true,
|
|
18
|
+
"forceConsistentCasingInFileNames": true,
|
|
19
|
+
"module": "CommonJS",
|
|
20
|
+
"lib": [
|
|
21
|
+
"esnext"
|
|
22
|
+
],
|
|
23
|
+
"target": "ES2018",
|
|
24
|
+
"inlineSourceMap": true,
|
|
25
|
+
"incremental": true,
|
|
26
|
+
"composite": true,
|
|
27
|
+
"declarationMap": true,
|
|
28
|
+
"experimentalDecorators": true,
|
|
29
|
+
"emitDecoratorMetadata": true,
|
|
30
|
+
"noImplicitAny": true,
|
|
31
|
+
"useUnknownInCatchVariables": true,
|
|
32
|
+
"noImplicitThis": true,
|
|
33
|
+
"outDir": "./dist",
|
|
34
|
+
"resolveJsonModule": true,
|
|
35
|
+
"moduleResolution": "Node16",
|
|
36
|
+
"downlevelIteration": true,
|
|
37
|
+
"noErrorTruncation": true,
|
|
38
|
+
"tsPlusTypes": [
|
|
39
|
+
"./node_modules/@effect-app/core/vendor/effect-ts-tsplus.json",
|
|
40
|
+
"./node_modules/@effect-app/core/vendor/effect-io-tsplus.json",
|
|
41
|
+
"./node_modules/@effect-app/core/vendor/effect-stm-tsplus.json",
|
|
42
|
+
"./node_modules/@effect-app/core/vendor/effect-stream-tsplus.json",
|
|
43
|
+
"./node_modules/@effect-app/core/vendor/fp-ts-data-tsplus.json"
|
|
44
|
+
],
|
|
45
|
+
"plugins": [
|
|
46
|
+
{
|
|
47
|
+
"name": "@effect/language-service",
|
|
48
|
+
"diagnostics": {
|
|
49
|
+
"1002": "none"
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
],
|
|
53
|
+
"transformers": [
|
|
54
|
+
{
|
|
55
|
+
"name": "@effect/language-service/transformer",
|
|
56
|
+
"trace": {}
|
|
57
|
+
}
|
|
58
|
+
],
|
|
59
|
+
"baseUrl": ".",
|
|
60
|
+
"tsBuildInfoFile": "./dist/.tsbuildinfo",
|
|
61
|
+
"rootDir": "./src"
|
|
62
|
+
},
|
|
63
|
+
"$schema": "https://json.schemastore.org/tsconfig",
|
|
64
|
+
"display": "Strictest",
|
|
65
|
+
"watchOptions": {
|
|
66
|
+
"watchFile": "useFsEvents",
|
|
67
|
+
"watchDirectory": "useFsEvents",
|
|
68
|
+
"fallbackPolling": "dynamicPriority",
|
|
69
|
+
"excludeDirectories": [
|
|
70
|
+
"**/node_modules",
|
|
71
|
+
"**/dist",
|
|
72
|
+
"**/.build",
|
|
73
|
+
"**/.git",
|
|
74
|
+
"**/.data",
|
|
75
|
+
"**/.logs",
|
|
76
|
+
"**/.*"
|
|
77
|
+
],
|
|
78
|
+
"excludeFiles": [
|
|
79
|
+
"**/*.tmp",
|
|
80
|
+
"openapi.json",
|
|
81
|
+
"*.json"
|
|
82
|
+
]
|
|
83
|
+
},
|
|
84
|
+
"include": [
|
|
85
|
+
"src/**/*"
|
|
86
|
+
],
|
|
87
|
+
"exclude": [
|
|
88
|
+
"**/node_modules",
|
|
89
|
+
"**/build",
|
|
90
|
+
"**/dist",
|
|
91
|
+
"**/.*"
|
|
92
|
+
],
|
|
93
|
+
"references": []
|
|
94
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "../../tsconfig.base.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"baseUrl": ".",
|
|
5
|
+
"outDir": "./dist",
|
|
6
|
+
"tsBuildInfoFile": "./dist/.tsbuildinfo",
|
|
7
|
+
"rootDir": "./src",
|
|
8
|
+
"target": "ES2018",
|
|
9
|
+
"module": "CommonJS",
|
|
10
|
+
"emitDecoratorMetadata": true,
|
|
11
|
+
"experimentalDecorators": true
|
|
12
|
+
},
|
|
13
|
+
"include": [
|
|
14
|
+
"src/**/*"
|
|
15
|
+
]
|
|
16
|
+
}
|