pake-cli 0.0.1-beta.4 → 0.0.1-beta.5

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.
@@ -0,0 +1,2 @@
1
+ github: ['tw93']
2
+ custom: ['https://miaoyan.app/cats.html']
@@ -0,0 +1,7 @@
1
+ {
2
+ "cSpell.words": [
3
+ "Pake",
4
+ "tauri"
5
+ ],
6
+ "typescript.preferences.importModuleSpecifierEnding": "js"
7
+ }
@@ -0,0 +1,12 @@
1
+ import { IS_MAC } from '@/utils/platform.js';
2
+ import { IBuilder } from './base.js';
3
+ import MacBuilder from './MacBuilder.js';
4
+
5
+ export default class BuilderFactory {
6
+ static create(): IBuilder {
7
+ if (IS_MAC) {
8
+ return new MacBuilder();
9
+ }
10
+ throw new Error('The current system does not support');
11
+ }
12
+ }
File without changes
@@ -0,0 +1,69 @@
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+ import prompts from 'prompts';
4
+ import { checkRustInstalled, installRust } from '@/helpers/rust.js';
5
+ import { PakeAppOptions } from '@/types.js';
6
+ import { IBuilder } from './base.js';
7
+ import appRootPath from 'app-root-path';
8
+ import { shellExec } from '@/utils/shell.js';
9
+
10
+ export default class MacBuilder implements IBuilder {
11
+ async prepare() {
12
+ if (checkRustInstalled()) {
13
+ console.log('Rust has been installed');
14
+ return;
15
+ }
16
+
17
+ console.warn('Rust is not installed, show prompt');
18
+ const res = await prompts({
19
+ type: 'confirm',
20
+ message: 'Detect you have not installed Rust, install it now?',
21
+ name: 'value',
22
+ });
23
+
24
+ if (res.value) {
25
+ // TODO 国内有可能会超时
26
+ await installRust();
27
+ } else {
28
+ console.error('Error: Pake need Rust to package your webapp!!!');
29
+ process.exit(2);
30
+ }
31
+ }
32
+
33
+ async build(url: string, options: PakeAppOptions) {
34
+ console.log('PakeAppOptions', options);
35
+ const tauriConfPath = path.join(appRootPath.path, 'src-tauri/tauri.conf.json');
36
+ const tauriConfString = await fs.readFile(tauriConfPath, 'utf-8');
37
+ try {
38
+ const tauriConf = JSON.parse(tauriConfString);
39
+
40
+ const { width, height, fullscreen, transparent, title, resizable, identifier, name } = options;
41
+
42
+ const tauriConfWindowOptions = {
43
+ width,
44
+ height,
45
+ fullscreen,
46
+ transparent,
47
+ title,
48
+ resizable,
49
+ };
50
+
51
+ Object.assign(tauriConf.tauri.windows[0], { url, ...tauriConfWindowOptions });
52
+ tauriConf.package.productName = name;
53
+ tauriConf.tauri.bundle.identifier = identifier;
54
+ tauriConf.tauri.bundle.icon = [options.icon];
55
+
56
+ await fs.writeFile(tauriConfPath, JSON.stringify(tauriConf, null, 2));
57
+ const code = await shellExec(`${path.join(appRootPath.path, '/node_modules/.bin/tauri')} build --config ${tauriConfPath} --target universal-apple-darwin`);
58
+ const dmgName = `${name}_${'0.2.0'}_universal.dmg`;
59
+ await fs.copyFile(this.getBuildedAppPath(dmgName), path.resolve(dmgName));
60
+ } catch (error) {
61
+ console.error('handle tauri.conf.json error', error);
62
+ return;
63
+ }
64
+ }
65
+
66
+ getBuildedAppPath(dmgName: string) {
67
+ return path.join(appRootPath.path, 'src-tauri/target/universal-apple-darwin/release/bundle/dmg', dmgName);
68
+ }
69
+ }
File without changes
@@ -0,0 +1,7 @@
1
+ import { PakeAppOptions } from '@/types.js';
2
+
3
+ export interface IBuilder {
4
+ prepare(): Promise<void>;
5
+ build(url: string, options: PakeAppOptions): Promise<void>;
6
+ // getIcon(icon?: string): Promise<string>;
7
+ }
@@ -0,0 +1,29 @@
1
+ import path from 'path';
2
+ import { checkRustInstalled, installRust } from '@/helpers/rust.js';
3
+ import appRootPath from 'app-root-path';
4
+ import prompts from 'prompts';
5
+
6
+ export async function prepareCheck() {
7
+ if (checkRustInstalled()) {
8
+ console.log('Rust has been installed');
9
+ return;
10
+ }
11
+
12
+ console.warn('Rust is not installed, show prompt');
13
+ const res = await prompts({
14
+ type: 'confirm',
15
+ message: 'Detect you have not installed Rust, install it now?',
16
+ name: 'value',
17
+ });
18
+
19
+ if (res.value) {
20
+ await installRust();
21
+ } else {
22
+ console.error('Error: Pake need Rust to package your webapp!!!');
23
+ process.exit(2);
24
+ }
25
+ }
26
+
27
+ export function getBuildedAppPath(name: string, version: string) {
28
+ return path.join(appRootPath.path, 'src-tauri/target/universal-apple-darwin/release/bundle/dmg', `${name}_${version}_universal.dmg`);
29
+ }
package/bin/cli.ts ADDED
@@ -0,0 +1,29 @@
1
+ import { program } from 'commander';
2
+ import { DEFAULT_PAKE_OPTIONS } from './defaults.js';
3
+ import { PakeCliOptions } from './types.js';
4
+ import { validateNumberInput, validateUrlInput } from './utils/validate.js';
5
+ import handleInputOptions from './options/index.js';
6
+ import BuilderFactory from './builders/BuilderFactory.js';
7
+
8
+ program.version('0.0.1').description('A cli application can build website to app, driven by tauri');
9
+
10
+ program
11
+ .argument('<url>', 'the web url you want to package', validateUrlInput)
12
+ .option('--name <string>', 'application name')
13
+ .option('--title <string>', 'application window title')
14
+ .option('--icon <string>', 'application icon', DEFAULT_PAKE_OPTIONS.icon)
15
+ .option('--height <number>', 'application window height', validateNumberInput, DEFAULT_PAKE_OPTIONS.height)
16
+ .option('--width <number>', 'application window width', validateNumberInput, DEFAULT_PAKE_OPTIONS.width)
17
+ .option('--no-resizable', 'whether the application window can be resizable', DEFAULT_PAKE_OPTIONS.resizable)
18
+ .option('--fullscreen', 'makes the packaged app start in full screen', DEFAULT_PAKE_OPTIONS.fullscreen)
19
+ .option('--transparent', 'transparent title bar', DEFAULT_PAKE_OPTIONS.transparent)
20
+ .action(async (url: string, options: PakeCliOptions) => {
21
+ const builder = BuilderFactory.create();
22
+ await builder.prepare();
23
+
24
+ const appOptions = await handleInputOptions(options, url);
25
+
26
+ builder.build(url, appOptions);
27
+ });
28
+
29
+ program.parse();
@@ -0,0 +1,12 @@
1
+ import { PakeCliOptions } from './types.js';
2
+
3
+ export const DEFAULT_PAKE_OPTIONS: PakeCliOptions = {
4
+ icon: '',
5
+ height: 800,
6
+ width: 1280,
7
+ fullscreen: false,
8
+ resizable: true,
9
+ transparent: false,
10
+ };
11
+
12
+ export const DEFAULT_APP_NAME = 'Pake';
@@ -0,0 +1,21 @@
1
+ import ora from 'ora';
2
+ import shelljs from 'shelljs';
3
+ import { shellExec } from '../utils/shell.js';
4
+
5
+ const InstallRustScript = "curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y";
6
+ export async function installRust() {
7
+ const spinner = ora('Downloading Rust').start();
8
+ try {
9
+ await shellExec(InstallRustScript);
10
+ spinner.succeed();
11
+ } catch (error) {
12
+ console.error('install rust return code', error.message);
13
+ spinner.fail();
14
+
15
+ process.exit(1);
16
+ }
17
+ }
18
+
19
+ export function checkRustInstalled() {
20
+ return shelljs.exec('rustc --version', { silent: true }).code === 0;
21
+ }
@@ -0,0 +1,8 @@
1
+ import crypto from 'crypto';
2
+
3
+ export function getIdentifier(name: string, url: string) {
4
+ const hash = crypto.createHash('md5');
5
+ hash.update(url);
6
+ const postFixHash = hash.digest('hex').substring(0, 6);
7
+ return `pake-${postFixHash}`;
8
+ }
@@ -0,0 +1,99 @@
1
+ import axios from 'axios';
2
+ import { fileTypeFromBuffer } from 'file-type';
3
+ import url from 'url';
4
+ import { PakeAppOptions } from '../types.js';
5
+ import { dir, file } from 'tmp-promise';
6
+ import path from 'path';
7
+ import pageIcon from 'page-icon';
8
+ import png2icons from 'png2icons';
9
+ import ICO from 'icojs';
10
+ import fs from 'fs/promises';
11
+
12
+ export async function handleIcon(options: PakeAppOptions, url: string) {
13
+ console.log(options.icon);
14
+ if (options.icon) {
15
+ if (options.icon.startsWith('http')) {
16
+ return downloadIcon(options.icon);
17
+ } else {
18
+ return path.resolve(options.icon);
19
+ }
20
+ }
21
+ if (!options.icon) {
22
+ return inferIcon(options.name, url);
23
+ }
24
+ }
25
+
26
+ export async function inferIcon(name: string, url: string) {
27
+ let icon = await getIconFromMacosIcons(name);
28
+ if (!icon) {
29
+ icon = await getIconFromPageUrl(url);
30
+ }
31
+ if (!icon) {
32
+ // TODO default icon
33
+ }
34
+ return icon;
35
+ }
36
+
37
+ export async function getIconFromPageUrl(url: string) {
38
+ const icon = await pageIcon(url);
39
+ console.log(icon);
40
+ if (icon.ext === '.ico') {
41
+ const a = await ICO.parse(icon.data);
42
+ icon.data = Buffer.from(a[0].buffer);
43
+ }
44
+
45
+ const iconDir = (await dir()).path;
46
+ const iconPath = path.join(iconDir, `/icon.icns`);
47
+
48
+ const out = png2icons.createICNS(icon.data, png2icons.BILINEAR, 0);
49
+
50
+ await fs.writeFile(iconPath, out);
51
+ return iconPath;
52
+ }
53
+
54
+ export async function getIconFromMacosIcons(name: string) {
55
+ const data = {
56
+ query: name,
57
+ filters: 'approved:true',
58
+ hitsPerPage: 10,
59
+ page: 1,
60
+ };
61
+ const res = await axios.post('https://p1txh7zfb3-2.algolianet.com/1/indexes/macOSicons/query?x-algolia-agent=Algolia%20for%20JavaScript%20(4.13.1)%3B%20Browser', data, {
62
+ headers: {
63
+ 'x-algolia-api-key': '0ba04276e457028f3e11e38696eab32c',
64
+ 'x-algolia-application-id': 'P1TXH7ZFB3',
65
+ },
66
+ });
67
+ if (!res.data.hits.length) {
68
+ return '';
69
+ } else {
70
+ return downloadIcon(res.data.hits[0].icnsUrl);
71
+ }
72
+ }
73
+
74
+ export async function downloadIcon(iconUrl: string) {
75
+ let iconResponse;
76
+ try {
77
+ iconResponse = await axios.get(iconUrl, {
78
+ responseType: 'arraybuffer',
79
+ });
80
+ } catch (error) {
81
+ if (error.response && error.response.status === 404) {
82
+ return null;
83
+ }
84
+ throw error;
85
+ }
86
+
87
+ const iconData = await iconResponse.data;
88
+ if (!iconData) {
89
+ return null;
90
+ }
91
+ const fileDetails = await fileTypeFromBuffer(iconData);
92
+ if (!fileDetails) {
93
+ return null;
94
+ }
95
+ const { path } = await dir();
96
+ const iconPath = `${path}/icon.${fileDetails.ext}`;
97
+ await fs.writeFile(iconPath, iconData);
98
+ return iconPath;
99
+ }
@@ -0,0 +1,24 @@
1
+ import { getIdentifier } from '../helpers/tauriConfig.js';
2
+ import { PakeAppOptions, PakeCliOptions } from '../types.js';
3
+ import { handleIcon } from './icon.js';
4
+ import { getTitleByURL } from './title.js';
5
+
6
+ export default async function handleOptions(options: PakeCliOptions, url: string): Promise<PakeAppOptions> {
7
+ const appOptions: PakeAppOptions = {
8
+ ...options,
9
+ identifier: '',
10
+ };
11
+ if (!appOptions.title) {
12
+ appOptions.title = await getTitleByURL(url);
13
+ }
14
+
15
+ if (!appOptions.name) {
16
+ appOptions.name = appOptions.title;
17
+ }
18
+
19
+ appOptions.identifier = getIdentifier(appOptions.name, url);
20
+
21
+ appOptions.icon = await handleIcon(appOptions, url);
22
+
23
+ return appOptions;
24
+ }
@@ -0,0 +1,14 @@
1
+ import axios from 'axios';
2
+
3
+ const USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36';
4
+
5
+ export async function getTitleByURL(url: string): Promise<string> {
6
+ const { data } = await axios.get<string>(url, {
7
+ headers: {
8
+ // Fake user agent for pages like http://messenger.com
9
+ 'User-Agent': USER_AGENT,
10
+ },
11
+ });
12
+ const title = /<\s*title.*?>(?<title>.+?)<\s*\/title\s*?>/i.exec(data)?.groups?.title ?? 'Webapp';
13
+ return title;
14
+ }
package/bin/types.ts ADDED
@@ -0,0 +1,29 @@
1
+ export interface PakeCliOptions {
2
+ /** 应用名称 */
3
+ name?: string;
4
+
5
+ /** 不填会取 url 的 title,取不到默认 webapp */
6
+ title?: string;
7
+
8
+ /** 应用icon */
9
+ icon: string;
10
+
11
+ /** 应用窗口宽度,默认 1280px */
12
+ width: number;
13
+
14
+ /** 应用窗口高度,默认 800px */
15
+ height: number;
16
+
17
+ /** 是否可以拖动,默认true */
18
+ resizable: boolean;
19
+
20
+ /** 是否可以全屏,默认 false */
21
+ fullscreen: boolean;
22
+
23
+ /** 是否开启沉浸式头部,默认为 false 不开启 ƒ*/
24
+ transparent: boolean;
25
+ }
26
+
27
+ export interface PakeAppOptions extends PakeCliOptions {
28
+ identifier: string;
29
+ }
@@ -0,0 +1,5 @@
1
+ export const IS_MAC = process.platform === 'darwin';
2
+
3
+ export const IS_WIN = process.platform === 'win32';
4
+
5
+ export const IS_LINUX = process.platform === 'linux';
@@ -0,0 +1,12 @@
1
+ import shelljs from 'shelljs';
2
+ export function shellExec(command: string) {
3
+ return new Promise<number>((resolve, reject) => {
4
+ shelljs.exec(command, { async: true, silent: false}, (code) => {
5
+ if (code === 0) {
6
+ resolve(0);
7
+ } else {
8
+ reject(new Error(`${code}`));
9
+ }
10
+ });
11
+ });
12
+ }
@@ -0,0 +1,21 @@
1
+ import url from 'url';
2
+ import isurl from 'is-url';
3
+
4
+ function appendProtocol(inputUrl: string): string {
5
+ const parsed = url.parse(inputUrl);
6
+ if (!parsed.protocol) {
7
+ const urlWithProtocol = `https://${inputUrl}`;
8
+ return urlWithProtocol;
9
+ }
10
+ return inputUrl;
11
+ }
12
+
13
+ export function normalizeUrl(urlToNormalize: string): string {
14
+ const urlWithProtocol = appendProtocol(urlToNormalize);
15
+
16
+ if (isurl(urlWithProtocol)) {
17
+ return urlWithProtocol;
18
+ } else {
19
+ throw new Error(`Your url "${urlWithProtocol}" is invalid`);
20
+ }
21
+ }
@@ -0,0 +1,18 @@
1
+ import * as Commander from 'commander';
2
+ import { normalizeUrl } from './url.js';
3
+
4
+ export function validateNumberInput(value: string) {
5
+ const parsedValue = Number(value);
6
+ if (isNaN(parsedValue)) {
7
+ throw new Commander.InvalidArgumentError('Not a number.');
8
+ }
9
+ return parsedValue;
10
+ }
11
+
12
+ export function validateUrlInput(url: string) {
13
+ try {
14
+ return normalizeUrl(url);
15
+ } catch (error) {
16
+ throw new Commander.InvalidArgumentError(error.message);
17
+ }
18
+ }
package/cli.js CHANGED
@@ -1,2 +1,2 @@
1
1
  #!/usr/bin/env node
