@dusted/anqst 0.1.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/LICENSE +13 -0
- package/README.md +136 -0
- package/dist/src/app.js +369 -0
- package/dist/src/bin/anqst.js +8 -0
- package/dist/src/emit.js +2500 -0
- package/dist/src/errors.js +24 -0
- package/dist/src/model.js +2 -0
- package/dist/src/parser.js +259 -0
- package/dist/src/project.js +123 -0
- package/dist/src/verify.js +200 -0
- package/package.json +53 -0
- package/spec/AnQst-Spec-DSL.d.ts +217 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.VerifyError = void 0;
|
|
7
|
+
exports.formatVerifyError = formatVerifyError;
|
|
8
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
9
|
+
class VerifyError extends Error {
|
|
10
|
+
constructor(message, loc) {
|
|
11
|
+
super(message);
|
|
12
|
+
this.name = "VerifyError";
|
|
13
|
+
this.loc = loc;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
exports.VerifyError = VerifyError;
|
|
17
|
+
function formatVerifyError(error) {
|
|
18
|
+
const normalizeSlashes = (inputPath) => inputPath.split(node_path_1.default.sep).join("/");
|
|
19
|
+
const normalizedFile = normalizeSlashes(error.loc?.file ?? "<unknown>");
|
|
20
|
+
if (error.loc) {
|
|
21
|
+
return `\nAnQst spec invalid: ${normalizedFile}\n ${normalizedFile}:${error.loc.line}:${error.loc.column} ${error.message}\n`;
|
|
22
|
+
}
|
|
23
|
+
return `\nAnQst spec invalid: ${normalizedFile}\n ${error.message}\n`;
|
|
24
|
+
}
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.parseSpecFile = parseSpecFile;
|
|
7
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
8
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
9
|
+
const typescript_1 = __importDefault(require("typescript"));
|
|
10
|
+
const errors_1 = require("./errors");
|
|
11
|
+
function locFromNode(source, node) {
|
|
12
|
+
const lc = source.getLineAndCharacterOfPosition(node.getStart(source));
|
|
13
|
+
return {
|
|
14
|
+
file: source.fileName,
|
|
15
|
+
line: lc.line + 1,
|
|
16
|
+
column: lc.character + 1
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
function qNameToText(name) {
|
|
20
|
+
if (typescript_1.default.isIdentifier(name))
|
|
21
|
+
return name.text;
|
|
22
|
+
return `${qNameToText(name.left)}.${name.right.text}`;
|
|
23
|
+
}
|
|
24
|
+
function collectReferencedTypeNames(node) {
|
|
25
|
+
const refs = new Set();
|
|
26
|
+
const visit = (n) => {
|
|
27
|
+
if (typescript_1.default.isTypeReferenceNode(n)) {
|
|
28
|
+
refs.add(qNameToText(n.typeName));
|
|
29
|
+
}
|
|
30
|
+
else if (typescript_1.default.isExpressionWithTypeArguments(n) && typescript_1.default.isIdentifier(n.expression)) {
|
|
31
|
+
refs.add(n.expression.text);
|
|
32
|
+
}
|
|
33
|
+
typescript_1.default.forEachChild(n, visit);
|
|
34
|
+
};
|
|
35
|
+
visit(node);
|
|
36
|
+
return [...refs];
|
|
37
|
+
}
|
|
38
|
+
function parseTypeDecl(source, node) {
|
|
39
|
+
const name = node.name.text;
|
|
40
|
+
return {
|
|
41
|
+
name,
|
|
42
|
+
kind: typescript_1.default.isInterfaceDeclaration(node) ? "interface" : "type",
|
|
43
|
+
nodeText: node.getText(source),
|
|
44
|
+
referencedTypeNames: collectReferencedTypeNames(node),
|
|
45
|
+
loc: locFromNode(source, node)
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
function parseMemberKindFromAnQstType(typeNode) {
|
|
49
|
+
if (!typescript_1.default.isTypeReferenceNode(typeNode))
|
|
50
|
+
return null;
|
|
51
|
+
const typeName = qNameToText(typeNode.typeName);
|
|
52
|
+
if (!typeName.startsWith("AnQst."))
|
|
53
|
+
return null;
|
|
54
|
+
const kind = typeName.slice("AnQst.".length);
|
|
55
|
+
if (!["Call", "Slot", "Emitter", "Output", "Input"].includes(kind))
|
|
56
|
+
return null;
|
|
57
|
+
if (kind === "Emitter")
|
|
58
|
+
return { kind, payload: null };
|
|
59
|
+
const arg = typeNode.typeArguments?.[0];
|
|
60
|
+
return { kind, payload: arg ? arg.getText() : null };
|
|
61
|
+
}
|
|
62
|
+
function parseServiceMember(source, member) {
|
|
63
|
+
if (typescript_1.default.isMethodSignature(member)) {
|
|
64
|
+
if (member.questionToken)
|
|
65
|
+
throw new errors_1.VerifyError("Optional service methods are not allowed.", locFromNode(source, member));
|
|
66
|
+
const returnType = member.type;
|
|
67
|
+
if (!returnType)
|
|
68
|
+
throw new errors_1.VerifyError("Service method must declare return type.", locFromNode(source, member));
|
|
69
|
+
const parsed = parseMemberKindFromAnQstType(returnType);
|
|
70
|
+
if (!parsed)
|
|
71
|
+
throw new errors_1.VerifyError(`Unsupported service method return type '${returnType.getText()}'.`, locFromNode(source, member));
|
|
72
|
+
if (parsed.kind === "Input" || parsed.kind === "Output") {
|
|
73
|
+
throw new errors_1.VerifyError(`${parsed.kind} must be declared as property, not method.`, locFromNode(source, member));
|
|
74
|
+
}
|
|
75
|
+
if (!member.name || !typescript_1.default.isIdentifier(member.name)) {
|
|
76
|
+
throw new errors_1.VerifyError("Only identifier service method names are supported.", locFromNode(source, member));
|
|
77
|
+
}
|
|
78
|
+
const parameters = member.parameters.map((param) => {
|
|
79
|
+
if (param.dotDotDotToken)
|
|
80
|
+
throw new errors_1.VerifyError("Rest parameters are not allowed in service methods.", locFromNode(source, param));
|
|
81
|
+
if (!typescript_1.default.isIdentifier(param.name))
|
|
82
|
+
throw new errors_1.VerifyError("Only identifier parameter names are supported.", locFromNode(source, param));
|
|
83
|
+
if (!param.type)
|
|
84
|
+
throw new errors_1.VerifyError("Service parameters must declare type.", locFromNode(source, param));
|
|
85
|
+
return { name: param.name.text, typeText: param.type.getText() };
|
|
86
|
+
});
|
|
87
|
+
return {
|
|
88
|
+
kind: parsed.kind,
|
|
89
|
+
name: member.name.text,
|
|
90
|
+
payloadTypeText: parsed.payload,
|
|
91
|
+
parameters,
|
|
92
|
+
loc: locFromNode(source, member)
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
if (typescript_1.default.isPropertySignature(member)) {
|
|
96
|
+
if (!member.type)
|
|
97
|
+
throw new errors_1.VerifyError("Service property must declare type.", locFromNode(source, member));
|
|
98
|
+
if (member.questionToken)
|
|
99
|
+
throw new errors_1.VerifyError("Optional service properties are not allowed.", locFromNode(source, member));
|
|
100
|
+
const parsed = parseMemberKindFromAnQstType(member.type);
|
|
101
|
+
if (!parsed)
|
|
102
|
+
throw new errors_1.VerifyError(`Unsupported service property type '${member.type.getText()}'.`, locFromNode(source, member));
|
|
103
|
+
if (parsed.kind !== "Input" && parsed.kind !== "Output") {
|
|
104
|
+
throw new errors_1.VerifyError(`${parsed.kind} must be declared as method, not property.`, locFromNode(source, member));
|
|
105
|
+
}
|
|
106
|
+
if (!member.name || !typescript_1.default.isIdentifier(member.name)) {
|
|
107
|
+
throw new errors_1.VerifyError("Only identifier service property names are supported.", locFromNode(source, member));
|
|
108
|
+
}
|
|
109
|
+
return {
|
|
110
|
+
kind: parsed.kind,
|
|
111
|
+
name: member.name.text,
|
|
112
|
+
payloadTypeText: parsed.payload,
|
|
113
|
+
parameters: [],
|
|
114
|
+
loc: locFromNode(source, member)
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
throw new errors_1.VerifyError("Unsupported service member declaration.", locFromNode(source, member));
|
|
118
|
+
}
|
|
119
|
+
function tryResolveImportFile(specFilePath, moduleName) {
|
|
120
|
+
const baseDir = node_path_1.default.dirname(specFilePath);
|
|
121
|
+
const candidates = [];
|
|
122
|
+
const pushCandidates = (p) => {
|
|
123
|
+
candidates.push(p, `${p}.d.ts`, `${p}.ts`, node_path_1.default.join(p, "index.d.ts"), node_path_1.default.join(p, "index.ts"));
|
|
124
|
+
};
|
|
125
|
+
if (moduleName.startsWith(".") || moduleName.startsWith("/")) {
|
|
126
|
+
pushCandidates(node_path_1.default.resolve(baseDir, moduleName));
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
// Support path-like bare spec imports (e.g. types/exchange) relative to spec file dir.
|
|
130
|
+
pushCandidates(node_path_1.default.resolve(baseDir, moduleName));
|
|
131
|
+
}
|
|
132
|
+
for (const candidate of candidates) {
|
|
133
|
+
if (node_fs_1.default.existsSync(candidate) && node_fs_1.default.statSync(candidate).isFile())
|
|
134
|
+
return candidate;
|
|
135
|
+
}
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
function requiresLocalImportResolution(moduleName) {
|
|
139
|
+
if (moduleName.startsWith(".") || moduleName.startsWith("/"))
|
|
140
|
+
return true;
|
|
141
|
+
if (moduleName.startsWith("@"))
|
|
142
|
+
return false;
|
|
143
|
+
return moduleName.includes("/");
|
|
144
|
+
}
|
|
145
|
+
function parseImportedTypeDecls(specFilePath, source) {
|
|
146
|
+
const importedTypeDecls = new Map();
|
|
147
|
+
const importedTypeSymbols = new Set();
|
|
148
|
+
const specImports = [];
|
|
149
|
+
for (const stmt of source.statements) {
|
|
150
|
+
if (!typescript_1.default.isImportDeclaration(stmt) || !stmt.importClause || !typescript_1.default.isStringLiteral(stmt.moduleSpecifier))
|
|
151
|
+
continue;
|
|
152
|
+
const moduleName = stmt.moduleSpecifier.text;
|
|
153
|
+
const importModel = {
|
|
154
|
+
moduleSpecifier: moduleName,
|
|
155
|
+
defaultImport: null,
|
|
156
|
+
namedImports: []
|
|
157
|
+
};
|
|
158
|
+
if (stmt.importClause.name) {
|
|
159
|
+
importedTypeSymbols.add(stmt.importClause.name.text);
|
|
160
|
+
importModel.defaultImport = stmt.importClause.name.text;
|
|
161
|
+
}
|
|
162
|
+
const bindings = stmt.importClause.namedBindings;
|
|
163
|
+
if (bindings && typescript_1.default.isNamedImports(bindings)) {
|
|
164
|
+
for (const el of bindings.elements) {
|
|
165
|
+
importedTypeSymbols.add((el.propertyName ?? el.name).text);
|
|
166
|
+
importedTypeSymbols.add(el.name.text);
|
|
167
|
+
importModel.namedImports.push({
|
|
168
|
+
importedName: (el.propertyName ?? el.name).text,
|
|
169
|
+
localName: el.name.text
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
else if (bindings && typescript_1.default.isNamespaceImport(bindings)) {
|
|
174
|
+
throw new errors_1.VerifyError("Namespace imports ('import * as X') are not allowed in AnQst spec files.", locFromNode(source, bindings));
|
|
175
|
+
}
|
|
176
|
+
specImports.push(importModel);
|
|
177
|
+
const resolved = tryResolveImportFile(specFilePath, moduleName);
|
|
178
|
+
if (!resolved) {
|
|
179
|
+
if (requiresLocalImportResolution(moduleName)) {
|
|
180
|
+
throw new errors_1.VerifyError(`Unable to resolve import '${moduleName}' from spec file.`, locFromNode(source, stmt.moduleSpecifier));
|
|
181
|
+
}
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
const text = node_fs_1.default.readFileSync(resolved, "utf8");
|
|
185
|
+
const importedSource = typescript_1.default.createSourceFile(resolved, text, typescript_1.default.ScriptTarget.Latest, true, typescript_1.default.ScriptKind.TS);
|
|
186
|
+
for (const importedStmt of importedSource.statements) {
|
|
187
|
+
if (typescript_1.default.isInterfaceDeclaration(importedStmt) || typescript_1.default.isTypeAliasDeclaration(importedStmt)) {
|
|
188
|
+
const decl = parseTypeDecl(importedSource, importedStmt);
|
|
189
|
+
importedTypeDecls.set(decl.name, decl);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return { importedTypeDecls, importedTypeSymbols, specImports };
|
|
194
|
+
}
|
|
195
|
+
function serviceBaseType(iface) {
|
|
196
|
+
if (!iface.heritageClauses)
|
|
197
|
+
return null;
|
|
198
|
+
for (const clause of iface.heritageClauses) {
|
|
199
|
+
if (clause.token !== typescript_1.default.SyntaxKind.ExtendsKeyword)
|
|
200
|
+
continue;
|
|
201
|
+
for (const t of clause.types) {
|
|
202
|
+
if (t.expression.getText() === "AnQst.Service")
|
|
203
|
+
return "Service";
|
|
204
|
+
if (t.expression.getText() === "AnQst.AngularHTTPBaseServerClass")
|
|
205
|
+
return "AngularHTTPBaseServerClass";
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
function parseSpecFile(specFilePath) {
|
|
211
|
+
if (!node_fs_1.default.existsSync(specFilePath))
|
|
212
|
+
throw new errors_1.VerifyError(`Spec file does not exist: ${specFilePath}`);
|
|
213
|
+
const text = node_fs_1.default.readFileSync(specFilePath, "utf8");
|
|
214
|
+
const source = typescript_1.default.createSourceFile(specFilePath, text, typescript_1.default.ScriptTarget.Latest, true, typescript_1.default.ScriptKind.TS);
|
|
215
|
+
const namespaces = source.statements.filter((s) => typescript_1.default.isModuleDeclaration(s));
|
|
216
|
+
if (namespaces.length !== 1)
|
|
217
|
+
throw new errors_1.VerifyError("Spec must declare exactly one top-level namespace.");
|
|
218
|
+
const ns = namespaces[0];
|
|
219
|
+
if (!typescript_1.default.isIdentifier(ns.name))
|
|
220
|
+
throw new errors_1.VerifyError("Namespace name must be an identifier.", locFromNode(source, ns));
|
|
221
|
+
const hasDeclare = !!ns.modifiers?.some((m) => m.kind === typescript_1.default.SyntaxKind.DeclareKeyword);
|
|
222
|
+
if (!hasDeclare)
|
|
223
|
+
throw new errors_1.VerifyError("Top-level namespace must be declared with 'declare namespace'.", locFromNode(source, ns));
|
|
224
|
+
if (!ns.body || !typescript_1.default.isModuleBlock(ns.body))
|
|
225
|
+
throw new errors_1.VerifyError("Namespace body must be a block.", locFromNode(source, ns));
|
|
226
|
+
const services = [];
|
|
227
|
+
const namespaceTypeDecls = [];
|
|
228
|
+
let supportsDevelopmentModeTransport = false;
|
|
229
|
+
for (const stmt of ns.body.statements) {
|
|
230
|
+
if (typescript_1.default.isInterfaceDeclaration(stmt)) {
|
|
231
|
+
const baseType = serviceBaseType(stmt);
|
|
232
|
+
if (baseType !== null) {
|
|
233
|
+
const members = stmt.members.map((member) => parseServiceMember(source, member));
|
|
234
|
+
if (baseType === "AngularHTTPBaseServerClass") {
|
|
235
|
+
supportsDevelopmentModeTransport = true;
|
|
236
|
+
}
|
|
237
|
+
services.push({ name: stmt.name.text, baseType, members, loc: locFromNode(source, stmt) });
|
|
238
|
+
}
|
|
239
|
+
else {
|
|
240
|
+
namespaceTypeDecls.push(parseTypeDecl(source, stmt));
|
|
241
|
+
}
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
if (typescript_1.default.isTypeAliasDeclaration(stmt)) {
|
|
245
|
+
namespaceTypeDecls.push(parseTypeDecl(source, stmt));
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
const importInfo = parseImportedTypeDecls(specFilePath, source);
|
|
249
|
+
return {
|
|
250
|
+
filePath: specFilePath,
|
|
251
|
+
widgetName: ns.name.text,
|
|
252
|
+
services,
|
|
253
|
+
supportsDevelopmentModeTransport,
|
|
254
|
+
namespaceTypeDecls,
|
|
255
|
+
importedTypeDecls: importInfo.importedTypeDecls,
|
|
256
|
+
importedTypeSymbols: importInfo.importedTypeSymbols,
|
|
257
|
+
specImports: importInfo.specImports
|
|
258
|
+
};
|
|
259
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.DEFAULT_ANQST_GENERATE_TARGETS = void 0;
|
|
7
|
+
exports.readProjectPackage = readProjectPackage;
|
|
8
|
+
exports.resolveAnQstSpecPath = resolveAnQstSpecPath;
|
|
9
|
+
exports.resolveAnQstGenerateTargets = resolveAnQstGenerateTargets;
|
|
10
|
+
exports.installDslShim = installDslShim;
|
|
11
|
+
exports.buildSpecScaffold = buildSpecScaffold;
|
|
12
|
+
exports.runInstill = runInstill;
|
|
13
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
14
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
15
|
+
const errors_1 = require("./errors");
|
|
16
|
+
exports.DEFAULT_ANQST_GENERATE_TARGETS = ["QWidget", "AngularService", "//DOM", "//node_express_ws"];
|
|
17
|
+
function readJsonFile(filePath) {
|
|
18
|
+
const raw = node_fs_1.default.readFileSync(filePath, "utf8");
|
|
19
|
+
return JSON.parse(raw);
|
|
20
|
+
}
|
|
21
|
+
function prependScript(existing, prefix) {
|
|
22
|
+
if (!existing || existing.trim().length === 0)
|
|
23
|
+
return prefix;
|
|
24
|
+
const trimmed = existing.trim();
|
|
25
|
+
if (trimmed === prefix || trimmed.startsWith(`${prefix} &&`))
|
|
26
|
+
return trimmed;
|
|
27
|
+
return `${prefix} && ${trimmed}`;
|
|
28
|
+
}
|
|
29
|
+
function readProjectPackage(cwd) {
|
|
30
|
+
const packagePath = node_path_1.default.join(cwd, "package.json");
|
|
31
|
+
if (!node_fs_1.default.existsSync(packagePath)) {
|
|
32
|
+
throw new errors_1.VerifyError("No package.json: Can only instill AnQst inside an npm project.");
|
|
33
|
+
}
|
|
34
|
+
return {
|
|
35
|
+
packagePath,
|
|
36
|
+
packageJson: readJsonFile(packagePath)
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
function resolveAnQstSpecPath(cwd) {
|
|
40
|
+
const { packageJson } = readProjectPackage(cwd);
|
|
41
|
+
const spec = packageJson.AnQst?.spec;
|
|
42
|
+
if (!spec || spec.trim().length === 0) {
|
|
43
|
+
throw new errors_1.VerifyError("Missing package.json key 'AnQst.spec'. Run 'anqst instill <WidgetName>' first.");
|
|
44
|
+
}
|
|
45
|
+
return node_path_1.default.resolve(cwd, spec);
|
|
46
|
+
}
|
|
47
|
+
function resolveAnQstGenerateTargets(cwd) {
|
|
48
|
+
const { packageJson } = readProjectPackage(cwd);
|
|
49
|
+
const configured = packageJson.AnQst?.generate;
|
|
50
|
+
if (configured === undefined) {
|
|
51
|
+
return [...exports.DEFAULT_ANQST_GENERATE_TARGETS];
|
|
52
|
+
}
|
|
53
|
+
if (!Array.isArray(configured) || configured.some((value) => typeof value !== "string")) {
|
|
54
|
+
throw new errors_1.VerifyError("Invalid package.json key 'AnQst.generate': expected string array.");
|
|
55
|
+
}
|
|
56
|
+
return [...configured];
|
|
57
|
+
}
|
|
58
|
+
function loadDslSource() {
|
|
59
|
+
const candidates = [
|
|
60
|
+
node_path_1.default.resolve(__dirname, "../../spec/AnQst-Spec-DSL.d.ts"),
|
|
61
|
+
node_path_1.default.resolve(__dirname, "../../../spec/AnQst-Spec-DSL.d.ts")
|
|
62
|
+
];
|
|
63
|
+
for (const filePath of candidates) {
|
|
64
|
+
if (node_fs_1.default.existsSync(filePath))
|
|
65
|
+
return node_fs_1.default.readFileSync(filePath, "utf8");
|
|
66
|
+
}
|
|
67
|
+
return `export namespace AnQst {
|
|
68
|
+
interface Service {}
|
|
69
|
+
interface Call<T> { dummy: T }
|
|
70
|
+
interface Slot<T> { dummy: T }
|
|
71
|
+
interface Emitter {}
|
|
72
|
+
interface Output<T> {}
|
|
73
|
+
interface Input<T> {}
|
|
74
|
+
enum Type {
|
|
75
|
+
string = "string",
|
|
76
|
+
number = "number",
|
|
77
|
+
qint64 = "qint64",
|
|
78
|
+
qint32 = "qint32"
|
|
79
|
+
}
|
|
80
|
+
}`;
|
|
81
|
+
}
|
|
82
|
+
function installDslShim(cwd) {
|
|
83
|
+
const dslDir = node_path_1.default.join(cwd, "anqst-dsl");
|
|
84
|
+
node_fs_1.default.mkdirSync(dslDir, { recursive: true });
|
|
85
|
+
node_fs_1.default.writeFileSync(node_path_1.default.join(dslDir, "AnQst-Spec-DSL.d.ts"), loadDslSource(), "utf8");
|
|
86
|
+
}
|
|
87
|
+
function buildSpecScaffold(widgetName) {
|
|
88
|
+
return `import { AnQst } from "./anqst-dsl/AnQst-Spec-DSL";
|
|
89
|
+
|
|
90
|
+
declare namespace ${widgetName} {
|
|
91
|
+
|
|
92
|
+
}
|
|
93
|
+
`;
|
|
94
|
+
}
|
|
95
|
+
function runInstill(cwd, widgetName) {
|
|
96
|
+
if (!widgetName || widgetName.trim().length === 0) {
|
|
97
|
+
throw new errors_1.VerifyError("Usage: anqst instill <WidgetName>");
|
|
98
|
+
}
|
|
99
|
+
const cleanName = widgetName.trim();
|
|
100
|
+
const { packagePath, packageJson } = readProjectPackage(cwd);
|
|
101
|
+
if (packageJson.AnQst) {
|
|
102
|
+
throw new errors_1.VerifyError("AnQst already instilled, did you mean to run 'npx anqst build'?");
|
|
103
|
+
}
|
|
104
|
+
const next = {
|
|
105
|
+
...packageJson,
|
|
106
|
+
scripts: {
|
|
107
|
+
...packageJson.scripts,
|
|
108
|
+
build: prependScript(packageJson.scripts?.build, "npx anqst build"),
|
|
109
|
+
test: prependScript(packageJson.scripts?.test, "npx anqst test")
|
|
110
|
+
},
|
|
111
|
+
AnQst: {
|
|
112
|
+
spec: `${cleanName}.AnQst.d.ts`,
|
|
113
|
+
generate: [...exports.DEFAULT_ANQST_GENERATE_TARGETS]
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
node_fs_1.default.writeFileSync(packagePath, `${JSON.stringify(next, null, 2)}\n`, "utf8");
|
|
117
|
+
const specPath = node_path_1.default.join(cwd, `${cleanName}.AnQst.d.ts`);
|
|
118
|
+
if (!node_fs_1.default.existsSync(specPath)) {
|
|
119
|
+
node_fs_1.default.writeFileSync(specPath, buildSpecScaffold(cleanName), "utf8");
|
|
120
|
+
}
|
|
121
|
+
installDslShim(cwd);
|
|
122
|
+
return `Instill completed: configured package.json and scaffolded ${cleanName}.AnQst.d.ts`;
|
|
123
|
+
}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.verifySpec = verifySpec;
|
|
7
|
+
const typescript_1 = __importDefault(require("typescript"));
|
|
8
|
+
const errors_1 = require("./errors");
|
|
9
|
+
function parseTypeNodeFromText(typeText) {
|
|
10
|
+
const source = typescript_1.default.createSourceFile("__inline__.ts", `type __X = ${typeText};`, typescript_1.default.ScriptTarget.Latest, true, typescript_1.default.ScriptKind.TS);
|
|
11
|
+
const stmt = source.statements.find(typescript_1.default.isTypeAliasDeclaration);
|
|
12
|
+
if (!stmt)
|
|
13
|
+
throw new errors_1.VerifyError(`Unable to parse type: ${typeText}`);
|
|
14
|
+
return stmt.type;
|
|
15
|
+
}
|
|
16
|
+
function qNameText(name) {
|
|
17
|
+
if (typescript_1.default.isIdentifier(name))
|
|
18
|
+
return name.text;
|
|
19
|
+
return `${qNameText(name.left)}.${name.right.text}`;
|
|
20
|
+
}
|
|
21
|
+
function typeRefs(typeNode) {
|
|
22
|
+
const refs = new Set();
|
|
23
|
+
const visit = (node) => {
|
|
24
|
+
if (typescript_1.default.isTypeReferenceNode(node))
|
|
25
|
+
refs.add(qNameText(node.typeName));
|
|
26
|
+
typescript_1.default.forEachChild(node, visit);
|
|
27
|
+
};
|
|
28
|
+
visit(typeNode);
|
|
29
|
+
return [...refs];
|
|
30
|
+
}
|
|
31
|
+
function checkForbiddenTypeNodes(typeNode) {
|
|
32
|
+
let forbidden = null;
|
|
33
|
+
const visit = (node) => {
|
|
34
|
+
if (forbidden)
|
|
35
|
+
return;
|
|
36
|
+
if (node.kind === typescript_1.default.SyntaxKind.AnyKeyword)
|
|
37
|
+
forbidden = "any";
|
|
38
|
+
else if (node.kind === typescript_1.default.SyntaxKind.UnknownKeyword)
|
|
39
|
+
forbidden = "unknown";
|
|
40
|
+
else if (node.kind === typescript_1.default.SyntaxKind.NeverKeyword)
|
|
41
|
+
forbidden = "never";
|
|
42
|
+
else if (node.kind === typescript_1.default.SyntaxKind.SymbolKeyword)
|
|
43
|
+
forbidden = "symbol";
|
|
44
|
+
else if (typescript_1.default.isFunctionTypeNode(node))
|
|
45
|
+
forbidden = "function type";
|
|
46
|
+
else if (typescript_1.default.isTypeReferenceNode(node) && qNameText(node.typeName) === "Promise")
|
|
47
|
+
forbidden = "Promise";
|
|
48
|
+
typescript_1.default.forEachChild(node, visit);
|
|
49
|
+
};
|
|
50
|
+
visit(typeNode);
|
|
51
|
+
return forbidden;
|
|
52
|
+
}
|
|
53
|
+
function checkServiceMember(member) {
|
|
54
|
+
if (member.payloadTypeText && member.kind === "Call") {
|
|
55
|
+
const payload = parseTypeNodeFromText(member.payloadTypeText);
|
|
56
|
+
const refs = typeRefs(payload);
|
|
57
|
+
if (refs.includes("Promise")) {
|
|
58
|
+
throw new errors_1.VerifyError(`Promise is not allowed inside ${member.kind}<T> payload for '${member.name}'.`, member.loc);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (member.payloadTypeText) {
|
|
62
|
+
const node = parseTypeNodeFromText(member.payloadTypeText);
|
|
63
|
+
const forbidden = checkForbiddenTypeNodes(node);
|
|
64
|
+
if (forbidden) {
|
|
65
|
+
throw new errors_1.VerifyError(`Forbidden type '${forbidden}' in payload of '${member.name}'.`, member.loc);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
for (const parameter of member.parameters) {
|
|
69
|
+
const node = parseTypeNodeFromText(parameter.typeText);
|
|
70
|
+
const forbidden = checkForbiddenTypeNodes(node);
|
|
71
|
+
if (forbidden) {
|
|
72
|
+
throw new errors_1.VerifyError(`Forbidden type '${forbidden}' in parameter '${parameter.name}' of '${member.name}'.`, member.loc);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function checkServiceDuplicates(spec) {
|
|
77
|
+
const globalMemberToService = new Map();
|
|
78
|
+
for (const service of spec.services) {
|
|
79
|
+
const byName = new Map();
|
|
80
|
+
for (const member of service.members) {
|
|
81
|
+
const list = byName.get(member.name) ?? [];
|
|
82
|
+
list.push(member);
|
|
83
|
+
byName.set(member.name, list);
|
|
84
|
+
const ownerService = globalMemberToService.get(member.name);
|
|
85
|
+
if (ownerService && ownerService !== service.name) {
|
|
86
|
+
throw new errors_1.VerifyError(`Duplicate member name '${member.name}' across services ('${ownerService}' and '${service.name}') is invalid.`, member.loc);
|
|
87
|
+
}
|
|
88
|
+
if (!ownerService) {
|
|
89
|
+
globalMemberToService.set(member.name, service.name);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
for (const [name, list] of byName) {
|
|
93
|
+
if (list.length > 1) {
|
|
94
|
+
throw new errors_1.VerifyError(`Duplicate method signature '${name}' is invalid.`, list[1].loc);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
function isBuiltinOrLiteral(ref) {
|
|
100
|
+
return [
|
|
101
|
+
"string",
|
|
102
|
+
"number",
|
|
103
|
+
"boolean",
|
|
104
|
+
"void",
|
|
105
|
+
"object",
|
|
106
|
+
"bigint",
|
|
107
|
+
"BigInt",
|
|
108
|
+
"Array",
|
|
109
|
+
"Record",
|
|
110
|
+
"Partial",
|
|
111
|
+
"Readonly",
|
|
112
|
+
"Date"
|
|
113
|
+
].includes(ref);
|
|
114
|
+
}
|
|
115
|
+
function resolveRefsOrThrow(spec, refs, ctxLoc, context) {
|
|
116
|
+
const local = new Set(spec.namespaceTypeDecls.map((d) => d.name));
|
|
117
|
+
const imported = spec.importedTypeDecls;
|
|
118
|
+
for (const ref of refs) {
|
|
119
|
+
if (isBuiltinOrLiteral(ref))
|
|
120
|
+
continue;
|
|
121
|
+
if (ref.startsWith("AnQst."))
|
|
122
|
+
continue;
|
|
123
|
+
if (local.has(ref))
|
|
124
|
+
continue;
|
|
125
|
+
if (imported.has(ref))
|
|
126
|
+
continue;
|
|
127
|
+
const namespacePrefix = ref.split(".")[0];
|
|
128
|
+
if (spec.importedTypeSymbols.has(namespacePrefix))
|
|
129
|
+
continue;
|
|
130
|
+
throw new errors_1.VerifyError(`Unresolved type reference '${ref}' in ${context}.`, ctxLoc);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
function collectReachableTypeNames(spec) {
|
|
134
|
+
const allDecls = new Map();
|
|
135
|
+
for (const d of spec.namespaceTypeDecls)
|
|
136
|
+
allDecls.set(d.name, d);
|
|
137
|
+
for (const [name, d] of spec.importedTypeDecls)
|
|
138
|
+
allDecls.set(name, d);
|
|
139
|
+
const queue = [];
|
|
140
|
+
const seen = new Set();
|
|
141
|
+
for (const d of spec.namespaceTypeDecls)
|
|
142
|
+
queue.push(d.name);
|
|
143
|
+
for (const service of spec.services) {
|
|
144
|
+
for (const member of service.members) {
|
|
145
|
+
const texts = [...member.parameters.map((p) => p.typeText)];
|
|
146
|
+
if (member.payloadTypeText)
|
|
147
|
+
texts.push(member.payloadTypeText);
|
|
148
|
+
for (const typeText of texts) {
|
|
149
|
+
const refs = typeRefs(parseTypeNodeFromText(typeText));
|
|
150
|
+
for (const ref of refs) {
|
|
151
|
+
if (!isBuiltinOrLiteral(ref) && !ref.startsWith("AnQst."))
|
|
152
|
+
queue.push(ref);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
while (queue.length > 0) {
|
|
158
|
+
const cur = queue.shift();
|
|
159
|
+
if (seen.has(cur))
|
|
160
|
+
continue;
|
|
161
|
+
seen.add(cur);
|
|
162
|
+
const decl = allDecls.get(cur);
|
|
163
|
+
if (!decl)
|
|
164
|
+
continue;
|
|
165
|
+
for (const ref of decl.referencedTypeNames) {
|
|
166
|
+
if (!isBuiltinOrLiteral(ref) && !ref.startsWith("AnQst.") && !seen.has(ref)) {
|
|
167
|
+
queue.push(ref);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return seen;
|
|
172
|
+
}
|
|
173
|
+
function verifySpec(spec) {
|
|
174
|
+
checkServiceDuplicates(spec);
|
|
175
|
+
for (const service of spec.services) {
|
|
176
|
+
for (const member of service.members) {
|
|
177
|
+
checkServiceMember(member);
|
|
178
|
+
const texts = [...member.parameters.map((p) => p.typeText)];
|
|
179
|
+
if (member.payloadTypeText)
|
|
180
|
+
texts.push(member.payloadTypeText);
|
|
181
|
+
for (const typeText of texts) {
|
|
182
|
+
const refs = typeRefs(parseTypeNodeFromText(typeText));
|
|
183
|
+
resolveRefsOrThrow(spec, refs, member.loc, `service member '${member.name}'`);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
for (const decl of spec.namespaceTypeDecls) {
|
|
188
|
+
resolveRefsOrThrow(spec, decl.referencedTypeNames, decl.loc, `type declaration '${decl.name}'`);
|
|
189
|
+
}
|
|
190
|
+
const reachable = collectReachableTypeNames(spec);
|
|
191
|
+
const stats = {
|
|
192
|
+
namespaceDeclaredTypes: spec.namespaceTypeDecls.length,
|
|
193
|
+
reachableGeneratedTypes: reachable.size,
|
|
194
|
+
serviceCount: spec.services.length
|
|
195
|
+
};
|
|
196
|
+
return {
|
|
197
|
+
stats,
|
|
198
|
+
message: `AnQst spec valid:\n ${stats.namespaceDeclaredTypes} types.\n ${stats.serviceCount} services.`
|
|
199
|
+
};
|
|
200
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@dusted/anqst",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Opinionated backend generator for webapps.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"nodejs",
|
|
7
|
+
"qt",
|
|
8
|
+
"angular",
|
|
9
|
+
"webapp"
|
|
10
|
+
],
|
|
11
|
+
"homepage": "https://github.com/DusteDdk/AnQst#readme",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/DusteDdk/AnQst/issues"
|
|
14
|
+
},
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "git+https://github.com/DusteDdk/AnQst.git",
|
|
18
|
+
"directory": "AnQstGen"
|
|
19
|
+
},
|
|
20
|
+
"license": "WTFPL",
|
|
21
|
+
"author": "DusteD",
|
|
22
|
+
"type": "commonjs",
|
|
23
|
+
"main": "dist/src/app.js",
|
|
24
|
+
"exports": {
|
|
25
|
+
".": "./dist/src/app.js"
|
|
26
|
+
},
|
|
27
|
+
"bin": {
|
|
28
|
+
"anqst": "dist/src/bin/anqst.js"
|
|
29
|
+
},
|
|
30
|
+
"files": [
|
|
31
|
+
"dist/src",
|
|
32
|
+
"spec/AnQst-Spec-DSL.d.ts",
|
|
33
|
+
"README.md",
|
|
34
|
+
"LICENSE"
|
|
35
|
+
],
|
|
36
|
+
"publishConfig": {
|
|
37
|
+
"access": "public"
|
|
38
|
+
},
|
|
39
|
+
"scripts": {
|
|
40
|
+
"clean": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\"",
|
|
41
|
+
"build": "npm run clean && tsc -p tsconfig.build.json",
|
|
42
|
+
"build:test": "npm run clean && tsc -p tsconfig.json",
|
|
43
|
+
"prepare": "npm run build",
|
|
44
|
+
"test": "npm run build:test && node --test dist/test/**/*.test.js",
|
|
45
|
+
"start": "node dist/src/bin/anqst.js"
|
|
46
|
+
},
|
|
47
|
+
"dependencies": {
|
|
48
|
+
"typescript": "^5.9.2"
|
|
49
|
+
},
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"@types/node": "^24.3.0"
|
|
52
|
+
}
|
|
53
|
+
}
|