fast-ejs-builder 1.0.0 → 1.0.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 +95 -24
- package/core/index.js +6 -0
- package/core/lib.js +53 -15
- package/fast.ejs.json +4 -3
- package/fast.ejs.schema.json +9 -5
- package/package.json +1 -1
- package/src/build-dev.js +3 -1
- package/src/build.js +69 -9
- package/src/tailwind.js +40 -1
package/README.md
CHANGED
|
@@ -44,10 +44,9 @@ npm install fast-ejs-builder --save-dev
|
|
|
44
44
|
|
|
45
45
|
```txt
|
|
46
46
|
my-site/
|
|
47
|
-
├──
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
│ └── pages/ # EJS templates
|
|
47
|
+
├── components/ # Reusable EJS components
|
|
48
|
+
├── data/ # Global and local data files
|
|
49
|
+
└── pages/ # EJS templates
|
|
51
50
|
│ └── public/ # Static assets
|
|
52
51
|
└── fast.ejs.json # Configuration file
|
|
53
52
|
```
|
|
@@ -62,14 +61,15 @@ npm install fast-ejs-builder --save-dev
|
|
|
62
61
|
"useIndexRouting": true
|
|
63
62
|
},
|
|
64
63
|
"components": {
|
|
65
|
-
"dir": "
|
|
64
|
+
"dir": "components",
|
|
65
|
+
"autoGenerate": false
|
|
66
66
|
},
|
|
67
67
|
"data": {
|
|
68
|
-
"dir": "
|
|
68
|
+
"dir": "data",
|
|
69
69
|
"allow": "all"
|
|
70
70
|
},
|
|
71
71
|
"pages": {
|
|
72
|
-
"dir": "
|
|
72
|
+
"dir": "pages"
|
|
73
73
|
},
|
|
74
74
|
"tailwind": {
|
|
75
75
|
"output": "public/app.css",
|
|
@@ -128,18 +128,20 @@ The `fast.ejs.json` (occasionnally called _the FEJ_) file controls all aspects o
|
|
|
128
128
|
"useIndexRouting": true
|
|
129
129
|
},
|
|
130
130
|
"components": {
|
|
131
|
-
"dir": "
|
|
131
|
+
"dir": "components",
|
|
132
|
+
"autoGenerate": false
|
|
132
133
|
},
|
|
133
134
|
"data": {
|
|
134
|
-
"dir": "
|
|
135
|
+
"dir": "data",
|
|
135
136
|
"allow": "all"
|
|
136
137
|
},
|
|
137
138
|
"pages": {
|
|
138
|
-
"dir": "
|
|
139
|
+
"dir": "pages"
|
|
139
140
|
},
|
|
140
141
|
"tailwind": {
|
|
141
142
|
"output": "public/app.css",
|
|
142
143
|
"imports": [
|
|
144
|
+
"public/style.css",
|
|
143
145
|
"https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap"
|
|
144
146
|
]
|
|
145
147
|
}
|
|
@@ -152,11 +154,12 @@ The `fast.ejs.json` (occasionnally called _the FEJ_) file controls all aspects o
|
|
|
152
154
|
- **`build.interval`**: Milliseconds between rebuilds in development mode
|
|
153
155
|
- **`build.useIndexRouting`**: Generate `route/index.html` instead of `route.html`
|
|
154
156
|
- **`components.dir`**: Directory containing reusable EJS components
|
|
157
|
+
- **`components.autoGenerate`**: Generate missing components. Default is `false`
|
|
155
158
|
- **`data.dir`**: Directory for global and page-specific data files
|
|
156
159
|
- **`data.allow`**: Data file format (`"js"`, `"json"`, or `"all"`)
|
|
157
160
|
- **`pages.dir`**: Directory containing your EJS page templates. **Here is where you should mainly work**.
|
|
158
161
|
- **`tailwind.output`**: Path to generated Tailwind CSS file
|
|
159
|
-
- **`tailwind.imports`**: Array of external CSS URLs to include
|
|
162
|
+
- **`tailwind.imports`**: Array of external CSS URLs to include. Each `@layer` will be detected if specified.
|
|
160
163
|
|
|
161
164
|
## Usage Examples
|
|
162
165
|
|
|
@@ -165,7 +168,7 @@ The `fast.ejs.json` (occasionnally called _the FEJ_) file controls all aspects o
|
|
|
165
168
|
Create EJS templates in your `pages.dir` directory:
|
|
166
169
|
|
|
167
170
|
```ejs
|
|
168
|
-
<!--
|
|
171
|
+
<!-- pages/index.ejs -->
|
|
169
172
|
<!DOCTYPE html>
|
|
170
173
|
<html lang="en">
|
|
171
174
|
<head>
|
|
@@ -193,7 +196,7 @@ Create EJS templates in your `pages.dir` directory:
|
|
|
193
196
|
Create reusable components in your `components.dir`:
|
|
194
197
|
|
|
195
198
|
```ejs
|
|
196
|
-
<!--
|
|
199
|
+
<!-- components/header.ejs -->
|
|
197
200
|
<header class="bg-white shadow-sm">
|
|
198
201
|
<nav class="container mx-auto px-4 py-4">
|
|
199
202
|
<div class="flex items-center justify-between">
|
|
@@ -243,7 +246,7 @@ Fast EJS comes with default data that can't be overrided.
|
|
|
243
246
|
In the previous example, we can access `"https://placehold.co/400"` by using `$0` (`0` for the first arg).
|
|
244
247
|
|
|
245
248
|
```ejs
|
|
246
|
-
<!--
|
|
249
|
+
<!--components/users/avatar.ejs-->
|
|
247
250
|
<img src="<%= $0 %>" class="w-10 aspect-square rounded-full"/>
|
|
248
251
|
```
|
|
249
252
|
|
|
@@ -258,15 +261,15 @@ Fast EJS comes with default data that can't be overrided.
|
|
|
258
261
|
Returns the current route relative to the `pages.dir`. In this example, it will return `/home`
|
|
259
262
|
|
|
260
263
|
```ejs
|
|
261
|
-
<!--
|
|
264
|
+
<!--pages/home.ejs-->
|
|
262
265
|
<%= $route %>
|
|
263
266
|
```
|
|
264
267
|
|
|
265
268
|
- `$css` :\
|
|
266
269
|
Automatically imports the relative path of generated tailwind css from `tailwind.output` inside a page. No need to manually write the css path and change everytime.\
|
|
267
270
|
\
|
|
268
|
-
For example, inside `
|
|
269
|
-
while inside `
|
|
271
|
+
For example, inside `pages/users/profile.ejs`, it can return something like `../../public/app.css`\
|
|
272
|
+
while inside `pages/index.ejs`, it will return something like `./public/app.css`
|
|
270
273
|
|
|
271
274
|
```ejs
|
|
272
275
|
<%- $css %>
|
|
@@ -306,7 +309,7 @@ Fast EJS comes with default data that can't be overrided.
|
|
|
306
309
|
Prints anything in the console during build. Use it to debug your pages or components
|
|
307
310
|
|
|
308
311
|
```ejs
|
|
309
|
-
<!--
|
|
312
|
+
<!--components/header.ejs-->
|
|
310
313
|
<%- $debug("Header component says hi !") %>
|
|
311
314
|
```
|
|
312
315
|
|
|
@@ -319,14 +322,15 @@ Fast EJS comes with default data that can't be overrided.
|
|
|
319
322
|
|
|
320
323
|
#### 2. Using your own data
|
|
321
324
|
|
|
322
|
-
Fill data files in `data.dir` (generated automatically if missing)
|
|
325
|
+
Fill data files in `data.dir` (generated automatically if missing).\
|
|
326
|
+
Remember that using js data allow you to use function, async function and getter as data.
|
|
323
327
|
|
|
324
328
|
- **Global data** : Can be accessed inside every pages and components
|
|
325
329
|
|
|
326
330
|
If `data.allow` is `all` or `js` (recommended)
|
|
327
331
|
|
|
328
332
|
```javascript
|
|
329
|
-
//
|
|
333
|
+
// data/global.js
|
|
330
334
|
module.exports = {
|
|
331
335
|
siteName: "My Awesome Site",
|
|
332
336
|
navItems: [
|
|
@@ -336,13 +340,16 @@ module.exports = {
|
|
|
336
340
|
],
|
|
337
341
|
add: (a, b) => a + b,
|
|
338
342
|
getUsers: async () => await db.getUsers(),
|
|
343
|
+
get randomBool() {
|
|
344
|
+
return Math.random() > 0.5;
|
|
345
|
+
},
|
|
339
346
|
};
|
|
340
347
|
```
|
|
341
348
|
|
|
342
349
|
If `data.allow` is `all` or `json`
|
|
343
350
|
|
|
344
351
|
```js
|
|
345
|
-
//
|
|
352
|
+
// data/global.json
|
|
346
353
|
{
|
|
347
354
|
"siteName": "My Awesome Site",
|
|
348
355
|
"navItems": [
|
|
@@ -356,18 +363,79 @@ If `data.allow` is `all` or `json`
|
|
|
356
363
|
- **Local data** : Can be accessed only in the target page
|
|
357
364
|
|
|
358
365
|
```javascript
|
|
359
|
-
//
|
|
366
|
+
// data/local.js
|
|
360
367
|
module.exports = {
|
|
361
|
-
// for page "
|
|
368
|
+
// for page "pages/users/profile.ejs"
|
|
362
369
|
"users/profile": {
|
|
363
370
|
title: "Welcome to My Site",
|
|
364
371
|
description: "This is a fast-ejs powered website with Tailwind CSS.",
|
|
365
372
|
},
|
|
373
|
+
|
|
374
|
+
// dynamic routes
|
|
375
|
+
"user/$id": (params) => ({
|
|
376
|
+
id: params.id,
|
|
377
|
+
username: params.name,
|
|
378
|
+
}),
|
|
379
|
+
"blog/$id": ({ title, id }) => ({
|
|
380
|
+
id,
|
|
381
|
+
blogTitle: title,
|
|
382
|
+
}),
|
|
383
|
+
};
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
- **Route params** : Params sent to dynamic routes. Should return an array of object
|
|
387
|
+
|
|
388
|
+
```javascript
|
|
389
|
+
// data/route.js
|
|
390
|
+
module.exports = {
|
|
391
|
+
"user/$id": [{ id: "u1", name: "John Doe" }],
|
|
392
|
+
"blog/$id": () => {
|
|
393
|
+
const arr = [];
|
|
394
|
+
for (let i = 0; i < 5; i++) {
|
|
395
|
+
arr.push({ title: "Article " + i, id: i, isPremium: i % 2 == 0 });
|
|
396
|
+
}
|
|
397
|
+
return arr;
|
|
398
|
+
},
|
|
399
|
+
// or getter
|
|
400
|
+
get "blog/$id"() {
|
|
401
|
+
// ...
|
|
402
|
+
},
|
|
366
403
|
};
|
|
367
404
|
```
|
|
368
405
|
|
|
369
406
|
**Note that all JS data can be asynchronous, and each asynchronous data will affect the build time.**
|
|
370
407
|
|
|
408
|
+
## Use cases
|
|
409
|
+
|
|
410
|
+
You can use fast-ejs to build any static website. Just keep in mind that all pages are pre-built, not in runtime like front-end frameworks.\
|
|
411
|
+
For example, if you want to create a blog, you have to use dynamic routes (ex: `articles/$id.ejs`) in order to generate each article page at once:
|
|
412
|
+
|
|
413
|
+
```js
|
|
414
|
+
// - Create "articles/$id.ejs" with the logic
|
|
415
|
+
// - Inside "data/route.js", add all articles with their data
|
|
416
|
+
module.exports = {
|
|
417
|
+
// ...,
|
|
418
|
+
"articles/$id": async () => {
|
|
419
|
+
const list = await db.getArticles();
|
|
420
|
+
return list.map((a) => ({ ...a, id: a._id })); // ensure "id" key
|
|
421
|
+
},
|
|
422
|
+
};
|
|
423
|
+
|
|
424
|
+
// - Add your view data inside "data/local.js"
|
|
425
|
+
module.exports = {
|
|
426
|
+
// ...,
|
|
427
|
+
"articles/$id": (article /*represents the data from route.js*/) => {
|
|
428
|
+
const { name, date } = article;
|
|
429
|
+
return { name, date }; // if only name and date are used in the view
|
|
430
|
+
},
|
|
431
|
+
};
|
|
432
|
+
|
|
433
|
+
/** Then, when the build is done, route.js will generate files based on your list. And each files built with the corresponding data.
|
|
434
|
+
* /articles/1 (retrieve data "articles/$id" but with the article with id=1 )
|
|
435
|
+
* etc..
|
|
436
|
+
*/
|
|
437
|
+
```
|
|
438
|
+
|
|
371
439
|
## Commands
|
|
372
440
|
|
|
373
441
|
- **`fast-ejs dev`**: Start development server with live reloading
|
|
@@ -380,7 +448,10 @@ Here is the real building flow :
|
|
|
380
448
|
- Get the data inside `data.dir` that match the `data.allow`.
|
|
381
449
|
- Scan the specified `pages.dir`.
|
|
382
450
|
- All empty folder will be ignored
|
|
383
|
-
- If any `.ejs` file is found,
|
|
451
|
+
- If any `.ejs` file is found, search for used component inside `components.dir`.
|
|
452
|
+
- If a component is missing, it will be generated if `components.autoGenerate` is `true`
|
|
453
|
+
- If the file it is dynamic file (ex: $id.ejs), search for corresponding routes inside `data.dir/route.js(json)`
|
|
454
|
+
- Render the html with the corresponding data and generate the right file inside `build.output` depending on `build.useIndexRouting`.
|
|
384
455
|
- For any other file, just copy it inside the `build.output`.
|
|
385
456
|
- Generate the css with tailwind at `tailwind.output` along with `tailwind.imports` if specified.
|
|
386
457
|
- Scan the `build.output` and clean all junk files and folders (files from previous build and empty folders).
|
package/core/index.js
CHANGED
|
@@ -72,6 +72,7 @@ const _r = (pathname, json = true) => {
|
|
|
72
72
|
const data = fs.readFileSync(_p(pathname), { encoding: "utf8" });
|
|
73
73
|
return json ? JSON.parse(data) : data;
|
|
74
74
|
}
|
|
75
|
+
return null;
|
|
75
76
|
};
|
|
76
77
|
const _d = (...messages) => {
|
|
77
78
|
let obj = "";
|
|
@@ -89,9 +90,13 @@ const _cp = (i, o) => fs.copyFileSync(_p(i), _p(o));
|
|
|
89
90
|
|
|
90
91
|
const _has = (obj, k) => Object.hasOwn(obj, k);
|
|
91
92
|
|
|
93
|
+
const _v = async (v, ...args) =>
|
|
94
|
+
typeof v === "function" ? await v(...args) : v;
|
|
95
|
+
|
|
92
96
|
module.exports = {
|
|
93
97
|
_ce,
|
|
94
98
|
_cp,
|
|
99
|
+
|
|
95
100
|
_d,
|
|
96
101
|
_ds,
|
|
97
102
|
_e,
|
|
@@ -104,6 +109,7 @@ module.exports = {
|
|
|
104
109
|
_rd,
|
|
105
110
|
_tree,
|
|
106
111
|
_name,
|
|
112
|
+
_v,
|
|
107
113
|
_w,
|
|
108
114
|
args,
|
|
109
115
|
root,
|
package/core/lib.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const { args, _r, _has, _ce, _d, _w, _md, _e, _p, _ds } = require(".");
|
|
1
|
+
const { args, _r, _has, _ce, _d, _w, _md, _e, _p, _ds, _v } = require(".");
|
|
2
2
|
const { $confirm, $input, $select, $number } = require("./prompter");
|
|
3
3
|
const default_fej = require("../fast.ejs.json");
|
|
4
4
|
const config = {
|
|
@@ -14,6 +14,7 @@ function getConfigFile() {
|
|
|
14
14
|
if (v) return v;
|
|
15
15
|
}
|
|
16
16
|
}
|
|
17
|
+
return "fast.ejs.json";
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
function smartMerge(target = {}, source = {}) {
|
|
@@ -45,7 +46,7 @@ function smartMerge(target = {}, source = {}) {
|
|
|
45
46
|
}
|
|
46
47
|
|
|
47
48
|
async function getConfig() {
|
|
48
|
-
const file = getConfigFile()
|
|
49
|
+
const file = getConfigFile();
|
|
49
50
|
|
|
50
51
|
if (!_ce(file, "Config file")) {
|
|
51
52
|
const auto = await $confirm("Do you want to use default settings ?", true);
|
|
@@ -62,6 +63,10 @@ async function getConfig() {
|
|
|
62
63
|
"In which folder are your components ?",
|
|
63
64
|
config.components.dir,
|
|
64
65
|
);
|
|
66
|
+
config.components.autoGenerate = await $input(
|
|
67
|
+
"Auto generate missing components ?",
|
|
68
|
+
config.components.autoGenerate,
|
|
69
|
+
);
|
|
65
70
|
|
|
66
71
|
config.build.output = await $input(
|
|
67
72
|
"Where do you want to output ?",
|
|
@@ -116,8 +121,9 @@ function generateBaseDirs() {
|
|
|
116
121
|
}
|
|
117
122
|
|
|
118
123
|
async function getDatas() {
|
|
119
|
-
const globalDataPath = (type) => `${config.data.dir}/global
|
|
120
|
-
const localDataPath = (type) => `${config.data.dir}/local
|
|
124
|
+
const globalDataPath = (type) => `${config.data.dir}/global.${type}`;
|
|
125
|
+
const localDataPath = (type) => `${config.data.dir}/local.${type}`;
|
|
126
|
+
const routeDataPath = (type) => `${config.data.dir}/route.${type}`;
|
|
121
127
|
|
|
122
128
|
const globalJs = globalDataPath("js");
|
|
123
129
|
const globalJson = globalDataPath("json");
|
|
@@ -125,6 +131,9 @@ async function getDatas() {
|
|
|
125
131
|
const localJs = localDataPath("js");
|
|
126
132
|
const localJson = localDataPath("json");
|
|
127
133
|
|
|
134
|
+
const routeJs = routeDataPath("js");
|
|
135
|
+
const routeJson = routeDataPath("json");
|
|
136
|
+
|
|
128
137
|
const templates = {
|
|
129
138
|
globalJS: `
|
|
130
139
|
module.exports = async () => ({
|
|
@@ -138,9 +147,24 @@ module.exports = () => ({
|
|
|
138
147
|
index: {
|
|
139
148
|
title: process.env.APP_NAME,
|
|
140
149
|
},
|
|
141
|
-
about: {}, // about.ejs,
|
|
142
150
|
"contact/menu": {}, // contact/menu.ejs
|
|
151
|
+
|
|
152
|
+
"blog/$id": (params) => ({
|
|
153
|
+
blogId: params.id,
|
|
154
|
+
}), // blog/$id.ejs
|
|
155
|
+
|
|
156
|
+
"article/$name": ({ name }) => ({
|
|
157
|
+
name,
|
|
158
|
+
}), // blog/$id.ejs
|
|
143
159
|
});
|
|
160
|
+
`,
|
|
161
|
+
routeJS: `
|
|
162
|
+
module.exports = {
|
|
163
|
+
"blog/$id": [
|
|
164
|
+
{ id: 123, title: "Article 1" },
|
|
165
|
+
{ id: 456, title: "Article 2", isPremium: true },
|
|
166
|
+
],
|
|
167
|
+
};
|
|
144
168
|
`,
|
|
145
169
|
};
|
|
146
170
|
|
|
@@ -150,14 +174,13 @@ module.exports = () => ({
|
|
|
150
174
|
const resolved = require.resolve(_p(path));
|
|
151
175
|
delete require.cache[resolved];
|
|
152
176
|
const v = require(resolved);
|
|
153
|
-
return
|
|
177
|
+
return await _v(v, default_data_args);
|
|
154
178
|
};
|
|
155
179
|
|
|
156
180
|
const getGlobalJSData = async () => {
|
|
157
181
|
if (_e(globalJs)) {
|
|
158
182
|
return await parseJSDate(globalJs);
|
|
159
183
|
} else {
|
|
160
|
-
_d(`Global file '${globalJs}' not found. It will be generated`);
|
|
161
184
|
_w(globalJs, templates.globalJS.trim());
|
|
162
185
|
return {};
|
|
163
186
|
}
|
|
@@ -167,18 +190,24 @@ module.exports = () => ({
|
|
|
167
190
|
if (_e(localJs)) {
|
|
168
191
|
return await parseJSDate(localJs);
|
|
169
192
|
} else {
|
|
170
|
-
_d(`Local file '${localJs}' not found. It will be generated`);
|
|
171
193
|
_w(localJs, templates.localJS.trim());
|
|
172
194
|
return {};
|
|
173
195
|
}
|
|
174
196
|
};
|
|
175
197
|
|
|
198
|
+
const getRouteJSData = async () => {
|
|
199
|
+
if (_e(routeJs)) {
|
|
200
|
+
return await parseJSDate(routeJs);
|
|
201
|
+
} else {
|
|
202
|
+
_w(routeJs, templates.routeJS.trim());
|
|
203
|
+
return {};
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
|
|
176
207
|
const getGlobalJSONData = () => {
|
|
177
208
|
if (_e(globalJson)) {
|
|
178
209
|
return _r(globalJson);
|
|
179
210
|
} else {
|
|
180
|
-
_d(`Global file '${globalJson}' not found. It will be generated`);
|
|
181
|
-
_w(globalJson, {});
|
|
182
211
|
return {};
|
|
183
212
|
}
|
|
184
213
|
};
|
|
@@ -187,31 +216,40 @@ module.exports = () => ({
|
|
|
187
216
|
if (_e(localJson)) {
|
|
188
217
|
return _r(localJson);
|
|
189
218
|
} else {
|
|
190
|
-
_d(`Local file '${localJson}' not found. It will be generated`);
|
|
191
|
-
_w(localJson, { index: { title: "HomePage" }, about: {} });
|
|
192
219
|
return {};
|
|
193
220
|
}
|
|
194
221
|
};
|
|
195
222
|
|
|
196
|
-
|
|
197
|
-
|
|
223
|
+
const getRouteJSONData = () => {
|
|
224
|
+
if (_e(routeJson)) {
|
|
225
|
+
return _r(routeJson);
|
|
226
|
+
} else {
|
|
227
|
+
return {};
|
|
228
|
+
}
|
|
229
|
+
};
|
|
198
230
|
|
|
231
|
+
let globalData = {},
|
|
232
|
+
localData = {},
|
|
233
|
+
routeData = {};
|
|
199
234
|
switch (config.data.allow) {
|
|
200
235
|
case "js":
|
|
201
236
|
globalData = await getGlobalJSData();
|
|
202
237
|
localData = await getLocalJSData();
|
|
238
|
+
routeData = await getRouteJSData();
|
|
203
239
|
break;
|
|
204
240
|
case "json":
|
|
205
241
|
globalData = getGlobalJSONData();
|
|
206
242
|
localData = getLocalJSONData();
|
|
243
|
+
routeData = getRouteJSONData();
|
|
207
244
|
break;
|
|
208
245
|
default:
|
|
209
246
|
globalData = { ...getGlobalJSONData(), ...(await getGlobalJSData()) };
|
|
210
247
|
localData = { ...getLocalJSONData(), ...(await getLocalJSData()) };
|
|
248
|
+
routeData = { ...getRouteJSONData(), ...(await getRouteJSData()) };
|
|
211
249
|
break;
|
|
212
250
|
}
|
|
213
251
|
|
|
214
|
-
return { globalData, localData };
|
|
252
|
+
return { globalData, localData, routeData };
|
|
215
253
|
}
|
|
216
254
|
|
|
217
255
|
const configTailwindOutput = () => {
|
package/fast.ejs.json
CHANGED
|
@@ -6,14 +6,15 @@
|
|
|
6
6
|
"useIndexRouting": true
|
|
7
7
|
},
|
|
8
8
|
"components": {
|
|
9
|
-
"dir": "
|
|
9
|
+
"dir": "components",
|
|
10
|
+
"autoGenerate": false
|
|
10
11
|
},
|
|
11
12
|
"data": {
|
|
12
13
|
"allow": "all",
|
|
13
|
-
"dir": "
|
|
14
|
+
"dir": "data"
|
|
14
15
|
},
|
|
15
16
|
"pages": {
|
|
16
|
-
"dir": "
|
|
17
|
+
"dir": "pages"
|
|
17
18
|
},
|
|
18
19
|
"tailwind": {
|
|
19
20
|
"imports": [],
|
package/fast.ejs.schema.json
CHANGED
|
@@ -31,9 +31,14 @@
|
|
|
31
31
|
"components": {
|
|
32
32
|
"properties": {
|
|
33
33
|
"dir": {
|
|
34
|
-
"default": "
|
|
34
|
+
"default": "components",
|
|
35
35
|
"description": "The folder containing your EJS components",
|
|
36
36
|
"type": "string"
|
|
37
|
+
},
|
|
38
|
+
"autoGenerate": {
|
|
39
|
+
"default": false,
|
|
40
|
+
"description": "Automatically generate missing components when they are referenced",
|
|
41
|
+
"type": "boolean"
|
|
37
42
|
}
|
|
38
43
|
},
|
|
39
44
|
"required": ["dir"],
|
|
@@ -42,8 +47,8 @@
|
|
|
42
47
|
"data": {
|
|
43
48
|
"properties": {
|
|
44
49
|
"dir": {
|
|
45
|
-
"default": "
|
|
46
|
-
"description": "The folder containing global and local data files (global.
|
|
50
|
+
"default": "data",
|
|
51
|
+
"description": "The folder containing global and local data files (global.js/json, local.js/json, route.js/json)",
|
|
47
52
|
"type": "string"
|
|
48
53
|
},
|
|
49
54
|
"allow": {
|
|
@@ -56,11 +61,10 @@
|
|
|
56
61
|
"required": ["dir"],
|
|
57
62
|
"additionalProperties": false
|
|
58
63
|
},
|
|
59
|
-
|
|
60
64
|
"pages": {
|
|
61
65
|
"properties": {
|
|
62
66
|
"dir": {
|
|
63
|
-
"default": "
|
|
67
|
+
"default": "pages",
|
|
64
68
|
"description": "The folder containing your EJS templates",
|
|
65
69
|
"type": "string"
|
|
66
70
|
}
|
package/package.json
CHANGED
package/src/build-dev.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const { args, _d, _p } = require("../core");
|
|
2
2
|
const ejsbuild = require("../src/build");
|
|
3
3
|
const nodemon = require("nodemon");
|
|
4
|
-
const { config, getConfig } = require("../core/lib");
|
|
4
|
+
const { config, getConfig, getConfigFile } = require("../core/lib");
|
|
5
5
|
|
|
6
6
|
async function ejsbuild_dev() {
|
|
7
7
|
await getConfig();
|
|
@@ -10,7 +10,9 @@ async function ejsbuild_dev() {
|
|
|
10
10
|
config.pages.dir,
|
|
11
11
|
config.components.dir,
|
|
12
12
|
config.data.dir,
|
|
13
|
+
getConfigFile(),
|
|
13
14
|
".env",
|
|
15
|
+
"tailwind.config.js",
|
|
14
16
|
];
|
|
15
17
|
|
|
16
18
|
_d("Watching :", toWatch.map((d) => `'${d}'`).join(", "));
|
package/src/build.js
CHANGED
|
@@ -18,6 +18,7 @@ const {
|
|
|
18
18
|
_w,
|
|
19
19
|
_ds,
|
|
20
20
|
_cp,
|
|
21
|
+
_v,
|
|
21
22
|
} = require("../core");
|
|
22
23
|
const {
|
|
23
24
|
getConfig,
|
|
@@ -40,7 +41,7 @@ async function ejsbuild(code = 0) {
|
|
|
40
41
|
const start = new Date();
|
|
41
42
|
|
|
42
43
|
generateBaseDirs();
|
|
43
|
-
const { globalData, localData } = await getDatas();
|
|
44
|
+
const { globalData, localData, routeData } = await getDatas();
|
|
44
45
|
|
|
45
46
|
/** @param {File } file */
|
|
46
47
|
function getOutputName(file) {
|
|
@@ -61,6 +62,13 @@ async function ejsbuild(code = 0) {
|
|
|
61
62
|
return { normalized, original };
|
|
62
63
|
}
|
|
63
64
|
|
|
65
|
+
const params_regex = /\$([a-zA-Z_]\w*)\b/g;
|
|
66
|
+
function fillParams(s = "", p = {}) {
|
|
67
|
+
return s.replace(params_regex, (_, key) => {
|
|
68
|
+
return p[key] ?? `$${key}`;
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
64
72
|
// generate tailwind file only if detected any view
|
|
65
73
|
if (
|
|
66
74
|
_tree(config.pages.dir).filter((f) => !f.isDir && f.ext == "ejs").length > 0
|
|
@@ -74,7 +82,7 @@ async function ejsbuild(code = 0) {
|
|
|
74
82
|
const built = [];
|
|
75
83
|
|
|
76
84
|
/** @param {File } file */
|
|
77
|
-
async function writeFile(file) {
|
|
85
|
+
async function writeFile(file, templatePath, templateData) {
|
|
78
86
|
const outputName = getOutputName(file);
|
|
79
87
|
const hasConflict = !!allOutputNames.find(
|
|
80
88
|
(f) => f.original == outputName.normalized,
|
|
@@ -91,16 +99,57 @@ async function ejsbuild(code = 0) {
|
|
|
91
99
|
return;
|
|
92
100
|
}
|
|
93
101
|
output += ".html";
|
|
102
|
+
const routeParams = outputName.original.match(params_regex) ?? [];
|
|
103
|
+
|
|
104
|
+
const hasParams = routeParams.length > 0;
|
|
105
|
+
|
|
106
|
+
const local =
|
|
107
|
+
localData[outputName.original] ?? localData[`/${outputName.original}`];
|
|
108
|
+
|
|
109
|
+
if (hasParams) {
|
|
110
|
+
const routes = await _v(
|
|
111
|
+
routeData[outputName.original] ?? routeData[`/${outputName.original}`],
|
|
112
|
+
);
|
|
113
|
+
if (typeof routes == "object") {
|
|
114
|
+
const params = Array.isArray(routes) ? routes : [routes];
|
|
115
|
+
for (let p of params) {
|
|
116
|
+
const fill = (s) => fillParams(s, p);
|
|
117
|
+
const pf = {
|
|
118
|
+
name: fill(file.name),
|
|
119
|
+
fullpath: fill(file.fullpath),
|
|
120
|
+
path: fill(file.path),
|
|
121
|
+
isDir: file.isDir,
|
|
122
|
+
ext: file.ext,
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const notFoundParams = [
|
|
126
|
+
...(pf.fullpath.matchAll(params_regex) ?? []),
|
|
127
|
+
].map((m) => m[1]);
|
|
128
|
+
if (notFoundParams.length > 0) {
|
|
129
|
+
_d(
|
|
130
|
+
`\x1b[31m${notFoundParams.map((p) => `'${p}'`).join(", ")} not found for route '${outputName.original}'`,
|
|
131
|
+
);
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
writeFile(pf, file.fullpath, await _v(local, p));
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
94
142
|
try {
|
|
95
143
|
const data = {
|
|
96
144
|
...globalData,
|
|
97
|
-
...(
|
|
145
|
+
...((await _v(local, {})) ?? {}),
|
|
146
|
+
...(templateData ?? {}),
|
|
98
147
|
};
|
|
99
148
|
|
|
100
149
|
const getComponent = (component, ...args) => {
|
|
101
150
|
if (!component.endsWith(".ejs")) component += ".ejs";
|
|
102
151
|
const content = _r(`${config.components.dir}/${component}`, false);
|
|
103
|
-
if (
|
|
152
|
+
if (content == null) throw new Error("Component not found.");
|
|
104
153
|
const component_data = {};
|
|
105
154
|
for (let i in args) {
|
|
106
155
|
component_data[`$${i}`] = args[i];
|
|
@@ -108,6 +157,18 @@ async function ejsbuild(code = 0) {
|
|
|
108
157
|
|
|
109
158
|
return { content, component_data };
|
|
110
159
|
};
|
|
160
|
+
|
|
161
|
+
const handleComponentError = (component, error) => {
|
|
162
|
+
if (config.components.autoGenerate) {
|
|
163
|
+
_md(config.components.dir + `/${component}`, false);
|
|
164
|
+
_w(config.components.dir + `/${component}.ejs`, "", false);
|
|
165
|
+
_ds(`Missing component '${component}' generated.`);
|
|
166
|
+
} else {
|
|
167
|
+
_d(`\x1b[31mFailed to build component '${component}'`);
|
|
168
|
+
console.log("\x1b[31m", error.message, "\x1b[0m");
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
|
|
111
172
|
// NOTE - Reserved keywords
|
|
112
173
|
const context = (d = {}) =>
|
|
113
174
|
new Proxy(d, {
|
|
@@ -134,8 +195,7 @@ async function ejsbuild(code = 0) {
|
|
|
134
195
|
);
|
|
135
196
|
return comp_render;
|
|
136
197
|
} catch (error) {
|
|
137
|
-
|
|
138
|
-
console.log("\x1b[31m", error.message, "\x1b[0m");
|
|
198
|
+
handleComponentError(component, error);
|
|
139
199
|
}
|
|
140
200
|
},
|
|
141
201
|
$async: async (component, ...args) => {
|
|
@@ -155,8 +215,7 @@ async function ejsbuild(code = 0) {
|
|
|
155
215
|
);
|
|
156
216
|
return comp_render;
|
|
157
217
|
} catch (error) {
|
|
158
|
-
|
|
159
|
-
console.log("\x1b[31m", error.message, "\x1b[0m");
|
|
218
|
+
handleComponentError(component, error);
|
|
160
219
|
}
|
|
161
220
|
},
|
|
162
221
|
$env: (k) => process.env[k],
|
|
@@ -187,8 +246,9 @@ async function ejsbuild(code = 0) {
|
|
|
187
246
|
return new Date();
|
|
188
247
|
},
|
|
189
248
|
};
|
|
249
|
+
// const fileContent = _r(file.fullpath, false);
|
|
190
250
|
const out = await ejs.renderFile(
|
|
191
|
-
file.fullpath,
|
|
251
|
+
templatePath ?? file.fullpath,
|
|
192
252
|
context({
|
|
193
253
|
...data,
|
|
194
254
|
...defaultData,
|
package/src/tailwind.js
CHANGED
|
@@ -3,7 +3,7 @@ const path = require("path");
|
|
|
3
3
|
const postcss = require("postcss");
|
|
4
4
|
const tailwindcss = require("tailwindcss");
|
|
5
5
|
const autoprefixer = require("autoprefixer");
|
|
6
|
-
const { _p, _e, _w, _js, _ce } = require("../core");
|
|
6
|
+
const { _p, _e, _w, _js, _ce, _r } = require("../core");
|
|
7
7
|
const { config, configTailwindOutput } = require("../core/lib");
|
|
8
8
|
|
|
9
9
|
const twConfigPath = "tailwind.config.js";
|
|
@@ -41,6 +41,24 @@ function mergeTailwindConfig(base, user) {
|
|
|
41
41
|
};
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
function extractLayers(path) {
|
|
45
|
+
const css = _r(path, false);
|
|
46
|
+
if (!css) return {};
|
|
47
|
+
const regex = /@layer\s+([a-zA-Z0-9_-]+)\s*\{([\s\S]*?\})\s*\}/g;
|
|
48
|
+
const result = {};
|
|
49
|
+
let match;
|
|
50
|
+
|
|
51
|
+
while ((match = regex.exec(css)) !== null) {
|
|
52
|
+
const name = match[1];
|
|
53
|
+
const content = match[2].trim();
|
|
54
|
+
|
|
55
|
+
result[name] ??= [];
|
|
56
|
+
result[name].push(content);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return result;
|
|
60
|
+
}
|
|
61
|
+
|
|
44
62
|
async function buildTailwind() {
|
|
45
63
|
const inputCss = path.join(__dirname, "../templates/tailwind.css");
|
|
46
64
|
const outputCss = configTailwindOutput();
|
|
@@ -83,13 +101,34 @@ module.exports = ${_js(baseConfig)};
|
|
|
83
101
|
let css = fs.readFileSync(inputCss, "utf8");
|
|
84
102
|
const tailwindOutput = path.dirname(configTailwindOutput());
|
|
85
103
|
let imported = "";
|
|
104
|
+
const layers = {};
|
|
86
105
|
for (let style of config.tailwind.imports) {
|
|
106
|
+
const uri_regex = /^https?:\/\/[^\s/$.?#].[^\s]*$/i;
|
|
107
|
+
if (uri_regex.test(style)) {
|
|
108
|
+
imported += `@import "${style}";\n`;
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
87
111
|
const style_path = _p(`${config.build.output}/${style}`);
|
|
88
112
|
const relative_path = path.relative(tailwindOutput, style_path);
|
|
89
113
|
imported += `@import "./${relative_path}";\n`;
|
|
114
|
+
|
|
115
|
+
const lay = extractLayers(`${config.pages.dir}/${style}`);
|
|
116
|
+
for (let k in lay) {
|
|
117
|
+
layers[k] ??= "";
|
|
118
|
+
layers[k] += `${lay[k]}\n`;
|
|
119
|
+
}
|
|
90
120
|
}
|
|
121
|
+
|
|
91
122
|
css = `${imported}\n${css}`;
|
|
92
123
|
|
|
124
|
+
for (let k in layers) {
|
|
125
|
+
css += `
|
|
126
|
+
@layer ${k} {
|
|
127
|
+
${layers[k]?.trim()}
|
|
128
|
+
}
|
|
129
|
+
`;
|
|
130
|
+
}
|
|
131
|
+
|
|
93
132
|
const result = await postcss([
|
|
94
133
|
tailwindcss(finalConfig),
|
|
95
134
|
autoprefixer(),
|