@kinotic-ai/spawn 0.4.0 → 0.5.0
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/dist/graal-spawn-renderer.js +165 -44
- package/dist/index.cjs +64 -17
- package/dist/index.d.cts +37 -1
- package/dist/index.d.ts +37 -1
- package/dist/index.js +1 -1
- package/dist/node/index.cjs +97 -43
- package/dist/node/index.d.cts +32 -2
- package/dist/node/index.d.ts +32 -2
- package/dist/node/index.js +34 -27
- package/dist/shared/{spawn-4k8xtpe9.js → spawn-1h1k8w8r.js} +64 -17
- package/package.json +3 -3
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
});
|
|
15
15
|
};
|
|
16
16
|
|
|
17
|
-
// ../../node_modules/.bun/liquidjs@10.
|
|
17
|
+
// ../../node_modules/.bun/liquidjs@10.27.0/node_modules/liquidjs/dist/liquid.browser.mjs
|
|
18
18
|
class Token {
|
|
19
19
|
constructor(kind, input, begin, end, file) {
|
|
20
20
|
this.kind = kind;
|
|
@@ -154,10 +154,10 @@
|
|
|
154
154
|
}
|
|
155
155
|
function pad(str, length, ch, add) {
|
|
156
156
|
str = String(str);
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
return str;
|
|
157
|
+
const n = length - str.length;
|
|
158
|
+
if (n <= 0)
|
|
159
|
+
return str;
|
|
160
|
+
return add(str, ch.repeat(n));
|
|
161
161
|
}
|
|
162
162
|
function identify(val) {
|
|
163
163
|
return val;
|
|
@@ -703,8 +703,10 @@ From ` + this.originalError.stack;
|
|
|
703
703
|
m: (d) => d.getMonth() + 1,
|
|
704
704
|
M: (d) => d.getMinutes(),
|
|
705
705
|
N: (d, opts) => {
|
|
706
|
+
var _a;
|
|
706
707
|
const width = Number(opts.width) || 9;
|
|
707
708
|
const str = String(d.getMilliseconds()).slice(0, width);
|
|
709
|
+
(_a = opts.memoryLimit) === null || _a === undefined || _a.use(width - str.length);
|
|
708
710
|
return padEnd(str, width, "0");
|
|
709
711
|
},
|
|
710
712
|
p: (d) => d.getHours() < 12 ? "AM" : "PM",
|
|
@@ -728,18 +730,18 @@ From ` + this.originalError.stack;
|
|
|
728
730
|
"%": () => "%"
|
|
729
731
|
};
|
|
730
732
|
formatCodes.h = formatCodes.b;
|
|
731
|
-
function strftime(d, formatStr) {
|
|
733
|
+
function strftime(d, formatStr, memoryLimit) {
|
|
732
734
|
let output = "";
|
|
733
735
|
let remaining = formatStr;
|
|
734
736
|
let match;
|
|
735
737
|
while (match = rFormat.exec(remaining)) {
|
|
736
738
|
output += remaining.slice(0, match.index);
|
|
737
739
|
remaining = remaining.slice(match.index + match[0].length);
|
|
738
|
-
output += format(d, match);
|
|
740
|
+
output += format(d, match, memoryLimit);
|
|
739
741
|
}
|
|
740
742
|
return output + remaining;
|
|
741
743
|
}
|
|
742
|
-
function format(d, match) {
|
|
744
|
+
function format(d, match, memoryLimit) {
|
|
743
745
|
const [input, flagStr = "", width, modifier, conversion] = match;
|
|
744
746
|
const convert = formatCodes[conversion];
|
|
745
747
|
if (!convert)
|
|
@@ -747,7 +749,7 @@ From ` + this.originalError.stack;
|
|
|
747
749
|
const flags = {};
|
|
748
750
|
for (const flag of flagStr)
|
|
749
751
|
flags[flag] = true;
|
|
750
|
-
let ret = String(convert(d, { flags, width, modifier }));
|
|
752
|
+
let ret = String(convert(d, { flags, width, modifier, memoryLimit }));
|
|
751
753
|
let padChar = padSpaceChars.has(conversion) ? " " : "0";
|
|
752
754
|
let padWidth = width || padWidths[conversion] || 0;
|
|
753
755
|
if (flags["^"])
|
|
@@ -760,6 +762,7 @@ From ` + this.originalError.stack;
|
|
|
760
762
|
padChar = "0";
|
|
761
763
|
if (flags["-"])
|
|
762
764
|
padWidth = 0;
|
|
765
|
+
memoryLimit === null || memoryLimit === undefined || memoryLimit.use(Number(padWidth) - ret.length);
|
|
763
766
|
return padStart(ret, padWidth, padChar);
|
|
764
767
|
}
|
|
765
768
|
function getDateTimeFormat() {
|
|
@@ -1192,6 +1195,7 @@ From ` + this.originalError.stack;
|
|
|
1192
1195
|
if (!emitter) {
|
|
1193
1196
|
emitter = ctx.opts.keepOutputType ? new KeepingTypeEmitter : new SimpleEmitter;
|
|
1194
1197
|
}
|
|
1198
|
+
ctx.renderLimit.check(getPerformance().now());
|
|
1195
1199
|
const errors = [];
|
|
1196
1200
|
for (const tpl of templates) {
|
|
1197
1201
|
ctx.renderLimit.check(getPerformance().now());
|
|
@@ -1577,7 +1581,28 @@ From ` + this.originalError.stack;
|
|
|
1577
1581
|
function strip_html(v) {
|
|
1578
1582
|
const str = stringify(v);
|
|
1579
1583
|
this.context.memoryLimit.use(str.length);
|
|
1580
|
-
|
|
1584
|
+
const blocks = new Map([["<script", "</script>"], ["<style", "</style>"], ["<!--", "-->"], ["<", ">"]]);
|
|
1585
|
+
let out = "";
|
|
1586
|
+
let i = 0;
|
|
1587
|
+
while (i < str.length) {
|
|
1588
|
+
const lt = str.indexOf("<", i);
|
|
1589
|
+
if (lt < 0)
|
|
1590
|
+
return out + str.slice(i);
|
|
1591
|
+
out += str.slice(i, lt);
|
|
1592
|
+
for (const [opener, closer] of blocks) {
|
|
1593
|
+
if (!str.startsWith(opener, lt))
|
|
1594
|
+
continue;
|
|
1595
|
+
const e = str.indexOf(closer, lt + opener.length);
|
|
1596
|
+
if (e >= 0) {
|
|
1597
|
+
i = e + closer.length;
|
|
1598
|
+
break;
|
|
1599
|
+
}
|
|
1600
|
+
blocks.delete(opener);
|
|
1601
|
+
}
|
|
1602
|
+
if (i === lt)
|
|
1603
|
+
return out + str.slice(lt);
|
|
1604
|
+
}
|
|
1605
|
+
return out;
|
|
1581
1606
|
}
|
|
1582
1607
|
var htmlFilters = /* @__PURE__ */ Object.freeze({
|
|
1583
1608
|
__proto__: null,
|
|
@@ -2845,10 +2870,17 @@ From ` + this.originalError.stack;
|
|
|
2845
2870
|
function getKind(val) {
|
|
2846
2871
|
return val ? val.kind : -1;
|
|
2847
2872
|
}
|
|
2873
|
+
function createScope(from) {
|
|
2874
|
+
const scope = Object.create(null);
|
|
2875
|
+
if (from)
|
|
2876
|
+
Object.assign(scope, from);
|
|
2877
|
+
return scope;
|
|
2878
|
+
}
|
|
2879
|
+
|
|
2848
2880
|
class Context {
|
|
2849
2881
|
constructor(env = {}, opts = defaultOptions, renderOptions = {}, { memoryLimit, renderLimit } = {}) {
|
|
2850
2882
|
var _a, _b, _c, _d, _e;
|
|
2851
|
-
this.scopes = [
|
|
2883
|
+
this.scopes = [createScope()];
|
|
2852
2884
|
this.registers = {};
|
|
2853
2885
|
this.breakCalled = false;
|
|
2854
2886
|
this.continueCalled = false;
|
|
@@ -2913,7 +2945,8 @@ From ` + this.originalError.stack;
|
|
|
2913
2945
|
return new Context(scope, this.opts, {
|
|
2914
2946
|
sync: this.sync,
|
|
2915
2947
|
globals: this.globals,
|
|
2916
|
-
strictVariables: this.strictVariables
|
|
2948
|
+
strictVariables: this.strictVariables,
|
|
2949
|
+
ownPropertyOnly: this.ownPropertyOnly
|
|
2917
2950
|
}, {
|
|
2918
2951
|
renderLimit: this.renderLimit,
|
|
2919
2952
|
memoryLimit: this.memoryLimit
|
|
@@ -3307,15 +3340,16 @@ From ` + this.originalError.stack;
|
|
|
3307
3340
|
sample
|
|
3308
3341
|
});
|
|
3309
3342
|
function date(v, format2, timezoneOffset) {
|
|
3310
|
-
var _a, _b
|
|
3311
|
-
const size2 = ((_a = v === null || v === undefined ? undefined : v.length) !== null && _a !== undefined ? _a : 0) + ((_b =
|
|
3343
|
+
var _a, _b;
|
|
3344
|
+
const size2 = ((_a = v === null || v === undefined ? undefined : v.length) !== null && _a !== undefined ? _a : 0) + ((_b = timezoneOffset === null || timezoneOffset === undefined ? undefined : timezoneOffset.length) !== null && _b !== undefined ? _b : 0);
|
|
3312
3345
|
this.context.memoryLimit.use(size2);
|
|
3313
3346
|
const date2 = parseDate(v, this.context.opts, timezoneOffset);
|
|
3314
3347
|
if (!date2)
|
|
3315
3348
|
return v;
|
|
3316
3349
|
format2 = toValue(format2);
|
|
3317
3350
|
format2 = isNil(format2) ? this.context.opts.dateFormat : stringify(format2);
|
|
3318
|
-
|
|
3351
|
+
this.context.memoryLimit.use(format2.length);
|
|
3352
|
+
return strftime(date2, format2, this.context.memoryLimit);
|
|
3319
3353
|
}
|
|
3320
3354
|
function date_to_xmlschema(v) {
|
|
3321
3355
|
return date.call(this, v, "%Y-%m-%dT%H:%M:%S%:z");
|
|
@@ -3333,11 +3367,12 @@ From ` + this.originalError.stack;
|
|
|
3333
3367
|
const date2 = parseDate(v, this.context.opts);
|
|
3334
3368
|
if (!date2)
|
|
3335
3369
|
return v;
|
|
3370
|
+
const ml = this.context.memoryLimit;
|
|
3336
3371
|
if (type === "ordinal") {
|
|
3337
3372
|
const d = date2.getDate();
|
|
3338
|
-
return style === "US" ? strftime(date2, `${month_type} ${d}%q, %Y
|
|
3373
|
+
return style === "US" ? strftime(date2, `${month_type} ${d}%q, %Y`, ml) : strftime(date2, `${d}%q ${month_type} %Y`, ml);
|
|
3339
3374
|
}
|
|
3340
|
-
return strftime(date2, `%d ${month_type} %Y
|
|
3375
|
+
return strftime(date2, `%d ${month_type} %Y`, ml);
|
|
3341
3376
|
}
|
|
3342
3377
|
function parseDate(v, opts, timezoneOffset) {
|
|
3343
3378
|
let date2;
|
|
@@ -3609,7 +3644,46 @@ From ` + this.originalError.stack;
|
|
|
3609
3644
|
base64_encode,
|
|
3610
3645
|
base64_decode
|
|
3611
3646
|
});
|
|
3612
|
-
|
|
3647
|
+
function bufferToHex(buffer) {
|
|
3648
|
+
const bytes = new Uint8Array(buffer);
|
|
3649
|
+
let hex = "";
|
|
3650
|
+
for (let i = 0;i < bytes.length; i++) {
|
|
3651
|
+
hex += bytes[i].toString(16).padStart(2, "0");
|
|
3652
|
+
}
|
|
3653
|
+
return hex;
|
|
3654
|
+
}
|
|
3655
|
+
function sha256(str) {
|
|
3656
|
+
return __awaiter(this, undefined, undefined, function* () {
|
|
3657
|
+
const data = new TextEncoder().encode(str);
|
|
3658
|
+
const digest = yield crypto.subtle.digest("SHA-256", data);
|
|
3659
|
+
return bufferToHex(digest);
|
|
3660
|
+
});
|
|
3661
|
+
}
|
|
3662
|
+
function hmacSha256(str, key) {
|
|
3663
|
+
return __awaiter(this, undefined, undefined, function* () {
|
|
3664
|
+
const encoder = new TextEncoder;
|
|
3665
|
+
const cryptoKey = yield crypto.subtle.importKey("raw", encoder.encode(key), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
|
|
3666
|
+
const signature = yield crypto.subtle.sign("HMAC", cryptoKey, encoder.encode(str));
|
|
3667
|
+
return bufferToHex(signature);
|
|
3668
|
+
});
|
|
3669
|
+
}
|
|
3670
|
+
function sha256$1(value) {
|
|
3671
|
+
const str = stringify(value);
|
|
3672
|
+
this.context.memoryLimit.use(str.length);
|
|
3673
|
+
return sha256(str);
|
|
3674
|
+
}
|
|
3675
|
+
function hmac_sha256(value, key) {
|
|
3676
|
+
const str = stringify(value);
|
|
3677
|
+
const keyStr = stringify(key);
|
|
3678
|
+
this.context.memoryLimit.use(str.length + keyStr.length);
|
|
3679
|
+
return hmacSha256(str, keyStr);
|
|
3680
|
+
}
|
|
3681
|
+
var cryptoFilters = /* @__PURE__ */ Object.freeze({
|
|
3682
|
+
__proto__: null,
|
|
3683
|
+
sha256: sha256$1,
|
|
3684
|
+
hmac_sha256
|
|
3685
|
+
});
|
|
3686
|
+
var filters = Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({}, htmlFilters), mathFilters), urlFilters), arrayFilters), dateFilters), stringFilters), base64Filters), cryptoFilters), misc);
|
|
3613
3687
|
|
|
3614
3688
|
class AssignTag extends Tag {
|
|
3615
3689
|
constructor(token, remainTokens, liquid) {
|
|
@@ -3668,7 +3742,7 @@ From ` + this.originalError.stack;
|
|
|
3668
3742
|
return;
|
|
3669
3743
|
}
|
|
3670
3744
|
const continueKey = "continue-" + this.variable + "-" + this.collection.getText();
|
|
3671
|
-
ctx.push({ continue: ctx.getRegister(continueKey, {}) });
|
|
3745
|
+
ctx.push(createScope({ continue: ctx.getRegister(continueKey, {}) }));
|
|
3672
3746
|
const hash = yield this.hash.render(ctx);
|
|
3673
3747
|
ctx.pop();
|
|
3674
3748
|
const modifiers = this.liquid.options.orderedFilterParameters ? Object.keys(hash).filter((x) => MODIFIERS.includes(x)) : MODIFIERS.filter((x) => hash[x] !== undefined);
|
|
@@ -3680,7 +3754,7 @@ From ` + this.originalError.stack;
|
|
|
3680
3754
|
return reversed(collection2);
|
|
3681
3755
|
}, collection);
|
|
3682
3756
|
ctx.setRegister(continueKey, (hash["offset"] || 0) + collection.length);
|
|
3683
|
-
const scope = { forloop: new ForloopDrop(collection.length, this.collection.getText(), this.variable) };
|
|
3757
|
+
const scope = createScope({ forloop: new ForloopDrop(collection.length, this.collection.getText(), this.variable) });
|
|
3684
3758
|
ctx.push(scope);
|
|
3685
3759
|
for (const item of collection) {
|
|
3686
3760
|
scope[this.variable] = item;
|
|
@@ -4006,11 +4080,11 @@ From ` + this.originalError.stack;
|
|
|
4006
4080
|
const saved = ctx.saveRegister("blocks", "blockMode");
|
|
4007
4081
|
ctx.setRegister("blocks", {});
|
|
4008
4082
|
ctx.setRegister("blockMode", BlockMode.OUTPUT);
|
|
4009
|
-
const scope = yield hash.render(ctx);
|
|
4083
|
+
const scope = createScope(yield hash.render(ctx));
|
|
4010
4084
|
if (withVar)
|
|
4011
4085
|
scope[filepath] = yield evalToken(withVar, ctx);
|
|
4012
4086
|
const templates = yield liquid._parsePartialFile(filepath, ctx.sync, this["currentFile"]);
|
|
4013
|
-
ctx.push(ctx.opts.jekyllInclude ? { include: scope } : scope);
|
|
4087
|
+
ctx.push(ctx.opts.jekyllInclude ? createScope({ include: scope }) : scope);
|
|
4014
4088
|
yield renderer.renderTemplates(templates, ctx, emitter);
|
|
4015
4089
|
ctx.pop();
|
|
4016
4090
|
ctx.restoreRegister(saved);
|
|
@@ -4199,7 +4273,7 @@ From ` + this.originalError.stack;
|
|
|
4199
4273
|
if (blocks[""] === undefined)
|
|
4200
4274
|
blocks[""] = (parent, emitter2) => emitter2.write(html);
|
|
4201
4275
|
ctx.setRegister("blockMode", BlockMode.OUTPUT);
|
|
4202
|
-
ctx.push(yield args.render(ctx));
|
|
4276
|
+
ctx.push(createScope(yield args.render(ctx)));
|
|
4203
4277
|
yield renderer.renderTemplates(templates, ctx, emitter);
|
|
4204
4278
|
ctx.pop();
|
|
4205
4279
|
}
|
|
@@ -4259,7 +4333,7 @@ From ` + this.originalError.stack;
|
|
|
4259
4333
|
if (stack.includes(self))
|
|
4260
4334
|
throw new Error("block tag cannot be nested");
|
|
4261
4335
|
stack.push(self);
|
|
4262
|
-
ctx.push({ block: superBlock });
|
|
4336
|
+
ctx.push(createScope({ block: superBlock }));
|
|
4263
4337
|
yield liquid.renderer.renderTemplates(templates, ctx, emitter);
|
|
4264
4338
|
ctx.pop();
|
|
4265
4339
|
stack.pop();
|
|
@@ -4343,7 +4417,7 @@ From ` + this.originalError.stack;
|
|
|
4343
4417
|
const cols = args.cols || collection.length;
|
|
4344
4418
|
const r = this.liquid.renderer;
|
|
4345
4419
|
const tablerowloop = new TablerowloopDrop(collection.length, cols, this.collection.getText(), this.variable);
|
|
4346
|
-
const scope = { tablerowloop };
|
|
4420
|
+
const scope = createScope({ tablerowloop });
|
|
4347
4421
|
ctx.push(scope);
|
|
4348
4422
|
for (let idx = 0;idx < collection.length; idx++, tablerowloop.next()) {
|
|
4349
4423
|
scope[this.variable] = collection[idx];
|
|
@@ -4516,8 +4590,8 @@ From ` + this.originalError.stack;
|
|
|
4516
4590
|
class Liquid {
|
|
4517
4591
|
constructor(opts = {}) {
|
|
4518
4592
|
this.renderer = new Render;
|
|
4519
|
-
this.filters =
|
|
4520
|
-
this.tags =
|
|
4593
|
+
this.filters = Object.create(null);
|
|
4594
|
+
this.tags = Object.create(null);
|
|
4521
4595
|
this.options = normalize(opts);
|
|
4522
4596
|
this.parser = new Parser(this);
|
|
4523
4597
|
forOwn(tags, (conf, name) => this.registerTag(name, conf));
|
|
@@ -19014,23 +19088,7 @@ From ` + this.originalError.stack;
|
|
|
19014
19088
|
this.engine.registerFilter("upperFirst", (v) => upperFirst(v));
|
|
19015
19089
|
}
|
|
19016
19090
|
async renderSpawn(spawn, options) {
|
|
19017
|
-
const trees =
|
|
19018
|
-
const configs = [];
|
|
19019
|
-
let currentConfig = this.parseConfig(spawn);
|
|
19020
|
-
if (currentConfig) {
|
|
19021
|
-
configs.push(currentConfig);
|
|
19022
|
-
}
|
|
19023
|
-
while (currentConfig?.inherits) {
|
|
19024
|
-
if (!options?.loadInherited) {
|
|
19025
|
-
throw new Error(`Spawn inherits '${currentConfig.inherits}' but no loadInherited callback was provided`);
|
|
19026
|
-
}
|
|
19027
|
-
const inherited = await options.loadInherited(currentConfig.inherits);
|
|
19028
|
-
trees.push(inherited);
|
|
19029
|
-
currentConfig = this.parseConfig(inherited);
|
|
19030
|
-
if (currentConfig) {
|
|
19031
|
-
configs.push(currentConfig);
|
|
19032
|
-
}
|
|
19033
|
-
}
|
|
19091
|
+
const { trees, configs } = await this.walkInheritance(spawn, options?.loadInherited);
|
|
19034
19092
|
let globals = {};
|
|
19035
19093
|
let propertySchemas = {};
|
|
19036
19094
|
for (const config2 of [...configs].reverse()) {
|
|
@@ -19069,6 +19127,69 @@ From ` + this.originalError.stack;
|
|
|
19069
19127
|
}
|
|
19070
19128
|
return { files, sources, context };
|
|
19071
19129
|
}
|
|
19130
|
+
async lint(spawn, options) {
|
|
19131
|
+
const { trees, configs } = await this.walkInheritance(spawn, options?.loadInherited);
|
|
19132
|
+
const declared = new Set;
|
|
19133
|
+
for (const config2 of configs) {
|
|
19134
|
+
if (config2.globals) {
|
|
19135
|
+
Object.keys(config2.globals).forEach((key) => declared.add(key));
|
|
19136
|
+
}
|
|
19137
|
+
if (config2.propertySchema) {
|
|
19138
|
+
Object.keys(config2.propertySchema).forEach((key) => declared.add(key));
|
|
19139
|
+
}
|
|
19140
|
+
}
|
|
19141
|
+
const usedIn = new Map;
|
|
19142
|
+
const record2 = (name, file2) => {
|
|
19143
|
+
let files = usedIn.get(name);
|
|
19144
|
+
if (!files) {
|
|
19145
|
+
files = new Set;
|
|
19146
|
+
usedIn.set(name, files);
|
|
19147
|
+
}
|
|
19148
|
+
files.add(file2);
|
|
19149
|
+
};
|
|
19150
|
+
const externalVars = (template) => Object.keys(this.engine.parseAndAnalyzeSync(template, undefined, { partials: false }).globals);
|
|
19151
|
+
for (const tree of trees) {
|
|
19152
|
+
for (const source of Object.keys(tree)) {
|
|
19153
|
+
const fileName = source.substring(source.lastIndexOf("/") + 1);
|
|
19154
|
+
if (IGNORED_FILE_NAMES.includes(fileName)) {
|
|
19155
|
+
continue;
|
|
19156
|
+
}
|
|
19157
|
+
externalVars(source).forEach((name) => record2(name, source));
|
|
19158
|
+
const content = tree[source];
|
|
19159
|
+
if (source.endsWith(".liquid") && typeof content === "string") {
|
|
19160
|
+
externalVars(content).forEach((name) => record2(name, source));
|
|
19161
|
+
}
|
|
19162
|
+
}
|
|
19163
|
+
}
|
|
19164
|
+
const undeclared = [];
|
|
19165
|
+
for (const [name, files] of usedIn) {
|
|
19166
|
+
if (!declared.has(name)) {
|
|
19167
|
+
undeclared.push({ name, files: [...files].sort() });
|
|
19168
|
+
}
|
|
19169
|
+
}
|
|
19170
|
+
undeclared.sort((a, b) => a.name.localeCompare(b.name));
|
|
19171
|
+
return { undeclared };
|
|
19172
|
+
}
|
|
19173
|
+
async walkInheritance(spawn, loadInherited) {
|
|
19174
|
+
const trees = [spawn];
|
|
19175
|
+
const configs = [];
|
|
19176
|
+
let currentConfig = this.parseConfig(spawn);
|
|
19177
|
+
if (currentConfig) {
|
|
19178
|
+
configs.push(currentConfig);
|
|
19179
|
+
}
|
|
19180
|
+
while (currentConfig?.inherits) {
|
|
19181
|
+
if (!loadInherited) {
|
|
19182
|
+
throw new Error(`Spawn inherits '${currentConfig.inherits}' but no loadInherited callback was provided`);
|
|
19183
|
+
}
|
|
19184
|
+
const inherited = await loadInherited(currentConfig.inherits);
|
|
19185
|
+
trees.push(inherited);
|
|
19186
|
+
currentConfig = this.parseConfig(inherited);
|
|
19187
|
+
if (currentConfig) {
|
|
19188
|
+
configs.push(currentConfig);
|
|
19189
|
+
}
|
|
19190
|
+
}
|
|
19191
|
+
return { trees, configs };
|
|
19192
|
+
}
|
|
19072
19193
|
parseConfig(tree) {
|
|
19073
19194
|
const raw2 = tree["spawn.json"];
|
|
19074
19195
|
if (raw2 === undefined) {
|
package/dist/index.cjs
CHANGED
|
@@ -104,23 +104,7 @@ class SpawnEngine {
|
|
|
104
104
|
this.engine.registerFilter("upperFirst", (v) => upperFirst(v));
|
|
105
105
|
}
|
|
106
106
|
async renderSpawn(spawn, options) {
|
|
107
|
-
const trees =
|
|
108
|
-
const configs = [];
|
|
109
|
-
let currentConfig = this.parseConfig(spawn);
|
|
110
|
-
if (currentConfig) {
|
|
111
|
-
configs.push(currentConfig);
|
|
112
|
-
}
|
|
113
|
-
while (currentConfig?.inherits) {
|
|
114
|
-
if (!options?.loadInherited) {
|
|
115
|
-
throw new Error(`Spawn inherits '${currentConfig.inherits}' but no loadInherited callback was provided`);
|
|
116
|
-
}
|
|
117
|
-
const inherited = await options.loadInherited(currentConfig.inherits);
|
|
118
|
-
trees.push(inherited);
|
|
119
|
-
currentConfig = this.parseConfig(inherited);
|
|
120
|
-
if (currentConfig) {
|
|
121
|
-
configs.push(currentConfig);
|
|
122
|
-
}
|
|
123
|
-
}
|
|
107
|
+
const { trees, configs } = await this.walkInheritance(spawn, options?.loadInherited);
|
|
124
108
|
let globals = {};
|
|
125
109
|
let propertySchemas = {};
|
|
126
110
|
for (const config of [...configs].reverse()) {
|
|
@@ -159,6 +143,69 @@ class SpawnEngine {
|
|
|
159
143
|
}
|
|
160
144
|
return { files, sources, context };
|
|
161
145
|
}
|
|
146
|
+
async lint(spawn, options) {
|
|
147
|
+
const { trees, configs } = await this.walkInheritance(spawn, options?.loadInherited);
|
|
148
|
+
const declared = new Set;
|
|
149
|
+
for (const config of configs) {
|
|
150
|
+
if (config.globals) {
|
|
151
|
+
Object.keys(config.globals).forEach((key) => declared.add(key));
|
|
152
|
+
}
|
|
153
|
+
if (config.propertySchema) {
|
|
154
|
+
Object.keys(config.propertySchema).forEach((key) => declared.add(key));
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
const usedIn = new Map;
|
|
158
|
+
const record = (name, file) => {
|
|
159
|
+
let files = usedIn.get(name);
|
|
160
|
+
if (!files) {
|
|
161
|
+
files = new Set;
|
|
162
|
+
usedIn.set(name, files);
|
|
163
|
+
}
|
|
164
|
+
files.add(file);
|
|
165
|
+
};
|
|
166
|
+
const externalVars = (template) => Object.keys(this.engine.parseAndAnalyzeSync(template, undefined, { partials: false }).globals);
|
|
167
|
+
for (const tree of trees) {
|
|
168
|
+
for (const source of Object.keys(tree)) {
|
|
169
|
+
const fileName = source.substring(source.lastIndexOf("/") + 1);
|
|
170
|
+
if (IGNORED_FILE_NAMES.includes(fileName)) {
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
externalVars(source).forEach((name) => record(name, source));
|
|
174
|
+
const content = tree[source];
|
|
175
|
+
if (source.endsWith(".liquid") && typeof content === "string") {
|
|
176
|
+
externalVars(content).forEach((name) => record(name, source));
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
const undeclared = [];
|
|
181
|
+
for (const [name, files] of usedIn) {
|
|
182
|
+
if (!declared.has(name)) {
|
|
183
|
+
undeclared.push({ name, files: [...files].sort() });
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
undeclared.sort((a, b) => a.name.localeCompare(b.name));
|
|
187
|
+
return { undeclared };
|
|
188
|
+
}
|
|
189
|
+
async walkInheritance(spawn, loadInherited) {
|
|
190
|
+
const trees = [spawn];
|
|
191
|
+
const configs = [];
|
|
192
|
+
let currentConfig = this.parseConfig(spawn);
|
|
193
|
+
if (currentConfig) {
|
|
194
|
+
configs.push(currentConfig);
|
|
195
|
+
}
|
|
196
|
+
while (currentConfig?.inherits) {
|
|
197
|
+
if (!loadInherited) {
|
|
198
|
+
throw new Error(`Spawn inherits '${currentConfig.inherits}' but no loadInherited callback was provided`);
|
|
199
|
+
}
|
|
200
|
+
const inherited = await loadInherited(currentConfig.inherits);
|
|
201
|
+
trees.push(inherited);
|
|
202
|
+
currentConfig = this.parseConfig(inherited);
|
|
203
|
+
if (currentConfig) {
|
|
204
|
+
configs.push(currentConfig);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return { trees, configs };
|
|
208
|
+
}
|
|
162
209
|
parseConfig(tree) {
|
|
163
210
|
const raw = tree["spawn.json"];
|
|
164
211
|
if (raw === undefined) {
|
package/dist/index.d.cts
CHANGED
|
@@ -61,6 +61,31 @@ interface RenderSpawnOptions {
|
|
|
61
61
|
loadInherited?: (ref: string) => Promise<SpawnTree>;
|
|
62
62
|
}
|
|
63
63
|
/**
|
|
64
|
+
* A variable referenced in a spawn's templates that is declared neither in
|
|
65
|
+
* propertySchema nor in globals, reported by {@link SpawnEngine#lint}.
|
|
66
|
+
*
|
|
67
|
+
* @see SpawnLintResult
|
|
68
|
+
*/
|
|
69
|
+
interface UndeclaredVariable {
|
|
70
|
+
/** The variable name as referenced in the templates. */
|
|
71
|
+
name: string;
|
|
72
|
+
/**
|
|
73
|
+
* The spawn paths where the variable appears — file contents for
|
|
74
|
+
* {@code .liquid} files, and path templates for any file whose path contains
|
|
75
|
+
* the variable.
|
|
76
|
+
*/
|
|
77
|
+
files: string[];
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* The outcome of {@link SpawnEngine#lint}: the variables a spawn's templates
|
|
81
|
+
* reference that are not declared in propertySchema or globals. A spawn is
|
|
82
|
+
* lint-clean when {@code undeclared} is empty.
|
|
83
|
+
*/
|
|
84
|
+
interface SpawnLintResult {
|
|
85
|
+
/** Referenced-but-undeclared variables, sorted by name. */
|
|
86
|
+
undeclared: UndeclaredVariable[];
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
64
89
|
* The outcome of rendering a spawn.
|
|
65
90
|
*/
|
|
66
91
|
interface SpawnRenderResult {
|
|
@@ -113,7 +138,18 @@ declare class SpawnEngine {
|
|
|
113
138
|
* context used, including values added by the property resolver.
|
|
114
139
|
*/
|
|
115
140
|
renderSpawn(spawn: SpawnTree, options?: RenderSpawnOptions): Promise<SpawnRenderResult>;
|
|
141
|
+
/**
|
|
142
|
+
* Reports the variables a spawn's templates reference that are declared
|
|
143
|
+
* neither in propertySchema nor in globals, across the full inheritance
|
|
144
|
+
* chain. Variables come from the path of every file plus the contents of
|
|
145
|
+
* {@code .liquid} files; registered filters are not mistaken for variables.
|
|
146
|
+
* A clean spawn has an empty {@code undeclared} list.
|
|
147
|
+
*/
|
|
148
|
+
lint(spawn: SpawnTree, options?: {
|
|
149
|
+
loadInherited?: (ref: string) => Promise<SpawnTree>;
|
|
150
|
+
}): Promise<SpawnLintResult>;
|
|
151
|
+
private walkInheritance;
|
|
116
152
|
private parseConfig;
|
|
117
153
|
private resolveMissingProperties;
|
|
118
154
|
}
|
|
119
|
-
export { SpawnTree, SpawnRenderResult, SpawnEngine, RenderSpawnOptions, PropertySchema, PropertyResolver };
|
|
155
|
+
export { UndeclaredVariable, SpawnTree, SpawnRenderResult, SpawnLintResult, SpawnEngine, RenderSpawnOptions, PropertySchema, PropertyResolver };
|
package/dist/index.d.ts
CHANGED
|
@@ -61,6 +61,31 @@ interface RenderSpawnOptions {
|
|
|
61
61
|
loadInherited?: (ref: string) => Promise<SpawnTree>;
|
|
62
62
|
}
|
|
63
63
|
/**
|
|
64
|
+
* A variable referenced in a spawn's templates that is declared neither in
|
|
65
|
+
* propertySchema nor in globals, reported by {@link SpawnEngine#lint}.
|
|
66
|
+
*
|
|
67
|
+
* @see SpawnLintResult
|
|
68
|
+
*/
|
|
69
|
+
interface UndeclaredVariable {
|
|
70
|
+
/** The variable name as referenced in the templates. */
|
|
71
|
+
name: string;
|
|
72
|
+
/**
|
|
73
|
+
* The spawn paths where the variable appears — file contents for
|
|
74
|
+
* {@code .liquid} files, and path templates for any file whose path contains
|
|
75
|
+
* the variable.
|
|
76
|
+
*/
|
|
77
|
+
files: string[];
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* The outcome of {@link SpawnEngine#lint}: the variables a spawn's templates
|
|
81
|
+
* reference that are not declared in propertySchema or globals. A spawn is
|
|
82
|
+
* lint-clean when {@code undeclared} is empty.
|
|
83
|
+
*/
|
|
84
|
+
interface SpawnLintResult {
|
|
85
|
+
/** Referenced-but-undeclared variables, sorted by name. */
|
|
86
|
+
undeclared: UndeclaredVariable[];
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
64
89
|
* The outcome of rendering a spawn.
|
|
65
90
|
*/
|
|
66
91
|
interface SpawnRenderResult {
|
|
@@ -113,7 +138,18 @@ declare class SpawnEngine {
|
|
|
113
138
|
* context used, including values added by the property resolver.
|
|
114
139
|
*/
|
|
115
140
|
renderSpawn(spawn: SpawnTree, options?: RenderSpawnOptions): Promise<SpawnRenderResult>;
|
|
141
|
+
/**
|
|
142
|
+
* Reports the variables a spawn's templates reference that are declared
|
|
143
|
+
* neither in propertySchema nor in globals, across the full inheritance
|
|
144
|
+
* chain. Variables come from the path of every file plus the contents of
|
|
145
|
+
* {@code .liquid} files; registered filters are not mistaken for variables.
|
|
146
|
+
* A clean spawn has an empty {@code undeclared} list.
|
|
147
|
+
*/
|
|
148
|
+
lint(spawn: SpawnTree, options?: {
|
|
149
|
+
loadInherited?: (ref: string) => Promise<SpawnTree>;
|
|
150
|
+
}): Promise<SpawnLintResult>;
|
|
151
|
+
private walkInheritance;
|
|
116
152
|
private parseConfig;
|
|
117
153
|
private resolveMissingProperties;
|
|
118
154
|
}
|
|
119
|
-
export { SpawnTree, SpawnRenderResult, SpawnEngine, RenderSpawnOptions, PropertySchema, PropertyResolver };
|
|
155
|
+
export { UndeclaredVariable, SpawnTree, SpawnRenderResult, SpawnLintResult, SpawnEngine, RenderSpawnOptions, PropertySchema, PropertyResolver };
|
package/dist/index.js
CHANGED
package/dist/node/index.cjs
CHANGED
|
@@ -64,6 +64,7 @@ var __export = (target, all) => {
|
|
|
64
64
|
// packages/spawn/src/node/index.ts
|
|
65
65
|
var exports_node = {};
|
|
66
66
|
__export(exports_node, {
|
|
67
|
+
lintSpawnDir: () => lintSpawnDir,
|
|
67
68
|
assertPathWithin: () => assertPathWithin,
|
|
68
69
|
NodeSpawnRenderer: () => NodeSpawnRenderer
|
|
69
70
|
});
|
|
@@ -108,23 +109,7 @@ class SpawnEngine {
|
|
|
108
109
|
this.engine.registerFilter("upperFirst", (v) => upperFirst(v));
|
|
109
110
|
}
|
|
110
111
|
async renderSpawn(spawn, options) {
|
|
111
|
-
const trees =
|
|
112
|
-
const configs = [];
|
|
113
|
-
let currentConfig = this.parseConfig(spawn);
|
|
114
|
-
if (currentConfig) {
|
|
115
|
-
configs.push(currentConfig);
|
|
116
|
-
}
|
|
117
|
-
while (currentConfig?.inherits) {
|
|
118
|
-
if (!options?.loadInherited) {
|
|
119
|
-
throw new Error(`Spawn inherits '${currentConfig.inherits}' but no loadInherited callback was provided`);
|
|
120
|
-
}
|
|
121
|
-
const inherited = await options.loadInherited(currentConfig.inherits);
|
|
122
|
-
trees.push(inherited);
|
|
123
|
-
currentConfig = this.parseConfig(inherited);
|
|
124
|
-
if (currentConfig) {
|
|
125
|
-
configs.push(currentConfig);
|
|
126
|
-
}
|
|
127
|
-
}
|
|
112
|
+
const { trees, configs } = await this.walkInheritance(spawn, options?.loadInherited);
|
|
128
113
|
let globals = {};
|
|
129
114
|
let propertySchemas = {};
|
|
130
115
|
for (const config of [...configs].reverse()) {
|
|
@@ -163,6 +148,69 @@ class SpawnEngine {
|
|
|
163
148
|
}
|
|
164
149
|
return { files, sources, context };
|
|
165
150
|
}
|
|
151
|
+
async lint(spawn, options) {
|
|
152
|
+
const { trees, configs } = await this.walkInheritance(spawn, options?.loadInherited);
|
|
153
|
+
const declared = new Set;
|
|
154
|
+
for (const config of configs) {
|
|
155
|
+
if (config.globals) {
|
|
156
|
+
Object.keys(config.globals).forEach((key) => declared.add(key));
|
|
157
|
+
}
|
|
158
|
+
if (config.propertySchema) {
|
|
159
|
+
Object.keys(config.propertySchema).forEach((key) => declared.add(key));
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
const usedIn = new Map;
|
|
163
|
+
const record = (name, file) => {
|
|
164
|
+
let files = usedIn.get(name);
|
|
165
|
+
if (!files) {
|
|
166
|
+
files = new Set;
|
|
167
|
+
usedIn.set(name, files);
|
|
168
|
+
}
|
|
169
|
+
files.add(file);
|
|
170
|
+
};
|
|
171
|
+
const externalVars = (template) => Object.keys(this.engine.parseAndAnalyzeSync(template, undefined, { partials: false }).globals);
|
|
172
|
+
for (const tree of trees) {
|
|
173
|
+
for (const source of Object.keys(tree)) {
|
|
174
|
+
const fileName = source.substring(source.lastIndexOf("/") + 1);
|
|
175
|
+
if (IGNORED_FILE_NAMES.includes(fileName)) {
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
externalVars(source).forEach((name) => record(name, source));
|
|
179
|
+
const content = tree[source];
|
|
180
|
+
if (source.endsWith(".liquid") && typeof content === "string") {
|
|
181
|
+
externalVars(content).forEach((name) => record(name, source));
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
const undeclared = [];
|
|
186
|
+
for (const [name, files] of usedIn) {
|
|
187
|
+
if (!declared.has(name)) {
|
|
188
|
+
undeclared.push({ name, files: [...files].sort() });
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
undeclared.sort((a, b) => a.name.localeCompare(b.name));
|
|
192
|
+
return { undeclared };
|
|
193
|
+
}
|
|
194
|
+
async walkInheritance(spawn, loadInherited) {
|
|
195
|
+
const trees = [spawn];
|
|
196
|
+
const configs = [];
|
|
197
|
+
let currentConfig = this.parseConfig(spawn);
|
|
198
|
+
if (currentConfig) {
|
|
199
|
+
configs.push(currentConfig);
|
|
200
|
+
}
|
|
201
|
+
while (currentConfig?.inherits) {
|
|
202
|
+
if (!loadInherited) {
|
|
203
|
+
throw new Error(`Spawn inherits '${currentConfig.inherits}' but no loadInherited callback was provided`);
|
|
204
|
+
}
|
|
205
|
+
const inherited = await loadInherited(currentConfig.inherits);
|
|
206
|
+
trees.push(inherited);
|
|
207
|
+
currentConfig = this.parseConfig(inherited);
|
|
208
|
+
if (currentConfig) {
|
|
209
|
+
configs.push(currentConfig);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return { trees, configs };
|
|
213
|
+
}
|
|
166
214
|
parseConfig(tree) {
|
|
167
215
|
const raw = tree["spawn.json"];
|
|
168
216
|
if (raw === undefined) {
|
|
@@ -207,6 +255,36 @@ function assertContained(root, resolved) {
|
|
|
207
255
|
function assertPathWithin(root, target) {
|
|
208
256
|
return assertContained(root, import_node_path.default.resolve(root, target));
|
|
209
257
|
}
|
|
258
|
+
async function loadSpawnTree(dir) {
|
|
259
|
+
const tree = {};
|
|
260
|
+
const entries = await import_promises.default.readdir(dir, { recursive: true, withFileTypes: true });
|
|
261
|
+
for (const entry of entries) {
|
|
262
|
+
if (!entry.isFile()) {
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
const filePath = import_node_path.default.join(entry.parentPath, entry.name);
|
|
266
|
+
const treePath = import_node_path.default.relative(dir, filePath).split(import_node_path.default.sep).join("/");
|
|
267
|
+
if (treePath.endsWith(".liquid")) {
|
|
268
|
+
tree[treePath] = await import_promises.default.readFile(filePath, { encoding: "utf8" });
|
|
269
|
+
} else {
|
|
270
|
+
tree[treePath] = await import_promises.default.readFile(filePath);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
return tree;
|
|
274
|
+
}
|
|
275
|
+
function diskInheritanceLoader(spawnRoot) {
|
|
276
|
+
let currentDir = spawnRoot;
|
|
277
|
+
return async (ref) => {
|
|
278
|
+
currentDir = assertContained(spawnRoot, import_node_path.default.resolve(currentDir, ref));
|
|
279
|
+
if (!import_node_fs.default.existsSync(import_node_path.default.resolve(currentDir, "spawn.json"))) {
|
|
280
|
+
throw new Error(`Inherited spawn ${import_node_path.default.resolve(currentDir, "spawn.json")} does not exist`);
|
|
281
|
+
}
|
|
282
|
+
return loadSpawnTree(currentDir);
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
async function lintSpawnDir(dir) {
|
|
286
|
+
return new SpawnEngine().lint(await loadSpawnTree(dir), { loadInherited: diskInheritanceLoader(dir) });
|
|
287
|
+
}
|
|
210
288
|
|
|
211
289
|
class NodeSpawnRenderer {
|
|
212
290
|
engine = new SpawnEngine;
|
|
@@ -214,38 +292,14 @@ class NodeSpawnRenderer {
|
|
|
214
292
|
if (import_node_fs.default.existsSync(destination)) {
|
|
215
293
|
throw new Error(`The target directory ${destination} already exists`);
|
|
216
294
|
}
|
|
217
|
-
|
|
218
|
-
const result = await this.engine.renderSpawn(await this.loadSpawnTree(spawnDir), {
|
|
295
|
+
const result = await this.engine.renderSpawn(await loadSpawnTree(spawnDir), {
|
|
219
296
|
context: options?.context,
|
|
220
297
|
propertyResolver: options?.propertyResolver,
|
|
221
|
-
loadInherited:
|
|
222
|
-
currentDir = assertContained(spawnDir, import_node_path.default.resolve(currentDir, ref));
|
|
223
|
-
if (!import_node_fs.default.existsSync(import_node_path.default.resolve(currentDir, "spawn.json"))) {
|
|
224
|
-
throw new Error(`Inherited spawn ${import_node_path.default.resolve(currentDir, "spawn.json")} does not exist`);
|
|
225
|
-
}
|
|
226
|
-
return this.loadSpawnTree(currentDir);
|
|
227
|
-
}
|
|
298
|
+
loadInherited: diskInheritanceLoader(spawnDir)
|
|
228
299
|
});
|
|
229
300
|
await this.writeSpawnTree(result.files, destination);
|
|
230
301
|
return result.context;
|
|
231
302
|
}
|
|
232
|
-
async loadSpawnTree(dir) {
|
|
233
|
-
const tree = {};
|
|
234
|
-
const entries = await import_promises.default.readdir(dir, { recursive: true, withFileTypes: true });
|
|
235
|
-
for (const entry of entries) {
|
|
236
|
-
if (!entry.isFile()) {
|
|
237
|
-
continue;
|
|
238
|
-
}
|
|
239
|
-
const filePath = import_node_path.default.join(entry.parentPath, entry.name);
|
|
240
|
-
const treePath = import_node_path.default.relative(dir, filePath).split(import_node_path.default.sep).join("/");
|
|
241
|
-
if (treePath.endsWith(".liquid")) {
|
|
242
|
-
tree[treePath] = await import_promises.default.readFile(filePath, { encoding: "utf8" });
|
|
243
|
-
} else {
|
|
244
|
-
tree[treePath] = await import_promises.default.readFile(filePath);
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
return tree;
|
|
248
|
-
}
|
|
249
303
|
async writeSpawnTree(tree, destination) {
|
|
250
304
|
await import_promises.default.mkdir(destination, { recursive: true });
|
|
251
305
|
for (const [treePath, content] of Object.entries(tree)) {
|
package/dist/node/index.d.cts
CHANGED
|
@@ -31,6 +31,31 @@ interface PropertyResolver {
|
|
|
31
31
|
resolve(key: string, schema: PropertySchema, message: string, defaultValue?: unknown): Promise<unknown>;
|
|
32
32
|
}
|
|
33
33
|
/**
|
|
34
|
+
* A variable referenced in a spawn's templates that is declared neither in
|
|
35
|
+
* propertySchema nor in globals, reported by {@link SpawnEngine#lint}.
|
|
36
|
+
*
|
|
37
|
+
* @see SpawnLintResult
|
|
38
|
+
*/
|
|
39
|
+
interface UndeclaredVariable {
|
|
40
|
+
/** The variable name as referenced in the templates. */
|
|
41
|
+
name: string;
|
|
42
|
+
/**
|
|
43
|
+
* The spawn paths where the variable appears — file contents for
|
|
44
|
+
* {@code .liquid} files, and path templates for any file whose path contains
|
|
45
|
+
* the variable.
|
|
46
|
+
*/
|
|
47
|
+
files: string[];
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* The outcome of {@link SpawnEngine#lint}: the variables a spawn's templates
|
|
51
|
+
* reference that are not declared in propertySchema or globals. A spawn is
|
|
52
|
+
* lint-clean when {@code undeclared} is empty.
|
|
53
|
+
*/
|
|
54
|
+
interface SpawnLintResult {
|
|
55
|
+
/** Referenced-but-undeclared variables, sorted by name. */
|
|
56
|
+
undeclared: UndeclaredVariable[];
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
34
59
|
* Resolves {@code target} against {@code root} and asserts the result stays at or
|
|
35
60
|
* below {@code root}, so a path can't escape the directory being operated in via
|
|
36
61
|
* {@code ..} (whether authored that way or injected through a property value).
|
|
@@ -38,6 +63,12 @@ interface PropertyResolver {
|
|
|
38
63
|
*/
|
|
39
64
|
declare function assertPathWithin(root: string, target: string): string;
|
|
40
65
|
/**
|
|
66
|
+
* Reports the variables a spawn directory's templates reference that are
|
|
67
|
+
* declared neither in propertySchema nor in globals (following inheritance on
|
|
68
|
+
* disk, confined to {@code dir}). See {@link SpawnEngine#lint}.
|
|
69
|
+
*/
|
|
70
|
+
declare function lintSpawnDir(dir: string): Promise<SpawnLintResult>;
|
|
71
|
+
/**
|
|
41
72
|
* Options for {@link NodeSpawnRenderer#render}.
|
|
42
73
|
*/
|
|
43
74
|
interface NodeRenderOptions {
|
|
@@ -66,7 +97,6 @@ declare class NodeSpawnRenderer {
|
|
|
66
97
|
* rendered path escapes its root, or a required property has no value
|
|
67
98
|
*/
|
|
68
99
|
render(spawnDir: string, destination: string, options?: NodeRenderOptions): Promise<Record<string, unknown>>;
|
|
69
|
-
private loadSpawnTree;
|
|
70
100
|
private writeSpawnTree;
|
|
71
101
|
}
|
|
72
|
-
export { assertPathWithin, NodeSpawnRenderer, NodeRenderOptions };
|
|
102
|
+
export { lintSpawnDir, assertPathWithin, NodeSpawnRenderer, NodeRenderOptions };
|
package/dist/node/index.d.ts
CHANGED
|
@@ -31,6 +31,31 @@ interface PropertyResolver {
|
|
|
31
31
|
resolve(key: string, schema: PropertySchema, message: string, defaultValue?: unknown): Promise<unknown>;
|
|
32
32
|
}
|
|
33
33
|
/**
|
|
34
|
+
* A variable referenced in a spawn's templates that is declared neither in
|
|
35
|
+
* propertySchema nor in globals, reported by {@link SpawnEngine#lint}.
|
|
36
|
+
*
|
|
37
|
+
* @see SpawnLintResult
|
|
38
|
+
*/
|
|
39
|
+
interface UndeclaredVariable {
|
|
40
|
+
/** The variable name as referenced in the templates. */
|
|
41
|
+
name: string;
|
|
42
|
+
/**
|
|
43
|
+
* The spawn paths where the variable appears — file contents for
|
|
44
|
+
* {@code .liquid} files, and path templates for any file whose path contains
|
|
45
|
+
* the variable.
|
|
46
|
+
*/
|
|
47
|
+
files: string[];
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* The outcome of {@link SpawnEngine#lint}: the variables a spawn's templates
|
|
51
|
+
* reference that are not declared in propertySchema or globals. A spawn is
|
|
52
|
+
* lint-clean when {@code undeclared} is empty.
|
|
53
|
+
*/
|
|
54
|
+
interface SpawnLintResult {
|
|
55
|
+
/** Referenced-but-undeclared variables, sorted by name. */
|
|
56
|
+
undeclared: UndeclaredVariable[];
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
34
59
|
* Resolves {@code target} against {@code root} and asserts the result stays at or
|
|
35
60
|
* below {@code root}, so a path can't escape the directory being operated in via
|
|
36
61
|
* {@code ..} (whether authored that way or injected through a property value).
|
|
@@ -38,6 +63,12 @@ interface PropertyResolver {
|
|
|
38
63
|
*/
|
|
39
64
|
declare function assertPathWithin(root: string, target: string): string;
|
|
40
65
|
/**
|
|
66
|
+
* Reports the variables a spawn directory's templates reference that are
|
|
67
|
+
* declared neither in propertySchema nor in globals (following inheritance on
|
|
68
|
+
* disk, confined to {@code dir}). See {@link SpawnEngine#lint}.
|
|
69
|
+
*/
|
|
70
|
+
declare function lintSpawnDir(dir: string): Promise<SpawnLintResult>;
|
|
71
|
+
/**
|
|
41
72
|
* Options for {@link NodeSpawnRenderer#render}.
|
|
42
73
|
*/
|
|
43
74
|
interface NodeRenderOptions {
|
|
@@ -66,7 +97,6 @@ declare class NodeSpawnRenderer {
|
|
|
66
97
|
* rendered path escapes its root, or a required property has no value
|
|
67
98
|
*/
|
|
68
99
|
render(spawnDir: string, destination: string, options?: NodeRenderOptions): Promise<Record<string, unknown>>;
|
|
69
|
-
private loadSpawnTree;
|
|
70
100
|
private writeSpawnTree;
|
|
71
101
|
}
|
|
72
|
-
export { assertPathWithin, NodeSpawnRenderer, NodeRenderOptions };
|
|
102
|
+
export { lintSpawnDir, assertPathWithin, NodeSpawnRenderer, NodeRenderOptions };
|
package/dist/node/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
SpawnEngine
|
|
3
|
-
} from "../shared/spawn-
|
|
3
|
+
} from "../shared/spawn-1h1k8w8r.js";
|
|
4
4
|
|
|
5
5
|
// packages/spawn/src/node/index.ts
|
|
6
6
|
import fs from "node:fs";
|
|
@@ -16,6 +16,36 @@ function assertContained(root, resolved) {
|
|
|
16
16
|
function assertPathWithin(root, target) {
|
|
17
17
|
return assertContained(root, path.resolve(root, target));
|
|
18
18
|
}
|
|
19
|
+
async function loadSpawnTree(dir) {
|
|
20
|
+
const tree = {};
|
|
21
|
+
const entries = await fsP.readdir(dir, { recursive: true, withFileTypes: true });
|
|
22
|
+
for (const entry of entries) {
|
|
23
|
+
if (!entry.isFile()) {
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
const filePath = path.join(entry.parentPath, entry.name);
|
|
27
|
+
const treePath = path.relative(dir, filePath).split(path.sep).join("/");
|
|
28
|
+
if (treePath.endsWith(".liquid")) {
|
|
29
|
+
tree[treePath] = await fsP.readFile(filePath, { encoding: "utf8" });
|
|
30
|
+
} else {
|
|
31
|
+
tree[treePath] = await fsP.readFile(filePath);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return tree;
|
|
35
|
+
}
|
|
36
|
+
function diskInheritanceLoader(spawnRoot) {
|
|
37
|
+
let currentDir = spawnRoot;
|
|
38
|
+
return async (ref) => {
|
|
39
|
+
currentDir = assertContained(spawnRoot, path.resolve(currentDir, ref));
|
|
40
|
+
if (!fs.existsSync(path.resolve(currentDir, "spawn.json"))) {
|
|
41
|
+
throw new Error(`Inherited spawn ${path.resolve(currentDir, "spawn.json")} does not exist`);
|
|
42
|
+
}
|
|
43
|
+
return loadSpawnTree(currentDir);
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
async function lintSpawnDir(dir) {
|
|
47
|
+
return new SpawnEngine().lint(await loadSpawnTree(dir), { loadInherited: diskInheritanceLoader(dir) });
|
|
48
|
+
}
|
|
19
49
|
|
|
20
50
|
class NodeSpawnRenderer {
|
|
21
51
|
engine = new SpawnEngine;
|
|
@@ -23,38 +53,14 @@ class NodeSpawnRenderer {
|
|
|
23
53
|
if (fs.existsSync(destination)) {
|
|
24
54
|
throw new Error(`The target directory ${destination} already exists`);
|
|
25
55
|
}
|
|
26
|
-
|
|
27
|
-
const result = await this.engine.renderSpawn(await this.loadSpawnTree(spawnDir), {
|
|
56
|
+
const result = await this.engine.renderSpawn(await loadSpawnTree(spawnDir), {
|
|
28
57
|
context: options?.context,
|
|
29
58
|
propertyResolver: options?.propertyResolver,
|
|
30
|
-
loadInherited:
|
|
31
|
-
currentDir = assertContained(spawnDir, path.resolve(currentDir, ref));
|
|
32
|
-
if (!fs.existsSync(path.resolve(currentDir, "spawn.json"))) {
|
|
33
|
-
throw new Error(`Inherited spawn ${path.resolve(currentDir, "spawn.json")} does not exist`);
|
|
34
|
-
}
|
|
35
|
-
return this.loadSpawnTree(currentDir);
|
|
36
|
-
}
|
|
59
|
+
loadInherited: diskInheritanceLoader(spawnDir)
|
|
37
60
|
});
|
|
38
61
|
await this.writeSpawnTree(result.files, destination);
|
|
39
62
|
return result.context;
|
|
40
63
|
}
|
|
41
|
-
async loadSpawnTree(dir) {
|
|
42
|
-
const tree = {};
|
|
43
|
-
const entries = await fsP.readdir(dir, { recursive: true, withFileTypes: true });
|
|
44
|
-
for (const entry of entries) {
|
|
45
|
-
if (!entry.isFile()) {
|
|
46
|
-
continue;
|
|
47
|
-
}
|
|
48
|
-
const filePath = path.join(entry.parentPath, entry.name);
|
|
49
|
-
const treePath = path.relative(dir, filePath).split(path.sep).join("/");
|
|
50
|
-
if (treePath.endsWith(".liquid")) {
|
|
51
|
-
tree[treePath] = await fsP.readFile(filePath, { encoding: "utf8" });
|
|
52
|
-
} else {
|
|
53
|
-
tree[treePath] = await fsP.readFile(filePath);
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
return tree;
|
|
57
|
-
}
|
|
58
64
|
async writeSpawnTree(tree, destination) {
|
|
59
65
|
await fsP.mkdir(destination, { recursive: true });
|
|
60
66
|
for (const [treePath, content] of Object.entries(tree)) {
|
|
@@ -65,6 +71,7 @@ class NodeSpawnRenderer {
|
|
|
65
71
|
}
|
|
66
72
|
}
|
|
67
73
|
export {
|
|
74
|
+
lintSpawnDir,
|
|
68
75
|
assertPathWithin,
|
|
69
76
|
NodeSpawnRenderer
|
|
70
77
|
};
|
|
@@ -34,23 +34,7 @@ class SpawnEngine {
|
|
|
34
34
|
this.engine.registerFilter("upperFirst", (v) => upperFirst(v));
|
|
35
35
|
}
|
|
36
36
|
async renderSpawn(spawn, options) {
|
|
37
|
-
const trees =
|
|
38
|
-
const configs = [];
|
|
39
|
-
let currentConfig = this.parseConfig(spawn);
|
|
40
|
-
if (currentConfig) {
|
|
41
|
-
configs.push(currentConfig);
|
|
42
|
-
}
|
|
43
|
-
while (currentConfig?.inherits) {
|
|
44
|
-
if (!options?.loadInherited) {
|
|
45
|
-
throw new Error(`Spawn inherits '${currentConfig.inherits}' but no loadInherited callback was provided`);
|
|
46
|
-
}
|
|
47
|
-
const inherited = await options.loadInherited(currentConfig.inherits);
|
|
48
|
-
trees.push(inherited);
|
|
49
|
-
currentConfig = this.parseConfig(inherited);
|
|
50
|
-
if (currentConfig) {
|
|
51
|
-
configs.push(currentConfig);
|
|
52
|
-
}
|
|
53
|
-
}
|
|
37
|
+
const { trees, configs } = await this.walkInheritance(spawn, options?.loadInherited);
|
|
54
38
|
let globals = {};
|
|
55
39
|
let propertySchemas = {};
|
|
56
40
|
for (const config of [...configs].reverse()) {
|
|
@@ -89,6 +73,69 @@ class SpawnEngine {
|
|
|
89
73
|
}
|
|
90
74
|
return { files, sources, context };
|
|
91
75
|
}
|
|
76
|
+
async lint(spawn, options) {
|
|
77
|
+
const { trees, configs } = await this.walkInheritance(spawn, options?.loadInherited);
|
|
78
|
+
const declared = new Set;
|
|
79
|
+
for (const config of configs) {
|
|
80
|
+
if (config.globals) {
|
|
81
|
+
Object.keys(config.globals).forEach((key) => declared.add(key));
|
|
82
|
+
}
|
|
83
|
+
if (config.propertySchema) {
|
|
84
|
+
Object.keys(config.propertySchema).forEach((key) => declared.add(key));
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
const usedIn = new Map;
|
|
88
|
+
const record = (name, file) => {
|
|
89
|
+
let files = usedIn.get(name);
|
|
90
|
+
if (!files) {
|
|
91
|
+
files = new Set;
|
|
92
|
+
usedIn.set(name, files);
|
|
93
|
+
}
|
|
94
|
+
files.add(file);
|
|
95
|
+
};
|
|
96
|
+
const externalVars = (template) => Object.keys(this.engine.parseAndAnalyzeSync(template, undefined, { partials: false }).globals);
|
|
97
|
+
for (const tree of trees) {
|
|
98
|
+
for (const source of Object.keys(tree)) {
|
|
99
|
+
const fileName = source.substring(source.lastIndexOf("/") + 1);
|
|
100
|
+
if (IGNORED_FILE_NAMES.includes(fileName)) {
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
externalVars(source).forEach((name) => record(name, source));
|
|
104
|
+
const content = tree[source];
|
|
105
|
+
if (source.endsWith(".liquid") && typeof content === "string") {
|
|
106
|
+
externalVars(content).forEach((name) => record(name, source));
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
const undeclared = [];
|
|
111
|
+
for (const [name, files] of usedIn) {
|
|
112
|
+
if (!declared.has(name)) {
|
|
113
|
+
undeclared.push({ name, files: [...files].sort() });
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
undeclared.sort((a, b) => a.name.localeCompare(b.name));
|
|
117
|
+
return { undeclared };
|
|
118
|
+
}
|
|
119
|
+
async walkInheritance(spawn, loadInherited) {
|
|
120
|
+
const trees = [spawn];
|
|
121
|
+
const configs = [];
|
|
122
|
+
let currentConfig = this.parseConfig(spawn);
|
|
123
|
+
if (currentConfig) {
|
|
124
|
+
configs.push(currentConfig);
|
|
125
|
+
}
|
|
126
|
+
while (currentConfig?.inherits) {
|
|
127
|
+
if (!loadInherited) {
|
|
128
|
+
throw new Error(`Spawn inherits '${currentConfig.inherits}' but no loadInherited callback was provided`);
|
|
129
|
+
}
|
|
130
|
+
const inherited = await loadInherited(currentConfig.inherits);
|
|
131
|
+
trees.push(inherited);
|
|
132
|
+
currentConfig = this.parseConfig(inherited);
|
|
133
|
+
if (currentConfig) {
|
|
134
|
+
configs.push(currentConfig);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return { trees, configs };
|
|
138
|
+
}
|
|
92
139
|
parseConfig(tree) {
|
|
93
140
|
const raw = tree["spawn.json"];
|
|
94
141
|
if (raw === undefined) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kinotic-ai/spawn",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"files": [
|
|
6
6
|
"dist"
|
|
@@ -43,8 +43,8 @@
|
|
|
43
43
|
"build:graal-spawn-renderer": "bun build ./src/graalSpawnRendererMain.ts --outfile ./dist/graal-spawn-renderer.js --format=iife --target=browser"
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
|
-
"liquidjs": "10.
|
|
47
|
-
"zod": "
|
|
46
|
+
"liquidjs": "10.27.0",
|
|
47
|
+
"zod": "4.4.3"
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
50
50
|
"@types/node": "^25.3.2",
|