2
- require('./dist/cli');
2
+ import './dist/cli.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pake-cli",
3
- "version": "0.0.1-beta.4",
3
+ "version": "0.0.1-beta.5",
4
4
  "description": "用 Rust 来打包你的 App,底层使用 Tauri,当前支持微信读书、Flomo、Vercel",
5
5
  "bin": {
6
6
  "pake": "./cli.js"
@@ -13,13 +13,6 @@
13
13
  "name": "Tw93",
14
14
  "email": "tw93@qq.com"
15
15
  },
16
- "files": [
17
- "dist",
18
- "src-tauri",
19
- "README_EN.md",
20
- "README.md",
21
- "tsconfig.json"
22
- ],
23
16
  "scripts": {
24
17
  "start": "npm run dev",
25
18
  "dev": "npm run tauri dev",
@@ -0,0 +1,25 @@
1
+ import path from 'path';
2
+ import appRootPath from 'app-root-path';
3
+ import typescript from '@rollup/plugin-typescript';
4
+ import alias from '@rollup/plugin-alias';
5
+ import { nodeResolve } from '@rollup/plugin-node-resolve';
6
+ import commonjs from '@rollup/plugin-commonjs';
7
+ import json from '@rollup/plugin-json';
8
+ export default {
9
+ input: 'bin/cli.ts',
10
+ output: {
11
+ file: 'dist/cli.js',
12
+ format: 'es'
13
+ },
14
+ plugins: [
15
+ typescript({
16
+ sourceMap: false,
17
+ }),
18
+ json(),
19
+ commonjs(),
20
+ nodeResolve(),
21
+ alias({
22
+ entries: [{ find: '@', replacement: path.join(appRootPath.path, 'bin') }],
23
+ }),
24
+ ],
25
+ };