@shevky/core 0.0.2 → 0.0.4
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 +787 -0
- package/engines/pluginEngine.js +103 -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 +6 -2
- package/registries/contentRegistry.js +323 -0
- package/registries/pageRegistry.js +29 -0
- package/registries/pluginRegistry.js +94 -0
- package/registries/templateRegistry.js +152 -0
- package/scripts/cli.js +2 -2
- package/scripts/main.js +1 -1
- 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 +98 -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.4",
|
|
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
|
],
|
|
@@ -32,7 +36,7 @@
|
|
|
32
36
|
},
|
|
33
37
|
"homepage": "https://github.com/shevky/core#readme",
|
|
34
38
|
"dependencies": {
|
|
35
|
-
"@shevky/base": "^0.0.
|
|
39
|
+
"@shevky/base": "^0.0.2",
|
|
36
40
|
"@types/node": "^20.11.30",
|
|
37
41
|
"@types/mustache": "^4.2.6",
|
|
38
42
|
"@types/html-minifier-terser": "^7.0.2",
|
|
@@ -0,0 +1,323 @@
|
|
|
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.#_resetCaches();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
get count() {
|
|
59
|
+
return this.#_cache.length;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
get files() {
|
|
63
|
+
return this.#_cache;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* @param {import("../types/index.d.ts").ContentFileLike | ContentFile} input
|
|
68
|
+
*/
|
|
69
|
+
addContent(input) {
|
|
70
|
+
if (!input) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (input instanceof ContentFile) {
|
|
75
|
+
this.#_cache.push(input);
|
|
76
|
+
this.#_resetCaches();
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const header =
|
|
81
|
+
input.header && typeof input.header === "object" ? input.header : {};
|
|
82
|
+
const content =
|
|
83
|
+
typeof input.content === "string"
|
|
84
|
+
? input.content
|
|
85
|
+
: typeof input.body?.content === "string"
|
|
86
|
+
? input.body.content
|
|
87
|
+
: "";
|
|
88
|
+
const sourcePath =
|
|
89
|
+
typeof input.sourcePath === "string"
|
|
90
|
+
? input.sourcePath
|
|
91
|
+
: "plugin://content/unknown.md";
|
|
92
|
+
const isValid = typeof input.isValid === "boolean" ? input.isValid : true;
|
|
93
|
+
|
|
94
|
+
const contentFile = new ContentFile(header, content, sourcePath, isValid);
|
|
95
|
+
this.#_cache.push(contentFile);
|
|
96
|
+
this.#_resetCaches();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* @returns {Record<string, Array<{ key: string, label: string, url: string, lang: string }>>}
|
|
101
|
+
*/
|
|
102
|
+
buildFooterPolicies() {
|
|
103
|
+
if (this.#_footerPoliciesCache) {
|
|
104
|
+
return this.#_footerPoliciesCache;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (this.count === 0) {
|
|
108
|
+
this.#_footerPoliciesCache = {};
|
|
109
|
+
return this.#_footerPoliciesCache;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** @type {Record<string, Array<{ key: string, label: string, url: string, lang: string }>>} */
|
|
113
|
+
const policiesByLang = {};
|
|
114
|
+
const contentFiles = this.files;
|
|
115
|
+
for (const file of contentFiles) {
|
|
116
|
+
if (
|
|
117
|
+
!file.isValid ||
|
|
118
|
+
file.isDraft ||
|
|
119
|
+
!file.isPublished ||
|
|
120
|
+
file.category !== "policy"
|
|
121
|
+
) {
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const policy = {
|
|
126
|
+
lang: file.lang,
|
|
127
|
+
key: file.id,
|
|
128
|
+
label: file.menuLabel,
|
|
129
|
+
url: this.#_metaEngine.buildContentUrl(
|
|
130
|
+
file.canonical,
|
|
131
|
+
file.lang,
|
|
132
|
+
file.slug,
|
|
133
|
+
),
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
if (!Array.isArray(policiesByLang[file.lang])) {
|
|
137
|
+
policiesByLang[file.lang] = [];
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
policiesByLang[file.lang].push(policy);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
Object.keys(policiesByLang).forEach((lang) => {
|
|
144
|
+
policiesByLang[lang].sort((a, b) => a.label.localeCompare(b.label, lang));
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
this.#_footerPoliciesCache = policiesByLang;
|
|
148
|
+
return this.#_footerPoliciesCache;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* @returns {Record<string, Record<string, { id: string, lang: string, title: string, canonical: string }>>}
|
|
153
|
+
*/
|
|
154
|
+
buildContentIndex() {
|
|
155
|
+
if (this.#_contentIndexCache) {
|
|
156
|
+
return this.#_contentIndexCache;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (this.count === 0) {
|
|
160
|
+
this.#_contentIndexCache = {};
|
|
161
|
+
return this.#_contentIndexCache;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** @type {Record<string, Record<string, { id: string, lang: string, title: string, canonical: string }>>} */
|
|
165
|
+
const index = {};
|
|
166
|
+
const contentFiles = this.files;
|
|
167
|
+
for (const file of contentFiles) {
|
|
168
|
+
if (!file.isValid || file.isDraft || !file.isPublished || !file.id) {
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (!index[file.id]) {
|
|
173
|
+
index[file.id] = {};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
index[file.id][file.lang] = {
|
|
177
|
+
id: file.id,
|
|
178
|
+
lang: file.lang,
|
|
179
|
+
title: file.title,
|
|
180
|
+
canonical: this.#_metaEngine.buildContentUrl(
|
|
181
|
+
file.canonical,
|
|
182
|
+
file.lang,
|
|
183
|
+
file.slug,
|
|
184
|
+
),
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
this.#_contentIndexCache = index;
|
|
189
|
+
return this.#_contentIndexCache;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* @returns {import("../types/index.d.ts").CollectionsByLang}
|
|
194
|
+
*/
|
|
195
|
+
buildCategoryTagCollections() {
|
|
196
|
+
if (this.#_collectionsCache) {
|
|
197
|
+
return this.#_collectionsCache;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (this.count === 0) {
|
|
201
|
+
this.#_collectionsCache = {};
|
|
202
|
+
return this.#_collectionsCache;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** @type {import("../types/index.d.ts").CollectionsByLang} */
|
|
206
|
+
const pagesByLang = {};
|
|
207
|
+
const contentFiles = this.files;
|
|
208
|
+
for (const file of contentFiles) {
|
|
209
|
+
if (!file.isValid || file.isDraft || !file.isPublished) {
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const contentSummary = new ContentSummary(file);
|
|
214
|
+
const summary = /** @type {import("../types/index.d.ts").CollectionEntry} */ ({
|
|
215
|
+
...contentSummary.toObject(),
|
|
216
|
+
canonical: this.#_metaEngine.buildContentUrl(
|
|
217
|
+
file.canonical,
|
|
218
|
+
file.lang,
|
|
219
|
+
file.slug,
|
|
220
|
+
),
|
|
221
|
+
});
|
|
222
|
+
const langStore = pagesByLang[file.lang] ?? (pagesByLang[file.lang] = {});
|
|
223
|
+
|
|
224
|
+
if (file.isPostTemplate && file.isFeatured) {
|
|
225
|
+
this.#_addCollectionEntry(langStore, "home", summary, "home");
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (file.category) {
|
|
229
|
+
this.#_addCollectionEntry(
|
|
230
|
+
langStore,
|
|
231
|
+
file.category,
|
|
232
|
+
summary,
|
|
233
|
+
"category",
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
for (const tag of file.tags) {
|
|
238
|
+
this.#_addCollectionEntry(langStore, tag, summary, "tag");
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (file.series) {
|
|
242
|
+
this.#_addCollectionEntry(
|
|
243
|
+
langStore,
|
|
244
|
+
file.series,
|
|
245
|
+
{
|
|
246
|
+
...contentSummary.toObject(),
|
|
247
|
+
seriesTitle: file.seriesTitle,
|
|
248
|
+
},
|
|
249
|
+
"series",
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
this.#_collectionsCache = this.#_sortCollectionEntries(pagesByLang);
|
|
255
|
+
return this.#_collectionsCache;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* @param {string} filePath
|
|
260
|
+
* @returns {Promise<ContentFile>}
|
|
261
|
+
*/
|
|
262
|
+
async #_loadFromFile(filePath) {
|
|
263
|
+
const raw = await _io.file.read(filePath);
|
|
264
|
+
let isValid = false;
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* @type {{data: Record<string, unknown>, content: string}}
|
|
268
|
+
*/
|
|
269
|
+
let matterResponse = { data: {}, content: "" };
|
|
270
|
+
|
|
271
|
+
try {
|
|
272
|
+
matterResponse = matter(raw);
|
|
273
|
+
isValid = true;
|
|
274
|
+
} catch {}
|
|
275
|
+
|
|
276
|
+
const { data, content } = matterResponse;
|
|
277
|
+
return new ContentFile(data, content, filePath, isValid);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
#_resetCaches() {
|
|
281
|
+
this.#_collectionsCache = null;
|
|
282
|
+
this.#_footerPoliciesCache = null;
|
|
283
|
+
this.#_contentIndexCache = null;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* @param {Record<string, import("../types/index.d.ts").CollectionEntry[]>} store
|
|
288
|
+
* @param {string} key
|
|
289
|
+
* @param {import("../types/index.d.ts").CollectionEntry} entry
|
|
290
|
+
* @param {string} type
|
|
291
|
+
*/
|
|
292
|
+
#_addCollectionEntry(store, key, entry, type) {
|
|
293
|
+
if (!store[key]) {
|
|
294
|
+
store[key] = [];
|
|
295
|
+
}
|
|
296
|
+
store[key].push({
|
|
297
|
+
...entry,
|
|
298
|
+
type,
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* @param {import("../types/index.d.ts").CollectionsByLang} collections
|
|
304
|
+
*/
|
|
305
|
+
#_sortCollectionEntries(collections) {
|
|
306
|
+
/** @type {import("../types/index.d.ts").CollectionsByLang} */
|
|
307
|
+
const sorted = {};
|
|
308
|
+
Object.keys(collections).forEach((lang) => {
|
|
309
|
+
sorted[lang] = {};
|
|
310
|
+
Object.keys(collections[lang]).forEach((key) => {
|
|
311
|
+
sorted[lang][key] = collections[lang][key].slice().sort((a, b) => {
|
|
312
|
+
const aDate = Date.parse(String(a.date ?? "")) || 0;
|
|
313
|
+
const bDate = Date.parse(String(b.date ?? "")) || 0;
|
|
314
|
+
if (aDate === bDate) {
|
|
315
|
+
return (a.title ?? "").localeCompare(b.title ?? "", lang);
|
|
316
|
+
}
|
|
317
|
+
return bDate - aDate;
|
|
318
|
+
});
|
|
319
|
+
});
|
|
320
|
+
});
|
|
321
|
+
return sorted;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
@@ -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
|
+
}
|