@monosmith/plugin-sdk 0.0.0-stage → 0.2.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/README.md +90 -2
- package/build-dir/build-dir.algorithm.d.ts +16 -0
- package/build-dir/build-dir.algorithm.js +26 -0
- package/build-dir/index.d.ts +1 -0
- package/build-dir/index.js +1 -0
- package/ci/ci-contribution.algorithm.d.ts +28 -0
- package/ci/ci-contribution.algorithm.js +19 -0
- package/ci/index.d.ts +2 -0
- package/ci/index.js +1 -0
- package/codec/encode-by-schema.algorithm.d.ts +12 -0
- package/codec/encode-by-schema.algorithm.js +105 -0
- package/codec/encode-error.error.d.ts +4 -0
- package/codec/encode-error.error.js +7 -0
- package/codec/encode-json-value.algorithm.d.ts +13 -0
- package/codec/encode-json-value.algorithm.js +51 -0
- package/codec/index.d.ts +6 -0
- package/codec/index.js +5 -0
- package/codec/message-parse.error.d.ts +4 -0
- package/codec/message-parse.error.js +7 -0
- package/codec/method-codec.algorithm.d.ts +8 -0
- package/codec/method-codec.algorithm.js +31 -0
- package/codec/normalize-by-schema.algorithm.d.ts +14 -0
- package/codec/normalize-by-schema.algorithm.js +103 -0
- package/codec/params-error.error.d.ts +4 -0
- package/codec/params-error.error.js +7 -0
- package/codec/schema-lookup.algorithm.d.ts +13 -0
- package/codec/schema-lookup.algorithm.js +56 -0
- package/codec/wire-message.mapper.d.ts +27 -0
- package/codec/wire-message.mapper.js +62 -0
- package/concurrency/concurrency.policy.d.ts +21 -0
- package/concurrency/concurrency.policy.js +30 -0
- package/concurrency/index.d.ts +1 -0
- package/concurrency/index.js +1 -0
- package/conformance/check-generation.use-case.d.ts +12 -0
- package/conformance/check-generation.use-case.js +91 -0
- package/conformance/check-plugin.use-case.d.ts +25 -0
- package/conformance/check-plugin.use-case.js +81 -0
- package/conformance/check-scheduling.use-case.d.ts +11 -0
- package/conformance/check-scheduling.use-case.js +98 -0
- package/conformance/conformance-report.model.d.ts +22 -0
- package/conformance/conformance-report.model.js +23 -0
- package/conformance/find-manifests.algorithm.d.ts +7 -0
- package/conformance/find-manifests.algorithm.js +34 -0
- package/conformance/go-list.mapper.d.ts +4 -0
- package/conformance/go-list.mapper.js +8 -0
- package/conformance/index.d.ts +5 -0
- package/conformance/index.js +3 -0
- package/framing/frame-message.algorithm.d.ts +5 -0
- package/framing/frame-message.algorithm.js +8 -0
- package/framing/frame-reader.store.d.ts +15 -0
- package/framing/frame-reader.store.js +59 -0
- package/framing/framing.error.d.ts +4 -0
- package/framing/framing.error.js +7 -0
- package/framing/index.d.ts +3 -0
- package/framing/index.js +3 -0
- package/host/call-interrupted.error.d.ts +16 -0
- package/host/call-interrupted.error.js +20 -0
- package/host/connect-plugin.use-case.d.ts +26 -0
- package/host/connect-plugin.use-case.js +44 -0
- package/host/host-options.contract.d.ts +34 -0
- package/host/host-options.contract.js +4 -0
- package/host/index.d.ts +9 -0
- package/host/index.js +5 -0
- package/host/plugin-client.client.d.ts +61 -0
- package/host/plugin-client.client.js +297 -0
- package/host/start-plugin.use-case.d.ts +10 -0
- package/host/start-plugin.use-case.js +57 -0
- package/index.d.ts +16 -0
- package/index.js +21 -0
- package/jsondoc/indent-raw-json.algorithm.d.ts +8 -0
- package/jsondoc/indent-raw-json.algorithm.js +60 -0
- package/jsondoc/index.d.ts +2 -0
- package/jsondoc/index.js +2 -0
- package/jsondoc/json-document.error.d.ts +4 -0
- package/jsondoc/json-document.error.js +7 -0
- package/jsondoc/json-document.model.d.ts +38 -0
- package/jsondoc/json-document.model.js +113 -0
- package/jsondoc/scan-json-object.algorithm.d.ts +17 -0
- package/jsondoc/scan-json-object.algorithm.js +132 -0
- package/package.json +34 -4
- package/plugin-refs/index.d.ts +1 -0
- package/plugin-refs/index.js +1 -0
- package/plugin-refs/plugin-ref.policy.d.ts +9 -0
- package/plugin-refs/plugin-ref.policy.js +22 -0
- package/plugin-roles/index.d.ts +1 -0
- package/plugin-roles/index.js +1 -0
- package/plugin-roles/plugin-role.policy.d.ts +16 -0
- package/plugin-roles/plugin-role.policy.js +30 -0
- package/projects/audit-settings.policy.d.ts +5 -0
- package/projects/audit-settings.policy.js +8 -0
- package/projects/index.d.ts +6 -0
- package/projects/index.js +6 -0
- package/projects/manifest-match.algorithm.d.ts +7 -0
- package/projects/manifest-match.algorithm.js +38 -0
- package/projects/project-kind.policy.d.ts +9 -0
- package/projects/project-kind.policy.js +28 -0
- package/projects/publishes.policy.d.ts +7 -0
- package/projects/publishes.policy.js +8 -0
- package/projects/set-toolchain.algorithm.d.ts +7 -0
- package/projects/set-toolchain.algorithm.js +13 -0
- package/projects/stack-plugins.algorithm.d.ts +3 -0
- package/projects/stack-plugins.algorithm.js +4 -0
- package/protocol/index.d.ts +4 -0
- package/protocol/index.js +3 -0
- package/protocol/protocol-schema.contract.d.ts +3 -0
- package/protocol/protocol-schema.contract.js +1976 -0
- package/protocol/protocol-types.contract.d.ts +1057 -0
- package/protocol/protocol-types.contract.js +43 -0
- package/protocol/rpc-error.error.d.ts +9 -0
- package/protocol/rpc-error.error.js +13 -0
- package/protocol/schema-document.contract.d.ts +58 -0
- package/protocol/schema-document.contract.js +1 -0
- package/scaffold/index.d.ts +3 -0
- package/scaffold/index.js +2 -0
- package/scaffold/scaffold-data.mapper.d.ts +15 -0
- package/scaffold/scaffold-data.mapper.js +24 -0
- package/scaffold/scaffold-project.use-case.d.ts +20 -0
- package/scaffold/scaffold-project.use-case.js +13 -0
- package/serve/capabilities-of.algorithm.d.ts +14 -0
- package/serve/capabilities-of.algorithm.js +31 -0
- package/serve/claim-stdout.use-case.d.ts +10 -0
- package/serve/claim-stdout.use-case.js +14 -0
- package/serve/dispatch-request.use-case.d.ts +15 -0
- package/serve/dispatch-request.use-case.js +57 -0
- package/serve/index.d.ts +4 -0
- package/serve/index.js +2 -0
- package/serve/initialize-plugin.use-case.d.ts +9 -0
- package/serve/initialize-plugin.use-case.js +16 -0
- package/serve/plugin-members.config.d.ts +16 -0
- package/serve/plugin-members.config.js +25 -0
- package/serve/plugin.contract.d.ts +80 -0
- package/serve/plugin.contract.js +1 -0
- package/serve/protocol-streams.contract.d.ts +13 -0
- package/serve/protocol-streams.contract.js +1 -0
- package/serve/request-outcome.contract.d.ts +9 -0
- package/serve/request-outcome.contract.js +1 -0
- package/serve/request-pool.store.d.ts +49 -0
- package/serve/request-pool.store.js +110 -0
- package/serve/serve.use-case.d.ts +31 -0
- package/serve/serve.use-case.js +180 -0
- package/tasks/contribute-tasks.algorithm.d.ts +15 -0
- package/tasks/contribute-tasks.algorithm.js +22 -0
- package/tasks/index.d.ts +2 -0
- package/tasks/index.js +2 -0
- package/tasks/task.algorithm.d.ts +18 -0
- package/tasks/task.algorithm.js +23 -0
- package/templating/file-exists.error.d.ts +6 -0
- package/templating/file-exists.error.js +10 -0
- package/templating/index.d.ts +4 -0
- package/templating/index.js +4 -0
- package/templating/parse-template.algorithm.d.ts +11 -0
- package/templating/parse-template.algorithm.js +197 -0
- package/templating/render-template.algorithm.d.ts +7 -0
- package/templating/render-template.algorithm.js +87 -0
- package/templating/template-functions.algorithm.d.ts +10 -0
- package/templating/template-functions.algorithm.js +104 -0
- package/templating/template-syntax.contract.d.ts +33 -0
- package/templating/template-syntax.contract.js +1 -0
- package/templating/template.error.d.ts +4 -0
- package/templating/template.error.js +7 -0
- package/templating/tokenize-action.algorithm.d.ts +20 -0
- package/templating/tokenize-action.algorithm.js +57 -0
- package/templating/write-templates.use-case.d.ts +11 -0
- package/templating/write-templates.use-case.js +44 -0
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { parseTemplate } from './parse-template.algorithm.js';
|
|
2
|
+
import { TEMPLATE_FUNCTIONS, byKey, format, isObject, isTruthy } from './template-functions.algorithm.js';
|
|
3
|
+
import { TemplateError } from './template.error.js';
|
|
4
|
+
/**
|
|
5
|
+
* Renders a `[[ ]]` template with the data, the way the Go SDK's `templating`
|
|
6
|
+
* does with `text/template` and `missingkey=error`: a field the data does not
|
|
7
|
+
* have is an error, not an empty string. See {@link parseTemplate} for the
|
|
8
|
+
* language. `name` is only used in error messages.
|
|
9
|
+
*/
|
|
10
|
+
export function renderTemplate(text, data, name = 'template') {
|
|
11
|
+
const fail = (message) => {
|
|
12
|
+
throw new TemplateError(name, message);
|
|
13
|
+
};
|
|
14
|
+
return render(parseTemplate(text, name), { dot: data, root: data }, fail);
|
|
15
|
+
}
|
|
16
|
+
function render(nodes, scope, fail) {
|
|
17
|
+
return nodes.map(node => renderNode(node, scope, fail)).join('');
|
|
18
|
+
}
|
|
19
|
+
function renderNode(node, scope, fail) {
|
|
20
|
+
if (node.type === 'text')
|
|
21
|
+
return node.text;
|
|
22
|
+
const value = evaluatePipeline(node.pipeline, scope, fail);
|
|
23
|
+
if (node.type === 'action')
|
|
24
|
+
return format(value);
|
|
25
|
+
if (node.type === 'if')
|
|
26
|
+
return render(isTruthy(value) ? node.body : node.otherwise, scope, fail);
|
|
27
|
+
if (node.type === 'with')
|
|
28
|
+
return isTruthy(value) ? render(node.body, { dot: value, root: scope.root }, fail) : render(node.otherwise, scope, fail);
|
|
29
|
+
const items = rangeItems(value, fail);
|
|
30
|
+
return items.length === 0 ? render(node.otherwise, scope, fail) : items.map(item => render(node.body, { dot: item, root: scope.root }, fail)).join('');
|
|
31
|
+
}
|
|
32
|
+
/** What `range` loops over: the elements of an array, or the values of a map in key order. */
|
|
33
|
+
function rangeItems(value, fail) {
|
|
34
|
+
if (Array.isArray(value))
|
|
35
|
+
return value;
|
|
36
|
+
if (value === null || value === undefined)
|
|
37
|
+
return [];
|
|
38
|
+
return isObject(value) ? Object.keys(value).sort(byKey).map(key => value[key]) : fail(`range can't iterate over ${format(value)}`);
|
|
39
|
+
}
|
|
40
|
+
const NONE = Symbol('nothing piped');
|
|
41
|
+
function evaluatePipeline(pipeline, scope, fail) {
|
|
42
|
+
let piped = NONE;
|
|
43
|
+
for (const command of pipeline)
|
|
44
|
+
piped = evaluateCommand(command, scope, piped, fail);
|
|
45
|
+
return piped;
|
|
46
|
+
}
|
|
47
|
+
function evaluateCommand(command, scope, piped, fail) {
|
|
48
|
+
const [head, ...rest] = command;
|
|
49
|
+
if (head.kind === 'name')
|
|
50
|
+
return callFunction(head.name, rest, scope, piped, fail);
|
|
51
|
+
if (rest.length > 0)
|
|
52
|
+
return fail('can\'t give an argument to a value that is not a function');
|
|
53
|
+
return piped === NONE ? evaluateOperand(head, scope, fail) : fail('can\'t pipe into a value that is not a function');
|
|
54
|
+
}
|
|
55
|
+
function callFunction(name, operands, scope, piped, fail) {
|
|
56
|
+
const fn = Object.hasOwn(TEMPLATE_FUNCTIONS, name) ? TEMPLATE_FUNCTIONS[name] : undefined;
|
|
57
|
+
if (fn === undefined)
|
|
58
|
+
return fail(`function "${name}" not defined`);
|
|
59
|
+
const args = operands.map(operand => evaluateOperand(operand, scope, fail));
|
|
60
|
+
if (piped !== NONE)
|
|
61
|
+
args.push(piped);
|
|
62
|
+
try {
|
|
63
|
+
return fn(args);
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
return fail(`error calling ${name}: ${error instanceof Error ? error.message : String(error)}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
function evaluateOperand(operand, scope, fail) {
|
|
70
|
+
if (operand.kind === 'nil')
|
|
71
|
+
return null;
|
|
72
|
+
if ('value' in operand)
|
|
73
|
+
return operand.value;
|
|
74
|
+
if (operand.kind === 'pipeline')
|
|
75
|
+
return evaluatePipeline(operand.pipeline, scope, fail);
|
|
76
|
+
if (operand.kind === 'name')
|
|
77
|
+
return evaluateCommand([operand], scope, NONE, fail);
|
|
78
|
+
let value = operand.root ? scope.root : scope.dot;
|
|
79
|
+
for (const key of operand.path) {
|
|
80
|
+
if (!isObject(value))
|
|
81
|
+
return fail(`can't evaluate field ${key} in ${value === null || value === undefined ? 'nil' : typeof value}`);
|
|
82
|
+
if (!Object.hasOwn(value, key))
|
|
83
|
+
return fail(`map has no entry for key "${key}"`);
|
|
84
|
+
value = value[key];
|
|
85
|
+
}
|
|
86
|
+
return value;
|
|
87
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export type TemplateFunction = (args: unknown[]) => unknown;
|
|
2
|
+
export declare function isObject(value: unknown): value is Record<string, unknown>;
|
|
3
|
+
/** Keys in the order Go sorts the keys of a map in. */
|
|
4
|
+
export declare function byKey(a: string, b: string): number;
|
|
5
|
+
/** Go's truth: false, 0, nil, and an empty string, array or map are false; everything else is true. */
|
|
6
|
+
export declare function isTruthy(value: unknown): boolean;
|
|
7
|
+
/** A value as `text/template` prints it. */
|
|
8
|
+
export declare function format(value: unknown): string;
|
|
9
|
+
/** The functions of `text/template` the subset has. */
|
|
10
|
+
export declare const TEMPLATE_FUNCTIONS: Readonly<Record<string, TemplateFunction>>;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
export function isObject(value) {
|
|
2
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
3
|
+
}
|
|
4
|
+
/** Keys in the order Go sorts the keys of a map in. */
|
|
5
|
+
export function byKey(a, b) {
|
|
6
|
+
return Number(a > b) - Number(a < b);
|
|
7
|
+
}
|
|
8
|
+
/** Go's truth: false, 0, nil, and an empty string, array or map are false; everything else is true. */
|
|
9
|
+
export function isTruthy(value) {
|
|
10
|
+
if ([undefined, null, false, 0, ''].includes(value))
|
|
11
|
+
return false;
|
|
12
|
+
return Array.isArray(value) ? value.length > 0 : !isObject(value) || Object.keys(value).length > 0;
|
|
13
|
+
}
|
|
14
|
+
/** A value as `text/template` prints it. */
|
|
15
|
+
export function format(value) {
|
|
16
|
+
if (value === undefined || value === null)
|
|
17
|
+
return '<no value>';
|
|
18
|
+
if (Array.isArray(value))
|
|
19
|
+
return `[${value.map(item => format(item)).join(' ')}]`;
|
|
20
|
+
if (!isObject(value))
|
|
21
|
+
return String(value);
|
|
22
|
+
const entries = Object.keys(value).sort(byKey).map(key => `${key}:${format(value[key])}`);
|
|
23
|
+
return `map[${entries.join(' ')}]`;
|
|
24
|
+
}
|
|
25
|
+
function comparable(a, b) {
|
|
26
|
+
const kind = typeof a;
|
|
27
|
+
if (typeof b !== kind || !['string', 'number', 'boolean'].includes(kind))
|
|
28
|
+
throw new TypeError('incompatible types for comparison');
|
|
29
|
+
return kind;
|
|
30
|
+
}
|
|
31
|
+
function ordered(test) {
|
|
32
|
+
return ([a, b]) => {
|
|
33
|
+
if (comparable(a, b) === 'boolean')
|
|
34
|
+
throw new TypeError('invalid type for comparison');
|
|
35
|
+
return test(a === b ? 0 : (Number(a > b) || -1));
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
function indexInto(current, key) {
|
|
39
|
+
if (Array.isArray(current)) {
|
|
40
|
+
if (typeof key !== 'number' || key < 0 || key >= current.length)
|
|
41
|
+
throw new RangeError(`index out of range: ${format(key)}`);
|
|
42
|
+
return current[key];
|
|
43
|
+
}
|
|
44
|
+
if (isObject(current))
|
|
45
|
+
return Object.hasOwn(current, String(key)) ? current[String(key)] : undefined;
|
|
46
|
+
throw new TypeError(`can't index ${format(current)}`);
|
|
47
|
+
}
|
|
48
|
+
/** What Sprint puts between operands: a space when neither side is a string. */
|
|
49
|
+
function sprint(args) {
|
|
50
|
+
return args.reduce((out, arg, i) => {
|
|
51
|
+
const space = i > 0 && typeof arg !== 'string' && typeof args[i - 1] !== 'string';
|
|
52
|
+
return `${out}${space ? ' ' : ''}${format(arg)}`;
|
|
53
|
+
}, '');
|
|
54
|
+
}
|
|
55
|
+
/** The functions of `text/template` the subset has. */
|
|
56
|
+
export const TEMPLATE_FUNCTIONS = {
|
|
57
|
+
eq: ([first, ...others]) => {
|
|
58
|
+
if (others.length === 0)
|
|
59
|
+
throw new TypeError('missing argument for comparison');
|
|
60
|
+
return others.some((other) => {
|
|
61
|
+
if (first === null || other === null)
|
|
62
|
+
return first === other;
|
|
63
|
+
comparable(first, other);
|
|
64
|
+
return first === other;
|
|
65
|
+
});
|
|
66
|
+
},
|
|
67
|
+
ne: ([a, b]) => {
|
|
68
|
+
if (a === null || b === null)
|
|
69
|
+
return a !== b;
|
|
70
|
+
comparable(a, b);
|
|
71
|
+
return a !== b;
|
|
72
|
+
},
|
|
73
|
+
lt: ordered(difference => difference < 0),
|
|
74
|
+
le: ordered(difference => difference <= 0),
|
|
75
|
+
gt: ordered(difference => difference > 0),
|
|
76
|
+
ge: ordered(difference => difference >= 0),
|
|
77
|
+
not: ([value]) => !isTruthy(value),
|
|
78
|
+
and: args => args.find(arg => !isTruthy(arg)) ?? args.at(-1),
|
|
79
|
+
or: args => args.find(arg => isTruthy(arg)) ?? args.at(-1),
|
|
80
|
+
len: ([value]) => {
|
|
81
|
+
if (typeof value === 'string')
|
|
82
|
+
return Buffer.byteLength(value);
|
|
83
|
+
if (Array.isArray(value))
|
|
84
|
+
return value.length;
|
|
85
|
+
if (isObject(value))
|
|
86
|
+
return Object.keys(value).length;
|
|
87
|
+
throw new TypeError(`len of ${format(value)}`);
|
|
88
|
+
},
|
|
89
|
+
index: ([value, ...keys]) => keys.reduce((current, key) => indexInto(current, key), value),
|
|
90
|
+
print: args => sprint(args),
|
|
91
|
+
printf: ([pattern, ...args]) => {
|
|
92
|
+
if (typeof pattern !== 'string')
|
|
93
|
+
throw new TypeError('printf needs a format string');
|
|
94
|
+
let next = 0;
|
|
95
|
+
return pattern.replaceAll(/%([%sdvqt])/g, (_match, verb) => {
|
|
96
|
+
if (verb === '%')
|
|
97
|
+
return '%';
|
|
98
|
+
if (next >= args.length)
|
|
99
|
+
throw new RangeError(`missing argument for %${verb}`);
|
|
100
|
+
const arg = args[next++];
|
|
101
|
+
return verb === 'q' ? JSON.stringify(String(arg)) : format(arg);
|
|
102
|
+
});
|
|
103
|
+
},
|
|
104
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/** One operand of a command: a field of the data, a literal, a parenthesised pipeline or a function name. */
|
|
2
|
+
export type Operand = {
|
|
3
|
+
kind: 'field';
|
|
4
|
+
root: boolean;
|
|
5
|
+
path: string[];
|
|
6
|
+
} | {
|
|
7
|
+
kind: 'string' | 'number' | 'bool';
|
|
8
|
+
value: string | number | boolean;
|
|
9
|
+
} | {
|
|
10
|
+
kind: 'nil';
|
|
11
|
+
} | {
|
|
12
|
+
kind: 'pipeline';
|
|
13
|
+
pipeline: Pipeline;
|
|
14
|
+
} | {
|
|
15
|
+
kind: 'name';
|
|
16
|
+
name: string;
|
|
17
|
+
};
|
|
18
|
+
/** Operands in a row: a function call (`eq .Kind "app"`) or a single value. */
|
|
19
|
+
export type Command = Operand[];
|
|
20
|
+
/** Commands joined by `|`, each receiving the previous one's value as its last argument. */
|
|
21
|
+
export type Pipeline = Command[];
|
|
22
|
+
export type TemplateNode = {
|
|
23
|
+
type: 'text';
|
|
24
|
+
text: string;
|
|
25
|
+
} | {
|
|
26
|
+
type: 'action';
|
|
27
|
+
pipeline: Pipeline;
|
|
28
|
+
} | {
|
|
29
|
+
type: 'if' | 'with' | 'range';
|
|
30
|
+
pipeline: Pipeline;
|
|
31
|
+
body: TemplateNode[];
|
|
32
|
+
otherwise: TemplateNode[];
|
|
33
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** A template that cannot be parsed or rendered: bad syntax, a field the data does not have, a construct outside the supported subset. */
|
|
2
|
+
export class TemplateError extends Error {
|
|
3
|
+
constructor(name, message) {
|
|
4
|
+
super(`template ${name}: ${message}`);
|
|
5
|
+
this.name = 'TemplateError';
|
|
6
|
+
}
|
|
7
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export type Token = {
|
|
2
|
+
t: 'open' | 'close' | 'pipe';
|
|
3
|
+
} | {
|
|
4
|
+
t: 'field';
|
|
5
|
+
root: boolean;
|
|
6
|
+
path: string[];
|
|
7
|
+
} | {
|
|
8
|
+
t: 'string';
|
|
9
|
+
value: string;
|
|
10
|
+
} | {
|
|
11
|
+
t: 'number';
|
|
12
|
+
value: number;
|
|
13
|
+
} | {
|
|
14
|
+
t: 'word';
|
|
15
|
+
value: string;
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Splits the text of an action into tokens. `fail` reports what cannot be a token.
|
|
19
|
+
*/
|
|
20
|
+
export declare function tokenizeAction(body: string, fail: (message: string) => never): Token[];
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The elements of an action, in the order the alternatives are tried: space,
|
|
3
|
+
* brackets and pipe, a quoted or raw string, a number, a field (`.A.B`, `$`,
|
|
4
|
+
* `$.A`, or a lone `.`), a word, and `:=`/`=`, which are variables. Anything else
|
|
5
|
+
* is the last alternative, so every character belongs to some match.
|
|
6
|
+
*/
|
|
7
|
+
const ELEMENT = new RegExp([
|
|
8
|
+
String.raw `\s+`,
|
|
9
|
+
String.raw `(?<open>\()`,
|
|
10
|
+
String.raw `(?<close>\))`,
|
|
11
|
+
String.raw `(?<pipe>\|)`,
|
|
12
|
+
String.raw `(?<string>"(?:[^"\\]|\\.)*")`,
|
|
13
|
+
'(?<raw>`[^`]*`)',
|
|
14
|
+
String.raw `(?<number>-?\d+(?:\.\d+)?(?![\w.]))`,
|
|
15
|
+
String.raw `(?<field>\$(?:\.[A-Z_]\w*)*|(?:\.[A-Z_]\w*)+|\.)`,
|
|
16
|
+
String.raw `(?<word>[A-Z_]\w*)`,
|
|
17
|
+
'(?<variable>:?=)',
|
|
18
|
+
'(?<other>.)',
|
|
19
|
+
].join('|'), 'gis');
|
|
20
|
+
/**
|
|
21
|
+
* Splits the text of an action into tokens. `fail` reports what cannot be a token.
|
|
22
|
+
*/
|
|
23
|
+
export function tokenizeAction(body, fail) {
|
|
24
|
+
const tokens = [];
|
|
25
|
+
for (const match of body.matchAll(ELEMENT)) {
|
|
26
|
+
const found = match.groups ?? {};
|
|
27
|
+
if (found.open !== undefined)
|
|
28
|
+
tokens.push({ t: 'open' });
|
|
29
|
+
else if (found.close !== undefined)
|
|
30
|
+
tokens.push({ t: 'close' });
|
|
31
|
+
else if (found.pipe !== undefined)
|
|
32
|
+
tokens.push({ t: 'pipe' });
|
|
33
|
+
else if (found.string !== undefined)
|
|
34
|
+
tokens.push({ t: 'string', value: unquote(found.string, fail) });
|
|
35
|
+
else if (found.raw !== undefined)
|
|
36
|
+
tokens.push({ t: 'string', value: found.raw.slice(1, -1) });
|
|
37
|
+
else if (found.number !== undefined)
|
|
38
|
+
tokens.push({ t: 'number', value: Number(found.number) });
|
|
39
|
+
else if (found.field !== undefined)
|
|
40
|
+
tokens.push({ t: 'field', root: found.field.startsWith('$'), path: found.field.split('.').filter(part => part !== '' && part !== '$') });
|
|
41
|
+
else if (found.word !== undefined)
|
|
42
|
+
tokens.push({ t: 'word', value: found.word });
|
|
43
|
+
else if (found.variable !== undefined)
|
|
44
|
+
fail('variables are not supported by the SDK\'s templates');
|
|
45
|
+
else if (found.other !== undefined)
|
|
46
|
+
fail(`unexpected character "${found.other}" in action`);
|
|
47
|
+
}
|
|
48
|
+
return tokens;
|
|
49
|
+
}
|
|
50
|
+
function unquote(literal, fail) {
|
|
51
|
+
try {
|
|
52
|
+
return JSON.parse(literal);
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return fail(`invalid string ${literal}`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Renders every template in `files` (target path to template text) under
|
|
3
|
+
* `root` with `data`, creating directories as needed. A target path may itself
|
|
4
|
+
* contain template actions (`[[.Dir]]/main.ts`). Returns the written paths,
|
|
5
|
+
* forward-slashed and relative to `root`, in the order of the sorted templates.
|
|
6
|
+
*
|
|
7
|
+
* Nothing is written if any template fails to render or any target exists
|
|
8
|
+
* ({@link FileExistsError}): it never overwrites a file. A target that would
|
|
9
|
+
* land outside `root` is refused.
|
|
10
|
+
*/
|
|
11
|
+
export declare function writeTemplates(root: string, files: Record<string, string>, data?: unknown): Promise<string[]>;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { access, mkdir, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { dirname, isAbsolute, join, posix, resolve, sep } from 'node:path';
|
|
3
|
+
import { FileExistsError } from './file-exists.error.js';
|
|
4
|
+
import { renderTemplate } from './render-template.algorithm.js';
|
|
5
|
+
import { byKey } from './template-functions.algorithm.js';
|
|
6
|
+
import { TemplateError } from './template.error.js';
|
|
7
|
+
/**
|
|
8
|
+
* Renders every template in `files` (target path to template text) under
|
|
9
|
+
* `root` with `data`, creating directories as needed. A target path may itself
|
|
10
|
+
* contain template actions (`[[.Dir]]/main.ts`). Returns the written paths,
|
|
11
|
+
* forward-slashed and relative to `root`, in the order of the sorted templates.
|
|
12
|
+
*
|
|
13
|
+
* Nothing is written if any template fails to render or any target exists
|
|
14
|
+
* ({@link FileExistsError}): it never overwrites a file. A target that would
|
|
15
|
+
* land outside `root` is refused.
|
|
16
|
+
*/
|
|
17
|
+
export async function writeTemplates(root, files, data) {
|
|
18
|
+
const rendered = [];
|
|
19
|
+
for (const target of Object.keys(files).sort(byKey)) {
|
|
20
|
+
const path = posix.normalize(renderTemplate(target, data, 'path'));
|
|
21
|
+
const full = resolve(root, ...path.split('/'));
|
|
22
|
+
if (path === '..' || path.startsWith('../') || isAbsolute(path) || !full.startsWith(resolve(root) + sep)) {
|
|
23
|
+
throw new TemplateError(path, 'the target is outside the project root');
|
|
24
|
+
}
|
|
25
|
+
if (await exists(full))
|
|
26
|
+
throw new FileExistsError(path);
|
|
27
|
+
rendered.push({ path, content: renderTemplate(files[target], data, path) });
|
|
28
|
+
}
|
|
29
|
+
for (const file of rendered) {
|
|
30
|
+
const full = join(root, ...file.path.split('/'));
|
|
31
|
+
await mkdir(dirname(full), { recursive: true, mode: 0o755 });
|
|
32
|
+
await writeFile(full, file.content, { mode: 0o644 });
|
|
33
|
+
}
|
|
34
|
+
return rendered.map(file => file.path);
|
|
35
|
+
}
|
|
36
|
+
async function exists(path) {
|
|
37
|
+
try {
|
|
38
|
+
await access(path);
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
}
|