Skip to content

Commit ab07e4e

Browse files
committed
feat: template, factory
1 parent 7eda62d commit ab07e4e

File tree

7 files changed

+4427
-8539
lines changed

7 files changed

+4427
-8539
lines changed

package-lock.json

Lines changed: 4167 additions & 8428 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 13 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
2-
"name": "@propero/ts-library-template",
2+
"name": "@propero/typescript-transformer-source-templates",
33
"version": "0.0.0",
4-
"description": "Template for Typescript Libraries",
4+
"description": "Generate fresh ASTs from source strings",
55
"since": "2020",
66
"main": "dist/main.cjs.js",
77
"module": "dist/main.esm.js",
@@ -16,13 +16,17 @@
1616
"build": "rollup -c rollup.config.js",
1717
"clean": "rimraf dist temp docs coverage",
1818
"test": "jest",
19+
"test:watch": "jest --watch",
1920
"lint": "eslint --ignore-path .gitignore --ext .ts .",
20-
"lint:fix": "eslint --ignore-path .gitignore --ext .ts --fix .",
21-
"postinstall": "ts-node postinstall.ts"
21+
"lint:fix": "eslint --ignore-path .gitignore --ext .ts --fix ."
2222
},
2323
"keywords": [],
2424
"author": "Propero Team <team@propero.dev>",
2525
"license": "MIT",
26+
"dependencies": {
27+
"typescript": "*",
28+
"@wessberg/ts-clone-node": "^0.3.3"
29+
},
2630
"devDependencies": {
2731
"@commitlint/cli": "^8.3.5",
2832
"@commitlint/config-conventional": "^8.3.4",
@@ -38,14 +42,9 @@
3842
"@semantic-release/npm": "^6.0.0",
3943
"@semantic-release/release-notes-generator": "^7.3.5",
4044
"@types/dotenv-flow": "^3.0.0",
41-
"@types/is-ci": "^2.0.0",
4245
"@types/jest": "^24.9.0",
4346
"@types/lodash": "^4.14.149",
4447
"@types/node": "^13.7.1",
45-
"@types/promptly": "^3.0.0",
46-
"@types/webpack": "^4.41.6",
47-
"@types/webpack-dev-server": "^3.10.0",
48-
"@types/webpack-env": "^1.15.1",
4948
"@typescript-eslint/eslint-plugin": "^2.17.0",
5049
"@typescript-eslint/parser": "^2.17.0",
5150
"@wessberg/rollup-plugin-ts": "^1.2.17",
@@ -56,13 +55,11 @@
5655
"eslint-config-prettier": "^6.10.0",
5756
"eslint-plugin-prettier": "^3.1.2",
5857
"husky": "^4.2.3",
59-
"is-ci": "^2.0.0",
6058
"jest": "^25.1.0",
6159
"jest-preset-typescript": "^1.2.0",
6260
"lint-staged": "^10.0.1",
6361
"lodash": "^4.17.15",
6462
"prettier": "^1.19.1",
65-
"promptly": "^3.0.3",
6663
"rollup": "^1.29.1",
6764
"rollup-plugin-commonjs": "^10.1.0",
6865
"rollup-plugin-json": "^4.0.0",
@@ -71,11 +68,7 @@
7168
"rollup-plugin-ts-paths": "^1.0.3",
7269
"semantic-release": "^16.0.2",
7370
"ts-jest": "^25.2.1",
74-
"ts-node": "^8.6.2",
75-
"typescript": "^3.7.5",
76-
"webpack": "^4.41.6",
77-
"webpack-cli": "^3.3.11",
78-
"webpack-dev-server": "^3.10.3"
71+
"ts-node": "^8.6.2"
7972
},
8073
"husky": {
8174
"hooks": {
@@ -97,15 +90,14 @@
9790
}
9891
},
9992
"directories": {
100-
"doc": "docs"
93+
"test": "test"
10194
},
102-
"dependencies": {},
10395
"repository": {
10496
"type": "git",
105-
"url": "git+https://github.com/propero-oss/ts-library-template.git"
97+
"url": "git+https://github.com/propero-oss/typescript-transformer-source-templates.git"
10698
},
10799
"bugs": {
108-
"url": "https://github.com/propero-oss/ts-library-template/issues"
100+
"url": "https://github.com/propero-oss/typescript-transformer-source-templates/issues"
109101
},
110-
"homepage": "https://github.com/propero-oss/ts-library-template#readme"
102+
"homepage": "https://github.com/propero-oss/typescript-transformer-source-templates#readme"
111103
}

