Skip to content

Commit

Permalink
init
Browse files Browse the repository at this point in the history
  • Loading branch information
zzzorgo committed May 20, 2022
0 parents commit f33e43a
Show file tree
Hide file tree
Showing 22 changed files with 7,446 additions and 0 deletions.
17 changes: 17 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
module.exports = {
extends: ['@salutejs/eslint-config'],
ignorePatterns: ['*.d.ts', 'coverage/*', 'build/*'],
rules: {
'react/react-in-jsx-scope': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'no-console': ['warn', { allow: ['warn', 'error'] }],
},
overrides: [
{
files: ['*.mjs'],
rules: {
'no-console': 'off',
},
},
],
};
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
node_modules
template-app
.template-package
2 changes: 2 additions & 0 deletions .npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
save-exact=true
@salutejs:registry=https://registry.npmjs.org/
10 changes: 10 additions & 0 deletions .prettierrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
module.exports = {
"parser": "typescript",
"arrowParens": "always",
"printWidth": 120,
"endOfLine": "auto",
"semi": true,
"singleQuote": true,
"tabWidth": 4,
"trailingComma": "all"
};
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# @salutejs/cerate-app
Шаблонизатор проектов

Простейший способ получить заготовку проекта это выполнить команду


```bash
npx @salutejs/cerate-app
```

После запуска появится визард, в котором можно указать имя проекта и прочие опции. После завершения работы скрипта в текущей рабоче директории терминала будет создана еще одна директория с именем указанным как имя проекта.


### Параметр `--templatePackage`
При запуске команды без аргументов будет выбран пакет `@salutejs/canvas-example` как пакет-шаблон. Но можно указать в качестве шаблона любой другой пакет. При отсутствии дополнительной конфигурации пакет-шаблон будет просто скопирован в целевую папку.

```bash
npx @salutejs/cerate-app --templatePackage any-npm-package
```

Если ваш пакет требует авторизации в npm, можно задать токен доступа в env-переменную `NPM_REGISTRY_TOKEN`.
9 changes: 9 additions & 0 deletions index.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#!/usr/bin/env bash

SELF_PATH=$0
LINK_PATH=$(readlink $SELF_PATH)
LINK_DIR=$(dirname -- $LINK_PATH)
PACKAGE_DIR=$(dirname -- $SELF_PATH)/$LINK_DIR
RUN_DIR=$(pwd)

node --experimental-import-meta-resolve $PACKAGE_DIR/src/cliWizard.mjs $@ --runDir $RUN_DIR
6,620 changes: 6,620 additions & 0 deletions package-lock.json

Large diffs are not rendered by default.

56 changes: 56 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
{
"name": "@salutejs/create-app",
"version": "0.1.7",
"scripts": {
"build": "echo no build needed",
"test": "jest --testTimeout=30000",
"lint": "eslint --ext .js,.mjs ./src",
"start": "bash index.sh"
},
"dependencies": {
"chalk": "5.0.0",
"gulp-if": "3.0.0",
"hpagent": "1.0.0",
"inquirer": "8.2.0",
"lodash": "4.17.21",
"micromatch": "4.0.4",
"mustache": "4.2.0",
"node-fetch": "3.2.4",
"through2": "4.0.2",
"vinyl-fs": "3.0.3",
"yargs": "17.3.1"
},
"devDependencies": {
"@salutejs/eslint-config": "0.4.0",
"@typescript-eslint/eslint-plugin": "2.29.0",
"@typescript-eslint/parser": "2.29.0",
"babel-eslint": "10.1.0",
"concat-stream": "2.0.0",
"eslint": "6.8.0",
"eslint-config-airbnb": "18.1.0",
"eslint-config-prettier": "6.11.0",
"eslint-config-react-app": "5.2.1",
"eslint-plugin-cypress": "2.11.1",
"eslint-plugin-flowtype": "4.7.0",
"eslint-plugin-import": "2.22.1",
"eslint-plugin-jest": "23.8.2",
"eslint-plugin-jsx-a11y": "6.2.3",
"eslint-plugin-no-only-tests": "2.4.0",
"eslint-plugin-prettier": "3.1.3",
"eslint-plugin-react": "7.19.0",
"eslint-plugin-react-hooks": "3.0.0",
"eslint-plugin-react-perf": "3.3.0",
"eslint-plugin-vue": "6.2.2",
"jest": "27.5.1",
"prettier": "2.0.5",
"typescript": "3.8.3"
},
"main": "./src/cliWizard.mjs",
"files": [
"src/**/*"
],
"bin": "./index.sh",
"publishConfig": {
"access": "public"
}
}
1 change: 1 addition & 0 deletions src/asciiLogo.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"\n\u001b[32m░█▀▀░█▀█░█░░░█░█░▀█▀░█▀▀░▀▀█░█▀▀░░░█░█▀▀░█▀▄░█▀▀░█▀█░▀█▀░█▀▀░░░░░█▀█░█▀█░█▀█\u001b[39m\n\u001b[32m░▀▀█░█▀█░█░░░█░█░░█░░█▀▀░░░█░▀▀█░▄▀░░█░░░█▀▄░█▀▀░█▀█░░█░░█▀▀░▄▄▄░█▀█░█▀▀░█▀▀\u001b[39m\n\u001b[32m░▀▀▀░▀░▀░▀▀▀░▀▀▀░░▀░░▀▀▀░▀▀░░▀▀▀░▀░░░▀▀▀░▀░▀░▀▀▀░▀░▀░░▀░░▀▀▀░░░░░▀░▀░▀░░░▀░░\u001b[39m\n"
64 changes: 64 additions & 0 deletions src/buildTemplate.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import fs from 'fs';
import vinylFs from 'vinyl-fs';
import gulpIf from 'gulp-if';
import micromatch from 'micromatch';

