@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/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();
@@ -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.2",
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.1",
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
+ }