@amerilux/netsuite-api 0.1.0 → 0.2.1
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 +42 -30
- package/dist/client/apiClient.d.ts +33 -6
- package/dist/client/apiClient.js +60 -30
- package/dist/client/index.d.ts +3 -3
- package/dist/client/index.js +2 -2
- package/dist-tooling/cli/main.js +9 -7
- package/dist-tooling/config.d.ts +14 -13
- package/dist-tooling/config.js +6 -8
- package/dist-tooling/controllerReader.d.ts +30 -9
- package/dist-tooling/controllerReader.js +29 -14
- package/dist-tooling/emit.d.ts +24 -13
- package/dist-tooling/emit.js +53 -28
- package/dist-tooling/file-system.d.ts +2 -0
- package/dist-tooling/file-system.js +4 -0
- package/dist-tooling/generate.d.ts +4 -1
- package/dist-tooling/generate.js +66 -50
- package/dist-tooling/index.d.ts +6 -6
- package/dist-tooling/index.js +3 -3
- package/dist-tooling/typesFileReader.d.ts +31 -0
- package/dist-tooling/typesFileReader.js +97 -0
- package/package.json +1 -1
- package/dist-tooling/appReader.d.ts +0 -12
- package/dist-tooling/appReader.js +0 -31
- package/dist-tooling/scriptsReader.d.ts +0 -17
- package/dist-tooling/scriptsReader.js +0 -57
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import ts from 'typescript';
|
|
2
|
+
import { readLeadingJsDoc } from './controllerReader.js';
|
|
3
|
+
function leftmostIdentifier(name) {
|
|
4
|
+
return ts.isIdentifier(name) ? name.text : leftmostIdentifier(name.left);
|
|
5
|
+
}
|
|
6
|
+
function collectTypeReferences(node, references) {
|
|
7
|
+
let referenced;
|
|
8
|
+
if (ts.isTypeReferenceNode(node))
|
|
9
|
+
referenced = leftmostIdentifier(node.typeName);
|
|
10
|
+
else if (ts.isTypeQueryNode(node))
|
|
11
|
+
referenced = leftmostIdentifier(node.exprName);
|
|
12
|
+
else if (ts.isExpressionWithTypeArguments(node) && ts.isIdentifier(node.expression))
|
|
13
|
+
referenced = node.expression.text;
|
|
14
|
+
if (referenced !== undefined && !references.includes(referenced))
|
|
15
|
+
references.push(referenced);
|
|
16
|
+
ts.forEachChild(node, (child) => collectTypeReferences(child, references));
|
|
17
|
+
}
|
|
18
|
+
function readImportedNames(statement, importedNames) {
|
|
19
|
+
const clause = statement.importClause;
|
|
20
|
+
if (!clause || !ts.isStringLiteral(statement.moduleSpecifier))
|
|
21
|
+
return;
|
|
22
|
+
const moduleSpecifier = statement.moduleSpecifier.text;
|
|
23
|
+
if (clause.name)
|
|
24
|
+
importedNames.set(clause.name.text, moduleSpecifier);
|
|
25
|
+
const bindings = clause.namedBindings;
|
|
26
|
+
if (!bindings)
|
|
27
|
+
return;
|
|
28
|
+
if (ts.isNamespaceImport(bindings))
|
|
29
|
+
importedNames.set(bindings.name.text, moduleSpecifier);
|
|
30
|
+
else
|
|
31
|
+
for (const element of bindings.elements)
|
|
32
|
+
importedNames.set(element.name.text, moduleSpecifier);
|
|
33
|
+
}
|
|
34
|
+
export function readInlinableTypesFile(filePath, source) {
|
|
35
|
+
const sourceFile = ts.createSourceFile(filePath, source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
|
|
36
|
+
const declarations = new Map();
|
|
37
|
+
const importedNames = new Map();
|
|
38
|
+
for (const statement of sourceFile.statements) {
|
|
39
|
+
if (ts.isImportDeclaration(statement)) {
|
|
40
|
+
readImportedNames(statement, importedNames);
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
if (!ts.isInterfaceDeclaration(statement) && !ts.isTypeAliasDeclaration(statement) && !ts.isEnumDeclaration(statement))
|
|
44
|
+
continue;
|
|
45
|
+
const references = [];
|
|
46
|
+
if (!ts.isEnumDeclaration(statement)) {
|
|
47
|
+
const own = new Set([statement.name.text, ...(statement.typeParameters ?? []).map((parameter) => parameter.name.text)]);
|
|
48
|
+
const found = [];
|
|
49
|
+
collectTypeReferences(statement, found);
|
|
50
|
+
references.push(...found.filter((name) => !own.has(name)));
|
|
51
|
+
}
|
|
52
|
+
const jsDoc = readLeadingJsDoc(statement, sourceFile);
|
|
53
|
+
declarations.set(statement.name.text, { name: statement.name.text, text: `${jsDoc ? `${jsDoc}\n` : ''}${statement.getText(sourceFile)}`, references });
|
|
54
|
+
}
|
|
55
|
+
return { filePath, declarations, importedNames };
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* The declarations a controller's imports pull out of the file: the named types and, transitively,
|
|
59
|
+
* what they refer to. A name the file does not declare is a problem reported against the controller.
|
|
60
|
+
*/
|
|
61
|
+
export function selectInlinedTypes(file, names, controllerPath) {
|
|
62
|
+
const problems = [];
|
|
63
|
+
const selected = new Set();
|
|
64
|
+
const pending = names.map((imported) => ({ name: imported.name }));
|
|
65
|
+
while (pending.length > 0) {
|
|
66
|
+
const { name, dependent } = pending.shift();
|
|
67
|
+
if (selected.has(name))
|
|
68
|
+
continue;
|
|
69
|
+
const declaration = file.declarations.get(name);
|
|
70
|
+
if (declaration) {
|
|
71
|
+
selected.add(name);
|
|
72
|
+
pending.push(...declaration.references.map((reference) => ({ name: reference, dependent: name })));
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
const importedFrom = file.importedNames.get(name);
|
|
76
|
+
if (importedFrom !== undefined) {
|
|
77
|
+
problems.push({
|
|
78
|
+
filePath: controllerPath,
|
|
79
|
+
message: dependent
|
|
80
|
+
? `type '${dependent}' (${file.filePath}) is built on ${name} from '${importedFrom}', which the client cannot carry; write the wire shape out in the controller instead.`
|
|
81
|
+
: `type '${name}' is imported into ${file.filePath} from '${importedFrom}', not declared there; the client cannot carry it.`,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
else if (dependent === undefined) {
|
|
85
|
+
problems.push({ filePath: controllerPath, message: `type '${name}' is not declared in ${file.filePath}.` });
|
|
86
|
+
}
|
|
87
|
+
// Anything else a declaration refers to is a global (Date, Record, Array): nothing to copy.
|
|
88
|
+
}
|
|
89
|
+
const declarations = Array.from(file.declarations.values())
|
|
90
|
+
.filter((declaration) => selected.has(declaration.name))
|
|
91
|
+
.map(({ name, text }) => ({ name, text }));
|
|
92
|
+
for (const imported of names) {
|
|
93
|
+
if (imported.alias && selected.has(imported.name))
|
|
94
|
+
declarations.push({ name: imported.alias, text: `export type ${imported.alias} = ${imported.name};` });
|
|
95
|
+
}
|
|
96
|
+
return { declarations, problems };
|
|
97
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@amerilux/netsuite-api",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "The API layer for a NetSuite single-page app: endpoints served by a Restlet or a Suitelet, a typed browser client, SuiteScript module stubs for tests, and a generator that writes the client module from the controllers.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import type { ControllerProblem } from './controllerReader.js';
|
|
2
|
-
/**
|
|
3
|
-
* Reads the project's app file (netsuite.ts): the application's names and the ids no controller or
|
|
4
|
-
* model owns. The client module carries it verbatim, so the file is plain declarations: exported
|
|
5
|
-
* constants and types, no imports, nothing that runs. The generator names anything else.
|
|
6
|
-
*/
|
|
7
|
-
export interface AppDeclarations {
|
|
8
|
-
/** Every exported declaration as written, JSDoc included, in file order. */
|
|
9
|
-
declarations: string[];
|
|
10
|
-
problems: ControllerProblem[];
|
|
11
|
-
}
|
|
12
|
-
export declare function readAppDeclarations(filePath: string, source: string): AppDeclarations;
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import ts from 'typescript';
|
|
2
|
-
import { readLeadingJsDoc } from './controllerReader.js';
|
|
3
|
-
function hasExportModifier(node) {
|
|
4
|
-
return ts.canHaveModifiers(node) && (ts.getModifiers(node) ?? []).some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword);
|
|
5
|
-
}
|
|
6
|
-
function describeStatement(statement, sourceFile) {
|
|
7
|
-
return statement.getText(sourceFile).split('\n')[0].trim();
|
|
8
|
-
}
|
|
9
|
-
export function readAppDeclarations(filePath, source) {
|
|
10
|
-
const sourceFile = ts.createSourceFile(filePath, source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
|
|
11
|
-
const declarations = [];
|
|
12
|
-
const problems = [];
|
|
13
|
-
for (const statement of sourceFile.statements) {
|
|
14
|
-
if (ts.isImportDeclaration(statement)) {
|
|
15
|
-
problems.push({ filePath, message: `imports nothing: it is copied into the client module verbatim (found ${describeStatement(statement, sourceFile)}).` });
|
|
16
|
-
continue;
|
|
17
|
-
}
|
|
18
|
-
const isDeclaration = ts.isVariableStatement(statement) || ts.isInterfaceDeclaration(statement) || ts.isTypeAliasDeclaration(statement) || ts.isEnumDeclaration(statement);
|
|
19
|
-
if (!isDeclaration || !hasExportModifier(statement)) {
|
|
20
|
-
problems.push({ filePath, message: `holds exported constants and types only, because the client module carries it verbatim (found ${describeStatement(statement, sourceFile)}).` });
|
|
21
|
-
continue;
|
|
22
|
-
}
|
|
23
|
-
if (ts.isVariableStatement(statement) && !(statement.declarationList.flags & ts.NodeFlags.Const)) {
|
|
24
|
-
problems.push({ filePath, message: `exports constants only (found ${describeStatement(statement, sourceFile)}).` });
|
|
25
|
-
continue;
|
|
26
|
-
}
|
|
27
|
-
const jsDoc = readLeadingJsDoc(statement, sourceFile);
|
|
28
|
-
declarations.push(`${jsDoc ? `${jsDoc}\n` : ''}${statement.getText(sourceFile)}`);
|
|
29
|
-
}
|
|
30
|
-
return { declarations, problems };
|
|
31
|
-
}
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import type { ControllerProblem } from './controllerReader.js';
|
|
2
|
-
/**
|
|
3
|
-
* Reads the `scripts` map of a project from its source: which scripts exist, and which the browser
|
|
4
|
-
* calls. The file cannot be executed here (a template renders placeholders into it), so this is a
|
|
5
|
-
* parse of `export const scripts = { name: { kind, scriptId, deployId, browser? }, ... }`.
|
|
6
|
-
*/
|
|
7
|
-
export interface ScriptRegistryEntry {
|
|
8
|
-
name: string;
|
|
9
|
-
kind?: string;
|
|
10
|
-
/** False only when the entry says `browser: false`. */
|
|
11
|
-
browser: boolean;
|
|
12
|
-
}
|
|
13
|
-
export interface ScriptRegistryReadResult {
|
|
14
|
-
entries: Map<string, ScriptRegistryEntry>;
|
|
15
|
-
problems: ControllerProblem[];
|
|
16
|
-
}
|
|
17
|
-
export declare function readScriptRegistry(filePath: string, source: string): ScriptRegistryReadResult;
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
import ts from 'typescript';
|
|
2
|
-
function unwrapExpression(expression) {
|
|
3
|
-
let current = expression;
|
|
4
|
-
while (ts.isSatisfiesExpression(current) || ts.isAsExpression(current) || ts.isParenthesizedExpression(current))
|
|
5
|
-
current = current.expression;
|
|
6
|
-
return current;
|
|
7
|
-
}
|
|
8
|
-
function findScriptsObject(sourceFile) {
|
|
9
|
-
for (const statement of sourceFile.statements) {
|
|
10
|
-
if (!ts.isVariableStatement(statement))
|
|
11
|
-
continue;
|
|
12
|
-
for (const declaration of statement.declarationList.declarations) {
|
|
13
|
-
if (!ts.isIdentifier(declaration.name) || declaration.name.text !== 'scripts' || !declaration.initializer)
|
|
14
|
-
continue;
|
|
15
|
-
const initializer = unwrapExpression(declaration.initializer);
|
|
16
|
-
if (ts.isObjectLiteralExpression(initializer))
|
|
17
|
-
return initializer;
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
return undefined;
|
|
21
|
-
}
|
|
22
|
-
function readEntry(property, sourceFile) {
|
|
23
|
-
if (!ts.isPropertyAssignment(property) || !(ts.isIdentifier(property.name) || ts.isStringLiteral(property.name)))
|
|
24
|
-
return undefined;
|
|
25
|
-
const value = unwrapExpression(property.initializer);
|
|
26
|
-
if (!ts.isObjectLiteralExpression(value))
|
|
27
|
-
return undefined;
|
|
28
|
-
const entry = { name: property.name.text, browser: true };
|
|
29
|
-
for (const member of value.properties) {
|
|
30
|
-
if (!ts.isPropertyAssignment(member) || !ts.isIdentifier(member.name))
|
|
31
|
-
continue;
|
|
32
|
-
const memberValue = unwrapExpression(member.initializer);
|
|
33
|
-
if (member.name.text === 'kind' && ts.isStringLiteral(memberValue))
|
|
34
|
-
entry.kind = memberValue.text;
|
|
35
|
-
if (member.name.text === 'browser')
|
|
36
|
-
entry.browser = memberValue.kind !== ts.SyntaxKind.FalseKeyword;
|
|
37
|
-
}
|
|
38
|
-
void sourceFile;
|
|
39
|
-
return entry;
|
|
40
|
-
}
|
|
41
|
-
export function readScriptRegistry(filePath, source) {
|
|
42
|
-
const sourceFile = ts.createSourceFile(filePath, source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
|
|
43
|
-
const entries = new Map();
|
|
44
|
-
const scriptsObject = findScriptsObject(sourceFile);
|
|
45
|
-
if (!scriptsObject) {
|
|
46
|
-
return { entries, problems: [{ filePath, message: "declares no 'export const scripts = { ... }'." }] };
|
|
47
|
-
}
|
|
48
|
-
const problems = [];
|
|
49
|
-
for (const property of scriptsObject.properties) {
|
|
50
|
-
const entry = readEntry(property, sourceFile);
|
|
51
|
-
if (entry)
|
|
52
|
-
entries.set(entry.name, entry);
|
|
53
|
-
else
|
|
54
|
-
problems.push({ filePath, message: `scripts has an entry that is not 'name: { kind, scriptId, deployId }' (${property.getText(sourceFile).split('\n')[0]}).` });
|
|
55
|
-
}
|
|
56
|
-
return { entries, problems };
|
|
57
|
-
}
|