import { mustachePlugin } from './gulpPlugins/mustachePlugin.mjs';
import { buildJsonPlugin } from './gulpPlugins/buildJsonPlugin.mjs';
import { renamePlugin } from './gulpPlugins/renamePlugin.mjs';

const { src, dest } = vinylFs;

function buildTemplate(srcGlob, destFolder, config, rules) {
let stream = src(srcGlob, { nodir: true, dot: true })
.pipe(
renamePlugin((file) => {
const entry = config.rename.find((renameEntry) => micromatch.isMatch(file.path, renameEntry.glob));

if (entry) {
entry.map(file);
}
}),
)
.pipe(
renamePlugin((file) => {
if (file.extname === '.mustache') {
file.extname = '';
}
}),
);

for (const rule of rules) {
stream = stream.pipe(gulpIf(rule.test, mustachePlugin(config.featureToggles, rule.tags)));
}

return stream.pipe(buildJsonPlugin(config)).pipe(dest(destFolder));
}

export const buildTemplateTask = async (config, rules) => {
const taskPromises = [];

Object.values(config.paths).map(({ source, destination }) => {
let resolve;
let reject;

const taskFinished = new Promise((res, rej) => {
resolve = res;
reject = rej;
});

taskPromises.push(taskFinished);

fs.rmdirSync(destination, { recursive: true });

return buildTemplate(source, destination, config, rules)
.on('end', () => {
resolve();
})
.on('error', (err) => {
reject(err);
});
});

await Promise.all(taskPromises);
};
144 changes: 144 additions & 0 deletions src/cliWizard.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import inquirer from 'inquirer';
import path from 'path';
import chalk from 'chalk';
import { execSync } from 'child_process';
import { existsSync } from 'fs';

import { cleanUp, downloadTemplatePackage, extractTemplatePackage, getArgv, printLogo } from './utils.mjs';
import { buildTemplateTask } from './buildTemplate.mjs';
import { getConfigFromAnswers } from './config.mjs';
import { defaultRules } from './defaults/rules.mjs';

const CONFIG_ENTRY_POINT = '.create-app/index.mjs';
const DEFAULT_TEMPLATE = '@salutejs/canvas-example';

const checkboxInstructionsTranslation = `\n (Нажмите ${chalk.bold(
chalk.cyan('<space>'),
)} чтобы переключить функцию, ${chalk.bold(chalk.cyan('<enter>'))} чтобы завершить выбор)\n`;

