@moonwave99/goffre 0.0.7 → 0.1.1

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/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  (The MIT License)
2
2
 
3
- Copyright (c) 2021 Diego Caponera <hello@diegocaponera.com>
3
+ Copyright (c) 2026 Diego Caponera <hello@diegocaponera.com>
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining
6
6
  a copy of this software and associated documentation files (the
package/README.md CHANGED
@@ -7,31 +7,31 @@ It uses [handlebars][handlebars] as templating system and [markdown + frontmatte
7
7
  ## Installation
8
8
 
9
9
  ```bash
10
- $ npm install goffre --save
10
+ npm install @moonwave99/goffre --save
11
11
  ```
12
12
 
13
13
  ## Basic Usage
14
14
 
15
15
  ```js
16
- import { load, render } from "goffre";
16
+ import { load, render } from "@moonwave99/goffre";
17
17
 
18
18
  (async () => {
19
- const { pages } = await load();
20
-
21
- try {
22
- const results = await render({ pages });
23
- console.log(`Generated ${results.length} pages`);
24
- } catch (error) {
25
- console.log("Error generating site", error);
26
- }
19
+ const { pages } = await load();
20
+
21
+ try {
22
+ const results = await render({ pages });
23
+ console.log(`Generated ${results.length} pages`);
24
+ } catch (error) {
25
+ console.log("Error generating site", error);
26
+ }
27
27
  })();
28
28
  ```
29
29
 
30
30
  Default paths:
31
31
 
32
- - **markdown files**: `./data` - used by `load()`
33
- - **output folder**: `./dist` - used by `render()`
34
- - **handlebars views**: `./src/views` - used by `render()`
32
+ - **markdown files**: `./data` - used by `load()`
33
+ - **output folder**: `./dist` - used by `render()`
34
+ - **handlebars views**: `./src/views` - used by `render()`
35
35
 
36
36
  See [examples](#examples) for a more advanced use case, and the [documentation][docs] for the complete reference.
37
37
 
@@ -66,18 +66,18 @@ The `render()` method writes then every incoming page to `{page.slug}.html` - yo
66
66
  ```js
67
67
  const { pages } = await load({ dataPath });
68
68
  const results = await render({
69
- buildPath,
70
- sitePath,
71
- pages: [
72
- ...pages,
73
- {
74
- title: "Goffre | Mini static site generator",
75
- description:
76
- "Goffre is a minimal static site generator available to the node.js ecosystem.",
77
- slug: "index",
78
- content: await readFile(path.join("..", "README.md"), "utf8"),
79
- },
80
- ],
69
+ buildPath,
70
+ sitePath,
71
+ pages: [
72
+ ...pages,
73
+ {
74
+ title: "Goffre | Mini static site generator",
75
+ description:
76
+ "Goffre is a minimal static site generator available to the node.js ecosystem.",
77
+ slug: "index",
78
+ content: await readFile(path.join("..", "README.md"), "utf8"),
79
+ },
80
+ ],
81
81
  });
82
82
  ```
83
83
 
@@ -97,11 +97,11 @@ The scripts of `package.json` will look more or less like:
97
97
 
98
98
  ```json
99
99
  {
100
- "clean": "rm -rf dist",
101
- "dev:client": "webpack serve --mode development",
102
- "dev:site": "nodemon -e js,json,md,handlebars --watch index.js --watch data --watch src/views",
103
- "build:client": "webpack --mode production",
104
- "build:site": "node index.js"
100
+ "clean": "rm -rf dist",
101
+ "dev:client": "webpack serve --mode development",
102
+ "dev:site": "nodemon -e js,json,md,handlebars --watch index.js --watch data --watch src/views",
103
+ "build:client": "webpack --mode production",
104
+ "build:site": "node index.js"
105
105
  }
106
106
  ```
107
107
 
@@ -109,17 +109,15 @@ Just `npm run dev:client` and `npm run dev:site` in two terminal tabs and you ar
109
109
 
110
110
  ## Examples
111
111
 
112
- - [devblog][examples-devblog] - a personal website with blog posts and project pages
113
- - this page of course
112
+ - [devblog][examples-devblog] - a personal website with blog posts and project pages
113
+ - this page of course
114
114
 
115
115
  [handlebars]: https://handlebarsjs.com/
116
- [express-handlebars]: https://www.npmjs.com/package/express-handlebars
117
116
  [mdfront]: https://www.google.com/search?q=markdown+frontmatter
118
117
  [webpack]: https://webpack.js.org/
119
118
  [webpack-dev-server]: https://webpack.js.org/configuration/dev-server/
120
119
  [http-server]: https://www.npmjs.com/package/http-server
121
120
  [nodemon]: https://www.npmjs.com/package/nodemon
122
- [example]: https://github.com/moonwave99/goffre/tree/main/examples/devblog
123
121
  [docs]: https://github.com/moonwave99/goffre/tree/main/examples/devblog
124
122
  [webpack-config]: https://github.com/moonwave99/goffre/blob/main/homepage/webpack.config.cjs
125
123
  [examples-devblog]: https://goffre-examples-devblog.netlify.app/
@@ -0,0 +1,83 @@
1
+ import { MarkedExtension, RendererObject } from 'marked';
2
+
3
+ type Page = {
4
+ slug: string;
5
+ link?: string;
6
+ template?: string;
7
+ layout?: string | null;
8
+ content?: string;
9
+ extname?: string;
10
+ };
11
+ declare function getSlug(slug: string, params: Record<string, unknown>): string;
12
+ type GetTemplateParams = {
13
+ page: Page;
14
+ templates: string[];
15
+ defaultTemplate?: string;
16
+ };
17
+ declare function getTemplate({ page, templates, defaultTemplate, }: GetTemplateParams): string;
18
+ type LoadParams = {
19
+ dataPath?: string;
20
+ };
21
+ declare function load({ dataPath }?: LoadParams): Promise<{
22
+ json: {};
23
+ pages: {
24
+ excerpt: string | undefined;
25
+ slug: string;
26
+ description: any;
27
+ content: string;
28
+ }[];
29
+ }>;
30
+ declare function loadJSON(cwd: string): Promise<{}>;
31
+ declare function loadMarkdown(cwd: string): Promise<{
32
+ excerpt: string | undefined;
33
+ slug: string;
34
+ description: any;
35
+ content: string;
36
+ }[]>;
37
+ type GetSorterParams = {
38
+ sortBy: string;
39
+ order: "asc" | "desc";
40
+ };
41
+ declare function getSorter<T extends Record<string, unknown>>({ sortBy, order, }: GetSorterParams): (a: T, b: T) => number;
42
+ type RenderParams = {
43
+ pages: Page[];
44
+ viewsPath?: string;
45
+ buildPath?: string;
46
+ blockSeparator?: string;
47
+ domain?: string;
48
+ uglyUrls?: boolean;
49
+ logLevel?: "silent" | "verbose" | "normal";
50
+ locals: Record<string, unknown>;
51
+ sitemap?: {
52
+ generate?: boolean;
53
+ template?: string;
54
+ };
55
+ env?: Record<string, unknown>;
56
+ handlebars?: {
57
+ extname?: string;
58
+ helpers?: Record<string, unknown>;
59
+ };
60
+ markdown?: {
61
+ middleware?: (MarkedExtension | (() => MarkedExtension))[];
62
+ renderer?: RendererObject;
63
+ };
64
+ };
65
+ declare function render({ pages, viewsPath, buildPath, blockSeparator, domain, uglyUrls, logLevel, locals, markdown, handlebars, sitemap, env, }: RenderParams): Promise<unknown[]>;
66
+ type PaginateParams<T extends Page> = {
67
+ collection: T[];
68
+ size?: number;
69
+ sortBy?: keyof T;
70
+ order?: "asc" | "desc";
71
+ };
72
+ type PaginatedResult<T extends Page> = {
73
+ pagination: {
74
+ page: number;
75
+ prev: number | null;
76
+ next: number | null;
77
+ total: number;
78
+ };
79
+ items: T[];
80
+ };
81
+ declare function paginate<T extends Page>({ collection, size, sortBy, order, }: PaginateParams<T>): PaginatedResult<T>[];
82
+
83
+ export { type Page, getSlug, getSorter, getTemplate, load, loadJSON, loadMarkdown, paginate, render };
package/dist/index.js ADDED
@@ -0,0 +1,395 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __export = (target, all) => {
3
+ for (var name in all)
4
+ __defProp(target, name, { get: all[name], enumerable: true });
5
+ };
6
+
7
+ // lib/goffre.ts
8
+ import path from "path";
9
+ import { globby } from "globby";
10
+ import { marked as marked2 } from "marked";
11
+ import matter from "gray-matter";
12
+ import { createRequire } from "module";
13
+ import fs from "fs-extra";
14
+ import express from "express";
15
+ import { engine } from "express-handlebars";
16
+ import chalk from "chalk";
17
+ import slugify from "slugify";
18
+
19
+ // lib/helpers.ts
20
+ var helpers_exports = {};
21
+ __export(helpers_exports, {
22
+ getAsset: () => getAsset,
23
+ getLink: () => getLink,
24
+ getNavClass: () => getNavClass,
25
+ getParamLink: () => getParamLink,
26
+ getSitemapLink: () => getSitemapLink,
27
+ list: () => list,
28
+ markdown: () => markdown,
29
+ nextItem: () => nextItem,
30
+ prevItem: () => prevItem
31
+ });
32
+ import { marked } from "marked";
33
+ var markdown = (text) => marked(text);
34
+ var getParamLink = (url, options) => {
35
+ const output = getSlug(url, options.hash);
36
+ return getAsset(output, options);
37
+ };
38
+ var getAsset = (asset, context) => {
39
+ const { options, env } = context.data.root;
40
+ return env.mode === "prod" && options.domain ? `${options.domain}${asset.startsWith("/") ? "" : "/"}${asset}` : asset;
41
+ };
42
+ var getSitemapLink = (page, context) => {
43
+ const { options } = context.data.root;
44
+ return `${options.domain}${getLink(page, context)}`;
45
+ };
46
+ var getLink = (page, context) => {
47
+ const base = page.link || `${page.slug.startsWith("/") ? "" : "/"}${page.slug}`;
48
+ const { uglyUrls } = context.data.root.options;
49
+ if (uglyUrls) {
50
+ return getAsset(`${base === "/" ? "/index" : base}.html`, context);
51
+ }
52
+ return getAsset(base.replace(/^\/index/, "/"), context);
53
+ };
54
+ var getNavClass = ({ slug }, currentPage) => {
55
+ const cleanSlug = slug && slug[0] === "/" ? slug.slice(1) : slug;
56
+ return currentPage.slug.startsWith(cleanSlug) ? `${cleanSlug} current` : cleanSlug;
57
+ };
58
+ var list = (context, options) => {
59
+ const offset = parseInt(options.hash.offset, 10) || 0;
60
+ const limit = parseInt(options.hash.limit, 10) || 100;
61
+ const sortBy = options.hash.sortBy || "slug";
62
+ const order = options.hash.order || "asc";
63
+ let output = "";
64
+ let i;
65
+ const data = context.toSorted(getSorter({ sortBy, order }));
66
+ if (offset < 0) {
67
+ i = -offset < data.length ? data.length - -offset : 0;
68
+ } else {
69
+ i = offset < data.length ? offset : 0;
70
+ }
71
+ const j = limit + i < data.length ? limit + i : data.length;
72
+ for (; i < j; i++) {
73
+ output += options.fn(data[i]);
74
+ }
75
+ return output;
76
+ };
77
+ var nextItem = (context, options) => {
78
+ const { list: list2 } = options.hash;
79
+ const index = list2.findIndex((x) => x.slug === context.slug);
80
+ const next = list2[index + 1];
81
+ if (!next) {
82
+ return;
83
+ }
84
+ return options.fn(next);
85
+ };
86
+ var prevItem = (context, options) => {
87
+ const { list: list2 } = options.hash;
88
+ const index = list2.findIndex((x) => x.slug === context.slug);
89
+ const prev = list2[index - 1];
90
+ if (!prev) {
91
+ return;
92
+ }
93
+ return options.fn(prev);
94
+ };
95
+
96
+ // lib/goffre.ts
97
+ var require2 = createRequire(import.meta.url);
98
+ var { readFile, outputFile } = fs;
99
+ var DEFAULT_DATA_PATH = path.join(process.cwd(), "data");
100
+ var DEFAULT_VIEWS_PATH = path.join(process.cwd(), "src", "views");
101
+ var DEFAULT_BUILD_PATH = path.join(process.cwd(), "dist");
102
+ var MAX_SLUG_LOG_LENGTH = 40;
103
+ var DEFAULT_BLOCK_SEPARATOR = "<!-- block -->";
104
+ function log(...args) {
105
+ console.log.apply(
106
+ null,
107
+ ["[goffre]", ...args].map((x) => chalk.cyan(x))
108
+ );
109
+ }
110
+ function getEnv() {
111
+ return {
112
+ mode: process.env.MODE || "dev"
113
+ };
114
+ }
115
+ function stringify(token) {
116
+ if (token instanceof Date) {
117
+ return token.toISOString().split("T")[0];
118
+ }
119
+ return `${token}`;
120
+ }
121
+ function getSlug(slug, params) {
122
+ return slug.split("/").reduce((memo, x) => {
123
+ if (!x.startsWith(":")) {
124
+ return [...memo, x];
125
+ }
126
+ const param = x.slice(1);
127
+ const value = params[param];
128
+ if (!value) {
129
+ throw new Error(`No value found for parameter: ${param}`);
130
+ }
131
+ return [
132
+ ...memo,
133
+ slugify(stringify(value), {
134
+ lower: true,
135
+ strict: true
136
+ })
137
+ ];
138
+ }, []).join("/");
139
+ }
140
+ function getTemplate({
141
+ page,
142
+ templates = [],
143
+ defaultTemplate = "_default"
144
+ }) {
145
+ if (templates.find((x) => x === `${page.template}.handlebars`)) {
146
+ return page.template;
147
+ }
148
+ if (templates.find((x) => x.startsWith(page.slug))) {
149
+ return page.slug;
150
+ }
151
+ return defaultTemplate;
152
+ }
153
+ function renderPage({
154
+ app,
155
+ templates,
156
+ buildPath,
157
+ maxSlugLogLength,
158
+ blockSeparator,
159
+ ...page
160
+ }) {
161
+ return new Promise((resolve, reject) => {
162
+ const template = getTemplate({ page, templates });
163
+ switch (app.locals.options.logLevel) {
164
+ case "silent":
165
+ break;
166
+ case "verbose":
167
+ log(
168
+ `Generating ${chalk.yellow(
169
+ page.slug.padEnd(maxSlugLogLength || MAX_SLUG_LOG_LENGTH, " ")
170
+ )} with template ${chalk.green(template)}...`
171
+ );
172
+ break;
173
+ case "normal":
174
+ default:
175
+ log(`Generating ${chalk.yellow(page.slug)}...`);
176
+ }
177
+ app.render(
178
+ template,
179
+ {
180
+ ...page,
181
+ layout: typeof page.layout === "undefined" ? "main" : page.layout,
182
+ content: page.content ? marked2.parse(page.content) : null,
183
+ blocks: getPageBlocks(
184
+ page.content,
185
+ blockSeparator || DEFAULT_BLOCK_SEPARATOR
186
+ )
187
+ },
188
+ async (error, html) => {
189
+ if (error) {
190
+ reject(error);
191
+ return;
192
+ }
193
+ const outputFileName = `${page.slug}${page.extname || ".html"}`;
194
+ await outputFile(path.join(buildPath, outputFileName), html);
195
+ resolve({
196
+ ...page,
197
+ outputFileName
198
+ });
199
+ }
200
+ );
201
+ });
202
+ }
203
+ function getPageBlocks(content = "", separator) {
204
+ if (!content.includes(separator)) {
205
+ return [];
206
+ }
207
+ const blocks = content.split(separator).map((x) => marked2.parse(x));
208
+ return blocks.filter(Boolean);
209
+ }
210
+ async function load({ dataPath } = {}) {
211
+ return {
212
+ json: await loadJSON(dataPath || DEFAULT_DATA_PATH),
213
+ pages: await loadMarkdown(dataPath || DEFAULT_DATA_PATH)
214
+ };
215
+ }
216
+ async function loadJSON(cwd) {
217
+ const files = await globby("**/*.json", { cwd });
218
+ return files.reduce(
219
+ (memo, x) => ({
220
+ ...memo,
221
+ [path.basename(x, ".json")]: require2(path.join(cwd, x))
222
+ }),
223
+ {}
224
+ );
225
+ }
226
+ async function loadMarkdown(cwd) {
227
+ const files = await globby("**/*.md", { cwd });
228
+ return Promise.all(
229
+ files.map(async (fileName) => {
230
+ const fullPath = path.join(cwd, fileName);
231
+ const contents = await readFile(fullPath, "utf-8");
232
+ const parsed = matter(contents, { excerpt: true });
233
+ const outputFileName = fileName.replace(".md", "");
234
+ const slug = !parsed.data.slug ? outputFileName : getSlug(parsed.data.slug, parsed.data);
235
+ return {
236
+ ...parsed.data,
237
+ excerpt: parsed.excerpt,
238
+ slug,
239
+ description: parsed.data.description || parsed.excerpt,
240
+ content: parsed.content
241
+ };
242
+ })
243
+ );
244
+ }
245
+ function getSorter({
246
+ sortBy,
247
+ order
248
+ }) {
249
+ return (a, b) => {
250
+ let output;
251
+ const valA = a[sortBy];
252
+ const valB = b[sortBy];
253
+ if (valA instanceof Date && valB instanceof Date) {
254
+ output = Number(new Date(valA)) - Number(new Date(valB));
255
+ } else {
256
+ output = Number(valA) - Number(valB);
257
+ }
258
+ return order === "desc" ? -output : output;
259
+ };
260
+ }
261
+ async function render({
262
+ pages,
263
+ viewsPath = DEFAULT_VIEWS_PATH,
264
+ buildPath = DEFAULT_BUILD_PATH,
265
+ blockSeparator = DEFAULT_BLOCK_SEPARATOR,
266
+ domain,
267
+ uglyUrls = false,
268
+ logLevel = "normal",
269
+ locals = {},
270
+ markdown: markdown2 = {},
271
+ handlebars = {},
272
+ sitemap = {},
273
+ env = {}
274
+ }) {
275
+ const extname = handlebars.extname || ".handlebars";
276
+ const app = express();
277
+ app.engine(
278
+ extname,
279
+ engine({
280
+ ...handlebars,
281
+ helpers: {
282
+ ...helpers_exports,
283
+ ...handlebars.helpers
284
+ }
285
+ })
286
+ );
287
+ app.set("view engine", "handlebars");
288
+ app.set("layoutsDir", path.join(viewsPath, "layouts"));
289
+ app.set("views", viewsPath);
290
+ const templates = await globby(`**/*${extname}`, {
291
+ cwd: viewsPath
292
+ });
293
+ app.locals = {
294
+ ...app.locals,
295
+ ...locals,
296
+ options: {
297
+ domain,
298
+ uglyUrls,
299
+ logLevel
300
+ },
301
+ env: { ...getEnv(), ...env }
302
+ };
303
+ if (markdown2.middleware) {
304
+ markdown2.middleware.forEach(
305
+ (x) => marked2.use(typeof x === "function" ? x() : x)
306
+ );
307
+ }
308
+ marked2.use(markdown2);
309
+ switch (logLevel) {
310
+ case "silent":
311
+ break;
312
+ case "verbose":
313
+ case "normal":
314
+ default:
315
+ log(`Start generation...`);
316
+ }
317
+ const results = await Promise.all(
318
+ pages.map(
319
+ (x) => renderPage({
320
+ ...x,
321
+ buildPath,
322
+ app,
323
+ templates,
324
+ blockSeparator,
325
+ maxSlugLogLength: Math.min(
326
+ Math.max.call(null, ...pages.map((x2) => x2.slug.length)),
327
+ MAX_SLUG_LOG_LENGTH
328
+ )
329
+ })
330
+ )
331
+ );
332
+ switch (logLevel) {
333
+ case "silent":
334
+ break;
335
+ case "verbose":
336
+ case "normal":
337
+ default:
338
+ log(`Generated ${results.length} pages`);
339
+ }
340
+ if (sitemap.generate) {
341
+ renderPage({
342
+ slug: "sitemap",
343
+ template: sitemap.template || "sitemap",
344
+ extname: ".xml",
345
+ layout: null,
346
+ pages: results,
347
+ buildPath,
348
+ app,
349
+ templates
350
+ });
351
+ }
352
+ return results;
353
+ }
354
+ function paginate({
355
+ collection,
356
+ size = 10,
357
+ sortBy = "slug",
358
+ order = "asc"
359
+ }) {
360
+ const total = Math.ceil(collection.length / size);
361
+ return collection.toSorted(getSorter({ sortBy, order })).reduce((memo, x, index) => {
362
+ if (index % size === 0) {
363
+ const page = Math.floor(index / size) + 1;
364
+ return [
365
+ ...memo,
366
+ {
367
+ pagination: {
368
+ page,
369
+ prev: page > 1 ? page - 1 : null,
370
+ next: page < total ? page + 1 : null,
371
+ total
372
+ },
373
+ items: [x]
374
+ }
375
+ ];
376
+ }
377
+ return [
378
+ ...memo.slice(0, -1),
379
+ {
380
+ ...memo[memo.length - 1],
381
+ items: [...memo[memo.length - 1].items, x]
382
+ }
383
+ ];
384
+ }, []);
385
+ }
386
+ export {
387
+ getSlug,
388
+ getSorter,
389
+ getTemplate,
390
+ load,
391
+ loadJSON,
392
+ loadMarkdown,
393
+ paginate,
394
+ render
395
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moonwave99/goffre",
3
- "version": "0.0.7",
3
+ "version": "0.1.1",
4
4
  "description": "Mini static site generator",
5
5
  "author": {
6
6
  "name": "Diego Caponera",
@@ -13,11 +13,23 @@
13
13
  },
14
14
  "homepage": "https://moonwave99.github.io/goffre",
15
15
  "type": "module",
16
- "scripts": {
17
- "test": "ava **/*.test.js",
18
- "lint": "npx eslint ./lib --ext .js,.jsx,.ts,.tsx"
16
+ "ava": {
17
+ "extensions": {
18
+ "ts": "module"
19
+ },
20
+ "nodeArguments": [
21
+ "--import=tsimp"
22
+ ],
23
+ "environmentVariables": {
24
+ "TSIMP_DIAG": "ignore"
25
+ }
19
26
  },
20
- "main": "index.js",
27
+ "main": "dist/index.js",
28
+ "module": "dist/index.mjs",
29
+ "types": "dist/index.d.ts",
30
+ "files": [
31
+ "dist"
32
+ ],
21
33
  "keywords": [
22
34
  "front-matter",
23
35
  "generate",
@@ -27,29 +39,46 @@
27
39
  "static-site",
28
40
  "yaml"
29
41
  ],
30
- "files": [
31
- "lib/goffre.js",
32
- "lib/helpers.js"
33
- ],
34
42
  "license": "MIT",
35
43
  "dependencies": {
36
44
  "chalk": "^4.1.2",
37
45
  "express": "^4.17.1",
38
- "express-handlebars": "^6.0.1",
46
+ "express-handlebars": "^8.0.6",
39
47
  "fs-extra": "^10.0.0",
40
48
  "globby": "^12.0.2",
41
49
  "gray-matter": "^4.0.3",
42
- "marked": "^4.0.3",
50
+ "marked": "^17.0.3",
43
51
  "slugify": "^1.6.2"
44
52
  },
45
53
  "devDependencies": {
46
- "ava": "^3.15.0",
54
+ "@eslint/compat": "^2.0.2",
55
+ "@eslint/eslintrc": "^3.3.3",
56
+ "@eslint/js": "^10.0.1",
57
+ "@faker-js/faker": "^10.3.0",
58
+ "@types/express": "^5.0.6",
59
+ "@types/fs-extra": "^11.0.4",
60
+ "@types/js-yaml": "^4.0.9",
61
+ "@types/node": "^25.2.3",
62
+ "@types/sinon": "^21.0.0",
63
+ "@typescript-eslint/eslint-plugin": "^8.56.0",
64
+ "@typescript-eslint/parser": "^8.56.0",
65
+ "ava": "^6.4.1",
47
66
  "cheerio": "^1.0.0-rc.10",
48
67
  "dirname-filename-esm": "^1.1.1",
49
- "eslint": "^8.3.0",
68
+ "eslint": "^10.0.0",
50
69
  "faker": "^5.5.3",
51
- "js-yaml": "^4.1.0",
52
- "rimraf": "^3.0.2",
53
- "sinon": "^12.0.1"
70
+ "globals": "^17.3.0",
71
+ "js-yaml": "^4.1.1",
72
+ "rimraf": "^6.1.3",
73
+ "sinon": "^21.0.1",
74
+ "tsimp": "^2.0.12",
75
+ "tsup": "^8.5.1",
76
+ "typescript": "^5.9.3"
77
+ },
78
+ "scripts": {
79
+ "build": "tsup",
80
+ "dev": "tsup --watch",
81
+ "test": "ava **/*.test.ts",
82
+ "lint": "pnpm eslint ./lib --ext .js,.jsx,.ts,.tsx"
54
83
  }
55
- }
84
+ }
package/index.js DELETED
@@ -1 +0,0 @@
1
- export * from "./lib/goffre.js";
package/lib/goffre.js DELETED
@@ -1,325 +0,0 @@
1
- import path from "path";
2
- import { globby } from "globby";
3
- import { marked } from "marked";
4
- import matter from "gray-matter";
5
- import { createRequire } from "module";
6
- import fs from "fs-extra";
7
- import express from "express";
8
- import { engine } from "express-handlebars";
9
- import chalk from "chalk";
10
- import slugify from "slugify";
11
- import * as defaultHelpers from "./helpers.js";
12
-
13
- const require = createRequire(import.meta.url);
14
- const { readFile, outputFile } = fs;
15
-
16
- const DEFAULT_DATA_PATH = path.join(process.cwd(), "data");
17
- const DEFAULT_VIEWS_PATH = path.join(process.cwd(), "src", "views");
18
- const DEFAULT_BUILD_PATH = path.join(process.cwd(), "dist");
19
- const MAX_SLUG_LOG_LENGTH = 40;
20
- const DEFAULT_BLOCK_SEPARATOR = "<!-- block -->";
21
-
22
- function log() {
23
- console.log.apply(
24
- null,
25
- ["[goffre]", ...arguments].map((x) => chalk.cyan(x)),
26
- );
27
- }
28
-
29
- function getEnv() {
30
- return {
31
- mode: process.env.MODE || "dev",
32
- };
33
- }
34
-
35
- function stringify(token) {
36
- if (token instanceof Date) {
37
- return token.toISOString().split("T")[0];
38
- }
39
- return token;
40
- }
41
-
42
- export function getSlug(slug, params) {
43
- return slug
44
- .split("/")
45
- .reduce(
46
- (memo, x) =>
47
- !x.startsWith(":")
48
- ? [...memo, x]
49
- : [
50
- ...memo,
51
- slugify(stringify(params[x.slice(1)]), {
52
- lower: true,
53
- strict: true,
54
- }),
55
- ],
56
- [],
57
- )
58
- .join("/");
59
- }
60
-
61
- export function getTemplate({
62
- page,
63
- templates = [],
64
- defaultTemplate = "_default",
65
- }) {
66
- if (templates.find((x) => x === `${page.template}.handlebars`)) {
67
- return page.template;
68
- }
69
- if (templates.find((x) => x.startsWith(page.slug))) {
70
- return page.slug;
71
- }
72
- return defaultTemplate;
73
- }
74
-
75
- function renderPage({
76
- app,
77
- templates,
78
- buildPath,
79
- maxSlugLogLength,
80
- blockSeparator,
81
- ...page
82
- }) {
83
- return new Promise((resolve, reject) => {
84
- const template = getTemplate({ page, templates });
85
-
86
- switch (app.locals.options.logLevel) {
87
- case "silent":
88
- break;
89
- case "verbose":
90
- log(
91
- `Generating ${chalk.yellow(
92
- page.slug.padEnd(maxSlugLogLength, " "),
93
- )} with template ${chalk.green(template)}...`,
94
- );
95
- break;
96
- case "normal":
97
- default:
98
- log(`Generating ${chalk.yellow(page.slug)}...`);
99
- }
100
-
101
- app.render(
102
- template,
103
- {
104
- ...page,
105
- layout: typeof page.layout === "undefined" ? "main" : page.layout,
106
- content: page.content ? marked.parse(page.content) : null,
107
- blocks: getPageBlocks(page.content, blockSeparator),
108
- },
109
- async (error, html) => {
110
- if (error) {
111
- reject(error);
112
- return;
113
- }
114
- const outputFileName = `${page.slug}${page.extname || ".html"}`;
115
- await outputFile(path.join(buildPath, outputFileName), html);
116
- resolve({
117
- ...page,
118
- outputFileName,
119
- });
120
- },
121
- );
122
- });
123
- }
124
-
125
- function getPageBlocks(content = "", separator) {
126
- if (!content.includes(separator)) {
127
- return [];
128
- }
129
- const blocks = content.split(separator).map((x) => marked.parse(x));
130
- return blocks.filter(Boolean);
131
- }
132
-
133
- export async function load({ dataPath } = {}) {
134
- return {
135
- json: await loadJSON(dataPath || DEFAULT_DATA_PATH),
136
- pages: await loadMarkdown(dataPath || DEFAULT_DATA_PATH),
137
- };
138
- }
139
-
140
- export async function loadJSON(cwd) {
141
- const files = await globby("**/*.json", { cwd });
142
- return files.reduce(
143
- (memo, x) => ({
144
- ...memo,
145
- [path.basename(x, ".json")]: require(path.join(cwd, x)),
146
- }),
147
- {},
148
- );
149
- }
150
-
151
- function excerpt(file) {
152
- file.excerpt = file.content.split("\n")[1];
153
- }
154
-
155
- export async function loadMarkdown(cwd) {
156
- const files = await globby("**/*.md", { cwd });
157
- return Promise.all(
158
- files.map(async (fileName) => {
159
- const fullPath = path.join(cwd, fileName);
160
- const contents = await readFile(fullPath, "utf-8");
161
- const parsed = matter(contents, { excerpt });
162
- const outputFileName = fileName.replace(".md", "");
163
- const slug = !parsed.data.slug
164
- ? outputFileName
165
- : getSlug(parsed.data.slug, parsed.data);
166
- return {
167
- ...parsed.data,
168
- excerpt: parsed.excerpt,
169
- slug,
170
- description: parsed.data.description || parsed.excerpt,
171
- content: parsed.content,
172
- };
173
- }),
174
- );
175
- }
176
-
177
- export const getSorter =
178
- ({ sortBy, order }) =>
179
- (a, b) => {
180
- let output;
181
- if (a[sortBy] instanceof Date) {
182
- output = new Date(a[sortBy]) - new Date(b[sortBy]);
183
- } else {
184
- output = a[sortBy] - b[sortBy];
185
- }
186
- return order === "desc" ? -output : output;
187
- };
188
-
189
- export async function render({
190
- pages,
191
- viewsPath = DEFAULT_VIEWS_PATH,
192
- buildPath = DEFAULT_BUILD_PATH,
193
- blockSeparator = DEFAULT_BLOCK_SEPARATOR,
194
- domain,
195
- uglyUrls = false,
196
- logLevel = "normal",
197
- locals = {},
198
- markdown = {},
199
- handlebars = {},
200
- sitemap = {},
201
- env = {},
202
- }) {
203
- const extname = handlebars.extname || ".handlebars";
204
- const app = express();
205
- app.engine(
206
- extname,
207
- engine({
208
- ...handlebars,
209
- helpers: {
210
- ...defaultHelpers,
211
- ...handlebars.helpers,
212
- },
213
- }),
214
- );
215
- app.set("view engine", "handlebars");
216
- app.set("layoutsDir", path.join(viewsPath, "layouts"));
217
- app.set("views", viewsPath);
218
-
219
- const templates = await globby(`**/*${extname}`, {
220
- cwd: viewsPath,
221
- });
222
-
223
- app.locals = {
224
- ...app.locals,
225
- ...locals,
226
- options: {
227
- domain,
228
- uglyUrls,
229
- logLevel,
230
- },
231
- env: { ...getEnv(), ...env },
232
- };
233
-
234
- if (markdown.middleware) {
235
- markdown.middleware.forEach((x) =>
236
- marked.use(typeof x === "function" ? x() : x),
237
- );
238
- }
239
-
240
- marked.use(markdown);
241
-
242
- switch (logLevel) {
243
- case "silent":
244
- break;
245
- case "verbose":
246
- case "normal":
247
- default:
248
- log(`Start generation...`);
249
- }
250
-
251
- const results = await Promise.all(
252
- pages.map((x) =>
253
- renderPage({
254
- ...x,
255
- buildPath,
256
- app,
257
- templates,
258
- blockSeparator,
259
- maxSlugLogLength: Math.min(
260
- Math.max.call(null, ...pages.map((x) => x.slug.length)),
261
- MAX_SLUG_LOG_LENGTH,
262
- ),
263
- }),
264
- ),
265
- );
266
-
267
- switch (logLevel) {
268
- case "silent":
269
- break;
270
- case "verbose":
271
- case "normal":
272
- default:
273
- log(`Generated ${results.length} pages`);
274
- }
275
-
276
- if (sitemap.generate) {
277
- renderPage({
278
- slug: "sitemap",
279
- template: sitemap.template || "sitemap",
280
- extname: ".xml",
281
- layout: null,
282
- pages: results,
283
- buildPath,
284
- app,
285
- templates,
286
- });
287
- }
288
-
289
- return results;
290
- }
291
-
292
- export function paginate({
293
- collection,
294
- size = 10,
295
- sortBy = "slug",
296
- order = "asc",
297
- }) {
298
- const total = Math.ceil(collection.length / size);
299
- return collection
300
- .sort(getSorter({ sortBy, order }))
301
- .reduce((memo, x, index) => {
302
- if (index % size === 0) {
303
- const page = Math.floor(index / size) + 1;
304
- return [
305
- ...memo,
306
- {
307
- pagination: {
308
- page,
309
- prev: page > 1 ? page - 1 : null,
310
- next: page < total ? page + 1 : null,
311
- total,
312
- },
313
- items: [x],
314
- },
315
- ];
316
- }
317
- return [
318
- ...memo.slice(0, -1),
319
- {
320
- ...memo[memo.length - 1],
321
- items: [...memo[memo.length - 1].items, x],
322
- },
323
- ];
324
- }, []);
325
- }
package/lib/helpers.js DELETED
@@ -1,84 +0,0 @@
1
- import { getSorter, getSlug } from "./goffre.js";
2
- import { marked } from "marked";
3
-
4
- export const markdown = (text) => marked(text);
5
-
6
- export const getParamLink = (url, options) => {
7
- const output = getSlug(url, options.hash);
8
- return getAsset(output, options);
9
- };
10
-
11
- export const getAsset = (asset, context) => {
12
- const { options, env } = context.data.root;
13
- return env.mode === "prod" && options.domain
14
- ? `${options.domain}${asset.startsWith("/") ? "" : "/"}${asset}`
15
- : asset;
16
- };
17
-
18
- export const getSitemapLink = (page, context) => {
19
- const { options } = context.data.root;
20
- return `${options.domain}${getLink(page, context)}`;
21
- };
22
-
23
- export const getLink = (page, context) => {
24
- const base =
25
- page.link || `${page.slug.startsWith("/") ? "" : "/"}${page.slug}`;
26
- const { uglyUrls } = context.data.root.options;
27
- if (uglyUrls) {
28
- return getAsset(`${base === "/" ? "/index" : base}.html`, context);
29
- }
30
- return getAsset(base.replace(/^\/index/, "/"), context);
31
- };
32
-
33
- export const getNavClass = ({ slug }, currentPage) => {
34
- const cleanSlug = slug && slug[0] === "/" ? slug.slice(1) : slug;
35
- return currentPage.slug.startsWith(cleanSlug)
36
- ? `${cleanSlug} current`
37
- : cleanSlug;
38
- };
39
-
40
- export const list = (context, options) => {
41
- const offset = parseInt(options.hash.offset, 10) || 0;
42
- const limit = parseInt(options.hash.limit, 10) || 100;
43
- const sortBy = options.hash.sortBy || "slug";
44
- const order = options.hash.order || "asc";
45
-
46
- let output = "";
47
- let i, j;
48
-
49
- const data = [...context].sort(getSorter({ sortBy, order }));
50
-
51
- if (offset < 0) {
52
- i = -offset < data.length ? data.length - -offset : 0;
53
- } else {
54
- i = offset < data.length ? offset : 0;
55
- }
56
-
57
- j = limit + i < data.length ? limit + i : data.length;
58
-
59
- for (i, j; i < j; i++) {
60
- output += options.fn(data[i]);
61
- }
62
-
63
- return output;
64
- };
65
-
66
- export const nextItem = (context, options) => {
67
- const { list } = options.hash;
68
- const index = list.findIndex((x) => x.slug === context.slug);
69
- const next = list[index + 1];
70
- if (!next) {
71
- return;
72
- }
73
- return options.fn(next);
74
- };
75
-
76
- export const prevItem = (context, options) => {
77
- const { list } = options.hash;
78
- const index = list.findIndex((x) => x.slug === context.slug);
79
- const prev = list[index - 1];
80
- if (!prev) {
81
- return;
82
- }
83
- return options.fn(prev);
84
- };