postinstall.ts

Lines changed: 0 additions & 38 deletions
This file was deleted.

src/main.ts

Lines changed: 172 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,172 @@
1-
/**
2-
* Documentation goes here
3-
* @public
4-
*/
5-
export const HELLO = "Hello";
1+
import { cloneNode, CloneNodeOptions } from "@wessberg/ts-clone-node";
2+
import {
3+
ScriptTarget,
4+
ScriptKind,
5+
createSourceFile,
6+
Statement,
7+
Expression,
8+
VariableStatement,
9+
VariableDeclaration,
10+
SourceFile,
11+
NodeArray
12+
} from "typescript";
13+
14+
interface Replacements {
15+
[key: string]: any;
16+
}
17+
18+
export class TemplateFactory {
19+
public constructor(
20+
protected source: string,
21+
protected target: ScriptTarget,
22+
protected kind: ScriptKind,
23+
protected cloneOpts?: CloneNodeOptions
24+
) {}
25+
26+
/**
27+
* Replaces all occurrences of all keys of the given replacement map with its corresponding values.
28+
* @param replacements - The map of strings to replace within the source
29+
* @param source - Optionally you can provide your own source instead of the internal source
30+
* @return the processed source string
31+
* @public
32+
*/
33+
public replace(replacements?: Replacements, source: string = this.source): string {
34+
if (!replacements) return source;
35+
return Object.keys(replacements).reduce((source, key) => source.replace(key, replacements[key]), source);
36+
}
37+
38+
/**
39+
* Creates a virtual source file from the internal source
40+
* @param fileName - The name of the file to create
41+
* @param replacements - The replacement map to apply to the source {@link replace}
42+
* @param source - Optionally you can provide your own source instead of the internal source
43+
* @return A typescript SourceFile instance
44+
* @public
45+
*/
46+
public file(fileName = "", replacements?: Replacements, source: string = this.source): SourceFile {
47+
return createSourceFile(fileName, this.replace(replacements, source), this.target, true, this.kind);
48+
}
49+
50+
/**
51+
* Builds a list of statements from the internal source.
52+
* @param replacements - The replacement map to apply to the source {@link replace}
53+
* @param source - Optionally you can provide your own source instead of the internal source
54+
* @return An array of typescript statements, cloned and ready for insertion / replacement
55+
* @public
56+
*/
57+
public statements<T extends Statement = Statement>(replacements?: Replacements, source: string = this.source): T[] {
58+
return this.unclonedStatements<T>(replacements, source).map(this.cloner);
59+
}
60+
61+
/**
62+
* Builds a statement from the internal source.
63+
* @param replacements - The replacement map to apply to the source {@link replace}
64+
* @param source - Optionally you can provide your own source instead of the internal source
65+
* @return A typescript statement, cloned and ready for insertion / replacement
66+
* @public
67+
*/
68+
public statement<T extends Statement = Statement>(replacements?: Replacements, source: string = this.source): T {
69+
return this.clone(this.unclonedStatement<T>(replacements, source));
70+
}
71+
72+
/**
73+
* Builds a declaration from the internal source.
74+
* @param replacements - The replacement map to apply to the source {@link replace}
75+
* @param source - Optionally you can provide your own source instead of the internal source
76+
* @return A typescript variable declaration, cloned and ready for insertion / replacement
77+
* @public
78+
*/
79+
public declaration<T extends VariableDeclaration = VariableDeclaration>(replacements?: Replacements, source: string = this.source): T {
80+
return this.clone(this.unclonedDeclaration<T>(replacements, source));
81+
}
82+
83+
/**
84+
* Builds an expression from the internal source.
85+
* @param replacements - The replacement map to apply to the source {@link replace}
86+
* @param source - Optionally you can provide your own source instead of the internal source
87+
* @return A typescript expression, cloned and ready for insertion / replacement
88+
* @public
89+
*/
90+
public expression<T extends Expression = Expression>(replacements?: Replacements, source: string = this.source): T {
91+
return this.clone(this.unclonedExpression<T>(replacements, source));
92+
}
93+
94+
/**
95+
* Builds a list of statements from the internal source.
96+
* @param replacements - The replacement map to apply to the source {@link replace}
97+
* @param source - Optionally you can provide your own source instead of the internal source
98+
* @return An array of typescript statements, not fit for insertion yet
99+
* @internal
100+
*/
101+
protected unclonedStatements<T extends Statement = Statement>(replacements?: Replacements, source: string = this.source): NodeArray<T> {
102+
return this.file("", replacements, source).statements as NodeArray<T>;
103+
}
104+
105+
/**
106+
* Builds a statement from the internal source.
107+
* @param replacements - The replacement map to apply to the source {@link replace}
108+
* @param source - Optionally you can provide your own source instead of the internal source
109+
* @return A typescript statement, not fit for insertion yet
110+
* @internal
111+
*/
112+
protected unclonedStatement<T extends Statement = Statement>(replacements?: Replacements, source: string = this.source): T {
113+
return this.unclonedStatements<T>(replacements, source)[0];
114+
}
115+
116+
/**
117+
* Builds a declaration from the internal source.
118+
* @param replacements - The replacement map to apply to the source {@link replace}
119+
* @param source - Optionally you can provide your own source instead of the internal source
120+
* @return A typescript variable declaration, not fit for insertion yet
121+
* @internal
122+
*/
123+
protected unclonedDeclaration<T extends VariableDeclaration = VariableDeclaration>(
124+
replacements?: Replacements,
125+
source: string = this.source
126+
): T {
127+
return this.unclonedStatement<VariableStatement>(replacements, source).declarationList.declarations[0] as T;
128+
}
129+
130+
/**
131+
* Builds an expression from the internal source.
132+
* @param replacements - The replacement map to apply to the source {@link replace}
133+
* @param source - Optionally you can provide your own source instead of the internal source
134+
* @return A typescript expression, not fit for insertion yet
135+
* @internal
136+
*/
137+
protected unclonedExpression<T extends Expression = Expression>(replacements?: Replacements, source: string = this.source): T {
138+
const wrapped = `const __TEMPLATE_REPLACE__${Date.now()}_${0 | (Math.random() * 10000)} = ${source};`;
139+
return this.unclonedDeclaration(replacements, wrapped).initializer as T;
140+
}
141+
142+
/**
143+
* A function for use in {@link Array#map}.
144+
* Clones all passed elements.
145+
* {@link clone}, {@link cloneOpts}
146+
* @internal
147+
*/
148+
protected get cloner() {
149+
return (node: any) => this.clone(node);
150+
}
151+
152+
/**
153+
* A function that clones all passed nodes using {@link cloneOpts}.
154+
* @param node - The node to clone
155+
* @returns The cloned node
156+
* {@link clone}, {@link cloneOpts}
157+
* @internal
158+
*/
159+
protected clone<T extends any>(node: T): T {
160+
return cloneNode(node, this.cloneOpts) as T;
161+
}
162+
}
163+
164+
export interface TemplateOptions {
165+
target?: ScriptTarget;
166+
kind?: ScriptKind;
167+
clone?: CloneNodeOptions;
168+
}
169+
170+
export function template(source: string, { target = ScriptTarget.Latest, kind = ScriptKind.TS, clone }: TemplateOptions = {}) {
171+
return new TemplateFactory(source, target, kind, clone);
172+
}

