@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 +38 -32
- package/lib/cache.js +34 -0
- package/lib/scope.js +58 -0
- package/package.json +1 -1
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 {
|
|
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 =
|
|
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
|
-
|
|
113
|
-
|
|
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
|
-
|
|
141
|
-
|
|
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
|
-
|
|
145
|
+
additions[idx] = key;
|
|
144
146
|
}
|
|
145
|
-
const
|
|
146
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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 (
|
|
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 (
|
|
598
|
-
|
|
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 =
|
|
604
|
+
object = scope[find];
|
|
601
605
|
}
|
|
602
|
-
}
|
|
606
|
+
}
|
|
603
607
|
}
|
|
604
|
-
if (
|
|
605
|
-
|
|
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 (
|
|
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
|
-
|
|
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 (
|
|
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 (
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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': ['',
|
|
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
|
+
};
|