@shevky/core 0.0.2 → 0.0.3
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/engines/menuEngine.js +117 -0
- package/engines/metaEngine.js +786 -0
- package/engines/pluginEngine.js +102 -0
- package/engines/renderEngine.js +823 -0
- package/lib/contentBody.js +12 -0
- package/lib/contentFile.js +170 -0
- package/lib/contentHeader.js +295 -0
- package/lib/contentSummary.js +85 -0
- package/lib/menuItem.js +51 -0
- package/lib/page.js +103 -0
- package/lib/project.js +82 -0
- package/lib/template.js +50 -0
- package/package.json +5 -1
- package/registries/contentRegistry.js +286 -0
- package/registries/pageRegistry.js +29 -0
- package/registries/pluginRegistry.js +94 -0
- package/registries/templateRegistry.js +152 -0
- package/types/command-line-args.d.ts +20 -0
- package/types/command-line-usage.d.ts +12 -0
- package/types/degit.d.ts +15 -0
- package/types/index.d.ts +79 -0
package/lib/project.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { io as _io } from "@shevky/base";
|
|
2
|
+
|
|
3
|
+
/** @typedef {import("../types/index.d.ts").ProjectPaths} ProjectPaths */
|
|
4
|
+
|
|
5
|
+
export class Project {
|
|
6
|
+
/**
|
|
7
|
+
* @type {string}
|
|
8
|
+
*/
|
|
9
|
+
#_baseDir;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @param {string} [baseDir="."]
|
|
13
|
+
*/
|
|
14
|
+
constructor(baseDir = ".") {
|
|
15
|
+
this.#_baseDir = baseDir;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
get rootDir() {
|
|
19
|
+
return _io.path.combine(this.#_baseDir);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
get srcDir() {
|
|
23
|
+
return _io.path.combine(this.rootDir, "src");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
get distDir() {
|
|
27
|
+
return _io.path.combine(this.rootDir, "dist");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
get tmpDir() {
|
|
31
|
+
return _io.path.combine(this.rootDir, "tmp");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
get contentDir() {
|
|
35
|
+
return _io.path.combine(this.srcDir, "content");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
get layoutsDir() {
|
|
39
|
+
return _io.path.combine(this.srcDir, "layouts");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
get componentsDir() {
|
|
43
|
+
return _io.path.combine(this.srcDir, "components");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
get templatesDir() {
|
|
47
|
+
return _io.path.combine(this.srcDir, "templates");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
get assetsDir() {
|
|
51
|
+
return _io.path.combine(this.srcDir, "assets");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
get siteConfig() {
|
|
55
|
+
return _io.path.combine(this.srcDir, "site.json");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
get i18nConfig() {
|
|
59
|
+
return _io.path.combine(this.srcDir, "i18n.json");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* @returns {ProjectPaths}
|
|
64
|
+
*/
|
|
65
|
+
toObject() {
|
|
66
|
+
return {
|
|
67
|
+
root: this.rootDir,
|
|
68
|
+
src: this.srcDir,
|
|
69
|
+
dist: this.distDir,
|
|
70
|
+
tmp: this.tmpDir,
|
|
71
|
+
content: this.contentDir,
|
|
72
|
+
layouts: this.layoutsDir,
|
|
73
|
+
components: this.componentsDir,
|
|
74
|
+
templates: this.templatesDir,
|
|
75
|
+
assets: this.assetsDir,
|
|
76
|
+
siteConfig: this.siteConfig,
|
|
77
|
+
i18nConfig: this.i18nConfig,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export default new Project();
|
package/lib/template.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export class Template {
|
|
2
|
+
/**
|
|
3
|
+
* @type {string}
|
|
4
|
+
*/
|
|
5
|
+
#_key;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @type {string}
|
|
9
|
+
*/
|
|
10
|
+
#_path;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @type {string}
|
|
14
|
+
*/
|
|
15
|
+
#_content;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @type {string}
|
|
19
|
+
*/
|
|
20
|
+
#_type;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @param {string} key
|
|
24
|
+
* @param {string} type
|
|
25
|
+
* @param {string} path
|
|
26
|
+
* @param {string} content
|
|
27
|
+
*/
|
|
28
|
+
constructor(key, type, path, content) {
|
|
29
|
+
this.#_key = key;
|
|
30
|
+
this.#_type = type;
|
|
31
|
+
this.#_path = path;
|
|
32
|
+
this.#_content = content;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
get key() {
|
|
36
|
+
return this.#_key;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
get type() {
|
|
40
|
+
return this.#_type;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
get path() {
|
|
44
|
+
return this.#_path;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
get content() {
|
|
48
|
+
return this.#_content;
|
|
49
|
+
}
|
|
50
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shevky/core",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.3",
|
|
4
4
|
"description": "A minimal, dependency-light static site generator.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "shevky.js",
|
|
@@ -9,7 +9,11 @@
|
|
|
9
9
|
},
|
|
10
10
|
"files": [
|
|
11
11
|
"shevky.js",
|
|
12
|
+
"lib/",
|
|
13
|
+
"engines/",
|
|
14
|
+
"registries/",
|
|
12
15
|
"scripts/",
|
|
16
|
+
"types/",
|
|
13
17
|
"README.md",
|
|
14
18
|
"LICENSE"
|
|
15
19
|
],
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import { io as _io } from "@shevky/base";
|
|
2
|
+
import matter from "gray-matter";
|
|
3
|
+
|
|
4
|
+
import { ContentFile } from "../lib/contentFile.js";
|
|
5
|
+
import { ContentSummary } from "../lib/contentSummary.js";
|
|
6
|
+
import { MetaEngine } from "../engines/metaEngine.js";
|
|
7
|
+
|
|
8
|
+
export class ContentRegistry {
|
|
9
|
+
/**
|
|
10
|
+
* @type {ContentFile[]}
|
|
11
|
+
*/
|
|
12
|
+
#_cache = [];
|
|
13
|
+
/** @type {import("../types/index.d.ts").CollectionsByLang | null} */
|
|
14
|
+
#_collectionsCache = null;
|
|
15
|
+
/** @type {ReturnType<ContentRegistry["buildFooterPolicies"]> | null} */
|
|
16
|
+
#_footerPoliciesCache = null;
|
|
17
|
+
/** @type {ReturnType<ContentRegistry["buildContentIndex"]> | null} */
|
|
18
|
+
#_contentIndexCache = null;
|
|
19
|
+
/** @type {MetaEngine} */
|
|
20
|
+
#_metaEngine;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @param {MetaEngine} metaEngine
|
|
24
|
+
*/
|
|
25
|
+
constructor(metaEngine) {
|
|
26
|
+
this.#_metaEngine = metaEngine;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @param {string} path
|
|
31
|
+
* @returns
|
|
32
|
+
*/
|
|
33
|
+
async load(path) {
|
|
34
|
+
const isExists = await _io.directory.exists(path);
|
|
35
|
+
if (!isExists) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const files = await _io.directory.read(path);
|
|
40
|
+
for (const entry of files) {
|
|
41
|
+
if (!entry.endsWith(".md")) {
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const filePath = _io.path.combine(path, entry);
|
|
46
|
+
const isFileExists = await _io.file.exists(filePath);
|
|
47
|
+
if (!isFileExists) {
|
|
48
|
+
throw new Error(`Failed to read content file at ${filePath}`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const contentFile = await this.#_loadFromFile(filePath);
|
|
52
|
+
this.#_cache.push(contentFile);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
this.#_collectionsCache = null;
|
|
56
|
+
this.#_footerPoliciesCache = null;
|
|
57
|
+
this.#_contentIndexCache = null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
get count() {
|
|
61
|
+
return this.#_cache.length;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
get files() {
|
|
65
|
+
return this.#_cache;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* @returns {Record<string, Array<{ key: string, label: string, url: string, lang: string }>>}
|
|
70
|
+
*/
|
|
71
|
+
buildFooterPolicies() {
|
|
72
|
+
if (this.#_footerPoliciesCache) {
|
|
73
|
+
return this.#_footerPoliciesCache;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (this.count === 0) {
|
|
77
|
+
this.#_footerPoliciesCache = {};
|
|
78
|
+
return this.#_footerPoliciesCache;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** @type {Record<string, Array<{ key: string, label: string, url: string, lang: string }>>} */
|
|
82
|
+
const policiesByLang = {};
|
|
83
|
+
const contentFiles = this.files;
|
|
84
|
+
for (const file of contentFiles) {
|
|
85
|
+
if (
|
|
86
|
+
!file.isValid ||
|
|
87
|
+
file.isDraft ||
|
|
88
|
+
!file.isPublished ||
|
|
89
|
+
file.category !== "policy"
|
|
90
|
+
) {
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const policy = {
|
|
95
|
+
lang: file.lang,
|
|
96
|
+
key: file.id,
|
|
97
|
+
label: file.menuLabel,
|
|
98
|
+
url: this.#_metaEngine.buildContentUrl(
|
|
99
|
+
file.canonical,
|
|
100
|
+
file.lang,
|
|
101
|
+
file.slug,
|
|
102
|
+
),
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
if (!Array.isArray(policiesByLang[file.lang])) {
|
|
106
|
+
policiesByLang[file.lang] = [];
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
policiesByLang[file.lang].push(policy);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
Object.keys(policiesByLang).forEach((lang) => {
|
|
113
|
+
policiesByLang[lang].sort((a, b) => a.label.localeCompare(b.label, lang));
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
this.#_footerPoliciesCache = policiesByLang;
|
|
117
|
+
return this.#_footerPoliciesCache;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* @returns {Record<string, Record<string, { id: string, lang: string, title: string, canonical: string }>>}
|
|
122
|
+
*/
|
|
123
|
+
buildContentIndex() {
|
|
124
|
+
if (this.#_contentIndexCache) {
|
|
125
|
+
return this.#_contentIndexCache;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (this.count === 0) {
|
|
129
|
+
this.#_contentIndexCache = {};
|
|
130
|
+
return this.#_contentIndexCache;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** @type {Record<string, Record<string, { id: string, lang: string, title: string, canonical: string }>>} */
|
|
134
|
+
const index = {};
|
|
135
|
+
const contentFiles = this.files;
|
|
136
|
+
for (const file of contentFiles) {
|
|
137
|
+
if (!file.isValid || file.isDraft || !file.isPublished || !file.id) {
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (!index[file.id]) {
|
|
142
|
+
index[file.id] = {};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
index[file.id][file.lang] = {
|
|
146
|
+
id: file.id,
|
|
147
|
+
lang: file.lang,
|
|
148
|
+
title: file.title,
|
|
149
|
+
canonical: this.#_metaEngine.buildContentUrl(
|
|
150
|
+
file.canonical,
|
|
151
|
+
file.lang,
|
|
152
|
+
file.slug,
|
|
153
|
+
),
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
this.#_contentIndexCache = index;
|
|
158
|
+
return this.#_contentIndexCache;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* @returns {import("../types/index.d.ts").CollectionsByLang}
|
|
163
|
+
*/
|
|
164
|
+
buildCategoryTagCollections() {
|
|
165
|
+
if (this.#_collectionsCache) {
|
|
166
|
+
return this.#_collectionsCache;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (this.count === 0) {
|
|
170
|
+
this.#_collectionsCache = {};
|
|
171
|
+
return this.#_collectionsCache;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** @type {import("../types/index.d.ts").CollectionsByLang} */
|
|
175
|
+
const pagesByLang = {};
|
|
176
|
+
const contentFiles = this.files;
|
|
177
|
+
for (const file of contentFiles) {
|
|
178
|
+
if (!file.isValid || file.isDraft || !file.isPublished) {
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const contentSummary = new ContentSummary(file);
|
|
183
|
+
const summary = /** @type {import("../types/index.d.ts").CollectionEntry} */ ({
|
|
184
|
+
...contentSummary.toObject(),
|
|
185
|
+
canonical: this.#_metaEngine.buildContentUrl(
|
|
186
|
+
file.canonical,
|
|
187
|
+
file.lang,
|
|
188
|
+
file.slug,
|
|
189
|
+
),
|
|
190
|
+
});
|
|
191
|
+
const langStore = pagesByLang[file.lang] ?? (pagesByLang[file.lang] = {});
|
|
192
|
+
|
|
193
|
+
if (file.isPostTemplate && file.isFeatured) {
|
|
194
|
+
this.#_addCollectionEntry(langStore, "home", summary, "home");
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (file.category) {
|
|
198
|
+
this.#_addCollectionEntry(
|
|
199
|
+
langStore,
|
|
200
|
+
file.category,
|
|
201
|
+
summary,
|
|
202
|
+
"category",
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
for (const tag of file.tags) {
|
|
207
|
+
this.#_addCollectionEntry(langStore, tag, summary, "tag");
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (file.series) {
|
|
211
|
+
this.#_addCollectionEntry(
|
|
212
|
+
langStore,
|
|
213
|
+
file.series,
|
|
214
|
+
{
|
|
215
|
+
...contentSummary.toObject(),
|
|
216
|
+
seriesTitle: file.seriesTitle,
|
|
217
|
+
},
|
|
218
|
+
"series",
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
this.#_collectionsCache = this.#_sortCollectionEntries(pagesByLang);
|
|
224
|
+
return this.#_collectionsCache;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* @param {string} filePath
|
|
229
|
+
* @returns {Promise<ContentFile>}
|
|
230
|
+
*/
|
|
231
|
+
async #_loadFromFile(filePath) {
|
|
232
|
+
const raw = await _io.file.read(filePath);
|
|
233
|
+
let isValid = false;
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* @type {{data: Record<string, unknown>, content: string}}
|
|
237
|
+
*/
|
|
238
|
+
let matterResponse = { data: {}, content: "" };
|
|
239
|
+
|
|
240
|
+
try {
|
|
241
|
+
matterResponse = matter(raw);
|
|
242
|
+
isValid = true;
|
|
243
|
+
} catch {}
|
|
244
|
+
|
|
245
|
+
const { data, content } = matterResponse;
|
|
246
|
+
return new ContentFile(data, content, filePath, isValid);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* @param {Record<string, import("../types/index.d.ts").CollectionEntry[]>} store
|
|
251
|
+
* @param {string} key
|
|
252
|
+
* @param {import("../types/index.d.ts").CollectionEntry} entry
|
|
253
|
+
* @param {string} type
|
|
254
|
+
*/
|
|
255
|
+
#_addCollectionEntry(store, key, entry, type) {
|
|
256
|
+
if (!store[key]) {
|
|
257
|
+
store[key] = [];
|
|
258
|
+
}
|
|
259
|
+
store[key].push({
|
|
260
|
+
...entry,
|
|
261
|
+
type,
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* @param {import("../types/index.d.ts").CollectionsByLang} collections
|
|
267
|
+
*/
|
|
268
|
+
#_sortCollectionEntries(collections) {
|
|
269
|
+
/** @type {import("../types/index.d.ts").CollectionsByLang} */
|
|
270
|
+
const sorted = {};
|
|
271
|
+
Object.keys(collections).forEach((lang) => {
|
|
272
|
+
sorted[lang] = {};
|
|
273
|
+
Object.keys(collections[lang]).forEach((key) => {
|
|
274
|
+
sorted[lang][key] = collections[lang][key].slice().sort((a, b) => {
|
|
275
|
+
const aDate = Date.parse(String(a.date ?? "")) || 0;
|
|
276
|
+
const bDate = Date.parse(String(b.date ?? "")) || 0;
|
|
277
|
+
if (aDate === bDate) {
|
|
278
|
+
return (a.title ?? "").localeCompare(b.title ?? "", lang);
|
|
279
|
+
}
|
|
280
|
+
return bDate - aDate;
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
});
|
|
284
|
+
return sorted;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Page } from "../lib/page.js";
|
|
2
|
+
|
|
3
|
+
export class PageRegistry {
|
|
4
|
+
/**
|
|
5
|
+
* @type {Page[]}
|
|
6
|
+
*/
|
|
7
|
+
#_cache = [];
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @param {Page} page
|
|
11
|
+
*/
|
|
12
|
+
add(page) {
|
|
13
|
+
if (page instanceof Page) {
|
|
14
|
+
this.#_cache.push(page);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
list() {
|
|
19
|
+
return [...this.#_cache];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
get count() {
|
|
23
|
+
return this.#_cache.length;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
clear() {
|
|
27
|
+
this.#_cache = [];
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { exec as _exec, log as _log, config as _cfg } from "@shevky/base";
|
|
2
|
+
import { Project } from "../lib/project.js";
|
|
3
|
+
|
|
4
|
+
/** @typedef {import("../types/index.d.ts").PluginInstance} PluginInstance */
|
|
5
|
+
/** @typedef {import("../types/index.d.ts").PluginLoadContext} PluginLoadContext */
|
|
6
|
+
|
|
7
|
+
export class PluginRegistry {
|
|
8
|
+
/**
|
|
9
|
+
* @type {Map<string, PluginInstance>}
|
|
10
|
+
*/
|
|
11
|
+
#_cache = new Map();
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @type {Project}
|
|
15
|
+
*/
|
|
16
|
+
#_project = new Project(process.cwd());
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @param {string[]|null|undefined} names
|
|
20
|
+
* @returns {Promise<void>}
|
|
21
|
+
*/
|
|
22
|
+
async load(names) {
|
|
23
|
+
const pluginNames = Array.isArray(names) ? names.filter(Boolean) : [];
|
|
24
|
+
if (!pluginNames.length) {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const resolveBase = this.#_project.rootDir;
|
|
29
|
+
for (const pluginName of pluginNames) {
|
|
30
|
+
try {
|
|
31
|
+
const fromCwd = _exec.resolve(pluginName, resolveBase);
|
|
32
|
+
const loaded = fromCwd
|
|
33
|
+
? await import(fromCwd)
|
|
34
|
+
: await import(pluginName);
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* @type {PluginInstance}
|
|
38
|
+
*/
|
|
39
|
+
const instance = loaded?.default ?? loaded;
|
|
40
|
+
|
|
41
|
+
if (!instance || typeof instance !== "object") {
|
|
42
|
+
_log.warn(`Plugin cannot load correctly. Plugin name: ${pluginName}`);
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* @type {string}
|
|
48
|
+
*/
|
|
49
|
+
const name =
|
|
50
|
+
typeof instance.name === "string" ? instance.name.trim() : "";
|
|
51
|
+
if (!name) {
|
|
52
|
+
_log.warn(
|
|
53
|
+
`Plugin cannot load correctly. Missing name: ${pluginName}`,
|
|
54
|
+
);
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (this.#_cache.has(name)) {
|
|
59
|
+
_log.warn(`Duplicate plugin name detected: ${name}`);
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
this.#_cache.set(name, instance);
|
|
64
|
+
|
|
65
|
+
if (instance.load) {
|
|
66
|
+
const loadContext = this.#_createContext();
|
|
67
|
+
await instance.load(loadContext);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
_log.debug(`The plugin '${pluginName}' has been loaded.`);
|
|
71
|
+
} catch (error) {
|
|
72
|
+
_log.err(`Failed to load plugin '${pluginName}':`, error);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
get count() {
|
|
78
|
+
return this.#_cache.size;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
get plugins() {
|
|
82
|
+
return this.#_cache;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* @returns {PluginLoadContext}
|
|
87
|
+
*/
|
|
88
|
+
#_createContext() {
|
|
89
|
+
return {
|
|
90
|
+
config: _cfg,
|
|
91
|
+
paths: this.#_project.toObject(),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { io as _io } from "@shevky/base";
|
|
2
|
+
|
|
3
|
+
import { Template } from "../lib/template.js";
|
|
4
|
+
|
|
5
|
+
export const TYPE_PARTIAL = "partial";
|
|
6
|
+
export const TYPE_COMPONENT = "component";
|
|
7
|
+
export const TYPE_LAYOUT = "layout";
|
|
8
|
+
export const TYPE_TEMPLATE = "template";
|
|
9
|
+
|
|
10
|
+
const MUSTACHE_EXT = ".mustache";
|
|
11
|
+
|
|
12
|
+
export class TemplateRegistry {
|
|
13
|
+
/**
|
|
14
|
+
* @type {Map<string, Map<string, Template>>}
|
|
15
|
+
*/
|
|
16
|
+
#_cache = new Map();
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @param {string} directoryPath
|
|
20
|
+
*/
|
|
21
|
+
async loadPartials(directoryPath) {
|
|
22
|
+
await this.#_loadDirectory(directoryPath, {
|
|
23
|
+
type: TYPE_PARTIAL,
|
|
24
|
+
keyPrefix: "partials/",
|
|
25
|
+
accept: (entry) => entry.startsWith("_") && entry.endsWith(MUSTACHE_EXT),
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @param {string} directoryPath
|
|
31
|
+
*/
|
|
32
|
+
async loadComponents(directoryPath) {
|
|
33
|
+
await this.#_loadDirectory(directoryPath, {
|
|
34
|
+
type: TYPE_COMPONENT,
|
|
35
|
+
keyPrefix: "components/",
|
|
36
|
+
accept: (entry) => entry.endsWith(MUSTACHE_EXT),
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* @param {string} directoryPath
|
|
42
|
+
*/
|
|
43
|
+
async loadLayouts(directoryPath) {
|
|
44
|
+
await this.#_loadDirectory(directoryPath, {
|
|
45
|
+
type: TYPE_LAYOUT,
|
|
46
|
+
accept: (entry) => !entry.startsWith("_") && entry.endsWith(MUSTACHE_EXT),
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* @param {string} directoryPath
|
|
52
|
+
*/
|
|
53
|
+
async loadTemplates(directoryPath) {
|
|
54
|
+
await this.#_loadDirectory(directoryPath, {
|
|
55
|
+
type: TYPE_TEMPLATE,
|
|
56
|
+
accept: (entry) => entry.endsWith(MUSTACHE_EXT),
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* @param {string} type
|
|
62
|
+
*/
|
|
63
|
+
list(type) {
|
|
64
|
+
return Array.from(this.#_ensure(type).keys());
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* @param {string} type
|
|
69
|
+
* @param {string} key
|
|
70
|
+
*/
|
|
71
|
+
get(type, key) {
|
|
72
|
+
const template = this.#_ensure(type).get(key);
|
|
73
|
+
if (!template) {
|
|
74
|
+
throw new Error(`Template not found: ${key}`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return template.content;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* @param {string} type
|
|
82
|
+
* @param {string} key
|
|
83
|
+
*/
|
|
84
|
+
getTemplate(type, key) {
|
|
85
|
+
const template = this.#_ensure(type).get(key);
|
|
86
|
+
if (!template) {
|
|
87
|
+
throw new Error(`Template not found: ${key}`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return template;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* @param {string} type
|
|
95
|
+
*/
|
|
96
|
+
getFiles(type) {
|
|
97
|
+
/** @type {Record<string, string>} */
|
|
98
|
+
const result = {};
|
|
99
|
+
for (const [key, template] of this.#_ensure(type).entries()) {
|
|
100
|
+
result[key] = template.content;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return result;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* @param {string} type
|
|
108
|
+
*/
|
|
109
|
+
getCount(type) {
|
|
110
|
+
return this.#_ensure(type).size;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* @param {string} directoryPath
|
|
115
|
+
* @param {{ type: string, keyPrefix?: string, accept: (entry: string) => boolean }} options
|
|
116
|
+
*/
|
|
117
|
+
async #_loadDirectory(directoryPath, options) {
|
|
118
|
+
const isExists = await _io.directory.exists(directoryPath);
|
|
119
|
+
if (!isExists) {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const { type, keyPrefix = "", accept } = options;
|
|
124
|
+
const entries = await _io.directory.read(directoryPath);
|
|
125
|
+
for (const entry of entries) {
|
|
126
|
+
if (!accept(entry)) {
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const key = `${keyPrefix}${entry.replace(`${MUSTACHE_EXT}`, "")}`;
|
|
131
|
+
const path = _io.path.combine(directoryPath, entry);
|
|
132
|
+
const raw = await _io.file.read(path);
|
|
133
|
+
|
|
134
|
+
const template = new Template(key, type, path, raw);
|
|
135
|
+
this.#_ensure(type).set(key, template);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* @param {string} type
|
|
141
|
+
* @returns {Map<string, Template>}
|
|
142
|
+
*/
|
|
143
|
+
#_ensure(type) {
|
|
144
|
+
let bucket = this.#_cache.get(type);
|
|
145
|
+
if (!bucket) {
|
|
146
|
+
bucket = new Map();
|
|
147
|
+
this.#_cache.set(type, bucket);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return bucket;
|
|
151
|
+
}
|
|
152
|
+
}
|