export async function run() {
try {
const {
templatePath,
output,
runDir,
configPath = CONFIG_ENTRY_POINT,
templatePackage = DEFAULT_TEMPLATE,
} = getArgv();

printLogo();

let resolvedTemplatePath;

if (templatePath) {
resolvedTemplatePath = path.resolve(templatePath);
} else {
const packageBuffer = await downloadTemplatePackage(templatePackage);
resolvedTemplatePath = extractTemplatePackage(packageBuffer);
}

const templateConfig = existsSync(`${resolvedTemplatePath}/${configPath}`)
? `${resolvedTemplatePath}/${configPath}`
: configPath;

const templateModule = existsSync(templateConfig) ? await import(templateConfig) : {};
const { featureToggles = [], featureConfigMap = {}, templateDescription = '', rules = [] } = templateModule;

if (templateDescription) {
console.log(`${chalk.bold('Описание шаблона:')} ${templateDescription}`);
console.log('');
}

const featureChoices = featureToggles
.filter((toggle) => !toggle.hidden)
.map((toggle) => {
return {
name: toggle.name,
checked: toggle.defaultValue,
value: toggle.featureId,
short: toggle.featureId,
};
});

const questions = [
{
type: 'input',
name: 'projectName',
message: 'Название вашего проекта:',
default: 'template-app',
},
{
type: 'confirm',
name: 'changeDefaults',
message: 'Хотите изменить список стандартных функций проекта?',
default: false,
when: featureChoices.length !== 0 && Object.keys(featureConfigMap).length !== 0,
},
{
type: 'checkbox',
name: 'featureIds',
message: 'Измените список стандартных функций',
suffix: checkboxInstructionsTranslation,
loop: false,
choices: featureChoices,
pageSize: 12,
when: (answers) => answers.changeDefaults,
},
{
type: 'confirm',
name: 'installDependencies',
message: 'Выполнить установку зависимостей (npm install)?',
default: true,
},
];

const answers = await inquirer.prompt(questions);

const defaultFeatureToggles = featureToggles
.filter((toggle) => toggle.hidden && toggle.defaultValue)
.map((toggle) => toggle.featureId);

const featureIds = answers.featureIds
? answers.featureIds
: featureToggles.filter((toggle) => toggle.defaultValue).map((toggle) => toggle.featureId);

const withHiddenToggles = {
...answers,
featureIds: [...new Set([...featureIds, ...defaultFeatureToggles])], // оставляем уникальное объединение
};

const destination = output || `${runDir}/${answers.projectName}`;
const config = getConfigFromAnswers({
answers: withHiddenToggles,
featureConfigMap,
templatePath: `${resolvedTemplatePath}/`,
output: destination,
runDir,
});

await buildTemplateTask(config, [...defaultRules, rules]);

if (answers.installDependencies) {
console.log('');
console.log('Установка зависимостей');
console.log('');
execSync('npm i', {
cwd: destination,
encoding: 'UTF-8',
stdio: 'inherit',
});
console.log('');
console.log('Установка зависимостей завершена');
}

console.log('');
console.log(chalk.green('✅ Готово!'));
console.log('');
} catch (err) {
console.error(err);
} finally {
cleanUp();
}
}

run();
61 changes: 61 additions & 0 deletions src/config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
export function getConfigFromAnswers({ answers, featureConfigMap, templatePath, output, runDir }) {
const enabledFeatureIds = Object.entries(featureConfigMap)
.filter(([, feature]) => feature.test(answers.featureIds))
.map(([key]) => key);

const defaultFeatureToggles = Object.fromEntries(Object.keys(featureConfigMap).map((key) => [key, false]));
const featureToggles = [...enabledFeatureIds, ...answers.featureIds].reduce((acc, id) => {
acc[id] = true;

return acc;
}, {});

const configDiff = Object.keys(defaultFeatureToggles).reduce(
(acc, featureId) => {
const state = enabledFeatureIds.includes(featureId) ? 'on' : 'off';
const featureConfig = featureConfigMap[featureId].config({ templatePath });

acc.jsonChanges.push(...(featureConfig[state].jsonChanges || []));
acc.sourceAddons.push(...(featureConfig[state].sourceAddon || []));
acc.rename.push(...(featureConfig[state].rename || []));

return acc;
},
{
jsonChanges: [],
sourceAddons: [],
rename: [],
},
);

return {
featureToggles: {
...defaultFeatureToggles,
...featureToggles,
},
paths: {
default: {
source: [
`${templatePath}**/*`,
`!${templatePath}node_modules/**`,
`!${templatePath}build/**`,
`!${templatePath}.next/**`,
...configDiff.sourceAddons,
],
destination: output || `${runDir}/${answers.projectName}`,
},
},
rename: configDiff.rename,
jsonChanges: [
{
glob: `${templatePath}package.json`,
changes: {
merge: {
name: answers.projectName,
},
},
},
...configDiff.jsonChanges,
],
};
}
Loading

0 comments on commit f33e43a

Please sign in to comment.