@rhinostone/swig-core 2.7.3 → 2.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/cache.js CHANGED
@@ -43,7 +43,10 @@ exports.cacheGet = function (key, options, engineCache, memoryStore) {
43
43
  }
44
44
 
45
45
  if (engineCache === 'memory') {
46
- return memoryStore[key];
46
+ // The store is a plain object, so an identifier-style loader that returns
47
+ // bare names (the loader contract allows it) could otherwise read an
48
+ // inherited member such as "constructor" as a cache hit.
49
+ return Object.prototype.hasOwnProperty.call(memoryStore, key) ? memoryStore[key] : undefined;
47
50
  }
48
51
 
49
52
  return engineCache.get(key);
package/lib/engine.js CHANGED
@@ -191,7 +191,15 @@ exports.getParents = function (tokens, options, deps) {
191
191
 
192
192
  parentFile = parentFile || options.filename;
193
193
  parentFile = deps.resolve(parentName, parentFile);
194
- parent = deps.cacheGet(parentFile, options) || deps.parseFile(parentFile, utils.extend({}, options, { filename: parentFile }));
194
+ // Only a compile-cached entry carries the parse tree (compile extends the
195
+ // wrapper with the token properties); a registered template function does
196
+ // not. Falling through to parseFile keeps a registration from shadowing a
197
+ // loadable parent, which would otherwise remap blocks against an undefined
198
+ // token list and render an empty string with no error.
199
+ parent = deps.cacheGet(parentFile, options);
200
+ if (!parent || !parent.tokens) {
201
+ parent = deps.parseFile(parentFile, utils.extend({}, options, { filename: parentFile }));
202
+ }
195
203
  parentName = parent.parent;
196
204
 
197
205
  if (parentFiles.indexOf(parentFile) !== -1) {
@@ -322,6 +330,111 @@ exports.install = function (self, frontend) {
322
330
  }
323
331
  };
324
332
 
333
+ /*!
334
+ * Wrap a pre-compiled template function in the locals-binding call shape
335
+ * cached entries have — include-emitted code invokes them as
336
+ * `(context)`, so the raw five-argument function is the wrong shape.
337
+ * @private
338
+ */
339
+ function buildRegistered(fn) {
340
+ var context = getLocals(),
341
+ contextLength = utils.keys(context).length;
342
+
343
+ return function registered(locals, blocks) {
344
+ var lcls;
345
+ if (locals && contextLength) {
346
+ lcls = utils.extend({}, context, locals);
347
+ } else if (locals && !contextLength) {
348
+ lcls = locals;
349
+ } else if (!locals && contextLength) {
350
+ lcls = context;
351
+ } else {
352
+ lcls = {};
353
+ }
354
+ return fn(self, lcls, filters, utils, efn, blocks);
355
+ };
356
+ }
357
+
358
+ /*!
359
+ * Shared argument checks for register / registerBundle, so a bundle can be
360
+ * validated in full before any entry is committed.
361
+ * @private
362
+ */
363
+ function validateRegistration(path, fn) {
364
+ if (typeof path !== 'string' || !path.length) {
365
+ throw new Error('Template registration path must be a non-empty string.');
366
+ }
367
+ if (typeof fn !== 'function') {
368
+ throw new Error('Registered template for "' + path + '" is not a function. Pass a pre-compiled template function.');
369
+ }
370
+ if (!self.options.cache) {
371
+ throw new Error('Cannot register templates while caching is disabled (cache: ' + self.options.cache + '). The registry is the compile cache.');
372
+ }
373
+ }
374
+
375
+ /**
376
+ * Register a pre-compiled template function under a template path so
377
+ * later `include` lookups and `compileFile` / `renderFile` calls are
378
+ * served from the template cache without compiling. The path is
379
+ * normalized through the active loader's `resolve` so a registration
380
+ * and an include-site lookup agree on the cache key.
381
+ *
382
+ * The registered entry is stored as a locals-binding wrapper with the
383
+ * same call contract as `compile`'s cached closure — `include`-emitted
384
+ * code invokes cached entries as `(context)`, so caching the raw
385
+ * five-argument template function (what `run`'s priming side effect
386
+ * stores) breaks any partial that touches `_ctx` / `_filters`. The
387
+ * wrapper closes over this instance's filters and default locals,
388
+ * mirroring `compiled(locals, blocks)` below.
389
+ *
390
+ * Throws when caching is disabled on the instance (any falsy `cache`) —
391
+ * the registry IS the compile cache, so nothing could retain the
392
+ * registration. `invalidateCache()` therefore also drops every
393
+ * registration made against the built-in memory cache.
394
+ *
395
+ * A registered entry cannot serve as an `{% extends %}` parent: it carries
396
+ * no parse tree, so the parent's source must still be reachable through
397
+ * the loader.
398
+ *
399
+ * @param {string} path Template path to register under.
400
+ * @param {function} fn Pre-compiled template function (CLI/AOT output).
401
+ * @return {object} The instance, for chaining.
402
+ */
403
+ self.register = function (path, fn) {
404
+ validateRegistration(path, fn);
405
+
406
+ cacheSet(self.options.loader.resolve(path), {}, buildRegistered(fn));
407
+ return self;
408
+ };
409
+
410
+ /**
411
+ * Register a map of pre-compiled template functions — the shape the
412
+ * CLI's `--recursive --register` bundle produces.
413
+ *
414
+ * Every entry is validated before any is committed, so a malformed bundle
415
+ * leaves the registry untouched instead of half-populated.
416
+ *
417
+ * @param {object} map `{ path: templateFn }` pairs.
418
+ * @return {object} The instance, for chaining.
419
+ */
420
+ self.registerBundle = function (map) {
421
+ var entries = [];
422
+
423
+ if (!map || typeof map !== 'object') {
424
+ throw new Error('Template bundle must be an object of { path: templateFn } pairs.');
425
+ }
426
+
427
+ utils.each(map, function (fn, path) {
428
+ entries.push([path, fn]);
429
+ validateRegistration(path, fn);
430
+ });
431
+
432
+ utils.each(entries, function (entry) {
433
+ self.register(entry[0], entry[1]);
434
+ });
435
+ return self;
436
+ };
437
+
325
438
  self.setFilter = function (name, method) {
326
439
  if (typeof method !== "function") {
327
440
  throw new Error('Filter "' + name + '" is not a valid function.');
@@ -634,7 +747,10 @@ exports.install = function (self, frontend) {
634
747
  self.run = function (tpl, locals, filepath) {
635
748
  var context = getLocals({ locals: locals });
636
749
  if (filepath) {
637
- cacheSet(filepath, {}, tpl);
750
+ // Cache the same wrapper `register` stores, under the same resolved
751
+ // key, so a template primed this way is actually usable by a later
752
+ // include instead of crashing on the raw function's argument shape.
753
+ cacheSet(self.options.loader.resolve(filepath), {}, buildRegistered(tpl));
638
754
  }
639
755
  return tpl(self, context, filters, utils, efn);
640
756
  };
@@ -28,6 +28,14 @@ module.exports = function (mapping, basepath) {
28
28
  */
29
29
  ret.resolve = function (to, from) {
30
30
  if (basepath) {
31
+ // A basepath ignores "from", so a climbing path can never mean what it
32
+ // says: path.resolve either walks out of the root or, when the root is
33
+ // "/", silently clamps and returns a different template. Reject it the
34
+ // way the filesystem loader rejects an escaping path, rather than
35
+ // resolving to something the caller did not ask for.
36
+ if ((/^\.\.([\\/]|$)/).test(path.normalize(to))) {
37
+ utils.throwError('Template "' + to + '" resolves outside the loader root "' + basepath + '".');
38
+ }
31
39
  from = basepath;
32
40
  } else {
33
41
  from = (from) ? path.dirname(from) : '/';
package/lib/runtime.js ADDED
@@ -0,0 +1,327 @@
1
+ var utils = require('./utils'),
2
+ cache = require('./cache'),
3
+ memoryLoader = require('./loaders/memory');
4
+
5
+ /**
6
+ * Runtime-only engine factory for the @rhinostone/swig family.
7
+ *
8
+ * Builds an instance that EXECUTES pre-compiled templates against a
9
+ * registration cache and contains no lexer, parser, or code generator —
10
+ * a browser bundle built from this module ships no `new Function`, so
11
+ * pages under a strict Content-Security-Policy (no `unsafe-eval`) can
12
+ * render AOT-compiled templates. Every compiling entry point is a stub
13
+ * that throws a clear error pointing at the CLI-plus-register workflow.
14
+ *
15
+ * Mirrors the relevant engine.js contracts:
16
+ * - `register` stores the same locals-binding wrapper shape that
17
+ * `compile`'s cached closure has (what include-emitted code calls).
18
+ * - `compileFile` resolves through the active loader, then serves the
19
+ * cache; a miss throws instead of falling through to a compiler.
20
+ * - `run(tpl, locals, filepath)` matches engine semantics.
21
+ *
22
+ * The default loader is a memory loader rooted at "/", so registration
23
+ * keys and root-relative include lookups both flatten to the exact keys a
24
+ * `swig compile --recursive --register` bundle carries. A basepath ignores
25
+ * the resolving-from path, so an include inside a bundled partial must name
26
+ * its target from the bundle root; a climbing path is rejected rather than
27
+ * silently resolved to a different template.
28
+ */
29
+
30
+ /*!
31
+ * Empty function used as a fallback in compiled template code.
32
+ */
33
+ function efn() { return ''; }
34
+
35
+ /*!
36
+ * Build the error every unavailable compiling entry point throws.
37
+ */
38
+ function noCompiler(what) {
39
+ return new Error(what + ' — the runtime-only build has no compiler. Precompile with the swig CLI (swig compile --recursive --register) and register the output, or load the full swig bundle instead.');
40
+ }
41
+
42
+ /**
43
+ * Create a runtime-only instance.
44
+ *
45
+ * @param {object} frontend Per-flavor wiring.
46
+ * @param {object} frontend.filters The flavor's filter catalog.
47
+ * @param {object} [frontend.defaults] Option overrides (locals, cache, loader).
48
+ * @return {object} The runtime instance.
49
+ */
50
+ exports.create = function (frontend) {
51
+ var filters = utils.extend({}, frontend.filters),
52
+ self = {};
53
+
54
+ function validateRuntimeOptions(options) {
55
+ if (!options) {
56
+ return;
57
+ }
58
+ if (options.hasOwnProperty('loader') && options.loader) {
59
+ if (!options.loader.load || !options.loader.resolve) {
60
+ throw new Error('Invalid loader option ' + JSON.stringify(options.loader) + ' found. Expected { load: function (pathname, cb) { ... }, resolve: function (to, from) { ... } }.');
61
+ }
62
+ }
63
+ if (options.hasOwnProperty('cache') && options.cache && options.cache !== 'memory') {
64
+ if (!options.cache.get || !options.cache.set) {
65
+ throw new Error('Invalid cache option ' + JSON.stringify(options.cache) + ' found. Expected "memory" or { get: function (key) { ... }, set: function (key, value) { ... } }.');
66
+ }
67
+ }
68
+ }
69
+
70
+ validateRuntimeOptions(frontend.defaults);
71
+
72
+ self.options = utils.extend({
73
+ autoescape: true,
74
+ locals: {},
75
+ cache: 'memory',
76
+ loader: memoryLoader({}, '/')
77
+ }, frontend.defaults || {});
78
+
79
+ self.cache = {};
80
+ self.extensions = {};
81
+
82
+ function getLocals(options) {
83
+ if (!options || !options.locals) {
84
+ return self.options.locals;
85
+ }
86
+ return utils.extend({}, self.options.locals, options.locals);
87
+ }
88
+
89
+ function cacheGet(key, options) {
90
+ return cache.cacheGet(key, options, self.options.cache, self.cache);
91
+ }
92
+
93
+ function cacheSet(key, options, val) {
94
+ cache.cacheSet(key, options, val, self.options.cache, self.cache);
95
+ }
96
+
97
+ /**
98
+ * Merge options into this instance. Compile-time options are not
99
+ * accepted — the runtime build has no parser to configure.
100
+ *
101
+ * @example
102
+ * rt.setDefaults({ locals: { site: 'Acme' } });
103
+ *
104
+ * @param {object} options Option overrides (locals, cache, loader).
105
+ * @return {undefined}
106
+ */
107
+ self.setDefaults = function (options) {
108
+ validateRuntimeOptions(options);
109
+ self.options = utils.extend(self.options, options || {});
110
+ };
111
+
112
+ /**
113
+ * Empty the template cache. Because the cache IS the registry, this
114
+ * also drops every registration made against the built-in memory cache.
115
+ *
116
+ * @example
117
+ * rt.invalidateCache();
118
+ *
119
+ * @return {undefined}
120
+ */
121
+ self.invalidateCache = function () {
122
+ if (self.options.cache === 'memory') {
123
+ self.cache = {};
124
+ }
125
+ };
126
+
127
+ /**
128
+ * Add or replace a filter available to registered templates. Filters
129
+ * bind at render time, so this affects templates registered earlier.
130
+ *
131
+ * @example
132
+ * rt.setFilter('shout', function (input) { return input + '!'; });
133
+ *
134
+ * @param {string} name Filter name as used in templates.
135
+ * @param {function} method Filter implementation.
136
+ * @return {undefined}
137
+ */
138
+ self.setFilter = function (name, method) {
139
+ if (typeof method !== "function") {
140
+ throw new Error('Filter "' + name + '" is not a valid function.');
141
+ }
142
+ filters[name] = method;
143
+ };
144
+
145
+ /**
146
+ * Expose an object to compiled template code as `_ext[name]`.
147
+ *
148
+ * @example
149
+ * rt.setExtension('translate', function (key) { return dictionary[key]; });
150
+ *
151
+ * @param {string} name Name the compiled code refers to.
152
+ * @param {*} object Value to expose.
153
+ * @return {undefined}
154
+ */
155
+ self.setExtension = function (name, object) {
156
+ self.extensions[name] = object;
157
+ };
158
+
159
+ /*!
160
+ * Wrap a pre-compiled template function in the locals-binding call shape
161
+ * cached entries have, mirroring the engine.
162
+ * @private
163
+ */
164
+ function buildRegistered(fn) {
165
+ var context = getLocals(),
166
+ contextLength = utils.keys(context).length;
167
+
168
+ return function registered(locals, blocks) {
169
+ var lcls;
170
+ if (locals && contextLength) {
171
+ lcls = utils.extend({}, context, locals);
172
+ } else if (locals && !contextLength) {
173
+ lcls = locals;
174
+ } else if (!locals && contextLength) {
175
+ lcls = context;
176
+ } else {
177
+ lcls = {};
178
+ }
179
+ return fn(self, lcls, filters, utils, efn, blocks);
180
+ };
181
+ }
182
+
183
+ /*!
184
+ * Shared argument checks for register / registerBundle, so a bundle is
185
+ * validated in full before any entry is committed.
186
+ * @private
187
+ */
188
+ function validateRegistration(path, fn) {
189
+ if (typeof path !== 'string' || !path.length) {
190
+ throw new Error('Template registration path must be a non-empty string.');
191
+ }
192
+ if (typeof fn !== 'function') {
193
+ throw new Error('Registered template for "' + path + '" is not a function. Pass a pre-compiled template function.');
194
+ }
195
+ if (!self.options.cache) {
196
+ throw new Error('Cannot register templates while caching is disabled (cache: ' + self.options.cache + '). The registry is the compile cache.');
197
+ }
198
+ }
199
+
200
+ /**
201
+ * Register a pre-compiled template function under a template path.
202
+ * Mirrors the engine.js register contract — see that JSDoc for the
203
+ * key-normalization and wrapper-shape rationale.
204
+ *
205
+ * @param {string} path Template path to register under.
206
+ * @param {function} fn Pre-compiled template function (CLI/AOT output).
207
+ * @return {object} The instance, for chaining.
208
+ */
209
+ self.register = function (path, fn) {
210
+ validateRegistration(path, fn);
211
+ cacheSet(self.options.loader.resolve(path), {}, buildRegistered(fn));
212
+ return self;
213
+ };
214
+
215
+ /**
216
+ * Register a map of pre-compiled template functions — the shape the
217
+ * CLI's `--recursive --register` bundle produces.
218
+ *
219
+ * @param {object} map `{ path: templateFn }` pairs.
220
+ * @return {object} The instance, for chaining.
221
+ */
222
+ self.registerBundle = function (map) {
223
+ var entries = [];
224
+
225
+ if (!map || typeof map !== 'object') {
226
+ throw new Error('Template bundle must be an object of { path: templateFn } pairs.');
227
+ }
228
+
229
+ utils.each(map, function (fn, path) {
230
+ entries.push([path, fn]);
231
+ validateRegistration(path, fn);
232
+ });
233
+
234
+ utils.each(entries, function (entry) {
235
+ self.register(entry[0], entry[1]);
236
+ });
237
+ return self;
238
+ };
239
+
240
+ /**
241
+ * Run a pre-compiled template function against locals. Passing a
242
+ * <var>filepath</var> also registers it, so later `include` lookups and
243
+ * `compileFile` calls resolve to it. Prefer `register` when no render is
244
+ * wanted.
245
+ *
246
+ * @example
247
+ * rt.run(templateFn, { name: 'Jane' }, 'page.html');
248
+ *
249
+ * @param {function} tpl Pre-compiled template function.
250
+ * @param {object} [locals] Template context.
251
+ * @param {string} [filepath] Path to register the template under.
252
+ * @return {string} Rendered output.
253
+ */
254
+ self.run = function (tpl, locals, filepath) {
255
+ var context = getLocals({ locals: locals });
256
+ if (filepath) {
257
+ cacheSet(self.options.loader.resolve(filepath), {}, buildRegistered(tpl));
258
+ }
259
+ return tpl(self, context, filters, utils, efn);
260
+ };
261
+
262
+ self.compileFile = function (pathname, options, cb) {
263
+ var resolved, cached, err;
264
+
265
+ if (!options) {
266
+ options = {};
267
+ }
268
+
269
+ resolved = self.options.loader.resolve(pathname, options.resolveFrom);
270
+ cached = cacheGet(resolved, options);
271
+
272
+ if (cached) {
273
+ if (cb) {
274
+ cb(null, cached);
275
+ return;
276
+ }
277
+ return cached;
278
+ }
279
+
280
+ err = noCompiler('Template "' + pathname + '" is not registered');
281
+ if (cb) {
282
+ cb(err);
283
+ return;
284
+ }
285
+ throw err;
286
+ };
287
+
288
+ self.renderFile = function (pathName, locals, cb) {
289
+ if (cb) {
290
+ self.compileFile(pathName, {}, function (err, fn) {
291
+ var result;
292
+
293
+ if (err) {
294
+ cb(err);
295
+ return;
296
+ }
297
+
298
+ try {
299
+ result = fn(locals);
300
+ } catch (err2) {
301
+ cb(err2);
302
+ return;
303
+ }
304
+
305
+ cb(null, result);
306
+ });
307
+ return;
308
+ }
309
+
310
+ return self.compileFile(pathName)(locals);
311
+ };
312
+
313
+ self.getTemplate = function (pathname) {
314
+ return Promise.reject(noCompiler('Async template loading of "' + pathname + '" is unavailable'));
315
+ };
316
+
317
+ self.parse = function () { throw noCompiler('parse() is unavailable'); };
318
+ self.parseFile = function () { throw noCompiler('parseFile() is unavailable'); };
319
+ self.precompile = function () { throw noCompiler('precompile() is unavailable'); };
320
+ self.compile = function () { throw noCompiler('compile() is unavailable'); };
321
+ self.render = function () { throw noCompiler('render(source) is unavailable'); };
322
+ self.setTag = function () { throw noCompiler('setTag() is parse-time machinery and is unavailable'); };
323
+ self.renderFileAsync = function () { throw noCompiler('renderFileAsync() is unavailable'); };
324
+ self.compileFileAsync = function () { throw noCompiler('compileFileAsync() is unavailable'); };
325
+
326
+ return self;
327
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rhinostone/swig-core",
3
- "version": "2.7.3",
3
+ "version": "2.8.1",
4
4
  "description": "Shared IR, backend, and runtime for the @rhinostone/swig family of template engines.",
5
5
  "keywords": [
6
6
  "template",