@intentius/chant-lexicon-fly 0.124.46 → 0.124.48
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/README.md +1 -1
- package/dist/export-resources.d.ts.map +1 -1
- package/dist/export-resources.js +12 -3
- package/dist/import/embedded.d.ts +33 -0
- package/dist/import/embedded.d.ts.map +1 -0
- package/dist/import/embedded.js +79 -0
- package/dist/import/generator.d.ts.map +1 -1
- package/dist/import/generator.js +40 -15
- package/dist/import/live-export.d.ts.map +1 -1
- package/dist/import/live-export.js +8 -2
- package/dist/import/parser.d.ts +10 -2
- package/dist/import/parser.d.ts.map +1 -1
- package/dist/import/parser.js +18 -1
- package/dist/integrity.json +3 -3
- package/dist/manifest.json +1 -1
- package/dist/okf/types/App.md +4 -4
- package/dist/types/index.d.ts +4 -0
- package/package.json +3 -3
- package/src/export-resources.ts +12 -3
- package/src/generated/index.d.ts +4 -0
- package/src/import/embedded-live.test.ts +190 -0
- package/src/import/embedded.ts +90 -0
- package/src/import/generator.ts +42 -15
- package/src/import/live-export.ts +9 -2
- package/src/import/parser.ts +22 -1
package/README.md
CHANGED
|
@@ -111,7 +111,7 @@ The agent-facing entry point is the `chant-fly` skill: a short operational playb
|
|
|
111
111
|
| ------------------------------------------------------------------------------------------ | ------------------------------------- |
|
|
112
112
|
| [@intentius/chant](https://www.npmjs.com/package/@intentius/chant) | Core type system, CLI, build pipeline |
|
|
113
113
|
| [@intentius/chant-lexicon-aws](https://www.npmjs.com/package/@intentius/chant-lexicon-aws) | AWS CloudFormation lexicon |
|
|
114
|
-
| [@intentius/chant-lexicon-gcp](https://www.npmjs.com/package/@intentius/chant-lexicon-gcp) | GCP
|
|
114
|
+
| [@intentius/chant-lexicon-gcp](https://www.npmjs.com/package/@intentius/chant-lexicon-gcp) | GCP Config Connector lexicon |
|
|
115
115
|
|
|
116
116
|
## License
|
|
117
117
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"export-resources.d.ts","sourceRoot":"","sources":["../src/export-resources.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AACnF,OAAO,EAIL,KAAK,OAAO,EAEb,MAAM,2BAA2B,CAAC;AA8CnC,wBAAsB,eAAe,CACnC,OAAO,EAAE;IACP,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,gBAAgB,CAAC;IAC5B,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB,EACD,IAAI,GAAE,OAA0B,EAChC,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,gBAAgB,CAAC,
|
|
1
|
+
{"version":3,"file":"export-resources.d.ts","sourceRoot":"","sources":["../src/export-resources.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AACnF,OAAO,EAIL,KAAK,OAAO,EAEb,MAAM,2BAA2B,CAAC;AA8CnC,wBAAsB,eAAe,CACnC,OAAO,EAAE;IACP,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,gBAAgB,CAAC;IAC5B,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB,EACD,IAAI,GAAE,OAA0B,EAChC,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,gBAAgB,CAAC,CAoD3B"}
|
package/dist/export-resources.js
CHANGED
|
@@ -61,7 +61,7 @@ export async function exportResources(options, http = defaultFlyHttp(), signal)
|
|
|
61
61
|
loggedBoundary = true;
|
|
62
62
|
console.warn("[fly] apps carry no per-resource ownership marker — ownership is inferred at the app boundary (#741/#743); only apps with a chant-managed machine are exported under --owned");
|
|
63
63
|
};
|
|
64
|
-
const
|
|
64
|
+
const parts = [];
|
|
65
65
|
for (const app of apps) {
|
|
66
66
|
const appRes = await http("GET", `${ctx.base}/v1/apps/${encodeURIComponent(app)}`, undefined, undefined, signal);
|
|
67
67
|
const appBody = appRes.status === 200 ? parseJson(appRes.text) : undefined;
|
|
@@ -75,7 +75,16 @@ export async function exportResources(options, http = defaultFlyHttp(), signal)
|
|
|
75
75
|
owned: options.owned,
|
|
76
76
|
onBoundaryInference,
|
|
77
77
|
});
|
|
78
|
-
|
|
78
|
+
parts.push(ir);
|
|
79
79
|
}
|
|
80
|
-
|
|
80
|
+
// Each app's export re-parses its own bundle; together they re-parse the
|
|
81
|
+
// whole export, so embedded collector configs reach the otel importer (#3373).
|
|
82
|
+
return {
|
|
83
|
+
resources: parts.flatMap((p) => p.resources),
|
|
84
|
+
parameters: [],
|
|
85
|
+
reparse: (context) => ({
|
|
86
|
+
resources: parts.flatMap((p) => (p.reparse ? p.reparse(context) : p).resources),
|
|
87
|
+
parameters: [],
|
|
88
|
+
}),
|
|
89
|
+
};
|
|
81
90
|
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where a Fly Machine embeds another lexicon's content, for `chant import`
|
|
3
|
+
* (#3373).
|
|
4
|
+
*
|
|
5
|
+
* A machine's `config.files[]` entry with a `raw_value` is a file written into
|
|
6
|
+
* the machine, base64 in flaps. The parser decodes each one and offers the text
|
|
7
|
+
* to core's embedded-content resolver, as k8s offers a ConfigMap value
|
|
8
|
+
* (#2962). `FlyOtelCollector` writes its collector config this way, at the otel
|
|
9
|
+
* lexicon's `COLLECTOR_CONFIG_PATH`, so a live or file import of that machine
|
|
10
|
+
* gives the same otel source `chant import config.yaml` gives.
|
|
11
|
+
*
|
|
12
|
+
* Which lexicon imports the content is decided by core at run time; the hint
|
|
13
|
+
* below only names the owner a warning suggests when none is installed.
|
|
14
|
+
*/
|
|
15
|
+
import type { EmbeddedContentResolver, EmbeddedReference } from "@intentius/chant/import/embedded";
|
|
16
|
+
/** The UTF-8 text a base64 `raw_value` holds, or undefined when it is not base64 of text. */
|
|
17
|
+
export declare function decodeRawValue(raw: string): string | undefined;
|
|
18
|
+
/**
|
|
19
|
+
* The marker the parser leaves on a file whose `raw_value` an owner imports:
|
|
20
|
+
* the generator renders it as the base64 of the reference's text.
|
|
21
|
+
*/
|
|
22
|
+
export interface EmbeddedRawValue {
|
|
23
|
+
readonly $base64: EmbeddedReference;
|
|
24
|
+
}
|
|
25
|
+
export declare function isEmbeddedRawValue(v: unknown): v is EmbeddedRawValue;
|
|
26
|
+
/**
|
|
27
|
+
* Offer each `config.files[].raw_value` of one machine to `embedded`, replacing
|
|
28
|
+
* the value an owner imports with an {@link EmbeddedRawValue}. `properties` is
|
|
29
|
+
* the machine's IR properties, changed in place with fresh copies on the way
|
|
30
|
+
* down.
|
|
31
|
+
*/
|
|
32
|
+
export declare function delegateEmbedded(machineName: string, properties: Record<string, unknown>, embedded: EmbeddedContentResolver): void;
|
|
33
|
+
//# sourceMappingURL=embedded.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"embedded.d.ts","sourceRoot":"","sources":["../../src/import/embedded.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAmB,uBAAuB,EAAE,iBAAiB,EAAE,MAAM,kCAAkC,CAAC;AAQpH,6FAA6F;AAC7F,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAM9D;AAUD;;;GAGG;AACH,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,OAAO,EAAE,iBAAiB,CAAC;CACrC;AAED,wBAAgB,kBAAkB,CAAC,CAAC,EAAE,OAAO,GAAG,CAAC,IAAI,gBAAgB,CAEpE;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAC9B,WAAW,EAAE,MAAM,EACnB,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACnC,QAAQ,EAAE,uBAAuB,GAChC,IAAI,CA2BN"}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where a Fly Machine embeds another lexicon's content, for `chant import`
|
|
3
|
+
* (#3373).
|
|
4
|
+
*
|
|
5
|
+
* A machine's `config.files[]` entry with a `raw_value` is a file written into
|
|
6
|
+
* the machine, base64 in flaps. The parser decodes each one and offers the text
|
|
7
|
+
* to core's embedded-content resolver, as k8s offers a ConfigMap value
|
|
8
|
+
* (#2962). `FlyOtelCollector` writes its collector config this way, at the otel
|
|
9
|
+
* lexicon's `COLLECTOR_CONFIG_PATH`, so a live or file import of that machine
|
|
10
|
+
* gives the same otel source `chant import config.yaml` gives.
|
|
11
|
+
*
|
|
12
|
+
* Which lexicon imports the content is decided by core at run time; the hint
|
|
13
|
+
* below only names the owner a warning suggests when none is installed.
|
|
14
|
+
*/
|
|
15
|
+
import { embeddedDocument } from "@intentius/chant/import/embedded";
|
|
16
|
+
import { COLLECTOR_CONFIG_PATH } from "@intentius/chant-lexicon-otel";
|
|
17
|
+
function isRecord(v) {
|
|
18
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
19
|
+
}
|
|
20
|
+
/** The UTF-8 text a base64 `raw_value` holds, or undefined when it is not base64 of text. */
|
|
21
|
+
export function decodeRawValue(raw) {
|
|
22
|
+
if (!/^[A-Za-z0-9+/\s]*={0,2}\s*$/.test(raw))
|
|
23
|
+
return undefined;
|
|
24
|
+
const bytes = Buffer.from(raw, "base64");
|
|
25
|
+
const text = bytes.toString("utf8");
|
|
26
|
+
// A round trip that changes the bytes means the file is not UTF-8 text.
|
|
27
|
+
return Buffer.from(text, "utf8").equals(bytes) ? text : undefined;
|
|
28
|
+
}
|
|
29
|
+
function hintFor(guestPath, doc) {
|
|
30
|
+
if (guestPath === COLLECTOR_CONFIG_PATH)
|
|
31
|
+
return { lexicon: "otel", what: "an OpenTelemetry Collector config" };
|
|
32
|
+
if (isRecord(doc) && isRecord(doc.service) && isRecord(doc.service.pipelines)) {
|
|
33
|
+
return { lexicon: "otel", what: "an OpenTelemetry Collector config" };
|
|
34
|
+
}
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
export function isEmbeddedRawValue(v) {
|
|
38
|
+
return isRecord(v) && isRecord(v.$base64);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Offer each `config.files[].raw_value` of one machine to `embedded`, replacing
|
|
42
|
+
* the value an owner imports with an {@link EmbeddedRawValue}. `properties` is
|
|
43
|
+
* the machine's IR properties, changed in place with fresh copies on the way
|
|
44
|
+
* down.
|
|
45
|
+
*/
|
|
46
|
+
export function delegateEmbedded(machineName, properties, embedded) {
|
|
47
|
+
const config = properties.config;
|
|
48
|
+
if (!isRecord(config) || !Array.isArray(config.files))
|
|
49
|
+
return;
|
|
50
|
+
const files = config.files;
|
|
51
|
+
let changed = false;
|
|
52
|
+
const next = files.map((file, i) => {
|
|
53
|
+
if (!isRecord(file) || typeof file.raw_value !== "string")
|
|
54
|
+
return file;
|
|
55
|
+
const text = decodeRawValue(file.raw_value);
|
|
56
|
+
if (text === undefined)
|
|
57
|
+
return file;
|
|
58
|
+
const doc = embeddedDocument(text);
|
|
59
|
+
if (!isRecord(doc))
|
|
60
|
+
return file;
|
|
61
|
+
const guestPath = typeof file.guest_path === "string" ? file.guest_path : `files[${i}]`;
|
|
62
|
+
const base = guestPath.split("/").pop().replace(/\.[^.]*$/, "");
|
|
63
|
+
const ref = embedded.resolve({
|
|
64
|
+
host: "fly",
|
|
65
|
+
hostType: "Fly::Machines::Machine",
|
|
66
|
+
location: `Machine ${machineName} file ${guestPath}`,
|
|
67
|
+
directory: files.length === 1 ? machineName : `${machineName}-${base}`,
|
|
68
|
+
text,
|
|
69
|
+
document: doc,
|
|
70
|
+
expectedOwner: hintFor(guestPath, doc),
|
|
71
|
+
});
|
|
72
|
+
if (!ref)
|
|
73
|
+
return file;
|
|
74
|
+
changed = true;
|
|
75
|
+
return { ...file, raw_value: { $base64: ref } };
|
|
76
|
+
});
|
|
77
|
+
if (changed)
|
|
78
|
+
properties.config = { ...config, files: next };
|
|
79
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"generator.d.ts","sourceRoot":"","sources":["../../src/import/generator.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,KAAK,EAAE,mBAAmB,EAAE,aAAa,EAAE,MAAM,mCAAmC,CAAC;AAC5F,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gCAAgC,CAAC;
|
|
1
|
+
{"version":3,"file":"generator.d.ts","sourceRoot":"","sources":["../../src/import/generator.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,KAAK,EAAE,mBAAmB,EAAE,aAAa,EAAE,MAAM,mCAAmC,CAAC;AAC5F,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gCAAgC,CAAC;AA+FjE,qBAAa,YAAa,YAAW,mBAAmB;IACtD,QAAQ,CAAC,EAAE,EAAE,UAAU,GAAG,aAAa,EAAE,CAqCxC;CACF"}
|
package/dist/import/generator.js
CHANGED
|
@@ -13,6 +13,8 @@
|
|
|
13
13
|
* the same authoring surface `chant init` scaffolds and the serializer walks back
|
|
14
14
|
* to the identical flaps body.
|
|
15
15
|
*/
|
|
16
|
+
import { EmbeddedImportScope, isEmbeddedReference, renderEmbeddedReference } from "@intentius/chant/import/embedded";
|
|
17
|
+
import { isEmbeddedRawValue } from "./embedded.js";
|
|
16
18
|
/**
|
|
17
19
|
* `<OwnerClass> → { <field>: { kind, cls } }` for every field whose declared
|
|
18
20
|
* type is a fly property declarable (a class, an array of one, or a
|
|
@@ -95,20 +97,36 @@ export class FlyGenerator {
|
|
|
95
97
|
generate(ir) {
|
|
96
98
|
const imports = new Set();
|
|
97
99
|
const bodyLines = [];
|
|
100
|
+
// Names an embedded reference's imports must not take: the module's own
|
|
101
|
+
// declarations and every fly class it could import.
|
|
102
|
+
const classNames = new Set(["App", "Machine", ...Object.keys(NESTED_DECLARABLES)]);
|
|
103
|
+
for (const fields of Object.values(NESTED_DECLARABLES))
|
|
104
|
+
for (const n of Object.values(fields))
|
|
105
|
+
classNames.add(n.cls);
|
|
106
|
+
for (const r of ir.resources) {
|
|
107
|
+
const parts = r.type.split("::");
|
|
108
|
+
classNames.add(parts.length >= 3 ? parts[2] : r.type);
|
|
109
|
+
}
|
|
110
|
+
const ctx = {
|
|
111
|
+
imports,
|
|
112
|
+
scope: new EmbeddedImportScope("", [...classNames, "Buffer", ...ir.resources.map((r) => camelCase(r.logicalId))]),
|
|
113
|
+
};
|
|
98
114
|
for (const resource of ir.resources) {
|
|
99
115
|
const parts = resource.type.split("::");
|
|
100
116
|
const className = parts.length >= 3 ? parts[2] : resource.type;
|
|
101
117
|
imports.add(className);
|
|
102
118
|
const varName = camelCase(resource.logicalId);
|
|
103
|
-
const props = formatProps(resource.properties, 0, className,
|
|
119
|
+
const props = formatProps(resource.properties, 0, className, ctx);
|
|
104
120
|
bodyLines.push(`export const ${varName} = new ${className}(${props});`);
|
|
105
121
|
bodyLines.push("");
|
|
106
122
|
}
|
|
107
123
|
const lines = [];
|
|
108
124
|
if (imports.size > 0) {
|
|
109
125
|
lines.push(`import { ${[...imports].sort().join(", ")} } from "@intentius/chant-lexicon-fly";`);
|
|
110
|
-
lines.push("");
|
|
111
126
|
}
|
|
127
|
+
lines.push(...ctx.scope.lines());
|
|
128
|
+
if (lines.length > 0)
|
|
129
|
+
lines.push("");
|
|
112
130
|
lines.push(...bodyLines);
|
|
113
131
|
return [{ path: "main.ts", content: lines.join("\n") + "\n" }];
|
|
114
132
|
}
|
|
@@ -123,7 +141,7 @@ function camelCase(str) {
|
|
|
123
141
|
* type is a nested declarable are wrapped in their constructor; everything else
|
|
124
142
|
* is emitted as a plain literal.
|
|
125
143
|
*/
|
|
126
|
-
function formatProps(props, indent, ownerClass,
|
|
144
|
+
function formatProps(props, indent, ownerClass, ctx) {
|
|
127
145
|
const entries = Object.entries(props);
|
|
128
146
|
if (entries.length === 0)
|
|
129
147
|
return "{}";
|
|
@@ -133,20 +151,20 @@ function formatProps(props, indent, ownerClass, imports) {
|
|
|
133
151
|
const lines = entries.map(([key, value]) => {
|
|
134
152
|
const spec = nested?.[key];
|
|
135
153
|
const rendered = spec !== undefined
|
|
136
|
-
? formatNested(value, indent + 1, spec,
|
|
137
|
-
: formatValue(value, indent + 1);
|
|
154
|
+
? formatNested(value, indent + 1, spec, ctx)
|
|
155
|
+
: formatValue(value, indent + 1, ctx);
|
|
138
156
|
return `${pad}${key}: ${rendered},`;
|
|
139
157
|
});
|
|
140
158
|
return `{\n${lines.join("\n")}\n${closePad}}`;
|
|
141
159
|
}
|
|
142
160
|
/** Wrap a nested-declarable value in its constructor per the field's kind. */
|
|
143
|
-
function formatNested(value, indent, spec,
|
|
161
|
+
function formatNested(value, indent, spec, ctx) {
|
|
144
162
|
const construct = (obj, at) => {
|
|
145
163
|
if (!obj || typeof obj !== "object" || Array.isArray(obj)) {
|
|
146
|
-
return formatValue(obj, at);
|
|
164
|
+
return formatValue(obj, at, ctx);
|
|
147
165
|
}
|
|
148
|
-
imports.add(spec.cls);
|
|
149
|
-
return `new ${spec.cls}(${formatProps(obj, at, spec.cls,
|
|
166
|
+
ctx.imports.add(spec.cls);
|
|
167
|
+
return `new ${spec.cls}(${formatProps(obj, at, spec.cls, ctx)})`;
|
|
150
168
|
};
|
|
151
169
|
if (spec.kind === "single") {
|
|
152
170
|
return construct(value, indent);
|
|
@@ -161,7 +179,7 @@ function formatNested(value, indent, spec, imports) {
|
|
|
161
179
|
}
|
|
162
180
|
// record: a plain object whose values are wrapped in the constructor.
|
|
163
181
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
164
|
-
return formatValue(value, indent);
|
|
182
|
+
return formatValue(value, indent, ctx);
|
|
165
183
|
}
|
|
166
184
|
const record = value;
|
|
167
185
|
const recKeys = Object.entries(record);
|
|
@@ -172,9 +190,16 @@ function formatNested(value, indent, spec, imports) {
|
|
|
172
190
|
const items = recKeys.map(([k, v]) => `${pad}${JSON.stringify(k)}: ${construct(v, indent + 1)},`);
|
|
173
191
|
return `{\n${items.join("\n")}\n${closePad}}`;
|
|
174
192
|
}
|
|
175
|
-
function formatValue(value, indent) {
|
|
193
|
+
function formatValue(value, indent, ctx) {
|
|
176
194
|
if (value === null || value === undefined)
|
|
177
195
|
return "undefined";
|
|
196
|
+
// A machine file another lexicon imported (#3373): flaps wants the file's
|
|
197
|
+
// text base64-encoded, so the reference's text is encoded at build time.
|
|
198
|
+
if (isEmbeddedRawValue(value)) {
|
|
199
|
+
return `Buffer.from(${renderEmbeddedReference(value.$base64, ctx.scope, indent * 2)}, "utf8").toString("base64")`;
|
|
200
|
+
}
|
|
201
|
+
if (isEmbeddedReference(value))
|
|
202
|
+
return renderEmbeddedReference(value, ctx.scope, indent * 2);
|
|
178
203
|
if (typeof value === "string")
|
|
179
204
|
return JSON.stringify(value);
|
|
180
205
|
if (typeof value === "number" || typeof value === "boolean")
|
|
@@ -184,22 +209,22 @@ function formatValue(value, indent) {
|
|
|
184
209
|
return "[]";
|
|
185
210
|
const pad = " ".repeat(indent + 1);
|
|
186
211
|
const closePad = " ".repeat(indent);
|
|
187
|
-
const items = value.map((v) => `${pad}${formatValue(v, indent + 1)},`);
|
|
212
|
+
const items = value.map((v) => `${pad}${formatValue(v, indent + 1, ctx)},`);
|
|
188
213
|
return `[\n${items.join("\n")}\n${closePad}]`;
|
|
189
214
|
}
|
|
190
215
|
if (typeof value === "object") {
|
|
191
|
-
return formatPlainObject(value, indent);
|
|
216
|
+
return formatPlainObject(value, indent, ctx);
|
|
192
217
|
}
|
|
193
218
|
return String(value);
|
|
194
219
|
}
|
|
195
220
|
/** A plain object literal (no constructor wrapping) — for scalar-valued maps like env/metadata. */
|
|
196
|
-
function formatPlainObject(obj, indent) {
|
|
221
|
+
function formatPlainObject(obj, indent, ctx) {
|
|
197
222
|
const entries = Object.entries(obj);
|
|
198
223
|
if (entries.length === 0)
|
|
199
224
|
return "{}";
|
|
200
225
|
const pad = " ".repeat(indent + 1);
|
|
201
226
|
const closePad = " ".repeat(indent);
|
|
202
|
-
const lines = entries.map(([key, value]) => `${pad}${objectKey(key)}: ${formatValue(value, indent + 1)},`);
|
|
227
|
+
const lines = entries.map(([key, value]) => `${pad}${objectKey(key)}: ${formatValue(value, indent + 1, ctx)},`);
|
|
203
228
|
return `{\n${lines.join("\n")}\n${closePad}}`;
|
|
204
229
|
}
|
|
205
230
|
/** Emit a bare identifier key when it is a valid one, else a quoted key. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"live-export.d.ts","sourceRoot":"","sources":["../../src/import/live-export.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;
|
|
1
|
+
{"version":3,"file":"live-export.d.ts","sourceRoot":"","sources":["../../src/import/live-export.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAEnF,OAAO,EAAgB,KAAK,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAW7E,sGAAsG;AACtG,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAIlG;AAED,iGAAiG;AACjG,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAQ1F;AAED;;;;;;;;;GASG;AACH,wBAAgB,kBAAkB,CAChC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,EACxC,QAAQ,EAAE,YAAY,EAAE,EACxB,IAAI,GAAE;IACJ,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,QAAQ,CAAC,EAAE,gBAAgB,CAAC;IAC5B,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,mBAAmB,CAAC,EAAE,MAAM,IAAI,CAAC;CAC7B,GACL,gBAAgB,CA6BlB"}
|
|
@@ -68,8 +68,14 @@ export function buildExportFromApp(app, machines, opts = {}) {
|
|
|
68
68
|
const cleanMachines = keptMachines.map((m) => stripMachineServerFields(m));
|
|
69
69
|
const cleanApp = keptApp ? stripAppServerFields(keptApp) : keptApp;
|
|
70
70
|
const bundle = { ...(cleanApp ?? {}), machines: cleanMachines };
|
|
71
|
-
const
|
|
72
|
-
|
|
71
|
+
const content = JSON.stringify(bundle);
|
|
72
|
+
// `reparse(context)` parses the same bundle again with a `ParseContext`, so
|
|
73
|
+
// `chant import --from` hands a collector config in a machine file to the
|
|
74
|
+
// otel importer, as file import does (#3373).
|
|
75
|
+
const parse = (context) => select(new FlyParser().parse(content, context), opts.selector);
|
|
76
|
+
return { ...parse(), reparse: (context) => parse(context) };
|
|
77
|
+
}
|
|
78
|
+
function select(ir, selector) {
|
|
73
79
|
if (!selector || (selector.type === undefined && selector.name === undefined)) {
|
|
74
80
|
return ir;
|
|
75
81
|
}
|
package/dist/import/parser.d.ts
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* shape. `logicalId` is the app/machine name (the generator camelCases it into a
|
|
19
19
|
* variable).
|
|
20
20
|
*/
|
|
21
|
-
import type { TemplateParser, TemplateIR } from "@intentius/chant/import/parser";
|
|
21
|
+
import type { TemplateParser, TemplateIR, ParseContext } from "@intentius/chant/import/parser";
|
|
22
22
|
import { BaseValueParser } from "@intentius/chant/import/base-parser";
|
|
23
23
|
export declare const APP_TYPE = "Fly::Machines::App";
|
|
24
24
|
export declare const MACHINE_TYPE = "Fly::Machines::Machine";
|
|
@@ -28,7 +28,15 @@ export declare const MACHINE_SERVER_FIELDS: string[];
|
|
|
28
28
|
export declare const APP_SERVER_FIELDS: string[];
|
|
29
29
|
export declare class FlyParser extends BaseValueParser implements TemplateParser {
|
|
30
30
|
protected dispatchIntrinsic(_key: string, _value: unknown, _obj: Record<string, unknown>): unknown | null;
|
|
31
|
-
parse(
|
|
31
|
+
/** Set for the length of one `parse` with a context (`chant import`). */
|
|
32
|
+
private embedded?;
|
|
33
|
+
/**
|
|
34
|
+
* With `context.embedded` (`chant import`), a machine file another lexicon
|
|
35
|
+
* owns (a collector config) is handed to it (see ./embedded.ts). Without
|
|
36
|
+
* it, every file is kept as written.
|
|
37
|
+
*/
|
|
38
|
+
parse(input: string, context?: ParseContext): TemplateIR;
|
|
39
|
+
private parseInput;
|
|
32
40
|
/** Map one serializer-plan request to a resource by its endpoint. */
|
|
33
41
|
private resourceFromRequest;
|
|
34
42
|
private looksLikeApp;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"parser.d.ts","sourceRoot":"","sources":["../../src/import/parser.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,KAAK,EACV,cAAc,EACd,UAAU,
|
|
1
|
+
{"version":3,"file":"parser.d.ts","sourceRoot":"","sources":["../../src/import/parser.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,KAAK,EACV,cAAc,EACd,UAAU,EAEV,YAAY,EACb,MAAM,gCAAgC,CAAC;AAExC,OAAO,EAAE,eAAe,EAAE,MAAM,qCAAqC,CAAC;AAItE,eAAO,MAAM,QAAQ,uBAAuB,CAAC;AAC7C,eAAO,MAAM,YAAY,2BAA2B,CAAC;AAErD,kFAAkF;AAClF,eAAO,MAAM,qBAAqB,UAajC,CAAC;AAEF,8EAA8E;AAC9E,eAAO,MAAM,iBAAiB,UAO7B,CAAC;AAMF,qBAAa,SAAU,SAAQ,eAAgB,YAAW,cAAc;IACtE,SAAS,CAAC,iBAAiB,CACzB,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,OAAO,EACf,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC5B,OAAO,GAAG,IAAI,CAGhB;IAED,yEAAyE;IACzE,OAAO,CAAC,QAAQ,CAAC,CAA0B;IAE3C;;;;OAIG;IACH,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,YAAY,GAAG,UAAU,CAOvD;IAED,OAAO,CAAC,UAAU;IAiDlB,qEAAqE;IACrE,OAAO,CAAC,mBAAmB;IAkB3B,OAAO,CAAC,YAAY;IAIpB,kFAAkF;IAClF,OAAO,CAAC,WAAW;IAuBnB,yEAAyE;IACzE,OAAO,CAAC,eAAe;CAgBxB"}
|
package/dist/import/parser.js
CHANGED
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
*/
|
|
21
21
|
import { BaseValueParser } from "@intentius/chant/import/base-parser";
|
|
22
22
|
import { isSerializerPlan } from "../detect.js";
|
|
23
|
+
import { delegateEmbedded } from "./embedded.js";
|
|
23
24
|
export const APP_TYPE = "Fly::Machines::App";
|
|
24
25
|
export const MACHINE_TYPE = "Fly::Machines::Machine";
|
|
25
26
|
/** Server-written read-only machine fields — never part of the authored shape. */
|
|
@@ -54,7 +55,21 @@ export class FlyParser extends BaseValueParser {
|
|
|
54
55
|
// flaps JSON has no intrinsic functions.
|
|
55
56
|
return null;
|
|
56
57
|
}
|
|
57
|
-
|
|
58
|
+
/**
|
|
59
|
+
* With `context.embedded` (`chant import`), a machine file another lexicon
|
|
60
|
+
* owns (a collector config) is handed to it (see ./embedded.ts). Without
|
|
61
|
+
* it, every file is kept as written.
|
|
62
|
+
*/
|
|
63
|
+
parse(input, context) {
|
|
64
|
+
this.embedded = context?.embedded;
|
|
65
|
+
try {
|
|
66
|
+
return this.parseInput(input);
|
|
67
|
+
}
|
|
68
|
+
finally {
|
|
69
|
+
this.embedded = undefined;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
parseInput(input) {
|
|
58
73
|
const trimmed = input.trim();
|
|
59
74
|
if (!trimmed)
|
|
60
75
|
return { resources: [], parameters: [] };
|
|
@@ -152,6 +167,8 @@ export class FlyParser extends BaseValueParser {
|
|
|
152
167
|
}
|
|
153
168
|
// Always carry a name so the round-trip keeps the machine's identity.
|
|
154
169
|
properties.name = name;
|
|
170
|
+
if (this.embedded)
|
|
171
|
+
delegateEmbedded(name, properties, this.embedded);
|
|
155
172
|
return { logicalId: name, type: MACHINE_TYPE, properties };
|
|
156
173
|
}
|
|
157
174
|
}
|
package/dist/integrity.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"algorithm": "sha256",
|
|
3
3
|
"artifacts": {
|
|
4
|
-
"manifest.json": "
|
|
4
|
+
"manifest.json": "862833387a9f34a223d92ef6c0c9a68b8b5750ba798c10a8dbe4b5b20455e63a",
|
|
5
5
|
"meta.json": "273f2a523b66f08ca91642dba2592c44765ab59ddf7329998884a0b08c6c3f24",
|
|
6
|
-
"types/index.d.ts": "
|
|
6
|
+
"types/index.d.ts": "75c29d3ba9faa7c2792a0a25c51dc13df011906ffbd4c9e3ccc80023f5f32667",
|
|
7
7
|
"rules/guest-sizing.ts": "8dd2db79a47ceab3d488f84f560aed1b1e4a66b4e36bf11f3baf6a8544d6e126",
|
|
8
8
|
"rules/no-secret-literals.ts": "3854307bd0e8d7a1bf6c23051b460b90f3212baa8bb2f7d4324d0e4418a69982",
|
|
9
9
|
"rules/valid-region.ts": "238afb252ffb59ef131d070a705a49a1ac87b98f6a86c029a69dc82200e1aabf",
|
|
@@ -15,5 +15,5 @@
|
|
|
15
15
|
"skills/chant-fly-ops.md": "44102f1fdf611b9262c04457258990e44026e83f9e5b19ae86b69322f3a37620",
|
|
16
16
|
"skills/chant-fly-sprites.md": "3d100680422151b58d082c3a7b30a286edfc52faa4887995c9ad5553873bab95"
|
|
17
17
|
},
|
|
18
|
-
"composite": "
|
|
18
|
+
"composite": "b72790ed3a71100ac3a137dab0d262b5391b94a649170d4a577642dddb9f1f22"
|
|
19
19
|
}
|
package/dist/manifest.json
CHANGED
package/dist/okf/types/App.md
CHANGED
|
@@ -10,8 +10,8 @@ resource_type: Fly::Machines::App
|
|
|
10
10
|
|
|
11
11
|
## Properties
|
|
12
12
|
|
|
13
|
-
- `enable_subdomains` (`boolean`, optional)
|
|
13
|
+
- `enable_subdomains` (`boolean`, optional): Request a wildcard TLS certificate for *.svc.<app-name>.fly.dev. Defaults to false. Certificate provisioning is asynchronous; this does not create services or configure their routing.
|
|
14
14
|
- `idempotency_key` (`string`, optional): When set, makes a retry of this exact request safe: a second create with the same key and the same name (or no name) returns the app the first request created, instead of erroring on a false name conflict or creating a duplicate. A second create with the same key but a *different* name is rejected outright; it won't silently hand back the first app.
|
|
15
|
-
- `name` (`string`, optional)
|
|
16
|
-
- `network` (`string`, optional)
|
|
17
|
-
- `org_slug` (`string`, optional)
|
|
15
|
+
- `name` (`string`, optional): Globally unique name for the app. Required.
|
|
16
|
+
- `network` (`string`, optional): Name of the organization's private network to attach the app to. Creates the network if it does not exist. Omit, use an empty string, or use "default" to select the organization's default network.
|
|
17
|
+
- `org_slug` (`string`, optional): Slug of the organization that will own the app, or "personal" for your personal organization. Required.
|
package/dist/types/index.d.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
export declare class App {
|
|
6
6
|
constructor(props: {
|
|
7
|
+
/** Request a wildcard TLS certificate for *.svc.<app-name>.fly.dev. Defaults to false. Certificate provisioning is asynchronous; this does not create services or configure their routing. */
|
|
7
8
|
enable_subdomains?: boolean;
|
|
8
9
|
/** When set, makes a retry of this exact request safe: a
|
|
9
10
|
second create with the same key and the same name (or no name) returns
|
|
@@ -12,8 +13,11 @@ conflict or creating a duplicate. A second create with the same key
|
|
|
12
13
|
but a *different* name is rejected outright; it won't silently
|
|
13
14
|
hand back the first app. */
|
|
14
15
|
idempotency_key?: string;
|
|
16
|
+
/** Globally unique name for the app. Required. */
|
|
15
17
|
name?: string;
|
|
18
|
+
/** Name of the organization's private network to attach the app to. Creates the network if it does not exist. Omit, use an empty string, or use "default" to select the organization's default network. */
|
|
16
19
|
network?: string;
|
|
20
|
+
/** Slug of the organization that will own the app, or "personal" for your personal organization. Required. */
|
|
17
21
|
org_slug?: string;
|
|
18
22
|
});
|
|
19
23
|
readonly id: string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@intentius/chant-lexicon-fly",
|
|
3
|
-
"version": "0.124.
|
|
3
|
+
"version": "0.124.48",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"files": [
|
|
6
6
|
"src/",
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
"build": "../../scripts/tsc7.sh -p tsconfig.build.json --sourceMap false && tsc-alias -p tsconfig.build.json"
|
|
59
59
|
},
|
|
60
60
|
"dependencies": {
|
|
61
|
-
"@intentius/chant-lexicon-otel": "^0.124.
|
|
61
|
+
"@intentius/chant-lexicon-otel": "^0.124.48",
|
|
62
62
|
"ws": "^8.18.0"
|
|
63
63
|
},
|
|
64
64
|
"devDependencies": {
|
|
@@ -69,7 +69,7 @@
|
|
|
69
69
|
"zod": "^4.3.6"
|
|
70
70
|
},
|
|
71
71
|
"peerDependencies": {
|
|
72
|
-
"@intentius/chant": "^0.124.
|
|
72
|
+
"@intentius/chant": "^0.124.48",
|
|
73
73
|
"zod": "^4.3.6"
|
|
74
74
|
},
|
|
75
75
|
"description": "Fly.io Machines lexicon for chant — declarative IaC in TypeScript",
|
package/src/export-resources.ts
CHANGED
|
@@ -93,7 +93,7 @@ export async function exportResources(
|
|
|
93
93
|
);
|
|
94
94
|
};
|
|
95
95
|
|
|
96
|
-
const
|
|
96
|
+
const parts: ExportedTemplate[] = [];
|
|
97
97
|
for (const app of apps) {
|
|
98
98
|
const appRes = await http(
|
|
99
99
|
"GET",
|
|
@@ -116,8 +116,17 @@ export async function exportResources(
|
|
|
116
116
|
owned: options.owned,
|
|
117
117
|
onBoundaryInference,
|
|
118
118
|
});
|
|
119
|
-
|
|
119
|
+
parts.push(ir);
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
-
|
|
122
|
+
// Each app's export re-parses its own bundle; together they re-parse the
|
|
123
|
+
// whole export, so embedded collector configs reach the otel importer (#3373).
|
|
124
|
+
return {
|
|
125
|
+
resources: parts.flatMap((p) => p.resources),
|
|
126
|
+
parameters: [],
|
|
127
|
+
reparse: (context) => ({
|
|
128
|
+
resources: parts.flatMap((p) => (p.reparse ? p.reparse(context) : p).resources),
|
|
129
|
+
parameters: [],
|
|
130
|
+
}),
|
|
131
|
+
};
|
|
123
132
|
}
|
package/src/generated/index.d.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
export declare class App {
|
|
6
6
|
constructor(props: {
|
|
7
|
+
/** Request a wildcard TLS certificate for *.svc.<app-name>.fly.dev. Defaults to false. Certificate provisioning is asynchronous; this does not create services or configure their routing. */
|
|
7
8
|
enable_subdomains?: boolean;
|
|
8
9
|
/** When set, makes a retry of this exact request safe: a
|
|
9
10
|
second create with the same key and the same name (or no name) returns
|
|
@@ -12,8 +13,11 @@ conflict or creating a duplicate. A second create with the same key
|
|
|
12
13
|
but a *different* name is rejected outright; it won't silently
|
|
13
14
|
hand back the first app. */
|
|
14
15
|
idempotency_key?: string;
|
|
16
|
+
/** Globally unique name for the app. Required. */
|
|
15
17
|
name?: string;
|
|
18
|
+
/** Name of the organization's private network to attach the app to. Creates the network if it does not exist. Omit, use an empty string, or use "default" to select the organization's default network. */
|
|
16
19
|
network?: string;
|
|
20
|
+
/** Slug of the organization that will own the app, or "personal" for your personal organization. Required. */
|
|
17
21
|
org_slug?: string;
|
|
18
22
|
});
|
|
19
23
|
readonly id: string;
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A `FlyOtelCollector`'s config through `chant import` (#3373): the machine
|
|
3
|
+
* file at `COLLECTOR_CONFIG_PATH` is handed to the otel importer, and a live
|
|
4
|
+
* import (`chant import --from`) gives the same otel source a file import of
|
|
5
|
+
* the build's `fly.json` gives, and both give what `chant import config.yaml`
|
|
6
|
+
* gives for the config itself.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { describe, expect, test } from "vitest";
|
|
10
|
+
import { mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from "fs";
|
|
11
|
+
import { join, relative, resolve } from "path";
|
|
12
|
+
import { load } from "js-yaml";
|
|
13
|
+
import { importCommand, liveImportFromPlugins } from "@intentius/chant/cli/commands/import";
|
|
14
|
+
import type { LexiconPlugin } from "@intentius/chant/lexicon";
|
|
15
|
+
import { build } from "@intentius/chant/build";
|
|
16
|
+
import { EmbeddedImports } from "@intentius/chant/import/embedded";
|
|
17
|
+
import { COLLECTOR_CONFIG_PATH, otelPlugin, otelSerializer } from "@intentius/chant-lexicon-otel";
|
|
18
|
+
import { OtelCollectorParser } from "@intentius/chant-lexicon-otel/import/parser";
|
|
19
|
+
import { OtelCollectorGenerator } from "@intentius/chant-lexicon-otel/import/generator";
|
|
20
|
+
import { flyPlugin } from "../plugin";
|
|
21
|
+
import { flySerializer } from "../serializer";
|
|
22
|
+
import { exportResources } from "../export-resources";
|
|
23
|
+
import type { FlyHttp } from "../op/activities/fly-apply";
|
|
24
|
+
import { FlyParser } from "./parser";
|
|
25
|
+
import { decodeRawValue } from "./embedded";
|
|
26
|
+
|
|
27
|
+
type Json = Record<string, unknown>;
|
|
28
|
+
|
|
29
|
+
const pkgDir = resolve(import.meta.dirname, "../..");
|
|
30
|
+
|
|
31
|
+
const SOURCE = `import { App, FlyOtelCollector } from "@intentius/chant-lexicon-fly";
|
|
32
|
+
import { OtlpHttpExporter } from "@intentius/chant-lexicon-otel";
|
|
33
|
+
|
|
34
|
+
export const app = new App({ name: "otel" });
|
|
35
|
+
export const collector = FlyOtelCollector({
|
|
36
|
+
region: "iad",
|
|
37
|
+
exporters: [new OtlpHttpExporter({ endpoint: "https://otlp.example.com" })],
|
|
38
|
+
});
|
|
39
|
+
`;
|
|
40
|
+
|
|
41
|
+
function walk(dir: string, root: string, out: Record<string, string>): void {
|
|
42
|
+
for (const entry of readdirSync(dir)) {
|
|
43
|
+
const path = join(dir, entry);
|
|
44
|
+
if (statSync(path).isDirectory()) walk(path, root, out);
|
|
45
|
+
else out[relative(root, path)] = readFileSync(path, "utf-8");
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function project(): { dir: string; srcDir: string } {
|
|
50
|
+
const dir = mkdtempSync(join(pkgDir, ".roundtrip-tmp-"));
|
|
51
|
+
writeFileSync(join(dir, "package.json"), '{ "name": "fly-embedded", "private": true, "type": "module" }\n');
|
|
52
|
+
writeFileSync(join(dir, "chant.config.ts"), 'export default { lexicons: ["fly", "otel"] };\n');
|
|
53
|
+
const srcDir = join(dir, "src");
|
|
54
|
+
mkdirSync(srcDir);
|
|
55
|
+
return { dir, srcDir };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function files(srcDir: string): Record<string, string> {
|
|
59
|
+
const out: Record<string, string> = {};
|
|
60
|
+
walk(srcDir, srcDir, out);
|
|
61
|
+
return out;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** The build's flaps plan for a FlyOtelCollector. */
|
|
65
|
+
async function plan(): Promise<Record<string, { endpoint: string; body: Json }>> {
|
|
66
|
+
const { dir, srcDir } = project();
|
|
67
|
+
try {
|
|
68
|
+
writeFileSync(join(srcDir, "main.ts"), SOURCE);
|
|
69
|
+
const result = await build(srcDir, [flySerializer, otelSerializer]);
|
|
70
|
+
expect(result.errors).toEqual([]);
|
|
71
|
+
const out = result.outputs.get("fly");
|
|
72
|
+
return JSON.parse(typeof out === "string" ? out : (out as { primary: string }).primary);
|
|
73
|
+
} finally {
|
|
74
|
+
rmSync(dir, { recursive: true, force: true });
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function machineBody(p: Awaited<ReturnType<typeof plan>>): Json {
|
|
79
|
+
const req = Object.values(p).find((r) => /\/machines\/?$/.test(r.endpoint));
|
|
80
|
+
if (!req) throw new Error("no machine in the plan");
|
|
81
|
+
return req.body;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function configText(machine: Json): string {
|
|
85
|
+
const files = (machine.config as Json).files as Json[];
|
|
86
|
+
const file = files.find((f) => f.guest_path === COLLECTOR_CONFIG_PATH)!;
|
|
87
|
+
return decodeRawValue(file.raw_value as string)!;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** `chant import config.yaml`: the otel modules for the config alone. */
|
|
91
|
+
function standalone(text: string): Record<string, string> {
|
|
92
|
+
return Object.fromEntries(new OtelCollectorGenerator().generate(new OtelCollectorParser().parse(text)).map((f) => [f.path, f.content]));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** The modules under `dir/`, keyed by their path inside it. */
|
|
96
|
+
function under(all: Record<string, string>, dir: string): Record<string, string> {
|
|
97
|
+
return Object.fromEntries(Object.entries(all).filter(([p]) => p.startsWith(`${dir}/`)).map(([p, c]) => [p.slice(dir.length + 1), c]));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** A fake flaps API holding one app and the plan's machine, with the fields a server adds. */
|
|
101
|
+
function flaps(machine: Json): FlyHttp {
|
|
102
|
+
const live = { ...machine, id: "m-1", state: "started", instance_id: "I1", private_ip: "fdaa::1" };
|
|
103
|
+
return async (_method, url) => {
|
|
104
|
+
if (/\/v1\/apps\/otel\/machines/.test(url)) return { status: 200, text: JSON.stringify([live]) };
|
|
105
|
+
if (/\/v1\/apps\/otel$/.test(url)) return { status: 200, text: JSON.stringify({ name: "otel", organization: { slug: "acme" } }) };
|
|
106
|
+
return { status: 404, text: "" };
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
describe("FlyOtelCollector through chant import (#3373)", () => {
|
|
111
|
+
test("a live import gives the otel source a file import gives, and builds back to the same config", async () => {
|
|
112
|
+
const p = await plan();
|
|
113
|
+
const machine = machineBody(p);
|
|
114
|
+
const text = configText(machine);
|
|
115
|
+
const name = machine.name as string;
|
|
116
|
+
const expected = standalone(text);
|
|
117
|
+
expect(Object.keys(expected).length).toBeGreaterThan(0);
|
|
118
|
+
|
|
119
|
+
// File import of the build's fly.json.
|
|
120
|
+
const fileProject = project();
|
|
121
|
+
// Live import from a fake flaps API.
|
|
122
|
+
const liveProject = project();
|
|
123
|
+
const prevApp = process.env.FLY_APP_NAME;
|
|
124
|
+
process.env.FLY_APP_NAME = "otel";
|
|
125
|
+
try {
|
|
126
|
+
const templatePath = join(fileProject.dir, "fly.json");
|
|
127
|
+
writeFileSync(templatePath, JSON.stringify(p));
|
|
128
|
+
const fileResult = await importCommand({ templatePath, output: fileProject.srcDir, lexicon: "fly" });
|
|
129
|
+
expect(fileResult.error).toBeUndefined();
|
|
130
|
+
expect(fileResult.warnings.filter((w) => w.includes("kept as written"))).toEqual([]);
|
|
131
|
+
const fromFile = files(fileProject.srcDir);
|
|
132
|
+
|
|
133
|
+
const plugin: LexiconPlugin = { ...flyPlugin, exportResources: (options) => exportResources(options, flaps(machine)) };
|
|
134
|
+
const liveResult = await liveImportFromPlugins([plugin, otelPlugin], {
|
|
135
|
+
environment: "test",
|
|
136
|
+
output: liveProject.srcDir,
|
|
137
|
+
lexicon: "fly",
|
|
138
|
+
force: true,
|
|
139
|
+
});
|
|
140
|
+
expect(liveResult.error).toBeUndefined();
|
|
141
|
+
expect(liveResult.success).toBe(true);
|
|
142
|
+
expect(liveResult.warnings.filter((w) => w.includes("kept as written"))).toEqual([]);
|
|
143
|
+
const fromLive = files(liveProject.srcDir);
|
|
144
|
+
|
|
145
|
+
expect(under(fromLive, name)).toEqual(expected);
|
|
146
|
+
expect(under(fromFile, name)).toEqual(expected);
|
|
147
|
+
expect(fromLive["main.ts"]).toContain('import { collectorYaml } from "@intentius/chant-lexicon-otel";');
|
|
148
|
+
expect(fromLive["main.ts"]).toContain("raw_value: Buffer.from(collectorYaml(");
|
|
149
|
+
|
|
150
|
+
// The imported source builds back to the config the machine holds.
|
|
151
|
+
const rebuilt = await build(liveProject.srcDir, [flySerializer, otelSerializer]);
|
|
152
|
+
expect(rebuilt.errors).toEqual([]);
|
|
153
|
+
const out = rebuilt.outputs.get("fly");
|
|
154
|
+
const after = machineBody(JSON.parse(typeof out === "string" ? out : (out as { primary: string }).primary));
|
|
155
|
+
expect(load(configText(after))).toEqual(load(text));
|
|
156
|
+
} finally {
|
|
157
|
+
if (prevApp === undefined) delete process.env.FLY_APP_NAME;
|
|
158
|
+
else process.env.FLY_APP_NAME = prevApp;
|
|
159
|
+
rmSync(fileProject.dir, { recursive: true, force: true });
|
|
160
|
+
rmSync(liveProject.dir, { recursive: true, force: true });
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test("without a context the export keeps the file as read, and offers a reparse", async () => {
|
|
165
|
+
const machine = machineBody(await plan());
|
|
166
|
+
const prevApp = process.env.FLY_APP_NAME;
|
|
167
|
+
process.env.FLY_APP_NAME = "otel";
|
|
168
|
+
try {
|
|
169
|
+
const ir = await exportResources({ environment: "test" }, flaps(machine));
|
|
170
|
+
const m = ir.resources.find((r) => r.type === "Fly::Machines::Machine")!;
|
|
171
|
+
const file = ((m.properties.config as Json).files as Json[])[0];
|
|
172
|
+
expect(typeof file.raw_value).toBe("string");
|
|
173
|
+
expect(typeof ir.reparse).toBe("function");
|
|
174
|
+
} finally {
|
|
175
|
+
if (prevApp === undefined) delete process.env.FLY_APP_NAME;
|
|
176
|
+
else process.env.FLY_APP_NAME = prevApp;
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
test("a file that is not a document, or not text, is never offered", () => {
|
|
181
|
+
const probe = new EmbeddedImports([], { quiet: true });
|
|
182
|
+
const files = [
|
|
183
|
+
{ guest_path: "/etc/motd", raw_value: Buffer.from("hello there\n").toString("base64") },
|
|
184
|
+
{ guest_path: "/bin/blob", raw_value: Buffer.from([0xff, 0xfe, 0x00, 0x81]).toString("base64") },
|
|
185
|
+
{ guest_path: "/etc/app.yaml", raw_value: Buffer.from("a: 1\n").toString("base64") },
|
|
186
|
+
];
|
|
187
|
+
new FlyParser().parse(JSON.stringify({ name: "m", config: { image: "x", files } }), { embedded: probe });
|
|
188
|
+
expect(probe.offered.map((c) => c.location)).toEqual(["Machine m file /etc/app.yaml"]);
|
|
189
|
+
});
|
|
190
|
+
});
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where a Fly Machine embeds another lexicon's content, for `chant import`
|
|
3
|
+
* (#3373).
|
|
4
|
+
*
|
|
5
|
+
* A machine's `config.files[]` entry with a `raw_value` is a file written into
|
|
6
|
+
* the machine, base64 in flaps. The parser decodes each one and offers the text
|
|
7
|
+
* to core's embedded-content resolver, as k8s offers a ConfigMap value
|
|
8
|
+
* (#2962). `FlyOtelCollector` writes its collector config this way, at the otel
|
|
9
|
+
* lexicon's `COLLECTOR_CONFIG_PATH`, so a live or file import of that machine
|
|
10
|
+
* gives the same otel source `chant import config.yaml` gives.
|
|
11
|
+
*
|
|
12
|
+
* Which lexicon imports the content is decided by core at run time; the hint
|
|
13
|
+
* below only names the owner a warning suggests when none is installed.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type { EmbeddedContent, EmbeddedContentResolver, EmbeddedReference } from "@intentius/chant/import/embedded";
|
|
17
|
+
import { embeddedDocument } from "@intentius/chant/import/embedded";
|
|
18
|
+
import { COLLECTOR_CONFIG_PATH } from "@intentius/chant-lexicon-otel";
|
|
19
|
+
|
|
20
|
+
function isRecord(v: unknown): v is Record<string, unknown> {
|
|
21
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** The UTF-8 text a base64 `raw_value` holds, or undefined when it is not base64 of text. */
|
|
25
|
+
export function decodeRawValue(raw: string): string | undefined {
|
|
26
|
+
if (!/^[A-Za-z0-9+/\s]*={0,2}\s*$/.test(raw)) return undefined;
|
|
27
|
+
const bytes = Buffer.from(raw, "base64");
|
|
28
|
+
const text = bytes.toString("utf8");
|
|
29
|
+
// A round trip that changes the bytes means the file is not UTF-8 text.
|
|
30
|
+
return Buffer.from(text, "utf8").equals(bytes) ? text : undefined;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function hintFor(guestPath: string, doc: unknown): EmbeddedContent["expectedOwner"] {
|
|
34
|
+
if (guestPath === COLLECTOR_CONFIG_PATH) return { lexicon: "otel", what: "an OpenTelemetry Collector config" };
|
|
35
|
+
if (isRecord(doc) && isRecord(doc.service) && isRecord(doc.service.pipelines)) {
|
|
36
|
+
return { lexicon: "otel", what: "an OpenTelemetry Collector config" };
|
|
37
|
+
}
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The marker the parser leaves on a file whose `raw_value` an owner imports:
|
|
43
|
+
* the generator renders it as the base64 of the reference's text.
|
|
44
|
+
*/
|
|
45
|
+
export interface EmbeddedRawValue {
|
|
46
|
+
readonly $base64: EmbeddedReference;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function isEmbeddedRawValue(v: unknown): v is EmbeddedRawValue {
|
|
50
|
+
return isRecord(v) && isRecord(v.$base64);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Offer each `config.files[].raw_value` of one machine to `embedded`, replacing
|
|
55
|
+
* the value an owner imports with an {@link EmbeddedRawValue}. `properties` is
|
|
56
|
+
* the machine's IR properties, changed in place with fresh copies on the way
|
|
57
|
+
* down.
|
|
58
|
+
*/
|
|
59
|
+
export function delegateEmbedded(
|
|
60
|
+
machineName: string,
|
|
61
|
+
properties: Record<string, unknown>,
|
|
62
|
+
embedded: EmbeddedContentResolver,
|
|
63
|
+
): void {
|
|
64
|
+
const config = properties.config;
|
|
65
|
+
if (!isRecord(config) || !Array.isArray(config.files)) return;
|
|
66
|
+
const files = config.files;
|
|
67
|
+
let changed = false;
|
|
68
|
+
const next = files.map((file, i) => {
|
|
69
|
+
if (!isRecord(file) || typeof file.raw_value !== "string") return file;
|
|
70
|
+
const text = decodeRawValue(file.raw_value);
|
|
71
|
+
if (text === undefined) return file;
|
|
72
|
+
const doc = embeddedDocument(text);
|
|
73
|
+
if (!isRecord(doc)) return file;
|
|
74
|
+
const guestPath = typeof file.guest_path === "string" ? file.guest_path : `files[${i}]`;
|
|
75
|
+
const base = guestPath.split("/").pop()!.replace(/\.[^.]*$/, "");
|
|
76
|
+
const ref = embedded.resolve({
|
|
77
|
+
host: "fly",
|
|
78
|
+
hostType: "Fly::Machines::Machine",
|
|
79
|
+
location: `Machine ${machineName} file ${guestPath}`,
|
|
80
|
+
directory: files.length === 1 ? machineName : `${machineName}-${base}`,
|
|
81
|
+
text,
|
|
82
|
+
document: doc,
|
|
83
|
+
expectedOwner: hintFor(guestPath, doc),
|
|
84
|
+
});
|
|
85
|
+
if (!ref) return file;
|
|
86
|
+
changed = true;
|
|
87
|
+
return { ...file, raw_value: { $base64: ref } satisfies EmbeddedRawValue };
|
|
88
|
+
});
|
|
89
|
+
if (changed) properties.config = { ...config, files: next };
|
|
90
|
+
}
|
package/src/import/generator.ts
CHANGED
|
@@ -16,6 +16,14 @@
|
|
|
16
16
|
|
|
17
17
|
import type { TypeScriptGenerator, GeneratedFile } from "@intentius/chant/import/generator";
|
|
18
18
|
import type { TemplateIR } from "@intentius/chant/import/parser";
|
|
19
|
+
import { EmbeddedImportScope, isEmbeddedReference, renderEmbeddedReference } from "@intentius/chant/import/embedded";
|
|
20
|
+
import { isEmbeddedRawValue } from "./embedded";
|
|
21
|
+
|
|
22
|
+
/** What one module's rendering collects: the fly classes it uses, and the imports embedded references need. */
|
|
23
|
+
interface Ctx {
|
|
24
|
+
imports: Set<string>;
|
|
25
|
+
scope: EmbeddedImportScope;
|
|
26
|
+
}
|
|
19
27
|
|
|
20
28
|
/**
|
|
21
29
|
* How a nested field is wrapped: `single` → `new Cls({...})`, `array` → each
|
|
@@ -106,13 +114,25 @@ export class FlyGenerator implements TypeScriptGenerator {
|
|
|
106
114
|
generate(ir: TemplateIR): GeneratedFile[] {
|
|
107
115
|
const imports = new Set<string>();
|
|
108
116
|
const bodyLines: string[] = [];
|
|
117
|
+
// Names an embedded reference's imports must not take: the module's own
|
|
118
|
+
// declarations and every fly class it could import.
|
|
119
|
+
const classNames = new Set(["App", "Machine", ...Object.keys(NESTED_DECLARABLES)]);
|
|
120
|
+
for (const fields of Object.values(NESTED_DECLARABLES)) for (const n of Object.values(fields)) classNames.add(n.cls);
|
|
121
|
+
for (const r of ir.resources) {
|
|
122
|
+
const parts = r.type.split("::");
|
|
123
|
+
classNames.add(parts.length >= 3 ? parts[2] : r.type);
|
|
124
|
+
}
|
|
125
|
+
const ctx: Ctx = {
|
|
126
|
+
imports,
|
|
127
|
+
scope: new EmbeddedImportScope("", [...classNames, "Buffer", ...ir.resources.map((r) => camelCase(r.logicalId))]),
|
|
128
|
+
};
|
|
109
129
|
|
|
110
130
|
for (const resource of ir.resources) {
|
|
111
131
|
const parts = resource.type.split("::");
|
|
112
132
|
const className = parts.length >= 3 ? parts[2] : resource.type;
|
|
113
133
|
imports.add(className);
|
|
114
134
|
const varName = camelCase(resource.logicalId);
|
|
115
|
-
const props = formatProps(resource.properties, 0, className,
|
|
135
|
+
const props = formatProps(resource.properties, 0, className, ctx);
|
|
116
136
|
bodyLines.push(`export const ${varName} = new ${className}(${props});`);
|
|
117
137
|
bodyLines.push("");
|
|
118
138
|
}
|
|
@@ -122,8 +142,9 @@ export class FlyGenerator implements TypeScriptGenerator {
|
|
|
122
142
|
lines.push(
|
|
123
143
|
`import { ${[...imports].sort().join(", ")} } from "@intentius/chant-lexicon-fly";`,
|
|
124
144
|
);
|
|
125
|
-
lines.push("");
|
|
126
145
|
}
|
|
146
|
+
lines.push(...ctx.scope.lines());
|
|
147
|
+
if (lines.length > 0) lines.push("");
|
|
127
148
|
lines.push(...bodyLines);
|
|
128
149
|
|
|
129
150
|
return [{ path: "main.ts", content: lines.join("\n") + "\n" }];
|
|
@@ -145,7 +166,7 @@ function formatProps(
|
|
|
145
166
|
props: Record<string, unknown>,
|
|
146
167
|
indent: number,
|
|
147
168
|
ownerClass: string,
|
|
148
|
-
|
|
169
|
+
ctx: Ctx,
|
|
149
170
|
): string {
|
|
150
171
|
const entries = Object.entries(props);
|
|
151
172
|
if (entries.length === 0) return "{}";
|
|
@@ -158,8 +179,8 @@ function formatProps(
|
|
|
158
179
|
const spec = nested?.[key];
|
|
159
180
|
const rendered =
|
|
160
181
|
spec !== undefined
|
|
161
|
-
? formatNested(value, indent + 1, spec,
|
|
162
|
-
: formatValue(value, indent + 1);
|
|
182
|
+
? formatNested(value, indent + 1, spec, ctx)
|
|
183
|
+
: formatValue(value, indent + 1, ctx);
|
|
163
184
|
return `${pad}${key}: ${rendered},`;
|
|
164
185
|
});
|
|
165
186
|
|
|
@@ -171,14 +192,14 @@ function formatNested(
|
|
|
171
192
|
value: unknown,
|
|
172
193
|
indent: number,
|
|
173
194
|
spec: Nested,
|
|
174
|
-
|
|
195
|
+
ctx: Ctx,
|
|
175
196
|
): string {
|
|
176
197
|
const construct = (obj: unknown, at: number): string => {
|
|
177
198
|
if (!obj || typeof obj !== "object" || Array.isArray(obj)) {
|
|
178
|
-
return formatValue(obj, at);
|
|
199
|
+
return formatValue(obj, at, ctx);
|
|
179
200
|
}
|
|
180
|
-
imports.add(spec.cls);
|
|
181
|
-
return `new ${spec.cls}(${formatProps(obj as Record<string, unknown>, at, spec.cls,
|
|
201
|
+
ctx.imports.add(spec.cls);
|
|
202
|
+
return `new ${spec.cls}(${formatProps(obj as Record<string, unknown>, at, spec.cls, ctx)})`;
|
|
182
203
|
};
|
|
183
204
|
|
|
184
205
|
if (spec.kind === "single") {
|
|
@@ -195,7 +216,7 @@ function formatNested(
|
|
|
195
216
|
|
|
196
217
|
// record: a plain object whose values are wrapped in the constructor.
|
|
197
218
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
198
|
-
return formatValue(value, indent);
|
|
219
|
+
return formatValue(value, indent, ctx);
|
|
199
220
|
}
|
|
200
221
|
const record = value as Record<string, unknown>;
|
|
201
222
|
const recKeys = Object.entries(record);
|
|
@@ -206,8 +227,14 @@ function formatNested(
|
|
|
206
227
|
return `{\n${items.join("\n")}\n${closePad}}`;
|
|
207
228
|
}
|
|
208
229
|
|
|
209
|
-
function formatValue(value: unknown, indent: number): string {
|
|
230
|
+
function formatValue(value: unknown, indent: number, ctx: Ctx): string {
|
|
210
231
|
if (value === null || value === undefined) return "undefined";
|
|
232
|
+
// A machine file another lexicon imported (#3373): flaps wants the file's
|
|
233
|
+
// text base64-encoded, so the reference's text is encoded at build time.
|
|
234
|
+
if (isEmbeddedRawValue(value)) {
|
|
235
|
+
return `Buffer.from(${renderEmbeddedReference(value.$base64, ctx.scope, indent * 2)}, "utf8").toString("base64")`;
|
|
236
|
+
}
|
|
237
|
+
if (isEmbeddedReference(value)) return renderEmbeddedReference(value, ctx.scope, indent * 2);
|
|
211
238
|
if (typeof value === "string") return JSON.stringify(value);
|
|
212
239
|
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
213
240
|
|
|
@@ -215,24 +242,24 @@ function formatValue(value: unknown, indent: number): string {
|
|
|
215
242
|
if (value.length === 0) return "[]";
|
|
216
243
|
const pad = " ".repeat(indent + 1);
|
|
217
244
|
const closePad = " ".repeat(indent);
|
|
218
|
-
const items = value.map((v) => `${pad}${formatValue(v, indent + 1)},`);
|
|
245
|
+
const items = value.map((v) => `${pad}${formatValue(v, indent + 1, ctx)},`);
|
|
219
246
|
return `[\n${items.join("\n")}\n${closePad}]`;
|
|
220
247
|
}
|
|
221
248
|
|
|
222
249
|
if (typeof value === "object") {
|
|
223
|
-
return formatPlainObject(value as Record<string, unknown>, indent);
|
|
250
|
+
return formatPlainObject(value as Record<string, unknown>, indent, ctx);
|
|
224
251
|
}
|
|
225
252
|
|
|
226
253
|
return String(value);
|
|
227
254
|
}
|
|
228
255
|
|
|
229
256
|
/** A plain object literal (no constructor wrapping) — for scalar-valued maps like env/metadata. */
|
|
230
|
-
function formatPlainObject(obj: Record<string, unknown>, indent: number): string {
|
|
257
|
+
function formatPlainObject(obj: Record<string, unknown>, indent: number, ctx: Ctx): string {
|
|
231
258
|
const entries = Object.entries(obj);
|
|
232
259
|
if (entries.length === 0) return "{}";
|
|
233
260
|
const pad = " ".repeat(indent + 1);
|
|
234
261
|
const closePad = " ".repeat(indent);
|
|
235
|
-
const lines = entries.map(([key, value]) => `${pad}${objectKey(key)}: ${formatValue(value, indent + 1)},`);
|
|
262
|
+
const lines = entries.map(([key, value]) => `${pad}${objectKey(key)}: ${formatValue(value, indent + 1, ctx)},`);
|
|
236
263
|
return `{\n${lines.join("\n")}\n${closePad}}`;
|
|
237
264
|
}
|
|
238
265
|
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
21
|
import type { ExportedTemplate, ResourceSelector } from "@intentius/chant/lexicon";
|
|
22
|
+
import type { ParseContext, TemplateIR } from "@intentius/chant/import/parser";
|
|
22
23
|
import { isChantOwned, type FlapsMachine } from "../op/activities/fly-apply";
|
|
23
24
|
import {
|
|
24
25
|
FlyParser,
|
|
@@ -90,9 +91,15 @@ export function buildExportFromApp(
|
|
|
90
91
|
const cleanApp = keptApp ? stripAppServerFields(keptApp) : keptApp;
|
|
91
92
|
|
|
92
93
|
const bundle: Record<string, unknown> = { ...(cleanApp ?? {}), machines: cleanMachines };
|
|
93
|
-
const
|
|
94
|
+
const content = JSON.stringify(bundle);
|
|
95
|
+
// `reparse(context)` parses the same bundle again with a `ParseContext`, so
|
|
96
|
+
// `chant import --from` hands a collector config in a machine file to the
|
|
97
|
+
// otel importer, as file import does (#3373).
|
|
98
|
+
const parse = (context?: ParseContext): TemplateIR => select(new FlyParser().parse(content, context), opts.selector);
|
|
99
|
+
return { ...parse(), reparse: (context) => parse(context) };
|
|
100
|
+
}
|
|
94
101
|
|
|
95
|
-
|
|
102
|
+
function select(ir: TemplateIR, selector: ResourceSelector | undefined): TemplateIR {
|
|
96
103
|
if (!selector || (selector.type === undefined && selector.name === undefined)) {
|
|
97
104
|
return ir;
|
|
98
105
|
}
|
package/src/import/parser.ts
CHANGED
|
@@ -23,9 +23,12 @@ import type {
|
|
|
23
23
|
TemplateParser,
|
|
24
24
|
TemplateIR,
|
|
25
25
|
ResourceIR,
|
|
26
|
+
ParseContext,
|
|
26
27
|
} from "@intentius/chant/import/parser";
|
|
28
|
+
import type { EmbeddedContentResolver } from "@intentius/chant/import/embedded";
|
|
27
29
|
import { BaseValueParser } from "@intentius/chant/import/base-parser";
|
|
28
30
|
import { isSerializerPlan } from "../detect";
|
|
31
|
+
import { delegateEmbedded } from "./embedded";
|
|
29
32
|
|
|
30
33
|
export const APP_TYPE = "Fly::Machines::App";
|
|
31
34
|
export const MACHINE_TYPE = "Fly::Machines::Machine";
|
|
@@ -70,7 +73,24 @@ export class FlyParser extends BaseValueParser implements TemplateParser {
|
|
|
70
73
|
return null;
|
|
71
74
|
}
|
|
72
75
|
|
|
73
|
-
parse(
|
|
76
|
+
/** Set for the length of one `parse` with a context (`chant import`). */
|
|
77
|
+
private embedded?: EmbeddedContentResolver;
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* With `context.embedded` (`chant import`), a machine file another lexicon
|
|
81
|
+
* owns (a collector config) is handed to it (see ./embedded.ts). Without
|
|
82
|
+
* it, every file is kept as written.
|
|
83
|
+
*/
|
|
84
|
+
parse(input: string, context?: ParseContext): TemplateIR {
|
|
85
|
+
this.embedded = context?.embedded;
|
|
86
|
+
try {
|
|
87
|
+
return this.parseInput(input);
|
|
88
|
+
} finally {
|
|
89
|
+
this.embedded = undefined;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
private parseInput(input: string): TemplateIR {
|
|
74
94
|
const trimmed = input.trim();
|
|
75
95
|
if (!trimmed) return { resources: [], parameters: [] };
|
|
76
96
|
|
|
@@ -179,6 +199,7 @@ export class FlyParser extends BaseValueParser implements TemplateParser {
|
|
|
179
199
|
}
|
|
180
200
|
// Always carry a name so the round-trip keeps the machine's identity.
|
|
181
201
|
properties.name = name;
|
|
202
|
+
if (this.embedded) delegateEmbedded(name, properties, this.embedded);
|
|
182
203
|
|
|
183
204
|
return { logicalId: name, type: MACHINE_TYPE, properties };
|
|
184
205
|
}
|