fast-ejs-builder 1.0.1 → 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 CHANGED
@@ -61,7 +61,8 @@ npm install fast-ejs-builder --save-dev
61
61
  "useIndexRouting": true
62
62
  },
63
63
  "components": {
64
- "dir": "components"
64
+ "dir": "components",
65
+ "autoGenerate": false
65
66
  },
66
67
  "data": {
67
68
  "dir": "data",
@@ -127,7 +128,8 @@ The `fast.ejs.json` (occasionnally called _the FEJ_) file controls all aspects o
127
128
  "useIndexRouting": true
128
129
  },
129
130
  "components": {
130
- "dir": "components"
131
+ "dir": "components",
132
+ "autoGenerate": false
131
133
  },
132
134
  "data": {
133
135
  "dir": "data",
@@ -139,6 +141,7 @@ The `fast.ejs.json` (occasionnally called _the FEJ_) file controls all aspects o
139
141
  "tailwind": {
140
142
  "output": "public/app.css",
141
143
  "imports": [
144
+ "public/style.css",
142
145
  "https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap"
143
146
  ]
144
147
  }
@@ -151,11 +154,12 @@ The `fast.ejs.json` (occasionnally called _the FEJ_) file controls all aspects o
151
154
  - **`build.interval`**: Milliseconds between rebuilds in development mode
152
155
  - **`build.useIndexRouting`**: Generate `route/index.html` instead of `route.html`
153
156
  - **`components.dir`**: Directory containing reusable EJS components
157
+ - **`components.autoGenerate`**: Generate missing components. Default is `false`
154
158
  - **`data.dir`**: Directory for global and page-specific data files
155
159
  - **`data.allow`**: Data file format (`"js"`, `"json"`, or `"all"`)
156
160
  - **`pages.dir`**: Directory containing your EJS page templates. **Here is where you should mainly work**.
157
161
  - **`tailwind.output`**: Path to generated Tailwind CSS file
158
- - **`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.
159
163
 
160
164
  ## Usage Examples
161
165
 
@@ -318,14 +322,15 @@ Fast EJS comes with default data that can't be overrided.
318
322
 
319
323
  #### 2. Using your own data
320
324
 
321
- 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.
322
327
 
323
328
  - **Global data** : Can be accessed inside every pages and components
324
329
 
325
330
  If `data.allow` is `all` or `js` (recommended)
326
331
 
327
332
  ```javascript
328
- // data/global.data.js
333
+ // data/global.js
329
334
  module.exports = {
330
335
  siteName: "My Awesome Site",
331
336
  navItems: [
@@ -335,13 +340,16 @@ module.exports = {
335
340
  ],
336
341
  add: (a, b) => a + b,
337
342
  getUsers: async () => await db.getUsers(),
343
+ get randomBool() {
344
+ return Math.random() > 0.5;
345
+ },
338
346
  };
339
347
  ```
340
348
 
341
349
  If `data.allow` is `all` or `json`
342
350
 
343
351
  ```js
344
- // data/global.data.json
352
+ // data/global.json
345
353
  {
346
354
  "siteName": "My Awesome Site",
347
355
  "navItems": [
@@ -355,18 +363,79 @@ If `data.allow` is `all` or `json`
355
363
  - **Local data** : Can be accessed only in the target page
356
364
 
357
365
  ```javascript
358
- // data/local.data.js
366
+ // data/local.js
359
367
  module.exports = {
360
368
  // for page "pages/users/profile.ejs"
361
369
  "users/profile": {
362
370
  title: "Welcome to My Site",
363
371
  description: "This is a fast-ejs powered website with Tailwind CSS.",
364
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
+ },
365
403
  };
366
404
  ```
367
405
 
368
406
  **Note that all JS data can be asynchronous, and each asynchronous data will affect the build time.**
369
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
+
370
439
  ## Commands
371
440
 
372
441
  - **`fast-ejs dev`**: Start development server with live reloading
@@ -379,7 +448,10 @@ Here is the real building flow :
379
448
  - Get the data inside `data.dir` that match the `data.allow`.
380
449
  - Scan the specified `pages.dir`.
381
450
  - All empty folder will be ignored
382
- - If any `.ejs` file is found, render the html with the corresponding data and generate the right file inside `build.output` depending on `build.useIndexRouting`.
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`.
383
455
  - For any other file, just copy it inside the `build.output`.
384
456
  - Generate the css with tailwind at `tailwind.output` along with `tailwind.imports` if specified.
385
457
  - Scan the `build.output` and clean all junk files and folders (files from previous build and empty folders).
package/bin/index.js CHANGED
File without changes
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() || "fast.ejs.json";
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.data.${type}`;
120
- const localDataPath = (type) => `${config.data.dir}/local.data.${type}`;
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 typeof v == "function" ? await v(default_data_args) : v;
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
- let globalData = {},
197
- localData = {};
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,7 +6,8 @@
6
6
  "useIndexRouting": true
7
7
  },
8
8
  "components": {
9
- "dir": "components"
9
+ "dir": "components",
10
+ "autoGenerate": false
10
11
  },
11
12
  "data": {
12
13
  "allow": "all",
@@ -34,6 +34,11 @@
34
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"],
@@ -43,7 +48,7 @@
43
48
  "properties": {
44
49
  "dir": {
45
50
  "default": "data",
46
- "description": "The folder containing global and local data files (global.data.js/json, local.data.js/json)",
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,7 +61,6 @@
56
61
  "required": ["dir"],
57
62
  "additionalProperties": false
58
63
  },
59
-
60
64
  "pages": {
61
65
  "properties": {
62
66
  "dir": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fast-ejs-builder",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "description": "Fast-EJS is a simple and fast tool to pre-render EJS templates into static HTML files with clean conventions and flexible data handling.",
5
5
  "keywords": [
6
6
  "tailwind",
@@ -27,10 +27,6 @@
27
27
  "bin": {
28
28
  "fast-ejs": "bin/index.js"
29
29
  },
30
- "scripts": {
31
- "test": "echo \"Error: no test specified\" && exit 1",
32
- "dev": "nodemon index.js"
33
- },
34
30
  "dependencies": {
35
31
  "@inquirer/prompts": "^8.2.0",
36
32
  "autoprefixer": "^10.4.23",
@@ -40,5 +36,9 @@
40
36
  "postcss": "^8.5.6",
41
37
  "prettier": "^3.8.0",
42
38
  "tailwindcss": "^3.4.17"
39
+ },
40
+ "scripts": {
41
+ "test": "echo \"Error: no test specified\" && exit 1",
42
+ "dev": "nodemon index.js"
43
43
  }
44
- }
44
+ }
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
- ...(localData[outputName.original] ?? {}),
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 (!content) throw new Error("Component not found.");
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
- _d(`\x1b[31mFailed to build component '${component}'`);
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
- _d(`\x1b[31mFailed to build component '${component}'`);
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(),