alchemymvc 1.4.0-alpha.2 → 1.4.0-alpha.3

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.
@@ -1,3 +1,5 @@
1
+ @use "alchemy";
2
+
1
3
  html {
2
4
  font-family: sans-serif;
3
5
  text-size-adjust: 100%;
@@ -89,7 +89,7 @@ LoopConduit.setMethod(function copyParentProperties(conduit) {
89
89
  *
90
90
  * @author Jelle De Loecker <jelle@elevenways.be>
91
91
  * @since 1.1.3
92
- * @version 1.3.0
92
+ * @version 1.4.0
93
93
  *
94
94
  * @param {Object} options
95
95
  * @param {Function} callback
@@ -108,17 +108,20 @@ LoopConduit.setMethod(function setOptions(options, callback) {
108
108
  options = JSON.clone(options);
109
109
 
110
110
  for (key in options) {
111
- let set_method = false;
111
+
112
+ // Keep track if a request method has been set
113
+ let got_method = false;
112
114
 
113
115
  info = Classes.Develry.Request.getMethodInfo(key);
114
116
 
115
117
  if (info && options[key]) {
116
118
 
117
- if (info.method == 'get' && set_method) {
118
- // Ignore
119
+ if (info.method == 'get' && got_method) {
120
+ // If a request method has already been set,
121
+ // do not let `get` options overwrite it!
119
122
  } else {
120
123
  this.method = key;
121
- set_method = true;
124
+ got_method = true;
122
125
  }
123
126
 
124
127
  if (info.has_body && typeof options[key] == 'object') {
@@ -131,6 +134,8 @@ LoopConduit.setMethod(function setOptions(options, callback) {
131
134
  this.method = options.method;
132
135
  }
133
136
 
137
+ let route_params = options.params;
138
+
134
139
  if (options.name) {
135
140
  // @TODO: what about path sections?
136
141
  route = this.getRouteByName(options.name);
@@ -147,7 +152,7 @@ LoopConduit.setMethod(function setOptions(options, callback) {
147
152
 
148
153
  // @WARNING: It's best to just generate the URL
149
154
  // and let it parse all the information that way
150
- options.href = this.routeUrl(options.name, options.params, {extra_get_parameters: false});
155
+ options.href = this.routeUrl(options.name, route_params, {extra_get_parameters: false});
151
156
  }
152
157
 
153
158
  if (!this.method) {
@@ -158,6 +163,10 @@ LoopConduit.setMethod(function setOptions(options, callback) {
158
163
 
159
164
  this.original_url = RURL.parse(options.href);
160
165
 
166
+ if (options.get && typeof options.get == 'object') {
167
+ this.original_url.addQuery(options.get);
168
+ }
169
+
161
170
  this.url = this.original_url;
162
171
  this.original_path = this.url.path;
163
172
  this.original_pathname = this.url.pathname;
@@ -169,14 +178,20 @@ LoopConduit.setMethod(function setOptions(options, callback) {
169
178
  this.parseSection();
170
179
 
171
180
  promise = this.parseRoute();
181
+ } else if (options.get && typeof options.get == 'object') {
182
+ if (!this.params) {
183
+ this.params = {};
184
+ }
185
+
186
+ Object.assign(this.params, options.get);
172
187
  }
173
188
 
174
189
  if (options.body) {
175
190
  this.body = options.body;
176
191
  }
177
192
 
178
- if (options.params) {
179
- this.setRouteParameters(options.params);
193
+ if (route_params) {
194
+ this.setRouteParameters(route_params);
180
195
  }
181
196
 
182
197
  if (options.arguments) {
@@ -40,7 +40,7 @@ EnumMap.setMethod(function get(name) {
40
40
  *
41
41
  * @author Jelle De Loecker <jelle@elevenways.be>
42
42
  * @since 1.2.1
43
- * @version 1.2.1
43
+ * @version 1.4.0
44
44
  *
45
45
  * @param {string} name
46
46
  * @param {*} value
@@ -65,18 +65,37 @@ EnumMap.setMethod(function set(name, value) {
65
65
  name : value,
66
66
  title : value,
67
67
  };
68
- } else if (typeof value == 'function') {
69
- result = {
70
- name : value.name,
71
- title : value.title,
72
- };
73
68
  } else {
74
- result = {
75
- name : value.name,
76
- title : value.title || value.name,
77
- };
69
+ if (typeof value == 'function') {
70
+ result = {
71
+ name : value.name,
72
+ title : value.title,
73
+ };
74
+ } else {
75
+ result = {
76
+ name : value.name,
77
+ title : value.title || value.name,
78
+ };
79
+
80
+ if (value.icon) {
81
+ result.icon = value.icon;
82
+ }
83
+
84
+ if (value.color) {
85
+ result.color = value.color;
86
+ }
87
+
88
+ if (value.text_color) {
89
+ result.text_color = value.text_color;
90
+ }
91
+ }
92
+
93
+ if (value.enum_info) {
94
+ Object.assign(result, value.enum_info);
95
+ }
78
96
  }
79
97
 
98
+ result.number = this.local.size + 1;
80
99
  result.value = value;
81
100
  result.is_enumified = true;
82
101
 
@@ -112,7 +112,7 @@ Router.setMethod(function isLocalUrl(url) {
112
112
  *
113
113
  * @author Jelle De Loecker <jelle@elevenways.be>
114
114
  * @since 1.1.0
115
- * @version 1.3.1
115
+ * @version 1.4.0
116
116
  *
117
117
  * @param {Element} element The element to apply to
118
118
  * @param {string} name The route name
@@ -225,6 +225,8 @@ Router.setMethod(function applyDirective(element, name, options) {
225
225
  disable_ajax = true;
226
226
  }
227
227
 
228
+ let method_attribute = false;
229
+
228
230
  if (element.role == 'link') {
229
231
  attribute_name = 'href';
230
232
  } else {
@@ -232,6 +234,7 @@ Router.setMethod(function applyDirective(element, name, options) {
232
234
  switch (element.nodeName) {
233
235
  case 'FORM':
234
236
  attribute_name = 'action';
237
+ method_attribute = 'method';
235
238
  break;
236
239
 
237
240
  case 'AREA':
@@ -260,6 +263,10 @@ Router.setMethod(function applyDirective(element, name, options) {
260
263
  }
261
264
  }
262
265
 
266
+ if (method_attribute && config.methods?.[0]) {
267
+ element.setAttribute(method_attribute, config.methods[0]);
268
+ }
269
+
263
270
  if (disable_ajax) {
264
271
  element.setAttribute('data-he-link', 'false');
265
272
  }
@@ -384,19 +391,13 @@ Router.setMethod(function routeConfig(name, socket_route) {
384
391
  */
385
392
  Router.setMethod(function routeUrl(name, parameters, options) {
386
393
 
387
- var base_url,
388
- locales,
389
- config,
390
- url,
391
- key,
392
- i;
393
-
394
394
  if (!options) {
395
395
  options = {};
396
396
  }
397
397
 
398
- url = '';
399
- config = options.config || this.routeConfig(name);
398
+ let locales,
399
+ config = options.config || this.routeConfig(name),
400
+ url = '';
400
401
 
401
402
  if (options.locale || options.prefix) {
402
403
  locales = [options.prefix || options.locale];
@@ -417,11 +418,15 @@ Router.setMethod(function routeUrl(name, parameters, options) {
417
418
  // (in case it's a special parameters object, like a Document)
418
419
  parameters = Object.create(parameters);
419
420
  }
421
+ } else {
422
+ parameters = {};
420
423
  }
421
424
 
422
425
  let chosen_prefix;
423
426
 
424
427
  if (config != null) {
428
+ let i;
429
+
425
430
  for (i = 0; i < locales.length; i++) {
426
431
  if (config.paths && config.paths[locales[i]]) {
427
432
  chosen_prefix = locales[i];
@@ -435,7 +440,7 @@ Router.setMethod(function routeUrl(name, parameters, options) {
435
440
  }
436
441
 
437
442
  if (!url && config.paths) {
438
- key = Object.keys(config.paths).first();
443
+ let key = Object.keys(config.paths).first();
439
444
 
440
445
  if (key) {
441
446
  url = '/' + key + config.paths[key];
@@ -520,6 +525,9 @@ Router.setMethod(function routeUrl(name, parameters, options) {
520
525
  }
521
526
 
522
527
  if (options.full || options.absolute) {
528
+
529
+ let base_url;
530
+
523
531
  if (this.view) {
524
532
  base_url = this.view.internal('url');
525
533
  }
@@ -83,4 +83,17 @@ Enum.setMethod(function getClientConfigOptions(wm) {
83
83
  let options = JSON.clone(this.options, 'toHawkejs', wm);
84
84
 
85
85
  return options;
86
+ });
87
+
88
+ /**
89
+ * Get the configuration entry for the given value
90
+ *
91
+ * @author Jelle De Loecker <jelle@elevenways.be>
92
+ * @since 1.4.0
93
+ * @version 1.4.0
94
+ *
95
+ * @return {Object}
96
+ */
97
+ Enum.setMethod(function getValueConfiguration(value) {
98
+ return this.getValues()?.get?.(value);
86
99
  });
@@ -1002,7 +1002,7 @@ Document.setMethod(function recomputeFieldIfNecessary(name, force = false) {
1002
1002
  *
1003
1003
  * @author Jelle De Loecker <jelle@elevenways.be>
1004
1004
  * @since 1.3.21
1005
- * @version 1.3.21
1005
+ * @version 1.4.0
1006
1006
  *
1007
1007
  * @param {string|Alchemy.Field} name
1008
1008
  * @param {*} value
@@ -1018,9 +1018,7 @@ Document.setMethod(function _setComputedFieldValue(name, value) {
1018
1018
  }
1019
1019
 
1020
1020
  if (Pledge.isThenable(value)) {
1021
- value.then(value => {
1022
- this.$main[name] = value;
1023
- });
1021
+ value = value.then(value => this.$main[name] = value);
1024
1022
  } else {
1025
1023
  this.$main[name] = value;
1026
1024
  }
@@ -1034,7 +1032,7 @@ Document.setMethod(function _setComputedFieldValue(name, value) {
1034
1032
  *
1035
1033
  * @author Jelle De Loecker <jelle@elevenways.be>
1036
1034
  * @since 1.3.21
1037
- * @version 1.3.21
1035
+ * @version 1.4.0
1038
1036
  *
1039
1037
  * @return {Pledge|undefined}
1040
1038
  */
@@ -1089,8 +1087,11 @@ Document.setMethod(function recomputeValues() {
1089
1087
  }
1090
1088
 
1091
1089
  let result = compute_method.call(this, this, field);
1090
+ result = this._setComputedFieldValue(key, result);
1092
1091
 
1093
- this._setComputedFieldValue(key, result);
1092
+ if (Pledge.isThenable(result)) {
1093
+ promises.push(result);
1094
+ }
1094
1095
  }
1095
1096
 
1096
1097
  if (promises.length) {
@@ -15,7 +15,7 @@ const SystemTask = Function.inherits('Alchemy.Model.System', 'Task');
15
15
  *
16
16
  * @author Jelle De Loecker <jelle@elevenways.be>
17
17
  * @since 1.3.17
18
- * @version 1.3.17
18
+ * @version 1.4.0
19
19
  */
20
20
  SystemTask.constitute(function addTaskFields() {
21
21
 
@@ -48,9 +48,18 @@ SystemTask.constitute(function addTaskFields() {
48
48
 
49
49
  this.addField('schedule_type', 'Enum', {
50
50
  values: {
51
- user : 'User',
52
- system_forced : 'System (forced)',
53
- system_fallback : 'System (fallback)',
51
+ user : {
52
+ title : 'User',
53
+ icon : 'user',
54
+ },
55
+ system_forced : {
56
+ title: 'System (forced)',
57
+ icon : 'shield-keyhole',
58
+ },
59
+ system_fallback : {
60
+ title: 'System (fallback)',
61
+ icon : 'shield-halved',
62
+ }
54
63
  },
55
64
  default: 'user',
56
65
  });
package/lib/bootstrap.js CHANGED
@@ -7,6 +7,11 @@ const libpath = require('path');
7
7
  */
8
8
  const Protoblast = require('protoblast')(true);
9
9
 
10
+ /**
11
+ * Require Hawkejs next
12
+ */
13
+ require('hawkejs');
14
+
10
15
  /**
11
16
  * Define shared global constants and require methods
12
17
  */
@@ -7,6 +7,22 @@
7
7
  */
8
8
  const Element = Function.inherits('Hawkejs.Element', 'Alchemy.Element', 'Element');
9
9
 
10
+ /**
11
+ * Let Alchemy handle the stylesheets of custom elements
12
+ *
13
+ * @author Jelle De Loecker <jelle@elevenways.be>
14
+ * @since 1.4.0
15
+ * @version 1.4.0
16
+ */
17
+ Classes.Hawkejs.Element.Element.setStylesheetHandler(function handleStylesheet(path) {
18
+
19
+ if (!path || Blast.isBrowser) {
20
+ return;
21
+ }
22
+
23
+ alchemy.registerRequiredStylesheet(path);
24
+ });
25
+
10
26
  /**
11
27
  * The default element prefix (when element contains no hyphen) is "al"
12
28
  *
@@ -1,4 +1,4 @@
1
- var publicDirs = alchemy.shared('public.directories', new Deck()),
1
+ let publicDirs = alchemy.shared('public.directories', new Deck()),
2
2
  scriptDirs = alchemy.shared('script.directories', new Deck()),
3
3
  assetDirs = alchemy.shared('asset.directories', new Deck()),
4
4
  styleDirs = alchemy.shared('stylesheet.directories', new Deck()),
@@ -11,9 +11,10 @@ var publicDirs = alchemy.shared('public.directories', new Deck()),
11
11
  Nodent = alchemy.use('nodent-compiler'),
12
12
  Terser = alchemy.use('terser'),
13
13
  libpath = alchemy.use('path'),
14
+ required_stylesheets = new Set(),
14
15
  nodent_compiler,
15
16
  regenerator_runtime,
16
- sass_functions,
17
+ sass_functions,
17
18
  babel_polyfill,
18
19
  babel_preset,
19
20
  babel_async,
@@ -122,7 +123,7 @@ function getMiddlePaths(paths, ext, new_ext) {
122
123
  }
123
124
 
124
125
  /**
125
- * Less middleware
126
+ * Stylesheet middleware
126
127
  *
127
128
  * @author Jelle De Loecker <jelle@elevenways.be>
128
129
  * @since 0.2.0
@@ -130,17 +131,11 @@ function getMiddlePaths(paths, ext, new_ext) {
130
131
  */
131
132
  Alchemy.setMethod(function styleMiddleware(req, res, nextMiddleware) {
132
133
 
133
- var warnings,
134
- compiled,
135
- o_stats,
136
- path,
137
- url;
138
-
139
134
  // Get the URL object
140
- url = req.conduit.url;
135
+ let url = req.conduit.url;
141
136
 
142
137
  // Get the path, including the query
143
- path = url.path;
138
+ let path = url.path;
144
139
 
145
140
  // If this file has already been found & compiled, serve it to the user
146
141
  if (asset_cache.has(path)) {
@@ -151,41 +146,106 @@ Alchemy.setMethod(function styleMiddleware(req, res, nextMiddleware) {
151
146
  }});
152
147
  }
153
148
 
154
- // Non-critical errors go here
155
- warnings = [];
149
+ let css_path = req.middlePath;
150
+
151
+ findStylesheet(css_path, {compile: true}).done((err, result) => {
152
+
153
+ if (err) {
154
+ return req.conduit.error(err);
155
+ }
156
+
157
+ let compiled_path = result?.compiled?.path;
158
+
159
+ if (!compiled_path) {
160
+ return nextMiddleware();
161
+ }
162
+
163
+ return req.conduit.serveFile(compiled_path, {
164
+ mimetype: 'text/css',
165
+ onError: function onError(err) {
166
+ if (fileCache[compiled_path]) {
167
+ fileCache[compiled_path] = null;
168
+
169
+ let source_path = result.source?.path;
170
+
171
+ if (stats && fileCache[source_path]) {
172
+ fileCache[source_path] = null;
173
+ }
174
+
175
+ alchemy.styleMiddleware(req, res, nextMiddleware);
176
+ }
177
+ }
178
+ });
179
+ });
180
+ });
181
+
182
+ /**
183
+ * Find a stylesheet
184
+ *
185
+ * @author Jelle De Loecker <jelle@elevenways.be>
186
+ * @since 1.4.0
187
+ * @version 1.4.0
188
+ *
189
+ * @param {string} css_path Relative path to the CSS source file
190
+ * @param {Object} options
191
+ *
192
+ * @return {Pledge<Object>}
193
+ */
194
+ function findStylesheet(css_path, options) {
195
+
196
+ let source_stats,
197
+ compiled,
198
+ warnings = [];
199
+
200
+ if (css_path[0] != '/') {
201
+ css_path = '/' + css_path;
202
+ }
203
+
204
+ if (!options) {
205
+ options = {};
206
+ }
207
+
208
+ let do_compile = options.compile === true;
209
+
210
+ if (!css_path.endsWith('.css')) {
211
+ return Pledge.reject(new Error('Not a .css file'));
212
+ }
213
+
214
+ return Function.series(function getCssPath(next) {
156
215
 
157
- Function.series(function getCssPath(next) {
158
216
  // Look for a regular .css file
159
- alchemy.findAssetPath(req.middlePath, styleDirs.getSorted(), function gotAssetPath(err, stats) {
217
+ alchemy.findAssetPath(css_path, styleDirs.getSorted(), function gotAssetPath(err, stats) {
160
218
 
161
219
  if (err || !stats || !stats.path) {
162
220
  return next();
163
221
  }
164
222
 
165
- compiled = stats;
223
+ source_stats = compiled = stats;
166
224
  next();
167
225
  });
168
226
  }, function getLessPath(next) {
169
227
 
170
- var lessPath;
171
-
172
- if (compiled || !less) {
228
+ if (source_stats || !less) {
173
229
  return next();
174
230
  }
175
231
 
176
- lessPath = getMiddlePaths(req.middlePath, /\.css$/, '.less');
232
+ let less_path = getMiddlePaths(css_path, /\.css$/, '.less');
177
233
 
178
234
  // Look through all the asset folders for the less style file
179
- alchemy.findAssetPath(lessPath, styleDirs.getSorted(), function gotAssetPath(err, stats) {
235
+ alchemy.findAssetPath(less_path, styleDirs.getSorted(), function gotAssetPath(err, stats) {
180
236
 
181
237
  if (err || !stats || !stats.path) {
182
238
  return next();
183
239
  }
184
240
 
185
- o_stats = stats;
241
+ source_stats = stats;
242
+
243
+ if (!do_compile) {
244
+ return next();
245
+ }
186
246
 
187
247
  // Compile this less file
188
- alchemy.getCompiledLessPath(stats.path, {}, function gotLessPath(err, cssInfo) {
248
+ alchemy.getCompiledLessPath(stats.path, options, function gotLessPath(err, cssInfo) {
189
249
 
190
250
  if (err) {
191
251
  warnings.push(err);
@@ -199,25 +259,27 @@ Alchemy.setMethod(function styleMiddleware(req, res, nextMiddleware) {
199
259
 
200
260
  }, function getScssPath(next) {
201
261
 
202
- var scssPath;
203
-
204
- if (compiled || !sass) {
262
+ if (source_stats || !sass) {
205
263
  return next();
206
264
  }
207
265
 
208
- scssPath = getMiddlePaths(req.middlePath, /\.css$/, '.scss');
266
+ let scss_path = getMiddlePaths(css_path, /\.css$/, '.scss');
209
267
 
210
268
  // Look through all the asset folders for the scss style file
211
- alchemy.findAssetPath(scssPath, styleDirs.getSorted(), function gotAssetPath(err, stats) {
269
+ alchemy.findAssetPath(scss_path, styleDirs.getSorted(), function gotAssetPath(err, stats) {
212
270
 
213
271
  if (err || !stats || !stats.path) {
214
272
  return next();
215
273
  }
216
274
 
217
- o_stats = stats;
275
+ source_stats = stats;
276
+
277
+ if (!do_compile) {
278
+ return next();
279
+ }
218
280
 
219
281
  // Compile this less file
220
- alchemy.getCompiledSassPath(stats.path, {}, function gotSassPath(err, cssInfo) {
282
+ alchemy.getCompiledSassPath(stats.path, options, function gotSassPath(err, cssInfo) {
221
283
 
222
284
  if (err) {
223
285
  warnings.push(err);
@@ -232,34 +294,28 @@ Alchemy.setMethod(function styleMiddleware(req, res, nextMiddleware) {
232
294
  }, function done(err) {
233
295
 
234
296
  if (err) {
235
- return req.conduit.error(err);
297
+ throw err;
236
298
  }
237
299
 
238
300
  // If there are warnings, use the first one
239
301
  if (warnings.length) {
240
- return req.conduit.error(warnings[0]);
302
+ throw warnings[0];
241
303
  }
242
304
 
243
- if (!compiled || !compiled.path) {
244
- return nextMiddleware();
305
+ if (options?.post_css_only) {
306
+ return compiled;
245
307
  }
246
308
 
247
- return req.conduit.serveFile(compiled.path, {
248
- mimetype: 'text/css',
249
- onError: function onError(err) {
250
- if (fileCache[compiled.path]) {
251
- fileCache[compiled.path] = null;
252
-
253
- if (o_stats && fileCache[o_stats.path]) {
254
- fileCache[o_stats.path] = null;
255
- }
309
+ if (do_compile && (!compiled || !compiled.path)) {
310
+ return false;
311
+ }
256
312
 
257
- alchemy.styleMiddleware(req, res, nextMiddleware);
258
- }
259
- }
260
- });
313
+ return {
314
+ compiled : compiled,
315
+ source : source_stats,
316
+ };
261
317
  });
262
- });
318
+ }
263
319
 
264
320
  /**
265
321
  * Sourcemap middleware:
@@ -807,7 +863,8 @@ Alchemy.setMethod(function findImagePath(image_path, callback) {
807
863
  */
808
864
  Alchemy.setMethod(function findAssetPath(assetFile, directories, callback) {
809
865
 
810
- var sub_path,
866
+ let found_asset_dir,
867
+ sub_path,
811
868
  found,
812
869
  iter;
813
870
 
@@ -888,6 +945,7 @@ Alchemy.setMethod(function findAssetPath(assetFile, directories, callback) {
888
945
  if (err == null && stats.isFile()) {
889
946
  stats.path = path;
890
947
  found = stats;
948
+ found_asset_dir = dir;
891
949
  }
892
950
 
893
951
  nextc();
@@ -910,6 +968,7 @@ Alchemy.setMethod(function findAssetPath(assetFile, directories, callback) {
910
968
  stats.servepath = cpath;
911
969
  stats.path = cpath;
912
970
  found = stats;
971
+ found_asset_dir = dir;
913
972
  }
914
973
 
915
974
  nextc();
@@ -927,6 +986,10 @@ Alchemy.setMethod(function findAssetPath(assetFile, directories, callback) {
927
986
  found = {};
928
987
  }
929
988
 
989
+ if (found_asset_dir) {
990
+ found.relative_path = libpath.relative(found_asset_dir, found.path);
991
+ }
992
+
930
993
  callback(null, found);
931
994
  });
932
995
  });
@@ -936,13 +999,19 @@ Alchemy.setMethod(function findAssetPath(assetFile, directories, callback) {
936
999
  *
937
1000
  * @author Jelle De Loecker <jelle@elevenways.be>
938
1001
  * @since 0.5.0
939
- * @version 1.0.5
1002
+ * @version 1.4.0
940
1003
  *
941
1004
  * @param {string} css
1005
+ * @param {Object} options
942
1006
  * @param {Function} callback
943
1007
  */
944
- function doPostCss(css, callback) {
1008
+ function doPostCss(css, options, callback) {
945
1009
  postcss([autoprefixer]).process(css, {from: undefined}).then(function gotCssResult(result) {
1010
+
1011
+ if (options?.post_css_only) {
1012
+ return callback(null, result);
1013
+ }
1014
+
946
1015
  callback(null, result.css);
947
1016
  }).catch(function gotError(err) {
948
1017
  callback(err);
@@ -958,14 +1027,16 @@ function doPostCss(css, callback) {
958
1027
  */
959
1028
  Alchemy.setMethod(function getCompiledLessPath(lessPath, options, callback) {
960
1029
 
1030
+ let check_cache = !options?.post_css_only && options?.cache !== false && alchemy.settings.performance.cache;
1031
+
961
1032
  // If it has been compiled before, return that
962
1033
  // @todo: check timestamps on dev. Not-founds are not cached
963
- if (alchemy.settings.performance.cache && fileCache[lessPath]) {
964
- return setImmediate(function lessCache(){callback(null, fileCache[lessPath])});
1034
+ if (check_cache && fileCache[lessPath]) {
1035
+ return setImmediate(() => callback(null, fileCache[lessPath]));
965
1036
  }
966
1037
 
967
1038
  if (!lessPath || typeof lessPath != 'string') {
968
- return setImmediate(function errCb(){callback(new Error('Invalid path given'))});
1039
+ return setImmediate(() => callback(new Error('Invalid path given')));
969
1040
  }
970
1041
 
971
1042
  fs.readFile(lessPath, {encoding: 'utf8'}, function gotLessCode(err, source) {
@@ -1005,6 +1076,10 @@ Alchemy.setMethod(function getCompiledLessPath(lessPath, options, callback) {
1005
1076
  // @todo: add minify options
1006
1077
  css = cssTree.css;
1007
1078
 
1079
+ if (options.post_css_only) {
1080
+ return doPostCss(css, options, callback);
1081
+ }
1082
+
1008
1083
  Blast.openTempFile({suffix: '.css'}, function getTempFile(err, info) {
1009
1084
 
1010
1085
  var cssBuffer;
@@ -1021,7 +1096,7 @@ Alchemy.setMethod(function getCompiledLessPath(lessPath, options, callback) {
1021
1096
  return callback(new Error('LESS file failed to compile'));
1022
1097
  }
1023
1098
 
1024
- doPostCss(css, function gotResult(err, css) {
1099
+ doPostCss(css, options, function gotResult(err, css) {
1025
1100
 
1026
1101
  if (err) {
1027
1102
  return callback(err);
@@ -1049,18 +1124,143 @@ Alchemy.setMethod(function getCompiledLessPath(lessPath, options, callback) {
1049
1124
  });
1050
1125
  });
1051
1126
 
1127
+ /**
1128
+ * Create the alchemy.scss content
1129
+ *
1130
+ * @author Jelle De Loecker <jelle@elevenways.be>
1131
+ * @since 1.4.0
1132
+ * @version 1.4.0
1133
+ */
1134
+ async function createAlchemyScss() {
1135
+
1136
+ let contents = '';
1137
+
1138
+ for (let css_name of required_stylesheets) {
1139
+ if (!css_name.endsWith('.scss') && !css_name.endsWith('.css') && !css_name.endsWith('.less')) {
1140
+ css_name += '.css';
1141
+ }
1142
+
1143
+ let result = await findStylesheet(css_name);
1144
+
1145
+ if (!result?.source?.relative_path) {
1146
+ throw new Error('Could not find ' + css_name);
1147
+ }
1148
+
1149
+ contents += '@use "' + result.source.relative_path + '";\n';
1150
+ }
1151
+
1152
+ return {contents};
1153
+ }
1154
+
1155
+ /**
1156
+ * Custom SCSS import logic
1157
+ *
1158
+ * @author Jelle De Loecker <jelle@elevenways.be>
1159
+ * @since 1.4.0
1160
+ * @version 1.4.0
1161
+ *
1162
+ * @param {string} path_to_import The requested path to import
1163
+ * @param {string} current_file_path The path to the current file
1164
+ */
1165
+ async function customScssImporter(path_to_import, current_file_path) {
1166
+
1167
+ // The alchemy.scss file is a special case
1168
+ if (path_to_import === 'alchemy.scss' || path_to_import === 'alchemy') {
1169
+ return createAlchemyScss();
1170
+ }
1171
+
1172
+ // Get the current directory this SCSS file is in
1173
+ let current_dir = libpath.dirname(current_file_path);
1174
+
1175
+ // Get the dirname to import
1176
+ let dir_to_import = libpath.dirname(path_to_import);
1177
+
1178
+ // Get the filename
1179
+ let filename_to_import = libpath.basename(path_to_import);
1180
+
1181
+ // Get all the style dirs
1182
+ let style_dirs = styleDirs.getSorted();
1183
+
1184
+ // Get the source style dir (the one this file is in)
1185
+ let source_style_dir;
1186
+
1187
+ for (let style_dir of style_dirs) {
1188
+ if (current_dir.startsWith(style_dir)) {
1189
+ source_style_dir = style_dir;
1190
+ break;
1191
+ }
1192
+ }
1193
+
1194
+ let include_paths = [
1195
+ current_dir,
1196
+ ...style_dirs,
1197
+ ];
1198
+
1199
+ // If the dir this file was in is from one of the allowed style dirs,
1200
+ // we have to add possible overrides to the include paths
1201
+ if (source_style_dir) {
1202
+ let extra_path = current_dir.after(source_style_dir);
1203
+
1204
+ if (extra_path.length > 2) {
1205
+ extra_path = libpath.resolve('/', extra_path);
1206
+ let new_path = libpath.join(PATH_APP, 'assets', 'stylesheets', extra_path);
1207
+ include_paths.unshift(new_path);
1208
+ }
1209
+ }
1210
+
1211
+ for (let include_path of include_paths) {
1212
+ let test_path = libpath.join(include_path, path_to_import);
1213
+
1214
+ let file = new Classes.Alchemy.Inode.File(test_path);
1215
+
1216
+ if (await file.exists()) {
1217
+ return {file: test_path};
1218
+ }
1219
+ }
1220
+
1221
+ // Look for files starting with an underscore
1222
+ if (filename_to_import[0] != '_') {
1223
+ let dashed_filename_to_import = '_' + filename_to_import;
1224
+ return customScssImporter(libpath.join(dir_to_import, dashed_filename_to_import), current_file_path);
1225
+ }
1226
+
1227
+ let extension = libpath.extname(filename_to_import);
1228
+
1229
+ // If no extension was given, look for that too
1230
+ if (!extension) {
1231
+ filename_to_import += '.scss';
1232
+ return customScssImporter(libpath.join(dir_to_import, filename_to_import), current_file_path);
1233
+ }
1234
+
1235
+ if (path_to_import.startsWith('/overrides/')) {
1236
+ return {contents: '// Failed to find ' + path_to_import};
1237
+ }
1238
+
1239
+ throw new Error('SCSS file not found');
1240
+ };
1241
+
1242
+ function logSassWarning(message, options) {
1243
+ console.warn('Sass warning:', message, options);
1244
+ }
1245
+
1246
+ function logSassDebug(message, options) {
1247
+ console.log('Sass debug:', message, options);
1248
+ }
1249
+
1052
1250
  /**
1053
1251
  * Callback with the compiled CSS filepath
1054
1252
  *
1055
1253
  * @author Jelle De Loecker <jelle@elevenways.be>
1056
1254
  * @since 0.2.0
1057
- * @version 1.2.5
1255
+ * @version 1.4.0
1058
1256
  */
1059
1257
  Alchemy.setMethod(function getCompiledSassPath(sassPath, options, callback) {
1060
1258
 
1259
+ let check_cache = !options?.post_css_only && options?.cache !== false && alchemy.settings.performance.cache;
1260
+
1061
1261
  // If it has been compiled before, return that
1062
1262
  // @todo: check timestamps on dev. Not-founds are not cached
1063
- if (alchemy.settings.performance.cache && fileCache[sassPath]) {
1263
+ if (check_cache && fileCache[sassPath]) {
1064
1264
  return setImmediate(function lessCache(){callback(null, fileCache[sassPath])});
1065
1265
  }
1066
1266
 
@@ -1079,8 +1279,13 @@ Alchemy.setMethod(function getCompiledSassPath(sassPath, options, callback) {
1079
1279
  }
1080
1280
 
1081
1281
  const render_options = {
1082
- includePaths : ['.'].concat(styleDirs.getSorted()).concat(libpath.dirname(sassPath)),
1282
+ includePaths : styleDirs.getSorted(),
1083
1283
  functions : sass_functions,
1284
+ importer : customScssImporter,
1285
+ logger : {
1286
+ warn : logSassWarning,
1287
+ debug : logSassDebug,
1288
+ },
1084
1289
  };
1085
1290
 
1086
1291
  if (alchemy.settings.debugging.debug) {
@@ -1104,6 +1309,11 @@ Alchemy.setMethod(function getCompiledSassPath(sassPath, options, callback) {
1104
1309
  });
1105
1310
 
1106
1311
  function writeToTemp(content) {
1312
+
1313
+ if (options?.post_css_only) {
1314
+ return doPostCss(content, options, callback);
1315
+ }
1316
+
1107
1317
  Blast.openTempFile({suffix: '.css'}, function getTempFile(err, info) {
1108
1318
 
1109
1319
  if (err) {
@@ -1118,7 +1328,7 @@ Alchemy.setMethod(function getCompiledSassPath(sassPath, options, callback) {
1118
1328
  return writeToFile(info, content);
1119
1329
  }
1120
1330
 
1121
- doPostCss(content, function gotPostCss(err, css) {
1331
+ doPostCss(content, options, function gotPostCss(err, css) {
1122
1332
 
1123
1333
  if (err) {
1124
1334
  return callback(err);
@@ -1147,7 +1357,121 @@ Alchemy.setMethod(function getCompiledSassPath(sassPath, options, callback) {
1147
1357
  callback(null, file);
1148
1358
  });
1149
1359
  });
1360
+ }
1361
+ });
1362
+ });
1363
+
1364
+ /**
1365
+ * Register a style that should always be included in the output
1366
+ *
1367
+ * @author Jelle De Loecker <jelle@elevenways.be>
1368
+ * @since 1.4.0
1369
+ * @version 1.4.0
1370
+ *
1371
+ * @param {string} css_path Could be a path, could be a name
1372
+ */
1373
+ Alchemy.setMethod(function registerRequiredStylesheet(css_path) {
1374
+ required_stylesheets.add(css_path);
1375
+ });
1376
+
1377
+ /**
1378
+ * Extract CSS exports from a PostCSS result object
1379
+ *
1380
+ * @author Jelle De Loecker <jelle@elevenways.be>
1381
+ * @since 1.4.0
1382
+ * @version 1.4.0
1383
+ *
1384
+ * @param {Object} postcss_result
1385
+ */
1386
+ function extractPostCSSExports(postcss_result) {
1387
+
1388
+ const icss_import_rx = /^:import\(("[^"]*"|'[^']*'|[^"']+)\)$/;
1389
+ const icss_balanced_quotes = /^("[^"]*"|'[^']*'|[^"']+)$/;
1390
+ const remove_rules = true;
1391
+ const mode = 'auto';
1392
+
1393
+ const getDeclsObject = (rule) => {
1394
+ const object = {};
1395
+
1396
+ rule.walkDecls((decl) => {
1397
+ const before = decl.raws.before ? decl.raws.before.trim() : "";
1398
+ object[before + decl.prop] = decl.value;
1399
+ });
1400
+
1401
+ return object;
1402
+ };
1403
+
1404
+ const icss_imports = {};
1405
+ const icss_exports = {};
1406
+
1407
+ function addImports(node, path) {
1408
+ const unquoted = path.replace(/'|"/g, "");
1409
+ icss_imports[unquoted] = Object.assign(
1410
+ icss_imports[unquoted] || {},
1411
+ getDeclsObject(node)
1412
+ );
1413
+
1414
+ if (remove_rules) {
1415
+ node.remove();
1416
+ }
1417
+ }
1150
1418
 
1419
+ function addExports(node) {
1420
+ Object.assign(icss_exports, getDeclsObject(node));
1421
+ if (remove_rules) {
1422
+ node.remove();
1423
+ }
1424
+ }
1425
+
1426
+ postcss_result.root.each((node) => {
1427
+ if (node.type === 'rule' && mode !== 'at-rule') {
1428
+ if (node.selector.slice(0, 7) === ':import') {
1429
+ const matches = icss_import_rx.exec(node.selector);
1430
+
1431
+ if (matches) {
1432
+ addImports(node, matches[1]);
1433
+ }
1434
+ }
1435
+
1436
+ if (node.selector === ':export') {
1437
+ addExports(node);
1438
+ }
1439
+ }
1440
+
1441
+ if (node.type === 'atrule' && mode !== 'rule') {
1442
+ if (node.name === 'icss-import') {
1443
+ const matches = icss_balanced_quotes.exec(node.params);
1444
+
1445
+ if (matches) {
1446
+ addImports(node, matches[1]);
1447
+ }
1448
+ }
1449
+ if (node.name === 'icss-export') {
1450
+ addExports(node);
1451
+ }
1151
1452
  }
1152
1453
  });
1454
+
1455
+ return {
1456
+ imports : icss_imports,
1457
+ exports : icss_exports,
1458
+ };
1459
+ }
1460
+
1461
+ /**
1462
+ * Extract CSS exports from a CSS file path
1463
+ *
1464
+ * @author Jelle De Loecker <jelle@elevenways.be>
1465
+ * @since 1.4.0
1466
+ * @version 1.4.0
1467
+ *
1468
+ * @param {string} css_path
1469
+ */
1470
+ Alchemy.setMethod(async function extractCSSExports(css_path) {
1471
+
1472
+ let result = await findStylesheet(css_path, {post_css_only: true, compile: true});
1473
+
1474
+ result = extractPostCSSExports(result);
1475
+
1476
+ return result?.exports;
1153
1477
  });
@@ -17,6 +17,21 @@ function DEFINE(name, value) {
17
17
  Object.defineProperty(globalThis, name, {value: value});
18
18
  }
19
19
 
20
+ /**
21
+ * Function to define global constants
22
+ * and have them inside Hawkejs views too
23
+ *
24
+ * @author Jelle De Loecker <jelle@elevenways.be>
25
+ * @since 1.4.0
26
+ * @version 1.4.0
27
+ *
28
+ * @type {Function}
29
+ */
30
+ function DEFINE_CLIENT(name, value) {
31
+ DEFINE(name, value);
32
+ Classes.Hawkejs.setGlobal(name, value);
33
+ }
34
+
20
35
  /**
21
36
  * Use DEFINE for itself
22
37
  *
@@ -37,7 +52,7 @@ DEFINE('DEFINE', DEFINE);
37
52
  *
38
53
  * @type {Informer}
39
54
  */
40
- DEFINE('Blast', __Protoblast);
55
+ DEFINE_CLIENT('Blast', __Protoblast);
41
56
 
42
57
  /**
43
58
  * All classes will be collected here
@@ -48,7 +63,7 @@ DEFINE('Blast', __Protoblast);
48
63
  *
49
64
  * @type {Object}
50
65
  */
51
- DEFINE('Classes', Blast.Classes);
66
+ DEFINE_CLIENT('Classes', Blast.Classes);
52
67
 
53
68
  /**
54
69
  * Available types
@@ -59,7 +74,7 @@ DEFINE('Classes', Blast.Classes);
59
74
  *
60
75
  * @type {Object}
61
76
  */
62
- DEFINE('Types', Blast.Types);
77
+ DEFINE_CLIENT('Types', Blast.Types);
63
78
 
64
79
  /**
65
80
  * The new Local Date/Time classes
@@ -68,9 +83,9 @@ DEFINE('Types', Blast.Types);
68
83
  * @since 1.3.20
69
84
  * @version 1.3.20
70
85
  */
71
- DEFINE('LocalDateTime', Classes.Develry.LocalDateTime);
72
- DEFINE('LocalDate', Classes.Develry.LocalDate);
73
- DEFINE('LocalTime', Classes.Develry.LocalTime);
86
+ DEFINE_CLIENT('LocalDateTime', Classes.Develry.LocalDateTime);
87
+ DEFINE_CLIENT('LocalDate', Classes.Develry.LocalDate);
88
+ DEFINE_CLIENT('LocalTime', Classes.Develry.LocalTime);
74
89
 
75
90
  /**
76
91
  * The new Decimal classes
@@ -79,10 +94,10 @@ DEFINE('LocalTime', Classes.Develry.LocalTime);
79
94
  * @since 1.3.20
80
95
  * @version 1.3.20
81
96
  */
82
- DEFINE('Decimal', Classes.Develry.Decimal);
83
- DEFINE('MutableDecimal', Classes.Develry.MutableDecimal);
84
- DEFINE('FixedDecimal', Classes.Develry.FixedDecimal);
85
- DEFINE('MutableFixedDecimal', Classes.Develry.MutableFixedDecimal);
97
+ DEFINE_CLIENT('Decimal', Classes.Develry.Decimal);
98
+ DEFINE_CLIENT('MutableDecimal', Classes.Develry.MutableDecimal);
99
+ DEFINE_CLIENT('FixedDecimal', Classes.Develry.FixedDecimal);
100
+ DEFINE_CLIENT('MutableFixedDecimal', Classes.Develry.MutableFixedDecimal);
86
101
 
87
102
  /**
88
103
  * The Trail class:
@@ -92,4 +107,4 @@ DEFINE('MutableFixedDecimal', Classes.Develry.MutableFixedDecimal);
92
107
  * @since 1.4.0
93
108
  * @version 1.4.0
94
109
  */
95
- DEFINE('Trail', Classes.Develry.Trail);
110
+ DEFINE_CLIENT('Trail', Classes.Develry.Trail);
@@ -49,7 +49,6 @@ alchemy.use('less', 'less');
49
49
  *
50
50
  * @link https://npmjs.org/package/hawkejs
51
51
  */
52
- alchemy.use('hawkejs', 'hawkejs');
53
52
  alchemy.hawkejs = Classes.Hawkejs.Hawkejs.getInstance();
54
53
 
55
54
  /**
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "alchemymvc",
3
3
  "description": "MVC framework for Node.js",
4
- "version": "1.4.0-alpha.2",
4
+ "version": "1.4.0-alpha.3",
5
5
  "author": "Jelle De Loecker <jelle@elevenways.be>",
6
6
  "keywords": [
7
7
  "alchemy",
@@ -46,8 +46,8 @@
46
46
  "optionalDependencies": {
47
47
  "janeway" : "~0.4.4",
48
48
  "less" : "~4.2.0",
49
- "sass" : "~1.68.0",
50
- "sass-embedded" : "~1.67.0",
49
+ "sass" : "~1.71.1",
50
+ "sass-embedded" : "~1.71.1",
51
51
  "nodent-compiler" : "~3.2.13",
52
52
  "socket.io-client" : "~4.7.2",
53
53
  "socket.io-msgpack-parser": "~3.0.2"