@znemz/cfn-include 2.1.18 → 2.1.20
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 +18 -18
- package/lib/cache.js +34 -0
- package/lib/replaceEnv.js +38 -2
- 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,7 @@ 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
29
|
|
|
30
30
|
/**
|
|
31
31
|
* @param {object} options
|
|
@@ -60,7 +60,7 @@ module.exports = async function (options) {
|
|
|
60
60
|
const base = parseLocation(options.url);
|
|
61
61
|
const scope = options.scope || {};
|
|
62
62
|
if (base.relative) throw new Error('url cannot be relative');
|
|
63
|
-
template =
|
|
63
|
+
template = !template
|
|
64
64
|
? fnInclude({ base, scope, cft: options.url, ...options })
|
|
65
65
|
: template;
|
|
66
66
|
// Resolve template if it's a promise to extract the root template for reference lookups
|
|
@@ -110,7 +110,7 @@ async function recurse({ base, scope, cft, rootTemplate, caller, ...opts }) {
|
|
|
110
110
|
console.log({ base, scope, cft, rootTemplate, caller, ...opts });
|
|
111
111
|
}
|
|
112
112
|
scope = _.clone(scope);
|
|
113
|
-
if (
|
|
113
|
+
if (Array.isArray(cft)) {
|
|
114
114
|
return Promise.all(cft.map((o) => recurse({ base, scope, cft: o, rootTemplate, caller: 'recurse:isArray', ...opts })));
|
|
115
115
|
}
|
|
116
116
|
if (_.isPlainObject(cft)) {
|
|
@@ -175,24 +175,24 @@ async function recurse({ base, scope, cft, rootTemplate, caller, ...opts }) {
|
|
|
175
175
|
}
|
|
176
176
|
if (cft['Fn::Flatten']) {
|
|
177
177
|
return recurse({ base, scope, cft: cft['Fn::Flatten'], rootTemplate, caller: 'Fn::Flatten', ...opts }).then(function (json) {
|
|
178
|
-
return
|
|
178
|
+
return json.flat();
|
|
179
179
|
});
|
|
180
180
|
}
|
|
181
181
|
if (cft['Fn::FlattenDeep']) {
|
|
182
182
|
return recurse({ base, scope, cft: cft['Fn::FlattenDeep'], rootTemplate, caller: 'Fn::FlattenDeep', ...opts }).then(
|
|
183
183
|
function (json) {
|
|
184
|
-
return
|
|
184
|
+
return json.flat(Infinity);
|
|
185
185
|
},
|
|
186
186
|
);
|
|
187
187
|
}
|
|
188
188
|
if (cft['Fn::Uniq']) {
|
|
189
189
|
return recurse({ base, scope, cft: cft['Fn::Uniq'], rootTemplate, caller: 'Fn::Uniq', ...opts }).then(function (json) {
|
|
190
|
-
return
|
|
190
|
+
return [...new Set(json)];
|
|
191
191
|
});
|
|
192
192
|
}
|
|
193
193
|
if (cft['Fn::Compact']) {
|
|
194
194
|
return recurse({ base, scope, cft: cft['Fn::Compact'], rootTemplate, caller: 'Fn::Compact', ...opts }).then(function (json) {
|
|
195
|
-
return
|
|
195
|
+
return json.filter(Boolean);
|
|
196
196
|
});
|
|
197
197
|
}
|
|
198
198
|
if (cft['Fn::Concat']) {
|
|
@@ -269,7 +269,7 @@ async function recurse({ base, scope, cft, rootTemplate, caller, ...opts }) {
|
|
|
269
269
|
}
|
|
270
270
|
if (cft['Fn::Filenames']) {
|
|
271
271
|
return recurse({ base, scope, cft: cft['Fn::Filenames'], rootTemplate, caller: 'Fn::Filenames', ...opts }).then(
|
|
272
|
-
function (json) {
|
|
272
|
+
async function (json) {
|
|
273
273
|
json = _.isPlainObject(json) ? { ...json } : { location: json };
|
|
274
274
|
if (json.doLog) {
|
|
275
275
|
|
|
@@ -284,7 +284,7 @@ async function recurse({ base, scope, cft, rootTemplate, caller, ...opts }) {
|
|
|
284
284
|
const absolute = location.relative
|
|
285
285
|
? path.join(path.dirname(base.path), location.host, location.path || '')
|
|
286
286
|
: [location.host, location.path].join('');
|
|
287
|
-
const globs =
|
|
287
|
+
const globs = (await glob(absolute)).sort();
|
|
288
288
|
if (json.omitExtension) {
|
|
289
289
|
return globs.map((f) => path.basename(f, path.extname(f)));
|
|
290
290
|
}
|
|
@@ -587,7 +587,7 @@ async function recurse({ base, scope, cft, rootTemplate, caller, ...opts }) {
|
|
|
587
587
|
);
|
|
588
588
|
}
|
|
589
589
|
|
|
590
|
-
if (
|
|
590
|
+
if (cft === undefined) {
|
|
591
591
|
return null;
|
|
592
592
|
}
|
|
593
593
|
return replaceEnv(cft, opts.inject, opts.doEnv);
|
|
@@ -609,7 +609,7 @@ function findAndReplace(scope, object) {
|
|
|
609
609
|
}
|
|
610
610
|
});
|
|
611
611
|
}
|
|
612
|
-
if (
|
|
612
|
+
if (Array.isArray(object)) {
|
|
613
613
|
object = object.map(_.bind(findAndReplace, this, scope));
|
|
614
614
|
} else if (_.isPlainObject(object)) {
|
|
615
615
|
object = _.mapKeys(object, function (value, key) {
|
|
@@ -632,7 +632,7 @@ function interpolate(lines, context) {
|
|
|
632
632
|
const match = _line.match(/^{{(\w+)}}$/);
|
|
633
633
|
const value = match ? context[match[1]] : undefined;
|
|
634
634
|
if (!match) return _line;
|
|
635
|
-
if (
|
|
635
|
+
if (value === undefined) {
|
|
636
636
|
return '';
|
|
637
637
|
}
|
|
638
638
|
return value;
|
|
@@ -661,7 +661,7 @@ function fnIncludeOptsFromArray(cft, opts) {
|
|
|
661
661
|
function fnIncludeOpts(cft, opts) {
|
|
662
662
|
if (_.isPlainObject(cft)) {
|
|
663
663
|
cft = _.merge(cft, _.cloneDeep(opts));
|
|
664
|
-
} else if (
|
|
664
|
+
} else if (Array.isArray(cft)) {
|
|
665
665
|
cft = fnIncludeOptsFromArray(cft, opts);
|
|
666
666
|
} else {
|
|
667
667
|
// should be string{
|
|
@@ -730,11 +730,11 @@ async function fnInclude({ base, scope, cft, ...opts }) {
|
|
|
730
730
|
|
|
731
731
|
handleInjectSetup();
|
|
732
732
|
if (isGlob(cft, absolute)) {
|
|
733
|
-
const paths =
|
|
733
|
+
const paths = (await glob(absolute)).sort();
|
|
734
734
|
const template = yaml.load(paths.map((_p) => `- Fn::Include: file://${_p}`).join('\n'));
|
|
735
735
|
return recurse({ base, scope, cft: template, rootTemplate: template, ...opts });
|
|
736
736
|
}
|
|
737
|
-
body =
|
|
737
|
+
body = cachedReadFile(absolute).then(procTemplate);
|
|
738
738
|
absolute = `${location.protocol}://${absolute}`;
|
|
739
739
|
} else if (location.protocol === 's3') {
|
|
740
740
|
const basedir = pathParse(base.path).dir;
|
|
@@ -829,7 +829,7 @@ async function handleIncludeBody({ scope, args, body, absolute }) {
|
|
|
829
829
|
return temp;
|
|
830
830
|
}
|
|
831
831
|
// once fully recursed we can query the resultant template
|
|
832
|
-
const query =
|
|
832
|
+
const query = typeof args.query === 'string'
|
|
833
833
|
? replaceEnv(args.query, args.inject, args.doEnv)
|
|
834
834
|
: await recurse({
|
|
835
835
|
base: parseLocation(absolute),
|
|
@@ -858,7 +858,7 @@ async function handleIncludeBody({ scope, args, body, absolute }) {
|
|
|
858
858
|
lines = interpolate(lines, args.context);
|
|
859
859
|
}
|
|
860
860
|
return {
|
|
861
|
-
'Fn::Join': ['',
|
|
861
|
+
'Fn::Join': ['', lines.flat()],
|
|
862
862
|
};
|
|
863
863
|
});
|
|
864
864
|
}
|
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/replaceEnv.js
CHANGED
|
@@ -1,6 +1,34 @@
|
|
|
1
1
|
const passThrough = (template) => template
|
|
2
2
|
const replaceProcessEnv = (template) => replaceEnv(template, process.env, false)
|
|
3
3
|
|
|
4
|
+
// Cache for compiled regex patterns to avoid re-creating them
|
|
5
|
+
const regexCache = new Map();
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Escape special regex characters in a string
|
|
9
|
+
* @param {string} str - String to escape
|
|
10
|
+
* @returns {string} Escaped string safe for regex
|
|
11
|
+
*/
|
|
12
|
+
function escapeRegExp(str) {
|
|
13
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Get a cached regex for variable substitution
|
|
18
|
+
* @param {string} key - Variable name
|
|
19
|
+
* @param {boolean} withBraces - Whether to match ${key} format (true) or $key format (false)
|
|
20
|
+
* @returns {RegExp} Cached regex pattern
|
|
21
|
+
*/
|
|
22
|
+
function getVarRegex(key, withBraces) {
|
|
23
|
+
const cacheKey = `${key}:${withBraces}`;
|
|
24
|
+
if (!regexCache.has(cacheKey)) {
|
|
25
|
+
const escaped = escapeRegExp(key);
|
|
26
|
+
const pattern = withBraces ? `\\$\\{${escaped}\\}` : `\\$${escaped}`;
|
|
27
|
+
regexCache.set(cacheKey, new RegExp(pattern, 'g'));
|
|
28
|
+
}
|
|
29
|
+
return regexCache.get(cacheKey);
|
|
30
|
+
}
|
|
31
|
+
|
|
4
32
|
const replaceEnv = (template, inject = {}, doEnv) => {
|
|
5
33
|
processTemplate = doEnv ? replaceProcessEnv : passThrough;
|
|
6
34
|
|
|
@@ -15,9 +43,17 @@ const replaceEnv = (template, inject = {}, doEnv) => {
|
|
|
15
43
|
const key = keys[i];
|
|
16
44
|
let val = inject[key];
|
|
17
45
|
|
|
46
|
+
// Use cached regex patterns instead of creating new ones each time
|
|
47
|
+
const bareRegex = getVarRegex(key, false);
|
|
48
|
+
const bracedRegex = getVarRegex(key, true);
|
|
49
|
+
|
|
50
|
+
// Reset lastIndex for global regex reuse
|
|
51
|
+
bareRegex.lastIndex = 0;
|
|
52
|
+
bracedRegex.lastIndex = 0;
|
|
53
|
+
|
|
18
54
|
template = template
|
|
19
|
-
.replace(
|
|
20
|
-
.replace(
|
|
55
|
+
.replace(bareRegex, val)
|
|
56
|
+
.replace(bracedRegex, val);
|
|
21
57
|
}
|
|
22
58
|
|
|
23
59
|
// return template;
|