feat: shift tauri create [not wired up] (#1330)

* Partial revert "refactor(tauri.js): remove create command (#1265)"

This reverts commit b29c0685bc.

* shift templates/recipes over

* shift remaining files that weren't removed

* add change file

* rename to create-tauri-app

* adjust covector config
This commit is contained in:
Jacob Bolda 2021-03-07 01:29:06 -06:00 committed by GitHub
parent b0c1009098
commit 4ec20a4a28
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
37 changed files with 428 additions and 320 deletions

View File

@ -169,6 +169,11 @@
}
]
},
"create-tauri-app": {
"path": "./cli/create-tauri-app",
"manager": "javascript",
"dependencies": ["tauri.js"]
},
"tauri-utils": {
"path": "./tauri-utils",
"manager": "rust"

View File

@ -0,0 +1,6 @@
---
"create-tauri-app": minor
"tauri.js": patch
---
Revert `tauri create` deletion and shift remaining pieces that weren't deleted to `create-tauri-app`.

View File

@ -0,0 +1,209 @@
const parseArgs = require("minimist");
const inquirer = require("inquirer");
const { resolve } = require("path");
const { merge } = require("lodash");
const {
recipeShortNames,
recipeDescriptiveNames,
recipeByDescriptiveName,
recipeByShortName,
} = require("../../tauri.js/dist/api/recipes");
/**
* @type {object}
* @property {boolean} h
* @property {boolean} help
* @property {string|boolean} f
* @property {string|boolean} force
* @property {boolean} l
* @property {boolean} log
* @property {boolean} d
* @property {boolean} directory
* @property {string} r
* @property {string} recipe
*/
function main(cliArgs) {
const argv = parseArgs(cliArgs, {
alias: {
h: "help",
f: "force",
l: "log",
d: "directory",
t: "tauri-path",
A: "app-name",
W: "window-title",
D: "dist-dir",
P: "dev-path",
r: "recipe",
},
boolean: ["h", "l", "ci"],
});
if (argv.help) {
printUsage();
return 0;
}
if (argv.ci) {
runInit(argv);
} else {
getOptionsInteractive(argv).then((responses) => runInit(argv, responses));
}
}
function printUsage() {
console.log(`
Description
Inits the Tauri template. If Tauri cannot find the tauri.conf.json
it will create one.
Usage
$ tauri create
Options
--help, -h Displays this message
--ci Skip prompts
--force, -f Force init to overwrite [conf|template|all]
--log, -l Logging [boolean]
--directory, -d Set target directory for init
--tauri-path, -t Path of the Tauri project to use (relative to the cwd)
--app-name, -A Name of your Tauri application
--window-title, -W Window title of your Tauri application
--dist-dir, -D Web assets location, relative to <project-dir>/src-tauri
--dev-path, -P Url of your dev server
--recipe, -r Add UI framework recipe. None by default.
Supported recipes: [${recipeShortNames.join("|")}]
`);
}
const getOptionsInteractive = (argv) => {
let defaultAppName = argv.A;
if (!defaultAppName) {
try {
const packageJson = JSON.parse(
readFileSync(resolve(process.cwd(), "package.json")).toString()
);
defaultAppName = packageJson.displayName || packageJson.name;
} catch {}
}
return inquirer
.prompt([
{
type: "input",
name: "appName",
message: "What is your app name?",
default: defaultAppName,
when: !argv.A,
},
{
type: "input",
name: "tauri.window.title",
message: "What should the window title be?",
default: "Tauri App",
when: () => !argv.W,
},
{
type: "list",
name: "recipeName",
message: "Would you like to add a UI recipe?",
choices: recipeDescriptiveNames,
default: "No recipe",
when: () => !argv.r,
},
])
.then((answers) =>
inquirer
.prompt([
{
type: "input",
name: "build.devPath",
message: "What is the url of your dev server?",
default: "http://localhost:4000",
when: () =>
(!argv.P && !argv.p && answers.recipeName === "No recipe") ||
argv.r === "none",
},
{
type: "input",
name: "build.distDir",
message:
'Where are your web assets (HTML/CSS/JS) located, relative to the "<current dir>/src-tauri" folder that will be created?',
default: "../dist",
when: () =>
(!argv.D && answers.recipeName === "No recipe") ||
argv.r === "none",
},
])
.then((answers2) => ({ ...answers, ...answers2 }))
)
.catch((error) => {
if (error.isTtyError) {
// Prompt couldn't be rendered in the current environment
console.log(
"It appears your terminal does not support interactive prompts. Using default values."
);
runInit();
} else {
// Something else when wrong
console.error("An unknown error occurred:", error);
}
});
};
async function runInit(argv, config = {}) {
const { appName, recipeName, ...configOptions } = config;
const init = require("../../tauri.js/dist/api/init");
let recipe;
let recipeSelection = "none";
if (recipeName !== undefined) {
recipe = recipeByDescriptiveName(recipeName);
} else if (argv.r) {
recipe = recipeByShortName(argv.r);
}
let buildConfig = {
distDir: argv.D,
devPath: argv.P,
};
if (recipe !== undefined) {
recipeSelection = recipe.shortName;
buildConfig = recipe.configUpdate(buildConfig);
}
const directory = argv.d || process.cwd();
init({
directory,
force: argv.f || null,
logging: argv.l || null,
tauriPath: argv.t || null,
appName: appName || argv.A || null,
customConfig: merge(configOptions, {
build: buildConfig,
tauri: {
window: {
title: argv.W,
},
},
}),
});
const {
installDependencies,
} = require("../../tauri.js/dist/api/dependency-manager");
await installDependencies();
if (recipe !== undefined) {
const {
installRecipeDependencies,
runRecipePostConfig,
} = require("../../tauri.js/dist/api/recipes/install");
await installRecipeDependencies(recipe, directory);
await runRecipePostConfig(recipe, directory);
}
}
module.exports = main;

View File

@ -0,0 +1,22 @@
{
"name": "create-tauri-app",
"version": "0.0.0",
"description": "Create Tauri App in seconds",
"bin": {
"create-tauri-app": "./bin/create-tauri-app.js"
},
"repository": {
"type": "git",
"url": "git+https://github.com/tauri-apps/tauri.git"
},
"license": "MIT",
"bugs": {
"url": "https://github.com/tauri-apps/tauri/issues"
},
"homepage": "https://github.com/tauri-apps/tauri#readme",
"dependencies": {
"minimist": "^1.2.5",
"scaffe": "^0.1.5",
"tauri": "^0.14.0"
}
}

View File

@ -1,99 +1,99 @@
import { ManagementType, Result } from './types'
import { ManagementType, Result } from "./types";
import {
getNpmLatestVersion,
getNpmPackageVersion,
installNpmPackage,
installNpmDevPackage,
updateNpmPackage,
semverLt
} from './util'
import logger from '../../helpers/logger'
import { resolve } from '../../helpers/app-paths'
import inquirer from 'inquirer'
import { existsSync } from 'fs'
import { sync as crossSpawnSync } from 'cross-spawn'
semverLt,
} from "./util";
import logger from "../../../tauri.js/src/helpers/logger";
import { resolve } from "../../../tauri.js/src/helpers/app-paths";
import inquirer from "inquirer";
import { existsSync } from "fs";
import { sync as crossSpawnSync } from "cross-spawn";
const log = logger('dependency:npm-packages')
const log = logger("dependency:npm-packages");
async function manageDependencies(
managementType: ManagementType,
dependencies: string[]
): Promise<Result> {
const installedDeps = []
const updatedDeps = []
const installedDeps = [];
const updatedDeps = [];
const npmChild = crossSpawnSync('npm', ['--version'])
const yarnChild = crossSpawnSync('yarn', ['--version'])
const npmChild = crossSpawnSync("npm", ["--version"]);
const yarnChild = crossSpawnSync("yarn", ["--version"]);
if (
(npmChild.status ?? npmChild.error) &&
(yarnChild.status ?? yarnChild.error)
) {
throw new Error(
'must have `npm` or `yarn` installed to manage dependenices'
)
"must have `npm` or `yarn` installed to manage dependenices"
);
}
if (existsSync(resolve.app('package.json'))) {
if (existsSync(resolve.app("package.json"))) {
for (const dependency of dependencies) {
const currentVersion = await getNpmPackageVersion(dependency)
const currentVersion = await getNpmPackageVersion(dependency);
if (currentVersion === null) {
log(`Installing ${dependency}...`)
log(`Installing ${dependency}...`);
if (managementType === ManagementType.Install) {
await installNpmPackage(dependency)
await installNpmPackage(dependency);
} else if (managementType === ManagementType.InstallDev) {
await installNpmDevPackage(dependency)
await installNpmDevPackage(dependency);
}
installedDeps.push(dependency)
installedDeps.push(dependency);
} else if (managementType === ManagementType.Update) {
const latestVersion = await getNpmLatestVersion(dependency)
const latestVersion = await getNpmLatestVersion(dependency);
if (semverLt(currentVersion, latestVersion)) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-var-requires, @typescript-eslint/no-unsafe-member-access
const inquired = await inquirer.prompt([
{
type: 'confirm',
name: 'answer',
type: "confirm",
name: "answer",
message: `[NPM]: "${dependency}" latest version is ${latestVersion}. Do you want to update?`,
default: false
}
])
default: false,
},
]);
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-var-requires, @typescript-eslint/no-unsafe-member-access
if (inquired.answer) {
log(`Updating ${dependency}...`)
updateNpmPackage(dependency)
updatedDeps.push(dependency)
log(`Updating ${dependency}...`);
updateNpmPackage(dependency);
updatedDeps.push(dependency);
}
} else {
log(`"${dependency}" is up to date`)
log(`"${dependency}" is up to date`);
}
} else {
log(`"${dependency}" is already installed`)
log(`"${dependency}" is already installed`);
}
}
}
const result: Result = new Map<ManagementType, string[]>()
result.set(ManagementType.Install, installedDeps)
result.set(ManagementType.Update, updatedDeps)
const result: Result = new Map<ManagementType, string[]>();
result.set(ManagementType.Install, installedDeps);
result.set(ManagementType.Update, updatedDeps);
return result
return result;
}
const dependencies = ['tauri']
const dependencies = ["tauri"];
async function install(): Promise<Result> {
return await manageDependencies(ManagementType.Install, dependencies)
return await manageDependencies(ManagementType.Install, dependencies);
}
async function installThese(dependencies: string[]): Promise<Result> {
return await manageDependencies(ManagementType.Install, dependencies)
return await manageDependencies(ManagementType.Install, dependencies);
}
async function installTheseDev(dependencies: string[]): Promise<Result> {
return await manageDependencies(ManagementType.InstallDev, dependencies)
return await manageDependencies(ManagementType.InstallDev, dependencies);
}
async function update(): Promise<Result> {
return await manageDependencies(ManagementType.Update, dependencies)
return await manageDependencies(ManagementType.Update, dependencies);
}
export { install, installThese, installTheseDev, update }
export { install, installThese, installTheseDev, update };

