Thanks to visit codestin.com
Credit goes to github.com

Skip to content

feat: Vue Devtools support #1060

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Sep 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 8 additions & 7 deletions demo/App_Resources/Android/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,20 @@
android:smallScreens="true"
android:normalScreens="true"
android:largeScreens="true"
android:xlargeScreens="true"/>
android:xlargeScreens="true" />

<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

<application
android:name="com.tns.NativeScriptApplication"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme"
android:usesCleartextTraffic="true"
android:hardwareAccelerated="true">

<activity
Expand All @@ -27,7 +28,7 @@
android:configChanges="keyboard|keyboardHidden|orientation|screenSize|smallestScreenSize|screenLayout|locale|uiMode"
android:theme="@style/LaunchScreenTheme"
android:hardwareAccelerated="true"
android:launchMode="singleTask"
android:launchMode="singleTask"
android:exported="true">

<meta-data android:name="SET_THEME_ON_LAUNCH" android:resource="@style/AppTheme" />
Expand All @@ -37,6 +38,6 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:name="com.tns.ErrorReportActivity"/>
<activity android:name="com.tns.ErrorReportActivity" />
</application>
</manifest>
2 changes: 1 addition & 1 deletion demo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,6 @@
"@nativescript/ios": "~8.5.2",
"@nativescript/types": "~8.5.0",
"@nativescript/webpack": "~5.0.14",
"typescript": "~5.1.3"
"typescript": "^5.2.2"
}
}
2 changes: 0 additions & 2 deletions demo/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,6 @@
"@/*": ["src/*"],
"nativescript-vue": ["../src/index.ts"]
},
"typeRoots": ["types"],
"types": ["node"],
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"experimentalDecorators": true,
Expand Down
33 changes: 33 additions & 0 deletions devtools.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
if (__DEV__) {
try {
const _global = globalThis.global;

const host = (_global.__VUE_DEVTOOLS_HOST__ ??= __NS_VUE_DEVTOOLS_HOST__);
const port = (_global.__VUE_DEVTOOLS_PORT__ ??= __NS_VUE_DEVTOOLS_PORT__);
_global.__VUE_DEVTOOLS_TOAST__ ??= (message) => {
console.warn('[VueDevtools]', message);
};

const platform = global.isAndroid ? 'Android' : 'iOS';

const documentShim = {
// this shows as the title in VueDevtools
title: `${platform} :: ${host}:${port} :: NativeScript`,
querySelector: () => null,
querySelectorAll: () => [],
};

_global.document = Object.assign({}, documentShim, _global.document);
_global.addEventListener ??= () => {};
_global.removeEventListener ??= () => {};
_global.window ??= _global;

console.warn(
`[VueDevtools] Connecting to ${global.__VUE_DEVTOOLS_HOST__}:${global.__VUE_DEVTOOLS_PORT__}...`
);
require('@vue/devtools/build/hook.js');
require('@vue/devtools/build/backend.js');
} catch (e) {
console.warn('[VueDevtools] Failed to init:', e);
}
}
100 changes: 98 additions & 2 deletions nativescript.webpack.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,108 @@
const { VueLoaderPlugin } = require('vue-loader');
const spawn = require('cross-spawn');

function findFreePort(startingPort = 8098) {
let found = false;
let port = startingPort;

const isPortFree = (port) =>
new Promise((resolve) => {
const server = require('http')
.createServer()
.listen(port, '0.0.0.0', () => {
server.close();
resolve(true);
})
.on('error', () => {
resolve(false);
});
});

const findFreePort = () => {
isPortFree(port).then((isFree) => {
if (!isFree) {
port++;
return findFreePort();
}
found = true;
});
};

findFreePort();

while (!found) {
process._tickCallback();
const start = Date.now();
while (Date.now() - start < 100) {
// busy wait... not ideal, but we need to find a port synchronously...
}
}

return port;
}

function startVueDevtools(port, isAndroid = false) {
console.log(`[VueDevtools] Starting standalone Vue Devtools on port ${port}`);
if (isAndroid) {
console.log(
`[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)`
);
}
spawn(require.resolve('@vue/devtools/bin.js'), [], {
stdio: 'ignore',
env: {
...process.env,
PORT: port,
},
});
}

