core-maugli 1.2.3 → 1.2.5
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 +13 -0
- package/package.json +6 -13
- package/public/img/examples/blog/previews/post-1-avtomatizaciya-marketinga-kak-ii-osvobozhdaet-predprinimatelei-ot-cifrovogo-rabstva.webp +0 -0
- package/public/img/examples/blog/previews/post-2-avtomatizaciya-kontenta-kak-neiroseti-ubivayut-perfekcionizm-v-biznese.webp +0 -0
- package/public/img/examples/blog/previews/post-3-laik-ne-valyuta-kak-avtomatizaciya-marketinga-spasaet-ot-lozhnyh-metrik.webp +0 -0
- package/public/img/examples/blog/previews/post-5-5-fatalnyh-oshibok-marketinga-kotorye-ubivayut-startapy-na-starte.webp +0 -0
- package/public/img/examples/blog/previews/post-6-5-strategii-kontent-marketinga-dlya-startapov-avtomatizaciya-i-revolyuciya.webp +0 -0
- package/public/img/examples/blog/previews/post-7-viralnyi-kontent-ne-udacha-a-strategiya-avtomatizaciya-marketinga.webp +0 -0
- package/public/img/examples/blog/previews/post-agent-experience-mcp-biznes-v-epohu-ii-agentov.webp +0 -0
- package/public/img/examples/blog/previews/post_11.webp +0 -0
- package/public/img/examples/blog/previews/post_12.webp +0 -0
- package/public/img/examples/blog/previews/post_1_jsonld_guide.webp +0 -0
- package/public/img/examples/blog/previews/test-post.webp +0 -0
- package/public/img/examples/blog/previews/tr-post-1.webp +0 -0
- package/public/img/examples/products/previews/product_1.webp +0 -0
- package/public/img/examples/products/previews/product_2.webp +0 -0
- package/public/img/examples/projects/previews/project_1.webp +0 -0
- package/public/img/examples/projects/previews/project_2.webp +0 -0
- package/scripts/generate-previews.js +175 -0
- package/scripts/update-components.js +175 -0
- package/scripts/upgrade-config.js +23 -3
- package/src/components/LanguageSwitcher.astro +2 -16
- package/astro.config.mjs +0 -92
- package/bin/init.js +0 -201
- package/resize-all.cjs +0 -29
- package/tsconfig.json +0 -8
- package/typograf-batch.js +0 -49
- package/vite.config.js +0 -11
package/README.md
CHANGED
@@ -87,6 +87,19 @@ Your blog will be available at `http://localhost:4321/`
|
|
87
87
|
npm run build
|
88
88
|
```
|
89
89
|
|
90
|
+
## Component Updates & Customization
|
91
|
+
|
92
|
+
**Important**: Maugli Blog is designed for centralized component updates. All components (`src/components/`, `src/layouts/`, `src/pages/`, etc.) are automatically updated to the latest version when you update the package with `npm install --save core-maugli@latest`. This ensures you always receive the latest features, bug fixes, and improvements.
|
93
|
+
|
94
|
+
This centralized update approach **does not affect**:
|
95
|
+
|
96
|
+
- Your content (`src/content/`)
|
97
|
+
- Your Maugli configuration (`src/config/maugli.config.ts`)
|
98
|
+
- Your custom styles (`src/styles/global.css` - preserved if customized)
|
99
|
+
- Your project settings (`package.json`, `astro.config.mjs`, etc.)
|
100
|
+
|
101
|
+
Only the core blog components are updated, while your customizations and settings remain intact.
|
102
|
+
|
90
103
|
`npm run build` runs [`scripts/verify-assets.js`](scripts/verify-assets.js)
|
91
104
|
before the Astro build. This script checks the SHA-256 hashes of the
|
92
105
|
floating label component and footer badge to ensure they haven't been
|
package/package.json
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
"name": "core-maugli",
|
3
3
|
"description": "Astro & Tailwind CSS blog theme for Maugli.",
|
4
4
|
"type": "module",
|
5
|
-
"version": "1.2.
|
5
|
+
"version": "1.2.5",
|
6
6
|
"license": "GPL-3.0-or-later OR Commercial",
|
7
7
|
"repository": {
|
8
8
|
"type": "git",
|
@@ -21,15 +21,17 @@
|
|
21
21
|
"typograf": "node typograf-batch.js",
|
22
22
|
"dev": "astro dev",
|
23
23
|
"start": "astro dev",
|
24
|
-
"build": "node typograf-batch.js && node scripts/verify-assets.js && astro build",
|
24
|
+
"build": "node typograf-batch.js && node scripts/generate-previews.js && node scripts/verify-assets.js && astro build",
|
25
25
|
"test": "node tests/examplesFilter.test.ts",
|
26
26
|
"astro": "astro",
|
27
27
|
"featured:add": "node scripts/featured.js add",
|
28
28
|
"featured:remove": "node scripts/featured.js remove",
|
29
29
|
"featured:list": "node scripts/featured.js list",
|
30
30
|
"upgrade": "node scripts/upgrade-config.js",
|
31
|
+
"update-components": "node scripts/update-components.js",
|
31
32
|
"backup-update": "node scripts/update-with-backup.js",
|
32
|
-
"postinstall": "node scripts/upgrade-config.js"
|
33
|
+
"postinstall": "node scripts/upgrade-config.js",
|
34
|
+
"generate-previews": "node scripts/generate-previews.js"
|
33
35
|
},
|
34
36
|
"dependencies": {
|
35
37
|
"@astrojs/mdx": "^4.3.0",
|
@@ -61,17 +63,8 @@
|
|
61
63
|
"files": [
|
62
64
|
"src",
|
63
65
|
"public",
|
64
|
-
"scripts"
|
65
|
-
"typograf-batch.js",
|
66
|
-
"resize-all.cjs",
|
67
|
-
"bin",
|
68
|
-
"astro.config.mjs",
|
69
|
-
"tsconfig.json",
|
70
|
-
"vite.config.js",
|
71
|
-
"LICENSE",
|
72
|
-
"README.md"
|
66
|
+
"scripts"
|
73
67
|
],
|
74
|
-
"main": "bin/index.js",
|
75
68
|
"bin": {
|
76
69
|
"core-maugli": "bin/index.js"
|
77
70
|
}
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
package/public/img/examples/blog/previews/post-agent-experience-mcp-biznes-v-epohu-ii-agentov.webp
ADDED
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
@@ -0,0 +1,175 @@
|
|
1
|
+
import fs from 'fs';
|
2
|
+
import path from 'path';
|
3
|
+
import sharp from 'sharp';
|
4
|
+
import { fileURLToPath } from 'url';
|
5
|
+
|
6
|
+
const __filename = fileURLToPath(import.meta.url);
|
7
|
+
const __dirname = path.dirname(__filename);
|
8
|
+
|
9
|
+
// Универсальное определение корня проекта
|
10
|
+
const rootDir = __dirname.includes('node_modules')
|
11
|
+
? path.join(__dirname, '../../..')
|
12
|
+
: path.join(__dirname, '..');
|
13
|
+
|
14
|
+
const previewWidth = 400;
|
15
|
+
const previewHeight = 210;
|
16
|
+
|
17
|
+
// Функция для извлечения путей изображений из markdown файлов
|
18
|
+
function extractImagePaths() {
|
19
|
+
const imagePaths = new Set();
|
20
|
+
const contentDir = path.join(rootDir, 'src/content');
|
21
|
+
|
22
|
+
function scanDirectory(dir) {
|
23
|
+
if (!fs.existsSync(dir)) return;
|
24
|
+
|
25
|
+
const items = fs.readdirSync(dir);
|
26
|
+
|
27
|
+
for (const item of items) {
|
28
|
+
const itemPath = path.join(dir, item);
|
29
|
+
const stat = fs.statSync(itemPath);
|
30
|
+
|
31
|
+
if (stat.isDirectory()) {
|
32
|
+
scanDirectory(itemPath);
|
33
|
+
} else if (item.endsWith('.md') || item.endsWith('.mdx')) {
|
34
|
+
const content = fs.readFileSync(itemPath, 'utf-8');
|
35
|
+
|
36
|
+
// Извлекаем изображения из frontmatter (image: путь)
|
37
|
+
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
38
|
+
if (frontmatterMatch) {
|
39
|
+
const frontmatter = frontmatterMatch[1];
|
40
|
+
// Улучшенные регулярки для frontmatter
|
41
|
+
const imageMatches = frontmatter.match(/(?:^|\n)\s*(?:image|src):\s*['"]*([^'">\s\n]+)['"]*(?:\s|$)/g);
|
42
|
+
if (imageMatches) {
|
43
|
+
imageMatches.forEach(match => {
|
44
|
+
const imagePath = match.replace(/.*?(?:image|src):\s*['"]*/, '').replace(/['"]*\s*$/, '');
|
45
|
+
if (imagePath && !imagePath.startsWith('http') && imagePath.includes('/') && !imagePath.includes('authors/')) {
|
46
|
+
imagePaths.add(imagePath);
|
47
|
+
}
|
48
|
+
});
|
49
|
+
}
|
50
|
+
}
|
51
|
+
|
52
|
+
// Извлекаем изображения из содержимого markdown ( и <img src="путь">)
|
53
|
+
const imgMatches = content.match(/!\[.*?\]\(([^)]+)\)|<img[^>]+src\s*=\s*['"]*([^'">\s]+)['"]*[^>]*>/g);
|
54
|
+
if (imgMatches) {
|
55
|
+
imgMatches.forEach(match => {
|
56
|
+
let imagePath;
|
57
|
+
if (match.startsWith('![')) {
|
58
|
+
imagePath = match.match(/!\[.*?\]\(([^)]+)\)/)?.[1];
|
59
|
+
} else {
|
60
|
+
imagePath = match.match(/src\s*=\s*['"]*([^'">\s]+)['"]*/)?.[1];
|
61
|
+
}
|
62
|
+
if (imagePath && !imagePath.startsWith('http') && imagePath.includes('/') && !imagePath.includes('authors/')) {
|
63
|
+
imagePaths.add(imagePath);
|
64
|
+
}
|
65
|
+
});
|
66
|
+
}
|
67
|
+
}
|
68
|
+
}
|
69
|
+
}
|
70
|
+
|
71
|
+
scanDirectory(contentDir);
|
72
|
+
|
73
|
+
// Также добавляем изображения из public/img/examples/ кроме авторов
|
74
|
+
const examplesDir = path.join(rootDir, 'public/img/examples');
|
75
|
+
if (fs.existsSync(examplesDir)) {
|
76
|
+
function addExampleImages(dir, relativePath = '') {
|
77
|
+
// Пропускаем папку authors - аватары не нужно обрабатывать
|
78
|
+
if (relativePath.includes('authors/')) return;
|
79
|
+
|
80
|
+
const items = fs.readdirSync(dir);
|
81
|
+
for (const item of items) {
|
82
|
+
const itemPath = path.join(dir, item);
|
83
|
+
const stat = fs.statSync(itemPath);
|
84
|
+
|
85
|
+
if (stat.isDirectory()) {
|
86
|
+
// Пропускаем папку authors
|
87
|
+
if (item === 'authors') continue;
|
88
|
+
addExampleImages(itemPath, `${relativePath}${item}/`);
|
89
|
+
} else if (item.match(/\.(webp|jpg|jpeg|png)$/i) && !dir.includes('previews')) {
|
90
|
+
imagePaths.add(`/img/examples/${relativePath}${item}`);
|
91
|
+
}
|
92
|
+
}
|
93
|
+
}
|
94
|
+
addExampleImages(examplesDir);
|
95
|
+
}
|
96
|
+
|
97
|
+
return Array.from(imagePaths);
|
98
|
+
}
|
99
|
+
|
100
|
+
// Функция для очистки всех существующих превьюшек
|
101
|
+
function cleanupExistingPreviews() {
|
102
|
+
const publicDir = path.join(rootDir, 'public');
|
103
|
+
|
104
|
+
function removePreviewDirs(dir) {
|
105
|
+
if (!fs.existsSync(dir)) return;
|
106
|
+
|
107
|
+
const items = fs.readdirSync(dir);
|
108
|
+
|
109
|
+
for (const item of items) {
|
110
|
+
const itemPath = path.join(dir, item);
|
111
|
+
const stat = fs.statSync(itemPath);
|
112
|
+
|
113
|
+
if (stat.isDirectory()) {
|
114
|
+
if (item === 'previews') {
|
115
|
+
console.log(`Removing existing preview directory: ${itemPath}`);
|
116
|
+
fs.rmSync(itemPath, { recursive: true, force: true });
|
117
|
+
} else {
|
118
|
+
removePreviewDirs(itemPath);
|
119
|
+
}
|
120
|
+
}
|
121
|
+
}
|
122
|
+
}
|
123
|
+
|
124
|
+
removePreviewDirs(publicDir);
|
125
|
+
}
|
126
|
+
|
127
|
+
// Функция для создания превьюшки
|
128
|
+
async function createPreview(imagePath) {
|
129
|
+
const fullImagePath = path.join(rootDir, 'public', imagePath.replace(/^\//, ''));
|
130
|
+
|
131
|
+
if (!fs.existsSync(fullImagePath)) {
|
132
|
+
console.warn(`Image not found: ${fullImagePath}`);
|
133
|
+
return;
|
134
|
+
}
|
135
|
+
|
136
|
+
const dir = path.dirname(fullImagePath);
|
137
|
+
const ext = path.extname(fullImagePath);
|
138
|
+
const name = path.basename(fullImagePath, ext);
|
139
|
+
const previewPath = path.join(dir, 'previews', `${name}${ext}`);
|
140
|
+
|
141
|
+
// Создаем папку previews если её нет
|
142
|
+
const previewDir = path.dirname(previewPath);
|
143
|
+
if (!fs.existsSync(previewDir)) {
|
144
|
+
fs.mkdirSync(previewDir, { recursive: true });
|
145
|
+
}
|
146
|
+
|
147
|
+
try {
|
148
|
+
await sharp(fullImagePath)
|
149
|
+
.resize(previewWidth, previewHeight, { fit: 'cover' })
|
150
|
+
.toFile(previewPath);
|
151
|
+
|
152
|
+
console.log(`Preview created: ${previewPath}`);
|
153
|
+
} catch (error) {
|
154
|
+
console.error(`Error processing ${fullImagePath}:`, error.message);
|
155
|
+
}
|
156
|
+
}
|
157
|
+
|
158
|
+
// Основная функция
|
159
|
+
async function generatePreviews() {
|
160
|
+
console.log('Cleaning up existing previews...');
|
161
|
+
cleanupExistingPreviews();
|
162
|
+
|
163
|
+
console.log('Scanning content for images...');
|
164
|
+
const imagePaths = extractImagePaths();
|
165
|
+
|
166
|
+
console.log(`Found ${imagePaths.length} images to process`);
|
167
|
+
|
168
|
+
for (const imagePath of imagePaths) {
|
169
|
+
await createPreview(imagePath);
|
170
|
+
}
|
171
|
+
|
172
|
+
console.log('Preview generation completed!');
|
173
|
+
}
|
174
|
+
|
175
|
+
generatePreviews().catch(console.error);
|
@@ -0,0 +1,175 @@
|
|
1
|
+
#!/usr/bin/env node
|
2
|
+
|
3
|
+
import fs from 'fs/promises';
|
4
|
+
import path from 'path';
|
5
|
+
import { fileURLToPath, pathToFileURL } from 'url';
|
6
|
+
|
7
|
+
const __filename = fileURLToPath(import.meta.url);
|
8
|
+
const __dirname = path.dirname(__filename);
|
9
|
+
|
10
|
+
// Определяем корневые папки
|
11
|
+
const isInNodeModules = __dirname.includes('node_modules');
|
12
|
+
const isSourceProject = !isInNodeModules && (__dirname.includes('core-maugli-blog') || process.cwd().includes('core-maugli-blog'));
|
13
|
+
|
14
|
+
const packageRoot = isInNodeModules
|
15
|
+
? path.join(__dirname, '../../..', 'node_modules', 'core-maugli') // из node_modules
|
16
|
+
: path.join(__dirname, '..'); // из исходников
|
17
|
+
|
18
|
+
const userRoot = isInNodeModules
|
19
|
+
? path.join(__dirname, '../../..') // корень пользовательского проекта
|
20
|
+
: process.env.INIT_CWD || process.cwd(); // для разработки
|
21
|
+
|
22
|
+
// Список папок и файлов для полного обновления (перезаписи)
|
23
|
+
const FORCE_UPDATE_PATHS = [
|
24
|
+
'src/components',
|
25
|
+
'src/layouts',
|
26
|
+
'src/pages',
|
27
|
+
'src/utils',
|
28
|
+
'src/scripts',
|
29
|
+
'src/icons',
|
30
|
+
'src/i18n',
|
31
|
+
'public/flags',
|
32
|
+
'public/img/default'
|
33
|
+
// Исключили src/styles - может содержать пользовательские стили
|
34
|
+
];
|
35
|
+
|
36
|
+
// Список файлов, которые НЕ должны перезаписываться (пользовательские)
|
37
|
+
const PRESERVE_PATHS = [
|
38
|
+
'src/content',
|
39
|
+
'src/config/maugli.config.ts', // обновляется через upgrade-config.js
|
40
|
+
'src/styles/global.css', // может быть кастомизирован пользователем
|
41
|
+
'package.json',
|
42
|
+
'astro.config.mjs',
|
43
|
+
'tailwind.config.js',
|
44
|
+
'tsconfig.json'
|
45
|
+
];
|
46
|
+
|
47
|
+
async function copyDirectory(src, dest) {
|
48
|
+
try {
|
49
|
+
await fs.mkdir(dest, { recursive: true });
|
50
|
+
|
51
|
+
const entries = await fs.readdir(src, { withFileTypes: true });
|
52
|
+
|
53
|
+
for (const entry of entries) {
|
54
|
+
const srcPath = path.join(src, entry.name);
|
55
|
+
const destPath = path.join(dest, entry.name);
|
56
|
+
|
57
|
+
if (entry.isDirectory()) {
|
58
|
+
await copyDirectory(srcPath, destPath);
|
59
|
+
} else {
|
60
|
+
await fs.copyFile(srcPath, destPath);
|
61
|
+
console.log(`Updated: ${path.relative(userRoot, destPath)}`);
|
62
|
+
}
|
63
|
+
}
|
64
|
+
} catch (error) {
|
65
|
+
console.warn(`Warning: Could not copy ${src} to ${dest}:`, error.message);
|
66
|
+
}
|
67
|
+
}
|
68
|
+
|
69
|
+
async function updateStyles() {
|
70
|
+
const srcStylesPath = path.join(packageRoot, 'src/styles');
|
71
|
+
const destStylesPath = path.join(userRoot, 'src/styles');
|
72
|
+
|
73
|
+
try {
|
74
|
+
// Проверяем, существует ли папка styles в пакете
|
75
|
+
await fs.stat(srcStylesPath);
|
76
|
+
|
77
|
+
// Проверяем, есть ли уже пользовательские стили
|
78
|
+
try {
|
79
|
+
const userGlobalCss = path.join(destStylesPath, 'global.css');
|
80
|
+
await fs.stat(userGlobalCss);
|
81
|
+
console.log('📝 Preserving user styles (global.css exists)');
|
82
|
+
|
83
|
+
// Копируем только новые файлы стилей, не трогая global.css
|
84
|
+
const entries = await fs.readdir(srcStylesPath, { withFileTypes: true });
|
85
|
+
await fs.mkdir(destStylesPath, { recursive: true });
|
86
|
+
|
87
|
+
for (const entry of entries) {
|
88
|
+
if (entry.name !== 'global.css') {
|
89
|
+
const srcFile = path.join(srcStylesPath, entry.name);
|
90
|
+
const destFile = path.join(destStylesPath, entry.name);
|
91
|
+
await fs.copyFile(srcFile, destFile);
|
92
|
+
console.log(`Updated style: ${entry.name}`);
|
93
|
+
}
|
94
|
+
}
|
95
|
+
} catch {
|
96
|
+
// Пользовательских стилей нет, копируем все
|
97
|
+
await copyDirectory(srcStylesPath, destStylesPath);
|
98
|
+
console.log('📝 Copied default styles');
|
99
|
+
}
|
100
|
+
} catch (error) {
|
101
|
+
console.warn('Warning: Could not update styles:', error.message);
|
102
|
+
}
|
103
|
+
}
|
104
|
+
|
105
|
+
async function updateComponents() {
|
106
|
+
console.log('🔄 Updating Maugli components and assets...');
|
107
|
+
|
108
|
+
// Проверяем, что мы не в исходном проекте (чтобы не удалить исходники)
|
109
|
+
if (isSourceProject) {
|
110
|
+
console.log('⚠️ Skipping component update (running in source project)');
|
111
|
+
return;
|
112
|
+
}
|
113
|
+
|
114
|
+
// Дополнительная проверка
|
115
|
+
if (packageRoot === userRoot) {
|
116
|
+
console.log('⚠️ Skipping component update (packageRoot equals userRoot)');
|
117
|
+
return;
|
118
|
+
}
|
119
|
+
|
120
|
+
let updatedCount = 0;
|
121
|
+
|
122
|
+
for (const updatePath of FORCE_UPDATE_PATHS) {
|
123
|
+
const srcPath = path.join(packageRoot, updatePath);
|
124
|
+
const destPath = path.join(userRoot, updatePath);
|
125
|
+
|
126
|
+
try {
|
127
|
+
// Проверяем, существует ли исходная папка/файл
|
128
|
+
const stats = await fs.stat(srcPath);
|
129
|
+
|
130
|
+
if (stats.isDirectory()) {
|
131
|
+
// Удаляем существующую папку и копируем новую
|
132
|
+
try {
|
133
|
+
await fs.rm(destPath, { recursive: true, force: true });
|
134
|
+
} catch (e) {
|
135
|
+
// Папки может не быть - это нормально
|
136
|
+
}
|
137
|
+
|
138
|
+
await copyDirectory(srcPath, destPath);
|
139
|
+
updatedCount++;
|
140
|
+
} else if (stats.isFile()) {
|
141
|
+
// Копируем отдельный файл
|
142
|
+
await fs.mkdir(path.dirname(destPath), { recursive: true });
|
143
|
+
await fs.copyFile(srcPath, destPath);
|
144
|
+
console.log(`Updated: ${path.relative(userRoot, destPath)}`);
|
145
|
+
updatedCount++;
|
146
|
+
}
|
147
|
+
} catch (error) {
|
148
|
+
if (error.code !== 'ENOENT') {
|
149
|
+
console.warn(`Warning: Could not update ${updatePath}:`, error.message);
|
150
|
+
}
|
151
|
+
}
|
152
|
+
}
|
153
|
+
|
154
|
+
// Обрабатываем стили отдельно
|
155
|
+
await updateStyles();
|
156
|
+
|
157
|
+
console.log(`✅ Updated ${updatedCount} component directories/files`);
|
158
|
+
}
|
159
|
+
|
160
|
+
async function main() {
|
161
|
+
try {
|
162
|
+
await updateComponents();
|
163
|
+
console.log('🎉 Component update completed successfully!');
|
164
|
+
} catch (error) {
|
165
|
+
console.error('❌ Component update failed:', error);
|
166
|
+
process.exit(1);
|
167
|
+
}
|
168
|
+
}
|
169
|
+
|
170
|
+
// Запускаем только если вызывается напрямую
|
171
|
+
if (import.meta.url === pathToFileURL(process.argv[1]).href) {
|
172
|
+
main();
|
173
|
+
}
|
174
|
+
|
175
|
+
export { updateComponents };
|
@@ -1,14 +1,25 @@
|
|
1
1
|
#!/usr/bin/env node
|
2
2
|
|
3
3
|
import fs from 'fs/promises';
|
4
|
-
import path from 'path';
|
5
4
|
import os from 'os';
|
6
|
-
import
|
5
|
+
import path from 'path';
|
7
6
|
import ts from 'typescript';
|
7
|
+
import { fileURLToPath, pathToFileURL } from 'url';
|
8
8
|
|
9
9
|
const __filename = fileURLToPath(import.meta.url);
|
10
10
|
const __dirname = path.dirname(__filename);
|
11
11
|
|
12
|
+
// Импортируем функцию обновления компонентов
|
13
|
+
async function importUpdateComponents() {
|
14
|
+
try {
|
15
|
+
const { updateComponents } = await import('./update-components.js');
|
16
|
+
return updateComponents;
|
17
|
+
} catch (error) {
|
18
|
+
console.warn('Could not load update-components.js:', error.message);
|
19
|
+
return null;
|
20
|
+
}
|
21
|
+
}
|
22
|
+
|
12
23
|
const defaultConfigPath = path.join(__dirname, '../src/config/maugli.config.ts');
|
13
24
|
const userRoot = process.env.INIT_CWD || process.cwd();
|
14
25
|
const userConfigPath = path.join(userRoot, 'src/config/maugli.config.ts');
|
@@ -43,6 +54,15 @@ function mergeMissing(target, source) {
|
|
43
54
|
}
|
44
55
|
|
45
56
|
async function main() {
|
57
|
+
console.log('🔄 Starting Maugli upgrade process...');
|
58
|
+
|
59
|
+
// Сначала обновляем компоненты
|
60
|
+
const updateComponents = await importUpdateComponents();
|
61
|
+
if (updateComponents) {
|
62
|
+
await updateComponents();
|
63
|
+
}
|
64
|
+
|
65
|
+
// Затем обновляем конфиг
|
46
66
|
const pkg = await loadTsModule(defaultConfigPath);
|
47
67
|
const defCfg = pkg.maugliConfig;
|
48
68
|
const newVersion = pkg.MAUGLI_CONFIG_VERSION || defCfg.configVersion;
|
@@ -50,7 +70,7 @@ async function main() {
|
|
50
70
|
try {
|
51
71
|
await fs.access(userConfigPath);
|
52
72
|
} catch {
|
53
|
-
console.warn(`User config not found at ${userConfigPath}, skipping upgrade`);
|
73
|
+
console.warn(`User config not found at ${userConfigPath}, skipping config upgrade`);
|
54
74
|
return;
|
55
75
|
}
|
56
76
|
|
@@ -24,15 +24,7 @@ const current = availableLanguages.find((l) => l.code === currentLang) || availa
|
|
24
24
|
class="flag-box flex items-center justify-center w-7 h-7 rounded-[var(--radius-main)] shadow-[var(--shadow-main)]"
|
25
25
|
style="min-width:28px; min-height:28px; max-width:28px; max-height:28px;"
|
26
26
|
>
|
27
|
-
{current && (
|
28
|
-
<img
|
29
|
-
src={current.icon.replace(/^\/public\/|^\/|^flags\//, '/src/icons/flags/')}
|
30
|
-
alt={current.code.toUpperCase()}
|
31
|
-
width="28"
|
32
|
-
height="28"
|
33
|
-
style="pointer-events:none;"
|
34
|
-
/>
|
35
|
-
)}
|
27
|
+
{current && <img src={current.icon} alt={current.code.toUpperCase()} width="28" height="28" style="pointer-events:none;" />}
|
36
28
|
</span>
|
37
29
|
<svg class="w-4 h-4" viewBox="0 0 20 20" fill="currentColor">
|
38
30
|
<path
|
@@ -65,13 +57,7 @@ const current = availableLanguages.find((l) => l.code === currentLang) || availa
|
|
65
57
|
class="flag-box flex items-center justify-center w-7 h-7 rounded-[var(--radius-main)] shadow-[var(--shadow-main)]"
|
66
58
|
style="min-width:28px; min-height:28px; max-width:28px; max-height:28px;"
|
67
59
|
>
|
68
|
-
<img
|
69
|
-
src={lang.icon.replace(/^\/public\/|^\/|^flags\//, '/src/icons/flags/')}
|
70
|
-
alt={lang.code.toUpperCase()}
|
71
|
-
width="28"
|
72
|
-
height="28"
|
73
|
-
style="pointer-events:none;"
|
74
|
-
/>
|
60
|
+
<img src={lang.icon} alt={lang.code.toUpperCase()} width="28" height="28" style="pointer-events:none;" />
|
75
61
|
</span>
|
76
62
|
<span class="text-heading">{lang.label}</span>
|
77
63
|
</a>
|
package/astro.config.mjs
DELETED
@@ -1,92 +0,0 @@
|
|
1
|
-
import mdx from '@astrojs/mdx';
|
2
|
-
import sitemap from '@astrojs/sitemap';
|
3
|
-
import tailwindcss from '@tailwindcss/vite';
|
4
|
-
import { defineConfig } from 'astro/config';
|
5
|
-
import { imagetools } from 'vite-imagetools';
|
6
|
-
import { VitePWA } from 'vite-plugin-pwa';
|
7
|
-
import siteConfig from './src/data/site-config';
|
8
|
-
import remarkSlug from 'remark-slug';
|
9
|
-
import customSlugify from './src/utils/remark-slugify';
|
10
|
-
import { maugliConfig } from './src/config/maugli.config';
|
11
|
-
|
12
|
-
export const pwaOptions = {
|
13
|
-
registerType: 'autoUpdate',
|
14
|
-
includeAssets: ['favicon.svg', 'favicon.ico', 'robots.txt', 'apple-touch-icon.png'],
|
15
|
-
manifest: {
|
16
|
-
name: "Maugli Blog",
|
17
|
-
short_name: "Maugli",
|
18
|
-
start_url: "/",
|
19
|
-
display: "standalone",
|
20
|
-
background_color: maugliConfig.pwa?.backgroundColor ?? '#ffffff',
|
21
|
-
theme_color: maugliConfig.pwa?.themeColor ?? '#0cbf11',
|
22
|
-
icons: maugliConfig.pwa?.icons ?? [
|
23
|
-
{
|
24
|
-
src: "/icon-192.png",
|
25
|
-
sizes: "192x192",
|
26
|
-
type: "image/png",
|
27
|
-
purpose: "any maskable",
|
28
|
-
},
|
29
|
-
{
|
30
|
-
src: "/icon-512.png",
|
31
|
-
sizes: "512x512",
|
32
|
-
type: "image/png",
|
33
|
-
},
|
34
|
-
],
|
35
|
-
},
|
36
|
-
workbox: {
|
37
|
-
navigateFallback: '/index.html',
|
38
|
-
cleanupOutdatedCaches: true,
|
39
|
-
navigateFallbackDenylist: [/^\/api\//],
|
40
|
-
clientsClaim: true,
|
41
|
-
globPatterns: ['**/*.{js,css,html,png,jpg,jpeg,webp,svg}'],
|
42
|
-
runtimeCaching: [
|
43
|
-
{
|
44
|
-
urlPattern: ({ request }) => request.destination === 'image',
|
45
|
-
handler: 'CacheFirst',
|
46
|
-
options: {
|
47
|
-
cacheName: 'images-cache',
|
48
|
-
expiration: {
|
49
|
-
maxEntries: 50,
|
50
|
-
maxAgeSeconds: 30 * 24 * 60 * 60 // 30 дней
|
51
|
-
}
|
52
|
-
}
|
53
|
-
},
|
54
|
-
{
|
55
|
-
urlPattern: ({ request }) => request.destination === 'font',
|
56
|
-
handler: 'CacheFirst',
|
57
|
-
options: {
|
58
|
-
cacheName: 'fonts-cache',
|
59
|
-
expiration: {
|
60
|
-
maxEntries: 20,
|
61
|
-
maxAgeSeconds: 365 * 24 * 60 * 60 // 1 год
|
62
|
-
}
|
63
|
-
}
|
64
|
-
}
|
65
|
-
]
|
66
|
-
},
|
67
|
-
devOptions: {
|
68
|
-
enabled: true, // чтобы работал в деве
|
69
|
-
type: 'module',
|
70
|
-
}
|
71
|
-
};
|
72
|
-
|
73
|
-
// https://astro.build/config
|
74
|
-
export default defineConfig({
|
75
|
-
site: siteConfig.website,
|
76
|
-
integrations: [
|
77
|
-
mdx(),
|
78
|
-
sitemap()
|
79
|
-
],
|
80
|
-
vite: {
|
81
|
-
plugins: [
|
82
|
-
tailwindcss(),
|
83
|
-
imagetools(),
|
84
|
-
VitePWA(pwaOptions)
|
85
|
-
]
|
86
|
-
},
|
87
|
-
markdown: {
|
88
|
-
remarkPlugins: [
|
89
|
-
[remarkSlug, { slug: customSlugify }]
|
90
|
-
]
|
91
|
-
}
|
92
|
-
});
|
package/bin/init.js
DELETED
@@ -1,201 +0,0 @@
|
|
1
|
-
#!/usr/bin/env node
|
2
|
-
|
3
|
-
import { execSync } from 'child_process';
|
4
|
-
import { cpSync, existsSync, readFileSync, writeFileSync } from 'fs';
|
5
|
-
import path from 'path';
|
6
|
-
import readline from 'readline';
|
7
|
-
import { fileURLToPath } from 'url';
|
8
|
-
|
9
|
-
const __filename = fileURLToPath(import.meta.url);
|
10
|
-
const __dirname = path.dirname(__filename);
|
11
|
-
const templateRoot = path.join(__dirname, '..');
|
12
|
-
|
13
|
-
function getLanguageCodes() {
|
14
|
-
const file = readFileSync(path.join(templateRoot, 'src/i18n/languages.ts'), 'utf8');
|
15
|
-
const codes = [];
|
16
|
-
const regex = /{\s*code:\s*'([^']+)'/g;
|
17
|
-
let match;
|
18
|
-
while ((match = regex.exec(file)) !== null) {
|
19
|
-
codes.push(match[1]);
|
20
|
-
}
|
21
|
-
return codes;
|
22
|
-
}
|
23
|
-
|
24
|
-
function promptLang(codes) {
|
25
|
-
return new Promise(resolve => {
|
26
|
-
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
27
|
-
rl.question(`Choose language (${codes.join(', ')}): `, answer => {
|
28
|
-
rl.close();
|
29
|
-
resolve(codes.includes(answer) ? answer : codes[0]);
|
30
|
-
});
|
31
|
-
});
|
32
|
-
}
|
33
|
-
|
34
|
-
function promptRepo() {
|
35
|
-
return new Promise(resolve => {
|
36
|
-
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
37
|
-
rl.question('Repository URL: ', answer => {
|
38
|
-
rl.close();
|
39
|
-
resolve(answer.trim());
|
40
|
-
});
|
41
|
-
});
|
42
|
-
}
|
43
|
-
|
44
|
-
async function getRepoUrl(targetDir, repoOption) {
|
45
|
-
if (repoOption) return repoOption;
|
46
|
-
try {
|
47
|
-
const url = execSync('git remote get-url origin', {
|
48
|
-
cwd: targetDir,
|
49
|
-
stdio: ['ignore', 'pipe', 'ignore']
|
50
|
-
})
|
51
|
-
.toString()
|
52
|
-
.trim();
|
53
|
-
if (url) return url;
|
54
|
-
} catch {
|
55
|
-
// ignore
|
56
|
-
}
|
57
|
-
return await promptRepo();
|
58
|
-
}
|
59
|
-
|
60
|
-
function updateReadme(targetDir, repoUrl) {
|
61
|
-
if (!repoUrl) return;
|
62
|
-
const readmePath = path.join(targetDir, 'README.md');
|
63
|
-
if (!existsSync(readmePath)) return;
|
64
|
-
let content = readFileSync(readmePath, 'utf8');
|
65
|
-
const pattern = /https:\/\/app\.netlify\.com\/start\/deploy\?repository=[^\)\s]+/;
|
66
|
-
content = content.replace(
|
67
|
-
pattern,
|
68
|
-
`https://app.netlify.com/start/deploy?repository=${repoUrl}`
|
69
|
-
);
|
70
|
-
writeFileSync(readmePath, content);
|
71
|
-
console.log('Updated Netlify link in README.md');
|
72
|
-
}
|
73
|
-
|
74
|
-
function updateConfig(targetDir, lang, repoUrl) {
|
75
|
-
const configPath = path.join(targetDir, 'src', 'config', 'maugli.config.ts');
|
76
|
-
if (!existsSync(configPath)) return;
|
77
|
-
let content = readFileSync(configPath, 'utf8');
|
78
|
-
content = content.replace(/defaultLang:\s*'[^']*'/, `defaultLang: '${lang}'`);
|
79
|
-
const multiMatch = content.match(/enableMultiLang:\s*(true|false)/);
|
80
|
-
const multi = multiMatch ? multiMatch[1] === 'true' : false;
|
81
|
-
content = content.replace(/showLangSwitcher:\s*(true|false)/, `showLangSwitcher: ${multi}`);
|
82
|
-
|
83
|
-
// Update repository URL if provided
|
84
|
-
if (repoUrl) {
|
85
|
-
content = content.replace(
|
86
|
-
/repository:\s*{[^}]*url:\s*'[^']*'/,
|
87
|
-
`repository: {\n url: '${repoUrl}'`
|
88
|
-
);
|
89
|
-
}
|
90
|
-
|
91
|
-
writeFileSync(configPath, content);
|
92
|
-
console.log(`Configured default language to ${lang}`);
|
93
|
-
if (repoUrl) {
|
94
|
-
console.log(`Configured repository URL to ${repoUrl}`);
|
95
|
-
}
|
96
|
-
}
|
97
|
-
|
98
|
-
export default async function init(targetName, langOption, repoOption) {
|
99
|
-
const targetDir = targetName ? path.resolve(targetName) : process.cwd();
|
100
|
-
const codes = getLanguageCodes();
|
101
|
-
const lang = langOption && codes.includes(langOption) ? langOption : await promptLang(codes);
|
102
|
-
|
103
|
-
function copyItem(item) {
|
104
|
-
const src = path.join(templateRoot, item);
|
105
|
-
const dest = path.join(targetDir, item);
|
106
|
-
|
107
|
-
if (!existsSync(src)) {
|
108
|
-
console.log(`Skipped ${item} (not found)`);
|
109
|
-
return;
|
110
|
-
}
|
111
|
-
|
112
|
-
cpSync(src, dest, { recursive: true });
|
113
|
-
console.log(`Copied ${item}`);
|
114
|
-
}
|
115
|
-
|
116
|
-
// Copy package files first so npm install works correctly
|
117
|
-
['package.json', 'package-lock.json'].forEach(file => {
|
118
|
-
if (existsSync(path.join(templateRoot, file))) {
|
119
|
-
copyItem(file);
|
120
|
-
}
|
121
|
-
});
|
122
|
-
|
123
|
-
const items = [
|
124
|
-
'astro.config.mjs',
|
125
|
-
'tsconfig.json',
|
126
|
-
'vite.config.js',
|
127
|
-
'public',
|
128
|
-
'src',
|
129
|
-
'scripts',
|
130
|
-
'typograf-batch.js',
|
131
|
-
'resize-all.cjs',
|
132
|
-
'README.md',
|
133
|
-
'LICENSE'
|
134
|
-
];
|
135
|
-
items.forEach(copyItem);
|
136
|
-
|
137
|
-
const repoUrl = await getRepoUrl(targetDir, repoOption);
|
138
|
-
updateReadme(targetDir, repoUrl);
|
139
|
-
|
140
|
-
// Create essential config files
|
141
|
-
const gitignoreContent = `
|
142
|
-
# Dependencies
|
143
|
-
node_modules/
|
144
|
-
.pnpm-debug.log*
|
145
|
-
|
146
|
-
# Environment
|
147
|
-
.env
|
148
|
-
.env.local
|
149
|
-
.env.production
|
150
|
-
|
151
|
-
# Build outputs
|
152
|
-
dist/
|
153
|
-
.astro/
|
154
|
-
|
155
|
-
# Generated files
|
156
|
-
.DS_Store
|
157
|
-
.vscode/settings.json
|
158
|
-
|
159
|
-
# Cache
|
160
|
-
.typograf-cache.json
|
161
|
-
`;
|
162
|
-
|
163
|
-
const prettierrcContent = `{
|
164
|
-
"semi": true,
|
165
|
-
"singleQuote": true,
|
166
|
-
"tabWidth": 2,
|
167
|
-
"trailingComma": "es5",
|
168
|
-
"printWidth": 100,
|
169
|
-
"plugins": ["prettier-plugin-tailwindcss"]
|
170
|
-
}
|
171
|
-
`;
|
172
|
-
|
173
|
-
writeFileSync(path.join(targetDir, '.gitignore'), gitignoreContent.trim());
|
174
|
-
console.log('Created .gitignore');
|
175
|
-
|
176
|
-
writeFileSync(path.join(targetDir, '.prettierrc'), prettierrcContent);
|
177
|
-
console.log('Created .prettierrc');
|
178
|
-
|
179
|
-
execSync('npm install', { cwd: targetDir, stdio: 'inherit' });
|
180
|
-
updateConfig(targetDir, lang, repoUrl);
|
181
|
-
}
|
182
|
-
|
183
|
-
// Если скрипт запускается напрямую
|
184
|
-
if (import.meta.url === `file://${process.argv[1]}`) {
|
185
|
-
const args = process.argv.slice(2);
|
186
|
-
let targetName;
|
187
|
-
let lang;
|
188
|
-
let repo;
|
189
|
-
for (let i = 0; i < args.length; i++) {
|
190
|
-
if (args[i] === '--lang' && i + 1 < args.length) {
|
191
|
-
lang = args[i + 1];
|
192
|
-
i++;
|
193
|
-
} else if (args[i] === '--repo' && i + 1 < args.length) {
|
194
|
-
repo = args[i + 1];
|
195
|
-
i++;
|
196
|
-
} else {
|
197
|
-
targetName = args[i];
|
198
|
-
}
|
199
|
-
}
|
200
|
-
await init(targetName, lang, repo);
|
201
|
-
}
|
package/resize-all.cjs
DELETED
@@ -1,29 +0,0 @@
|
|
1
|
-
// resize-all.js
|
2
|
-
const fs = require('fs');
|
3
|
-
const path = require('path');
|
4
|
-
const sharp = require('sharp');
|
5
|
-
|
6
|
-
// Какие размеры тебе нужны
|
7
|
-
const sizes = [400, 800, 1200];
|
8
|
-
|
9
|
-
const inputDir = './public'; // свои пути
|
10
|
-
const outputDir = './public';
|
11
|
-
|
12
|
-
// Перебираем все файлы в исходной папке
|
13
|
-
fs.readdirSync(inputDir).forEach(file => {
|
14
|
-
const ext = path.extname(file);
|
15
|
-
const base = path.basename(file, ext);
|
16
|
-
if (!['.jpg', '.jpeg', '.png', '.webp'].includes(ext.toLowerCase())) return;
|
17
|
-
|
18
|
-
sizes.forEach(width => {
|
19
|
-
sharp(path.join(inputDir, file))
|
20
|
-
.resize(width)
|
21
|
-
.toFile(path.join(outputDir, `${base}-${width}${ext}`), (err) => {
|
22
|
-
if (err) console.error(`Ошибка на ${base}-${width}${ext}:`, err);
|
23
|
-
else console.log(`Сделан: ${base}-${width}${ext}`);
|
24
|
-
});
|
25
|
-
});
|
26
|
-
|
27
|
-
// Копируем оригинал тоже (максимальный)
|
28
|
-
fs.copyFileSync(path.join(inputDir, file), path.join(outputDir, `${base}${ext}`));
|
29
|
-
});
|
package/tsconfig.json
DELETED
package/typograf-batch.js
DELETED
@@ -1,49 +0,0 @@
|
|
1
|
-
import Typograf from 'typograf';
|
2
|
-
import { readFileSync, writeFileSync, readdirSync, statSync, existsSync } from 'fs';
|
3
|
-
import yaml from 'js-yaml';
|
4
|
-
|
5
|
-
const tp = new Typograf({ locale: ['ru', 'en-US'] });
|
6
|
-
const dir = './src/content/blog';
|
7
|
-
const cacheFile = './.typograf-cache.json';
|
8
|
-
|
9
|
-
let cache = {};
|
10
|
-
if (existsSync(cacheFile)) {
|
11
|
-
cache = JSON.parse(readFileSync(cacheFile, 'utf8'));
|
12
|
-
}
|
13
|
-
|
14
|
-
let cacheUpdated = false;
|
15
|
-
|
16
|
-
readdirSync(dir)
|
17
|
-
.filter(f => f.endsWith('.md'))
|
18
|
-
.forEach(f => {
|
19
|
-
const file = dir + '/' + f;
|
20
|
-
const stats = statSync(file);
|
21
|
-
const mtime = stats.mtimeMs;
|
22
|
-
|
23
|
-
// Если не изменялся — скипаем
|
24
|
-
if (cache[f] === mtime) return;
|
25
|
-
|
26
|
-
const data = readFileSync(file, 'utf8');
|
27
|
-
const parts = data.split('---');
|
28
|
-
if (parts.length < 3) return;
|
29
|
-
let fm = yaml.load(parts[1]);
|
30
|
-
if (fm.title) fm.title = tp.execute(fm.title);
|
31
|
-
if (fm.description) fm.description = tp.execute(fm.description);
|
32
|
-
|
33
|
-
const newData = [
|
34
|
-
'---',
|
35
|
-
yaml.dump(fm).trim(),
|
36
|
-
'---',
|
37
|
-
tp.execute(parts.slice(2).join('---'))
|
38
|
-
].join('\n');
|
39
|
-
|
40
|
-
writeFileSync(file, newData);
|
41
|
-
cache[f] = mtime;
|
42
|
-
cacheUpdated = true;
|
43
|
-
console.log(`Typografed: ${file}`);
|
44
|
-
});
|
45
|
-
|
46
|
-
// Сохраняем кеш только если были изменения
|
47
|
-
if (cacheUpdated) {
|
48
|
-
writeFileSync(cacheFile, JSON.stringify(cache, null, 2));
|
49
|
-
}
|
package/vite.config.js
DELETED
@@ -1,11 +0,0 @@
|
|
1
|
-
import { defineConfig } from 'vite';
|
2
|
-
import { imagetools } from 'vite-imagetools';
|
3
|
-
import { VitePWA } from 'vite-plugin-pwa';
|
4
|
-
import { pwaOptions } from './astro.config.mjs';
|
5
|
-
|
6
|
-
export default defineConfig({
|
7
|
-
plugins: [
|
8
|
-
imagetools(),
|
9
|
-
VitePWA(pwaOptions)
|
10
|
-
]
|
11
|
-
});
|