View File

@ -0,0 +1,44 @@
import { map, identity, find } from "lodash";
import { TauriBuildConfig } from "./types/config";
import { reactjs, reactts } from "./recipes/react";
export interface Recipe {
descriptiveName: string;
shortName: string;
configUpdate: (cfg: TauriBuildConfig) => TauriBuildConfig;
extraNpmDependencies: string[];
extraNpmDevDependencies: string[];
postConfiguration: (cwd: string) => void;
}
const none = {
descriptiveName: "No recipe",
shortName: "none",
configUpdate: identity,
extraNpmDependencies: [],
extraNpmDevDependencies: [],
postConfiguration: (cwd: string) => {},
};
export const allRecipes: Recipe[] = [none, reactjs, reactts];
export const recipeNames: Array<[string, string]> = map(
allRecipes,
(r: Recipe) => [r.shortName, r.descriptiveName]
);
export const recipeByShortName = (name: string): Recipe | undefined =>
find(allRecipes, (r: Recipe) => r.shortName === name);
export const recipeByDescriptiveName = (name: string): Recipe | undefined =>
find(allRecipes, (r: Recipe) => r.descriptiveName === name);
export const recipeShortNames: string[] = map(
allRecipes,
(r: Recipe) => r.shortName
);
export const recipeDescriptiveNames: string[] = map(
allRecipes,
(r: Recipe) => r.descriptiveName
);

