Skip to content

Commit 961e97d

Browse files
authored
fix: bugs (#12)
* fix: fix stack-tracking parse bug * chore: update readme and help message * fix(tagged-template): support array substitution * chore: add type-check command * perf: add option to forceIntegrate
1 parent 3891a04 commit 961e97d

File tree

11 files changed

+133
-59
lines changed

11 files changed

+133
-59
lines changed

README.md

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -100,13 +100,17 @@ const createFileInformationMap = typeshot.registerTypeDefinition((createTypeFrag
100100
});
101101
```
102102

103-
`typeshot.registerTypeDefinition` is a core API to create custom type definitions. It receives a function to describe type definition and returns a function to instantiate type definition.
103+
`typeshot.registerTypeDefinition` is a core API to create custom type definitions. It receives a function to describe type definition and returns a function to instantiate type definition. `typeshot` converts returned value of the describer function into type definitions by parsing the value down to `TypeFragment` and usual values and evaluating them. You can see how `typeshot` evaluates `TypeFragment` and values, by enabling a CLI Option `-E`/`--emitIntermediateFiles`.
104104

105-
The describer function receives a utility function called `createTypeFragment` and arguments which come from the instantiator function.
105+
The describer function receives a utility function called `createTypeFragment` and rest arguments which come from the instantiator function. There are two steps to instantiate type definitions, the first step is to pass arguments that the describer function requires, and the second step is to specify type format and type name.
106106

107-
`typeshot` converts returned value of the describer function into type definitions by parsing the value down to `TypeFragment` and usual values and evaluating them.
108-
109-
You can see how `typeshot` evaluates `TypeFragment` and values, by enabling a CLI Option `-E`/`--emitIntermediateFiles`.
107+
In this example, it passes `paths` to describer function and specifies to output the instance as interface named `FileInformationMap`. Make sure to use `typeshot.print` to commit instances to the result.
108+
```ts
109+
typeshot.print`
110+
// Hello, I'm comment.
111+
export ${createFileInformationMap(paths).interface('FileInformationMap')}
112+
`;
113+
```
110114

111115
#### `TypeFragment`
112116
`TypeFragment` is a special object which contains information to connect values and types.
@@ -122,7 +126,7 @@ const fragment = createTypeFragment<{ [K in typeof p]: FileType[typeof extname]
122126

123127
Note that the names of type query target must match with the keys of object at the argument.
124128

125-
### Intersection Types
129+
#### Intersection Types
126130
As the example does, you can merge `TypeFragment` into a one object. Merged object is evaluated as an intersection type. In this example, it's internally evaluated as like this:
127131
```ts
128132
& { [K in './foo/bar.ts']: FileType['.ts'] }
@@ -164,9 +168,9 @@ type Example = ${example().literal()};
164168
This example will be evaluated into `type Example = 'foo' | 'bar' | 'baz';`.
165169

166170
### `print`
167-
To commit type definition instance to result, you need to use `typeshot.print`. It's accepts Tagged Template Literal.
171+
`typeshot.print` is a tag function to commit type definition instance to result.
168172

169-
As the example does, you can specify type format and name. You can specify extra strings to add comments, modifiers, etc.
173+
As the example does, it accepts extra contents to add comments, modifiers, etc.
170174
```ts
171175
typeshot.print`
172176
// Hello, I'm comment.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"main": "index.js",
66
"scripts": {
77
"build": "rimraf ./lib && tsc -p tsconfig.lib.json && cp package.json README.md LICENSE ./lib",
8+
"type-check": "tsc --noEmit -p tsconfig.lib.json",
89
"test": "jest",
910
"prepare": "mkdir -p node_modules/typeshot && echo \"export * as m from '../../src';\" > node_modules/typeshot/index.d.ts && echo \"module.exports = require('../../src')\" > node_modules/typeshot/index.js"
1011
},

src/cli/bin.ts

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -141,9 +141,47 @@ export const parseCLIOptions = (argv: string[]) => {
141141
const exitWithHelp = (error?: string) => {
142142
if (error) console.log(error);
143143
console.log(`
144-
typeshot
144+
SYNOPSIS
145+
typeshot [--outFile <path> | --outDir <path>] [--basePath <path>] [--rootDir <dir>] [--project <path>]
146+
[--prettierConfig <path>] [--systemModule <path>] [--maxParallel <count>] [--emitIntermediateFiles]
147+
[--inputFile <path> | [--] <path>...]
145148
146-
Help:
149+
OPTIONS
150+
<path>...
151+
input file paths
152+
153+
-h, --help
154+
display this message
155+
156+
-i, --inputFile
157+
input file path
158+
159+
-o, --outFile
160+
output file path, make sure to use with --inputFile
161+
162+
-O, --outDir
163+
output directory path
164+
165+
-b, --basePath
166+
base path to resolve relative paths, default value is process.cwd()
167+
168+
-p, --project
169+
tsconfig path, default value is tsconfig.json
170+
171+
--rootDir
172+
root directory to resolve output file path with outDir, basePath is used if omitted
173+
174+
--prettierConfig
175+
prettier config path
176+
177+
--systemModule
178+
see README.md about detail, default value is 'typescript'
179+
180+
--maxParallel
181+
max number of subprocess, default value is 3
182+
183+
-E, --emitIntermediateFiles
184+
whether to emit intermediate files, default value is false
147185
`);
148186
process.exit(error ? 1 : 0);
149187
};

src/program/intermediate-type/create-template.ts

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import ts from 'typescript';
22
import { getNodeByStack } from '../../utils/ast';
33
import { AstTemplate } from '../../utils/ast-template';
4-
import { CodeStack } from '../../utils/stack-tracking';
4+
import { TypeFragmentInfo } from '../type-definition';
55

66
interface TemplateSector {
77
types: FragmentAstTemplate[];
@@ -34,21 +34,29 @@ export class FragmentAstTemplate extends AstTemplate<string, [dependencies: Map<
3434
}
3535

3636
export const createFragmentTemplate = (
37-
fragmentId: string,
38-
stack: CodeStack,
37+
fragmentInfo: TypeFragmentInfo,
3938
sourceFile: ts.SourceFile,
4039
sourceText: string,
4140
): FragmentTemplate => {
42-
const { nodePath } = getNodeByStack(stack, sourceFile, ts.isCallExpression);
41+
const { nodePath } = getNodeByStack(fragmentInfo.stack, sourceFile, ts.isCallExpression);
4342
const nearestCallExpression = nodePath[nodePath.length - 1];
4443
if (!nearestCallExpression.typeArguments || nearestCallExpression.typeArguments.length !== 1) {
4544
const length = nearestCallExpression.typeArguments?.length || 0;
4645
throw new RangeError(
47-
`Invalid Type Argument Range: 'createTypeFragment' requires 1 type argument, but received ${length} at '${fragmentId}'`,
46+
`Invalid Type Argument Range: 'createTypeFragment' requires 1 type argument, but received ${length} at '${fragmentInfo.id}'`,
4847
);
4948
}
5049

5150
const node = nearestCallExpression.typeArguments[0];
51+
if (fragmentInfo.forceIntegrate) {
52+
return {
53+
solubleSigs: null,
54+
solubleTypes: new FragmentAstTemplate(node, sourceText).wrap('(', ')'),
55+
insolubleSigs: null,
56+
insolubleTypes: null,
57+
};
58+
}
59+
5260
const { soluble: S, insoluble: I } = parseFragmentTypeNode(undefined, node, sourceText);
5361
return {
5462
solubleSigs: S.sigs.length ? new FragmentAstTemplate(null).append(S.sigs) : null,

src/program/intermediate-type/create-type-text.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,5 +116,5 @@ const finalizeIntermediateTypeText = (
116116
result.push(`{${nestedInsolubleSigs.join('')}}`);
117117
}
118118

119-
return result.join(' & ');
119+
return result.join(' & ') || '{}';
120120
};

src/program/type-definition.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,16 @@ import { TypeInstance } from '../typeshot/type-instance';
44
import { getNodeByPosition, getNodeByStack, getSourceFileByStack } from '../utils/ast';
55
import { FragmentTemplate, createFragmentTemplate } from './intermediate-type/create-template';
66
import { evaluateIntermediateTypeNode } from './intermediate-type/evaluate-type-node';
7+
import { TypeFragmentInfoInit } from '../typeshot/register-type-definition';
78

8-
type FragmentInfo = CodeStack;
9+
export interface TypeFragmentInfo extends TypeFragmentInfoInit {
10+
id: string;
11+
stack: CodeStack;
12+
}
913
export interface TypeDefinitionInfo {
1014
id: string;
1115
stack: CodeStack;
12-
fragments: Map<string, FragmentInfo>;
16+
fragments: Map<string, TypeFragmentInfo>;
1317
}
1418

1519
export class TypeDefinition {
@@ -32,8 +36,8 @@ export class TypeDefinition {
3236
this.sourceFile = getSourceFileByStack(definition.stack, getSourceFile);
3337
[this.start, this.end] = varidateTypeDefinitionInfo(definition, this.sourceFile);
3438
this.sourceText = this.sourceFile.getFullText();
35-
definition.fragments.forEach((stack, id) => {
36-
this.fragmentTempaltes.set(id, createFragmentTemplate(id, stack, this.sourceFile, this.sourceText));
39+
definition.fragments.forEach((fragment, id) => {
40+
this.fragmentTempaltes.set(id, createFragmentTemplate(fragment, this.sourceFile, this.sourceText));
3741
});
3842
}
3943

src/typeshot/printer.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { reduceTaggedTemplate } from '../utils/tagged-template';
33
import { SourceTrace } from './source-trace';
44
import { isTypeInstance, TypeInstance } from './type-instance';
55

6-
export type PrinterSubtitution = string | number | boolean | undefined | null | TypeInstance;
6+
export type PrinterSubtitution = string | number | boolean | undefined | null | TypeInstance | PrinterTemplate;
77
export type PrinterTemplate = (string | TypeInstance | SourceTrace)[];
88

99
const stringifySubstitution = (substitution: PrinterSubtitution): PrinterTemplate[number] => {

src/typeshot/register-type-definition.ts

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,15 @@ import { withStackTracking } from '../utils/stack-tracking';
44
import { createTypeInstanceFactory, TypeInstanceFactory } from './type-instance';
55

66
// eslint-disable-next-line @typescript-eslint/no-unused-vars
7-
export type CreateTypeFragment = <_>(deps: FragmentDependencies) => Readonly<Fragment>;
8-
export type Fragment = Record<symbol, FragmentDependencies>;
7+
export type CreateTypeFragment = <_>(
8+
deps: FragmentDependencies,
9+
options?: TypeFragmentInfoInit,
10+
) => Readonly<TypeFragment>;
11+
export type TypeFragment = Record<symbol, FragmentDependencies>;
912
export type FragmentDependencies = Record<string, any>;
13+
export interface TypeFragmentInfoInit {
14+
forceIntegrate?: boolean;
15+
}
1016

1117
export const registerTypeDefinition: {
1218
<T extends any[]>(factory: (createTypeFragment: CreateTypeFragment, ...args: T) => any): {
@@ -24,17 +30,14 @@ export const registerTypeDefinition: {
2430

2531
return (...args): TypeInstanceFactory => {
2632
const { fragments } = definitionInfo;
27-
const createTypeFragment: CreateTypeFragment = withStackTracking(
28-
(fragmentStack, dependencies): Fragment => {
29-
// TODO: put some comment
30-
const fragmentId = `fragment@${fragmentStack.composed}`;
31-
if (!fragments.has(fragmentId)) {
32-
fragments.set(fragmentId, fragmentStack);
33-
}
33+
const createTypeFragment: CreateTypeFragment = withStackTracking((fragmentStack, dependencies, options = {}) => {
34+
const fragmentId = `fragment@${fragmentStack.composed}`;
35+
if (!fragments.has(fragmentId)) {
36+
fragments.set(fragmentId, { ...options, id: fragmentId, stack: fragmentStack });
37+
}
3438

35-
return Object.assign(Object.create(null), { [Symbol(fragmentId)]: dependencies });
36-
},
37-
);
39+
return Object.assign(Object.create(null), { [Symbol(fragmentId)]: dependencies });
40+
});
3841

3942
const value = factory(createTypeFragment, ...args);
4043
return createTypeInstanceFactory(definitionId, value);

src/utils/__tests__/tagged-template.test.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,5 +25,14 @@ describe('reduceTaggedTemplate', () => {
2525
expect(result).toEqual(prev);
2626
expect(stringify).not.toBeCalled();
2727
}
28+
29+
{
30+
const stringify = jest.fn(() => '');
31+
const arr = [...acc];
32+
const result = reduceTaggedTemplate(acc, ['-', '_'], [arr], stringify);
33+
expect(result).toBe(acc);
34+
expect(result).toEqual(['foo1bar', bar, 'baz', '-', 'foo1bar', bar, 'baz', '_']);
35+
expect(stringify).not.toBeCalled();
36+
}
2837
});
2938
});

src/utils/stack-tracking.ts

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,29 +5,30 @@ export interface CodeStack {
55
col: number;
66
}
77

8-
const parseStack = (stack: string = '', depth: number): CodeStack => {
8+
const parseStack = (stack: string, depth: number): CodeStack => {
99
if (!stack) throw new Error('Invalid Stack Information: make sure to run with node');
10-
stack = stack.replace(/^Error:\s+\n/, '');
11-
for (let i = depth; stack && i > 0; i--) {
12-
stack = stack.slice(stack.indexOf('\n') + 1);
10+
let cursor = 0;
11+
for (let i = depth + 1; stack && i > 0; i--) {
12+
cursor = stack.indexOf('\n', cursor) + 1;
1313
}
14-
stack = stack.replace(/^\s+at\s.+\((.+)\)$/m, '$1');
14+
// Ignore the case if the stack doesn't include any new line
15+
const composed = stack
16+
.slice(cursor + /* '____at_' ('_'=' ') */ 7, stack.indexOf('\n', cursor))
17+
.replace(STACK_PARENTHESES, '');
1518

1619
if (!stack) throw new Error('Invalid Stack Information: invalid depth');
17-
const tailingNewLineIndex = stack.indexOf('\n');
18-
const composed = tailingNewLineIndex === -1 ? stack : stack.slice(0, tailingNewLineIndex);
1920
const [filename, line, col] = composed.split(':');
20-
2121
return { composed, filename, line: ~~line, col: ~~col };
2222
};
23+
const STACK_PARENTHESES = /(^.+\(|\)$)/g;
2324

24-
const getCachedStack = (stack: string = '', depth: number, cacheStore: Map<string, CodeStack>) => {
25-
const result = parseStack(stack, depth);
26-
const cached = cacheStore.get(result.composed);
27-
return cached || result;
28-
};
29-
25+
const target = Object.create(null) as { stack: string };
26+
const cacheStore = new Map<string, CodeStack>();
3027
export const withStackTracking = <T, U extends any[]>(func: (stack: CodeStack, ...args: U) => T) => {
31-
const cacheStore = new Map<string, CodeStack>();
32-
return (...args: U) => func(getCachedStack(new Error().stack, 1, cacheStore), ...args);
28+
return (...args: U) => {
29+
Error.stackTraceLimit = 2;
30+
Error.captureStackTrace(target);
31+
Error.stackTraceLimit = 10;
32+
return func(cacheStore.get(target.stack) || parseStack(target.stack, 1), ...args);
33+
};
3334
};

0 commit comments

Comments
 (0)