@mrktg/project-generator 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,51 @@
1
+ # @mrktg/project-generator
2
+
3
+ Движок генератора лендингов на Astro. Публикуется в npm как `@mrktg/project-generator`. Ничего не знает о брендах и регионах: реестр брендов,
4
+ регион, региональный оверлей шаблона и темы приходят от регионального генератора.
5
+
6
+ Региональные генераторы — тонкие обёртки: `bin` с вызовом `main(config)`, свой `template/`
7
+ с `Layout.astro` под пакет компонентов региона, карта тем. Пользователь запускает их:
8
+ `npm create <регион>-project my-landing`.
9
+
10
+ ## Что делает движок
11
+
12
+ 1. Вопросы (`@clack/prompts`): имя проекта (или `.` — текущая папка), бренд из реестра,
13
+ путь после `/journey/`, ставить ли зависимости, делать ли `git init`. Любой ответ можно передать
14
+ флагом (`--brand`, `--path`, `--install`/`--no-install`, `--git`/`--no-git`), тогда вопрос не задаётся.
15
+ 2. `buildVars`: из бренда и региона собирает словарь для шаблона — прод-урл
16
+ `https://<хост бренда>/journey/<путь>/`, `base` для Astro, фавиконка и язык бренда, урлы
17
+ инфраструктуры региона (лоадер футера, clamp-scaling, лоадер аналитики), пакет и версия компонентов.
18
+ 3. `scaffold`: копирует `template/base` движка, поверх — `config.templateDir` регионального генератора.
19
+ Плейсхолдеры `{{KEY}}` подставляются, неизвестный ключ — ошибка. Файлы `_gitignore`, `_nvmrc`,
20
+ `_editorconfig` переименовываются в dot-файлы (npm выкидывает их при публикации).
21
+
22
+ ## Конфиг регионального генератора
23
+
24
+ ```js
25
+ import { main } from '@mrktg/project-generator';
26
+
27
+ main({
28
+ command: 'create-xx-project', // имя bin, для справки и README сгенерированного проекта
29
+ registry, // реестр брендов региона (brands-core IRegistry)
30
+ region, // { id, host, bucketHost, componentsPackage }
31
+ templateDir, // абсолютный путь к региональному оверлею шаблона
32
+ brandTheme: (brand) => 'theme', // какую тему пакета компонентов подключать бренду
33
+ componentsVersion: async (pkg) => '^1.0.0', // необязательно; по умолчанию — свежая из npm
34
+ });
35
+ ```
36
+
37
+ Региональный оверлей обязан содержать `src/layouts/Layout.astro` — базовый шаблон его импортирует.
38
+ Доступные плейсхолдеры: см. `src/vars.js`.
39
+
40
+ ## Урлы инфраструктуры
41
+
42
+ `src/urls.js` знает три точки входа на хосте региона: `/footers/loader.js`, `/clamp-scaling/v1`,
43
+ `/loader/loader.js`. Это публичные контракты соответствующих репозиториев; меняется контракт —
44
+ правится здесь.
45
+
46
+ ## Разработка
47
+
48
+ ```bash
49
+ npm install
50
+ npm test # node --test, на вымышленном регионе и бренде
51
+ ```
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "@mrktg/project-generator",
3
+ "version": "1.0.0",
4
+ "description": "Движок генератора лендингов: вопросы, шаблон Astro, подстановка. Бренды, регионы и региональные оверлеи приходят от региональных генераторов.",
5
+ "type": "module",
6
+ "files": [
7
+ "src",
8
+ "template",
9
+ "README.md"
10
+ ],
11
+ "scripts": {
12
+ "test": "node --test"
13
+ },
14
+ "engines": {
15
+ "node": ">=22.18"
16
+ },
17
+ "dependencies": {
18
+ "@clack/prompts": "^0.11.0"
19
+ },
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "git+ssh://git@gitlab.srv.team/marketing-common/project-generator.git"
23
+ },
24
+ "keywords": [
25
+ "astro",
26
+ "generator",
27
+ "landing"
28
+ ],
29
+ "main": "src/index.js",
30
+ "exports": {
31
+ ".": "./src/index.js"
32
+ },
33
+ "publishConfig": {
34
+ "access": "public"
35
+ }
36
+ }
package/src/args.js ADDED
@@ -0,0 +1,46 @@
1
+ import { parseArgs } from 'node:util';
2
+
3
+ /**
4
+ * npm create <region>-project my-project -- --brand <id> --path greece --no-install --no-git
5
+ * Любой ответ можно передать флагом, тогда вопрос не задаётся.
6
+ */
7
+ export const readArgs = (argv) => {
8
+ const { values, positionals } = parseArgs({
9
+ args: argv,
10
+ allowPositionals: true,
11
+ options: {
12
+ brand: { type: 'string' },
13
+ path: { type: 'string' },
14
+ install: { type: 'boolean' },
15
+ 'no-install': { type: 'boolean' },
16
+ git: { type: 'boolean' },
17
+ 'no-git': { type: 'boolean' },
18
+ help: { type: 'boolean', short: 'h' },
19
+ },
20
+ });
21
+
22
+ const flag = (yes, no) => (values[no] ? false : values[yes] ? true : undefined);
23
+
24
+ return {
25
+ help: values.help ?? false,
26
+ answers: {
27
+ projectName: positionals[0],
28
+ brandId: values.brand,
29
+ landingPath: values.path,
30
+ install: flag('install', 'no-install'),
31
+ git: flag('git', 'no-git'),
32
+ },
33
+ };
34
+ };
35
+
36
+ export const buildHelp = ({ command, registry }) => `
37
+ Использование:
38
+ npm create ${command.replace(/^create-/, '')} [имя-проекта | .] [-- флаги]
39
+
40
+ Флаги (каждый заменяет соответствующий вопрос):
41
+ --brand <id> бренд: ${registry.brands.map((brand) => `${brand.id} (${brand.title.en})`).join(', ')}
42
+ --path <slug> путь лендинга после /journey/ (прод-урл: https://<хост бренда>/journey/<slug>/)
43
+ --install / --no-install
44
+ --git / --no-git
45
+ -h, --help
46
+ `;
package/src/cli.js ADDED
@@ -0,0 +1,78 @@
1
+ import { spawn } from 'node:child_process';
2
+ import path from 'node:path';
3
+ import * as p from '@clack/prompts';
4
+ import { buildHelp, readArgs } from './args.js';
5
+ import { askQuestions } from './questions.js';
6
+ import { buildVars } from './vars.js';
7
+ import { scaffold } from './scaffold.js';
8
+
9
+ const run = (cmd, args, cwd) =>
10
+ new Promise((resolve, reject) => {
11
+ const child = spawn(cmd, args, { cwd, stdio: 'inherit', shell: process.platform === 'win32' });
12
+ child.on('exit', (code) => (code === 0 ? resolve() : reject(new Error(`${cmd} ${args.join(' ')} завершился с кодом ${code}`))));
13
+ child.on('error', reject);
14
+ });
15
+
16
+ /**
17
+ * Точка входа регионального генератора. Движок ничего не знает о брендах и регионах:
18
+ * всё это приходит в config от регионального пакета.
19
+ *
20
+ * config:
21
+ * command имя команды для справки, например create-xx-project
22
+ * registry реестр брендов региона (brands-core IRegistry)
23
+ * region регион с host, bucketHost, componentsPackage
24
+ * templateDir папка регионального оверлея шаблона (кладётся поверх template/base движка)
25
+ * brandTheme(brand) какую тему пакета компонентов подключать бренду
26
+ * componentsVersion(pkg)? версия пакета компонентов; по умолчанию — свежая из npm
27
+ */
28
+ export const runCli = async (config, argv = process.argv.slice(2)) => {
29
+ const { help, answers: initial } = readArgs(argv);
30
+ if (help) {
31
+ console.log(buildHelp(config));
32
+ return;
33
+ }
34
+
35
+ p.intro(config.command);
36
+
37
+ const answers = await askQuestions(config, initial);
38
+ const vars = await buildVars(config, answers);
39
+ const targetDir = path.resolve(process.cwd(), answers.projectName);
40
+
41
+ const spinner = p.spinner();
42
+ spinner.start('Копирую шаблон');
43
+ await scaffold(config, targetDir, vars);
44
+ spinner.stop(`Проект создан в ${path.relative(process.cwd(), targetDir) || 'текущей папке'}`);
45
+
46
+ if (answers.git) {
47
+ await run('git', ['init', '-q'], targetDir);
48
+ p.log.success('git init');
49
+ }
50
+ if (answers.install) {
51
+ p.log.step('npm install');
52
+ await run('npm', ['install'], targetDir);
53
+ }
54
+
55
+ p.note(
56
+ [
57
+ `Бренд: ${vars.BRAND_TITLE} (${vars.REGION})`,
58
+ `Прод: ${vars.PROD_URL}`,
59
+ `Компоненты: ${vars.COMPONENTS_PACKAGE}@${vars.COMPONENTS_VERSION}`,
60
+ `Футер: ${vars.FOOTER_LOADER_URL} (data-brand=${vars.BRAND_ID})`,
61
+ '',
62
+ answers.projectName === '.' ? '' : `cd ${answers.projectName}`,
63
+ answers.install ? '' : 'npm install',
64
+ 'npm run dev',
65
+ ]
66
+ .filter((line) => line !== '')
67
+ .join('\n'),
68
+ 'Дальше',
69
+ );
70
+ p.outro('Готово.');
71
+ };
72
+
73
+ /** Обёртка для bin регионального генератора: ошибки — в лог и код выхода 1. */
74
+ export const main = (config) =>
75
+ runCli(config).catch((error) => {
76
+ p.log.error(error.message);
77
+ process.exit(1);
78
+ });
@@ -0,0 +1,18 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { promisify } from 'node:util';
3
+
4
+ const execFileAsync = promisify(execFile);
5
+
6
+ /**
7
+ * Версия пакета компонентов для package.json нового проекта: свежая из npm.
8
+ * Если npm недоступен — `latest`, npm install разрешит его сам.
9
+ */
10
+ export const defaultComponentsVersion = async (pkg) => {
11
+ try {
12
+ const { stdout } = await execFileAsync('npm', ['view', pkg, 'version'], { timeout: 15_000 });
13
+ const version = stdout.trim();
14
+ return version ? `^${version}` : 'latest';
15
+ } catch {
16
+ return 'latest';
17
+ }
18
+ };
package/src/index.js ADDED
@@ -0,0 +1,4 @@
1
+ export { runCli, main } from './cli.js';
2
+ export { buildVars } from './vars.js';
3
+ export { scaffold } from './scaffold.js';
4
+ export { defaultComponentsVersion } from './components-version.js';
@@ -0,0 +1,89 @@
1
+ import path from 'node:path';
2
+ import * as p from '@clack/prompts';
3
+
4
+ /**
5
+ * Цепочка вопросов. Каждый вопрос — { id, ask(ctx) }: читает уже собранные ответы
6
+ * из ctx, возвращает свой. Ответ, переданный флагом CLI (ctx[id] уже есть),
7
+ * не переспрашивается — так генератор работает и без TTY.
8
+ *
9
+ * Чтобы добавить вопрос — добавить объект в массив и использовать ответ
10
+ * в buildVars (src/vars.js).
11
+ */
12
+
13
+ const PROJECT_NAME_RE = /^[a-z0-9][a-z0-9._-]*$/;
14
+
15
+ const validateProjectName = (value) => {
16
+ if (!value) return 'Введите имя проекта';
17
+ if (value === '.') return undefined;
18
+ if (!PROJECT_NAME_RE.test(value)) return 'Только строчные латинские буквы, цифры, «-», «_», «.»';
19
+ return undefined;
20
+ };
21
+
22
+ const LANDING_PATH_RE = /^[a-z0-9][a-z0-9-]*(\/[a-z0-9][a-z0-9-]*)*$/;
23
+
24
+ const validateLandingPath = (value) => {
25
+ if (!value) return 'Введите путь';
26
+ if (!LANDING_PATH_RE.test(value)) return 'Строчные латинские буквы, цифры и «-», без ведущего и конечного слэша: greece или promo/greece';
27
+ return undefined;
28
+ };
29
+
30
+ export const buildQuestions = ({ registry }) => [
31
+ {
32
+ id: 'projectName',
33
+ ask: () =>
34
+ p.text({
35
+ message: 'Имя проекта (папка и name в package.json; «.» — текущая папка)',
36
+ placeholder: 'my-landing',
37
+ validate: validateProjectName,
38
+ }),
39
+ validate: validateProjectName,
40
+ },
41
+ {
42
+ id: 'brandId',
43
+ ask: () =>
44
+ p.select({
45
+ message: 'WHITE_LABEL — под какой бренд лендинг?',
46
+ options: registry.brands.map((brand) => ({ value: brand.id, label: brand.title.en })),
47
+ }),
48
+ validate: (value) => (registry.isBrandId(value) ? undefined : `Неизвестный бренд «${value}». Есть: ${registry.brandIds.join(', ')}`),
49
+ },
50
+ {
51
+ id: 'landingPath',
52
+ ask: (ctx) =>
53
+ p.text({
54
+ message: `Путь лендинга после /journey/ (${registry.getBrand(ctx.brandId).siteHost}/journey/…)`,
55
+ // initialValue, а не defaultValue: default подставляется после валидации и пустой ввод не проходит
56
+ initialValue: ctx.projectName === '.' ? path.basename(process.cwd()) : ctx.projectName,
57
+ validate: validateLandingPath,
58
+ }),
59
+ validate: validateLandingPath,
60
+ },
61
+ {
62
+ id: 'install',
63
+ ask: () => p.confirm({ message: 'Установить зависимости (npm install)?', initialValue: true }),
64
+ },
65
+ {
66
+ id: 'git',
67
+ ask: () => p.confirm({ message: 'Инициализировать git-репозиторий?', initialValue: true }),
68
+ },
69
+ ];
70
+
71
+ export const askQuestions = async (config, initial) => {
72
+ const ctx = { ...initial };
73
+
74
+ for (const question of buildQuestions(config)) {
75
+ if (ctx[question.id] !== undefined) {
76
+ const error = question.validate?.(ctx[question.id]);
77
+ if (error) throw new Error(`--${question.id}: ${error}`);
78
+ continue;
79
+ }
80
+ const answer = await question.ask(ctx);
81
+ if (p.isCancel(answer)) {
82
+ p.cancel('Отменено.');
83
+ process.exit(0);
84
+ }
85
+ ctx[question.id] = answer;
86
+ }
87
+
88
+ return ctx;
89
+ };
package/src/render.js ADDED
@@ -0,0 +1,10 @@
1
+ const PLACEHOLDER_RE = /\{\{([A-Z0-9_]+)\}\}/g;
2
+
3
+ /** Подставляет {{KEY}} из vars. Неизвестный ключ — ошибка, чтобы шаблон не разъезжался с vars.js. */
4
+ export const render = (source, vars, fileName = '') =>
5
+ source.replace(PLACEHOLDER_RE, (_, key) => {
6
+ if (!(key in vars)) {
7
+ throw new Error(`В шаблоне ${fileName} плейсхолдер {{${key}}}, которого нет в buildVars()`);
8
+ }
9
+ return String(vars[key]);
10
+ });
@@ -0,0 +1,51 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { render } from './render.js';
5
+
6
+ const TEMPLATE_ROOT = fileURLToPath(new URL('../template/', import.meta.url));
7
+
8
+ /** npm при публикации выкидывает .gitignore и т.п., поэтому в шаблоне они лежат с «_». */
9
+ const RENAMES = { _gitignore: '.gitignore', _npmrc: '.npmrc', _editorconfig: '.editorconfig', _nvmrc: '.nvmrc' };
10
+
11
+ const BINARY_EXT = new Set(['.png', '.jpg', '.jpeg', '.webp', '.gif', '.ico', '.woff', '.woff2']);
12
+
13
+ const copyDir = async (from, to, vars) => {
14
+ const entries = await fs.readdir(from, { withFileTypes: true });
15
+ for (const entry of entries) {
16
+ const src = path.join(from, entry.name);
17
+ const dest = path.join(to, RENAMES[entry.name] ?? entry.name);
18
+ if (entry.isDirectory()) {
19
+ await fs.mkdir(dest, { recursive: true });
20
+ await copyDir(src, dest, vars);
21
+ continue;
22
+ }
23
+ if (BINARY_EXT.has(path.extname(entry.name))) {
24
+ await fs.copyFile(src, dest);
25
+ continue;
26
+ }
27
+ const source = await fs.readFile(src, 'utf8');
28
+ await fs.writeFile(dest, render(source, vars, src));
29
+ }
30
+ };
31
+
32
+ /**
33
+ * Собирает проект: template/base движка, поверх него региональный оверлей config.templateDir.
34
+ * Файл из оверлея перекрывает одноимённый файл базы.
35
+ */
36
+ export const scaffold = async ({ templateDir }, targetDir, vars) => {
37
+ await fs.mkdir(targetDir, { recursive: true });
38
+ const existing = await fs.readdir(targetDir);
39
+ if (existing.length > 0) {
40
+ throw new Error(`Папка ${targetDir} не пустая`);
41
+ }
42
+
43
+ for (const layerDir of [path.join(TEMPLATE_ROOT, 'base'), templateDir]) {
44
+ try {
45
+ await fs.access(layerDir);
46
+ } catch {
47
+ throw new Error(`Нет слоя шаблона ${layerDir}`);
48
+ }
49
+ await copyDir(layerDir, targetDir, vars);
50
+ }
51
+ };
package/src/urls.js ADDED
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Урлы инфраструктуры региона. Реестр брендов знает только хост, схемы путей принадлежат
3
+ * репозиториям, которые туда выкладывают. Здесь ровно три точки входа, дальше каждая система
4
+ * разбирается сама (лоадер футера — с брендом и языком, clamp-scaling — с брейкпоинтами).
5
+ */
6
+
7
+ /** live-footer-loader: один скрипт, сборку футера выбирает сам. */
8
+ export const footerLoaderUrl = (region) => `${region.host}/footers/loader.js`;
9
+
10
+ /** clamp-scaling: два файла, версия зашита в путь. */
11
+ export const clampScalingBaseUrl = (region) => `${region.host}/clamp-scaling/v1`;
12
+
13
+ /** Лоадер аналитики региона. */
14
+ export const analyticsLoaderUrl = (region) => `${region.host}/loader/loader.js`;
package/src/vars.js ADDED
@@ -0,0 +1,41 @@
1
+ import { analyticsLoaderUrl, clampScalingBaseUrl, footerLoaderUrl } from './urls.js';
2
+ import { defaultComponentsVersion } from './components-version.js';
3
+
4
+ /**
5
+ * Из ответов на вопросы собирает плоский словарь для подстановки в шаблон.
6
+ * Ключ словаря = имя плейсхолдера {{KEY}} в файлах шаблона.
7
+ */
8
+ export const buildVars = async (config, answers) => {
9
+ const { registry, region, brandTheme, componentsVersion = defaultComponentsVersion } = config;
10
+ const brand = registry.getBrand(answers.brandId);
11
+ // Все лендинги живут на https://<хост бренда>/journey/<путь>/
12
+ const prodUrl = new URL(`/journey/${answers.landingPath}/`, registry.siteUrl(brand.id));
13
+ // base для Astro: без конечного слэша, /journey/<путь>
14
+ const basePath = prodUrl.pathname.replace(/\/$/, '');
15
+
16
+ return {
17
+ // «.» — проект в текущей папке, имя пакета берём из её названия
18
+ PROJECT_NAME: answers.projectName === '.' ? basenameOfCwd() : answers.projectName,
19
+ GENERATOR: config.command,
20
+ BRAND_ID: brand.id,
21
+ BRAND_TITLE: brand.title.en,
22
+ REGION: region.id,
23
+ LANG: brand.siteLang,
24
+ THEME: brandTheme(brand),
25
+ PROD_URL: prodUrl.href,
26
+ LANDING_PATH: answers.landingPath,
27
+ BASE_PATH: basePath,
28
+ PROD_ORIGIN: prodUrl.origin,
29
+ // Та же иконка, что у основного продукта бренда
30
+ FAVICON_URL: brand.favicon.href,
31
+ FAVICON_TYPE: brand.favicon.type,
32
+ COMPONENTS_PACKAGE: region.componentsPackage,
33
+ COMPONENTS_VERSION: await componentsVersion(region.componentsPackage),
34
+ ANALYTICS_LOADER_URL: analyticsLoaderUrl(region),
35
+ CLAMP_SCALING_BASE_URL: clampScalingBaseUrl(region),
36
+ FOOTER_LOADER_URL: footerLoaderUrl(region),
37
+ ASSETS_HOST: region.host,
38
+ };
39
+ };
40
+
41
+ const basenameOfCwd = () => process.cwd().split(/[\\/]/).filter(Boolean).at(-1);
@@ -0,0 +1,5 @@
1
+ /** @type {import("prettier").Config} */
2
+ export default {
3
+ plugins: ["prettier-plugin-astro"],
4
+ overrides: [{ files: "*.astro", options: { parser: "astro" } }],
5
+ };
@@ -0,0 +1,46 @@
1
+ # {{PROJECT_NAME}}
2
+
3
+ Лендинг **{{BRAND_TITLE}}** на [Astro](https://astro.build/). Сгенерирован `{{GENERATOR}}`.
4
+
5
+ | Параметр | Значение |
6
+ | -------------- | ------------------------------------------- |
7
+ | Бренд | {{BRAND_TITLE}} (`{{BRAND_ID}}`) |
8
+ | Регион | `{{REGION}}` |
9
+ | Прод | {{PROD_URL}} |
10
+ | Локально | http://localhost:4321{{BASE_PATH}}/ |
11
+ | Компоненты | `{{COMPONENTS_PACKAGE}}` |
12
+ | Хост ассетов | {{ASSETS_HOST}} |
13
+ | Футер | {{FOOTER_LOADER_URL}}, `data-brand={{BRAND_ID}}` |
14
+
15
+ Эти значения также лежат в `src/site.config.ts`.
16
+
17
+ ## Что уже подключено
18
+
19
+ - **Тема компонентов** — `ThemeProvider` в `src/layouts/Layout.astro`: шрифты и токены бренда.
20
+ - **Флюидная вёрстка** — `clamp-scaling` (`base.css` + `media.css`) в `<head>`. Верстаем в `rem`,
21
+ 1rem = 1px макета. Если флюид нужен не на всех страницах, `<link>` переносятся из общего layout
22
+ на конкретную страницу.
23
+ - **Аналитика** — лоадер региона `{{ANALYTICS_LOADER_URL}}` в `<head>`.
24
+ - **Живой футер** — `src/components/LiveFooter.astro`: лоадер из `live-footer-loader`, сборку выбирает по `data-brand`,
25
+ язык берёт из `<html lang>`, fluid включает сам по переменной clamp-scaling.
26
+
27
+ ## Команды
28
+
29
+ | Команда | Описание |
30
+ | ----------------- | ----------------------------------------- |
31
+ | `npm install` | Установить зависимости |
32
+ | `npm run dev` | Сервер разработки, http://localhost:4321{{BASE_PATH}}/ |
33
+ | `npm run build` | Собрать проект для продакшена (`./dist/`) |
34
+ | `npm run preview` | Локальный предпросмотр сборки |
35
+ | `npm run pack` | Собрать `dist.zip` для публикации |
36
+
37
+ ## Развертывание
38
+
39
+ 1. `npm run pack`
40
+ 2. Выложить `dist.zip` через админку.
41
+
42
+ ## Документация
43
+
44
+ - [Astro](https://docs.astro.build)
45
+ - [React](https://react.dev/)
46
+ - Компоненты: `node_modules/{{COMPONENTS_PACKAGE}}/README.md`
@@ -0,0 +1,9 @@
1
+ root = true
2
+
3
+ [*]
4
+ charset = utf-8
5
+ indent_style = space
6
+ indent_size = 2
7
+ end_of_line = lf
8
+ insert_final_newline = true
9
+ trim_trailing_whitespace = true
@@ -0,0 +1,24 @@
1
+ # build output
2
+ dist/
3
+ dist.zip
4
+ # generated types
5
+ .astro/
6
+
7
+ # dependencies
8
+ node_modules/
9
+
10
+ # logs
11
+ npm-debug.log*
12
+ yarn-debug.log*
13
+ yarn-error.log*
14
+ pnpm-debug.log*
15
+
16
+ # environment variables
17
+ .env
18
+ .env.production
19
+
20
+ # macOS-specific files
21
+ .DS_Store
22
+
23
+ # jetbrains setting folder
24
+ .idea/
@@ -0,0 +1 @@
1
+ 24
@@ -0,0 +1,20 @@
1
+ import { defineConfig } from "astro/config";
2
+ import svgr from "vite-plugin-svgr";
3
+ import react from "@astrojs/react";
4
+
5
+ // https://astro.build/config
6
+ export default defineConfig({
7
+ // Хост бренда: канонические ссылки, og:url, sitemap.
8
+ site: "{{PROD_ORIGIN}}",
9
+ // Путь лендинга: в проде https://<хост>/journey/<путь>/, локально http://localhost:4321/journey/<путь>/
10
+ base: "{{BASE_PATH}}",
11
+ build: {
12
+ format: "directory",
13
+ // Относительные пути к ассетам: архив статики кладётся в админку по любому пути.
14
+ assetsPrefix: "./",
15
+ },
16
+ integrations: [react()],
17
+ vite: {
18
+ plugins: [svgr()],
19
+ },
20
+ });
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "{{PROJECT_NAME}}",
3
+ "type": "module",
4
+ "version": "1.0.0",
5
+ "private": true,
6
+ "scripts": {
7
+ "dev": "astro dev",
8
+ "build": "astro build",
9
+ "preview": "astro preview",
10
+ "astro": "astro",
11
+ "prettier": "npx prettier . --write",
12
+ "zip": "zip -r dist.zip dist",
13
+ "pack": "npm run build && rm -rf dist.zip && npm run zip"
14
+ },
15
+ "dependencies": {
16
+ "@astrojs/react": "^4.2.0",
17
+ "@types/react": "^19.0.10",
18
+ "@types/react-dom": "^19.0.4",
19
+ "astro": "^5.3.1",
20
+ "classnames": "^2.5.1",
21
+ "{{COMPONENTS_PACKAGE}}": "{{COMPONENTS_VERSION}}",
22
+ "react": "^19.0.0",
23
+ "react-dom": "^19.0.0"
24
+ },
25
+ "devDependencies": {
26
+ "prettier": "^3.5.2",
27
+ "prettier-plugin-astro": "^0.14.1",
28
+ "vite-plugin-svgr": "^4.3.0"
29
+ }
30
+ }
@@ -0,0 +1,34 @@
1
+ ---
2
+ /**
3
+ * Демо-секция, чтобы проект сразу собирался и было видно флюидную вёрстку.
4
+ * Размеры в rem: 1rem = 1px макета благодаря clamp-scaling из Layout.
5
+ * Заменить на реальные секции лендинга.
6
+ */
7
+ import { SITE } from "../site.config";
8
+ ---
9
+
10
+ <section class="hero">
11
+ <h1 class="hero__title">{SITE.brandTitle}</h1>
12
+ <p class="hero__text">Лендинг сгенерирован из стаба. Регион: {SITE.region}, прод: {SITE.prodUrl}</p>
13
+ </section>
14
+
15
+ <style>
16
+ .hero {
17
+ padding: 80rem 24rem;
18
+ max-width: 1440rem;
19
+ margin: 0 auto;
20
+ box-sizing: border-box;
21
+ }
22
+
23
+ .hero__title {
24
+ margin: 0 0 16rem;
25
+ font-size: 48rem;
26
+ line-height: 1.1;
27
+ }
28
+
29
+ .hero__text {
30
+ margin: 0;
31
+ font-size: 18rem;
32
+ line-height: 1.4;
33
+ }
34
+ </style>
@@ -0,0 +1,10 @@
1
+ ---
2
+ /**
3
+ * Живой футер бренда. Подключается через лоадер из репозитория live-footer-loader:
4
+ * он сам выбирает сборку по data-brand, язык берёт из <html lang>, fluid — по переменной
5
+ * clamp-scaling на :root. Схема урлов сборок футера лендингу неизвестна и не нужна.
6
+ * Настройки футера (data-max-width, data-target и др.) пишутся на этом же теге.
7
+ */
8
+ ---
9
+
10
+ <script src="{{FOOTER_LOADER_URL}}" data-brand="{{BRAND_ID}}" async is:inline></script>
@@ -0,0 +1,10 @@
1
+ ---
2
+ import Layout from "../layouts/Layout.astro";
3
+ import Hero from "../components/Hero.astro";
4
+ import LiveFooter from "../components/LiveFooter.astro";
5
+ ---
6
+
7
+ <Layout>
8
+ <Hero />
9
+ <LiveFooter />
10
+ </Layout>
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Параметры лендинга, заданные при генерации ({{GENERATOR}}).
3
+ * Единственное место, где живут бренд, регион и прод-урл — остальной код читает отсюда.
4
+ */
5
+ export const SITE = {
6
+ brand: "{{BRAND_ID}}",
7
+ brandTitle: "{{BRAND_TITLE}}",
8
+ region: "{{REGION}}",
9
+ lang: "{{LANG}}",
10
+ prodUrl: "{{PROD_URL}}",
11
+ assetsHost: "{{ASSETS_HOST}}",
12
+ } as const;
@@ -0,0 +1,10 @@
1
+ {
2
+ "extends": "astro/tsconfigs/strict",
3
+ "include": [".astro/types.d.ts", "**/*"],
4
+ "exclude": ["dist"],
5
+ "compilerOptions": {
6
+ "jsx": "react-jsx",
7
+ "jsxImportSource": "react",
8
+ "moduleResolution": "node"
9
+ }
10
+ }
@@ -0,0 +1,9 @@
1
+ /// <reference types="vite-plugin-svgr/client" />
2
+
3
+ declare global {
4
+ interface Window {
5
+ dataLayer: Record<string, unknown>[];
6
+ }
7
+ }
8
+
9
+ export {};