Skip to content

Commit 6544fd0

Browse files
heywhyrigor789
andauthored
feat: Vue Devtools support (nativescript-vue#1060)
Co-authored-by: Igor Randjelovic <rigor789@gmail.com>
1 parent 7b9efc4 commit 6544fd0

File tree

16 files changed

+1680
-668
lines changed

16 files changed

+1680
-668
lines changed

demo/App_Resources/Android/src/main/AndroidManifest.xml

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,20 @@
66
android:smallScreens="true"
77
android:normalScreens="true"
88
android:largeScreens="true"
9-
android:xlargeScreens="true"/>
9+
android:xlargeScreens="true" />
1010

11-
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
12-
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
13-
<uses-permission android:name="android.permission.INTERNET"/>
14-
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
11+
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
12+
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
13+
<uses-permission android:name="android.permission.INTERNET" />
14+
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
1515

1616
<application
1717
android:name="com.tns.NativeScriptApplication"
1818
android:allowBackup="true"
1919
android:icon="@mipmap/ic_launcher"
2020
android:label="@string/app_name"
2121
android:theme="@style/AppTheme"
22+
android:usesCleartextTraffic="true"
2223
android:hardwareAccelerated="true">
2324

2425
<activity
@@ -27,7 +28,7 @@
2728
android:configChanges="keyboard|keyboardHidden|orientation|screenSize|smallestScreenSize|screenLayout|locale|uiMode"
2829
android:theme="@style/LaunchScreenTheme"
2930
android:hardwareAccelerated="true"
30-
android:launchMode="singleTask"
31+
android:launchMode="singleTask"
3132
android:exported="true">
3233

3334
<meta-data android:name="SET_THEME_ON_LAUNCH" android:resource="@style/AppTheme" />
@@ -37,6 +38,6 @@
3738
<category android:name="android.intent.category.LAUNCHER" />
3839
</intent-filter>
3940
</activity>
40-
<activity android:name="com.tns.ErrorReportActivity"/>
41+
<activity android:name="com.tns.ErrorReportActivity" />
4142
</application>
4243
</manifest>

demo/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,6 @@
1414
"@nativescript/ios": "~8.5.2",
1515
"@nativescript/types": "~8.5.0",
1616
"@nativescript/webpack": "~5.0.14",
17-
"typescript": "~5.1.3"
17+
"typescript": "^5.2.2"
1818
}
1919
}

demo/tsconfig.json

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,6 @@
1414
"@/*": ["src/*"],
1515
"nativescript-vue": ["../src/index.ts"]
1616
},
17-
"typeRoots": ["types"],
18-
"types": ["node"],
1917
"allowSyntheticDefaultImports": true,
2018
"esModuleInterop": true,
2119
"experimentalDecorators": true,

devtools.js

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
if (__DEV__) {
2+
try {
3+
const _global = globalThis.global;
4+
5+
const host = (_global.__VUE_DEVTOOLS_HOST__ ??= __NS_VUE_DEVTOOLS_HOST__);
6+
const port = (_global.__VUE_DEVTOOLS_PORT__ ??= __NS_VUE_DEVTOOLS_PORT__);
7+
_global.__VUE_DEVTOOLS_TOAST__ ??= (message) => {
8+
console.warn('[VueDevtools]', message);
9+
};
10+
11+
const platform = global.isAndroid ? 'Android' : 'iOS';
12+
13+
const documentShim = {
14+
// this shows as the title in VueDevtools
15+
title: `${platform} :: ${host}:${port} :: NativeScript`,
16+
querySelector: () => null,
17+
querySelectorAll: () => [],
18+
};
19+
20+
_global.document = Object.assign({}, documentShim, _global.document);
21+
_global.addEventListener ??= () => {};
22+
_global.removeEventListener ??= () => {};
23+
_global.window ??= _global;
24+
25+
console.warn(
26+
`[VueDevtools] Connecting to ${global.__VUE_DEVTOOLS_HOST__}:${global.__VUE_DEVTOOLS_PORT__}...`
27+
);
28+
require('@vue/devtools/build/hook.js');
29+
require('@vue/devtools/build/backend.js');
30+
} catch (e) {
31+
console.warn('[VueDevtools] Failed to init:', e);
32+
}
33+
}

nativescript.webpack.js