/**
* @param {typeof import("@nativescript/webpack")} webpack
*/
module.exports = (webpack) => {
webpack.useConfig('vue');

webpack.chainWebpack((config) => {
webpack.chainWebpack((config, env) => {
const additionalDefines = {
__VUE_PROD_DEVTOOLS__: false,
};

// todo: support configuring the devtools host/port from the nativescript.config.ts...
if (!!env.vueDevtools) {
// find a free port for the devtools
const vueDevtoolsPort = findFreePort(8098);
const isAndroid = webpack.Utils.platform.getPlatformName() === 'android';

// on android simulators, localhost is not the host machine...
const vueDevtoolsHost = isAndroid
? 'http://10.0.2.2'
: 'http://localhost';

additionalDefines['__VUE_PROD_DEVTOOLS__'] = true;
additionalDefines['__NS_VUE_DEVTOOLS_HOST__'] =
JSON.stringify(vueDevtoolsHost);
additionalDefines['__NS_VUE_DEVTOOLS_PORT__'] = vueDevtoolsPort;

const devtoolsEntryPath = require.resolve('./devtools.js');
const entryPath = webpack.Utils.platform.getEntryPath();
const paths = config.entry('bundle').values();
const entryIndex = paths.indexOf(entryPath);

if (entryIndex === -1) {
// if the app entry is not found, add the devtools entry at the beginning - generally should not happen, but just in case.
paths.unshift(entryPath);
} else {
// insert devtools entry before the app entry, but after globals etc.
paths.splice(entryIndex, 0, devtoolsEntryPath);
}

config.entry('bundle').clear().merge(paths);

// start the devtools...
startVueDevtools(vueDevtoolsPort, isAndroid);
}

// resolve any imports from "vue" to "nativescript-vue"
config.resolve.alias.set('vue', 'nativescript-vue');

Expand Down Expand Up @@ -41,7 +137,7 @@ module.exports = (webpack) => {
config.plugin('DefinePlugin').tap((args) => {
Object.assign(args[0], {
__VUE_OPTIONS_API__: true,
__VUE_PROD_DEVTOOLS__: false,
...additionalDefines,
});

return args;
Expand Down
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"main": "dist/index.js",
"files": [
"dist/",
"devtools.js",
"nativescript.webpack.js"
],
"license": "MIT",
Expand All @@ -16,8 +17,10 @@
},
"dependencies": {
"@vue/compiler-sfc": "^3.3.4",
"@vue/devtools": "^6.5.0",
"@vue/runtime-core": "^3.3.4",
"@vue/shared": "^3.3.4",
"cross-spawn": "^7.0.3",
"set-value": "^4.1.0",
"vue-loader": "^17.2.2"
},
Expand Down
2 changes: 1 addition & 1 deletion packages/stackblitz-template/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
"@nativescript/webpack": "~5.0.0",
"@types/node": "~17.0.21",
"tailwindcss": "^3.1.8",
"typescript": "~4.9.5"
"typescript": "^5.2.2"
},
"stackblitz": {
"installDependencies": true,
Expand Down
2 changes: 0 additions & 2 deletions packages/stackblitz-template/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,6 @@
"~/*": ["src/*"],
"@/*": ["src/*"]
},
"typeRoots": ["types"],
"types": ["node"],
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"experimentalDecorators": true,
Expand Down
2 changes: 1 addition & 1 deletion packages/template-blank/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,6 @@
"@nativescript/webpack": "~5.0.0",
"@types/node": "~17.0.21",
"tailwindcss": "^3.1.8",
"typescript": "~4.9.5"
"typescript": "^5.2.2"
}
}
2 changes: 0 additions & 2 deletions packages/template-blank/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,6 @@
"~/*": ["src/*"],
"@/*": ["src/*"]
},
"typeRoots": ["types"],
"types": ["node"],
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"experimentalDecorators": true,
Expand Down
1 change: 1 addition & 0 deletions src/components/ActionBar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ registerElement('NSCActionBar', () => NSCActionBar, {
});

export const ActionBar = /*#__PURE__*/ defineComponent({
name: 'ActionBar',
setup(props, ctx) {
return () => {
return h(
Expand Down
1 change: 1 addition & 0 deletions src/components/ListView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ function getListItem(item: any, index: number): ListItem {
const LIST_CELL_ID = Symbol('list_cell_id');

export const ListView = /*#__PURE__*/ defineComponent({
name: 'ListView',
props: {
items: {
validator(value) {
Expand Down
2 changes: 1 addition & 1 deletion src/dom/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { markRaw } from "@vue/runtime-core";
import { markRaw } from '@vue/runtime-core';
import {
getViewClass,
getViewMeta,
Expand Down
4 changes: 2 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { renderer } from './renderer';
import { install as modalsPlugin } from './plugins/modals';
import { install as navigationPlugin } from './plugins/navigation';
import { isKnownView, registerElement } from './registry';
import { setRootContext } from './runtimeHelpers';
import { setRootApp } from './runtimeHelpers';

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

startApp(componentInstance);
setRootContext(componentInstance.$.appContext);
setRootApp(app);

return componentInstance;
};
Expand Down
40 changes: 21 additions & 19 deletions src/runtimeHelpers.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { View } from '@nativescript/core';
import {
AppContext,
App,
Component,
h,
RendererElement,
RendererNode,
VNode,
Expand All @@ -14,51 +13,54 @@ type Props = Record<string, unknown>;

const __DEV__ = true;

let rootContext: AppContext = null;
let rootApp: App = null;

export const setRootContext = (context: AppContext) => {
rootContext = context;
export const setRootApp = (app: App) => {
rootApp = app;
};

export const createNativeView = <T = View>(
component: Component,
props?: Props,
contextOverrides?: any
contextOverrides?: { reload?(): void }
) => {
let vnode: VNode;
let isMounted = false;
let container: NSVNode;
const newApp = renderer.createApp(component, props);
// Destructure so as not to copy over the root app instance
const { app, ...rootContext } = rootApp._context;
const context = { ...rootContext, ...contextOverrides };

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

return {
context,
get vnode() {
return newApp._instance?.vnode;
},
get nativeView(): T {
return vnode.el.nativeView;
return this.vnode?.el.nativeView;
},
mount(root: NSVNode = new NSVRoot()) {
if (isMounted) {
return vnode as M;
return this.vnode as M;
}

vnode = h(component, props);

vnode.appContext = context;
Object.keys(context).forEach((key) => {
newApp._context[key] = context[key];
});

renderer.render(vnode, root);
newApp.mount(root);

isMounted = true;
container = root;

return vnode as M;
return this.vnode as M;
},
unmount() {
if (!isMounted) return;
vnode = null;
renderer.render(null, container);

newApp.unmount();

isMounted = false;
container = null;
},
};
};
Expand Down
Loading