Skip to content

Commit 7dc31ed

Browse files
author
Thomas Mair
committed
Use typescript to parse source files
Fixes TheLarkInn#54
1 parent c2dc887 commit 7dc31ed

File tree

6 files changed

+208
-53
lines changed

6 files changed

+208
-53
lines changed

.vscode/settings.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
{
2-
"vsicons.presets.angular": false
2+
"vsicons.presets.angular": false,
3+
"prettier.printWidth": 100
34
}

index.js

Lines changed: 152 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,59 +1,178 @@
1-
21
var loaderUtils = require("loader-utils");
2+
var ts = require("typescript");
33

4-
// using: regex, capture groups, and capture group variables.
5-
var templateUrlRegex = /templateUrl\s*:(\s*['"`](.*?)['"`]\s*([,}]))/gm;
6-
var stylesRegex = /styleUrls *:(\s*\[[^\]]*?\])/g;
7-
var stringRegex = /(['`"])((?:[^\\]\\\1|.)*?)\1/g;
4+
function calculateTemplateUrlReplacement(propertyAssignment, replacedPropertyName) {
5+
return [
6+
{
7+
start: propertyAssignment.name.getStart(),
8+
end: propertyAssignment.name.getEnd(),
9+
replacement: replacedPropertyName
10+
},
11+
constructUrlReplacement(propertyAssignment.initializer)
12+
];
13+
}
814

9-
function replaceStringsWithRequires(string) {
10-
return string.replace(stringRegex, function (match, quote, url) {
11-
if (url.charAt(0) !== ".") {
12-
url = "./" + url;
15+
function calculateStyleUrlsReplacement(propertyAssignment, replacedPropertyName) {
16+
var styleUrlsReplacements = [];
17+
for (var i = 0; i < propertyAssignment.initializer.elements.length; i++) {
18+
styleUrlsReplacements.push(constructUrlReplacement(propertyAssignment.initializer.elements[i]));
19+
}
20+
return [
21+
{
22+
start: propertyAssignment.name.getStart(),
23+
end: propertyAssignment.name.getEnd(),
24+
replacement: replacedPropertyName
1325
}
14-
return "require('" + url + "')";
15-
});
26+
].concat(styleUrlsReplacements);
1627
}
1728

18-
module.exports = function(source, sourcemap) {
29+
function constructUrlReplacement(element) {
30+
var delimiter = "'";
31+
if (element.kind === ts.SyntaxKind.FirstTemplateToken) {
32+
delimiter = "`";
33+
}
34+
var url = element.text.replace(/(['"\\])/g, "\\$&");
35+
if (url.charAt(0) !== ".") {
36+
url = "./" + url;
37+
}
38+
39+
return {
40+
start: element.getStart(),
41+
end: element.getEnd(),
42+
replacement: "require(" + delimiter + url + delimiter + ")";
43+
};
44+
}
45+
46+
function isTemplateUrlProperty(propertyAssignment) {
47+
return (
48+
propertyAssignment.name.kind === ts.SyntaxKind.Identifier &&
49+
propertyAssignment.name.text === "templateUrl" &&
50+
(propertyAssignment.initializer.kind === ts.SyntaxKind.StringLiteral ||
51+
propertyAssignment.initializer.kind === ts.SyntaxKind.FirstTemplateToken)
52+
);
53+
}
54+
55+
function isStyleUrlsPropery(propertyAssignment) {
56+
return (
57+
propertyAssignment.name.kind === ts.SyntaxKind.Identifier &&
58+
propertyAssignment.name.text === "styleUrls" &&
59+
propertyAssignment.initializer.kind === ts.SyntaxKind.ArrayLiteralExpression &&
60+
propertyAssignment.initializer.elements.every(function(element) {
61+
return (
62+
element.kind === ts.SyntaxKind.StringLiteral ||
63+
element.kind === ts.SyntaxKind.FirstTemplateToken
64+
);
65+
})
66+
);
67+
}
68+
69+
function flatten(prev, current) {
70+
return prev.concat(current);
71+
}
1972

73+
function findeReplacements(node, templateReplacement, styleReplacement) {
74+
var positions = [];
75+
76+
calculateReplacements(node);
77+
78+
function calculateReplacements(node) {
79+
switch (node.kind) {
80+
// search only for class declarations
81+
case ts.SyntaxKind.ClassDeclaration:
82+
if (node.decorators && node.decorators.length > 0) {
83+
// filter for all decorators containing a component decorator
84+
var replacementPositions = node.decorators
85+
.filter(function(decorator) {
86+
return (
87+
decorator.expression.kind === ts.SyntaxKind.CallExpression &&
88+
decorator.expression.expression.kind === ts.SyntaxKind.Identifier &&
89+
decorator.expression.expression.text === "Component" &&
90+
decorator.expression.arguments.length > 0
91+
);
92+
})
93+
.map(function(decorator) {
94+
return decorator.expression.arguments[0];
95+
})
96+
// the argument must be a literal expression
97+
.filter(function(argument) {
98+
return (argument.kind = ts.SyntaxKind.ObjectLiteralExpression);
99+
})
100+
.map(function(literalExpression) {
101+
return literalExpression.properties;
102+
})
103+
.reduce(flatten, [])
104+
// filter for property assignments
105+
.filter(function(properties) {
106+
return properties.kind === ts.SyntaxKind.PropertyAssignment;
107+
})
108+
// only filter for property assignments with the text templateUrl or styleUrls
109+
.filter(function(propertyAssignment) {
110+
return (
111+
isTemplateUrlProperty(propertyAssignment) || isStyleUrlsPropery(propertyAssignment)
112+
);
113+
})
114+
.map(function(propertyAssignment) {
115+
if (propertyAssignment.name.text === "templateUrl") {
116+
return calculateTemplateUrlReplacement(propertyAssignment, templateReplacement);
117+
} else {
118+
return calculateStyleUrlsReplacement(propertyAssignment, styleReplacement);
119+
}
120+
})
121+
.reduce(flatten, []);
122+
positions = positions.concat(replacementPositions);
123+
}
124+
break;
125+
}
126+
127+
return ts.forEachChild(node, calculateReplacements);
128+
}
129+
130+
return positions;
131+
}
132+
133+
module.exports = function(source, sourcemap) {
20134
var config = {};
21135
var query = loaderUtils.parseQuery(this.query);
22-
var styleProperty = 'styles';
23-
var templateProperty = 'template';
136+
var styleProperty = "styles";
137+
var templateProperty = "template";
24138

25139
if (this.options != null) {
26-
Object.assign(config, this.options['angular2TemplateLoader']);
140+
Object.assign(config, this.options["angular2TemplateLoader"]);
27141
}
28142

29143
Object.assign(config, query);
30144

31145
if (config.keepUrl === true) {
32-
styleProperty = 'styleUrls';
33-
templateProperty = 'templateUrl';
146+
styleProperty = "styleUrls";
147+
templateProperty = "templateUrl";
34148
}
35149

36-
// Not cacheable during unit tests;
150+
// Not cacheable during unit tests;
37151
this.cacheable && this.cacheable();
38152

39-
var newSource = source.replace(templateUrlRegex, function (match, url) {
40-
// replace: templateUrl: './path/to/template.html'
41-
// with: template: require('./path/to/template.html')
42-
// or: templateUrl: require('./path/to/template.html')
43-
// if `keepUrl` query parameter is set to true.
44-
return templateProperty + ":" + replaceStringsWithRequires(url);
45-
})
46-
.replace(stylesRegex, function (match, urls) {
47-
// replace: stylesUrl: ['./foo.css', "./baz.css", "./index.component.css"]
48-
// with: styles: [require('./foo.css'), require("./baz.css"), require("./index.component.css")]
49-
// or: styleUrls: [require('./foo.css'), require("./baz.css"), require("./index.component.css")]
50-
// if `keepUrl` query parameter is set to true.
51-
return styleProperty + ":" + replaceStringsWithRequires(urls);
52-
});
153+
var fileName = this.resourcePath;
154+
155+
var sourceFile = ts.createSourceFile(
156+
fileName,
157+
source,
158+
ts.ScriptTarget.ES6,
159+
/*setParentNodes */ true
160+
);
161+
162+
var positions = findeReplacements(sourceFile, templateProperty, styleProperty);
163+
164+
var newSource = source;
165+
166+
for (var i = positions.length - 1; i >= 0; i--) {
167+
var pos = positions[i];
168+
var prefix = newSource.substring(0, pos.start);
169+
var postfix = newSource.substring(pos.end);
170+
newSource = prefix + pos.replacement + postfix;
171+
}
53172

54173
// Support for tests
55174
if (this.callback) {
56-
this.callback(null, newSource, sourcemap)
175+
this.callback(null, newSource, sourcemap);
57176
} else {
58177
return newSource;
59178
}

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
"should": "^9.0.0"
3636
},
3737
"dependencies": {
38-
"loader-utils": "^0.2.15"
38+
"loader-utils": "^0.2.15",
39+
"typescript": "^2.3.4"
3940
}
4041
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
var componentWithTemplateLiterals = `
2+
import {Component} from '@angular/core';
3+
4+
@Component({
5+
selector: 'test-component',
6+
templateUrl: \`./some/path/to/file.html\`,
7+
styleUrls: [\`./app/css/styles.css\`]
8+
})
9+
export class TestComponent {}
10+
`;
11+
12+
module.exports = componentWithTemplateLiterals;

test/fixtures/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ var componentWithoutRelPeriodSlash = require('./component_without_relative_perio
55
var componentWithSpacing = require('./component_with_spacing.js');
66
var componentWithSingleLineDecorator = require('./component_with_single_line_decorator.js');
77
var componentWithTemplateUrlEndingBySpace = require('./component_with_template_url_ending_by_space.js');
8+
var componentWithTemplateLiterals = require('./component_with_template_literals.js');
89

910
exports.simpleAngularTestComponentFileStringSimple = sampleAngularComponentSimpleFixture;
1011
exports.componentWithQuoteInUrls = componentWithQuoteInUrls;
@@ -13,3 +14,4 @@ exports.componentWithoutRelPeriodSlash = componentWithoutRelPeriodSlash;
1314
exports.componentWithSpacing = componentWithSpacing;
1415
exports.componentWithSingleLineDecorator = componentWithSingleLineDecorator;
1516
exports.componentWithTemplateUrlEndingBySpace = componentWithTemplateUrlEndingBySpace;
17+
exports.componentWithTemplateLiterals = componentWithTemplateLiterals;

0 commit comments

Comments
 (0)