test/index.test.ts

Lines changed: 74 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,77 @@
1-
import { HELLO } from "src/main";
1+
import { template, TemplateFactory } from "src/main";
2+
import { isSourceFile, VariableStatement, isVariableStatement, isVariableDeclaration, ObjectLiteralExpression, isObjectLiteralExpression } from "typescript";
23

3-
describe("main.ts", () => {
4-
it("should export its members", () => {
5-
expect(HELLO).toEqual("Hello");
4+
const source = "const HELLO = 'WORLD';";
5+
const tmpl = template(source);
6+
const sourceMulti = "const HELLO = 'WORLD';\nconst WORLD = 'HELLO';\nconsole.log('HELLO');";
7+
const tmplMulti = template(sourceMulti);
8+
9+
describe("template", () => {
10+
it("should construct an instance of TemplateFactory", () => {
11+
expect(tmpl).toBeInstanceOf(TemplateFactory);
12+
});
13+
});
14+
15+
describe("TemplateFactory", () => {
16+
describe("#replace()", () => {
17+
it("should return the plain source if no replacements are given", () => {
18+
expect(tmpl.replace()).toEqual(source);
19+
});
20+
it("should only replace matching strings", () => {
21+
expect(tmpl.replace({ HELLO: "GOODBYE" })).toEqual("const GOODBYE = 'WORLD';");
22+
});
23+
it("should support custom source", () => {
24+
expect(tmpl.replace({ HELLO: "GOODBYE" }, "const WORLD = 'HELLO';")).toEqual("const WORLD = 'GOODBYE';");
25+
});
26+
it("should execute replacer functions", () => {
27+
expect(tmpl.replace({ HELLO: () => "GOODBYE" })).toEqual("const GOODBYE = 'WORLD';");
28+
});
29+
});
30+
describe("#file()", () => {
31+
it("should create a source file", () => {
32+
const file = tmpl.file("test.ts");
33+
expect(isSourceFile(file)).toBeTruthy();
34+
expect(file.fileName).toEqual("test.ts");
35+
});
36+
});
37+
describe("#statements()", () => {
38+
const stmts = tmplMulti.statements<VariableStatement>();
39+
it("should extract all statements", () => {
40+
expect(stmts).toHaveLength(3);
41+
expect(stmts.map(isVariableStatement)).toEqual([true, true, false]);
42+
});
43+
it("should clone all statements", () => {
44+
expect(stmts.map(it => it.parent)).toEqual([undefined, undefined, undefined]);
45+
});
46+
});
47+
describe("#statement()", () => {
48+
const stmt = tmplMulti.statement<VariableStatement>();
49+
it("should extract the first statement", () => {
50+
expect(stmt).toBeDefined();
51+
expect(isVariableStatement(stmt)).toBeTruthy();
52+
});
53+
it("should clone the first statement", () => {
54+
expect(stmt.parent).toBeUndefined();
55+
});
56+
});
57+
describe("#declaration()", () => {
58+
const dec = tmplMulti.declaration();
59+
it("should extract the first variable declaration", () => {
60+
expect(dec).toBeDefined();
61+
expect(isVariableDeclaration(dec)).toBeTruthy();
62+
});
63+
it("should clone the first variable declaration", () => {
64+
expect(dec.parent).toBeUndefined();
65+
});
66+
});
67+
describe("#expression()", () => {
68+
const expr = template("{ HELLO: 'WORLD' }").expression<ObjectLiteralExpression>();
69+
it("should extract an expression", () => {
70+
expect(expr).toBeDefined();
71+
expect(isObjectLiteralExpression(expr)).toBeTruthy();
72+
});
73+
it("should clone the expression", () => {
74+
expect(expr.parent).toBeUndefined();
75+
});
676
});
777
});

0 commit comments

Comments
 (0)