@nestia/sdk
Version:
Nestia SDK and Swagger generator
286 lines • 11.8 kB
JavaScript
;
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.SdkWebSocketCloneProgrammer = void 0;
const fs_1 = __importDefault(require("fs"));
const path_1 = __importDefault(require("path"));
var SdkWebSocketCloneProgrammer;
(function (SdkWebSocketCloneProgrammer) {
SdkWebSocketCloneProgrammer.write = (app) => __awaiter(this, void 0, void 0, function* () {
var _a;
var _b;
const ctx = {
output: `${app.project.config.output}/structures`,
outputs: new Map(),
visited: new Map(),
};
const cloned = new Set();
for (const route of app.routes)
if (route.protocol === "websocket")
for (const imp of route.imports)
for (const local of imp.elements) {
const imported = (_b = (_a = imp.elementAliases) === null || _a === void 0 ? void 0 : _a[local]) !== null && _b !== void 0 ? _b : local;
if (yield clone(ctx)(imp.file, imported))
cloned.add(SdkWebSocketCloneProgrammer.importKey(imp.file, imported));
}
return cloned;
});
const clone = (ctx) => (file, name) => __awaiter(this, void 0, void 0, function* () {
const location = yield resolveSourceFile(file);
if (location === null || SdkWebSocketCloneProgrammer.isNodeModulesPath(location))
return false;
const key = `${location}#${name}`;
const status = ctx.visited.get(key);
if (status === "written" || status === "pending")
return true;
else if (status === "missing")
return false;
ctx.visited.set(key, "pending");
const text = yield fs_1.default.promises.readFile(location, "utf8");
const declarations = getDeclarations(text, name);
if (declarations.length === 0) {
ctx.visited.set(key, "missing");
return false;
}
const body = declarations.join("\n\n");
const imports = yield collectImports(ctx)({
body,
location,
source: text,
target: name,
});
const content = [...imports, body].filter((line) => line.length).join("\n\n") + "\n";
const oldbie = ctx.outputs.get(name);
if (oldbie !== undefined && oldbie !== content) {
ctx.visited.set(key, "missing");
return false;
}
ctx.outputs.set(name, content);
yield fs_1.default.promises.mkdir(ctx.output, { recursive: true });
yield fs_1.default.promises.writeFile(`${ctx.output}/${name}.ts`, content, "utf8");
ctx.visited.set(key, "written");
return true;
});
const collectImports = (ctx) => (props) => __awaiter(this, void 0, void 0, function* () {
const imports = [];
const add = (line) => {
if (imports.includes(line) === false)
imports.push(line);
};
for (const imp of getImports(props.source)) {
const relative = imp.specifier.startsWith(".");
for (const elem of imp.elements) {
if (uses(props.body, elem.local) === false)
continue;
if (relative === false) {
add(imp.text);
continue;
}
const sourceFile = path_1.default.resolve(path_1.default.dirname(props.location), imp.specifier);
if ((yield clone(ctx)(sourceFile, elem.imported)) === false) {
add(imp.text);
continue;
}
add(`import type { ${elem.imported}${elem.imported === elem.local ? "" : ` as ${elem.local}`} } from "./${elem.imported}";`);
}
for (const name of [imp.default, imp.namespace])
if (name !== null && uses(props.body, name))
add(imp.text);
}
for (const name of getExportedNames(props.source)) {
if (name === null || name === props.target)
continue;
if (uses(props.body, name) === false)
continue;
if ((yield clone(ctx)(props.location, name)) === true)
add(`import type { ${name} } from "./${name}";`);
}
return imports.sort();
});
const resolveSourceFile = (file) => __awaiter(this, void 0, void 0, function* () {
const base = trimRuntimeExtension(file);
const candidates = [
file,
base,
`${base}.ts`,
`${base}.tsx`,
`${base}.d.ts`,
path_1.default.join(base, "index.ts"),
path_1.default.join(base, "index.d.ts"),
];
for (const candidate of candidates) {
try {
const location = path_1.default.resolve(candidate);
const stat = yield fs_1.default.promises.stat(location);
if (stat.isFile())
return location;
}
catch (_a) { }
}
return null;
});
const trimRuntimeExtension = (file) => {
for (const ext of [".js", ".jsx", ".mjs", ".cjs"])
if (file.endsWith(ext))
return file.slice(0, -ext.length);
return file;
};
const getDeclarations = (source, name) => {
const output = [];
const searchable = maskTrivia(source);
const regex = declarationRegex(name);
for (const match of searchable.matchAll(regex)) {
if (isTopLevel(searchable, match.index) === false)
continue;
const end = declarationEnd(searchable, match.index, match[1]);
if (end !== -1)
output.push(source.slice(match.index, end).trim());
}
return output;
};
const getExportedNames = (source) => {
const searchable = maskTrivia(source);
return Array.from(searchable.matchAll(declarationRegex("[A-Za-z_$][\\w$]*")))
.filter((match) => isTopLevel(searchable, match.index))
.map((match) => match[2]);
};
const declarationRegex = (name) => new RegExp(`export\\s+(?:declare\\s+)?(interface|type|enum|namespace|class)\\s+(${name})\\b`, "g");
const declarationEnd = (source, start, kind) => {
let depth = 0;
let block = false;
for (let i = start; i < source.length; ++i) {
const ch = source[i];
if (ch === "{") {
++depth;
block = true;
}
else if (ch === "}") {
--depth;
if (kind !== "type" && block && depth === 0)
return i + 1;
}
else if (ch === ";" && block === false && depth === 0)
return i + 1;
else if (ch === ";" && kind === "type" && depth === 0)
return i + 1;
}
return -1;
};
const isTopLevel = (source, index) => {
let depth = 0;
for (let i = 0; i < index; ++i) {
const ch = source[i];
if (ch === "{")
++depth;
else if (ch === "}")
--depth;
}
return depth === 0;
};
const maskTrivia = (source) => {
const out = source.split("");
const mask = (index) => {
if (out[index] !== "\n" && out[index] !== "\r")
out[index] = " ";
};
let quote = null;
let escaped = false;
for (let i = 0; i < out.length; ++i) {
const ch = source[i];
const next = source[i + 1];
if (quote !== null) {
mask(i);
if (escaped)
escaped = false;
else if (ch === "\\")
escaped = true;
else if (ch === quote)
quote = null;
}
else if (ch === "/" && next === "/") {
mask(i++);
for (; i < out.length && source[i] !== "\n"; ++i)
mask(i);
--i;
}
else if (ch === "/" && next === "*") {
mask(i++);
mask(i);
for (++i; i < out.length; ++i) {
const current = source[i];
const following = source[i + 1];
mask(i);
if (current === "*" && following === "/") {
mask(++i);
break;
}
}
}
else if (ch === '"' || ch === "'" || ch === "`") {
quote = ch;
mask(i);
}
}
return out.join("");
};
const getImports = (source) => Array.from(source.matchAll(/import\s+([\s\S]*?)\s+from\s+["']([^"']+)["'];?/g)).map((match) => {
const clause = match[1].trim();
const specifier = match[2];
return {
text: match[0].trim(),
specifier,
default: defaultImportName(clause),
namespace: namespaceImportName(clause),
elements: namedImportElements(clause),
};
});
const defaultImportName = (clause) => {
const first = clause
.split(",")[0]
.trim()
.replace(/^type\s+/, "");
return first.length && first.startsWith("{") === false ? first : null;
};
const namespaceImportName = (clause) => {
var _a;
const match = clause.match(/\*\s+as\s+(\w+)/);
return (_a = match === null || match === void 0 ? void 0 : match[1]) !== null && _a !== void 0 ? _a : null;
};
const namedImportElements = (clause) => {
const match = clause.match(/\{([\s\S]*?)\}/);
if (match === null)
return [];
return match[1]
.split(",")
.map((part) => part.trim().replace(/^type\s+/, ""))
.filter((part) => part.length)
.map((part) => {
var _a;
const pieces = part.split(/\s+as\s+/);
const imported = pieces[0].trim();
return {
imported,
local: ((_a = pieces[1]) !== null && _a !== void 0 ? _a : imported).trim(),
};
});
};
const uses = (body, name) => new RegExp(`\\b${escapeRegExp(name)}\\b`).test(body);
const escapeRegExp = (str) => str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
SdkWebSocketCloneProgrammer.importKey = (file, name) => `${file}#${name}`;
SdkWebSocketCloneProgrammer.isNodeModulesPath = (file) => path_1.default
.resolve(file)
.split(/[\\/]+/)
.includes("node_modules");
})(SdkWebSocketCloneProgrammer || (exports.SdkWebSocketCloneProgrammer = SdkWebSocketCloneProgrammer = {}));
//# sourceMappingURL=SdkWebSocketCloneProgrammer.js.map