mikel-press 0.2.0 → 0.18.2
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 +137 -19
- package/index.js +438 -180
- package/package.json +16 -20
- package/index.d.ts +0 -55
package/README.md
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
# mikel-press
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+

|
|
4
|
+

|
|
5
|
+
|
|
6
|
+
**mikel-press** is a static site generator inspired by [Jekyll](https://jekyllrb.com/) and built on top of **mikel**, a Mustache-based templating engine. It allows you to generate static websites from HTML and Markdown files using a flexible plugin system.
|
|
4
7
|
|
|
5
8
|
## Installation
|
|
6
9
|
|
|
@@ -16,37 +19,152 @@ Or **npm**:
|
|
|
16
19
|
$ npm install --dev mikel-press
|
|
17
20
|
```
|
|
18
21
|
|
|
19
|
-
##
|
|
22
|
+
## Configuration
|
|
23
|
+
|
|
24
|
+
**mikel-press** can be configured using a `config` object that accepts the following options:
|
|
25
|
+
|
|
26
|
+
| Field | Description | Default |
|
|
27
|
+
|-------|-------------|---------|
|
|
28
|
+
| `source` | The path to the directory containing the site's HTML or Markdown files. | `"content"` |
|
|
29
|
+
| `destination` | The output directory where the generated static site will be saved. | `"www"` |
|
|
30
|
+
| `layout` | The path to the layout file that will be used as the base template for all pages. | - |
|
|
31
|
+
| `plugins` | A list of plugins used to extend the functionality of mikel-press. | `[]` |
|
|
32
|
+
| `*` | Any other properties passed in config will be available as `site.*` inside each page template. | - |
|
|
33
|
+
|
|
34
|
+
Here is an example configuration object:
|
|
35
|
+
|
|
36
|
+
```javascript
|
|
37
|
+
const config = {
|
|
38
|
+
source: "./content",
|
|
39
|
+
destination: "./www",
|
|
40
|
+
layout: "./layout.html",
|
|
41
|
+
title: "Hello world",
|
|
42
|
+
description: "My awesome site",
|
|
43
|
+
plugins: [
|
|
44
|
+
press.SourcePlugin(),
|
|
45
|
+
press.FrontmatterPlugin(),
|
|
46
|
+
press.PermalinkPlugin(),
|
|
47
|
+
press.ContentPlugin(),
|
|
48
|
+
press.CopyAssetsPlugin({
|
|
49
|
+
patterns: [
|
|
50
|
+
{ from: "./static/styles.css", to: "static/" },
|
|
51
|
+
],
|
|
52
|
+
}),
|
|
53
|
+
],
|
|
54
|
+
};
|
|
55
|
+
```
|
|
20
56
|
|
|
21
|
-
|
|
57
|
+
## Content
|
|
22
58
|
|
|
23
|
-
|
|
59
|
+
### Variables
|
|
24
60
|
|
|
25
|
-
|
|
26
|
-
$ mikel-press build
|
|
27
|
-
```
|
|
61
|
+
Each HTML file processed by **mikel-press** will be handled by the mikel templating engine, that will provide the following data variables to each page:
|
|
28
62
|
|
|
29
|
-
|
|
63
|
+
#### Global variables
|
|
30
64
|
|
|
31
|
-
|
|
65
|
+
| Variable | Description |
|
|
66
|
+
|----------|-------------|
|
|
67
|
+
| `site` | Contains the site information and all the additional keys provided in the configuration object. |
|
|
68
|
+
| `page` | Specific information about the page that is rendered. |
|
|
69
|
+
| `layout` | Specific information about the layout that is used for renderin the page. |
|
|
32
70
|
|
|
33
|
-
|
|
71
|
+
#### Site variables
|
|
34
72
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
73
|
+
| Variable | Description |
|
|
74
|
+
|----------|-------------|
|
|
75
|
+
| `site.data` | An object containing all data items loaded by `DataPlugin`. |
|
|
76
|
+
| `site.pages` | A list containing all pages processed by **mikel-pres**. |
|
|
77
|
+
| `site.*` | All the additional configuration fields provided in the configuration. |
|
|
78
|
+
|
|
79
|
+
#### Page variables
|
|
80
|
+
|
|
81
|
+
| Variable | Description |
|
|
82
|
+
|----------|-------------|
|
|
83
|
+
| `page.path` | The path to the page. Example: `about/index.html`. |
|
|
84
|
+
| `page.url` | The path to the page including the leading `/`. Example: `/about/index.html`. |
|
|
85
|
+
| `page.attributes` | An object containing all the frontmatter variables in the page processed by `FrontmatterPlugin`. |
|
|
86
|
+
| `page.content` | The raw content of the page before begin processed by **mikel**. |
|
|
87
|
+
|
|
88
|
+
#### Layout variables
|
|
89
|
+
|
|
90
|
+
| Variable | Description |
|
|
91
|
+
|----------|-------------|
|
|
92
|
+
| `layout.attributes` | An object containing all the frontmatter variables in the layout processed by `FrontmatterPlugin`. |
|
|
93
|
+
| `layout.content` | The raw content of the layout. |
|
|
94
|
+
|
|
95
|
+
## Plugins
|
|
96
|
+
|
|
97
|
+
**mikel-press** relies on plugins to handle file reading, transformation, and rendering. The following plugins are built-in:
|
|
98
|
+
|
|
99
|
+
### `press.SourcePlugin(options)`
|
|
100
|
+
|
|
101
|
+
This plugin reads content from the specified `config.source` directory and loads it into the system for processing.
|
|
102
|
+
|
|
103
|
+
Options:
|
|
104
|
+
- `options.source` (string): Specifies a custom source directory. If not provided, `config.source` is used.
|
|
105
|
+
- `options.extensions` (array): Defines the file extensions that should be processed. The default value is `[".html", ".md", ".markdown"]`.
|
|
106
|
+
|
|
107
|
+
### `press.DataPlugin(options)`
|
|
108
|
+
|
|
109
|
+
This plugin loads JSON files from the specified directory and makes them available in the site context.
|
|
110
|
+
|
|
111
|
+
Options:
|
|
112
|
+
- `options.source` (string): Specifies a custom source directory for data files. If not provided, `./data` is used.
|
|
113
|
+
|
|
114
|
+
### `press.FrontmatterPlugin(options)`
|
|
38
115
|
|
|
39
|
-
This
|
|
116
|
+
This plugin processes frontmatter in Markdown and HTML files.
|
|
40
117
|
|
|
41
|
-
|
|
118
|
+
Options:
|
|
119
|
+
- `options.extensions` (array): Defines the file extensions that should be processed. The default value is `[".md", ".markdown", ".html"]`.
|
|
120
|
+
- `options.parser` (function): Frontmatter parser function (e.g., `JSON.parse`, `YAML.load`).
|
|
42
121
|
|
|
43
|
-
|
|
122
|
+
### `press.PermalinkPlugin()`
|
|
44
123
|
|
|
124
|
+
This plugin allows defining custom permalinks for pages.
|
|
45
125
|
|
|
46
|
-
|
|
126
|
+
### `press.MarkdownPlugin(options)`
|
|
47
127
|
|
|
48
|
-
|
|
128
|
+
This plugin processes Markdown files and converts them to HTML.
|
|
129
|
+
|
|
130
|
+
Options:
|
|
131
|
+
- `options.parser` (function): Markdown parser function (e.g., `marked.parse`).
|
|
132
|
+
|
|
133
|
+
### `press.ContentPlugin(options)`
|
|
134
|
+
|
|
135
|
+
This plugin processes each page and saves it into `config.destination`. It accepts an `options` object, which is passed to mikel for template processing.
|
|
136
|
+
|
|
137
|
+
### `press.CopyAssetsPlugin(options)`
|
|
138
|
+
|
|
139
|
+
This plugin copies static files from the source to the destination.
|
|
140
|
+
|
|
141
|
+
Options:
|
|
142
|
+
- `options.patterns` (array): List of file patterns to copy. Each pattern should have `from` and `to`.
|
|
143
|
+
|
|
144
|
+
## Node API
|
|
145
|
+
|
|
146
|
+
**mikel-press** provides an API with two main methods:
|
|
147
|
+
|
|
148
|
+
### `press.build(config)`
|
|
149
|
+
|
|
150
|
+
Triggers the build of **mikel-press** with the given configuration object provided as an argument.
|
|
151
|
+
|
|
152
|
+
```javascript
|
|
153
|
+
import press from "mikel-press";
|
|
154
|
+
|
|
155
|
+
press.build(config);
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### `press.watch(config)`
|
|
159
|
+
|
|
160
|
+
Calling the `watch` method triggers the build, but also watches for changes and rebuilds the site as soon as it detects a change in any of the source files.
|
|
161
|
+
|
|
162
|
+
```javascript
|
|
163
|
+
import press from "mikel-press";
|
|
164
|
+
|
|
165
|
+
press.watch(config);
|
|
166
|
+
```
|
|
49
167
|
|
|
50
168
|
## License
|
|
51
169
|
|
|
52
|
-
This project is licensed under the MIT License.
|
|
170
|
+
This project is licensed under the [MIT License](../../LICENSE).
|
package/index.js
CHANGED
|
@@ -1,204 +1,462 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
+
import * as http from "node:http";
|
|
3
4
|
import mikel from "mikel";
|
|
4
5
|
|
|
5
|
-
// @description
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
6
|
+
// @description default mime types
|
|
7
|
+
const DEFAULT_MIME_TYPES = {
|
|
8
|
+
".css": "text/css",
|
|
9
|
+
".gif": "image/gif",
|
|
10
|
+
".html": "text/html",
|
|
11
|
+
".ico": "image/vnd.microsoft.icon",
|
|
12
|
+
".jpg": "image/jpeg",
|
|
13
|
+
".jpeg": "image/jpeg",
|
|
14
|
+
".js": "text/javascript",
|
|
15
|
+
".json": "application/json",
|
|
16
|
+
".mjs": "text/javascript",
|
|
17
|
+
".png": "image/png",
|
|
18
|
+
".svg": "image/svg+xml",
|
|
19
|
+
".txt": "text/plain",
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
// @description general utilities
|
|
23
|
+
const utils = {
|
|
24
|
+
// @description read a file from disk
|
|
25
|
+
// @param {String} file path to the file to read
|
|
26
|
+
read: (file, encoding = "utf8") => {
|
|
27
|
+
return fs.readFileSync(file, encoding);
|
|
28
|
+
},
|
|
29
|
+
// @description write a file to disk
|
|
30
|
+
// @param {String} file path to the file to save
|
|
31
|
+
// @param {String} content content to save
|
|
32
|
+
write: (file, content = "") => {
|
|
33
|
+
const folder = path.dirname(file);
|
|
34
|
+
if (!fs.existsSync(folder)) {
|
|
35
|
+
fs.mkdirSync(folder, {recursive: true});
|
|
12
36
|
}
|
|
13
|
-
|
|
14
|
-
|
|
37
|
+
fs.writeFileSync(file, content, "utf8");
|
|
38
|
+
},
|
|
39
|
+
// @description copy a file
|
|
40
|
+
copy: (source, target) => {
|
|
41
|
+
const folder = path.dirname(target);
|
|
42
|
+
if (!fs.existsSync(folder)) {
|
|
43
|
+
fs.mkdirSync(folder, {recursive: true});
|
|
15
44
|
}
|
|
16
|
-
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
45
|
+
fs.copyFileSync(source, target);
|
|
46
|
+
},
|
|
47
|
+
// @description get all files from the given folder and the given extensions
|
|
48
|
+
readdir: (folder, extensions = "*") => {
|
|
49
|
+
if (!fs.existsSync(folder) || !fs.statSync(folder).isDirectory()) {
|
|
50
|
+
return [];
|
|
51
|
+
}
|
|
52
|
+
return fs.readdirSync(folder, "utf8").filter(file => {
|
|
53
|
+
return extensions === "*" || extensions.includes(path.extname(file));
|
|
54
|
+
});
|
|
55
|
+
},
|
|
56
|
+
// @description walk through the given folder and get all files
|
|
57
|
+
// @params {String} folder folder to walk through
|
|
58
|
+
// @params {Array|String} extensions extensions to include. Default: "*"
|
|
59
|
+
walkdir: (folder, extensions = "*") => {
|
|
60
|
+
const files = [];
|
|
61
|
+
const walkSync = currentFolder => {
|
|
62
|
+
const fullFolderPath = path.join(folder, currentFolder);
|
|
63
|
+
fs.readdirSync(fullFolderPath).forEach(file => {
|
|
64
|
+
const filePath = path.join(currentFolder, file);
|
|
65
|
+
const fullFilePath = path.join(fullFolderPath, file);
|
|
66
|
+
if (fs.statSync(fullFilePath).isDirectory()) {
|
|
67
|
+
return walkSync(filePath);
|
|
68
|
+
}
|
|
69
|
+
if (extensions === "*" || extensions.includes(path.extname(file))) {
|
|
70
|
+
files.push(filePath);
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
};
|
|
74
|
+
walkSync("./");
|
|
75
|
+
return files;
|
|
76
|
+
},
|
|
77
|
+
// @description watch for file changes
|
|
78
|
+
// @param {String} filePath path to the file to watch
|
|
79
|
+
// @param {Function} listener method to listen for file changes
|
|
80
|
+
watch: (filePath, listener) => {
|
|
81
|
+
let lastModifiedTime = null;
|
|
82
|
+
fs.watch(filePath, "utf8", () => {
|
|
83
|
+
const modifiedTime = fs.statSync(filePath).mtimeMs;
|
|
84
|
+
if (lastModifiedTime !== modifiedTime) {
|
|
85
|
+
lastModifiedTime = modifiedTime;
|
|
86
|
+
return listener(filePath);
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
},
|
|
90
|
+
// @description change the properties of the given path (dirname, basename, extname)
|
|
91
|
+
format: (filePath, options = {}) => {
|
|
92
|
+
const dirname = options.dirname || path.dirname(filePath);
|
|
93
|
+
const extname = options.extname || path.extname(filePath);
|
|
94
|
+
const basename = options.basename || path.basename(filePath, path.extname(filePath));
|
|
95
|
+
return path.join(dirname, `${basename}${extname}`);
|
|
96
|
+
},
|
|
97
|
+
// @description get the mime type from the given extension
|
|
98
|
+
getMimeType: (extname = ".txt") => {
|
|
99
|
+
return DEFAULT_MIME_TYPES[extname] || "text/plain";
|
|
100
|
+
},
|
|
58
101
|
};
|
|
59
102
|
|
|
60
|
-
// @description
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
const extname = options.extname || path.extname(options.file || "") || ".html";
|
|
64
|
-
const basename = options.basename || path.basename(options.file || "", extname) || "virtual";
|
|
65
|
-
const {body, attributes} = typeof options?.frontmatter == "function" ? options.frontmatter(content) : {body: content, attributes: {}};
|
|
66
|
-
return {
|
|
67
|
-
name: basename + extname,
|
|
68
|
-
basename: basename,
|
|
69
|
-
extname: extname,
|
|
70
|
-
url: options.url || attributes?.permalink || path.join("/", basename + extname),
|
|
71
|
-
data: attributes || {}, // DEPRECATED
|
|
72
|
-
attributes: attributes || {},
|
|
73
|
-
content: typeof options.transform === "function" ? options.transform(body) : body,
|
|
74
|
-
};
|
|
103
|
+
// @description add a new node item
|
|
104
|
+
const createNode = (source, path, label = "", data = {}) => {
|
|
105
|
+
return {source, path, label, data};
|
|
75
106
|
};
|
|
76
107
|
|
|
77
|
-
// @description get
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
return [];
|
|
93
|
-
};
|
|
94
|
-
|
|
95
|
-
// @description get assets
|
|
96
|
-
const readAssets = (folder, fm = null) => {
|
|
97
|
-
if (fs.existsSync(folder) && fs.lstatSync(folder).isDirectory()) {
|
|
98
|
-
const assetPaths = fs.readdirSync(folder, "utf8");
|
|
99
|
-
return Object.fromEntries(assetPaths.map(file => {
|
|
100
|
-
const asset = createVirtualPage({
|
|
101
|
-
file: path.join(folder, file),
|
|
102
|
-
frontmatter: fm,
|
|
103
|
-
});
|
|
104
|
-
const assetName = asset.basename.replaceAll(".", "_").replaceAll("-", "_");
|
|
105
|
-
return [assetName, asset];
|
|
106
|
-
}));
|
|
107
|
-
}
|
|
108
|
-
return {};
|
|
109
|
-
};
|
|
110
|
-
|
|
111
|
-
// @description read a data folder
|
|
112
|
-
const readData = folder => {
|
|
113
|
-
if (fs.existsSync(folder) && fs.lstatSync(folder).isDirectory()) {
|
|
114
|
-
const files = fs.readdirSync(folder, "utf8")
|
|
115
|
-
.filter(file => path.extname(file) === ".json")
|
|
116
|
-
.map(file => path.join(folder, file))
|
|
117
|
-
.map(file => {
|
|
118
|
-
return [path.basename(file, ".json"), JSON.parse(fs.readFileSync(file, "utf8"))];
|
|
119
|
-
});
|
|
120
|
-
return Object.fromEntries(files);
|
|
121
|
-
}
|
|
122
|
-
return {};
|
|
123
|
-
};
|
|
124
|
-
|
|
125
|
-
// @description run mikel press with the provided configuration
|
|
126
|
-
const run = (config = {}) => {
|
|
127
|
-
// 0. initialize context object
|
|
128
|
-
const hooks = ["initialize", "compiler", "beforeEmit", "emitPage", "emitAsset", "done"];
|
|
129
|
-
const context = {
|
|
130
|
-
site: config || {},
|
|
131
|
-
source: path.resolve(process.cwd(), config?.source || "."),
|
|
132
|
-
destination: path.resolve(process.cwd(), config?.destination || "./www"),
|
|
133
|
-
layout: getLayoutContent(config),
|
|
134
|
-
hooks: Object.freeze(Object.fromEntries(hooks.map(name => {
|
|
135
|
-
return [name, new Set()];
|
|
136
|
-
}))),
|
|
108
|
+
// @description get nodes with the specified label
|
|
109
|
+
const getNodesByLabel = (nodes, label) => {
|
|
110
|
+
return Array.from(nodes).filter(node => node.label === label);
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
// @description get all nodes to update
|
|
114
|
+
const getNodesToUpdate = (graph, affectedNode) => {
|
|
115
|
+
const listOfAffectedNodes = new Set();
|
|
116
|
+
const walkNodes = currentNode => {
|
|
117
|
+
listOfAffectedNodes.add(currentNode);
|
|
118
|
+
return graph.forEach(edge => {
|
|
119
|
+
if (edge[0] === currentNode && !listOfAffectedNodes.has(edge[1])) {
|
|
120
|
+
walkNodes(edge[1]);
|
|
121
|
+
}
|
|
122
|
+
});
|
|
137
123
|
};
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
context
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
124
|
+
walkNodes(affectedNode);
|
|
125
|
+
return listOfAffectedNodes;
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
// @description get plugins with the specified function
|
|
129
|
+
const getPlugins = (plugins, functionName) => {
|
|
130
|
+
return plugins.filter(plugin => typeof plugin[functionName] === "function");
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
// create a new context from the provided configuration
|
|
134
|
+
const createContext = config => {
|
|
135
|
+
const {source, destination, plugins, ...otherConfiguration} = config;
|
|
136
|
+
const context = Object.freeze({
|
|
137
|
+
config: otherConfiguration,
|
|
138
|
+
source: path.resolve(source || "."),
|
|
139
|
+
destination: path.resolve(destination || "./www"),
|
|
140
|
+
plugins: plugins || [],
|
|
141
|
+
nodes: [],
|
|
142
|
+
edges: [],
|
|
143
|
+
});
|
|
144
|
+
// load nodes into context
|
|
145
|
+
const nodesPaths = new Set(); // prevent adding duplicated nodes
|
|
146
|
+
getPlugins(context.plugins, "load").forEach(plugin => {
|
|
147
|
+
const nodes = plugin.load(context) || [];
|
|
148
|
+
[nodes].flat().forEach(node => {
|
|
149
|
+
const nodeFullPath = path.join(node.source, node.path);
|
|
150
|
+
if (nodesPaths.has(nodeFullPath)) {
|
|
151
|
+
throw new Error(`File ${nodeFullPath} has been already processed by another plugin`);
|
|
152
|
+
}
|
|
153
|
+
context.nodes.push(node);
|
|
154
|
+
nodesPaths.add(nodeFullPath);
|
|
159
155
|
});
|
|
160
|
-
dispatch("emitPage", [page, content]);
|
|
161
|
-
saveFile(path.join(context.destination, page.url), content);
|
|
162
156
|
});
|
|
163
|
-
//
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
157
|
+
// generate dependency graph
|
|
158
|
+
const edgesPaths = new Set(); // prevent adding duplicated edges
|
|
159
|
+
getPlugins(context.plugins, "getDependencyGraph").forEach(plugin => {
|
|
160
|
+
(plugin.getDependencyGraph(context) || []).forEach(edge => {
|
|
161
|
+
if (!edge.every(node => nodesPaths.has(node))) {
|
|
162
|
+
throw new Error(`Dependency graph contains nodes that have not been loaded`);
|
|
163
|
+
}
|
|
164
|
+
const edgePath = edge.join(" -> ");
|
|
165
|
+
if (!edgesPaths.has(edgePath)) {
|
|
166
|
+
context.edges.push(edge);
|
|
167
|
+
edgesPaths.add(edgePath);
|
|
168
|
+
}
|
|
169
|
+
});
|
|
167
170
|
});
|
|
168
|
-
|
|
171
|
+
// return context
|
|
172
|
+
return context;
|
|
169
173
|
};
|
|
170
174
|
|
|
171
|
-
//
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
175
|
+
// @description build context
|
|
176
|
+
const buildContext = (context, nodes = null) => {
|
|
177
|
+
const nodesToBuild = (nodes && Array.isArray(nodes)) ? nodes : context.nodes;
|
|
178
|
+
// reset nodes path
|
|
179
|
+
nodesToBuild.forEach(node => {
|
|
180
|
+
node.data.path = node.path;
|
|
181
|
+
});
|
|
182
|
+
// transform nodes
|
|
183
|
+
const transformPlugins = getPlugins(context.plugins, "transform");
|
|
184
|
+
nodesToBuild.forEach((node, _, allNodes) => {
|
|
185
|
+
transformPlugins.forEach(plugin => {
|
|
186
|
+
return plugin.transform(context, node, allNodes);
|
|
178
187
|
});
|
|
179
|
-
};
|
|
188
|
+
});
|
|
189
|
+
// filter nodes and get only the ones that are going to be emitted
|
|
190
|
+
const shouldEmitPlugins = getPlugins(context.plugins, "shouldEmit");
|
|
191
|
+
const filteredNodes = nodesToBuild.filter((node, _, allNodes) => {
|
|
192
|
+
for (let i = 0; i < shouldEmitPlugins.length; i++) {
|
|
193
|
+
const plugin = shouldEmitPlugins[i];
|
|
194
|
+
if (!plugin.shouldEmit(context, node, allNodes)) {
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return true;
|
|
199
|
+
});
|
|
200
|
+
// emit each node
|
|
201
|
+
getPlugins(context.plugins, "emit").forEach(plugin => {
|
|
202
|
+
return plugin.emit(context, filteredNodes);
|
|
203
|
+
});
|
|
180
204
|
};
|
|
181
205
|
|
|
182
|
-
//
|
|
183
|
-
const
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
const
|
|
187
|
-
context.
|
|
188
|
-
|
|
189
|
-
log("info", `destination directory: ${context.destination}`);
|
|
190
|
-
});
|
|
191
|
-
context.hooks.emitPage.add(page => {
|
|
192
|
-
log("info", `saving page: ${page.url} --> ${path.join(context.destination, page.url)}`);
|
|
193
|
-
});
|
|
194
|
-
context.hooks.emitAsset.add(asset => {
|
|
195
|
-
log("info", `saving asset: ${asset.url} --> ${path.join(context.destination, asset.url)}`);
|
|
206
|
+
// @description start a watch on the current context
|
|
207
|
+
const watchContext = context => {
|
|
208
|
+
// force to rebuild
|
|
209
|
+
const rebuild = changedNodePath => {
|
|
210
|
+
const nodesPathsToBuild = getNodesToUpdate(context.edges, changedNodePath);
|
|
211
|
+
const nodesToRebuild = context.nodes.filter(node => {
|
|
212
|
+
return nodesPathsToBuild.has(path.join(node.source, node.path));
|
|
196
213
|
});
|
|
197
|
-
|
|
198
|
-
|
|
214
|
+
// perform the rebuild of the context
|
|
215
|
+
buildContext(context, nodesToRebuild);
|
|
216
|
+
};
|
|
217
|
+
// create a watch for each registered node in the context
|
|
218
|
+
context.nodes.forEach(node => {
|
|
219
|
+
return utils.watch(path.join(node.source, node.path), rebuild);
|
|
220
|
+
});
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
// @description start a server on the current context
|
|
224
|
+
// @param {Object} context current site context
|
|
225
|
+
// @param {Object} options server options
|
|
226
|
+
// @param {String} options.port port that the server will listen. Default: "3000"
|
|
227
|
+
// @param {Function} options.getMimeType function to obtain the associated mime type from the given extension
|
|
228
|
+
const serveContext = (context, options = {}) => {
|
|
229
|
+
const port = parseInt(options?.port || "3000");
|
|
230
|
+
const getMimeType = options?.getMimeType || utils.getMimeType;
|
|
231
|
+
const server = http.createServer((request, response) => {
|
|
232
|
+
let responseCode = 200;
|
|
233
|
+
let url = path.join(context.destination, path.normalize(request.url));
|
|
234
|
+
// check for directory
|
|
235
|
+
if (url.endsWith("/") || (fs.existsSync(url) && fs.statSync(url).isDirectory())) {
|
|
236
|
+
url = path.join(url, "index.html");
|
|
237
|
+
}
|
|
238
|
+
// check if we have to append the '.html' extension
|
|
239
|
+
if (!fs.existsSync(url) && fs.existsSync(url + ".html")) {
|
|
240
|
+
url = url + ".html";
|
|
241
|
+
}
|
|
242
|
+
// check if the file does not exist
|
|
243
|
+
if (!fs.existsSync(url)) {
|
|
244
|
+
url = path.join(context.destination, "404.html");
|
|
245
|
+
responseCode = 404;
|
|
246
|
+
}
|
|
247
|
+
// send the file
|
|
248
|
+
response.writeHead(responseCode, {
|
|
249
|
+
"Content-Type": getMimeType?.(path.extname(url)) || "text/plain",
|
|
199
250
|
});
|
|
251
|
+
fs.createReadStream(url).pipe(response);
|
|
252
|
+
console.log(`[${responseCode}] ${request.method} ${request.url}`);
|
|
253
|
+
});
|
|
254
|
+
// launch server
|
|
255
|
+
server.listen(port);
|
|
256
|
+
console.log(`Server running at http://127.0.0.1:${port}/`);
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
// @description source plugin
|
|
260
|
+
const SourcePlugin = (options = {}) => {
|
|
261
|
+
const label = options.label || "pages";
|
|
262
|
+
return {
|
|
263
|
+
name: "SourcePlugin",
|
|
264
|
+
load: context => {
|
|
265
|
+
const folder = path.resolve(context.source, options.source || "./content");
|
|
266
|
+
const nodes = utils.walkdir(folder, options?.extensions || "*").map(file => {
|
|
267
|
+
return createNode(folder, file, label);
|
|
268
|
+
});
|
|
269
|
+
return nodes;
|
|
270
|
+
},
|
|
271
|
+
transform: (_, node) => {
|
|
272
|
+
if (node.label === label) {
|
|
273
|
+
node.data.content = utils.read(path.join(node.source, node.path));
|
|
274
|
+
}
|
|
275
|
+
},
|
|
276
|
+
};
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
// @description data plugin
|
|
280
|
+
const DataPlugin = (options = {}) => {
|
|
281
|
+
const label = options?.label || "asset/data";
|
|
282
|
+
return {
|
|
283
|
+
name: "DataPlugin",
|
|
284
|
+
load: context => {
|
|
285
|
+
const folder = path.resolve(context.source, options.source || "./data");
|
|
286
|
+
return utils.readdir(folder, [".json"]).map(file => {
|
|
287
|
+
return createNode(folder, file, label);
|
|
288
|
+
});
|
|
289
|
+
},
|
|
290
|
+
transform: (_, node) => {
|
|
291
|
+
if (node.label === label && path.extname(node.path) === ".json") {
|
|
292
|
+
node.data.name = path.basename(node.path, ".json");
|
|
293
|
+
node.data.content = JSON.parse(utils.read(path.join(node.source, node.path)));
|
|
294
|
+
}
|
|
295
|
+
},
|
|
296
|
+
shouldEmit: (_, node) => {
|
|
297
|
+
return node.label !== label;
|
|
298
|
+
},
|
|
299
|
+
};
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
// @description frontmatter plugin
|
|
303
|
+
// @params {Object} options options for this plugin
|
|
304
|
+
// @params {Array} options.extensions extensions to process. Default: [".md", ".markdown", ".html"]
|
|
305
|
+
// @params {Function} options.parser frontmatter parser (JSON.parse, YAML.load)
|
|
306
|
+
const FrontmatterPlugin = (options = {}) => {
|
|
307
|
+
const extensions = options.extensions || [".md", ".markdown", ".html"];
|
|
308
|
+
return {
|
|
309
|
+
name: "FrontmatterPlugin",
|
|
310
|
+
transform: (_, node) => {
|
|
311
|
+
if ((extensions === "*" || extensions.includes(path.extname(node.path))) && typeof node.data.content === "string") {
|
|
312
|
+
node.data.attributes = {};
|
|
313
|
+
const matches = Array.from(node.data.content.matchAll(/^(--- *)/gm))
|
|
314
|
+
if (matches?.length === 2 && matches[0].index === 0) {
|
|
315
|
+
const front = node.data.content.substring(0 + matches[0][1].length, matches[1].index).trim();
|
|
316
|
+
node.data.content = node.data.content.substring(matches[1].index + matches[1][1].length).trim();
|
|
317
|
+
if (typeof options.parser === "function") {
|
|
318
|
+
node.data.attributes = options.parser(front);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
},
|
|
323
|
+
};
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
// @description permalink plugin
|
|
327
|
+
const PermalinkPlugin = () => {
|
|
328
|
+
return {
|
|
329
|
+
name: "PermalinkPlugin",
|
|
330
|
+
transform: (_, node) => {
|
|
331
|
+
node.data.path = node.data?.attributes?.permalink || node.data.path;
|
|
332
|
+
// node.data.url = path.normalize("/" + node.data.path);
|
|
333
|
+
},
|
|
334
|
+
};
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
// @description markdown plugin
|
|
338
|
+
// @params {Object} options options for this plugin
|
|
339
|
+
// @params {Object} options.parser markdown parser (for example marked.parse)
|
|
340
|
+
const MarkdownPlugin = (options = {}) => {
|
|
341
|
+
return {
|
|
342
|
+
name: "MarkdownPlugin",
|
|
343
|
+
transform: (_, node) => {
|
|
344
|
+
if (path.extname(node.path) === ".md" || path.extname(node.path) === ".markdown") {
|
|
345
|
+
// const marked = new Marked(options);
|
|
346
|
+
// getPlugins(context.plugins, "markdownPlugins").forEach(plugin => {
|
|
347
|
+
// (plugin.markdownPlugins(context, node) || []).forEach(markedPlugin => {
|
|
348
|
+
// marked.use(markedPlugin);
|
|
349
|
+
// });
|
|
350
|
+
// });
|
|
351
|
+
node.data.content = options.parser(node.data.content);
|
|
352
|
+
node.data.path = utils.format(node.data.path, {extname: ".html"});
|
|
353
|
+
}
|
|
354
|
+
},
|
|
355
|
+
};
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
// @description content plugin
|
|
359
|
+
const ContentPlugin = (options = {}) => {
|
|
360
|
+
const label = options.label || "asset/layout";
|
|
361
|
+
const extensions = options.extensions || [".html", ".md", ".markdown"];
|
|
362
|
+
return {
|
|
363
|
+
name: "ContentPlugin",
|
|
364
|
+
load: context => {
|
|
365
|
+
const layoutPath = path.resolve(context.source, context.config.layout || options.layout);
|
|
366
|
+
return createNode(path.dirname(layoutPath), path.basename(layoutPath), label);
|
|
367
|
+
},
|
|
368
|
+
transform: (_, node) => {
|
|
369
|
+
if (node.label === label) {
|
|
370
|
+
node.data.content = utils.read(path.join(node.source, node.path));
|
|
371
|
+
}
|
|
372
|
+
},
|
|
373
|
+
getDependencyGraph: context => {
|
|
374
|
+
const graph = [];
|
|
375
|
+
const template = getNodesByLabel(context.nodes, label)[0];
|
|
376
|
+
context.nodes.forEach(node => {
|
|
377
|
+
if (node.label !== label && extensions.includes(path.extname(node.path))) {
|
|
378
|
+
graph.push([
|
|
379
|
+
path.join(template.source, template.path),
|
|
380
|
+
path.join(node.source, node.path),
|
|
381
|
+
]);
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
return graph;
|
|
385
|
+
},
|
|
386
|
+
shouldEmit: (_, node) => {
|
|
387
|
+
return node.label !== label;
|
|
388
|
+
},
|
|
389
|
+
emit: (context, nodesToEmit) => {
|
|
390
|
+
// prepare site data
|
|
391
|
+
const siteData = Object.assign({}, context.config, {
|
|
392
|
+
data: Object.fromEntries(getNodesByLabel(context.nodes, "asset/data").map(node => {
|
|
393
|
+
return [node.data.name, node.data.content];
|
|
394
|
+
})),
|
|
395
|
+
pages: getNodesByLabel(context.nodes, "pages").map(n => n.data),
|
|
396
|
+
posts: getNodesByLabel(context.nodes, "posts").map(n => n.data),
|
|
397
|
+
});
|
|
398
|
+
// get data files
|
|
399
|
+
const template = getNodesByLabel(context.nodes, label)[0];
|
|
400
|
+
const compiler = mikel.create(template.data.content, options);
|
|
401
|
+
nodesToEmit.forEach(node => {
|
|
402
|
+
if (extensions.includes(path.extname(node.path))) {
|
|
403
|
+
compiler.addPartial("content", node.data.content);
|
|
404
|
+
const content = compiler({
|
|
405
|
+
site: siteData,
|
|
406
|
+
page: node.data,
|
|
407
|
+
layout: template.data,
|
|
408
|
+
});
|
|
409
|
+
// const filePath = utils.format(node.data.path || node.path, {extname: ".html"});
|
|
410
|
+
const filePath = node.data?.path || node.path;
|
|
411
|
+
utils.write(path.join(context.destination, filePath), content);
|
|
412
|
+
}
|
|
413
|
+
});
|
|
414
|
+
},
|
|
200
415
|
};
|
|
201
416
|
};
|
|
202
417
|
|
|
203
|
-
//
|
|
204
|
-
|
|
418
|
+
// @description copy plugin
|
|
419
|
+
const CopyAssetsPlugin = (options = {}) => {
|
|
420
|
+
return {
|
|
421
|
+
name: "CopyAssetsPlugin",
|
|
422
|
+
emit: context => {
|
|
423
|
+
(options.patterns || []).forEach(item => {
|
|
424
|
+
if (item.from && item.to && fs.existsSync(item.from)) {
|
|
425
|
+
utils.copy(item.from, path.join(context.destination, item.to));
|
|
426
|
+
}
|
|
427
|
+
});
|
|
428
|
+
},
|
|
429
|
+
};
|
|
430
|
+
};
|
|
431
|
+
|
|
432
|
+
// @description default export of mikel-press
|
|
433
|
+
export default {
|
|
434
|
+
// @description run mikel-press and generate the static site
|
|
435
|
+
// @param {Object} config configuration object
|
|
436
|
+
build: config => {
|
|
437
|
+
buildContext(createContext(config));
|
|
438
|
+
},
|
|
439
|
+
// @description watch for changes in the source folder and rebuild the site
|
|
440
|
+
// @param {Object} config configuration object
|
|
441
|
+
watch: config => {
|
|
442
|
+
const context = createContext(config);
|
|
443
|
+
buildContext(context, context.nodes);
|
|
444
|
+
watchContext(context);
|
|
445
|
+
},
|
|
446
|
+
// utilities for working with files
|
|
447
|
+
utils: utils,
|
|
448
|
+
// helpers for working with the context
|
|
449
|
+
createNode: createNode,
|
|
450
|
+
createContext: createContext,
|
|
451
|
+
buildContext: buildContext,
|
|
452
|
+
watchContext: watchContext,
|
|
453
|
+
serveContext: serveContext,
|
|
454
|
+
// plugins
|
|
455
|
+
SourcePlugin: SourcePlugin,
|
|
456
|
+
DataPlugin: DataPlugin,
|
|
457
|
+
MarkdownPlugin: MarkdownPlugin,
|
|
458
|
+
FrontmatterPlugin: FrontmatterPlugin,
|
|
459
|
+
PermalinkPlugin: PermalinkPlugin,
|
|
460
|
+
ContentPlugin: ContentPlugin,
|
|
461
|
+
CopyAssetsPlugin: CopyAssetsPlugin,
|
|
462
|
+
};
|
package/package.json
CHANGED
|
@@ -1,40 +1,36 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mikel-press",
|
|
3
|
-
"description": "A
|
|
4
|
-
"version": "0.2
|
|
3
|
+
"description": "A tiny and fast static site generator based on mikel templating",
|
|
4
|
+
"version": "0.18.2",
|
|
5
5
|
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
6
7
|
"author": {
|
|
7
8
|
"name": "Josemi Juanes",
|
|
8
9
|
"email": "hello@josemi.xyz"
|
|
9
10
|
},
|
|
10
|
-
"license": "MIT",
|
|
11
11
|
"repository": "https://github.com/jmjuanes/mikel",
|
|
12
12
|
"bugs": "https://github.com/jmjuanes/mikel/issues",
|
|
13
|
-
"main": "index.js",
|
|
14
|
-
"module": "index.js",
|
|
15
|
-
"types": "index.d.ts",
|
|
16
13
|
"exports": {
|
|
17
14
|
".": "./index.js",
|
|
18
15
|
"./index.js": "./index.js",
|
|
19
16
|
"./package.json": "./package.json"
|
|
20
17
|
},
|
|
21
|
-
"
|
|
22
|
-
"
|
|
18
|
+
"engines": {
|
|
19
|
+
"node": ">=20"
|
|
23
20
|
},
|
|
24
|
-
"keywords": [
|
|
25
|
-
"static",
|
|
26
|
-
"site",
|
|
27
|
-
"generator",
|
|
28
|
-
"mikel",
|
|
29
|
-
"template",
|
|
30
|
-
"templating"
|
|
31
|
-
],
|
|
32
21
|
"dependencies": {
|
|
33
|
-
"mikel": "^0.
|
|
22
|
+
"mikel": "^0.18.2"
|
|
34
23
|
},
|
|
35
24
|
"files": [
|
|
36
25
|
"README.md",
|
|
37
|
-
"index.js"
|
|
38
|
-
|
|
26
|
+
"index.js"
|
|
27
|
+
],
|
|
28
|
+
"keywords": [
|
|
29
|
+
"static-site",
|
|
30
|
+
"site-generator",
|
|
31
|
+
"digital-garden",
|
|
32
|
+
"markdown",
|
|
33
|
+
"blog",
|
|
34
|
+
"mikel"
|
|
39
35
|
]
|
|
40
|
-
}
|
|
36
|
+
}
|
package/index.d.ts
DELETED
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
interface MikelTemplateOptions {
|
|
2
|
-
functions: {[key: string]: (args: any) => string};
|
|
3
|
-
helpers: {[key: string]: (args: any) => string};
|
|
4
|
-
partials: {[key: string]: string};
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
interface FrontmatterResult {
|
|
8
|
-
body: string;
|
|
9
|
-
attributes: any;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
interface VirtualPageOptions {
|
|
13
|
-
content?: string;
|
|
14
|
-
file?: string;
|
|
15
|
-
extname?: string;
|
|
16
|
-
basename?: string;
|
|
17
|
-
frontmatter?: (str: string) => FrontmatterResult;
|
|
18
|
-
transform?: (str: string) => string;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
interface VirtualPage {
|
|
22
|
-
content: string;
|
|
23
|
-
attributes: any;
|
|
24
|
-
name: string;
|
|
25
|
-
extname: string;
|
|
26
|
-
basename: string;
|
|
27
|
-
url: string;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
interface PostsPluginOptions {
|
|
31
|
-
dir: string;
|
|
32
|
-
parser: (str: string) => string;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
interface SiteConfig {
|
|
36
|
-
source: string;
|
|
37
|
-
destination: string;
|
|
38
|
-
layout: string;
|
|
39
|
-
layoutContent: string;
|
|
40
|
-
dataDir: string;
|
|
41
|
-
pagesDir: string;
|
|
42
|
-
assetsDir: string;
|
|
43
|
-
frontmatter: (str: string) => FrontmatterResult;
|
|
44
|
-
mikel: Partial<MikelTemplateOptions>;
|
|
45
|
-
plugins: any[];
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
declare module "mikel-press" {
|
|
49
|
-
export function frontmatter(str: string): FrontmatterResult;
|
|
50
|
-
export function createVirtualPage(options: VirtualPageOptions): VirtualPage;
|
|
51
|
-
export function run(config: Partial<SiteConfig>): void;
|
|
52
|
-
|
|
53
|
-
export function postsPlugin(options: PostsPluginOptions): any;
|
|
54
|
-
export function progressPlugin(): any;
|
|
55
|
-
}
|