fix(compiler): support css stylesheets in offline compiler
This commit is contained in:
@ -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);
|
||||
|
7
tools/compiler_cli/src/compiler_private.ts
Normal file
7
tools/compiler_cli/src/compiler_private.ts
Normal 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;
|
@ -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;
|
||||
|
@ -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);
|
||||
|
@ -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 {
|
||||
|
3
tools/compiler_cli/test/src/basic.css
Normal file
3
tools/compiler_cli/test/src/basic.css
Normal file
@ -0,0 +1,3 @@
|
||||
@import './shared.css';
|
||||
|
||||
.green { color: green }
|
@ -1,2 +1,3 @@
|
||||
<div>{{ctxProp}}</div>
|
||||
<form><input type="button" [(ngModel)]="ctxProp"/></form>
|
||||
<my-comp></my-comp>
|
@ -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'; }
|
||||
|
@ -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);
|
||||
|
1
tools/compiler_cli/test/src/shared.css
Normal file
1
tools/compiler_cli/test/src/shared.css
Normal file
@ -0,0 +1 @@
|
||||
.blue { color: blue }
|
@ -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": {
|
||||
|
Reference in New Issue
Block a user