View File

@ -0,0 +1,35 @@
import {
installThese,
installTheseDev,
} from "./dependency-manager/npm-packages";
import { Recipe } from ".";
import { Result } from "./dependency-manager/types";
import logger from "../../tauri.js/src/helpers/logger";
export async function installRecipeDependencies(
recipe: Recipe
): Promise<Result> {
const log = logger("recipe:install");
log(`Installing dependencies for ${recipe.descriptiveName}`);
return await installThese(recipe.extraNpmDependencies).then(
async (results) =>
await installTheseDev(recipe.extraNpmDevDependencies).then(
(results2) =>
new Map([
...Array.from(results.entries()),
...Array.from(results2.entries()),
])
)
);
}
export async function runRecipePostConfig(
recipe: Recipe,
cwd: string
): Promise<void> {
const log = logger("recipe:postconfig");
log(`Running post configuration for ${recipe.descriptiveName}`);
return await new Promise(() => recipe.postConfiguration(cwd));
}

View File

@ -0,0 +1,65 @@
import { Recipe } from ".";
import { TauriBuildConfig } from "./../types/config";
import { spawnSync } from "../../../tauri.js/src/helpers/spawn";
import logger from "../../../tauri.js/src/helpers/logger";
import copyTemplates from "../../../tauri.js/src/helpers/copy-templates";
import { resolve, join } from "path";
const uiAppDir = "app-ui";
const log = logger("react-recipe");
const completeLogMsg = `
Your installation completed.
To start, run yarn tauri dev
`;
const afterCra = (): void => {
copyTemplates({
source: resolve(__dirname, "../../templates/recipes/react/"),
scope: {},
target: join(uiAppDir, "./src/"),
});
log(completeLogMsg);
};
const reactjs: Recipe = {
descriptiveName: "React.js",
shortName: "reactjs",
configUpdate: (cfg: TauriBuildConfig): TauriBuildConfig => ({
...cfg,
distDir: `../${uiAppDir}/build`,
devPath: "http://localhost:3000",
beforeDevCommand: `yarn --cwd ${uiAppDir} start`,
beforeBuildCommand: `yarn --cwd ${uiAppDir} build`,
}),
extraNpmDevDependencies: ["create-react-app"],
extraNpmDependencies: ["react"],
postConfiguration: (cwd: string) => {
spawnSync("yarn", ["create-react-app", uiAppDir], cwd);
afterCra();
},
};
const reactts: Recipe = {
...reactjs,
descriptiveName: "React with Typescript",
shortName: "reactts",
extraNpmDependencies: [
"typescript",
"@types/node",
"@types/react",
"@types/react-dom",
"@types/jest",
],
postConfiguration: (cwd: string) => {
spawnSync(
"yarn",
["create-react-app", "--template", "typescript", uiAppDir],
cwd
);
afterCra();
},
};
export { reactjs, reactts };

