Merge pull request #6665 from NativeScript/vchimev/app-css-hmr

feat(HMR): apply changes in application styles at runtime
This commit is contained in:
Svetoslav
2018-12-14 16:39:42 +02:00
committed by GitHub
15 changed files with 227 additions and 26 deletions

View File

@@ -0,0 +1,3 @@
Button, Label {
color: green;
}

View File

@@ -0,0 +1,3 @@
Button, Label {
color: green;
}

View File

@@ -0,0 +1,3 @@
Button, Label {
color: black;
}

View File

@@ -2,6 +2,8 @@
import * as trace from "tns-core-modules/trace"; import * as trace from "tns-core-modules/trace";
import * as tests from "../testRunner"; import * as tests from "../testRunner";
let executeTests = true;
trace.enable(); trace.enable();
trace.addCategories(trace.categories.Test + "," + trace.categories.Error); trace.addCategories(trace.categories.Test + "," + trace.categories.Error);
@@ -21,6 +23,8 @@ function runTests() {
export function onNavigatedTo(args) { export function onNavigatedTo(args) {
args.object.off(Page.loadedEvent, onNavigatedTo); args.object.off(Page.loadedEvent, onNavigatedTo);
if (executeTests) {
executeTests = false;
runTests(); runTests();
} }
}

View File

@@ -1,3 +1,3 @@
<Page navigatedTo="onNavigatedTo"> <Page navigatedTo="onNavigatedTo">
<Label text="Running non-UI tests..." /> <Label id="label" text="Running non-UI tests..." />
</Page> </Page>

View File

@@ -0,0 +1,113 @@
import * as app from "tns-core-modules/application/application";
import * as frame from "tns-core-modules/ui/frame";
import * as helper from "../ui/helper";
import * as TKUnit from "../TKUnit";
import { Color } from "tns-core-modules/color";
import { parse } from "tns-core-modules/ui/builder";
import { Page } from "tns-core-modules/ui/page";
const appCssFileName = "./app/application.css";
const appNewCssFileName = "./app/app-new.css";
const appNewScssFileName = "./app/app-new.scss";
const appJsFileName = "./app/app.js";
const appTsFileName = "./app/app.ts";
const mainPageCssFileName = "./app/main-page.css";
const mainPageHtmlFileName = "./app/main-page.html";
const mainPageXmlFileName = "./app/main-page.xml";
const green = new Color("green");
const mainPageTemplate = `
<Page>
<StackLayout>
<Label id="label" text="label"></Label>
</StackLayout>
</Page>`;
const pageTemplate = `
<Page>
<StackLayout>
<Button id="button" text="button"></Button>
</StackLayout>
</Page>`;
export function test_onLiveSync_HmrContext_AppStyle_AppNewCss() {
_test_onLiveSync_HmrContext_AppStyle(appNewCssFileName);
}
export function test_onLiveSync_HmrContext_AppStyle_AppNewScss() {
_test_onLiveSync_HmrContext_AppStyle(appNewScssFileName);
}
export function test_onLiveSync_HmrContext_ContextUndefined() {
_test_onLiveSync_HmrContext({ type: undefined, module: undefined });
}
export function test_onLiveSync_HmrContext_ModuleUndefined() {
_test_onLiveSync_HmrContext({ type: "script", module: undefined });
}
export function test_onLiveSync_HmrContext_Script_AppJs() {
_test_onLiveSync_HmrContext({ type: "script", module: appJsFileName });
}
export function test_onLiveSync_HmrContext_Script_AppTs() {
_test_onLiveSync_HmrContext({ type: "script", module: appTsFileName });
}
export function test_onLiveSync_HmrContext_Style_MainPageCss() {
_test_onLiveSync_HmrContext({ type: "style", module: mainPageCssFileName });
}
export function test_onLiveSync_HmrContext_Markup_MainPageHtml() {
_test_onLiveSync_HmrContext({ type: "markup", module: mainPageHtmlFileName });
}
export function test_onLiveSync_HmrContext_Markup_MainPageXml() {
_test_onLiveSync_HmrContext({ type: "markup", module: mainPageXmlFileName });
}
export function setUpModule() {
const mainPage = <Page>parse(mainPageTemplate);
helper.navigate(() => mainPage);
}
export function tearDown() {
app.setCssFileName(appCssFileName);
}
function _test_onLiveSync_HmrContext_AppStyle(styleFileName: string) {
const pageBeforeNavigation = helper.getCurrentPage();
const page = <Page>parse(pageTemplate);
helper.navigateWithHistory(() => page);
app.setCssFileName(styleFileName);
const pageBeforeLiveSync = helper.getCurrentPage();
global.__onLiveSync({ type: "style", module: styleFileName });
const pageAfterLiveSync = helper.getCurrentPage();
TKUnit.waitUntilReady(() => pageAfterLiveSync.getViewById("button").style.color.toString() === green.toString());
TKUnit.assertTrue(pageAfterLiveSync.frame.canGoBack(), "App styles NOT applied - livesync navigation executed!");
TKUnit.assertEqual(pageAfterLiveSync, pageBeforeLiveSync, "Pages are different - livesync navigation executed!");
TKUnit.assertTrue(pageAfterLiveSync._cssState.isSelectorsLatestVersionApplied(), "Latest selectors version NOT applied!");
helper.goBack();
const pageAfterNavigationBack = helper.getCurrentPage();
TKUnit.assertEqual(pageAfterNavigationBack.getViewById("label").style.color, green, "App styles NOT applied on back navigation!");
TKUnit.assertEqual(pageBeforeNavigation, pageAfterNavigationBack, "Pages are different - livesync navigation executed!");
TKUnit.assertTrue(pageAfterNavigationBack._cssState.isSelectorsLatestVersionApplied(), "Latest selectors version is NOT applied!");
}
function _test_onLiveSync_HmrContext(context: { type, module }) {
const page = <Page>parse(pageTemplate);
helper.navigateWithHistory(() => page);
global.__onLiveSync({ type: context.type, module: context.module });
TKUnit.waitUntilReady(() => !!frame.topmost());
const topmostFrame = frame.topmost();
TKUnit.waitUntilReady(() => topmostFrame.currentPage && topmostFrame.currentPage.isLoaded && !topmostFrame.canGoBack());
TKUnit.assertTrue(topmostFrame.currentPage.getViewById("label").isLoaded);
}

View File

@@ -153,9 +153,6 @@ allTests["STYLE-PROPERTIES"] = stylePropertiesTests;
import * as frameTests from "./ui/frame/frame-tests"; import * as frameTests from "./ui/frame/frame-tests";
allTests["FRAME"] = frameTests; allTests["FRAME"] = frameTests;
import * as tabViewRootTests from "./ui/tab-view/tab-view-root-tests";
allTests["TAB-VIEW-ROOT"] = tabViewRootTests;
import * as viewTests from "./ui/view/view-tests"; import * as viewTests from "./ui/view/view-tests";
allTests["VIEW"] = viewTests; allTests["VIEW"] = viewTests;
@@ -255,6 +252,12 @@ allTests["SEARCH-BAR"] = searchBarTests;
import * as navigationTests from "./navigation/navigation-tests"; import * as navigationTests from "./navigation/navigation-tests";
allTests["NAVIGATION"] = navigationTests; allTests["NAVIGATION"] = navigationTests;
import * as livesyncTests from "./livesync/livesync-tests";
allTests["LIVESYNC"] = livesyncTests;
import * as tabViewRootTests from "./ui/tab-view/tab-view-root-tests";
allTests["TAB-VIEW-ROOT"] = tabViewRootTests;
import * as resetRootViewTests from "./ui/root-view/reset-root-view-tests"; import * as resetRootViewTests from "./ui/root-view/reset-root-view-tests";
allTests["RESET-ROOT-VIEW"] = resetRootViewTests; allTests["RESET-ROOT-VIEW"] = resetRootViewTests;

View File

@@ -32,7 +32,14 @@ export function hasLaunched(): boolean {
export { Observable }; export { Observable };
import { UnhandledErrorEventData, iOSApplication, AndroidApplication, CssChangedEventData, LoadAppCSSEventData } from "."; import {
AndroidApplication,
CssChangedEventData,
getRootView,
iOSApplication,
LoadAppCSSEventData,
UnhandledErrorEventData
} from "./application";
export { UnhandledErrorEventData, CssChangedEventData, LoadAppCSSEventData }; export { UnhandledErrorEventData, CssChangedEventData, LoadAppCSSEventData };
@@ -70,10 +77,21 @@ export function setApplication(instance: iOSApplication | AndroidApplication): v
app = instance; app = instance;
} }
export function livesync() { export function livesync(context?: HmrContext) {
events.notify(<EventData>{ eventName: "livesync", object: app }); events.notify(<EventData>{ eventName: "livesync", object: app });
const liveSyncCore = global.__onLiveSyncCore; const liveSyncCore = global.__onLiveSyncCore;
if (liveSyncCore) { let reapplyAppCss = false
if (context) {
const fullFileName = getCssFileName();
const fileName = fullFileName.substring(0, fullFileName.lastIndexOf(".") + 1);
const extensions = ["css", "scss"];
reapplyAppCss = extensions.some(ext => context.module === fileName.concat(ext));
}
if (reapplyAppCss) {
getRootView()._onCssStateChange();
} else if (liveSyncCore) {
liveSyncCore(); liveSyncCore();
} }
} }

