refactor(tauri): support for building without environmental variables (#850)

Co-authored-by: Lucas Nogueira <lucas@tauri.studio>
This commit is contained in:
chip 2021-02-09 10:22:04 -08:00 committed by GitHub
parent bffbf7d242
commit e02c9419cb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
60 changed files with 1310 additions and 955 deletions

View File

@ -0,0 +1,8 @@
---
"tauri-utils": minor
"tauri-api": minor
"tauri": minor
---
The Tauri files are now read on the app space instead of the `tauri` create.
Also, the `AppBuilder` `build` function now returns a Result.

View File

@ -178,6 +178,11 @@
"manager": "rust",
"dependencies": ["tauri-utils"]
},
"tauri-macros": {
"path": "./tauri-macros",
"manager": "rust",
"dependencies": ["tauri-utils"]
},
"tauri-updater": {
"path": "./tauri-updater",
"manager": "rust",
@ -186,7 +191,7 @@
"tauri": {
"path": "./tauri",
"manager": "rust",
"dependencies": ["api", "tauri-api", "tauri-updater"]
"dependencies": ["api", "tauri-api", "tauri-macros", "tauri-updater"]
}
}
}

View File

@ -3,7 +3,3 @@
---
Added `async` support to the Tauri Rust core on commit [#a169b67](https://github.com/tauri-apps/tauri/commit/a169b67ef0277b958bdac97e33c6e4c41b6844c3).
This is a breaking change:
- Change `.setup(|webview, source| {` to `.setup(|webview, _source| async move {`.
- Change `.invoke_handler(|_webview, arg| {` to `.invoke_handler(|_webview, arg| async move {`.
- Add `.await` after `tauri::execute_promise()` calls.

View File

@ -3,5 +3,3 @@
---
The Tauri Node.js CLI package is now `@tauri-apps/cli`.
To use the new CLI, delete the old `tauri` from your `package.json` and install the new package:
`$ yarn remove tauri && yarn add --dev @tauri-apps/cli` or `$ npm uninstall tauri && npm install --save-dev @tauri-apps/cli`.

View File

@ -2,6 +2,7 @@
members = [
"tauri",
"tauri-api",
"tauri-macros",
"tauri-utils",
]
exclude = [

View File

@ -3,54 +3,54 @@ module.exports = {
env: {
node: true,
jest: true,
jest: true
},
parser: "@typescript-eslint/parser",
parser: '@typescript-eslint/parser',
extends: [
"standard-with-typescript",
"plugin:@typescript-eslint/recommended-requiring-type-checking",
"plugin:lodash-template/recommended",
'standard-with-typescript',
'plugin:@typescript-eslint/recommended-requiring-type-checking',
'plugin:lodash-template/recommended',
// TODO: make this work with typescript
// 'plugin:node/recommended'
"prettier",
"prettier/@typescript-eslint",
'prettier',
'prettier/@typescript-eslint'
],
plugins: ["@typescript-eslint", "node", "security"],
plugins: ['@typescript-eslint', 'node', 'security'],
parserOptions: {
tsconfigRootDir: __dirname,
project: "./tsconfig.json",
project: './tsconfig.json'
},
globals: {
__statics: true,
process: true,
process: true
},
// add your custom rules here
rules: {
// allow console.log during development only
"no-console": process.env.NODE_ENV === "production" ? "error" : "off",
'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off',
// allow debugger during development only
"no-debugger": process.env.NODE_ENV === "production" ? "error" : "off",
"no-process-exit": "off",
"security/detect-non-literal-fs-filename": "warn",
"security/detect-unsafe-regex": "error",
"security/detect-buffer-noassert": "error",
"security/detect-child-process": "warn",
"security/detect-disable-mustache-escape": "error",
"security/detect-eval-with-expression": "error",
"security/detect-no-csrf-before-method-override": "error",
"security/detect-non-literal-regexp": "error",
"security/detect-non-literal-require": "warn",
"security/detect-object-injection": "warn",
"security/detect-possible-timing-attacks": "error",
"security/detect-pseudoRandomBytes": "error",
"space-before-function-paren": "off",
"@typescript-eslint/default-param-last": "off",
"@typescript-eslint/strict-boolean-expressions": 0,
},
};
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
'no-process-exit': 'off',
'security/detect-non-literal-fs-filename': 'warn',
'security/detect-unsafe-regex': 'error',
'security/detect-buffer-noassert': 'error',
'security/detect-child-process': 'warn',
'security/detect-disable-mustache-escape': 'error',
'security/detect-eval-with-expression': 'error',
'security/detect-no-csrf-before-method-override': 'error',
'security/detect-non-literal-regexp': 'error',
'security/detect-non-literal-require': 'warn',
'security/detect-object-injection': 'warn',
'security/detect-possible-timing-attacks': 'error',
'security/detect-pseudoRandomBytes': 'error',
'space-before-function-paren': 'off',
'@typescript-eslint/default-param-last': 'off',
'@typescript-eslint/strict-boolean-expressions': 0
}
}

5
api/.prettierrc.js Normal file
View File

@ -0,0 +1,5 @@
module.exports = {
singleQuote: true,
semi: false,
trailingComma: 'none'
}

View File

@ -1,14 +1,14 @@
module.exports = {
presets: [
[
"@babel/preset-env",
'@babel/preset-env',
{
targets: {
node: "current",
node: 'current'
},
modules: "commonjs",
},
modules: 'commonjs'
}
],
"@babel/preset-typescript",
],
};
'@babel/preset-typescript'
]
}

View File

@ -1,107 +1,107 @@
// rollup.config.js
import { terser } from "rollup-plugin-terser";
import resolve from "@rollup/plugin-node-resolve";
import commonjs from "@rollup/plugin-commonjs";
import sucrase from "@rollup/plugin-sucrase";
import babel, { getBabelOutputPlugin } from "@rollup/plugin-babel";
import typescript from "@rollup/plugin-typescript";
import pkg from "./package.json";
import { terser } from 'rollup-plugin-terser'
import resolve from '@rollup/plugin-node-resolve'
import commonjs from '@rollup/plugin-commonjs'
import sucrase from '@rollup/plugin-sucrase'
import babel, { getBabelOutputPlugin } from '@rollup/plugin-babel'
import typescript from '@rollup/plugin-typescript'
import pkg from './package.json'
export default [
{
input: {
fs: "./src/fs.ts",
path: "./src/path.ts",
dialog: "./src/dialog.ts",
event: "./src/event.ts",
http: "./src/http.ts",
index: "./src/index.ts",
process: "./src/process.ts",
tauri: "./src/tauri.ts",
window: "./src/window.ts",
cli: "./src/cli.ts",
notification: "./src/notification.ts",
fs: './src/fs.ts',
path: './src/path.ts',
dialog: './src/dialog.ts',
event: './src/event.ts',
http: './src/http.ts',
index: './src/index.ts',
process: './src/process.ts',
tauri: './src/tauri.ts',
window: './src/window.ts',
cli: './src/cli.ts',
notification: './src/notification.ts'
},
treeshake: true,
perf: true,
output: [
{
dir: "dist/",
entryFileNames: "[name].js",
format: "cjs",
exports: "named",
globals: {},
dir: 'dist/',
entryFileNames: '[name].js',
format: 'cjs',
exports: 'named',
globals: {}
},
{
dir: "dist/",
entryFileNames: "[name].mjs",
format: "esm",
exports: "named",
globals: {},
},
dir: 'dist/',
entryFileNames: '[name].mjs',
format: 'esm',
exports: 'named',
globals: {}
}
],
plugins: [
commonjs({}),
resolve({
// pass custom options to the resolve plugin
customResolveOptions: {
moduleDirectory: "node_modules",
},
moduleDirectory: 'node_modules'
}
}),
typescript({
tsconfig: "./tsconfig.json",
tsconfig: './tsconfig.json'
}),
babel({
configFile: false,
presets: [["@babel/preset-env"], ["@babel/preset-typescript"]],
presets: [['@babel/preset-env'], ['@babel/preset-typescript']]
}),
terser(),
terser()
],
external: [
...Object.keys(pkg.dependencies || {}),
...Object.keys(pkg.peerDependencies || {}),
...Object.keys(pkg.peerDependencies || {})
],
watch: {
chokidar: true,
include: "src/**",
exclude: "node_modules/**",
},
include: 'src/**',
exclude: 'node_modules/**'
}
},
{
input: {
bundle: "./src/bundle.ts",
bundle: './src/bundle.ts'
},
output: [
{
name: "__TAURI__",
dir: "dist/", // if it needs to run in the browser
entryFileNames: "tauri.bundle.umd.js",
format: "umd",
name: '__TAURI__',
dir: 'dist/', // if it needs to run in the browser
entryFileNames: 'tauri.bundle.umd.js',
format: 'umd',
plugins: [
getBabelOutputPlugin({
presets: [["@babel/preset-env", { modules: "umd" }]],
allowAllFormats: true,
presets: [['@babel/preset-env', { modules: 'umd' }]],
allowAllFormats: true
}),
terser(),
terser()
],
globals: {},
},
globals: {}
}
],
plugins: [
sucrase({
exclude: ["node_modules"],
transforms: ["typescript"],
exclude: ['node_modules'],
transforms: ['typescript']
}),
resolve({
// pass custom options to the resolve plugin
customResolveOptions: {
moduleDirectory: "node_modules",
},
}),
moduleDirectory: 'node_modules'
}
})
],
external: [
...Object.keys(pkg.dependencies || {}),
...Object.keys(pkg.peerDependencies || {}),
],
},
];
...Object.keys(pkg.peerDependencies || {})
]
}
]

View File

@ -1,14 +1,14 @@
import "regenerator-runtime/runtime";
import * as cli from "./cli";
import * as dialog from "./dialog";
import * as event from "./event";
import * as fs from "./fs";
import * as path from "./path";
import http from "./http";
import * as process from "./process";
import * as tauri from "./tauri";
import * as window from "./window";
import * as notification from "./notification";
import 'regenerator-runtime/runtime'
import * as cli from './cli'
import * as dialog from './dialog'
import * as event from './event'
import * as fs from './fs'
import * as path from './path'
import http from './http'
import * as process from './process'
import * as tauri from './tauri'
import * as window from './window'
import * as notification from './notification'
export {
cli,
@ -20,5 +20,5 @@ export {
process,
tauri,
window,
notification,
};
notification
}

View File

@ -1,4 +1,4 @@
import { promisified } from "./tauri";
import { promisified } from './tauri'
export interface ArgMatch {
/**
@ -6,21 +6,21 @@ export interface ArgMatch {
* boolean if flag
* string[] or null if takes multiple values
*/
value: string | boolean | string[] | null;
value: string | boolean | string[] | null
/**
* number of occurrences
*/
occurrences: number;
occurrences: number
}
export interface SubcommandMatch {
name: string;
matches: CliMatches;
name: string
matches: CliMatches
}
export interface CliMatches {
args: { [name: string]: ArgMatch };
subcommand: SubcommandMatch | null;
args: { [name: string]: ArgMatch }
subcommand: SubcommandMatch | null
}
/**
@ -28,8 +28,8 @@ export interface CliMatches {
*/
async function getMatches(): Promise<CliMatches> {
return await promisified<CliMatches>({
cmd: "cliMatches",
});
cmd: 'cliMatches'
})
}
export { getMatches };
export { getMatches }

View File

@ -1,16 +1,16 @@
import { promisified } from "./tauri";
import { promisified } from './tauri'
export interface OpenDialogOptions {
filter?: string;
defaultPath?: string;
multiple?: boolean;
directory?: boolean;
filter?: string
defaultPath?: string
multiple?: boolean
directory?: boolean
}
export type SaveDialogOptions = Pick<
OpenDialogOptions,
"filter" | "defaultPath"
>;
'filter' | 'defaultPath'
>
/**
* @name openDialog
@ -25,14 +25,14 @@ export type SaveDialogOptions = Pick<
async function open(
options: OpenDialogOptions = {}
): Promise<string | string[]> {
if (typeof options === "object") {
Object.freeze(options);
if (typeof options === 'object') {
Object.freeze(options)
}
return await promisified({
cmd: "openDialog",
options,
});
cmd: 'openDialog',
options
})
}
/**
@ -44,14 +44,14 @@ async function open(
* @returns {Promise<string>} Promise resolving to the select path
*/
async function save(options: SaveDialogOptions = {}): Promise<string> {
if (typeof options === "object") {
Object.freeze(options);
if (typeof options === 'object') {
Object.freeze(options)
}
return await promisified({
cmd: "saveDialog",
options,
});
cmd: 'saveDialog',
options
})
}
export { open, save };
export { open, save }

