Skip to content

Commit

Permalink
Use typescript to parse source files
Browse files Browse the repository at this point in the history
Fixes #54
  • Loading branch information
Thomas Mair committed Jun 24, 2017
1 parent c2dc887 commit 7dc31ed
Show file tree
Hide file tree
Showing 6 changed files with 208 additions and 53 deletions.
3 changes: 2 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
{
"vsicons.presets.angular": false
"vsicons.presets.angular": false,
"prettier.printWidth": 100
}
185 changes: 152 additions & 33 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,59 +1,178 @@

var loaderUtils = require("loader-utils");
var ts = require("typescript");

// using: regex, capture groups, and capture group variables.
var templateUrlRegex = /templateUrl\s*:(\s*['"`](.*?)['"`]\s*([,}]))/gm;
var stylesRegex = /styleUrls *:(\s*\[[^\]]*?\])/g;
var stringRegex = /(['`"])((?:[^\\]\\\1|.)*?)\1/g;
function calculateTemplateUrlReplacement(propertyAssignment, replacedPropertyName) {
return [
{
start: propertyAssignment.name.getStart(),
end: propertyAssignment.name.getEnd(),
replacement: replacedPropertyName
},
constructUrlReplacement(propertyAssignment.initializer)
];
}

function replaceStringsWithRequires(string) {
return string.replace(stringRegex, function (match, quote, url) {
if (url.charAt(0) !== ".") {
url = "./" + url;
function calculateStyleUrlsReplacement(propertyAssignment, replacedPropertyName) {
var styleUrlsReplacements = [];
for (var i = 0; i < propertyAssignment.initializer.elements.length; i++) {
styleUrlsReplacements.push(constructUrlReplacement(propertyAssignment.initializer.elements[i]));
}
return [
{
start: propertyAssignment.name.getStart(),
end: propertyAssignment.name.getEnd(),
replacement: replacedPropertyName
}
return "require('" + url + "')";
});
].concat(styleUrlsReplacements);
}

module.exports = function(source, sourcemap) {
function constructUrlReplacement(element) {
var delimiter = "'";
if (element.kind === ts.SyntaxKind.FirstTemplateToken) {
delimiter = "`";
}
var url = element.text.replace(/(['"\\])/g, "\\$&");
if (url.charAt(0) !== ".") {
url = "./" + url;
}

return {
start: element.getStart(),
end: element.getEnd(),
replacement: "require(" + delimiter + url + delimiter + ")";
};
}

function isTemplateUrlProperty(propertyAssignment) {
return (
propertyAssignment.name.kind === ts.SyntaxKind.Identifier &&
propertyAssignment.name.text === "templateUrl" &&
(propertyAssignment.initializer.kind === ts.SyntaxKind.StringLiteral ||
propertyAssignment.initializer.kind === ts.SyntaxKind.FirstTemplateToken)
);
}

function isStyleUrlsPropery(propertyAssignment) {
return (
propertyAssignment.name.kind === ts.SyntaxKind.Identifier &&
propertyAssignment.name.text === "styleUrls" &&
propertyAssignment.initializer.kind === ts.SyntaxKind.ArrayLiteralExpression &&
propertyAssignment.initializer.elements.every(function(element) {
return (
element.kind === ts.SyntaxKind.StringLiteral ||
element.kind === ts.SyntaxKind.FirstTemplateToken
);
})
);
}

function flatten(prev, current) {
return prev.concat(current);
}

function findeReplacements(node, templateReplacement, styleReplacement) {
var positions = [];

calculateReplacements(node);

function calculateReplacements(node) {
switch (node.kind) {
// search only for class declarations
case ts.SyntaxKind.ClassDeclaration:
if (node.decorators && node.decorators.length > 0) {
// filter for all decorators containing a component decorator
var replacementPositions = node.decorators
.filter(function(decorator) {
return (
decorator.expression.kind === ts.SyntaxKind.CallExpression &&
decorator.expression.expression.kind === ts.SyntaxKind.Identifier &&
decorator.expression.expression.text === "Component" &&
decorator.expression.arguments.length > 0
);
})
.map(function(decorator) {
return decorator.expression.arguments[0];
})
// the argument must be a literal expression
.filter(function(argument) {
return (argument.kind = ts.SyntaxKind.ObjectLiteralExpression);
})
.map(function(literalExpression) {
return literalExpression.properties;
})
.reduce(flatten, [])
// filter for property assignments
.filter(function(properties) {
return properties.kind === ts.SyntaxKind.PropertyAssignment;
})
// only filter for property assignments with the text templateUrl or styleUrls
.filter(function(propertyAssignment) {
return (
isTemplateUrlProperty(propertyAssignment) || isStyleUrlsPropery(propertyAssignment)
);
})
.map(function(propertyAssignment) {
if (propertyAssignment.name.text === "templateUrl") {
return calculateTemplateUrlReplacement(propertyAssignment, templateReplacement);
} else {
return calculateStyleUrlsReplacement(propertyAssignment, styleReplacement);
}
})
.reduce(flatten, []);
positions = positions.concat(replacementPositions);
}
break;
}

return ts.forEachChild(node, calculateReplacements);
}

return positions;
}

module.exports = function(source, sourcemap) {
var config = {};
var query = loaderUtils.parseQuery(this.query);
var styleProperty = 'styles';
var templateProperty = 'template';
var styleProperty = "styles";
var templateProperty = "template";

if (this.options != null) {
Object.assign(config, this.options['angular2TemplateLoader']);
Object.assign(config, this.options["angular2TemplateLoader"]);
}

Object.assign(config, query);

if (config.keepUrl === true) {
styleProperty = 'styleUrls';
templateProperty = 'templateUrl';
styleProperty = "styleUrls";
templateProperty = "templateUrl";
}

// Not cacheable during unit tests;
// Not cacheable during unit tests;
this.cacheable && this.cacheable();

var newSource = source.replace(templateUrlRegex, function (match, url) {
// replace: templateUrl: './path/to/template.html'
// with: template: require('./path/to/template.html')
// or: templateUrl: require('./path/to/template.html')
// if `keepUrl` query parameter is set to true.
return templateProperty + ":" + replaceStringsWithRequires(url);
})
.replace(stylesRegex, function (match, urls) {
// replace: stylesUrl: ['./foo.css', "./baz.css", "./index.component.css"]
// with: styles: [require('./foo.css'), require("./baz.css"), require("./index.component.css")]
// or: styleUrls: [require('./foo.css'), require("./baz.css"), require("./index.component.css")]
// if `keepUrl` query parameter is set to true.
return styleProperty + ":" + replaceStringsWithRequires(urls);
});
var fileName = this.resourcePath;

var sourceFile = ts.createSourceFile(
fileName,
source,
ts.ScriptTarget.ES6,
/*setParentNodes */ true
);

var positions = findeReplacements(sourceFile, templateProperty, styleProperty);

var newSource = source;

for (var i = positions.length - 1; i >= 0; i--) {
var pos = positions[i];
var prefix = newSource.substring(0, pos.start);
var postfix = newSource.substring(pos.end);
newSource = prefix + pos.replacement + postfix;
}

// Support for tests
if (this.callback) {
this.callback(null, newSource, sourcemap)
this.callback(null, newSource, sourcemap);
} else {
return newSource;
}
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"should": "^9.0.0"
},
"dependencies": {
"loader-utils": "^0.2.15"
"loader-utils": "^0.2.15",
"typescript": "^2.3.4"
}
}
12 changes: 12 additions & 0 deletions test/fixtures/component_with_template_literals.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
var componentWithTemplateLiterals = `
import {Component} from '@angular/core';
@Component({
selector: 'test-component',
templateUrl: \`./some/path/to/file.html\`,
styleUrls: [\`./app/css/styles.css\`]
})
export class TestComponent {}
`;

module.exports = componentWithTemplateLiterals;
2 changes: 2 additions & 0 deletions test/fixtures/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ var componentWithoutRelPeriodSlash = require('./component_without_relative_perio
var componentWithSpacing = require('./component_with_spacing.js');
var componentWithSingleLineDecorator = require('./component_with_single_line_decorator.js');
var componentWithTemplateUrlEndingBySpace = require('./component_with_template_url_ending_by_space.js');
var componentWithTemplateLiterals = require('./component_with_template_literals.js');

exports.simpleAngularTestComponentFileStringSimple = sampleAngularComponentSimpleFixture;
exports.componentWithQuoteInUrls = componentWithQuoteInUrls;
Expand All @@ -13,3 +14,4 @@ exports.componentWithoutRelPeriodSlash = componentWithoutRelPeriodSlash;
exports.componentWithSpacing = componentWithSpacing;
exports.componentWithSingleLineDecorator = componentWithSingleLineDecorator;
exports.componentWithTemplateUrlEndingBySpace = componentWithTemplateUrlEndingBySpace;
exports.componentWithTemplateLiterals = componentWithTemplateLiterals;
Loading

0 comments on commit 7dc31ed

Please sign in to comment.