Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Better handle input & tracking #2113

Merged
merged 23 commits into from
Nov 16, 2023
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ import * as show from './messages';
contract,
frontend,
tests,
projectName,
templatesDir: path.resolve(__dirname, '../templates'),
projectPath,
});
Expand Down
30 changes: 14 additions & 16 deletions src/make.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,22 @@ import fs from 'fs';
import { ncp } from 'ncp';
import path from 'path';

export async function createProject({ contract, frontend, tests, projectPath, projectName, templatesDir }: CreateContractParams & CreateGatewayParams): Promise<boolean> {
export async function createProject({ contract, frontend, tests, projectPath, templatesDir }: CreateContractParams & CreateGatewayParams): Promise<boolean> {

if(contract !== 'none'){
await createContract({ contract, tests, projectPath, projectName, templatesDir });
}else{
await createGateway({ frontend, projectPath, projectName, templatesDir });
if (contract !== 'none') {
await createContract({ contract, tests, projectPath, templatesDir });
} else {
await createGateway({ frontend, projectPath, templatesDir });
}

return true;
}

async function createContract({ contract, tests, projectPath, projectName, templatesDir }: CreateContractParams) {
async function createContract({ contract, tests, projectPath, templatesDir }: CreateContractParams) {
// contract folder
const sourceContractDir = path.resolve(templatesDir, 'contracts', contract);
const targetContractDir = projectPath;
fs.mkdirSync(targetContractDir, { recursive: true });
await copyDir(sourceContractDir, targetContractDir);
fs.mkdirSync(projectPath, { recursive: true });
await copyDir(sourceContractDir, projectPath);

// copy sandbox-test dir
const targetTestDir = path.resolve(projectPath, `sandbox-${tests}`);
Expand All @@ -30,7 +29,7 @@ async function createContract({ contract, tests, projectPath, projectName, templ
fs.mkdirSync(targetTestDir);
await copyDir(sourceTestDir, targetTestDir);

if (contract === 'rs'){
if (contract === 'rs') {
if (tests === 'rs') {
// leave only one test script
fs.unlinkSync(path.resolve(projectPath, 'test-ts.sh'));
Expand All @@ -41,25 +40,24 @@ async function createContract({ contract, tests, projectPath, projectName, templ
const cargoToml = fs.readFileSync(cargoTomlPath).toString();
const cargoTomlWithWorkspace = cargoToml + '\n[workspace]\nmembers = ["sandbox-rs"]';
fs.writeFileSync(cargoTomlPath, cargoTomlWithWorkspace);
}else{
} else {
// leave only one test script
fs.unlinkSync(path.resolve(projectPath, 'test-rs.sh'));
fs.renameSync(path.resolve(projectPath, 'test-ts.sh'), path.resolve(projectPath, 'test.sh'));
}
}
}

async function createGateway({ frontend, projectPath, projectName, templatesDir }: CreateGatewayParams) {
async function createGateway({ frontend, projectPath, templatesDir }: CreateGatewayParams) {
const sourceFrontendDir = path.resolve(`${templatesDir}/frontend/${frontend}`);
const targetFrontendDir = path.resolve(`${projectPath}`);
fs.mkdirSync(targetFrontendDir, { recursive: true });
await copyDir(sourceFrontendDir, targetFrontendDir);
fs.mkdirSync(projectPath, { recursive: true });
await copyDir(sourceFrontendDir, projectPath);
}

// Wrap `ncp` tool to wait for the copy to finish when using `await`
export function copyDir(source: string, dest: string) {
return new Promise<void>((resolve, reject) => {
ncp(source, dest, { }, err => err? reject(err): resolve());
ncp(source, dest, {}, err => err ? reject(err) : resolve());
});
}

Expand Down
5 changes: 3 additions & 2 deletions src/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,10 @@ ${!install ? chalk` - {inverse Install all dependencies}
- {inverse Start your app}:
{blue pnpm {bold run dev}}`;

export const argsError = () => show(chalk`{red Arguments error}
export const argsError = (msg: string) => show(chalk`{red Arguments error: {white ${msg}}}

Run {blue npx create-near-app} without arguments, or use:
npx create-near-app <projectName> --frontend next|vanilla|none --contract rs|ts|none --tests rs|ts|none`);
npx create-near-app <projectName> [--frontend next|vanilla|none] [--contract rs|ts|none --tests rs|ts|none]`);

export const unsupportedNodeVersion = (supported: string) => show(chalk`{red We support node.js version ${supported} or later}`);

Expand Down
9 changes: 5 additions & 4 deletions src/tracking.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import {Contract, Frontend} from './types';
import {Contract, Frontend, TestingFramework} from './types';
import chalk from 'chalk';
import mixpanel from 'mixpanel';

const MIXPANEL_TOKEN = 'df164f13212cbb0dfdae991da60e87f2';
const MIXPANEL_TOKEN = '24177ef1ec09ffea5cb6f68909c66a61';

const tracker = mixpanel.init(MIXPANEL_TOKEN);

export const trackingMessage = chalk`Near collects anonymous information on the commands used. No personal information that could identify you is shared`;

// TODO: track different failures & install usage
export const trackUsage = async (frontend: Frontend, contract: Contract) => {
export const trackUsage = async (frontend: Frontend, contract: Contract, testing: TestingFramework) => {
// prevents logging from CI
if (process.env.NEAR_ENV === 'ci' || process.env.NODE_ENV === 'ci') {
console.log('Mixpanel logging is skipped in CI env');
Expand All @@ -19,11 +19,12 @@ export const trackUsage = async (frontend: Frontend, contract: Contract) => {
const mixPanelProperties = {
frontend,
contract,
testing,
os: process.platform,
nodeVersion: process.versions.node,
timestamp: new Date().toString()
};
tracker.track('track create near app', mixPanelProperties);
tracker.track('CNA', mixPanelProperties);
} catch (e) {
console.error(
'Warning: problem while sending tracking data. Error: ',
Expand Down
2 changes: 0 additions & 2 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,11 @@ export type CreateContractParams = {
contract: Contract,
tests: TestingFramework,
projectPath: string,
projectName: ProjectName,
templatesDir: string,
}

export type CreateGatewayParams = {
frontend: Frontend,
projectPath: string,
projectName: ProjectName,
templatesDir: string,
}
97 changes: 59 additions & 38 deletions src/user-input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ import fs from 'fs';
export async function getUserArgs(): Promise<UserConfig> {
program
.argument('[projectName]')
.option('--contract [ts|rs|none]')
.option('--frontend [next|vanilla|none]')
.option('--contract [ts|rs|none]')
.option('--tests [rs|ts|none]')
.option('--install')
.addHelpText('after', 'You can create a frontend or a contract with tests');
Expand All @@ -34,30 +34,6 @@ export async function getUserArgs(): Promise<UserConfig> {
return { contract, frontend, projectName, tests, install };
}

export function validateUserArgs(args: UserConfig): 'error' | 'ok' | 'none' {
if (args === null) {
return 'error';
}
const { projectName, contract, frontend, tests } = args;
const hasAllOptions = contract !== undefined && frontend !== undefined;
const hasPartialOptions = contract !== undefined || frontend !== undefined;
const hasProjectName = projectName !== undefined;
const hasAllArgs = hasAllOptions && hasProjectName;
const hasNoArgs = !hasPartialOptions && !hasProjectName;
const optionsAreValid = hasAllOptions
&& FRONTENDS.includes(frontend)
&& CONTRACTS.includes(contract)
&& TESTING_FRAMEWORKS.includes(tests);

if (hasNoArgs) {
return 'none';
} else if (hasAllArgs && optionsAreValid) {
return 'ok';
} else {
return 'error';
}
}

type Choices<T> = { title: string, description?: string, value: T }[];

const appChoices: Choices<App> = [
Expand Down Expand Up @@ -135,8 +111,8 @@ export async function getUserAnswers(): Promise<UserConfig> {
return { frontend, contract: 'none', tests: 'none', projectName, install };
} else {
// If contract, ask for the language for the contract and tests
let {contract, tests} = await promptUser(contractPrompt);
tests = contract === 'ts'? 'ts' : tests;
let { contract, tests } = await promptUser(contractPrompt);
tests = contract === 'ts' ? 'ts' : tests;
const { projectName } = await promptUser(namePrompts);
const install = contract === 'ts' ? (await promptUser(npmPrompt)).install as boolean : false;
return { frontend: 'none', contract, tests, projectName, install };
Expand All @@ -156,23 +132,22 @@ export async function promptAndGetConfig(): Promise<{ config: UserConfig, projec
// process cli args
let args = await getUserArgs();

if( args.contract && (!args.tests || args.frontend) ){
return show.argsError();
}

if( args.frontend && (args.tests || args.contract) ){
return show.argsError();
}

// If no args, prompt user
if( !args.contract && !args.frontend ){
if (!args.projectName) {
show.welcome();
args = await getUserAnswers();
}

// Homogenizing terminal args with prompt args
args.contract = args.contract || 'none';
args.frontend = args.frontend || 'none';
args.tests = args.tests || 'none';

if (!validateUserArgs(args)) return;

// track user input
const { frontend, contract } = args;
trackUsage(frontend, contract);
const { frontend, contract, tests } = args;
trackUsage(frontend, contract, tests);

let path = projectPath(args.projectName);

Expand All @@ -184,3 +159,49 @@ export async function promptAndGetConfig(): Promise<{ config: UserConfig, projec
}

export const projectPath = (projectName: ProjectName) => `${process.cwd()}/${projectName}`;

const validateUserArgs = (args: UserConfig) => {

if (!FRONTENDS.includes(args.frontend)) {
show.argsError(`Invalid frontend type: ${args.frontend}`);
return false;
}

if (!CONTRACTS.includes(args.contract)) {
show.argsError(`Invalid contract type: ${args.contract}`);
return false;
}

if (!TESTING_FRAMEWORKS.includes(args.tests)) {
show.argsError(`Invalid testing framework: ${args.tests}`);
return false;
}

if (!args.projectName) {
show.argsError('Please provide a project name');
return false;
}

if ((args.contract === 'none') === (args.frontend === 'none')) {
console.log(args.contract, args.frontend);
show.argsError('Please create a contract OR a frontend');
return false;
}

if (args.contract !== 'none' && args.tests === 'none') {
show.argsError('Please select a testing framework for your contract');
return false;
}

if (args.frontend !== 'none' && args.tests !== 'none') {
show.argsError('Remove the --tests flag when creating a frontend');
return false;
}

if (args.contract === 'ts' && args.tests === 'rs') {
show.argsError('We currently do not support creating a contract in TS with Rust tests, please create it manually');
return false;
}

return true;
};
10 changes: 5 additions & 5 deletions templates/sandbox-tests/sandbox-ts/ava.config.cjs
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
require("util").inspect.defaultOptions.depth = 5; // Increase AVA's printing depth
require('util').inspect.defaultOptions.depth = 5; // Increase AVA's printing depth

module.exports = {
timeout: "300000",
files: ["src/*.ava.ts"],
timeout: '300000',
files: ['src/*.ava.ts'],
failWithoutAssertions: false,
extensions: ["ts"],
require: ["ts-node/register"],
extensions: ['ts'],
require: ['ts-node/register'],
};
20 changes: 10 additions & 10 deletions test/__snapshots__/make.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -2937,14 +2937,14 @@ channel = "1.69"
exports[`create contract 'rs' 'none' 'ts': --rs_none_ts--sandbox-ts--ava.config.cjs 1`] = `
[
"--rs_none_ts--sandbox-ts--ava.config.cjs",
"require("util").inspect.defaultOptions.depth = 5; // Increase AVA's printing depth
"require('util').inspect.defaultOptions.depth = 5; // Increase AVA's printing depth

module.exports = {
timeout: "300000",
files: ["src/*.ava.ts"],
timeout: '300000',
files: ['src/*.ava.ts'],
failWithoutAssertions: false,
extensions: ["ts"],
require: ["ts-node/register"],
extensions: ['ts'],
require: ['ts-node/register'],
};
",
]
Expand Down Expand Up @@ -3508,14 +3508,14 @@ exports[`create contract 'ts' 'none' 'ts': --ts_none_ts--package.json 1`] = `
exports[`create contract 'ts' 'none' 'ts': --ts_none_ts--sandbox-ts--ava.config.cjs 1`] = `
[
"--ts_none_ts--sandbox-ts--ava.config.cjs",
"require("util").inspect.defaultOptions.depth = 5; // Increase AVA's printing depth
"require('util').inspect.defaultOptions.depth = 5; // Increase AVA's printing depth

module.exports = {
timeout: "300000",
files: ["src/*.ava.ts"],
timeout: '300000',
files: ['src/*.ava.ts'],
failWithoutAssertions: false,
extensions: ["ts"],
require: ["ts-node/register"],
extensions: ['ts'],
require: ['ts-node/register'],
};
",
]
Expand Down
5 changes: 3 additions & 2 deletions test/__snapshots__/user-input.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,10 @@ Notice: some platforms aren't supported (yet).
==========================================",
],
[
"Arguments error
"Arguments error: wrong args

Run npx create-near-app without arguments, or use:
npx create-near-app <projectName> --frontend next|vanilla|none --contract rs|ts|none --tests rs|ts|none",
npx create-near-app <projectName> [--frontend next|vanilla|none] [--contract rs|ts|none --tests rs|ts|none]",
],
[
"Notice: On Win32 please use WSL (Windows Subsystem for Linux).
Expand Down
2 changes: 0 additions & 2 deletions test/make.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ describe('create contract', () => {
contract,
frontend,
tests,
projectName,
templatesDir: rootDir,
projectPath,
});
Expand Down Expand Up @@ -70,7 +69,6 @@ describe('create', () => {
contract,
frontend,
tests,
projectName,
templatesDir: rootDir,
projectPath,
});
Expand Down
2 changes: 1 addition & 1 deletion test/user-input.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ describe('messages', () => {
showSpy = jest.spyOn(show, 'show').mockImplementation(() => {});
show.welcome();
show.setupFailed();
show.argsError();
show.argsError('wrong args');
show.windowsWarning();
show.creatingApp();
show.depsInstall();
Expand Down