@nci-gis/js-tmpl 0.1.1 → 0.1.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nci-gis/js-tmpl",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "The pure JavaScript templating engine that uses handlebars.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -24,7 +24,7 @@ function placeInTree(tree, chain, value) {
24
24
  let cur = tree;
25
25
  for (let i = 0; i < chain.length - 1; i++) {
26
26
  const seg = chain[i];
27
- if (!(seg in cur)) {
27
+ if (!Object.hasOwn(cur, seg)) {
28
28
  cur[seg] = {};
29
29
  }
30
30
  cur = /** @type {Record<string, unknown>} */ (cur[seg]);
@@ -48,7 +48,7 @@ export function buildView(args = {}) {
48
48
  * @param {string} valuesDir
49
49
  */
50
50
  function assertReservedEnvNotInPartials(partials, valuesDir) {
51
- if (RESERVED_ENV in partials) {
51
+ if (Object.hasOwn(partials, RESERVED_ENV)) {
52
52
  throw new Error(
53
53
  `Value partial conflicts with reserved 'env' namespace.\n` +
54
54
  ` Source: ${valuesDir} produced a top-level 'env' namespace.\n` +
@@ -73,7 +73,7 @@ function assertNoRootNamespaceCollision(
73
73
  if (key === RESERVED_ENV) {
74
74
  continue;
75
75
  }
76
- if (key in partials) {
76
+ if (Object.hasOwn(partials, key)) {
77
77
  throw new Error(
78
78
  `Duplicate view key '${key}' — registered by both:\n` +
79
79
  ` - ${valuesFile} top-level key\n` +
@@ -87,7 +87,7 @@ function assertNoRootNamespaceCollision(
87
87
  * @param {Record<string, unknown>} values
88
88
  */
89
89
  function warnOnReservedEnvInValuesFile(values) {
90
- if (RESERVED_ENV in values) {
90
+ if (Object.hasOwn(values, RESERVED_ENV)) {
91
91
  console.warn(
92
92
  'Warning: "env" is a reserved key in js-tmpl and will be overwritten.\n' +
93
93
  'Rename the "env" key in your values file to avoid this.',
@@ -109,7 +109,7 @@ export function pickEnv({ keys = [], prefix = '' }, source = process.env) {
109
109
  const result = {};
110
110
 
111
111
  for (const k of keys) {
112
- if (k in source) {
112
+ if (Object.hasOwn(source, k)) {
113
113
  result[k] = /** @type {string} */ (source[k]);
114
114
  }
115
115
  }
@@ -27,6 +27,10 @@ function shouldSkipSubtree(rel, view) {
27
27
  * prune the subtree before any filesystem descent (early-exit — no
28
28
  * `stat`/`readdir` on skipped paths).
29
29
  *
30
+ * Symbolic links are followed. A directory that resolves to one of its own
31
+ * ancestors throws instead of looping; the same directory linked from two
32
+ * places (no cycle) is walked twice.
33
+ *
30
34
  * @param {string} rootDir
31
35
  * @param {(string | { ext?: string, view?: Record<string, unknown> })} [optsOrExt]
32
36
  * Options object, or a bare `ext` string for back-compat.
@@ -40,10 +44,13 @@ export async function walkTemplateTree(rootDir, optsOrExt) {
40
44
 
41
45
  /** @type {import('../types.js').TemplateFile[]} */
42
46
  const results = [];
43
- const queue = [''];
47
+ /** @type {Array<{ rel: string, ancestors: Array<{ rel: string, real: string }> }>} */
48
+ const queue = [{ rel: '', ancestors: [] }];
44
49
 
45
50
  while (queue.length) {
46
- const rel = /** @type {string} */ (queue.shift());
51
+ const { rel, ancestors } = /** @type {(typeof queue)[number]} */ (
52
+ queue.shift()
53
+ );
47
54
  const abs = path.join(rootDir, rel);
48
55
  const stat = await fs.stat(abs);
49
56
 
@@ -51,9 +58,21 @@ export async function walkTemplateTree(rootDir, optsOrExt) {
51
58
  if (shouldSkipSubtree(rel, view)) {
52
59
  continue;
53
60
  }
61
+ const real = await fs.realpath(abs);
62
+ const loop = ancestors.find((a) => a.real === real);
63
+ if (loop) {
64
+ throw new Error(
65
+ `Template directory '${rel}' links back to '${loop.rel || '.'}' (a symbolic link cycle in '${rootDir}').\n` +
66
+ 'Remove the link, or point it outside its own parent directories.',
67
+ );
68
+ }
69
+ const chain = [...ancestors, { rel, real }];
54
70
  const items = (await fs.readdir(abs)).sort();
55
71
  for (const name of items) {
56
- queue.push(rel ? path.join(rel, name) : name);
72
+ queue.push({
73
+ rel: rel ? path.join(rel, name) : name,
74
+ ancestors: chain,
75
+ });
57
76
  }
58
77
  } else if (path.extname(abs) === ext) {
59
78
  results.push({ absPath: abs, relPath: rel });
@@ -21,6 +21,11 @@ const FLATTEN_SEGMENT_RE = /^@\w+$/;
21
21
  * @param {string} [label='namespace'] - Noun used in the error message (e.g. "partial name").
22
22
  */
23
23
  export function assertValidSegments(segments, filePath, label = 'namespace') {
24
+ if (segments.includes('__proto__')) {
25
+ throw new Error(
26
+ `Invalid ${label} segment '__proto__' in ${filePath} — reserved name`,
27
+ );
28
+ }
24
29
  if (!SEGMENT_RE.test(segments.join(''))) {
25
30
  throw new Error(
26
31
  `Invalid ${label} segment '${segments.join('>')}' in ${filePath} — only alphanumeric and underscore allowed`,