diff --git a/nativescript-angular/file-system/ns-file-system.ts b/nativescript-angular/file-system/ns-file-system.ts index f8cf590d5..222a54c12 100644 --- a/nativescript-angular/file-system/ns-file-system.ts +++ b/nativescript-angular/file-system/ns-file-system.ts @@ -1,12 +1,20 @@ import { Injectable } from "@angular/core"; -import { knownFolders, Folder } from "tns-core-modules/file-system"; +import { knownFolders, Folder, File } from "tns-core-modules/file-system"; // Allows greater flexibility with `file-system` and Angular // Also provides a way for `file-system` to be mocked for testing @Injectable() export class NSFileSystem { - public currentApp(): Folder { - return knownFolders.currentApp(); - } + public currentApp(): Folder { + return knownFolders.currentApp(); + } + + public fileFromPath(path: string): File { + return File.fromPath(path); + } + + public fileExists(path: string): boolean { + return File.exists(path); + } } diff --git a/nativescript-angular/platform.ts b/nativescript-angular/platform.ts index 1c418e794..f1a6ad80b 100644 --- a/nativescript-angular/platform.ts +++ b/nativescript-angular/platform.ts @@ -35,6 +35,7 @@ if ((global).___TS_UNUSED) { import "./dom-adapter"; import { NativeScriptElementSchemaRegistry } from "./schema-registry"; +import { NSFileSystem } from "./file-system/ns-file-system"; import { FileSystemResourceLoader } from "./resource-loader"; export const NS_COMPILER_PROVIDERS = [ @@ -43,6 +44,7 @@ export const NS_COMPILER_PROVIDERS = [ provide: COMPILER_OPTIONS, useValue: { providers: [ + NSFileSystem, { provide: ResourceLoader, useClass: FileSystemResourceLoader }, { provide: ElementSchemaRegistry, useClass: NativeScriptElementSchemaRegistry }, ] diff --git a/nativescript-angular/resource-loader.ts b/nativescript-angular/resource-loader.ts index e3c2ba4db..43a5091bc 100644 --- a/nativescript-angular/resource-loader.ts +++ b/nativescript-angular/resource-loader.ts @@ -1,25 +1,72 @@ -import { path, knownFolders, File } from "tns-core-modules/file-system"; +import { Injectable } from "@angular/core"; import { ResourceLoader } from "@angular/compiler"; +import { path } from "tns-core-modules/file-system"; +import { NSFileSystem } from "./file-system/ns-file-system"; + +const extensionsFallbacks = [ + [".scss", ".css"], + [".sass", ".css"], + [".less", ".css"] +]; + +@Injectable() export class FileSystemResourceLoader extends ResourceLoader { - resolve(url: string, baseUrl: string): string { - // Angular assembles absolute URL's and prefixes them with // + constructor(private fs: NSFileSystem) { + super(); + } + + get(url: string): Promise { + const resolvedPath = this.resolve(url); + + const templateFile = this.fs.fileFromPath(resolvedPath); + + return templateFile.readText(); + } + + resolve(url: string): string { + const normalizedUrl = this.resolveRelativeUrls(url); + + if (this.fs.fileExists(normalizedUrl)) { + return normalizedUrl; + } + + const { candidates: fallbackCandidates, resource: fallbackResource } = + this.fallbackResolve(normalizedUrl); + + if (fallbackResource) { + return fallbackResource; + } + + throw new Error(`Could not resolve ${url}. Looked for: ${normalizedUrl}, ${fallbackCandidates}`); + } + + private resolveRelativeUrls(url: string): string { + // Angular assembles absolute URLs and prefixes them with // if (url.indexOf("/") !== 0) { - // Resolve relative URL's based on the app root. - return path.join(baseUrl, url); + // Resolve relative URLs based on the app root. + return path.join(this.fs.currentApp().path, url); } else { return url; } } - get(url: string): Promise { - const appDir = knownFolders.currentApp().path; - const templatePath = this.resolve(url, appDir); + private fallbackResolve(url: string): + ({ resource: string, candidates: string[] }) { - if (!File.exists(templatePath)) { - throw new Error(`File ${templatePath} does not exist. Resolved from: ${url}.`); - } - let templateFile = File.fromPath(templatePath); - return templateFile.readText(); + const candidates = extensionsFallbacks + .filter(([extension]) => url.endsWith(extension)) + .map(([extension, fallback]) => + this.replaceExtension(url, extension, fallback)); + + const resource = candidates.find(candidate => this.fs.fileExists(candidate)); + + return { candidates, resource }; + } + + private replaceExtension(fileName: string, oldExtension: string, newExtension: string): string { + const baseName = fileName.substr(0, fileName.length - oldExtension.length); + return baseName + newExtension; } } + diff --git a/tests/app/tests/xhr-paths.ts b/tests/app/tests/xhr-paths.ts index 9a4927515..c3de6a6dc 100644 --- a/tests/app/tests/xhr-paths.ts +++ b/tests/app/tests/xhr-paths.ts @@ -1,22 +1,72 @@ // make sure you import mocha-config before @angular/core -import {assert} from "./test-config"; -import {FileSystemResourceLoader} from "nativescript-angular/resource-loader"; +import { assert } from "./test-config"; +import { FileSystemResourceLoader } from "nativescript-angular/resource-loader"; + +import { File } from "tns-core-modules/file-system"; +import { NSFileSystem } from "nativescript-angular/file-system/ns-file-system"; + +class NSFileSystemMock { + public currentApp(): any { + return { path: "/app/dir" }; + } + + public fileFromPath(path: string): File { + return null; + } + + public fileExists(path: string): boolean { + // mycomponent.html always exists + // mycomponent.css is the other file + return path.indexOf("mycomponent.html") >= 0 || path === "/app/dir/mycomponent.css"; + } +} +const fsMock = new NSFileSystemMock(); describe("XHR name resolution", () => { + let resourceLoader: FileSystemResourceLoader; + before(() => { + resourceLoader = new FileSystemResourceLoader(new NSFileSystemMock()); + }); + it("resolves relative paths from app root", () => { - const xhr = new FileSystemResourceLoader(); - assert.strictEqual("/app/dir/mydir/mycomponent.html", xhr.resolve("mydir/mycomponent.html", "/app/dir")); + assert.strictEqual("/app/dir/mydir/mycomponent.html", resourceLoader.resolve("mydir/mycomponent.html")); }); it("resolves double-slashed absolute paths as is", () => { - const xhr = new FileSystemResourceLoader(); - assert.strictEqual("//app/mydir/mycomponent.html", xhr.resolve("//app/mydir/mycomponent.html", "/app/dir")); + assert.strictEqual("//app/mydir/mycomponent.html", resourceLoader.resolve("//app/mydir/mycomponent.html")); }); it("resolves single-slashed absolute paths as is", () => { - const xhr = new FileSystemResourceLoader(); assert.strictEqual( "/data/data/app/mydir/mycomponent.html", - xhr.resolve("/data/data/app/mydir/mycomponent.html", "/app/dir")); + resourceLoader.resolve("/data/data/app/mydir/mycomponent.html")); + }); + + it("resolves existing CSS file", () => { + assert.strictEqual( + "/app/dir/mycomponent.css", + resourceLoader.resolve("mycomponent.css")); + }); + + it("resolves non-existing .scss file to existing .css file", () => { + assert.strictEqual( + "/app/dir/mycomponent.css", + resourceLoader.resolve("mycomponent.scss")); + }); + + it("resolves non-existing .sass file to existing .css file", () => { + assert.strictEqual( + "/app/dir/mycomponent.css", + resourceLoader.resolve("mycomponent.sass")); + }); + + it("resolves non-existing .less file to existing .css file", () => { + assert.strictEqual( + "/app/dir/mycomponent.css", + resourceLoader.resolve("mycomponent.less")); + }); + + it("throws for non-existing file that has no fallbacks", () => { + assert.throws(() => resourceLoader.resolve("does-not-exist.css")); }); });