feat(CSS): import of relative paths (#6023)

* feat(CSS): import of relative paths

* refactor: address comments
This commit is contained in:
Vasil Chimev
2018-07-21 05:54:32 +03:00
committed by GitHub
parent bdf55d9552
commit 6ce1d22d9a
3 changed files with 124 additions and 47 deletions

View File

@@ -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");
}

View File

@@ -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;

View File

@@ -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 && (<string>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<string, Keyframes>;
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";
}