View File

@ -1,11 +1,11 @@
import { invoke, transformCallback } from "./tauri";
import { invoke, transformCallback } from './tauri'
export interface Event<T> {
type: string;
payload: T;
type: string
payload: T
}
export type EventCallback<T> = (event: Event<T>) => void;
export type EventCallback<T> = (event: Event<T>) => void
/**
* listen to an event from the backend
@ -19,11 +19,11 @@ function listen<T>(
once = false
): void {
invoke({
cmd: "listen",
cmd: 'listen',
event,
handler: transformCallback(handler, once),
once,
});
once
})
}
/**
@ -34,10 +34,10 @@ function listen<T>(
*/
function emit(event: string, payload?: string): void {
invoke({
cmd: "emit",
cmd: 'emit',
event,
payload,
});
payload
})
}
export { listen, emit };
export { listen, emit }

View File

@ -1,47 +1,47 @@
import { promisified } from "./tauri";
import { promisified } from './tauri'
export enum ResponseType {
JSON = 1,
Text = 2,
Binary = 3,
Binary = 3
}
export enum BodyType {
Form = 1,
File = 2,
Auto = 3,
Auto = 3
}
export type Body = object | string | BinaryType;
export type Body = object | string | BinaryType
export type HttpVerb =
| "GET"
| "POST"
| "PUT"
| "DELETE"
| "PATCH"
| "HEAD"
| "OPTIONS"
| "CONNECT"
| "TRACE";
| 'GET'
| 'POST'
| 'PUT'
| 'DELETE'
| 'PATCH'
| 'HEAD'
| 'OPTIONS'
| 'CONNECT'
| 'TRACE'
export interface HttpOptions {
method: HttpVerb;
url: string;
headers?: Record<string, any>;
params?: Record<string, any>;
body?: Body;
followRedirects: boolean;
maxRedirections: boolean;
connectTimeout: number;
readTimeout: number;
timeout: number;
allowCompression: boolean;
responseType?: ResponseType;
bodyType: BodyType;
method: HttpVerb
url: string
headers?: Record<string, any>
params?: Record<string, any>
body?: Body
followRedirects: boolean
maxRedirections: boolean
connectTimeout: number
readTimeout: number
timeout: number
allowCompression: boolean
responseType?: ResponseType
bodyType: BodyType
}
export type PartialOptions = Omit<HttpOptions, "method" | "url">;
export type PartialOptions = Omit<HttpOptions, 'method' | 'url'>
/**
* makes a HTTP request
@ -52,9 +52,9 @@ export type PartialOptions = Omit<HttpOptions, "method" | "url">;
*/
async function request<T>(options: HttpOptions): Promise<T> {
return await promisified({
cmd: "httpRequest",
options: options,
});
cmd: 'httpRequest',
options: options
})
}
/**
@ -67,10 +67,10 @@ async function request<T>(options: HttpOptions): Promise<T> {
*/
async function get<T>(url: string, options: PartialOptions): Promise<T> {
return await request({
method: "GET",
method: 'GET',
url,
...options,
});
...options
})
}
/**
@ -88,11 +88,11 @@ async function post<T>(
options: PartialOptions
): Promise<T> {
return await request({
method: "POST",
method: 'POST',
url,
body,
...options,
});
...options
})
}
/**
@ -110,11 +110,11 @@ async function put<T>(
options: PartialOptions
): Promise<T> {
return await request({
method: "PUT",
method: 'PUT',
url,
body,
...options,
});
...options
})
}
/**
@ -127,10 +127,10 @@ async function put<T>(
*/
async function patch<T>(url: string, options: PartialOptions): Promise<T> {
return await request({
method: "PATCH",
method: 'PATCH',
url,
...options,
});
...options
})
}
/**
@ -146,10 +146,10 @@ async function deleteRequest<T>(
options: PartialOptions
): Promise<T> {
return await request({
method: "DELETE",
method: 'DELETE',
url,
...options,
});
...options
})
}
export default {
@ -160,5 +160,5 @@ export default {
patch,
delete: deleteRequest,
ResponseType,
BodyType,
};
BodyType
}

View File

@ -1,2 +1,2 @@
import * as api from "./bundle";
export default api;
import * as api from './bundle'
export default api

View File

@ -1,35 +1,35 @@
import { promisified } from "./tauri";
import { promisified } from './tauri'
export interface Options {
title: string;
body?: string;
icon?: string;
title: string
body?: string
icon?: string
}
export type PartialOptions = Omit<Options, "title">;
export type Permission = "granted" | "denied" | "default";
export type PartialOptions = Omit<Options, 'title'>
export type Permission = 'granted' | 'denied' | 'default'
async function isPermissionGranted(): Promise<boolean | null> {
if (window.Notification.permission !== "default") {
return await Promise.resolve(window.Notification.permission === "granted");
if (window.Notification.permission !== 'default') {
return await Promise.resolve(window.Notification.permission === 'granted')
}
return await promisified({
cmd: "isNotificationPermissionGranted",
});
cmd: 'isNotificationPermissionGranted'
})
}
async function requestPermission(): Promise<Permission> {
return await window.Notification.requestPermission();
return await window.Notification.requestPermission()
}
function sendNotification(options: Options | string): void {
if (typeof options === "string") {
if (typeof options === 'string') {
// eslint-disable-next-line no-new
new window.Notification(options);
new window.Notification(options)
} else {
// eslint-disable-next-line no-new
new window.Notification(options.title, options);
new window.Notification(options.title, options)
}
}
export { sendNotification, requestPermission, isPermissionGranted };
export { sendNotification, requestPermission, isPermissionGranted }

View File

@ -1,5 +1,5 @@
import { promisified } from "./tauri";
import { BaseDirectory } from "./fs";
import { promisified } from './tauri'
import { BaseDirectory } from './fs'
/**
* @name appDir
@ -8,10 +8,10 @@ import { BaseDirectory } from "./fs";
*/
async function appDir(): Promise<string> {
return await promisified<string>({
cmd: "resolvePath",
path: "",
directory: BaseDirectory.App,
});
cmd: 'resolvePath',
path: '',
directory: BaseDirectory.App
})
}
/**
@ -21,10 +21,10 @@ async function appDir(): Promise<string> {
*/
async function audioDir(): Promise<string> {
return await promisified<string>({
cmd: "resolvePath",
path: "",
directory: BaseDirectory.Audio,
});
cmd: 'resolvePath',
path: '',
directory: BaseDirectory.Audio
})
}
/**
@ -34,10 +34,10 @@ async function audioDir(): Promise<string> {
*/
async function cacheDir(): Promise<string> {
return await promisified<string>({
cmd: "resolvePath",
path: "",
directory: BaseDirectory.Cache,
});
cmd: 'resolvePath',
path: '',
directory: BaseDirectory.Cache
})
}
/**
@ -47,10 +47,10 @@ async function cacheDir(): Promise<string> {
*/
async function configDir(): Promise<string> {
return await promisified<string>({
cmd: "resolvePath",
path: "",
directory: BaseDirectory.Config,
});
cmd: 'resolvePath',
path: '',
directory: BaseDirectory.Config
})
}
/**
@ -60,10 +60,10 @@ async function configDir(): Promise<string> {
*/
async function dataDir(): Promise<string> {
return await promisified<string>({
cmd: "resolvePath",
path: "",
directory: BaseDirectory.Data,
});
cmd: 'resolvePath',
path: '',
directory: BaseDirectory.Data
})
}
/**
@ -73,10 +73,10 @@ async function dataDir(): Promise<string> {
*/
async function desktopDir(): Promise<string> {
return await promisified<string>({
cmd: "resolvePath",
path: "",
directory: BaseDirectory.Desktop,
});
cmd: 'resolvePath',
path: '',
directory: BaseDirectory.Desktop
})
}
/**
@ -86,10 +86,10 @@ async function desktopDir(): Promise<string> {
*/
async function documentDir(): Promise<string> {
return await promisified<string>({
cmd: "resolvePath",
path: "",
directory: BaseDirectory.Document,
});
cmd: 'resolvePath',
path: '',
directory: BaseDirectory.Document
})
}
/**
@ -99,10 +99,10 @@ async function documentDir(): Promise<string> {
*/
async function downloadDir(): Promise<string> {
return await promisified<string>({
cmd: "resolvePath",
path: "",
directory: BaseDirectory.Download,
});
cmd: 'resolvePath',
path: '',
directory: BaseDirectory.Download
})
}
/**
@ -112,10 +112,10 @@ async function downloadDir(): Promise<string> {
*/
async function executableDir(): Promise<string> {
return await promisified<string>({
cmd: "resolvePath",
path: "",
directory: BaseDirectory.Executable,
});
cmd: 'resolvePath',
path: '',
directory: BaseDirectory.Executable
})
}
/**
@ -125,10 +125,10 @@ async function executableDir(): Promise<string> {
*/
async function fontDir(): Promise<string> {
return await promisified<string>({
cmd: "resolvePath",
path: "",
directory: BaseDirectory.Font,
});
cmd: 'resolvePath',
path: '',
directory: BaseDirectory.Font
})
}
/**
@ -138,10 +138,10 @@ async function fontDir(): Promise<string> {
*/
async function homeDir(): Promise<string> {
return await promisified<string>({
cmd: "resolvePath",
path: "",
directory: BaseDirectory.Home,
});
cmd: 'resolvePath',
path: '',
directory: BaseDirectory.Home
})
}
/**
@ -151,10 +151,10 @@ async function homeDir(): Promise<string> {
*/
async function localDataDir(): Promise<string> {
return await promisified<string>({
cmd: "resolvePath",
path: "",
directory: BaseDirectory.LocalData,
});
cmd: 'resolvePath',
path: '',
directory: BaseDirectory.LocalData
})
}
/**
@ -164,10 +164,10 @@ async function localDataDir(): Promise<string> {
*/
async function pictureDir(): Promise<string> {
return await promisified<string>({
cmd: "resolvePath",
path: "",
directory: BaseDirectory.Picture,
});
cmd: 'resolvePath',
path: '',
directory: BaseDirectory.Picture
})
}
/**
@ -177,10 +177,10 @@ async function pictureDir(): Promise<string> {
*/
async function publicDir(): Promise<string> {
return await promisified<string>({
cmd: "resolvePath",
path: "",
directory: BaseDirectory.Public,
});
cmd: 'resolvePath',
path: '',
directory: BaseDirectory.Public
})
}
/**
@ -190,10 +190,10 @@ async function publicDir(): Promise<string> {
*/
async function resourceDir(): Promise<string> {
return await promisified<string>({
cmd: "resolvePath",
path: "",
directory: BaseDirectory.Resource,
});
cmd: 'resolvePath',
path: '',
directory: BaseDirectory.Resource
})
}
/**
@ -203,10 +203,10 @@ async function resourceDir(): Promise<string> {
*/
async function runtimeDir(): Promise<string> {
return await promisified<string>({
cmd: "resolvePath",
path: "",
directory: BaseDirectory.Runtime,
});
cmd: 'resolvePath',
path: '',
directory: BaseDirectory.Runtime
})
}
/**
@ -216,10 +216,10 @@ async function runtimeDir(): Promise<string> {
*/
async function templateDir(): Promise<string> {
return await promisified<string>({
cmd: "resolvePath",
path: "",
directory: BaseDirectory.Template,
});
cmd: 'resolvePath',
path: '',
directory: BaseDirectory.Template
})
}
/**
@ -229,10 +229,10 @@ async function templateDir(): Promise<string> {
*/
async function videoDir(): Promise<string> {
return await promisified<string>({
cmd: "resolvePath",
path: "",
directory: BaseDirectory.Video,
});
cmd: 'resolvePath',
path: '',
directory: BaseDirectory.Video
})
}
/**
@ -245,10 +245,10 @@ async function resolvePath(
directory: BaseDirectory
): Promise<string> {
return await promisified<string>({
cmd: "resolvePath",
cmd: 'resolvePath',
path,
directory,
});
directory
})
}
export {
@ -270,5 +270,5 @@ export {
runtimeDir,
templateDir,
videoDir,
resolvePath,
};
resolvePath
}

View File

@ -1,4 +1,4 @@
import { promisified } from "./tauri";
import { promisified } from './tauri'
/**
* spawns a process
@ -11,15 +11,15 @@ async function execute(
command: string,
args?: string | string[]
): Promise<string> {
if (typeof args === "object") {
Object.freeze(args);
if (typeof args === 'object') {
Object.freeze(args)
}
return await promisified({
cmd: "execute",
cmd: 'execute',
command,
args: typeof args === "string" ? [args] : args,
});
args: typeof args === 'string' ? [args] : args
})
}
export { execute };
export { execute }

View File

@ -1,31 +1,31 @@
declare global {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
interface Window {
__TAURI_INVOKE_HANDLER__: (command: string) => void;
__TAURI_INVOKE_HANDLER__: (command: string) => void
}
}
function s4(): string {
return Math.floor((1 + Math.random()) * 0x10000)
.toString(16)
.substring(1);
.substring(1)
}
function uid(): string {
return (
s4() +
s4() +
"-" +
'-' +
s4() +
"-" +
'-' +
s4() +
"-" +
'-' +
s4() +
"-" +
'-' +
s4() +
s4() +
s4()
);
)
}
/**
@ -34,28 +34,28 @@ function uid(): string {
* @param args
*/
function invoke(args: any): void {
window.__TAURI_INVOKE_HANDLER__(JSON.stringify(args));
window.__TAURI_INVOKE_HANDLER__(JSON.stringify(args))
}
function transformCallback(
callback?: (response: any) => void,
once = false
): string {
const identifier = uid();
const identifier = uid()
Object.defineProperty(window, identifier, {
value: (result: any) => {
if (once) {
Reflect.deleteProperty(window, identifier);
Reflect.deleteProperty(window, identifier)
}
return callback?.(result);
return callback?.(result)
},
writable: false,
configurable: true,
});
configurable: true
})
return identifier;
return identifier
}
/**
@ -68,20 +68,20 @@ function transformCallback(
async function promisified<T>(args: any): Promise<T> {
return await new Promise((resolve, reject) => {
const callback = transformCallback((e) => {
resolve(e);
Reflect.deleteProperty(window, error);
}, true);
resolve(e)
Reflect.deleteProperty(window, error)
}, true)
const error = transformCallback((e) => {
reject(e);
Reflect.deleteProperty(window, callback);
}, true);
reject(e)
Reflect.deleteProperty(window, callback)
}, true)
invoke({
callback,
error,
...args,
});
});
...args
})
})
}
export { invoke, transformCallback, promisified };
export { invoke, transformCallback, promisified }

View File

@ -1,4 +1,4 @@
import { invoke } from "./tauri";
import { invoke } from './tauri'
/**
* sets the window title
@ -7,9 +7,9 @@ import { invoke } from "./tauri";
*/
function setTitle(title: string): void {
invoke({
cmd: "setTitle",
title,
});
cmd: 'setTitle',
title
})
}
/**
@ -19,9 +19,9 @@ function setTitle(title: string): void {
*/
function open(url: string): void {
invoke({
cmd: "open",
uri: url,
});
cmd: 'open',
uri: url
})
}
export { setTitle, open };
export { setTitle, open }