View File

@@ -212,12 +212,12 @@ export function getNativeApplication(): android.app.Application {
return nativeApp; return nativeApp;
} }
global.__onLiveSync = function () { global.__onLiveSync = function __onLiveSync(context?: HmrContext) {
if (androidApp && androidApp.paused) { if (androidApp && androidApp.paused) {
return; return;
} }
livesync(); livesync(context);
}; };
function initLifecycleCallbacks() { function initLifecycleCallbacks() {

View File

@@ -373,10 +373,10 @@ function setViewControllerView(view: View): void {
} }
} }
global.__onLiveSync = function () { global.__onLiveSync = function __onLiveSync(context?: HmrContext) {
if (!started) { if (!started) {
return; return;
} }
livesync(); livesync(context);
} }

View File

@@ -51,7 +51,7 @@ declare namespace NodeJS {
__native?: any; __native?: any;
__inspector?: any; __inspector?: any;
__extends: any; __extends: any;
__onLiveSync: () => void; __onLiveSync: (context?: { type: string, module: string }) => void;
__onLiveSyncCore: () => void; __onLiveSyncCore: () => void;
__onUncaughtError: (error: NativeScriptError) => void; __onUncaughtError: (error: NativeScriptError) => void;
TNS_WEBPACK?: boolean; TNS_WEBPACK?: boolean;
@@ -64,6 +64,27 @@ declare function clearTimeout(timeoutId: number): void;
declare function setInterval(callback: (...args: any[]) => void, ms: number, ...args: any[]): number; declare function setInterval(callback: (...args: any[]) => void, ms: number, ...args: any[]): number;
declare function clearInterval(intervalId: number): void; declare function clearInterval(intervalId: number): void;
declare enum HmrType {
markup = "markup",
script = "script",
style = "style"
}
/**
* Define a context for Hot Module Replacement.
*/
interface HmrContext {
/**
* The type of module for replacement.
*/
type: HmrType;
/**
* The module for replacement.
*/
module: string;
}
/** /**
* An extended JavaScript Error which will have the nativeError property initialized in case the error is caused by executing platform-specific code. * An extended JavaScript Error which will have the nativeError property initialized in case the error is caused by executing platform-specific code.
*/ */

View File

@@ -103,6 +103,13 @@ export class PageBase extends ContentView implements PageDefinition {
public onNavigatingTo(context: any, isBackNavigation: boolean, bindingContext?: any) { public onNavigatingTo(context: any, isBackNavigation: boolean, bindingContext?: any) {
this._navigationContext = context; this._navigationContext = context;
if (isBackNavigation && this._styleScope) {
this._styleScope.ensureSelectors();
if (!this._cssState.isSelectorsLatestVersionApplied()) {
this._onCssStateChange();
}
}
//https://github.com/NativeScript/NativeScript/issues/731 //https://github.com/NativeScript/NativeScript/issues/731
if (!isBackNavigation && bindingContext !== undefined && bindingContext !== null) { if (!isBackNavigation && bindingContext !== undefined && bindingContext !== null) {
this.bindingContext = bindingContext; this.bindingContext = bindingContext;

View File

@@ -19,6 +19,11 @@ export class CssState {
* Gets the static selectors that match the view and the dynamic selectors that may potentially match the view. * Gets the static selectors that match the view and the dynamic selectors that may potentially match the view.
*/ */
public changeMap: ChangeMap<ViewBase>; public changeMap: ChangeMap<ViewBase>;
/**
* Checks whether style scope and CSS state selectors are in sync.
*/
public isSelectorsLatestVersionApplied(): boolean
} }
export class StyleScope { export class StyleScope {
@@ -29,6 +34,9 @@ export class StyleScope {
public static createSelectorsFromImports(tree: SyntaxTree, keyframes: Object): RuleSet[]; public static createSelectorsFromImports(tree: SyntaxTree, keyframes: Object): RuleSet[];
public ensureSelectors(): number; public ensureSelectors(): number;
public isApplicationCssSelectorsLatestVersionApplied(): boolean;
public isLocalCssSelectorsLatestVersionApplied(): boolean;
public applySelectors(view: ViewBase): void public applySelectors(view: ViewBase): void
public query(options: Node): SelectorCore[]; public query(options: Node): SelectorCore[];

View File

@@ -343,6 +343,7 @@ export class CssState {
_appliedChangeMap: Readonly<ChangeMap<ViewBase>>; _appliedChangeMap: Readonly<ChangeMap<ViewBase>>;
_appliedPropertyValues: Readonly<{}>; _appliedPropertyValues: Readonly<{}>;
_appliedAnimations: ReadonlyArray<kam.KeyframeAnimation>; _appliedAnimations: ReadonlyArray<kam.KeyframeAnimation>;
_appliedSelectorsVersion: number;
_match: SelectorsMatch<ViewBase>; _match: SelectorsMatch<ViewBase>;
_matchInvalid: boolean; _matchInvalid: boolean;
@@ -367,6 +368,10 @@ export class CssState {
} }
} }
public isSelectorsLatestVersionApplied(): boolean {
return this.view._styleScope._getSelectorsVersion() === this._appliedSelectorsVersion;
}
public onLoaded(): void { public onLoaded(): void {
if (this._matchInvalid) { if (this._matchInvalid) {
this.updateMatch(); this.updateMatch();
@@ -381,7 +386,12 @@ export class CssState {
@profile @profile
private updateMatch() { private updateMatch() {
this._match = this.view._styleScope ? this.view._styleScope.matchSelectors(this.view) : CssState.emptyMatch; if (this.view._styleScope) {
this._appliedSelectorsVersion = this.view._styleScope._getSelectorsVersion();
this._match = this.view._styleScope.matchSelectors(this.view);
} else {
this._match = CssState.emptyMatch;
}
this._matchInvalid = false; this._matchInvalid = false;
} }
@@ -597,8 +607,8 @@ export class StyleScope {
} }
public ensureSelectors(): number { public ensureSelectors(): number {
if (this._applicationCssSelectorsAppliedVersion !== applicationCssSelectorVersion || if (!this.isApplicationCssSelectorsLatestVersionApplied() ||
this._localCssSelectorVersion !== this._localCssSelectorsAppliedVersion || !this.isLocalCssSelectorsLatestVersionApplied() ||
!this._mergedCssSelectors) { !this._mergedCssSelectors) {
this._createSelectors(); this._createSelectors();
@@ -607,6 +617,14 @@ export class StyleScope {
return this._getSelectorsVersion(); return this._getSelectorsVersion();
} }
public isApplicationCssSelectorsLatestVersionApplied(): boolean {
return this._applicationCssSelectorsAppliedVersion === applicationCssSelectorVersion;
}
public isLocalCssSelectorsLatestVersionApplied(): boolean {
return this._localCssSelectorsAppliedVersion === this._localCssSelectorVersion;
}
@profile @profile
private _createSelectors() { private _createSelectors() {
let toMerge: RuleSet[][] = []; let toMerge: RuleSet[][] = [];