View File

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

Before

Width:  |  Height:  |  Size: 9.6 KiB

After

Width:  |  Height:  |  Size: 9.6 KiB

View File

@ -1,128 +0,0 @@
import https from 'https'
import { IncomingMessage } from 'http'
import { spawnSync } from '../../helpers/spawn'
import { sync as crossSpawnSync } from 'cross-spawn'
import { appDir, resolve as appResolve } from '../../helpers/app-paths'
import { existsSync } from 'fs'
import semver from 'semver'
const BASE_URL = 'https://docs.rs/crate/'
async function useYarn(): Promise<boolean> {
const hasYarnLockfile = existsSync(appResolve.app('yarn.lock'))
if (hasYarnLockfile) {
return true
} else {
return await new Promise((resolve) => {
const child = crossSpawnSync('npm', ['--version'])
resolve(!!(child.status ?? child.error))
})
}
}
async function getCrateLatestVersion(crateName: string): Promise<string> {
return await new Promise((resolve, reject) => {
const url = `${BASE_URL}${crateName}`
https.get(url, (res: IncomingMessage) => {
if (res.statusCode !== 302 || !res.headers.location) {
reject(res)
} else {
const version = res.headers.location.replace(url + '/', '')
resolve(version)
}
})
})
}
async function getNpmLatestVersion(packageName: string): Promise<string> {
if (await useYarn()) {
const child = crossSpawnSync(
'yarn',
['info', packageName, 'versions', '--json'],
{
cwd: appDir
}
)
const output = String(child.output[1])
const packageJson = JSON.parse(output) as { data: string[] }
return packageJson.data[packageJson.data.length - 1]
} else {
const child = crossSpawnSync('npm', ['show', packageName, 'version'], {
cwd: appDir
})
return String(child.output[1]).replace('\n', '')
}
}
async function getNpmPackageVersion(
packageName: string
): Promise<string | null> {
const child = (await useYarn())
? crossSpawnSync(
'yarn',
['list', '--patern', packageName, '--depth', '0'],
{
cwd: appDir
}
)
: crossSpawnSync('npm', ['list', packageName, 'version', '--depth', '0'], {
cwd: appDir
})
const output = String(child.output[1])
// eslint-disable-next-line security/detect-non-literal-regexp
const matches = new RegExp(packageName + '@(\\S+)', 'g').exec(output)
if (matches?.[1]) {
return matches[1]
} else {
return null
}
}
async function installNpmPackage(packageName: string): Promise<void> {
if (await useYarn()) {
spawnSync('yarn', ['add', packageName], appDir)
} else {
spawnSync('npm', ['install', packageName], appDir)
}
}
async function installNpmDevPackage(packageName: string): Promise<void> {
if (await useYarn()) {
spawnSync('yarn', ['add', packageName, '--dev'], appDir)
} else {
spawnSync('npm', ['install', packageName, '--save-dev'], appDir)
}
}
function updateNpmPackage(packageName: string): void {
const usesYarn = existsSync(appResolve.app('yarn.lock'))
if (usesYarn) {
spawnSync('yarn', ['upgrade', packageName, '--latest'], appDir)
} else {
spawnSync('npm', ['install', `${packageName}@latest`], appDir)
}
}
function padVersion(version: string): string {
let count = (version.match(/\./g) ?? []).length
while (count < 2) {
count++
version += '.0'
}
return version
}
function semverLt(first: string, second: string): boolean {
return semver.lt(padVersion(first), padVersion(second))
}
export {
getCrateLatestVersion,
getNpmLatestVersion,
getNpmPackageVersion,
installNpmPackage,
installNpmDevPackage,
updateNpmPackage,
padVersion,
semverLt
}

