@znemz/cfn-include 2.1.19 → 2.1.21

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/index.js CHANGED
@@ -1,8 +1,7 @@
1
1
  const url = require('url');
2
2
  const path = require('path');
3
- const { readFile } = require('fs/promises');
4
3
  const _ = require('lodash');
5
- const { globSync } = require('glob');
4
+ const { glob } = require('glob');
6
5
  const Promise = require('bluebird');
7
6
  const sortObject = require('@znemz/sort-object');
8
7
  const { S3Client, GetObjectCommand } = require('@aws-sdk/client-s3');
@@ -26,6 +25,8 @@ const replaceEnv = require('./lib/replaceEnv');
26
25
  const { lowerCamelCase, upperCamelCase } = require('./lib/utils');
27
26
  const { isOurExplicitFunction } = require('./lib/schema');
28
27
  const { getAwsPseudoParameters, buildResourceArn } = require('./lib/internals');
28
+ const { cachedReadFile } = require('./lib/cache');
29
+ const { createChildScope } = require('./lib/scope');
29
30
 
30
31
  /**
31
32
  * @param {object} options
@@ -60,7 +61,7 @@ module.exports = async function (options) {
60
61
  const base = parseLocation(options.url);
61
62
  const scope = options.scope || {};
62
63
  if (base.relative) throw new Error('url cannot be relative');
63
- template = _.isUndefined(template)
64
+ template = !template
64
65
  ? fnInclude({ base, scope, cft: options.url, ...options })
65
66
  : template;
66
67
  // Resolve template if it's a promise to extract the root template for reference lookups
@@ -109,8 +110,9 @@ async function recurse({ base, scope, cft, rootTemplate, caller, ...opts }) {
109
110
  if (opts.doLog) {
110
111
  console.log({ base, scope, cft, rootTemplate, caller, ...opts });
111
112
  }
112
- scope = _.clone(scope);
113
- if (_.isArray(cft)) {
113
+ // Use Object.create() for O(1) child scope creation instead of O(n) clone
114
+ scope = createChildScope(scope);
115
+ if (Array.isArray(cft)) {
114
116
  return Promise.all(cft.map((o) => recurse({ base, scope, cft: o, rootTemplate, caller: 'recurse:isArray', ...opts })));
115
117
  }
116
118
  if (_.isPlainObject(cft)) {
@@ -137,13 +139,14 @@ async function recurse({ base, scope, cft, rootTemplate, caller, ...opts }) {
137
139
  placeholder = '_';
138
140
  }
139
141
  return PromiseExt.mapX(recurse({ base, scope, cft: list, rootTemplate, caller: 'Fn::Map', ...opts }), (replace, key) => {
140
- scope = _.clone(scope);
141
- scope[placeholder] = replace;
142
+ // Use Object.create() for O(1) child scope creation instead of O(n) clone
143
+ const additions = { [placeholder]: replace };
142
144
  if (hasindex) {
143
- scope[idx] = key;
145
+ additions[idx] = key;
144
146
  }
145
- const replaced = findAndReplace(scope, _.cloneDeep(body));
146
- return recurse({ base, scope, cft: replaced, rootTemplate, caller: 'Fn::Map', ...opts });
147
+ const childScope = createChildScope(scope, additions);
148
+ const replaced = findAndReplace(childScope, _.cloneDeep(body));
149
+ return recurse({ base, scope: childScope, cft: replaced, rootTemplate, caller: 'Fn::Map', ...opts });
147
150
  }).then((_cft) => {
148
151
  if (hassize) {
149
152
  _cft = findAndReplace({ [sz]: _cft.length }, _cft);
@@ -175,24 +178,24 @@ async function recurse({ base, scope, cft, rootTemplate, caller, ...opts }) {
175
178
  }
176
179
  if (cft['Fn::Flatten']) {
177
180
  return recurse({ base, scope, cft: cft['Fn::Flatten'], rootTemplate, caller: 'Fn::Flatten', ...opts }).then(function (json) {
178
- return _.flatten(json);
181
+ return json.flat();
179
182
  });
180
183
  }
181
184
  if (cft['Fn::FlattenDeep']) {
182
185
  return recurse({ base, scope, cft: cft['Fn::FlattenDeep'], rootTemplate, caller: 'Fn::FlattenDeep', ...opts }).then(
183
186
  function (json) {
184
- return _.flattenDeep(json);
187
+ return json.flat(Infinity);
185
188
  },
186
189
  );
187
190
  }
188
191
  if (cft['Fn::Uniq']) {
189
192
  return recurse({ base, scope, cft: cft['Fn::Uniq'], rootTemplate, caller: 'Fn::Uniq', ...opts }).then(function (json) {
190
- return _.uniq(json);
193
+ return [...new Set(json)];
191
194
  });
192
195
  }
193
196
  if (cft['Fn::Compact']) {
194
197
  return recurse({ base, scope, cft: cft['Fn::Compact'], rootTemplate, caller: 'Fn::Compact', ...opts }).then(function (json) {
195
- return _.compact(json);
198
+ return json.filter(Boolean);
196
199
  });
197
200
  }
198
201
  if (cft['Fn::Concat']) {
@@ -269,7 +272,7 @@ async function recurse({ base, scope, cft, rootTemplate, caller, ...opts }) {
269
272
  }
270
273
  if (cft['Fn::Filenames']) {
271
274
  return recurse({ base, scope, cft: cft['Fn::Filenames'], rootTemplate, caller: 'Fn::Filenames', ...opts }).then(
272
- function (json) {
275
+ async function (json) {
273
276
  json = _.isPlainObject(json) ? { ...json } : { location: json };
274
277
  if (json.doLog) {
275
278
 
@@ -284,7 +287,7 @@ async function recurse({ base, scope, cft, rootTemplate, caller, ...opts }) {
284
287
  const absolute = location.relative
285
288
  ? path.join(path.dirname(base.path), location.host, location.path || '')
286
289
  : [location.host, location.path].join('');
287
- const globs = globSync(absolute).sort();
290
+ const globs = (await glob(absolute)).sort();
288
291
  if (json.omitExtension) {
289
292
  return globs.map((f) => path.basename(f, path.extname(f)));
290
293
  }
@@ -587,35 +590,38 @@ async function recurse({ base, scope, cft, rootTemplate, caller, ...opts }) {
587
590
  );
588
591
  }
589
592
 
590
- if (_.isUndefined(cft)) {
593
+ if (cft === undefined) {
591
594
  return null;
592
595
  }
593
596
  return replaceEnv(cft, opts.inject, opts.doEnv);
594
597
  }
595
598
 
596
599
  function findAndReplace(scope, object) {
597
- if (_.isString(object)) {
598
- _.forEach(scope, function (replace, find) {
600
+ if (typeof object === 'string') {
601
+ // Use for...in to walk prototype chain (Object.create() based scopes)
602
+ for (const find in scope) {
599
603
  if (object === find) {
600
- object = replace;
604
+ object = scope[find];
601
605
  }
602
- });
606
+ }
603
607
  }
604
- if (_.isString(object)) {
605
- _.forEach(scope, function (replace, find) {
608
+ if (typeof object === 'string') {
609
+ // Use for...in to walk prototype chain (Object.create() based scopes)
610
+ for (const find in scope) {
611
+ const replace = scope[find];
606
612
  const regex = new RegExp(`\\\${${find}}`, 'g');
607
613
  if (find !== '_' && object.match(regex)) {
608
614
  object = object.replace(regex, replace);
609
615
  }
610
- });
616
+ }
611
617
  }
612
- if (_.isArray(object)) {
618
+ if (Array.isArray(object)) {
613
619
  object = object.map(_.bind(findAndReplace, this, scope));
614
620
  } else if (_.isPlainObject(object)) {
615
621
  object = _.mapKeys(object, function (value, key) {
616
622
  return findAndReplace(scope, key);
617
623
  });
618
- _.keys(object).forEach(function (key) {
624
+ Object.keys(object).forEach(function (key) {
619
625
  if (key === 'Fn::Map') return;
620
626
  object[key] = findAndReplace(scope, object[key]);
621
627
  });
@@ -632,7 +638,7 @@ function interpolate(lines, context) {
632
638
  const match = _line.match(/^{{(\w+)}}$/);
633
639
  const value = match ? context[match[1]] : undefined;
634
640
  if (!match) return _line;
635
- if (_.isUndefined(value)) {
641
+ if (value === undefined) {
636
642
  return '';
637
643
  }
638
644
  return value;
@@ -661,7 +667,7 @@ function fnIncludeOptsFromArray(cft, opts) {
661
667
  function fnIncludeOpts(cft, opts) {
662
668
  if (_.isPlainObject(cft)) {
663
669
  cft = _.merge(cft, _.cloneDeep(opts));
664
- } else if (_.isArray(cft)) {
670
+ } else if (Array.isArray(cft)) {
665
671
  cft = fnIncludeOptsFromArray(cft, opts);
666
672
  } else {
667
673
  // should be string{
@@ -730,11 +736,11 @@ async function fnInclude({ base, scope, cft, ...opts }) {
730
736
 
731
737
  handleInjectSetup();
732
738
  if (isGlob(cft, absolute)) {
733
- const paths = globSync(absolute).sort();
739
+ const paths = (await glob(absolute)).sort();
734
740
  const template = yaml.load(paths.map((_p) => `- Fn::Include: file://${_p}`).join('\n'));
735
741
  return recurse({ base, scope, cft: template, rootTemplate: template, ...opts });
736
742
  }
737
- body = readFile(absolute).then(String).then(procTemplate);
743
+ body = cachedReadFile(absolute).then(procTemplate);
738
744
  absolute = `${location.protocol}://${absolute}`;
739
745
  } else if (location.protocol === 's3') {
740
746
  const basedir = pathParse(base.path).dir;
@@ -829,7 +835,7 @@ async function handleIncludeBody({ scope, args, body, absolute }) {
829
835
  return temp;
830
836
  }
831
837
  // once fully recursed we can query the resultant template
832
- const query = _.isString(args.query)
838
+ const query = typeof args.query === 'string'
833
839
  ? replaceEnv(args.query, args.inject, args.doEnv)
834
840
  : await recurse({
835
841
  base: parseLocation(absolute),
@@ -858,7 +864,7 @@ async function handleIncludeBody({ scope, args, body, absolute }) {
858
864
  lines = interpolate(lines, args.context);
859
865
  }
860
866
  return {
861
- 'Fn::Join': ['', _.flatten(lines)],
867
+ 'Fn::Join': ['', lines.flat()],
862
868
  };
863
869
  });
864
870
  }
package/lib/cache.js ADDED
@@ -0,0 +1,34 @@
1
+ /**
2
+ * File content cache to avoid redundant disk I/O.
3
+ */
4
+
5
+ const { readFile } = require('fs/promises');
6
+
7
+ // File content cache to avoid re-reading the same files
8
+ const fileCache = new Map();
9
+
10
+ /**
11
+ * Read a file with caching to avoid redundant disk I/O
12
+ * @param {string} absolutePath - Absolute path to the file
13
+ * @returns {Promise<string>} File content as a string
14
+ */
15
+ async function cachedReadFile(absolutePath) {
16
+ if (fileCache.has(absolutePath)) {
17
+ return fileCache.get(absolutePath);
18
+ }
19
+ const content = await readFile(absolutePath, 'utf8');
20
+ fileCache.set(absolutePath, content);
21
+ return content;
22
+ }
23
+
24
+ /**
25
+ * Clear the file cache (useful for testing)
26
+ */
27
+ function clearFileCache() {
28
+ fileCache.clear();
29
+ }
30
+
31
+ module.exports = {
32
+ cachedReadFile,
33
+ clearFileCache,
34
+ };
package/lib/scope.js ADDED
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Scope helper functions for lazy prototype-chain based scope management.
3
+ *
4
+ * Instead of _.clone(scope) which copies O(n) properties each time,
5
+ * we use Object.create(scope) which creates a child scope in O(1) time
6
+ * that inherits from the parent via the prototype chain.
7
+ */
8
+
9
+ /**
10
+ * Create a child scope that inherits from the parent.
11
+ * Uses Object.create() for O(1) creation instead of cloning.
12
+ *
13
+ * @param {Object} parent - The parent scope to inherit from
14
+ * @param {Object} [additions={}] - Properties to add to the child scope
15
+ * @returns {Object} A new child scope with prototype chain to parent
16
+ */
17
+ function createChildScope(parent, additions = {}) {
18
+ const child = Object.create(parent);
19
+ Object.assign(child, additions);
20
+ return child;
21
+ }
22
+
23
+ /**
24
+ * Convert a prototype-chain scope to a plain object.
25
+ * Uses for...in to walk the entire prototype chain.
26
+ *
27
+ * Useful when we need to pass scope to functions that don't
28
+ * walk the prototype chain (e.g., Object.keys, _.forEach).
29
+ *
30
+ * @param {Object} scope - The scope to flatten
31
+ * @returns {Object} A plain object with all inherited properties
32
+ */
33
+ function scopeToObject(scope) {
34
+ const result = {};
35
+ for (const key in scope) {
36
+ result[key] = scope[key];
37
+ }
38
+ return result;
39
+ }
40
+
41
+ /**
42
+ * Iterate over all properties in a scope, including inherited ones.
43
+ * This is a replacement for _.forEach that walks the prototype chain.
44
+ *
45
+ * @param {Object} scope - The scope to iterate over
46
+ * @param {Function} callback - Function to call with (value, key)
47
+ */
48
+ function forEachInScope(scope, callback) {
49
+ for (const key in scope) {
50
+ callback(scope[key], key);
51
+ }
52
+ }
53
+
54
+ module.exports = {
55
+ createChildScope,
56
+ scopeToObject,
57
+ forEachInScope,
58
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@znemz/cfn-include",
3
- "version": "2.1.19",
3
+ "version": "2.1.21",
4
4
  "description": "Preprocessor for CloudFormation templates with support for loops and flexible include statements",
5
5
  "keywords": [
6
6
  "aws",