win_webview2 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -0
- package/argtype.ts +14 -0
- package/index.js +110 -0
- package/package.json +22 -0
- package/winWebview2.mjs +100 -0
- package/win_lib/Win32/CmdWebview2.exe +0 -0
- package/win_lib/Win32/WebView2Loader.dll +0 -0
- package/win_lib/Win32/icon.ico +0 -0
- package/win_lib/Win32/index.conf +1 -0
- package/win_lib/x64/CmdWebview2.exe +0 -0
- package/win_lib/x64/WebView2Loader.dll +0 -0
- package/win_lib/x64/icon.ico +0 -0
- package/win_lib/x64/index.conf +1 -0
- package/ww2_builder.mjs +72 -0
- package/ww2_builder_deploy.mjs +171 -0
- package/ww2_builder_log.mjs +3 -0
- package/ww2_builder_type.ts +7 -0
package/README.md
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Win Webview2
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
Win Webview2 is a GUI toolkit for building desktop applications with Node.js. It is similar to Electron but significantly smaller because Win Webview2 utilizes Microsoft WebView2, which is already installed on Windows 10 and later versions.
|
|
5
|
+
|
|
6
|
+
## Building the Application
|
|
7
|
+
|
|
8
|
+
Run the following command:
|
|
9
|
+
|
|
10
|
+
```sh
|
|
11
|
+
npx ww2_builder
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
A menu will appear in the terminal with the following options:
|
|
15
|
+
- `init_webview2` – Creates a configuration file in JSON format.
|
|
16
|
+
- `deploy` – Builds the application from your Node.js project.
|
|
17
|
+
|
|
18
|
+
## Example Configuration File
|
|
19
|
+
|
|
20
|
+
```json
|
|
21
|
+
{
|
|
22
|
+
"entry_point": "app.js",
|
|
23
|
+
"appname": "openweb",
|
|
24
|
+
"icon_path": "./icon.png",
|
|
25
|
+
"outdir": "./dist"
|
|
26
|
+
}
|
|
27
|
+
```
|
package/argtype.ts
ADDED
package/index.js
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
|
|
2
|
+
var ffi = require('ffi-napi');
|
|
3
|
+
|
|
4
|
+
var arc = require('os').arch();
|
|
5
|
+
const path = require("path")
|
|
6
|
+
|
|
7
|
+
let libFilePath = "./bin/Win32/";
|
|
8
|
+
if (arc == "x64") {
|
|
9
|
+
console.log("using x64")
|
|
10
|
+
libFilePath = "./bin/x64/";
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
libFilePath = path.join(__dirname, libFilePath);
|
|
14
|
+
|
|
15
|
+
const mainDllPath = path.join(libFilePath, "win_webview2_lib.dll");
|
|
16
|
+
const webviewDllPath = path.join(libFilePath, "WebView2Loader.dll");
|
|
17
|
+
|
|
18
|
+
ffi.Library(webviewDllPath);
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
module.exports = {
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @typedef SimpleFFIcallback
|
|
28
|
+
*
|
|
29
|
+
* @param {(string : str)=>void} jscallback
|
|
30
|
+
* @return {SimpleFFIcallback}
|
|
31
|
+
*/
|
|
32
|
+
createFFIcallback : (jscallback)=>{
|
|
33
|
+
return ffi.Callback('void', ['string'], (g) => {
|
|
34
|
+
jscallback(g);
|
|
35
|
+
})
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
// void OpenWebview(
|
|
40
|
+
// const char* url,
|
|
41
|
+
// int width,
|
|
42
|
+
// int height,
|
|
43
|
+
// bool maximize,
|
|
44
|
+
// bool kiosk,
|
|
45
|
+
// const char* title,
|
|
46
|
+
// const char* windowclassname,
|
|
47
|
+
// const char* windowParentclassname,
|
|
48
|
+
// CallbackType cb
|
|
49
|
+
|
|
50
|
+
// )
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* @typedef OpenWebviewProp
|
|
54
|
+
* @property {string} url
|
|
55
|
+
* @property {int} width
|
|
56
|
+
* @property {int} height
|
|
57
|
+
* @property {bool} maximize
|
|
58
|
+
* @property {bool} kiosk
|
|
59
|
+
* @property {string} title
|
|
60
|
+
* @property {string} windowclassname
|
|
61
|
+
* @property {string} windowParentclassname
|
|
62
|
+
* @property {SimpleFFIcallback} ffiCallback
|
|
63
|
+
* @property {(string : str)=>void} callback
|
|
64
|
+
*
|
|
65
|
+
* @param {OpenWebviewProp} prop
|
|
66
|
+
*/
|
|
67
|
+
openWebview: (prop) => {
|
|
68
|
+
var libm = ffi.Library(mainDllPath, {
|
|
69
|
+
'OpenWebview': ['void',
|
|
70
|
+
[
|
|
71
|
+
'string', //url
|
|
72
|
+
'int', //width
|
|
73
|
+
'int', // height
|
|
74
|
+
'bool', // maximize
|
|
75
|
+
'bool', // kiosk
|
|
76
|
+
'string', // title.
|
|
77
|
+
'string', // windowclassname
|
|
78
|
+
'string', // windowParentclassname
|
|
79
|
+
'pointer', // CallbackType
|
|
80
|
+
]]
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
return new Promise((r,x)=>{
|
|
84
|
+
libm.OpenWebview.async(
|
|
85
|
+
prop.url,
|
|
86
|
+
prop.width,
|
|
87
|
+
prop.height,
|
|
88
|
+
prop.maximize,
|
|
89
|
+
prop.kiosk,
|
|
90
|
+
prop.title,
|
|
91
|
+
prop.windowclassname,
|
|
92
|
+
prop.windowParentclassname,
|
|
93
|
+
|
|
94
|
+
prop.ffiCallback,
|
|
95
|
+
(e, res) => {
|
|
96
|
+
if(e){
|
|
97
|
+
x(e);
|
|
98
|
+
} else {
|
|
99
|
+
r(res);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
)
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
}
|
|
110
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "win_webview2",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"repository": {
|
|
5
|
+
"type": "git",
|
|
6
|
+
"url": "https://github.com/nnttoo/win_webview2"
|
|
7
|
+
},
|
|
8
|
+
"description": "",
|
|
9
|
+
"main": "index.js",
|
|
10
|
+
"exports": "./winWebview2.mjs",
|
|
11
|
+
"bin": {
|
|
12
|
+
"ww2_builder": "./ww2_builder.mjs"
|
|
13
|
+
},
|
|
14
|
+
"author": "",
|
|
15
|
+
"license": "ISC",
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"prompts": "^2.4.2",
|
|
18
|
+
"rcedit": "^4.0.1",
|
|
19
|
+
"sharp": "^0.33.5",
|
|
20
|
+
"sharp-ico": "^0.1.5"
|
|
21
|
+
}
|
|
22
|
+
}
|
package/winWebview2.mjs
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
let isOncloseSetted = false;
|
|
3
|
+
const prefixWebview = "WINWEBVIEW_";
|
|
4
|
+
const prefixWebviewResult = "WIN_WEBVIEW2_RESULT";
|
|
5
|
+
|
|
6
|
+
/** @type {string | null} */
|
|
7
|
+
let resultDataFromWebview2 = "";
|
|
8
|
+
let resultInUse = false;
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
*
|
|
13
|
+
* @param {number} n
|
|
14
|
+
*/
|
|
15
|
+
function sleep(n){
|
|
16
|
+
return new Promise((r,x)=>{
|
|
17
|
+
setTimeout(r,n);
|
|
18
|
+
})
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
*
|
|
24
|
+
* @param {object } arg
|
|
25
|
+
*/
|
|
26
|
+
async function waitingResult(arg){
|
|
27
|
+
if(resultInUse ) return "";
|
|
28
|
+
resultInUse = true;
|
|
29
|
+
|
|
30
|
+
resultDataFromWebview2 = null;
|
|
31
|
+
|
|
32
|
+
var objstr = JSON.stringify(arg);
|
|
33
|
+
console.log(`${prefixWebview}JSON` + objstr);
|
|
34
|
+
|
|
35
|
+
while(resultDataFromWebview2 == null){
|
|
36
|
+
await sleep(200);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
resultInUse = false;
|
|
40
|
+
return resultDataFromWebview2;
|
|
41
|
+
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
*
|
|
47
|
+
* @param {string} data
|
|
48
|
+
*/
|
|
49
|
+
function setToResult(data){
|
|
50
|
+
if(!data.startsWith(prefixWebviewResult)) return false;
|
|
51
|
+
let result = data.substring(prefixWebviewResult.length);
|
|
52
|
+
resultDataFromWebview2 = result;
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function setOnClosed() {
|
|
57
|
+
process.stdin.on("data", (dataIn) => {
|
|
58
|
+
let data = dataIn.toString();
|
|
59
|
+
if(setToResult(data)) return;
|
|
60
|
+
|
|
61
|
+
console.log(data);
|
|
62
|
+
if (data == `${prefixWebview}MAINPAGEISCLOSED`) {
|
|
63
|
+
console.log("server diclose");
|
|
64
|
+
process.exit();
|
|
65
|
+
}
|
|
66
|
+
})
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
*
|
|
72
|
+
* @param {import("./argtype").OpenWebArg } arg
|
|
73
|
+
*/
|
|
74
|
+
export function openWeb(arg) {
|
|
75
|
+
if(!isOncloseSetted){
|
|
76
|
+
isOncloseSetted = true;
|
|
77
|
+
setOnClosed();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
arg["fun"] = "OpenWeb";
|
|
81
|
+
var objstr = JSON.stringify(arg);
|
|
82
|
+
console.log(`${prefixWebview}JSON` + objstr);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
*
|
|
87
|
+
* @param {import("./argtype").OpenDialogFileArg} arg
|
|
88
|
+
* @returns {Promise<string>}
|
|
89
|
+
*/
|
|
90
|
+
export function openDialogFile(arg){
|
|
91
|
+
arg["fun"] = "OpenDialogFIle";
|
|
92
|
+
return waitingResult(arg);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export async function openDialogFolder(){
|
|
96
|
+
let obj = {
|
|
97
|
+
fun : "OpenDialogFolder"
|
|
98
|
+
};
|
|
99
|
+
return waitingResult(obj);
|
|
100
|
+
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
node ../../../../example/express_dev/index.mjs
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
node ../../../../example/express_dev/index.mjs
|
package/ww2_builder.mjs
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// @ts-check
|
|
4
|
+
|
|
5
|
+
import prompts from "prompts";
|
|
6
|
+
import fs from 'fs'
|
|
7
|
+
import sharp from "sharp"
|
|
8
|
+
import path from "path";
|
|
9
|
+
import { logPrint } from "./ww2_builder_log.mjs";
|
|
10
|
+
import { WW2Deploy } from "./ww2_builder_deploy.mjs";
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
/** @typedef {import("./ww2_builder_type").ConfigWW2} ConfigWW2 */
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
*
|
|
17
|
+
* @param {string} msg
|
|
18
|
+
* @param {string[]} list
|
|
19
|
+
* @returns {Promise<string>}
|
|
20
|
+
*/
|
|
21
|
+
async function askWhitList(msg, list) {
|
|
22
|
+
let p = await prompts([
|
|
23
|
+
{
|
|
24
|
+
type: 'select', // Tipe pertanyaan list (menu dropdown)
|
|
25
|
+
name: 'hasilnya',
|
|
26
|
+
message: msg,
|
|
27
|
+
choices: list.map((val) => {
|
|
28
|
+
let r = {
|
|
29
|
+
title: val,
|
|
30
|
+
value: val
|
|
31
|
+
}
|
|
32
|
+
return r
|
|
33
|
+
}), // Opsi
|
|
34
|
+
},
|
|
35
|
+
]);
|
|
36
|
+
return p.hasilnya;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
async function run() {
|
|
47
|
+
let p = await askWhitList("menu", [
|
|
48
|
+
"init_webview2",
|
|
49
|
+
"deploy",
|
|
50
|
+
"back",
|
|
51
|
+
"exit"
|
|
52
|
+
]);
|
|
53
|
+
|
|
54
|
+
if (p == "init_webview2") {
|
|
55
|
+
|
|
56
|
+
await WW2Deploy.initWW2();
|
|
57
|
+
|
|
58
|
+
} else if (p == "deploy") {
|
|
59
|
+
await WW2Deploy.startDeploy();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
if (p == "exit") {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
run();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
run();
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
/** @typedef {import("./ww2_builder_type").ConfigWW2} ConfigWW2 */
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
import fs from 'fs'
|
|
7
|
+
import sharp from "sharp"
|
|
8
|
+
import path from "path";
|
|
9
|
+
import { logPrint } from "./ww2_builder_log.mjs";
|
|
10
|
+
import { fileURLToPath } from 'url';
|
|
11
|
+
import rcedit from "rcedit";
|
|
12
|
+
import sharpsToIco from "sharp-ico"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
const jsonConfigFilePath = "./win_webview2.json";
|
|
16
|
+
|
|
17
|
+
export class WW2Deploy {
|
|
18
|
+
|
|
19
|
+
/** @type {ConfigWW2 | null} */
|
|
20
|
+
|
|
21
|
+
configObjec = null;
|
|
22
|
+
|
|
23
|
+
/** @type {"x64" | "Win32" } */
|
|
24
|
+
platform = "Win32";
|
|
25
|
+
|
|
26
|
+
/** @type {string | null} */
|
|
27
|
+
outputExeFile = null;
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
async makeDistFolder() {
|
|
32
|
+
|
|
33
|
+
if (this.configObjec == null) return;
|
|
34
|
+
let config = this.configObjec;
|
|
35
|
+
|
|
36
|
+
let folderDist = config.outdir;
|
|
37
|
+
|
|
38
|
+
if (!fs.existsSync(folderDist)) {
|
|
39
|
+
await fs.promises.mkdir(folderDist);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
let libFolder = path.join(folderDist,"lib");
|
|
43
|
+
if (!fs.existsSync(libFolder)) {
|
|
44
|
+
await fs.promises.mkdir(libFolder);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async buildIcon() {
|
|
49
|
+
if (this.configObjec == null) return;
|
|
50
|
+
|
|
51
|
+
let config = this.configObjec;
|
|
52
|
+
|
|
53
|
+
let iconPath = config.icon_path;
|
|
54
|
+
if (!fs.existsSync(iconPath)) {
|
|
55
|
+
|
|
56
|
+
logPrint("icon notfound");
|
|
57
|
+
return;
|
|
58
|
+
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
let outputIcon = path.join(config.outdir, "icon.ico");
|
|
62
|
+
|
|
63
|
+
const sizes = [16, 32, 48, 256]; // Ukuran standar untuk rcedit
|
|
64
|
+
const images = await Promise.all(sizes.map(size =>
|
|
65
|
+
sharp(iconPath)
|
|
66
|
+
.resize(size, size)
|
|
67
|
+
.toFormat('png')
|
|
68
|
+
));
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
await sharpsToIco.sharpsToIco(images, outputIcon);
|
|
72
|
+
|
|
73
|
+
console.log('✅ Berhasil membuat icon.ico yang valid untuk rcedit!');
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async copyExe() {
|
|
79
|
+
let currentDirMjs = fileURLToPath(import.meta.url);
|
|
80
|
+
let currentDir = path.dirname(currentDirMjs);
|
|
81
|
+
|
|
82
|
+
if (this.configObjec == null) return;
|
|
83
|
+
let config = this.configObjec;
|
|
84
|
+
|
|
85
|
+
logPrint("COpy webview2 exe file");
|
|
86
|
+
logPrint(currentDir);
|
|
87
|
+
|
|
88
|
+
let inputFileExe = path.join(currentDir, "win_lib", this.platform, "CmdWebview2.exe");
|
|
89
|
+
|
|
90
|
+
let outFileExe = path.join(config.outdir, config.appname + ".exe");
|
|
91
|
+
this.outputExeFile = inputFileExe;
|
|
92
|
+
await fs.promises.copyFile(inputFileExe, outFileExe);
|
|
93
|
+
|
|
94
|
+
let inputFileDll = path.join(currentDir, "win_lib", this.platform, "WebView2Loader.dll");
|
|
95
|
+
let outFileDll = path.join(config.outdir, "WebView2Loader.dll");
|
|
96
|
+
await fs.promises.copyFile(inputFileDll, outFileDll);
|
|
97
|
+
|
|
98
|
+
logPrint("copy file success");
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async editIcon() {
|
|
102
|
+
if (this.outputExeFile == null) return;
|
|
103
|
+
if (this.configObjec == null) return;
|
|
104
|
+
|
|
105
|
+
let object = this.configObjec;
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
let iconPath = path.join(object.outdir, "icon.ico");
|
|
109
|
+
iconPath = path.resolve(iconPath);
|
|
110
|
+
|
|
111
|
+
await rcedit(this.outputExeFile, {
|
|
112
|
+
icon: iconPath,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async readConfig() {
|
|
117
|
+
let str = await fs.promises.readFile(jsonConfigFilePath);
|
|
118
|
+
let jsonObj = JSON.parse(str.toString());
|
|
119
|
+
|
|
120
|
+
this.configObjec = jsonObj;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async copyNodeExe(){
|
|
124
|
+
if(this.configObjec == null) return;
|
|
125
|
+
let obj = this.configObjec;
|
|
126
|
+
|
|
127
|
+
let inputNodeExe = process.execPath;
|
|
128
|
+
let outNodeExe = path.join(obj.outdir,"lib","node.exe");
|
|
129
|
+
await fs.promises.copyFile(inputNodeExe, outNodeExe);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async createIndexConf(){
|
|
133
|
+
if(this.configObjec == null) return;
|
|
134
|
+
let obj = this.configObjec;
|
|
135
|
+
|
|
136
|
+
let oututFileConf = path.join(obj.outdir,"index.conf");
|
|
137
|
+
|
|
138
|
+
let configCtn = `./lib/node.exe ./${obj.entry_point}`
|
|
139
|
+
await fs.promises.writeFile(oututFileConf,configCtn);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
static async initWW2() {
|
|
143
|
+
/** @type {ConfigWW2} */
|
|
144
|
+
|
|
145
|
+
let objConfig = {
|
|
146
|
+
entry_point: "app.js",
|
|
147
|
+
appname: "openweb",
|
|
148
|
+
icon_path: "./icon.png",
|
|
149
|
+
outdir: "./dist"
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
let objstr = JSON.stringify(objConfig, null, 2);
|
|
153
|
+
await fs.promises.writeFile(jsonConfigFilePath, objstr);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
static async startDeploy() {
|
|
159
|
+
logPrint("Start Deploy win_webview2");
|
|
160
|
+
let c = new WW2Deploy();
|
|
161
|
+
|
|
162
|
+
await c.readConfig();
|
|
163
|
+
await c.makeDistFolder();
|
|
164
|
+
await c.buildIcon();
|
|
165
|
+
await c.copyExe();
|
|
166
|
+
await c.editIcon();
|
|
167
|
+
await c.copyNodeExe();
|
|
168
|
+
await c.createIndexConf();
|
|
169
|
+
|
|
170
|
+
}
|
|
171
|
+
}
|