Lines changed: 98 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,108 @@
11
const { VueLoaderPlugin } = require('vue-loader');
2+
const spawn = require('cross-spawn');
3+
4+
function findFreePort(startingPort = 8098) {
5+
let found = false;
6+
let port = startingPort;
7+
8+
const isPortFree = (port) =>
9+
new Promise((resolve) => {
10+
const server = require('http')
11+
.createServer()
12+
.listen(port, '0.0.0.0', () => {
13+
server.close();
14+
resolve(true);
15+
})
16+
.on('error', () => {
17+
resolve(false);
18+
});
19+
});
20+
21+
const findFreePort = () => {
22+
isPortFree(port).then((isFree) => {
23+
if (!isFree) {
24+
port++;
25+
return findFreePort();
26+
}
27+
found = true;
28+
});
29+
};
30+
31+
findFreePort();
32+
33+
while (!found) {
34+
process._tickCallback();
35+
const start = Date.now();
36+
while (Date.now() - start < 100) {
37+
// busy wait... not ideal, but we need to find a port synchronously...
38+
}
39+
}
40+
41+
return port;
42+
}
43+
44+
function startVueDevtools(port, isAndroid = false) {
45+
console.log(`[VueDevtools] Starting standalone Vue Devtools on port ${port}`);
46+
if (isAndroid) {
47+
console.log(
48+
`[VueDevtools] If the app doesn't automatically connect, check if http traffic is allowed. (e.g. on Android, you may need to set android:usesCleartextTraffic="true" in AndroidManifest.xml)`
49+
);
50+
}
51+
spawn(require.resolve('@vue/devtools/bin.js'), [], {
52+
stdio: 'ignore',
53+
env: {
54+
...process.env,
55+
PORT: port,
56+
},
57+
});
58+
}
259

