fix(compiler): support css stylesheets in offline compiler

This commit is contained in:
Tobias Bosch
2016-05-02 09:38:46 -07:00
parent c386fc8379
commit 00d3b6083c
38 changed files with 436 additions and 388 deletions

View File

@ -6,6 +6,7 @@ import * as ts from 'typescript';
import * as path from 'path';
import * as compiler from '@angular/compiler';
import {ViewEncapsulation} from '@angular/core';
import {StaticReflector} from './static_reflector';
import {CompileMetadataResolver} from '@angular/compiler/src/metadata_resolver';
import {HtmlParser} from '@angular/compiler/src/html_parser';
@ -22,7 +23,8 @@ import {Parse5DomAdapter} from '@angular/platform-server';
import {MetadataCollector} from 'ts-metadata-collector';
import {NodeReflectorHost} from './reflector_host';
const SOURCE_EXTENSION = /\.[jt]s$/;
const GENERATED_FILES = /\.ngfactory\.ts$|\.css\.ts$|\.css\.shim\.ts$/;
const PREAMBLE = `/**
* This file is generated by the Angular 2 template compiler.
* Do not edit.
@ -89,31 +91,55 @@ export class CodeGenerator {
return result;
}
codegen(): Promise<void[]> {
// TODO(tbosch): add a cache for shared css files
// TODO(tbosch): detect cycles!
private generateStylesheet(filepath: string, shim: boolean): Promise<any> {
return this.compiler.loadAndCompileStylesheet(filepath, shim, '.ts')
.then((sourceWithImports) => {
// Write codegen in a directory structure matching the sources.
// TODO(alexeagle): relativize paths by the rootDirs option
const emitPath =
path.join(this.ngOptions.genDir,
path.relative(this.basePath, sourceWithImports.source.moduleUrl));
this.host.writeFile(emitPath, PREAMBLE + sourceWithImports.source.source, false);
return Promise.all(
sourceWithImports.importedUrls.map(url => this.generateStylesheet(url, shim)));
});
}
codegen(): Promise<any> {
Parse5DomAdapter.makeCurrent();
const generateOneFile = (absSourcePath: string) =>
Promise.all(this.readComponents(absSourcePath))
.then((metadatas: compiler.CompileDirectiveMetadata[]) => {
if (!metadatas || !metadatas.length) {
return;
}
let stylesheetPromises: Promise<any>[] = [];
metadatas.forEach((metadata) => {
let stylesheetPaths = metadata && metadata.template && metadata.template.styleUrls;
if (stylesheetPaths) {
stylesheetPaths.forEach((path) => {
stylesheetPromises.push(this.generateStylesheet(
path, metadata.template.encapsulation === ViewEncapsulation.Emulated));
});
}
});
const generated = this.generateSource(metadatas);
const sourceFile = this.program.getSourceFile(absSourcePath);
// Write codegen in a directory structure matching the sources.
// TODO(alexeagle): maybe use generated.moduleUrl instead of hardcoded ".ngfactory.ts"
// TODO(alexeagle): relativize paths by the rootDirs option
const emitPath =
path.join(this.ngOptions.genDir, path.relative(this.basePath, absSourcePath))
.replace(SOURCE_EXTENSION, '.ngfactory.ts');
const emitPath = path.join(this.ngOptions.genDir,
path.relative(this.basePath, generated.moduleUrl));
this.host.writeFile(emitPath, PREAMBLE + generated.source, false, () => {},
[sourceFile]);
return Promise.all(stylesheetPromises);
})
.catch((e) => { console.error(e.stack); });
return Promise.all(this.program.getRootFileNames()
.filter(f => !/\.ngfactory\.ts$/.test(f))
.map(generateOneFile));
return Promise.all(
this.program.getRootFileNames().filter(f => !GENERATED_FILES.test(f)).map(generateOneFile));
}
static create(ngOptions: AngularCompilerOptions, program: ts.Program, options: ts.CompilerOptions,
@ -129,7 +155,8 @@ export class CodeGenerator {
/*console*/ null, []);
const offlineCompiler = new compiler.OfflineCompiler(
normalizer, tmplParser, new StyleCompiler(urlResolver),
new ViewCompiler(new compiler.CompilerConfig(true, true, true)), new TypeScriptEmitter());
new ViewCompiler(new compiler.CompilerConfig(true, true, true)),
new TypeScriptEmitter(reflectorHost), xhr);
const resolver = new CompileMetadataResolver(
new compiler.DirectiveResolver(staticReflector), new compiler.PipeResolver(staticReflector),
new compiler.ViewResolver(staticReflector), null, null, staticReflector);

View File

@ -0,0 +1,7 @@
import {__compiler_private__ as _} from '@angular/compiler';
export var AssetUrl: typeof _.AssetUrl = _.AssetUrl;
export type AssetUrl = _.AssetUrl;
export var ImportGenerator: typeof _.ImportGenerator = _.ImportGenerator;
export type ImportGenerator = _.ImportGenerator;

View File

@ -4,11 +4,12 @@ import {MetadataCollector, ModuleMetadata} from 'ts-metadata-collector';
import * as fs from 'fs';
import * as path from 'path';
import {AngularCompilerOptions} from './codegen';
import {ImportGenerator, AssetUrl} from './compiler_private';
const EXT = /(\.ts|\.d\.ts|\.js|\.jsx|\.tsx)$/;
const DTS = /\.d\.ts$/;
export class NodeReflectorHost implements StaticReflectorHost {
export class NodeReflectorHost implements StaticReflectorHost, ImportGenerator {
private metadataCollector = new MetadataCollector();
constructor(private program: ts.Program, private compilerHost: ts.CompilerHost,
private options: ts.CompilerOptions, private ngOptions: AngularCompilerOptions) {}
@ -33,34 +34,48 @@ export class NodeReflectorHost implements StaticReflectorHost {
private resolve(m: string, containingFile: string) {
const resolved =
ts.resolveModuleName(m, containingFile, this.options, this.compilerHost).resolvedModule;
return resolved ? resolved.resolvedFileName : null
return resolved ? resolved.resolvedFileName : null;
};
private resolveAssetUrl(url: string, containingFile: string): string {
let assetUrl = AssetUrl.parse(url);
if (assetUrl) {
return this.resolve(`${assetUrl.packageName}/${assetUrl.modulePath}`, containingFile);
}
return url;
}
/**
* We want a moduleId that will appear in import statements in the generated code.
* These need to be in a form that system.js can load, so absolute file paths don't work.
* Relativize the paths by checking candidate prefixes of the absolute path, to see if
* they are resolvable by the moduleResolution strategy from the CompilerHost.
*/
private getModuleId(declarationFile: string, containingFile: string) {
const parts = declarationFile.replace(EXT, '').split(path.sep).filter(p => !!p);
getImportPath(containingFile: string, importedFile: string) {
importedFile = this.resolveAssetUrl(importedFile, '');
containingFile = this.resolveAssetUrl(containingFile, '');
// TODO(tbosch): if a file does not yet exist (because we compile it later),
// we still need to create it so that the `resolve` method works!
if (!this.compilerHost.fileExists(importedFile)) {
this.compilerHost.writeFile(importedFile, '', false);
fs.writeFileSync(importedFile, '');
}
const parts = importedFile.replace(EXT, '').split(path.sep).filter(p => !!p);
for (let index = parts.length - 1; index >= 0; index--) {
let candidate = parts.slice(index, parts.length).join(path.sep);
if (this.resolve(candidate, containingFile) === declarationFile) {
let pkg = parts[index];
let pkgPath = parts.slice(index + 1, parts.length).join(path.sep);
return `asset:${pkg}/lib/${pkgPath}`;
if (this.resolve('.' + path.sep + candidate, containingFile) === importedFile) {
return `./${candidate}`;
}
}
for (let index = parts.length - 1; index >= 0; index--) {
let candidate = parts.slice(index, parts.length).join(path.sep);
if (this.resolve('.' + path.sep + candidate, containingFile) === declarationFile) {
return `asset:./lib/${candidate}`;
if (this.resolve(candidate, containingFile) === importedFile) {
return candidate;
}
}
throw new Error(
`Unable to find any resolvable import for ${declarationFile} relative to ${containingFile}`);
`Unable to find any resolvable import for ${importedFile} relative to ${containingFile}`);
}
findDeclaration(module: string, symbolName: string, containingFile: string,
@ -93,9 +108,8 @@ export class NodeReflectorHost implements StaticReflectorHost {
}
const declaration = symbol.getDeclarations()[0];
const declarationFile = declaration.getSourceFile().fileName;
const moduleId = this.getModuleId(declarationFile, containingFile);
return this.getStaticSymbol(moduleId, declarationFile, symbol.getName());
return this.getStaticSymbol(declarationFile, symbol.getName());
} catch (e) {
console.error(`can't resolve module ${module} from ${containingFile}`);
throw e;
@ -108,15 +122,14 @@ export class NodeReflectorHost implements StaticReflectorHost {
* getStaticSymbol produces a Type whose metadata is known but whose implementation is not loaded.
* All types passed to the StaticResolver should be pseudo-types returned by this method.
*
* @param moduleId the module identifier as an absolute path.
* @param declarationFile the absolute path of the file where the symbol is declared
* @param name the name of the type.
*/
getStaticSymbol(moduleId: string, declarationFile: string, name: string): StaticSymbol {
getStaticSymbol(declarationFile: string, name: string): StaticSymbol {
let key = `"${declarationFile}".${name}`;
let result = this.typeCache.get(key);
if (!result) {
result = new StaticSymbol(moduleId, declarationFile, name);
result = new StaticSymbol(declarationFile, name);
this.typeCache.set(key, result);
}
return result;

View File

@ -43,9 +43,9 @@ import {
*/
export interface StaticReflectorHost {
/**
* Return a ModuleMetadata for the given module.
* Return a ModuleMetadata for the given module.
*
* @param moduleId is a string identifier for a module as an absolute path.
* @param modulePath is a string identifier for a module as an absolute path.
* @returns the metadata for the given module.
*/
getMetadataFor(modulePath: string): {[key: string]: any};
@ -57,7 +57,7 @@ export interface StaticReflectorHost {
*/
findDeclaration(modulePath: string, symbolName: string, containingFile?: string): StaticSymbol;
getStaticSymbol(moduleId: string, declarationFile: string, name: string): StaticSymbol;
getStaticSymbol(declarationFile: string, name: string): StaticSymbol;
angularImportLocations():
{coreDecorators: string, diDecorators: string, diMetadata: string, provider: string};
@ -66,10 +66,10 @@ export interface StaticReflectorHost {
/**
* A token representing the a reference to a static type.
*
* This token is unique for a moduleId and name and can be used as a hash table key.
* This token is unique for a filePath and name and can be used as a hash table key.
*/
export class StaticSymbol {
constructor(public moduleId: string, public filePath: string, public name: string) {}
constructor(public filePath: string, public name: string) {}
}
/**
@ -324,8 +324,7 @@ export class StaticReflector implements ReflectorReader {
staticSymbol = _this.host.findDeclaration(expression['module'], expression['name'],
context.filePath);
} else {
staticSymbol = _this.host.getStaticSymbol(context.moduleId, context.filePath,
expression['name']);
staticSymbol = _this.host.getStaticSymbol(context.filePath, expression['name']);
}
let result = staticSymbol;
let moduleMetadata = _this.getModuleMetadata(staticSymbol.filePath);

View File

@ -14,7 +14,7 @@ import {ListWrapper} from '@angular/facade/src/collection';
import {StaticReflector, StaticReflectorHost, StaticSymbol} from './static_reflector';
describe('StaticReflector', () => {
let noContext = new StaticSymbol('', '', '');
let noContext = new StaticSymbol('', '');
let host: StaticReflectorHost;
let reflector: StaticReflector;
@ -34,7 +34,6 @@ describe('StaticReflector', () => {
let annotation = annotations[0];
expect(annotation.selector).toEqual('[ngFor][ngForOf]');
expect(annotation.inputs).toEqual(['ngForTrackBy', 'ngForOf', 'ngForTemplate']);
});
it('should get constructor for NgFor', () => {
@ -226,15 +225,15 @@ describe('StaticReflector', () => {
});
it('should simplify a module reference', () => {
expect(simplify(new StaticSymbol('', '/src/cases', ''),
expect(simplify(new StaticSymbol('/src/cases', ''),
({__symbolic: "reference", module: "./extern", name: "s"})))
.toEqual("s");
});
it('should simplify a non existing reference as a static symbol', () => {
expect(simplify(new StaticSymbol('', '/src/cases', ''),
expect(simplify(new StaticSymbol('/src/cases', ''),
({__symbolic: "reference", module: "./extern", name: "nonExisting"})))
.toEqual(host.getStaticSymbol('', '/src/extern.d.ts', 'nonExisting'));
.toEqual(host.getStaticSymbol('/src/extern.d.ts', 'nonExisting'));
});
});
@ -249,11 +248,11 @@ class MockReflectorHost implements StaticReflectorHost {
provider: 'angular2/src/core/di/provider'
};
}
getStaticSymbol(moduleId: string, declarationFile: string, name: string): StaticSymbol {
getStaticSymbol(declarationFile: string, name: string): StaticSymbol {
var cacheKey = `${declarationFile}:${name}`;
var result = this.staticTypeCache.get(cacheKey);
if (isBlank(result)) {
result = new StaticSymbol(moduleId, declarationFile, name);
result = new StaticSymbol(declarationFile, name);
this.staticTypeCache.set(cacheKey, result);
}
return result;
@ -292,10 +291,9 @@ class MockReflectorHost implements StaticReflectorHost {
}
if (modulePath.indexOf('.') === 0) {
return this.getStaticSymbol(`mod/${symbolName}`, pathTo(containingFile, modulePath) + '.d.ts',
symbolName);
return this.getStaticSymbol(pathTo(containingFile, modulePath) + '.d.ts', symbolName);
}
return this.getStaticSymbol(`mod/${symbolName}`, '/tmp/' + modulePath + '.d.ts', symbolName);
return this.getStaticSymbol('/tmp/' + modulePath + '.d.ts', symbolName);
}
getMetadataFor(moduleId: string): any {

View File

@ -0,0 +1,3 @@
@import './shared.css';
.green { color: green }

View File

@ -1,2 +1,3 @@
<div>{{ctxProp}}</div>
<form><input type="button" [(ngModel)]="ctxProp"/></form>
<my-comp></my-comp>

View File

@ -2,7 +2,13 @@ import {Component, Inject} from '@angular/core';
import {FORM_DIRECTIVES} from '@angular/common';
import {MyComp} from './a/multiple_components';
@Component({selector: 'basic', templateUrl: './basic.html', directives: [MyComp, FORM_DIRECTIVES]})
@Component({
selector: 'basic',
templateUrl: './basic.html',
styles: ['.red { color: red }'],
styleUrls: ['./basic.css'],
directives: [MyComp, FORM_DIRECTIVES]
})
export class Basic {
ctxProp: string;
constructor() { this.ctxProp = 'initiaValue'; }

View File

@ -1,8 +1,8 @@
import {coreBootstrap, ReflectiveInjector} from '@angular/core';
import {browserPlatform, BROWSER_APP_PROVIDERS} from '@angular/platform-browser';
import {browserPlatform, BROWSER_APP_STATIC_PROVIDERS} from '@angular/platform-browser';
import {BasicNgFactory} from './basic.ngfactory';
import {Basic} from './basic';
const appInjector =
ReflectiveInjector.resolveAndCreate(BROWSER_APP_PROVIDERS, browserPlatform().injector);
ReflectiveInjector.resolveAndCreate(BROWSER_APP_STATIC_PROVIDERS, browserPlatform().injector);
coreBootstrap(appInjector, BasicNgFactory);

View File

@ -0,0 +1 @@
.blue { color: blue }

View File

@ -3,7 +3,7 @@
// For TypeScript 1.8, we have to lay out generated files
// in the same source directory with your code.
"genDir": ".",
"legacyPackageLayout": true
"legacyPackageLayout": false
},
"compilerOptions": {