Skip to content

Commit

Permalink
feat: add cli
Browse files Browse the repository at this point in the history
  • Loading branch information
twlite committed Oct 3, 2024
1 parent 7461e10 commit 6fffb14
Show file tree
Hide file tree
Showing 8 changed files with 248 additions and 4 deletions.
11 changes: 10 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,13 @@ Check out [examples](./examples) directory for more examples.

# Building executables

You can use [Single Executable Applications](https://nodejs.org/api/single-executable-applications.html) feature of Node.js to build an executable file.
> [!WARNING]
> The CLI feature is very experimental and may not work as expected. Please report any issues you find.
You can use [Single Executable Applications](https://nodejs.org/api/single-executable-applications.html) feature of Node.js to build an executable file. WebviewJS comes with a helper cli script to make this process easier.

```bash
webview --build --input ./path/to/your/script.js --output ./path/to/output-directory --name my-app
```

You can pass `--resources ./my-resource.json` to include additional resources in the executable. This resource can be imported using `getAsset()` or `getRawAsset()` functions from `node:sea` module.
6 changes: 6 additions & 0 deletions __test__/webview.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { test } from 'node:test'
import assert from 'node:assert/strict'

test('webview', () => {
assert.equal(1, 1)
})
104 changes: 104 additions & 0 deletions cli/build.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { execSync } from 'node:child_process';
import { copyFileSync, constants, writeFileSync, existsSync, mkdirSync, readFileSync } from 'node:fs';
import { dirname, join, sep } from 'node:path';
import { execPath } from 'node:process'

const isWindows = process.platform === 'win32';
const isMac = process.platform === 'darwin';
const NODE_SEA_FUSE = 'NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2';

const filename = (path) => path.split(sep).pop();

function writeSeaConfig(main, dest, resources) {
const config = {
main,
output: join(dirname(dest), 'sea-prep.blob'),
disableExperimentalSEAWarning: true,
assets: resources
};

writeFileSync(dest, JSON.stringify(config, null, 2));
}

function run(command, args) {
const cmd = !args?.length ? command : `${command} ${args.join(' ')}`;
execSync(cmd, { stdio: 'inherit' });
}

function generateBlob(configPath) {
run('node', ['--experimental-sea-config', configPath])

return join(dirname(configPath), 'sea-prep.blob');
}

function copyNode(output, name) {
const ext = isWindows ? '.exe' : '';
const f = join(output, name + ext);
copyFileSync(execPath, f, constants.COPYFILE_FICLONE);

return f;
}

function removeSignature(path) {
if (!isWindows && !isMac) return;

if (isWindows) {
try {
run('signtool remove /s ' + path)
} catch (e) {
console.warn(`Failed to remove signature: ${e.message}`)
}
} else {
run('codesign --remove-signature ' + path)
}
}

function injectFuse(target, blob) {
let args;

if (isMac) {
args = [`"${target}"`, 'NODE_SEA_BLOB', blob, '--sentinel-fuse', NODE_SEA_FUSE, '--macho-segment-name', 'NODE_SEA'];
} else {
args = [target, 'NODE_SEA_BLOB', blob, '--sentinel-fuse', NODE_SEA_FUSE];
}

run('npx', ['--yes', 'postject', ...args])
}

function sign(bin) {
if (isWindows) {
try {
run('signtool', ['sign', '/fd', 'SHA256', bin])
} catch (e) {
console.warn(`Failed to sign: ${e.message}`)
}
} else if (isMac) {
run('codesign', ['--sign', '-', bin])
}
}

export function build(input, output, name, resources) {
if (!existsSync(input)) {
throw new Error('Input file does not exist');
}

if (resources && !existsSync(resources)) {
throw new Error('Resources file does not exist');
}

if (!existsSync(output)) {
mkdirSync(output, { recursive: true });
}

const assets = JSON.parse(readFileSync(resources, 'utf-8'));
const configPath = join(output, 'sea-config.json');
const execPath = copyNode(output, name);

writeSeaConfig(input, configPath, assets);
const blob = generateBlob(configPath);
removeSignature(execPath);
injectFuse(execPath, blob);
sign(execPath);

return execPath;
}
110 changes: 110 additions & 0 deletions cli/index.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
#!/usr/bin/env node
import { readFile } from 'node:fs/promises'
import { parseArgs, styleText } from 'node:util'
import { join } from 'node:path';
import { stripIndents } from './utils.mjs';
import { build } from './build.mjs';

async function readPackageJSON() {
const packageJSON = await readFile(join(import.meta.dirname, '..', 'package.json'), 'utf-8');
return JSON.parse(packageJSON);
}

const { version, description, main } = await readPackageJSON();

const options = {
help: { type: 'boolean', short: 'h', description: 'Show help' },
version: { type: 'boolean', short: 'v', description: 'Show version' },
build: {
type: 'boolean',
short: 'b',
description: 'Build the project',
},
name: {
type: 'string',
short: 'n',
default: 'webviewjs',
description: 'Project name',
},
output: {
type: 'string',
short: 'o',
default: join(process.cwd(), 'dist'),
description: 'Output directory',
},
input: {
type: 'string',
short: 'i',
default: join(process.cwd(), main),
description: 'Entry file',
},
resources: {
type: 'string',
short: 'r',
description: 'Resources mapping json file path'
},
'dry-run': {
type: 'boolean',
short: 'd',
description: 'Dry run',
}
};

const args = parseArgs({
strict: true,
args: process.argv.slice(2),
options
});

let stdErr = false;


const logger = (message) => {
console.log(message);
process.exit(+stdErr);
}

const defaultValuesOptionNames = new Set(Object.keys(options).filter(k => !!options[k].default));

if (!Object.keys(args.values).filter(k => !defaultValuesOptionNames.has(k)).length) {
args.values.help = true;
stdErr = true;
}

if (args.values.help) {
const message = stripIndents`WebviewJS: ${styleText('greenBright', description)}
${styleText('dim', 'Usage:')} ${styleText('greenBright', 'webview [options]')}
${styleText('dim', 'Options:')}
${Object.entries(options).map(([name, { short, default: defaultValue, type }]) => {
const msg = ` ${styleText('greenBright', ` -${short}, --${name}`)} - ${styleText('dim', options[name].description || `${type} option`)}`;
if (defaultValue) {
return `${msg} (default: ${styleText('blueBright', defaultValue)})`;
}
return msg;
}).join('\n')}
`;

logger(message);
} else if (args.values.version) {
logger(`- WebviewJS v${version}\n- Node.js ${process.version}\n- Operating System: ${process.platform} ${process.arch}`);
} else if (args.values.build) {
const isDry = !!args.values['dry-run'];
const { output, input, resources } = args.values;

if (isDry) {
logger(`Dry run: building ${input} to ${output}`);
} else {
const projectName = args.values.name || 'webviewjs';
const target = build(input, output, prettify(projectName), resources);
logger(styleText('greenBright', `\nBuilt ${input} to ${target}. You can now run the executable using ${styleText(['cyanBright', 'bold'], target)}`));
}
}

function prettify(str) {
// remove stuff like @, /, whitespace, etc
return str.replace(/[^a-zA-Z0-9]/g, '');
}
7 changes: 7 additions & 0 deletions cli/utils.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export function stripIndents(strings, ...values) {
let str = '';
strings.forEach((string, i) => {
str += string + (values[i] || '');
});
return str.replace(/(\t)+/g, ' ').trim();
}
5 changes: 3 additions & 2 deletions examples/html.mjs → examples/html.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { Application } from '../index.js';
const requireScript = require('node:module').createRequire(__filename);
const { Application } = requireScript('../index.js');

const app = new Application();
const window = app.createBrowserWindow({
html: `<!DOCTYPE html>
html: `<!DOCTYPE html>
<html>
<head>
<title>Webview</title>
Expand Down
7 changes: 6 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
{
"name": "@webviewjs/webview",
"version": "0.1.0",
"bin": {
"webview": "./cli/index.mjs",
"webviewjs": "./cli/index.mjs"
},
"description": "Robust cross-platform webview library for Node.js written in Rust",
"main": "index.js",
"repository": "[email protected]:twlite/webview.git",
Expand Down Expand Up @@ -48,7 +52,8 @@
"format:rs": "cargo fmt",
"lint": "oxlint .",
"prepublishOnly": "napi prepublish -t npm",
"version": "napi version"
"version": "napi version",
"test": "node --test './__test__/**/*.test.*'"
},
"devDependencies": {
"@napi-rs/cli": "^2.18.4",
Expand Down
2 changes: 2 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -482,6 +482,8 @@ __metadata:
prettier: "npm:^3.3.3"
tinybench: "npm:^2.8.0"
typescript: "npm:^5.5.3"
bin:
webview: ./cli/index.mjs
languageName: unknown
linkType: soft

Expand Down

0 comments on commit 6fffb14

Please sign in to comment.