360
/**
461
* @param {typeof import("@nativescript/webpack")} webpack
562
*/
663
module.exports = (webpack) => {
764
webpack.useConfig('vue');
865

9-
webpack.chainWebpack((config) => {
66+
webpack.chainWebpack((config, env) => {
67+
const additionalDefines = {
68+
__VUE_PROD_DEVTOOLS__: false,
69+
};
70+
71+
// todo: support configuring the devtools host/port from the nativescript.config.ts...
72+
if (!!env.vueDevtools) {
73+
// find a free port for the devtools
74+
const vueDevtoolsPort = findFreePort(8098);
75+
const isAndroid = webpack.Utils.platform.getPlatformName() === 'android';
76+
77+
// on android simulators, localhost is not the host machine...
78+
const vueDevtoolsHost = isAndroid
79+
? 'http://10.0.2.2'
80+
: 'http://localhost';
81+
82+
additionalDefines['__VUE_PROD_DEVTOOLS__'] = true;
83+
additionalDefines['__NS_VUE_DEVTOOLS_HOST__'] =
84+
JSON.stringify(vueDevtoolsHost);
85+
additionalDefines['__NS_VUE_DEVTOOLS_PORT__'] = vueDevtoolsPort;
86+
87+
const devtoolsEntryPath = require.resolve('./devtools.js');
88+
const entryPath = webpack.Utils.platform.getEntryPath();
89+
const paths = config.entry('bundle').values();
90+
const entryIndex = paths.indexOf(entryPath);
91+
92+
if (entryIndex === -1) {
93+
// if the app entry is not found, add the devtools entry at the beginning - generally should not happen, but just in case.
94+
paths.unshift(entryPath);
95+
} else {
96+
// insert devtools entry before the app entry, but after globals etc.
97+
paths.splice(entryIndex, 0, devtoolsEntryPath);
98+
}
99+
100+
config.entry('bundle').clear().merge(paths);
101+
102+
// start the devtools...
103+
startVueDevtools(vueDevtoolsPort, isAndroid);
104+
}
105+
10106
// resolve any imports from "vue" to "nativescript-vue"
11107
config.resolve.alias.set('vue', 'nativescript-vue');
12108

@@ -41,7 +137,7 @@ module.exports = (webpack) => {
41137
config.plugin('DefinePlugin').tap((args) => {
42138
Object.assign(args[0], {
43139
__VUE_OPTIONS_API__: true,
44-
__VUE_PROD_DEVTOOLS__: false,
140+
...additionalDefines,
45141
});
46142

47143
return args;

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
"main": "dist/index.js",
55
"files": [
66
"dist/",
7+
"devtools.js",
78
"nativescript.webpack.js"
89
],
910
"license": "MIT",
@@ -16,8 +17,10 @@
1617
},
1718
"dependencies": {
1819
"@vue/compiler-sfc": "^3.3.4",
20+
"@vue/devtools": "^6.5.0",
1921
"@vue/runtime-core": "^3.3.4",
2022
"@vue/shared": "^3.3.4",
23+
"cross-spawn": "^7.0.3",
2124
"set-value": "^4.1.0",
2225
"vue-loader": "^17.2.2"
2326
},

packages/stackblitz-template/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
"@nativescript/webpack": "~5.0.0",
1515
"@types/node": "~17.0.21",
1616
"tailwindcss": "^3.1.8",
17-
"typescript": "~4.9.5"
17+
"typescript": "^5.2.2"
1818
},
1919
"stackblitz": {
2020
"installDependencies": true,

packages/stackblitz-template/tsconfig.json

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,6 @@
1313
"~/*": ["src/*"],
1414
"@/*": ["src/*"]
1515
},
16-
"typeRoots": ["types"],
17-
"types": ["node"],
1816
"allowSyntheticDefaultImports": true,
1917
"esModuleInterop": true,
2018
"experimentalDecorators": true,

packages/template-blank/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,6 @@
1212
"@nativescript/webpack": "~5.0.0",
1313
"@types/node": "~17.0.21",
1414
"tailwindcss": "^3.1.8",
15-
"typescript": "~4.9.5"
15+
"typescript": "^5.2.2"
1616
}
1717
}

packages/template-blank/tsconfig.json

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,6 @@
1313
"~/*": ["src/*"],
1414
"@/*": ["src/*"]
1515
},
16-
"typeRoots": ["types"],
17-
"types": ["node"],
1816
"allowSyntheticDefaultImports": true,
1917
"esModuleInterop": true,
2018
"experimentalDecorators": true,

src/components/ActionBar.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ registerElement('NSCActionBar', () => NSCActionBar, {
4949
});
5050

5151
export const ActionBar = /*#__PURE__*/ defineComponent({
52+
name: 'ActionBar',
5253
setup(props, ctx) {
5354
return () => {
5455
return h(

src/components/ListView.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ function getListItem(item: any, index: number): ListItem {
4141
const LIST_CELL_ID = Symbol('list_cell_id');
4242

4343
export const ListView = /*#__PURE__*/ defineComponent({
44+
name: 'ListView',
4445
props: {
4546
items: {
4647
validator(value) {

src/dom/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { markRaw } from "@vue/runtime-core";
1+
import { markRaw } from '@vue/runtime-core';
22
import {
33
getViewClass,
44
getViewMeta,

src/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import { renderer } from './renderer';
1616
import { install as modalsPlugin } from './plugins/modals';
1717
import { install as navigationPlugin } from './plugins/navigation';
1818
import { isKnownView, registerElement } from './registry';
19-
import { setRootContext } from './runtimeHelpers';
19+
import { setRootApp } from './runtimeHelpers';
2020

2121
declare module '@vue/runtime-core' {
2222
interface App {
@@ -89,7 +89,7 @@ export const createApp = ((...args) => {
8989
const componentInstance = app.mount(createAppRoot(), false, false);
9090

9191
startApp(componentInstance);
92-
setRootContext(componentInstance.$.appContext);
92+
setRootApp(app);
9393

9494
return componentInstance;
9595
};

src/runtimeHelpers.ts

Lines changed: 21 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
import { View } from '@nativescript/core';
22
import {
3-
AppContext,
3+
App,
44
Component,
5-
h,
65
RendererElement,
76
RendererNode,
87
VNode,
@@ -14,51 +13,54 @@ type Props = Record<string, unknown>;
1413

1514
const __DEV__ = true;
1615

17-
let rootContext: AppContext = null;
16+
let rootApp: App = null;
1817

19-
export const setRootContext = (context: AppContext) => {
20-
rootContext = context;
18+
export const setRootApp = (app: App) => {
19+
rootApp = app;
2120
};
2221

2322
export const createNativeView = <T = View>(
2423
component: Component,
2524
props?: Props,
26-
contextOverrides?: any
25+
contextOverrides?: { reload?(): void }
2726
) => {
28-
let vnode: VNode;
2927
let isMounted = false;
30-
let container: NSVNode;
28+
const newApp = renderer.createApp(component, props);
29+
// Destructure so as not to copy over the root app instance
30+
const { app, ...rootContext } = rootApp._context;
3131
const context = { ...rootContext, ...contextOverrides };
3232

3333
type M = VNode<RendererNode, RendererElement, { nativeView: T }>;
3434

3535
return {
3636
context,
37+
get vnode() {
38+
return newApp._instance?.vnode;
39+
},
3740
get nativeView(): T {
38-
return vnode.el.nativeView;
41+
return this.vnode?.el.nativeView;
3942
},
4043
mount(root: NSVNode = new NSVRoot()) {
4144
if (isMounted) {
42-
return vnode as M;
45+
return this.vnode as M;
4346
}
4447

45-
vnode = h(component, props);
46-
47-
vnode.appContext = context;
48+
Object.keys(context).forEach((key) => {
49+
newApp._context[key] = context[key];
50+
});
4851

49-
renderer.render(vnode, root);
52+
newApp.mount(root);
5053

5154
isMounted = true;
52-
container = root;
5355

54-
return vnode as M;
56+
return this.vnode as M;
5557
},
5658
unmount() {
5759
if (!isMounted) return;
58-
vnode = null;
59-
renderer.render(null, container);
60+
61+
newApp.unmount();
62+
6063
isMounted = false;
61-
container = null;
6264
},
6365
};
6466
};

0 commit comments

Comments
 (0)