View File

@ -1,27 +0,0 @@
[package]
name = "app"
version = "0.1.0"
description = "A Tauri App"
authors = ["you"]
license = ""
repository = ""
default-run = "app"
edition = "2018"
build = "src/build.rs"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] }
tauri = <%= tauriDep || `{ version = "0.5" }` %>
[target."cfg(windows)".build-dependencies]
winres = "0.1"
[features]
custom-protocol = [ "tauri/custom-protocol" ]
[[bin]]
name = "app"
path = "src/main.rs"

View File

@ -1,10 +0,0 @@
# Generated by Cargo
# will have compiled files and executables
/target/
WixTools
# These are backup files generated by rustfmt
**/*.rs.bk
config.json
bundle.json

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

View File

@ -1,14 +0,0 @@
max_width = 100
hard_tabs = false
tab_spaces = 2
newline_style = "Auto"
use_small_heuristics = "Default"
reorder_imports = true
reorder_modules = true
remove_nested_parens = true
edition = "2018"
merge_derives = true
use_try_shorthand = false
use_field_init_shorthand = false
force_explicit_abi = true
imports_granularity = "Crate"

View File

@ -1,16 +0,0 @@
#[cfg(windows)]
extern crate winres;
#[cfg(windows)]
fn main() {
if std::path::Path::new("icons/icon.ico").exists() {
let mut res = winres::WindowsResource::new();
res.set_icon_with_id("icons/icon.ico", "32512");
res.compile().expect("Unable to find visual studio tools");
} else {
panic!("No Icon.ico found. Please add one or check the path");
}
}
#[cfg(not(windows))]
fn main() {}

View File

@ -1,14 +0,0 @@
#![cfg_attr(
all(not(debug_assertions), target_os = "windows"),
windows_subsystem = "windows"
)]
#[derive(tauri::FromTauriContext)]
struct Context;
fn main() {
tauri::AppBuilder::<Context>::new()
.build()
.unwrap()
.run();
}

View File

@ -1,69 +0,0 @@
use crate::tauri::process::{ProcessExt, Signal, SystemExt};
fn update() -> Result<(), String> {
let target = tauri::platform::target_triple().map_err(|_| "Could not determine target")?;
let github_release = tauri::updater::github::get_latest_release("jaemk", "self_update")
.map_err(|_| "Could not fetch latest release")?;
match github_release.asset_for(&target) {
Some(github_release_asset) => {
let release = tauri::updater::Release {
version: github_release.tag.trim_start_matches('v').to_string(),
download_url: github_release_asset.download_url,
asset_name: github_release_asset.name,
};
let status = tauri::updater::Update::configure()
.unwrap()
.release(release)
.bin_path_in_archive("github")
.bin_name("app")
.bin_install_path(&tauri::command::command_path("app".to_string()).unwrap())
.show_download_progress(true)
.current_version(env!("CARGO_PKG_VERSION"))
.build()
.unwrap()
.update()
.unwrap();
println!("found release: {}", status.version());
/*let tmp_dir = tauri::dir::with_temp_dir(|dir| {
let file_path = dir.path().join("my-temporary-note.pdf");
let mut tmp_archive = std::fs::File::create(file_path).unwrap();
tauri::http::download(&"https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf".to_string(), &mut tmp_archive, true).unwrap();
});*/
Ok(())
}
None => Err(format!("Could not find release for target {}", target)),
}
}
fn restart_app(app_command: String) -> Result<(), String> {
let mut system = tauri::process::System::new();
let parent_process = tauri::process::get_parent_process(&mut system)
.map_err(|_| "Could not determine parent process")?;
if parent_process.name() == "app" {
parent_process.kill(Signal::Kill);
std::thread::sleep(std::time::Duration::from_secs(1));
std::process::Command::new(app_command)
.spawn()
.map_err(|_| "Could not start app")?;
}
Ok(())
}
fn run_updater() -> Result<(), String> {
let app_command = tauri::command::relative_command("app".to_string())
.map_err(|_| "Could not determine app path")?;
update()?;
restart_app(app_command)?;
Ok(())
}
fn main() {
match run_updater() {
Ok(_) => {}
Err(err) => panic!(err),
};
}