diff --git a/tests/app/ui/styling/style-tests.ts b/tests/app/ui/styling/style-tests.ts index a3519f220..4b095ff22 100644 --- a/tests/app/ui/styling/style-tests.ts +++ b/tests/app/ui/styling/style-tests.ts @@ -1409,50 +1409,72 @@ export function test_CascadingClassNamesAppliesAfterPageLoad() { } export function test_resolveFileNameFromUrl_local_file_tilda() { - const localFileExistsMock = (fileName: string ) => true; - let url = "~/theme/core.css"; - let appDirectory = "app"; - let expected = `${appDirectory}/theme/core.css`; - let result = resolveFileNameFromUrl(url, appDirectory, localFileExistsMock); + const localFileExistsMock = (fileName: string) => true; + const url = "~/theme/core.css"; + const appDirectory = "app"; + const expected = `${appDirectory}/theme/core.css`; + const result = resolveFileNameFromUrl(url, appDirectory, localFileExistsMock); TKUnit.assertEqual(result, expected, "Should resolve local file with leading tilda (~/)"); } export function test_resolveFileNameFromUrl_local_file_no_tilda() { - const localFileExistsMock = (fileName: string ) => true; - let url = "theme/core.css"; - let appDirectory = "app"; - let expected = `${appDirectory}/theme/core.css`; - let result = resolveFileNameFromUrl(url, appDirectory, localFileExistsMock); + const localFileExistsMock = (fileName: string) => true; + const url = "theme/core.css"; + const appDirectory = "app"; + const expected = `${appDirectory}/theme/core.css`; + const result = resolveFileNameFromUrl(url, appDirectory, localFileExistsMock); TKUnit.assertEqual(result, expected, "Should resolve local file without leading tilda (no ~/)"); } export function test_resolveFileNameFromUrl_external_file_tilda() { const externalFileExistsMock = (fileName: string) => (fileName.indexOf("tns_modules") !== -1); - let url = "~/theme/core.css"; - let appDirectory = "app"; - let expected = `${appDirectory}/tns_modules/theme/core.css`; - let result = resolveFileNameFromUrl(url, appDirectory, externalFileExistsMock); + const url = "~/theme/core.css"; + const appDirectory = "app"; + const expected = `${appDirectory}/tns_modules/theme/core.css`; + const result = resolveFileNameFromUrl(url, appDirectory, externalFileExistsMock); TKUnit.assertEqual(result, expected, "Should resolve file from tns_modules with leading tilda (~/)"); } export function test_resolveFileNameFromUrl_external_file_no_tilda() { const externalFileExistsMock = (fileName: string) => (fileName.indexOf("tns_modules") !== -1); - let url = "theme/core.css"; - let appDirectory = "app"; - let expected = `${appDirectory}/tns_modules/theme/core.css`; - let result = resolveFileNameFromUrl(url, appDirectory, externalFileExistsMock); + const url = "theme/core.css"; + const appDirectory = "app"; + const expected = `${appDirectory}/tns_modules/theme/core.css`; + const result = resolveFileNameFromUrl(url, appDirectory, externalFileExistsMock); TKUnit.assertEqual(result, expected, "Should resolve file from tns_modules without leading tilda (no ~/)"); } +export function test_resolveFileNameFromUrl_import_relative_file_curent_dir() { + const importedFileExistsMock = (fileName: string) => (fileName.indexOf("views") !== -1); + const importSource = "app/views/main-page.css"; + const url = "./common.css"; + const appDirectory = "app"; + const expected = `${appDirectory}/views/common.css`; + const result = resolveFileNameFromUrl(url, appDirectory, importedFileExistsMock, importSource); + + TKUnit.assertEqual(result, expected, "Should resolve relative file to current directory"); +} + +export function test_resolveFileNameFromUrl_import_relative_file_parent_dir() { + const importedFileExistsMock = (fileName: string) => (fileName.indexOf("views") !== -1); + const importSource = "app/views/shared/main-page.css"; + const url = "../common.css"; + const appDirectory = "app"; + const expected = `${appDirectory}/views/common.css`; + const result = resolveFileNameFromUrl(url, appDirectory, importedFileExistsMock, importSource); + + TKUnit.assertEqual(result, expected, "Should resolve relative file to parent directory"); +} + export function test_resolveFileNameFromUrl_unexisting_file() { const fileDoesNotExistMock = (fileName: string) => false; - let url = "~/theme/core.css"; - let appDirectory = "app"; - let result = resolveFileNameFromUrl(url, appDirectory, fileDoesNotExistMock); + const url = "~/theme/core.css"; + const appDirectory = "app"; + const result = resolveFileNameFromUrl(url, appDirectory, fileDoesNotExistMock); TKUnit.assertNull(result, "Shouldn't resolve unexisting file"); } diff --git a/tns-core-modules/ui/styling/style-scope.d.ts b/tns-core-modules/ui/styling/style-scope.d.ts index 4e2167754..b404ce9a7 100644 --- a/tns-core-modules/ui/styling/style-scope.d.ts +++ b/tns-core-modules/ui/styling/style-scope.d.ts @@ -36,5 +36,5 @@ export class StyleScope { public getAnimations(ruleset: RuleSet): KeyframeAnimationInfo[]; } -export function resolveFileNameFromUrl(url: string, appDirectory: string, fileExists: (string) => boolean): string; +export function resolveFileNameFromUrl(url: string, appDirectory: string, fileExists: (string) => boolean, importSource?: string): string; export function applyInlineStyle(view: ViewBase, style: string): void; diff --git a/tns-core-modules/ui/styling/style-scope.ts b/tns-core-modules/ui/styling/style-scope.ts index 0d9b05ee1..17a7b0ff7 100644 --- a/tns-core-modules/ui/styling/style-scope.ts +++ b/tns-core-modules/ui/styling/style-scope.ts @@ -79,14 +79,8 @@ class CSSSource { } public static fromURI(uri: string, keyframes: KeyframesMap): CSSSource { - // webpack modules require all file paths to be relative to /app folder. - let appRelativeUri = uri; - if (appRelativeUri.startsWith("/")) { - var app = knownFolders.currentApp().path + "/"; - if (appRelativeUri.startsWith(app)) { - appRelativeUri = "./" + appRelativeUri.substr(app.length); - } - } + // webpack modules require all file paths to be relative to /app folder + let appRelativeUri = CSSSource.pathRelativeToApp(uri); try { const cssOrAst = global.loadModule(appRelativeUri); @@ -109,6 +103,21 @@ class CSSSource { return CSSSource.fromFile(appRelativeUri, keyframes); } + private static pathRelativeToApp(uri: string): string { + if (!uri.startsWith("/")) { + return uri; + } + + const appPath = knownFolders.currentApp().path; + if (!uri.startsWith(appPath)) { + traceWrite(`${uri} does not start with ${appPath}`, traceCategories.Error, traceMessageType.error); + return uri; + } + + const relativeUri = `.${uri.substr(appPath.length)}`; + return relativeUri; + } + public static fromFile(url: string, keyframes: KeyframesMap): CSSSource { // .scss, .sass, etc. css files in vanilla app are usually compiled to .css so we will try to load a compiled file first. let cssFileUrl = url.replace(/\..\w+$/, ".css"); @@ -123,10 +132,15 @@ class CSSSource { return new CSSSource(undefined, url, file, keyframes, undefined); } + public static fromFileImport(url: string, keyframes: KeyframesMap, importSource: string): CSSSource { + const file = CSSSource.resolveCSSPathFromURL(url, importSource); + return new CSSSource(undefined, url, file, keyframes, undefined); + } + @profile - public static resolveCSSPathFromURL(url: string): string { + public static resolveCSSPathFromURL(url: string, importSource?: string): string { const app = knownFolders.currentApp().path; - const file = resolveFileNameFromUrl(url, app, File.exists); + const file = resolveFileNameFromUrl(url, app, File.exists, importSource); return file; } @@ -197,21 +211,33 @@ class CSSSource { } private createSelectorsFromImports(): RuleSet[] { - let selectors: RuleSet[] = []; const imports = this._ast["stylesheet"]["rules"].filter(r => r.type === "import"); - for (let i = 0; i < imports.length; i++) { - const importItem = imports[i]["import"]; - const match = importItem && (importItem).match(pattern); - const url = match && match[2]; + const urlFromImportObject = importObject => { + const importItem = importObject["import"] as string; + const urlMatch = importItem && importItem.match(pattern); + return urlMatch && urlMatch[2]; + }; - if (url !== null && url !== undefined) { - const cssFile = CSSSource.fromURI(url, this._keyframes); - selectors = selectors.concat(cssFile.selectors); - } - } + const sourceFromImportObject = importObject => + importObject["position"] && importObject["position"]["source"]; - return selectors; + const toUrlSourcePair = importObject => ({ + url: urlFromImportObject(importObject), + source: sourceFromImportObject(importObject), + }); + + const getCssFile = ({ url, source }) => source ? + CSSSource.fromFileImport(url, this._keyframes, source) : + CSSSource.fromURI(url, this._keyframes); + + const cssFiles = imports + .map(toUrlSourcePair) + .filter(({ url }) => !!url) + .map(getCssFile); + + const selectors = cssFiles.map(file => (file && file.selectors) || []); + return selectors.reduce((acc, val) => acc.concat(val), []); } private createSelectorsFromSyntaxTree(): RuleSet[] { @@ -521,13 +547,13 @@ export class StyleScope { let parsedCssSelectors = cssString ? CSSSource.fromSource(cssString, this._keyframes, cssFileName) : CSSSource.fromURI(cssFileName, this._keyframes); this._css = this._css + parsedCssSelectors.source; - this._localCssSelectors.push.apply(this._localCssSelectors, parsedCssSelectors.selectors); + this._localCssSelectors.push.apply(this._localCssSelectors, parsedCssSelectors.selectors); this._localCssSelectorVersion++; this.ensureSelectors(); } public getKeyframeAnimationWithName(animationName: string): kam.KeyframeAnimationInfo { - const cssKeyframes = this._keyframes[animationName]; + const cssKeyframes = this._keyframes[animationName]; if (!cssKeyframes) { return; } @@ -611,9 +637,9 @@ export class StyleScope { type KeyframesMap = Map; -export function resolveFileNameFromUrl(url: string, appDirectory: string, fileExists: (name: string) => boolean): string { - let fileName: string = typeof url === "string" ? url.trim() : ""; +export function resolveFileNameFromUrl(url: string, appDirectory: string, fileExists: (name: string) => boolean, importSource?: string): string { + let fileName: string = typeof url === "string" ? url.trim() : ""; if (fileName.indexOf("~/") === 0) { fileName = fileName.replace("~/", ""); } @@ -628,6 +654,14 @@ export function resolveFileNameFromUrl(url: string, appDirectory: string, fileEx if (fileName[0] === "~" && fileName[1] !== "/" && fileName[1] !== "\"") { fileName = fileName.substr(1); } + + if (importSource) { + const importFile = resolveFilePathFromImport(importSource, fileName); + if (fileExists(importFile)) { + return importFile; + } + } + const external = path.join(appDirectory, "tns_modules", fileName); if (fileExists(external)) { return external; @@ -637,6 +671,19 @@ export function resolveFileNameFromUrl(url: string, appDirectory: string, fileEx return null; } +function resolveFilePathFromImport(importSource: string, fileName: string): string { + const importSourceParts = importSource.split(path.separator); + const fileNameParts = fileName.split(path.separator) + // exclude the dot-segment for current directory + .filter(p => !isCurrentDirectory(p)); + + // remove current file name + importSourceParts.pop(); + // remove element in case of dot-segment for parent directory or add file name + fileNameParts.forEach(p => isParentDirectory(p) ? importSourceParts.pop() : importSourceParts.push(p)); + return importSourceParts.join(path.separator); +} + export const applyInlineStyle = profile(function applyInlineStyle(view: ViewBase, styleStr: string) { let localStyle = `local { ${styleStr} }`; let inlineRuleSet = CSSSource.fromSource(localStyle, new Map()).selectors; @@ -657,6 +704,14 @@ export const applyInlineStyle = profile(function applyInlineStyle(view: ViewBase }); }); +function isCurrentDirectory(uriPart: string): boolean { + return uriPart === "."; +} + +function isParentDirectory(uriPart: string): boolean { + return uriPart === ".."; +} + function isKeyframe(node: CssNode): node is KeyframesDefinition { return node.type === "keyframes"; }