View File

@ -130,4 +130,4 @@
- Create UMD, ESM and CJS artifacts for the JavaScript API entry point from TS source using rollup.
- Renaming window.tauri to window.\_\_TAURI\_\_, closing #435.
The **Tauri** object now follows the TypeScript API structure (e.g. window.tauri.readTextFile is now window.\_\_TAURI\_\_.fs.readTextFile).
If you want to keep the `window.tauri` object for a while, you can add a [mapping object](https://gist.github.com/lucasfernog/8f7b29cadd91d92ee2cf816a20c2ef01) to your code.
If you want to keep the `window.tauri` object for a while, you can add a [mapping object](https://gist.github.com/lucasfernog/8f7b29cadd91d92ee2cf816a20c2ef01) to your code.

View File

@ -9,8 +9,7 @@ function runCliCommand(
args: Args
): { pid: number; promise: Promise<void> } {
const argsArray = []
for (const argName in args) {
const argValue = args[argName]
for (const [argName, argValue] of Object.entries(args)) {
if (argValue === false) {
continue
}

View File

@ -406,6 +406,10 @@
}
],
"description": "the embedded server port number or the 'random' string to generate one at runtime"
},
"publicPath": {
"description": "The base path for all the assets within your application",
"type": "string"
}
},
"type": "object"

View File

@ -229,6 +229,10 @@ export interface TauriConfig {
* the embedded server port number or the 'random' string to generate one at runtime
*/
port?: number | 'random' | undefined
/**
* The base path for all the assets within your application
*/
publicPath?: string
}
/**
* tauri bundler configuration

View File

@ -458,6 +458,11 @@ export const TauriConfigSchema = {
],
description:
"the embedded server port number or the 'random' string to generate one at runtime"
},
publicPath: {
description:
'The base path for all the assets within your application.',
type: 'string'
}
},
type: 'object'

View File

@ -5,8 +5,11 @@
mod cmd;
#[derive(tauri::FromTauriContext)]
struct Context;
fn main() {
tauri::AppBuilder::<tauri::flavors::Wry>::new()
tauri::AppBuilder::<tauri::flavors::Wry, Context>::new()
.invoke_handler(|_webview, arg| async move {
use cmd::Cmd::*;
match serde_json::from_str(&arg) {
@ -24,5 +27,6 @@ fn main() {
}
})
.build()
.unwrap()
.run();
}

View File

@ -24,8 +24,6 @@ icon = [
serde_json = "1.0.61"
serde = "1.0"
serde_derive = "1.0"
phf = "0.8.0"
includedir = "0.6.0"
tauri = { path = "../../../../../../../tauri", features =["all-api"]}
[features]

View File

@ -1,8 +1,8 @@
#[derive(Deserialize)]
#[derive(serde::Deserialize)]
#[serde(tag = "cmd", rename_all = "camelCase")]
pub enum Cmd {
// your custom commands
// multiple arguments are allowed
// note that rename_all = "camelCase": you need to use "myCustomCommand" on JS
Exit { },
Exit {},
}

View File

@ -1,26 +1,25 @@
mod cmd;
#[macro_use]
extern crate serde_derive;
extern crate serde_json;
use tauri::ApplicationDispatcherExt;
#[derive(tauri::FromTauriContext)]
struct Context;
fn main() {
tauri::AppBuilder::new()
.setup(|webview, _| async move {
let mut webview_ = webview.as_mut();
tauri::AppBuilder::<tauri::flavors::Wry, Context>::new()
.setup(|dispatcher, _| async move {
let mut dispatcher_ = dispatcher.clone();
tauri::event::listen(String::from("hello"), move |_| {
tauri::event::emit(
&mut webview_,
&mut dispatcher_,
String::from("reply"),
Some("{ msg: 'TEST' }".to_string()),
)
.unwrap();
});
let _ = webview.dispatch(|w| {
w.eval("window.onTauriInit && window.onTauriInit()");
});
dispatcher.eval("window.onTauriInit && window.onTauriInit()");
})
.invoke_handler(|webview, arg| async move {
.invoke_handler(|dispatcher, arg| async move {
use cmd::Cmd::*;
match serde_json::from_str(&arg) {
Err(e) => Err(e.to_string()),
@ -28,7 +27,7 @@ fn main() {
match command {
// definitions for your custom commands from Cmd here
Exit {} => {
webview.terminate();
// TODO dispatcher.terminate();
}
}
Ok(())
@ -36,5 +35,6 @@ fn main() {
}
})
.build()
.unwrap()
.run();
}

View File

@ -24,8 +24,8 @@ semver = "0.11"
tempfile = "3"
either = "1.6.1"
tar = "0.4"
flate2 = "1"
anyhow = "1.0.38"
flate2 = "1.0"
thiserror = "1.0.23"
rand = "0.8"
nfd = "0.0.4"

View File

@ -1,39 +0,0 @@
use std::{
env,
error::Error,
fs::{read_to_string, File},
io::{BufWriter, Write},
path::Path,
};
pub fn main() -> Result<(), Box<dyn Error>> {
let out_dir = env::var("OUT_DIR")?;
let dest_config_path = Path::new(&out_dir).join("tauri.conf.json");
let mut config_file = BufWriter::new(File::create(&dest_config_path)?);
match env::var_os("TAURI_CONFIG") {
Some(tauri_config) => {
println!("cargo:rerun-if-env-changed=TAURI_CONFIG");
let tauri_config_string = tauri_config.into_string().unwrap();
write!(config_file, "{}", tauri_config_string)?;
}
None => match env::var_os("TAURI_DIR") {
Some(tauri_dir) => {
let tauri_dir_string = tauri_dir.into_string().unwrap();
println!("cargo:rerun-if-changed={}", tauri_dir_string);
let original_config_path = Path::new(&tauri_dir_string).join("tauri.conf.json");
let original_config = read_to_string(original_config_path)?;
write!(config_file, "{}", original_config)?;
}
None => {
write!(config_file, "{{}}")?;
println!("Build error: Couldn't find ENV: TAURI_CONFIG or TAURI_DIR");
}
},
}
Ok(())
}

View File

@ -1,4 +1,4 @@
use crate::config::{get as get_config, CliArg, CliConfig};
use crate::config::{CliArg, CliConfig, Config};
use clap::{App, Arg, ArgMatches};
use serde::Serialize;
@ -53,8 +53,7 @@ impl Matches {
}
/// Gets the arg matches of the CLI definition.
pub fn get_matches() -> crate::Result<Matches> {
let config = get_config()?;
pub fn get_matches(config: &Config) -> crate::Result<Matches> {
let cli = config
.tauri
.cli

View File

@ -3,8 +3,6 @@
/// The Command API module allows you to manage child processes.
pub mod command;
/// The Config module allows you to read the configuration from `tauri.conf.json`.
pub mod config;
/// The Dialog API module allows you to show messages and prompt for file paths.
pub mod dialog;
/// The Dir module is a helper for file system directory management.
@ -22,6 +20,9 @@ pub mod tcp;
/// The semver API.
pub mod version;
/// The Tauri config definition.
pub use tauri_utils::config;
/// The CLI args interface.
#[cfg(feature = "cli")]
pub mod cli;
@ -61,3 +62,15 @@ pub enum Error {
#[error("Network Error:{0}")]
Network(attohttpc::StatusCode),
}
// Not public API
#[doc(hidden)]
pub mod private {
pub trait AsTauriContext {
fn config_path() -> &'static std::path::Path;
fn raw_config() -> &'static str;
fn assets() -> &'static crate::assets::Assets;
fn raw_index() -> &'static str;
fn raw_tauri_script() -> &'static str;
}
}

View File

@ -1,6 +1,4 @@
#[cfg(windows)]
use crate::config::get as get_config;
#[cfg(windows)]
use std::path::MAIN_SEPARATOR;
/// The Notification definition.
@ -10,7 +8,7 @@ use std::path::MAIN_SEPARATOR;
/// ```
/// use tauri_api::notification::Notification;
/// // shows a notification with the given title and body
/// Notification::new()
/// Notification::new("studio.tauri.example")
/// .title("New message")
/// .body("You've got a new message.")
/// .show();
@ -24,12 +22,17 @@ pub struct Notification {
title: Option<String>,
/// The notification icon.
icon: Option<String>,
/// The notification identifier
identifier: String,
}
impl Notification {
/// Initializes a instance of a Notification.
pub fn new() -> Self {
Default::default()
pub fn new(identifier: impl Into<String>) -> Self {
Self {
identifier: identifier.into(),
..Default::default()
}
}
/// Sets the notification body.
@ -71,9 +74,7 @@ impl Notification {
if !(curr_dir.ends_with(format!("{S}target{S}debug", S = MAIN_SEPARATOR).as_str())
|| curr_dir.ends_with(format!("{S}target{S}release", S = MAIN_SEPARATOR).as_str()))
{
let config = get_config()?;
let identifier = config.tauri.bundle.identifier.clone();
notification.app_id(&identifier);
notification.app_id(&self.identifier);
}
}
notification

23
tauri-macros/Cargo.toml Normal file
View File

@ -0,0 +1,23 @@
[package]
name = "tauri-macros"
version = "0.1.0"
authors = [ "Tauri Community" ]
categories = [ "gui", "os", "filesystem", "web-programming" ]
license = "MIT"
homepage = "https://tauri.studio"
repository = "https://github.com/tauri-apps/tauri"
description = "Macros for the tauri crate."
edition = "2018"
[lib]
proc-macro = true
[dependencies]
flate2 = "1"
proc-macro2 = "1"
quote = "1"
serde = {version = "1", features = ["derive"]}
serde_json = "1"
syn = { version = "1", features = ["extra-traits"] }
tauri-utils = { version = "0.5", path = "../tauri-utils" }
walkdir = "2"

75
tauri-macros/src/error.rs Normal file
View File

@ -0,0 +1,75 @@
use proc_macro2::{Ident, TokenStream};
use quote::quote;
use std::io::Error as IoError;
use std::path::PathBuf;
use Error::*;
pub(crate) enum Error {
EnvOutDir,
EnvCargoManifestDir,
IncludeDirPrefix,
IncludeDirCacheDir,
IncludeDirEmptyFilename,
ConfigDir,
Serde(PathBuf, serde_json::Error),
Io(PathBuf, IoError),
}
impl Error {
/// Output a compiler error to the ast being transformed
pub(crate) fn into_compile_error(self, struct_: &Ident) -> TokenStream {
let error: String = match self {
EnvOutDir => "Unable to find OUT_DIR environmental variable from tauri-macros".into(),
EnvCargoManifestDir => {
"Unable to find CARGO_MANIFEST_DIR environmental variable from tauri-macros".into()
}
IncludeDirPrefix => "Invalid directory prefix encountered while including assets".into(),
IncludeDirCacheDir => {
"Unable to find cache directory to compress assets into during tauri-macros".into()
}
IncludeDirEmptyFilename => "Asset included during tauri-macros has empty filename".into(),
ConfigDir => {
"Unable to get the directory the config file was found in during tauri-macros".into()
}
Serde(path, error) => format!(
"{:?} encountered for {} during tauri-macros",
error,
path.display()
),
Io(path, error) => format!(
"{:?} encountered for {} during tauri-macros",
error.kind(),
path.display()
),
};
quote! {
compile_error!(#error);
impl ::tauri::api::private::AsTauriContext for #struct_ {
fn config_path() -> &'static std::path::Path {
unimplemented!()
}
/// Make the file a dependency for the compiler
fn raw_config() -> &'static str {
unimplemented!()
}
fn assets() -> &'static ::tauri::api::assets::Assets {
unimplemented!()
}
/// Make the index.tauri.html a dependency for the compiler
fn raw_index() -> &'static str {
unimplemented!()
}
/// Make the __tauri.js a dependency for the compiler
fn raw_tauri_script() -> &'static str {
unimplemented!()
}
}
}
}
}

120
tauri-macros/src/expand.rs Normal file
View File

@ -0,0 +1,120 @@
use crate::error::Error;
use crate::include_dir::IncludeDir;
use crate::DEFAULT_CONFIG_FILE;
use proc_macro2::TokenStream;
use quote::quote;
use std::collections::HashSet;
use std::env::var;
use std::fs::File;
use std::io::BufReader;
use std::path::{Path, PathBuf};
use syn::{DeriveInput, Lit::Str, Meta::NameValue, MetaNameValue};
use tauri_utils::{assets::AssetCompression, config::Config};
pub(crate) fn load_context(input: DeriveInput) -> Result<TokenStream, Error> {
let name = input.ident;
// quick way of parsing #[config_path = "path_goes_here"]
let mut config_file_path = DEFAULT_CONFIG_FILE.into();
let config_path_attr = input
.attrs
.iter()
.find(|attr| attr.path.is_ident("config_path"));
if let Some(attr) = config_path_attr {
if let Ok(meta) = attr.parse_meta() {
if let NameValue(MetaNameValue { lit: Str(path), .. }) = meta {
config_file_path = path.value()
}
}
}
// grab the manifest of the application the macro is in
let manifest = var("CARGO_MANIFEST_DIR")
.map(PathBuf::from)
.map_err(|_| Error::EnvCargoManifestDir)?;
let full_config_path = Path::new(&manifest).join(config_file_path);
let config = get_config(&full_config_path)?;
let config_dir = full_config_path.parent().ok_or(Error::ConfigDir)?;
let dist_dir = config_dir.join(config.build.dist_dir);
// generate the assets into a perfect hash function
let assets = generate_asset_map(&dist_dir)?;
// should be possible to do the index.tauri.hmtl manipulations during this macro too in the future
let tauri_index_html_path = dist_dir.join("index.tauri.html");
let tauri_script_path = dist_dir.join("__tauri.js");
// format paths into a string to use them in quote!
let tauri_config_path = full_config_path.display().to_string();
let tauri_index_html_path = tauri_index_html_path.display().to_string();
let tauri_script_path = tauri_script_path.display().to_string();
Ok(quote! {
impl ::tauri::api::private::AsTauriContext for #name {
fn config_path() -> &'static std::path::Path {
std::path::Path::new(#tauri_config_path)
}
/// Make the file a dependency for the compiler
fn raw_config() -> &'static str {
include_str!(#tauri_config_path)
}
fn assets() -> &'static ::tauri::api::assets::Assets {
use ::tauri::api::assets::{Assets, AssetCompression, phf, phf::phf_map};
static ASSETS: Assets = Assets::new(#assets);
&ASSETS
}
/// Make the index.tauri.html a dependency for the compiler
fn raw_index() -> &'static str {
include_str!(#tauri_index_html_path)
}
/// Make the __tauri.js a dependency for the compiler
fn raw_tauri_script() -> &'static str {
include_str!(#tauri_script_path)
}
}
})
}
fn get_config(path: &Path) -> Result<Config, Error> {
match var("TAURI_CONFIG") {
Ok(custom_config) => {
serde_json::from_str(&custom_config).map_err(|e| Error::Serde("TAURI_CONFIG".into(), e))
}
Err(_) => {
let file = File::open(&path).map_err(|e| Error::Io(path.into(), e))?;
let reader = BufReader::new(file);
serde_json::from_reader(reader).map_err(|e| Error::Serde(path.into(), e))
}
}
}
/// Generates a perfect hash function from `phf` of the assets in dist directory
///
/// The `TokenStream` produced by this function expects to have `phf` and
/// `phf_map` paths available. Make sure to `use` these so the macro has access to them.
/// It also expects `AssetCompression` to be in path.
fn generate_asset_map(dist: &Path) -> Result<TokenStream, Error> {
let mut inline_assets = HashSet::new();
if let Ok(assets) = std::env::var("TAURI_INLINED_ASSETS") {
assets
.split('|')
.filter(|&s| !s.trim().is_empty())
.map(PathBuf::from)
.for_each(|path| {
inline_assets.insert(path);
})
}
// the index.html is parsed so we always ignore it
inline_assets.insert("/index.html".into());
IncludeDir::new(&dist)
.dir(&dist, AssetCompression::Gzip)?
.set_filter(inline_assets)?
.build()
}

View File

@ -0,0 +1,164 @@
use crate::error::Error;
use flate2::bufread::GzEncoder;
use proc_macro2::TokenStream;
use quote::quote;
use quote::TokenStreamExt;
use std::collections::{HashMap, HashSet};
use std::env::var;
use std::fs::{canonicalize, create_dir_all, File};
use std::io::{BufReader, BufWriter};
use std::path::{Path, PathBuf};
use tauri_utils::assets::{AssetCompression, Assets};
use walkdir::WalkDir;
enum Asset {
Identity(PathBuf),
Compressed(PathBuf, PathBuf),
}
pub(crate) struct IncludeDir {
assets: HashMap<String, Asset>,
filter: HashSet<String>,
prefix: PathBuf,
}
impl IncludeDir {
pub fn new(prefix: impl Into<PathBuf>) -> Self {
Self {
assets: HashMap::new(),
filter: HashSet::new(),
prefix: prefix.into(),
}
}
/// get a relative path based on the `IncludeDir`'s prefix
fn relative<'p>(&self, path: &'p Path) -> Result<&'p Path, Error> {
path
.strip_prefix(&self.prefix)
.map_err(|_| Error::IncludeDirPrefix)
}
pub fn file(mut self, path: impl Into<PathBuf>, comp: AssetCompression) -> Result<Self, Error> {
let path = path.into();
let relative = self.relative(&path)?;
let key = Assets::format_key(&relative);
let asset = match comp {
AssetCompression::None => Asset::Identity(path),
AssetCompression::Gzip => {
let cache = var("OUT_DIR")
.map_err(|_| Error::EnvOutDir)
.and_then(|out| canonicalize(&out).map_err(|e| Error::Io(PathBuf::from(out), e)))
.map(|out| out.join(".tauri-assets"))?;
// normalize path separators
let relative: PathBuf = relative.components().collect();
let cache = cache.join(relative);
// append .br extension to filename
let filename = cache.file_name().ok_or(Error::IncludeDirEmptyFilename)?;
let filename = format!("{}.br", filename.to_string_lossy());
// remove filename from cache
let cache = cache.parent().ok_or(Error::IncludeDirCacheDir)?;
// append the filename to the canonical path
let cache_file = cache.join(filename);
// make sure the cache directory is created
create_dir_all(&cache).map_err(|e| Error::Io(cache.to_path_buf(), e))?;
// open original asset path
let reader = File::open(&path).map_err(|e| Error::Io(path.to_path_buf(), e))?;
let reader = BufReader::new(reader);
let mut reader = GzEncoder::new(reader, flate2::Compression::best());
// open cache path
let writer =
File::create(&cache_file).map_err(|e| Error::Io(cache_file.to_path_buf(), e))?;
let mut writer = BufWriter::new(writer);
std::io::copy(&mut reader, &mut writer).map_err(|e| Error::Io(path.to_path_buf(), e))?;
Asset::Compressed(path, cache_file)
}
};
self.assets.insert(key, asset);
Ok(self)
}
pub fn dir(mut self, path: impl AsRef<Path>, comp: AssetCompression) -> Result<Self, Error> {
let path = path.as_ref();
let walker = WalkDir::new(&path).follow_links(true);
for entry in walker.into_iter() {
match entry {
Ok(e) => {
if !e.file_type().is_dir() {
self = self.file(e.path(), comp)?
}
}
Err(e) => return Err(Error::Io(path.into(), e.into())),
}
}
Ok(self)
}
/// Set list of files to not embed. Paths should be relative to the dist dir
pub fn set_filter(mut self, filter: HashSet<PathBuf>) -> Result<Self, Error> {
self.filter = filter
.iter()
.map(|path| {
let path = if path.starts_with(&self.prefix) {
self.relative(path)?
} else {
&path
};
Ok(Assets::format_key(path))
})
.collect::<Result<_, _>>()?;
Ok(self)
}
pub fn build(self) -> Result<TokenStream, Error> {
let mut matches = TokenStream::new();
for (key, asset) in self.assets {
if self.filter.contains(&key) {
continue;
}
let value = match asset {
Asset::Identity(path) => {
let path = path.display().to_string();
quote! {
(AssetCompression::None, include_bytes!(#path))
}
}
Asset::Compressed(path, cache) => {
let path = path.display().to_string();
let cache = cache.display().to_string();
quote! {
{
// make compiler check asset file for re-run.
// rely on dead code elimination to remove it from target binary
const _: &[u8] = include_bytes!(#path);
(AssetCompression::Gzip, include_bytes!(#cache))
}
}
}
};
matches.append_all(quote! {
#key => #value,
})
}
Ok(quote! {
phf_map! {
#matches
}
})
}
}

19
tauri-macros/src/lib.rs Normal file
View File

@ -0,0 +1,19 @@
extern crate proc_macro;
use proc_macro::TokenStream;
use syn::{parse_macro_input, DeriveInput};
mod error;
mod expand;
mod include_dir;
const DEFAULT_CONFIG_FILE: &str = "tauri.conf.json";
#[proc_macro_derive(FromTauriContext, attributes(config_path))]
pub fn load_context(ast: TokenStream) -> TokenStream {
let input = parse_macro_input!(ast as DeriveInput);
let name = input.ident.clone();
expand::load_context(input)
.unwrap_or_else(|e| e.into_compile_error(&name))
.into()
}

View File

@ -8,8 +8,11 @@ repository = "https://github.com/tauri-apps/tauri"
description = "Utilities for Tauri"
edition = "2018"
[dependencies]
serde = "1.0"
serde_json = "1.0"
sysinfo = "0.10"
anyhow = "1.0.31"
thiserror = "1.0.19"
thiserror = "1.0.19"
phf = { version = "0.8", features = ["macros"] }
flate2 = "1"

108
tauri-utils/src/assets.rs Normal file
View File

@ -0,0 +1,108 @@
//! Assets handled by Tauri during compile time and runtime.
use flate2::read::{GzDecoder, GzEncoder};
pub use phf;
use std::io::Read;
use std::path::{Component, Path, PathBuf};
/// Type of compression applied to an asset
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum AssetCompression {
/// No compression applied
None,
/// Compressed with (gzip)[https://crates.io/crates/flate2]
Gzip,
}
/// How the embedded asset should be fetched from `Assets`
pub enum AssetFetch {
/// Do not modify the compression
Identity,
/// Ensure asset is decompressed
Decompress,
/// Ensure asset is compressed
Compress,
}
/// Runtime access to the included files
pub struct Assets {
inner: phf::Map<&'static str, (AssetCompression, &'static [u8])>,
}
impl Assets {
/// Create `Assets` container from `phf::Map`
pub const fn new(map: phf::Map<&'static str, (AssetCompression, &'static [u8])>) -> Self {
Self { inner: map }
}
/// Format a key used to identify a file embedded in `Assets`.
///
/// Output should use unix path separators and have a root directory to mimic
/// server urls.
pub fn format_key(path: impl Into<PathBuf>) -> String {
let path = path.into();
// add in root to mimic how it is used from a server url
let path = if path.has_root() {
path
} else {
Path::new(&Component::RootDir).join(path)
};
if cfg!(windows) {
let mut buf = String::new();
for component in path.components() {
match component {
Component::RootDir => buf.push('/'),
Component::CurDir => buf.push_str("./"),
Component::ParentDir => buf.push_str("../"),
Component::Prefix(prefix) => buf.push_str(&prefix.as_os_str().to_string_lossy()),
Component::Normal(s) => {
buf.push_str(&s.to_string_lossy());
buf.push('/')
}
}
}
// remove the last slash
if buf != "/" {
buf.pop();
}
buf
} else {
path.to_string_lossy().to_string()
}
}
/// Get embedded asset, automatically handling compression.
pub fn get(
&self,
path: impl Into<PathBuf>,
fetch: AssetFetch,
) -> Option<(Box<dyn Read>, AssetCompression)> {
use self::{AssetCompression::*, AssetFetch::*};
let key = Self::format_key(path);
let &(compression, content) = self.inner.get(&*key)?;
Some(match (compression, fetch) {
// content is already in compression format expected
(_, Identity) | (None, Decompress) | (Gzip, Compress) => (Box::new(content), compression),
// content is uncompressed, but fetched with compression
(None, Compress) => {
let compressor = GzEncoder::new(content, flate2::Compression::new(6));
(Box::new(compressor), Gzip)
}
// content is compressed, but fetched with decompression
(Gzip, Decompress) => {
let decompressor = GzDecoder::new(content);
(Box::new(decompressor), None)
}
})
}
}

View File

@ -2,11 +2,8 @@ use serde::de::{Deserializer, Error as DeError, Visitor};
use serde::Deserialize;
use serde_json::Value as JsonValue;
use once_cell::sync::OnceCell;
use std::collections::HashMap;
static CONFIG: OnceCell<Config> = OnceCell::new();
/// The window configuration object.
#[derive(PartialEq, Deserialize, Debug)]
#[serde(tag = "window", rename_all = "camelCase")]
@ -44,13 +41,15 @@ fn default_title() -> String {
"Tauri App".to_string()
}
fn default_window() -> WindowConfig {
WindowConfig {
width: default_width(),
height: default_height(),
resizable: default_resizable(),
title: default_title(),
fullscreen: false,
impl Default for WindowConfig {
fn default() -> Self {
Self {
width: default_width(),
height: default_height(),
resizable: default_resizable(),
title: default_title(),
fullscreen: false,
}
}
}
@ -74,12 +73,38 @@ pub struct EmbeddedServerConfig {
/// If it's `random`, we'll generate one at runtime.
#[serde(default = "default_port", deserialize_with = "port_deserializer")]
pub port: Port,
/// The base path of the embedded server.
/// The path should always start and end in a forward slash, which the deserializer will ensure
#[serde(
default = "default_public_path",
deserialize_with = "public_path_deserializer"
)]
pub public_path: String,
}
fn default_host() -> String {
"http://127.0.0.1".to_string()
}
fn default_port() -> Port {
Port::Random
}
fn default_public_path() -> String {
"/".to_string()
}
impl Default for EmbeddedServerConfig {
fn default() -> Self {
Self {
host: default_host(),
port: default_port(),
public_path: default_public_path(),
}
}
}
fn port_deserializer<'de, D>(deserializer: D) -> Result<Port, D::Error>
where
D: Deserializer<'de>,
@ -116,15 +141,45 @@ where
deserializer.deserialize_any(PortDeserializer {})
}
fn default_port() -> Port {
Port::Random
}
fn public_path_deserializer<'de, D>(deserializer: D) -> Result<String, D::Error>
where
D: Deserializer<'de>,
{
struct PublicPathDeserializer;
fn default_embedded_server() -> EmbeddedServerConfig {
EmbeddedServerConfig {
host: default_host(),
port: default_port(),
impl<'de> Visitor<'de> for PublicPathDeserializer {
type Value = String;
fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter.write_str("a string starting and ending in a forward slash /")
}
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
where
E: DeError,
{
match value.len() {
0 => return Ok("/".into()),
1 if value == "/" => return Ok("/".into()),
1 => return Ok(format!("/{}/", value)),
_ => {}
}
// we know there are at least 2 characters in the string
let mut chars = value.chars();
let first = chars.next().unwrap();
let last = chars.last().unwrap();
match (first == '/', last == '/') {
(true, true) => Ok(value.into()),
(true, false) => Ok(format!("{}/", value)),
(false, true) => Ok(format!("/{}", value)),
_ => Ok(format!("/{}/", value)),
}
}
}
deserializer.deserialize_any(PublicPathDeserializer {})
}
/// A CLI argument definition
@ -270,9 +325,11 @@ pub struct BundleConfig {
pub identifier: String,
}
fn default_bundle() -> BundleConfig {
BundleConfig {
identifier: String::from(""),
impl Default for BundleConfig {
fn default() -> Self {
Self {
identifier: String::from(""),
}
}
}
@ -281,19 +338,30 @@ fn default_bundle() -> BundleConfig {
#[serde(tag = "tauri", rename_all = "camelCase")]
pub struct TauriConfig {
/// The window configuration.
#[serde(default = "default_window")]
#[serde(default)]
pub window: WindowConfig,
/// The embeddedServer configuration.
#[serde(default = "default_embedded_server")]
#[serde(default)]
pub embedded_server: EmbeddedServerConfig,
/// The CLI configuration.
#[serde(default)]
pub cli: Option<CliConfig>,
/// The bundler configuration.
#[serde(default = "default_bundle")]
#[serde(default)]
pub bundle: BundleConfig,
}
impl Default for TauriConfig {
fn default() -> Self {
Self {
window: WindowConfig::default(),
embedded_server: EmbeddedServerConfig::default(),
cli: None,
bundle: BundleConfig::default(),
}
}
}
/// The Build configuration object.
#[derive(PartialEq, Deserialize, Debug)]
#[serde(tag = "build", rename_all = "camelCase")]
@ -301,11 +369,26 @@ pub struct BuildConfig {
/// the devPath config.
#[serde(default = "default_dev_path")]
pub dev_path: String,
/// the dist config.
#[serde(default = "default_dist_path")]
pub dist_dir: String,
}
fn default_dev_path() -> String {
"".to_string()
}
fn default_dist_path() -> String {
"../dist".to_string()
}
impl Default for BuildConfig {
fn default() -> Self {
Self {
dev_path: default_dev_path(),
dist_dir: default_dist_path(),
}
}
}
type JsonObject = HashMap<String, JsonValue>;
@ -314,10 +397,10 @@ type JsonObject = HashMap<String, JsonValue>;
#[serde(rename_all = "camelCase")]
pub struct Config {
/// The Tauri configuration.
#[serde(default = "default_tauri")]
#[serde(default)]
pub tauri: TauriConfig,
/// The build configuration.
#[serde(default = "default_build")]
#[serde(default)]
pub build: BuildConfig,
/// The plugins config.
#[serde(default)]
@ -331,160 +414,29 @@ impl Config {
}
}
fn default_tauri() -> TauriConfig {
TauriConfig {
window: default_window(),
embedded_server: default_embedded_server(),
cli: None,
bundle: default_bundle(),
}
}
fn default_build() -> BuildConfig {
BuildConfig {
dev_path: default_dev_path(),
}
}
/// Gets the static parsed config from `tauri.conf.json`.
pub fn get() -> crate::Result<&'static Config> {
if let Some(config) = CONFIG.get() {
return Ok(config);
}
let config: Config = match option_env!("TAURI_CONFIG") {
Some(config) => serde_json::from_str(config).expect("failed to parse TAURI_CONFIG env"),
None => {
let config = include_str!(concat!(env!("OUT_DIR"), "/tauri.conf.json"));
serde_json::from_str(&config).expect("failed to read tauri.conf.json")
}
};
CONFIG
.set(config)
.map_err(|_| anyhow::anyhow!("failed to set CONFIG"))?;
let config = CONFIG.get().unwrap();
Ok(config)
}
#[cfg(test)]
mod test {
use super::*;
// generate a test_config based on the test fixture
fn create_test_config() -> Config {
let mut subcommands = std::collections::HashMap::new();
subcommands.insert(
"update".to_string(),
CliConfig {
description: Some("Updates the app".to_string()),
long_description: None,
before_help: None,
after_help: None,
args: Some(vec![CliArg {
short: Some('b'),
name: "background".to_string(),
description: Some("Update in background".to_string()),
..Default::default()
}]),
subcommands: None,
},
);
Config {
tauri: TauriConfig {
window: WindowConfig {
width: 800,
height: 600,
resizable: true,
title: String::from("Tauri API Validation"),
fullscreen: false,
},
embedded_server: EmbeddedServerConfig {
host: String::from("http://127.0.0.1"),
port: Port::Random,
},
bundle: BundleConfig {
identifier: String::from("com.tauri.communication"),
},
cli: Some(CliConfig {
description: Some("Tauri communication example".to_string()),
long_description: None,
before_help: None,
after_help: None,
args: Some(vec![
CliArg {
short: Some('c'),
name: "config".to_string(),
takes_value: Some(true),
description: Some("Config path".to_string()),
..Default::default()
},
CliArg {
short: Some('t'),
name: "theme".to_string(),
takes_value: Some(true),
description: Some("App theme".to_string()),
possible_values: Some(vec![
"light".to_string(),
"dark".to_string(),
"system".to_string(),
]),
..Default::default()
},
CliArg {
short: Some('v'),
name: "verbose".to_string(),
multiple_occurrences: Some(true),
description: Some("Verbosity level".to_string()),
..Default::default()
},
]),
subcommands: Some(subcommands),
}),
},
build: BuildConfig {
dev_path: String::from("../dist"),
},
plugins: Default::default(),
}
}
#[test]
// test the get function. Will only resolve to true if the TAURI_CONFIG variable is set properly to the fixture.
fn test_get() {
// get test_config
let test_config = create_test_config();
// call get();
let config = get();
// check to see if there is an OK or Err, on Err fail test.
match config {
// On Ok, check that the config is the same as the test config.
Ok(c) => {
println!("{:?}", c);
assert_eq!(c, &test_config)
}
Err(e) => panic!("get config failed: {:?}", e.to_string()),
}
}
// TODO: create a test that compares a config to a json config
#[test]
// test all of the default functions
fn test_defaults() {
// get default tauri config
let t_config = default_tauri();
let t_config = TauriConfig::default();
// get default build config
let b_config = default_build();
let b_config = BuildConfig::default();
// get default dev path
let d_path = default_dev_path();
// get default embedded server
let de_server = default_embedded_server();
let de_server = EmbeddedServerConfig::default();
// get default window
let d_window = default_window();
let d_window = WindowConfig::default();
// get default title
let d_title = default_title();
// get default bundle
let d_bundle = default_bundle();
let d_bundle = BundleConfig::default();
// create a tauri config.
let tauri = TauriConfig {
@ -498,6 +450,7 @@ mod test {
embedded_server: EmbeddedServerConfig {
host: String::from("http://127.0.0.1"),
port: Port::Random,
public_path: "/".into(),
},
bundle: BundleConfig {
identifier: String::from(""),
@ -508,6 +461,7 @@ mod test {
// create a build config
let build = BuildConfig {
dev_path: String::from(""),
dist_dir: String::from("../dist"),
};
// test the configs

View File

@ -1,6 +1,10 @@
//! Tauri utility helpers
#![warn(missing_docs, rust_2018_idioms)]
/// The Assets module allows you to read files that have been bundled by tauri
pub mod assets;
/// Tauri config definition.
pub mod config;
/// Platform helpers
pub mod platform;
/// Process helpers

View File

@ -20,8 +20,6 @@ features = [ "all-api" ]
[dependencies]
serde_json = "1.0"
serde = { version = "1.0", features = [ "derive" ] }
tauri_includedir = "0.6.0"
phf = "0.8.0"
base64 = "0.13.0"
webbrowser = "0.5.5"
lazy_static = "1.4.0"
@ -34,6 +32,7 @@ anyhow = "1.0.38"
thiserror = "1.0.23"
once_cell = "1.5.2"
tauri-api = { version = "0.7.5", path = "../tauri-api" }
tauri-macros = { version = "0.1", path = "../tauri-macros" }
urlencoding = "1.1.1"
wry = { git = "https://github.com/tauri-apps/wry", rev = "42f4f2133f7921ed5adc47908787094da8abeac5" }
@ -41,7 +40,6 @@ wry = { git = "https://github.com/tauri-apps/wry", rev = "42f4f2133f7921ed5adc47
runas = "0.2"
[build-dependencies]
tauri_includedir_codegen = "0.6.2"
cfg_aliases = "0.1.1"
[dev-dependencies]

View File

@ -1,126 +1,6 @@
use cfg_aliases::cfg_aliases;
use std::{
env,
error::Error,
fs::{read_to_string, File},
io::{BufWriter, Write},
path::Path,
};
#[cfg(any(feature = "embedded-server", feature = "no-server"))]
pub fn main() -> Result<(), Box<dyn Error>> {
shared()?;
let out_dir = env::var("OUT_DIR")?;
let dest_index_html_path = Path::new(&out_dir).join("index.tauri.html");
let mut index_html_file = BufWriter::new(File::create(&dest_index_html_path)?);
match env::var_os("TAURI_DIST_DIR") {
Some(dist_path) => {
let dist_path_string = dist_path.into_string().unwrap();
let dist_path = Path::new(&dist_path_string);
println!("cargo:rerun-if-changed={}", dist_path_string);
let mut inlined_assets = match std::env::var_os("TAURI_INLINED_ASSETS") {
Some(assets) => assets
.into_string()
.unwrap()
.split('|')
.map(|s| s.to_string())
.filter(|s| !s.is_empty())
.collect(),
None => Vec::new(),
};
// the index.html is parsed so we always ignore it
inlined_assets.push(
dist_path
.join("index.html")
.into_os_string()
.into_string()
.expect("failed to convert dist path to string"),
);
if cfg!(feature = "no-server") {
// on no-server we include_str() the index.tauri.html on the runner
inlined_assets.push(
dist_path
.join("index.tauri.html")
.into_os_string()
.into_string()
.expect("failed to convert dist path to string"),
);
}
// include assets
tauri_includedir_codegen::start("ASSETS")
.dir(
dist_path_string.clone(),
tauri_includedir_codegen::Compression::None,
)
.build("data.rs", inlined_assets)
.expect("failed to build data.rs");
write!(
index_html_file,
"{}",
read_to_string(dist_path.join("index.tauri.html"))?
)?;
}
None => {
// dummy assets
tauri_includedir_codegen::start("ASSETS")
.dir("".to_string(), tauri_includedir_codegen::Compression::None)
.build("data.rs", vec![])
.expect("failed to build data.rs");
write!(
index_html_file,
"<html><body>Build error: Couldn't find ENV: TAURI_DIST_DIR</body></html>"
)?;
println!("Build error: Couldn't find ENV: TAURI_DIST_DIR");
}
}
Ok(())
}
#[cfg(not(any(feature = "embedded-server", feature = "no-server")))]
pub fn main() -> Result<(), Box<dyn Error>> {
shared()
}
fn shared() -> Result<(), Box<dyn Error>> {
let out_dir = env::var("OUT_DIR")?;
let dest_tauri_script_path = Path::new(&out_dir).join("__tauri.js");
let mut tauri_script_file = BufWriter::new(File::create(&dest_tauri_script_path)?);
match env::var_os("TAURI_DIST_DIR") {
Some(dist_path) => {
let dist_path_string = dist_path.into_string().unwrap();
let dist_path = Path::new(&dist_path_string);
write!(
tauri_script_file,
"{}",
read_to_string(dist_path.join("__tauri.js"))?
)?;
}
None => {
write!(
tauri_script_file,
r#"console.warning("Couldn't find ENV: TAURI_DIST_DIR, the Tauri API won't work. Please rebuild with the Tauri CLI.")"#,
)?;
}
}
setup_env_aliases();
Ok(())
}
#[allow(clippy::cognitive_complexity)]
fn setup_env_aliases() {
fn main() {
cfg_aliases! {
embedded_server: { feature = "embedded-server" },
no_server: { feature = "no-server" },

View File

@ -1717,17 +1717,9 @@ version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12"
dependencies = [
"phf_macros",
"phf_shared",
]
[[package]]
name = "phf_codegen"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cbffee61585b0411840d3ece935cce9cb6321f01c45477d30066498cd5e1a815"
dependencies = [
"phf_generator",
"phf_shared",
"proc-macro-hack",
]
[[package]]
@ -1740,6 +1732,20 @@ dependencies = [
"rand 0.7.3",
]
[[package]]
name = "phf_macros"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f6fde18ff429ffc8fe78e2bf7f8b7a5a5a6e2a8b58bc5a9ac69198bbda9189c"
dependencies = [
"phf_generator",
"phf_shared",
"proc-macro-hack",
"proc-macro2",
"quote 1.0.8",
"syn 1.0.60",
]
[[package]]
name = "phf_shared"
version = "0.8.0"
@ -2375,13 +2381,11 @@ dependencies = [
"futures",
"lazy_static",
"once_cell",
"phf",
"runas",
"serde",
"serde_json",
"tauri-api",
"tauri_includedir",
"tauri_includedir_codegen",
"tauri-macros",
"thiserror",
"tiny_http",
"tokio",
@ -2405,6 +2409,7 @@ dependencies = [
"nfd",
"notify-rust",
"once_cell",
"phf",
"rand 0.8.3",
"semver",
"serde",
@ -2437,6 +2442,20 @@ dependencies = [
"pkg-config",
]
[[package]]
name = "tauri-macros"
version = "0.1.0"
dependencies = [
"flate2",
"proc-macro2",
"quote 1.0.8",
"serde",
"serde_json",
"syn 1.0.60",
"tauri-api",
"walkdir",
]
[[package]]
name = "tauri-utils"
version = "0.5.1"
@ -2446,27 +2465,6 @@ dependencies = [
"thiserror",
]
[[package]]
name = "tauri_includedir"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7a3644510b11203f42cf6e6384d5c46e0d20e426aa6e05b9110f5e4583a1032"
dependencies = [
"flate2",
"phf",
]
[[package]]
name = "tauri_includedir_codegen"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "afccce834cf6062a1a19d2d0018fd2a5a2d13d75f547ed0dac2cc3d9813b6ae7"
dependencies = [
"flate2",
"phf_codegen",
"walkdir",
]
[[package]]
name = "tempfile"
version = "3.2.0"

View File

@ -12,21 +12,24 @@ struct Reply {
data: String,
}
#[derive(tauri::FromTauriContext)]
struct Context;
fn main() {
tauri::AppBuilder::<tauri::flavors::Wry>::new()
.setup(|webview, _source| async move {
let mut webview = webview.clone();
tauri::AppBuilder::<tauri::flavors::Wry, Context>::new()
.setup(|dispatcher, _source| async move {
let mut dispatcher = dispatcher.clone();
tauri::event::listen(String::from("js-event"), move |msg| {
println!("got js-event with message '{:?}'", msg);
let reply = Reply {
data: "something else".to_string(),
};
tauri::event::emit(&mut webview, String::from("rust-event"), Some(reply))
tauri::event::emit(&mut dispatcher, String::from("rust-event"), Some(reply))
.expect("failed to emit");
});
})
.invoke_handler(|mut webview, arg| async move {
.invoke_handler(|mut dispatcher, arg| async move {
use cmd::Cmd::*;
match serde_json::from_str(&arg) {
Err(e) => Err(e.to_string()),
@ -64,5 +67,6 @@ fn main() {
}
})
.build()
.unwrap()
.run();
}

View File

@ -1717,17 +1717,9 @@ version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12"
dependencies = [
"phf_macros",
"phf_shared",
]
[[package]]
name = "phf_codegen"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cbffee61585b0411840d3ece935cce9cb6321f01c45477d30066498cd5e1a815"
dependencies = [
"phf_generator",
"phf_shared",
"proc-macro-hack",
]
[[package]]
@ -1740,6 +1732,20 @@ dependencies = [
"rand 0.7.3",
]
[[package]]
name = "phf_macros"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f6fde18ff429ffc8fe78e2bf7f8b7a5a5a6e2a8b58bc5a9ac69198bbda9189c"
dependencies = [
"phf_generator",
"phf_shared",
"proc-macro-hack",
"proc-macro2",
"quote 1.0.8",
"syn 1.0.60",
]
[[package]]
name = "phf_shared"
version = "0.8.0"
@ -2375,13 +2381,11 @@ dependencies = [
"futures",
"lazy_static",
"once_cell",
"phf",
"runas",
"serde",
"serde_json",
"tauri-api",
"tauri_includedir",
"tauri_includedir_codegen",
"tauri-macros",
"thiserror",
"tiny_http",
"tokio",
@ -2405,6 +2409,7 @@ dependencies = [
"nfd",
"notify-rust",
"once_cell",
"phf",
"rand 0.8.3",
"semver",
"serde",
@ -2437,6 +2442,20 @@ dependencies = [
"pkg-config",
]
[[package]]
name = "tauri-macros"
version = "0.1.0"
dependencies = [
"flate2",
"proc-macro2",
"quote 1.0.8",
"serde",
"serde_json",
"syn 1.0.60",
"tauri-api",
"walkdir",
]
[[package]]
name = "tauri-utils"
version = "0.5.1"
@ -2446,27 +2465,6 @@ dependencies = [
"thiserror",
]
[[package]]
name = "tauri_includedir"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7a3644510b11203f42cf6e6384d5c46e0d20e426aa6e05b9110f5e4583a1032"
dependencies = [
"flate2",
"phf",
]
[[package]]
name = "tauri_includedir_codegen"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "afccce834cf6062a1a19d2d0018fd2a5a2d13d75f547ed0dac2cc3d9813b6ae7"
dependencies = [
"flate2",
"phf_codegen",
"walkdir",
]
[[package]]
name = "tempfile"
version = "3.2.0"

View File

@ -12,21 +12,25 @@ struct Reply {
data: String,
}
#[derive(tauri::FromTauriContext)]
#[config_path = "examples/communication/src-tauri/tauri.conf.json"]
struct Context;
fn main() {
tauri::AppBuilder::<tauri::flavors::Wry>::new()
.setup(|webview, _source| async move {
let mut webview = webview.clone();
tauri::AppBuilder::<tauri::flavors::Wry, Context>::new()
.setup(|dispatcher, _source| async move {
let mut dispatcher = dispatcher.clone();
tauri::event::listen(String::from("js-event"), move |msg| {
println!("got js-event with message '{:?}'", msg);
let reply = Reply {
data: "something else".to_string(),
};
tauri::event::emit(&mut webview, String::from("rust-event"), Some(reply))
tauri::event::emit(&mut dispatcher, String::from("rust-event"), Some(reply))
.expect("failed to emit");
});
})
.invoke_handler(|mut webview, arg| async move {
.invoke_handler(|mut dispatcher, arg| async move {
use cmd::Cmd::*;
match serde_json::from_str(&arg) {
Err(e) => Err(e.to_string()),
@ -44,7 +48,7 @@ fn main() {
// tauri::execute_promise is a helper for APIs that uses the tauri.promisified JS function
// so you can easily communicate between JS and Rust with promises
tauri::execute_promise(
&mut webview,
&mut dispatcher,
async move {
println!("{} {:?}", endpoint, body);
// perform an async operation here
@ -64,5 +68,6 @@ fn main() {
}
})
.build()
.unwrap()
.run();
}

View File

@ -1,11 +1,38 @@
use crate::ApplicationExt;
use futures::future::BoxFuture;
use std::marker::PhantomData;
use tauri_api::config::Config;
use tauri_api::private::AsTauriContext;
mod runner;
type InvokeHandler<W> = dyn Fn(W, String) -> BoxFuture<'static, Result<(), String>> + Send + Sync;
type Setup<W> = dyn Fn(W, String) -> BoxFuture<'static, ()> + Send + Sync;
/// `App` runtime information.
pub struct Context {
pub(crate) config: Config,
pub(crate) tauri_script: &'static str,
#[cfg(assets)]
pub(crate) assets: &'static tauri_api::assets::Assets,
#[cfg(any(dev, no_server))]
#[allow(dead_code)]
pub(crate) index: &'static str,
}
impl Context {
pub(crate) fn new<Context: AsTauriContext>() -> crate::Result<Self> {
Ok(Self {
config: serde_json::from_str(Context::raw_config())?,
tauri_script: Context::raw_tauri_script(),
#[cfg(assets)]
assets: Context::assets(),
#[cfg(any(dev, no_server))]
index: Context::raw_index(),
})
}
}
/// The application runner.
pub struct App<A: ApplicationExt> {
/// The JS message handler.
@ -14,6 +41,8 @@ pub struct App<A: ApplicationExt> {
setup: Option<Box<Setup<A::Dispatcher>>>,
/// The HTML of the splashscreen to render.
splashscreen_html: Option<String>,
/// The context the App was created with
pub(crate) context: Context,
}
impl<A: ApplicationExt + 'static> App<A> {
@ -54,22 +83,25 @@ impl<A: ApplicationExt + 'static> App<A> {
/// The App builder.
#[derive(Default)]
pub struct AppBuilder<A: ApplicationExt> {
pub struct AppBuilder<A: ApplicationExt, C: AsTauriContext> {
/// The JS message handler.
invoke_handler: Option<Box<InvokeHandler<A::Dispatcher>>>,
/// The setup callback, invoked when the webview is ready.
setup: Option<Box<Setup<A::Dispatcher>>>,
/// The HTML of the splashscreen to render.
splashscreen_html: Option<String>,
/// The configuration used
config: PhantomData<C>,
}
impl<A: ApplicationExt + 'static> AppBuilder<A> {
impl<A: ApplicationExt + 'static, C: AsTauriContext> AppBuilder<A, C> {
/// Creates a new App builder.
pub fn new() -> Self {
Self {
invoke_handler: None,
setup: None,
splashscreen_html: None,
config: Default::default(),
}
}
@ -117,11 +149,12 @@ impl<A: ApplicationExt + 'static> AppBuilder<A> {
}
/// Builds the App.
pub fn build(self) -> App<A> {
App {
pub fn build(self) -> crate::Result<App<A>> {
Ok(App {
invoke_handler: self.invoke_handler,
setup: self.setup,
splashscreen_html: self.splashscreen_html,
}
context: Context::new::<C>()?,
})
}
}

View File

@ -8,7 +8,7 @@ use crate::{ApplicationDispatcherExt, ApplicationExt, WebviewBuilderExt, WindowB
use super::App;
#[cfg(embedded_server)]
use crate::api::tcp::{get_available_port, port_is_available};
use tauri_api::config::get;
use crate::app::Context;
#[allow(dead_code)]
enum Content<T> {
@ -19,17 +19,21 @@ enum Content<T> {
/// Main entry point for running the Webview
pub(crate) fn run<A: ApplicationExt + 'static>(application: App<A>) -> crate::Result<()> {
// setup the content using the config struct depending on the compile target
let main_content = setup_content()?;
let main_content = setup_content(&application.context)?;
// setup the server url for the embedded-server
#[cfg(embedded_server)]
let server_url = {
if let Content::Url(ref url) = &main_content {
{
// setup the server url for the embedded-server
let server_url = if let Content::Url(url) = &main_content {
String::from(url)
} else {
String::from("")
}
};
};
// spawn the embedded server on our server url
#[cfg(embedded_server)]
spawn_server(server_url, &application.context)?;
}
let splashscreen_content = if application.splashscreen_html().is_some() {
Some(Content::Html(
@ -50,10 +54,6 @@ pub(crate) fn run<A: ApplicationExt + 'static>(application: App<A>) -> crate::Re
crate::plugin::created(A::plugin_store(), &mut dispatcher).await
});
// spawn the embedded server on our server url
#[cfg(embedded_server)]
spawn_server(server_url);
// spin up the updater process
#[cfg(feature = "updater")]
spawn_updater();
@ -65,14 +65,14 @@ pub(crate) fn run<A: ApplicationExt + 'static>(application: App<A>) -> crate::Re
}
#[cfg(all(embedded_server, no_server))]
fn setup_content() -> crate::Result<Content<String>> {
fn setup_content(_: &Context) -> crate::Result<Content<String>> {
panic!("only one of `embedded-server` and `no-server` is allowed")
}
// setup content for dev-server
#[cfg(dev)]
fn setup_content() -> crate::Result<Content<String>> {
let config = get()?;
fn setup_content(context: &Context) -> crate::Result<Content<String>> {
let config = &context.config;
if config.build.dev_path.starts_with("http") {
#[cfg(windows)]
{
@ -99,27 +99,19 @@ fn setup_content() -> crate::Result<Content<String>> {
}
Ok(Content::Url(config.build.dev_path.clone()))
} else {
let dev_dir = &config.build.dev_path;
let dev_path = std::path::Path::new(dev_dir).join("index.tauri.html");
if !dev_path.exists() {
panic!(
"Couldn't find 'index.tauri.html' inside {}; did you forget to run 'tauri dev'?",
dev_dir
);
}
Ok(Content::Html(format!(
"data:text/html,{}",
urlencoding::encode(&std::fs::read_to_string(dev_path)?)
urlencoding::encode(context.index)
)))
}
}
// setup content for embedded server
#[cfg(all(embedded_server, not(no_server)))]
fn setup_content() -> crate::Result<Content<String>> {
let (port, valid) = setup_port()?;
fn setup_content(context: &Context) -> crate::Result<Content<String>> {
let (port, valid) = setup_port(&context)?;
let url = (if valid {
setup_server_url(port)
setup_server_url(port, &context)
} else {
Err(anyhow::anyhow!("invalid port"))
})
@ -130,19 +122,18 @@ fn setup_content() -> crate::Result<Content<String>> {
// setup content for no-server
#[cfg(all(no_server, not(embedded_server)))]
fn setup_content() -> crate::Result<Content<String>> {
let html = include_str!(concat!(env!("OUT_DIR"), "/index.tauri.html"));
fn setup_content(context: &Context) -> crate::Result<Content<String>> {
Ok(Content::Html(format!(
"data:text/html,{}",
urlencoding::encode(html)
urlencoding::encode(context.index)
)))
}
// get the port for the embedded server
#[cfg(embedded_server)]
#[allow(dead_code)]
fn setup_port() -> crate::Result<(String, bool)> {
let config = get()?;
fn setup_port(context: &Context) -> crate::Result<(String, bool)> {
let config = &context.config;
match config.tauri.embedded_server.port {
tauri_api::config::Port::Random => match get_available_port() {
Some(available_port) => Ok((available_port.to_string(), true)),
@ -158,8 +149,8 @@ fn setup_port() -> crate::Result<(String, bool)> {
// setup the server url for embedded server
#[cfg(embedded_server)]
#[allow(dead_code)]
fn setup_server_url(port: String) -> crate::Result<String> {
let config = get()?;
fn setup_server_url(port: String, context: &Context) -> crate::Result<String> {
let config = &context.config;
let mut url = format!("{}:{}", config.tauri.embedded_server.host, port);
if !url.starts_with("http") {
url = format!("http://{}", url);
@ -169,21 +160,35 @@ fn setup_server_url(port: String) -> crate::Result<String> {
// spawn the embedded server
#[cfg(embedded_server)]
fn spawn_server(server_url: String) {
fn spawn_server(server_url: String, context: &Context) -> crate::Result<()> {
let assets = context.assets;
let public_path = context.config.tauri.embedded_server.public_path.clone();
std::thread::spawn(move || {
let server = tiny_http::Server::http(server_url.replace("http://", "").replace("https://", ""))
.expect("Unable to spawn server");
for request in server.incoming_requests() {
let url = match request.url() {
let url = request.url().replace(&server_url, "");
let url = match url.as_str() {
"/" => "/index.tauri.html",
url => url,
url => {
if url.starts_with(&public_path) {
&url[public_path.len() - 1..]
} else {
eprintln!(
"found url not matching public path.\nurl: {}\npublic path: {}",
url, public_path
);
url
}
}
}
.to_string();
request
.respond(crate::server::asset_response(&url))
.respond(crate::server::asset_response(&url, assets))
.expect("unable to setup response");
}
});
Ok(())
}
// spawn an updater process.
@ -242,7 +247,7 @@ fn build_webview<A: ApplicationExt + 'static>(
content: Content<String>,
splashscreen_content: Option<Content<String>>,
) -> crate::Result<(A, A::Dispatcher)> {
let config = get()?;
let config = &application.context.config;
// TODO let debug = cfg!(debug_assertions);
// get properties from config struct
// TODO let width = config.tauri.window.width;
@ -276,7 +281,7 @@ fn build_webview<A: ApplicationExt + 'static>(
}}
{plugin_init}
"#,
tauri_init = include_str!(concat!(env!("OUT_DIR"), "/__tauri.js")),
tauri_init = application.context.tauri_script,
event_init = init(),
plugin_init = crate::async_runtime::block_on(crate::plugin::init_script(A::plugin_store()))
);
@ -316,9 +321,10 @@ fn build_webview<A: ApplicationExt + 'static>(
} else if arg == r#"{"cmd":"closeSplashscreen"}"# {
dispatcher.eval(&format!(r#"window.location.href = "{}""#, content_url));
} else {
let mut endpoint_handle = crate::endpoints::handle(&mut dispatcher, &arg)
.await
.map_err(|e| e.to_string());
let mut endpoint_handle =
crate::endpoints::handle(&mut dispatcher, &arg, &application.context)
.await
.map_err(|e| e.to_string());
if let Err(ref tauri_handle_error) = endpoint_handle {
if tauri_handle_error.contains("unknown variant") {
let error = match application.run_invoke_handler(&mut dispatcher, &arg).await {
@ -388,21 +394,18 @@ fn get_api_error_message(arg: &str, handler_error_message: String) -> String {
#[cfg(test)]
mod test {
use super::Content;
use crate::Context;
use crate::FromTauriContext;
use proptest::prelude::*;
use std::env;
#[derive(FromTauriContext)]
#[config_path = "test/fixture/src-tauri/tauri.conf.json"]
struct TauriContext;
#[test]
fn check_setup_content() {
let tauri_dir = match option_env!("TAURI_DIR") {
Some(d) => d.to_string(),
None => env::current_dir()
.unwrap()
.into_os_string()
.into_string()
.expect("Unable to convert to normal String"),
};
env::set_current_dir(tauri_dir).expect("failed to change cwd");
let res = super::setup_content();
let context = Context::new::<TauriContext>().unwrap();
let res = super::setup_content(&context);
#[cfg(embedded_server)]
match res {
@ -413,23 +416,9 @@ mod test {
#[cfg(no_server)]
match res {
Ok(Content::Html(s)) => {
let dist_dir = match option_env!("TAURI_DIST_DIR") {
Some(d) => d.to_string(),
None => env::current_dir()
.unwrap()
.into_os_string()
.into_string()
.expect("Unable to convert to normal String"),
};
assert_eq!(
s,
format!(
"data:text/html,{}",
urlencoding::encode(
&std::fs::read_to_string(std::path::Path::new(&dist_dir).join("index.tauri.html"))
.unwrap()
)
)
format!("data:text/html,{}", urlencoding::encode(context.index))
);
}
_ => panic!("setup content failed"),
@ -437,20 +426,13 @@ mod test {
#[cfg(dev)]
{
let config = tauri_api::config::get().expect("unable to setup default config");
let config = &context.config;
match res {
Ok(Content::Url(dp)) => assert_eq!(dp, config.build.dev_path),
Ok(Content::Html(s)) => {
let dev_dir = &config.build.dev_path;
let dev_path = std::path::Path::new(dev_dir).join("index.tauri.html");
assert_eq!(
s,
format!(
"data:text/html,{}",
urlencoding::encode(
&std::fs::read_to_string(dev_path).expect("failed to read dev path")
)
)
format!("data:text/html,{}", urlencoding::encode(context.index))
);
}
_ => panic!("setup content failed"),
@ -461,7 +443,8 @@ mod test {
#[cfg(embedded_server)]
#[test]
fn check_setup_port() {
let res = super::setup_port();
let context = Context::new::<TauriContext>().unwrap();
let res = super::setup_port(&context);
match res {
Ok((_s, _b)) => {}
_ => panic!("setup port failed"),
@ -474,8 +457,9 @@ mod test {
#[test]
fn check_server_url(port in (any::<u32>().prop_map(|v| v.to_string()))) {
let p = port.clone();
let context = Context::new::<TauriContext>().unwrap();
let res = super::setup_server_url(port);
let res = super::setup_server_url(port, &context);
match res {
Ok(url) => assert!(url.contains(&p)),

View File

@ -1 +0,0 @@
include!(concat!(env!("OUT_DIR"), "/data.rs"));

View File

@ -1,9 +0,0 @@
use once_cell::sync::Lazy;
use tauri_api::cli::Matches;
/// Gets the CLI arg matches.
pub fn get_matches() -> &'static Option<Matches> {
static MATCHES: Lazy<Option<Matches>> = Lazy::new(|| tauri_api::cli::get_matches().ok());
&MATCHES
}

View File

@ -16,12 +16,13 @@ mod http;
#[cfg(notification)]
mod notification;
use crate::{ApplicationDispatcherExt, Event};
use crate::{app::Context, ApplicationDispatcherExt, Event};
#[allow(unused_variables)]
pub(crate) async fn handle<D: ApplicationDispatcherExt + 'static>(
dispatcher: &mut D,
arg: &str,
context: &Context,
) -> crate::Result<()> {
use cmd::Cmd::*;
match serde_json::from_str(arg) {
@ -277,22 +278,14 @@ pub(crate) async fn handle<D: ApplicationDispatcherExt + 'static>(
callback,
error,
} => {
asset::load(dispatcher, asset, asset_type, callback, error).await;
asset::load(dispatcher, asset, asset_type, callback, error, &context).await;
}
CliMatches { callback, error } => {
#[cfg(cli)]
crate::execute_promise(
dispatcher,
async move {
match crate::cli::get_matches() {
Some(matches) => Ok(matches),
None => Err(anyhow::anyhow!(r#""failed to get matches""#)),
}
},
callback,
error,
)
.await;
{
let matches = tauri_api::cli::get_matches(&context.config);
crate::execute_promise(dispatcher, async move { matches }, callback, error).await;
}
#[cfg(not(cli))]
api_error(
dispatcher,
@ -306,7 +299,7 @@ pub(crate) async fn handle<D: ApplicationDispatcherExt + 'static>(
error,
} => {
#[cfg(notification)]
notification::send(dispatcher, options, callback, error).await;
notification::send(dispatcher, options, callback, error, &context.config).await;
#[cfg(not(notification))]
allowlist_error(dispatcher, error, "notification");
}

View File

@ -1,5 +1,6 @@
use crate::ApplicationDispatcherExt;
use std::path::PathBuf;
use crate::{ApplicationDispatcherExt, Context};
use std::io::Read;
use tauri_api::assets::{AssetFetch, Assets};
#[allow(clippy::option_env_unwrap)]
pub async fn load<D: ApplicationDispatcherExt + 'static>(
@ -8,42 +9,43 @@ pub async fn load<D: ApplicationDispatcherExt + 'static>(
asset_type: String,
callback: String,
error: String,
ctx: &Context,
) {
let mut dispatcher_ = dispatcher.clone();
let assets = ctx.assets;
let public_path = ctx.config.tauri.embedded_server.public_path.clone();
crate::execute_promise(
dispatcher,
async move {
let mut path = PathBuf::from(if asset.starts_with('/') {
asset.replacen("/", "", 1)
// strip "about:" uri scheme if it exists
let asset = if asset.starts_with("about:") {
&asset[6..]
} else {
asset.clone()
});
let mut read_asset;
loop {
read_asset = crate::assets::ASSETS.get(&format!(
"{}/{}",
option_env!("TAURI_DIST_DIR")
.expect("tauri apps should be built with the TAURI_DIST_DIR environment variable"),
path.to_string_lossy()
));
if read_asset.is_err() {
match path.iter().next() {
Some(component) => {
let first_component = component.to_str().expect("failed to read path component");
path = PathBuf::from(path.to_string_lossy().replacen(
format!("{}/", first_component).as_str(),
"",
1,
));
}
None => {
return Err(anyhow::anyhow!("Asset '{}' not found", asset));
}
}
} else {
break;
}
&asset
};
// handle public path setting from tauri.conf > tauri > embeddedServer > publicPath
let asset = if asset.starts_with(&public_path) {
&asset[public_path.len() - 1..]
} else {
eprintln!(
"found url not matching public path.\nasset url: {}\npublic path: {}",
asset, public_path
);
asset
}
.to_string();
// how should that condition be handled now?
let asset_bytes = assets
.get(&Assets::format_key(&asset), AssetFetch::Decompress)
.ok_or_else(|| anyhow::anyhow!("Asset '{}' not found", asset))
.and_then(|(read, _)| {
read
.bytes()
.collect::<Result<Vec<u8>, _>>()
.map_err(Into::into)
})?;
if asset_type == "image" {
let mime_type = if asset.ends_with("gif") {
@ -62,12 +64,11 @@ pub async fn load<D: ApplicationDispatcherExt + 'static>(
"jpeg"
};
Ok(format!(
r#"data:image/{};base64,{}"#,
r#""data:image/{};base64,{}""#,
mime_type,
base64::encode(&read_asset.expect("Failed to read asset type").into_owned())
base64::encode(&asset_bytes)
))
} else {
let asset_bytes = read_asset.expect("Failed to read asset type");
let asset_str =
std::str::from_utf8(&asset_bytes).expect("failed to convert asset bytes to u8 slice");
if asset_type == "stylesheet" {

View File

@ -1,17 +1,22 @@
use super::cmd::NotificationOptions;
use crate::ApplicationDispatcherExt;
use serde_json::Value as JsonValue;
use tauri_api::config::Config;
use tauri_api::notification::Notification;
pub async fn send<D: ApplicationDispatcherExt>(
dispatcher: &mut D,
options: NotificationOptions,
callback: String,
error: String,
config: &Config,
) {
let identifier = config.tauri.bundle.identifier.clone();
crate::execute_promise(
dispatcher,
async move {
let mut notification = tauri_api::notification::Notification::new().title(options.title);
let mut notification = Notification::new(identifier).title(options.title);
if let Some(body) = options.body {
notification = notification.body(body);
}

View File

@ -6,9 +6,6 @@
//! Tauri uses (and contributes to) the MIT licensed project that you can find at [webview](https://github.com/webview/webview).
#![warn(missing_docs, rust_2018_idioms)]
/// The asset management module.
#[cfg(assets)]
pub mod assets;
/// The event system module.
pub mod event;
/// The embedded server helpers.
@ -17,10 +14,6 @@ pub mod server;
/// The Tauri-specific settings for your app e.g. notification permission status.
pub mod settings;
/// The CLI args interface.
#[cfg(cli)]
pub mod cli;
/// The webview application entry.
mod app;
/// The Tauri API endpoints.
@ -41,6 +34,7 @@ pub type SyncTask = Box<dyn FnOnce() + Send>;
pub use anyhow::Result;
pub use app::*;
pub use tauri_api as api;
pub use tauri_macros::FromTauriContext;
pub use webview::{
ApplicationDispatcherExt, ApplicationExt, Callback, Event, WebviewBuilderExt, WindowBuilderExt,
};

View File

@ -1,39 +1,39 @@
use tiny_http::{Header, Response};
use std::io::Read;
use tauri_api::assets::{AssetFetch, Assets};
use tiny_http::{Response, StatusCode};
/// Returns the HTTP response of the given asset path.
#[allow(clippy::option_env_unwrap)]
pub fn asset_response(path: &str) -> Response<std::io::Cursor<Vec<u8>>> {
let asset_path = &format!(
"{}{}",
option_env!("TAURI_DIST_DIR")
.expect("tauri apps should be built with the TAURI_DIST_DIR environment variable"),
path
);
let asset = crate::assets::ASSETS
.get(asset_path)
.unwrap_or_else(|_| panic!("Could not read asset {}", asset_path))
.into_owned();
let mut response = Response::from_data(asset);
let header;
pub fn asset_response(path: &str, assets: &'static Assets) -> Response<impl Read> {
let (asset, _) = assets
.get(path, AssetFetch::Compress)
.unwrap_or_else(|| panic!("Could not read asset {}", path));
if path.ends_with(".svg") {
header = Header::from_bytes(&b"Content-Type"[..], &b"image/svg+xml"[..])
.expect("Could not add svg+xml header");
let mut headers = Vec::new();
// Content-Encoding
const CONTENT_ENCODING: &str = "Content-Encoding: gzip";
let content_encoding = CONTENT_ENCODING
.parse()
.unwrap_or_else(|_| panic!("Could not add {} header", CONTENT_ENCODING));
headers.push(content_encoding);
// Content-Type
let mime = if path.ends_with(".svg") {
"Content-Type: image/svg+xml"
} else if path.ends_with(".css") {
header =
Header::from_bytes(&b"Content-Type"[..], &b"text/css"[..]).expect("Could not add css header");
"Content-Type: text/css"
} else if path.ends_with(".html") {
header = Header::from_bytes(&b"Content-Type"[..], &b"text/html"[..])
.expect("Could not add html header");
"Content-Type: text/html"
} else if path.ends_with(".js") {
header = Header::from_bytes(&b"Content-Type"[..], &b"text/javascript"[..])
.expect("Could not add Javascript header");
"Content-Type: text/javascript"
} else {
header = Header::from_bytes(&b"Content-Type"[..], &b"application/octet-stream"[..])
.expect("Could not add octet-stream header");
}
"Content-Type: application/octet-stream"
};
response.add_header(header);
let content_type = mime
.parse()
.unwrap_or_else(|_| panic!("Could not add {} header", mime));
headers.push(content_type);
response
Response::new(StatusCode(200), headers, asset, None, None)
}

0
tauri/test/fixture/dist/__tauri.js vendored Normal file
View File

View File

@ -9,6 +9,7 @@
"active": true
},
"bundle": {
"identifier": "studio.tauri.example",
"active": true
},
"allowlist": {