@amerilux/netsuite-api 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 +21 -0
- package/README.md +209 -0
- package/dist/client/apiClient.d.ts +57 -0
- package/dist/client/apiClient.js +96 -0
- package/dist/client/index.d.ts +8 -0
- package/dist/client/index.js +6 -0
- package/dist/index.d.ts +76 -0
- package/dist/index.js +9 -0
- package/dist/server/apiError.d.ts +9 -0
- package/dist/server/apiError.js +18 -0
- package/dist/server/defineRestlet.d.ts +15 -0
- package/dist/server/defineRestlet.js +4 -0
- package/dist/server/defineSuitelet.d.ts +16 -0
- package/dist/server/defineSuitelet.js +20 -0
- package/dist/server/endpoint.d.ts +59 -0
- package/dist/server/endpoint.js +95 -0
- package/dist/server/fileCabinet.d.ts +14 -0
- package/dist/server/fileCabinet.js +40 -0
- package/dist/server/index.d.ts +21 -0
- package/dist/server/index.js +14 -0
- package/dist/server/rawResponse.d.ts +39 -0
- package/dist/server/rawResponse.js +28 -0
- package/dist/server/suiteletClient.d.ts +23 -0
- package/dist/server/suiteletClient.js +54 -0
- package/dist/testing/N/error.d.ts +2 -0
- package/dist/testing/N/error.js +6 -0
- package/dist/testing/N/file.d.ts +9 -0
- package/dist/testing/N/file.js +6 -0
- package/dist/testing/N/format.d.ts +8 -0
- package/dist/testing/N/format.js +4 -0
- package/dist/testing/N/https.d.ts +15 -0
- package/dist/testing/N/https.js +10 -0
- package/dist/testing/N/log.d.ts +5 -0
- package/dist/testing/N/log.js +5 -0
- package/dist/testing/N/query.d.ts +7 -0
- package/dist/testing/N/query.js +7 -0
- package/dist/testing/N/record.d.ts +14 -0
- package/dist/testing/N/record.js +11 -0
- package/dist/testing/N/runtime.d.ts +11 -0
- package/dist/testing/N/runtime.js +8 -0
- package/dist/testing/N/search.d.ts +10 -0
- package/dist/testing/N/search.js +8 -0
- package/dist/testing/N/task.d.ts +7 -0
- package/dist/testing/N/task.js +4 -0
- package/dist/testing/N/ui/serverWidget.d.ts +7 -0
- package/dist/testing/N/ui/serverWidget.js +7 -0
- package/dist/testing/N/url.d.ts +9 -0
- package/dist/testing/N/url.js +6 -0
- package/dist/testing/index.d.ts +28 -0
- package/dist/testing/index.js +32 -0
- package/dist-tooling/appReader.d.ts +12 -0
- package/dist-tooling/appReader.js +31 -0
- package/dist-tooling/cli/arguments.d.ts +9 -0
- package/dist-tooling/cli/arguments.js +34 -0
- package/dist-tooling/cli/bin.d.ts +2 -0
- package/dist-tooling/cli/bin.js +7 -0
- package/dist-tooling/cli/main.d.ts +13 -0
- package/dist-tooling/cli/main.js +88 -0
- package/dist-tooling/config.d.ts +43 -0
- package/dist-tooling/config.js +77 -0
- package/dist-tooling/controllerReader.d.ts +68 -0
- package/dist-tooling/controllerReader.js +267 -0
- package/dist-tooling/emit.d.ts +29 -0
- package/dist-tooling/emit.js +79 -0
- package/dist-tooling/file-system.d.ts +15 -0
- package/dist-tooling/file-system.js +53 -0
- package/dist-tooling/generate.d.ts +36 -0
- package/dist-tooling/generate.js +128 -0
- package/dist-tooling/index.d.ts +15 -0
- package/dist-tooling/index.js +8 -0
- package/dist-tooling/scriptsReader.d.ts +17 -0
- package/dist-tooling/scriptsReader.js +57 -0
- package/package.json +77 -0
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import * as nodePath from 'node:path';
|
|
2
|
+
import ts from 'typescript';
|
|
3
|
+
import { toPosixPath } from './file-system.js';
|
|
4
|
+
/** The return type a handler writes, exactly, to answer with a document instead of the envelope. */
|
|
5
|
+
export const RAW_RESPONSE_TYPE_NAME = 'RawResponse';
|
|
6
|
+
const controllerFileNamePattern = /^([a-z][A-Za-z0-9]*)Controller\.ts$/;
|
|
7
|
+
const siblingControllerSpecifierPattern = /^\.\/[a-z][A-Za-z0-9]*Controller(?:\.js|\.ts)?$/;
|
|
8
|
+
const entryPointByFunction = {
|
|
9
|
+
defineRestlet: { kind: 'restlet', exportName: 'post', header: 'Restlet' },
|
|
10
|
+
defineSuitelet: { kind: 'suitelet', exportName: 'onRequest', header: 'Suitelet' },
|
|
11
|
+
};
|
|
12
|
+
export function isControllerFileName(fileName) {
|
|
13
|
+
return controllerFileNamePattern.test(fileName);
|
|
14
|
+
}
|
|
15
|
+
export function toPascalCase(name) {
|
|
16
|
+
return name.charAt(0).toUpperCase() + name.slice(1);
|
|
17
|
+
}
|
|
18
|
+
function hasExportModifier(node) {
|
|
19
|
+
return ts.canHaveModifiers(node) && (ts.getModifiers(node) ?? []).some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword);
|
|
20
|
+
}
|
|
21
|
+
/** The JSDoc block directly above a node (no blank line between them), or undefined. */
|
|
22
|
+
export function readLeadingJsDoc(node, sourceFile) {
|
|
23
|
+
const ranges = ts.getLeadingCommentRanges(sourceFile.text, node.getFullStart()) ?? [];
|
|
24
|
+
const jsDocRange = ranges.filter((range) => sourceFile.text.startsWith('/**', range.pos)).pop();
|
|
25
|
+
if (!jsDocRange)
|
|
26
|
+
return undefined;
|
|
27
|
+
const gap = sourceFile.text.slice(jsDocRange.end, node.getStart(sourceFile));
|
|
28
|
+
if (/\n[ \t]*\n/.test(gap))
|
|
29
|
+
return undefined;
|
|
30
|
+
return sourceFile.text.slice(jsDocRange.pos, jsDocRange.end);
|
|
31
|
+
}
|
|
32
|
+
/** The script type NetSuite reads from the leading JSDoc, or undefined. */
|
|
33
|
+
function readScriptTypeHeader(source) {
|
|
34
|
+
const header = source.match(/^\s*(\/\*[\s\S]*?\*\/)/)?.[1] ?? '';
|
|
35
|
+
return header.match(/@NScriptType\s+(\w+)/)?.[1];
|
|
36
|
+
}
|
|
37
|
+
function readTypeImport(statement, filePath, options) {
|
|
38
|
+
const problems = [];
|
|
39
|
+
const clause = statement.importClause;
|
|
40
|
+
if (!clause || !ts.isStringLiteral(statement.moduleSpecifier))
|
|
41
|
+
return { problems };
|
|
42
|
+
const moduleSpecifier = statement.moduleSpecifier.text;
|
|
43
|
+
const bindings = clause.namedBindings;
|
|
44
|
+
if (bindings && ts.isNamespaceImport(bindings)) {
|
|
45
|
+
if (clause.isTypeOnly) {
|
|
46
|
+
problems.push({ filePath, message: `'import type * as ${bindings.name.text}' from '${moduleSpecifier}' cannot be carried to the client; import the types by name.` });
|
|
47
|
+
}
|
|
48
|
+
return { problems };
|
|
49
|
+
}
|
|
50
|
+
const names = [];
|
|
51
|
+
if (bindings && ts.isNamedImports(bindings)) {
|
|
52
|
+
for (const element of bindings.elements) {
|
|
53
|
+
if (clause.isTypeOnly || element.isTypeOnly)
|
|
54
|
+
names.push(element.propertyName ? `${element.propertyName.text} as ${element.name.text}` : element.name.text);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (clause.name && clause.isTypeOnly) {
|
|
58
|
+
problems.push({ filePath, message: `'import type ${clause.name.text}' from '${moduleSpecifier}' cannot be carried to the client; import the types by name.` });
|
|
59
|
+
}
|
|
60
|
+
if (names.length === 0)
|
|
61
|
+
return { problems };
|
|
62
|
+
if (siblingControllerSpecifierPattern.test(moduleSpecifier))
|
|
63
|
+
return { problems };
|
|
64
|
+
const clientSpecifier = options.typeImports[moduleSpecifier];
|
|
65
|
+
if (clientSpecifier !== undefined)
|
|
66
|
+
return { typeImport: { moduleSpecifier: clientSpecifier, names }, problems };
|
|
67
|
+
const allowed = Object.keys(options.typeImports).map((specifier) => `'${specifier}'`).join(', ');
|
|
68
|
+
problems.push({ filePath, message: `type ${names.join(', ')} is imported from '${moduleSpecifier}'; a controller's wire shapes may only take types from ${allowed || 'the configured typeImports'} or from another controller.` });
|
|
69
|
+
return { problems };
|
|
70
|
+
}
|
|
71
|
+
function isTypeQueryAlias(statement) {
|
|
72
|
+
return ts.isTypeAliasDeclaration(statement) && ts.isTypeQueryNode(statement.type);
|
|
73
|
+
}
|
|
74
|
+
function readEndpoint(property, filePath, sourceFile) {
|
|
75
|
+
const problems = [];
|
|
76
|
+
const propertyName = property.name && (ts.isIdentifier(property.name) || ts.isStringLiteral(property.name)) ? property.name.text : undefined;
|
|
77
|
+
if (propertyName === undefined) {
|
|
78
|
+
problems.push({ filePath, message: 'an endpoint must be named by a plain identifier.' });
|
|
79
|
+
return { problems };
|
|
80
|
+
}
|
|
81
|
+
let handler;
|
|
82
|
+
if (ts.isMethodDeclaration(property))
|
|
83
|
+
handler = property;
|
|
84
|
+
else if (ts.isPropertyAssignment(property) && (ts.isArrowFunction(property.initializer) || ts.isFunctionExpression(property.initializer)))
|
|
85
|
+
handler = property.initializer;
|
|
86
|
+
if (!handler) {
|
|
87
|
+
problems.push({ filePath, message: `endpoint '${propertyName}' must be an inline function with its parameter and return type annotated; a reference to a function elsewhere carries no types the generator can read.` });
|
|
88
|
+
return { problems };
|
|
89
|
+
}
|
|
90
|
+
if (handler.parameters.length > 1) {
|
|
91
|
+
problems.push({ filePath, message: `endpoint '${propertyName}' takes ${handler.parameters.length} parameters; an endpoint takes one request or none.` });
|
|
92
|
+
return { problems };
|
|
93
|
+
}
|
|
94
|
+
if (!handler.type) {
|
|
95
|
+
problems.push({ filePath, message: `endpoint '${propertyName}' has no return type annotation; the response shape is read from it.` });
|
|
96
|
+
return { problems };
|
|
97
|
+
}
|
|
98
|
+
const parameter = handler.parameters[0];
|
|
99
|
+
if (parameter && !parameter.type) {
|
|
100
|
+
problems.push({ filePath, message: `endpoint '${propertyName}' has no parameter type annotation; the request shape is read from it.` });
|
|
101
|
+
return { problems };
|
|
102
|
+
}
|
|
103
|
+
const responseType = handler.type.getText(sourceFile);
|
|
104
|
+
return {
|
|
105
|
+
endpoint: {
|
|
106
|
+
name: propertyName,
|
|
107
|
+
requestType: parameter?.type?.getText(sourceFile),
|
|
108
|
+
requestOptional: parameter?.questionToken !== undefined || parameter?.initializer !== undefined,
|
|
109
|
+
responseType,
|
|
110
|
+
raw: responseType === RAW_RESPONSE_TYPE_NAME,
|
|
111
|
+
jsDoc: readLeadingJsDoc(property, sourceFile),
|
|
112
|
+
},
|
|
113
|
+
problems,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
function findCall(statement, calleeNames) {
|
|
117
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
118
|
+
if (!ts.isIdentifier(declaration.name))
|
|
119
|
+
continue;
|
|
120
|
+
const initializer = declaration.initializer;
|
|
121
|
+
if (initializer && ts.isCallExpression(initializer) && ts.isIdentifier(initializer.expression) && calleeNames.includes(initializer.expression.text)) {
|
|
122
|
+
return { declarationName: declaration.name.text, call: initializer };
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return undefined;
|
|
126
|
+
}
|
|
127
|
+
function readStringProperty(literal, propertyName) {
|
|
128
|
+
for (const property of literal.properties) {
|
|
129
|
+
if (ts.isPropertyAssignment(property) && ts.isIdentifier(property.name) && property.name.text === propertyName && ts.isStringLiteral(property.initializer))
|
|
130
|
+
return property.initializer.text;
|
|
131
|
+
}
|
|
132
|
+
return undefined;
|
|
133
|
+
}
|
|
134
|
+
function readBooleanProperty(literal, propertyName) {
|
|
135
|
+
for (const property of literal.properties) {
|
|
136
|
+
if (!ts.isPropertyAssignment(property) || !ts.isIdentifier(property.name) || property.name.text !== propertyName)
|
|
137
|
+
continue;
|
|
138
|
+
if (property.initializer.kind === ts.SyntaxKind.TrueKeyword)
|
|
139
|
+
return true;
|
|
140
|
+
if (property.initializer.kind === ts.SyntaxKind.FalseKeyword)
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
return undefined;
|
|
144
|
+
}
|
|
145
|
+
function readScriptDeclaration(found, statement, controllerName, endpointsConstantName, source, filePath) {
|
|
146
|
+
const problems = [];
|
|
147
|
+
const calleeName = found.call.expression.text;
|
|
148
|
+
const entryPoint = entryPointByFunction[calleeName];
|
|
149
|
+
const callLabel = `${calleeName}({ name, scriptId, deployId }, ${endpointsConstantName})`;
|
|
150
|
+
if (!hasExportModifier(statement) || found.declarationName !== entryPoint.exportName) {
|
|
151
|
+
problems.push({ filePath, message: `the entry point must be 'export const ${entryPoint.exportName} = ${callLabel}'; NetSuite looks for that export on a ${entryPoint.header}.` });
|
|
152
|
+
}
|
|
153
|
+
const header = readScriptTypeHeader(source);
|
|
154
|
+
if (header !== entryPoint.header) {
|
|
155
|
+
problems.push({ filePath, message: `the leading JSDoc says '@NScriptType ${header ?? '(none)'}' but the entry point is ${calleeName}; both must say ${entryPoint.header}.` });
|
|
156
|
+
}
|
|
157
|
+
const [declaration, endpointsArgument] = found.call.arguments;
|
|
158
|
+
if (!declaration || !ts.isObjectLiteralExpression(declaration)) {
|
|
159
|
+
problems.push({ filePath, message: `${calleeName} takes the script declaration as an object literal written inline: ${callLabel}.` });
|
|
160
|
+
return { problems };
|
|
161
|
+
}
|
|
162
|
+
if (!endpointsArgument || !ts.isIdentifier(endpointsArgument) || endpointsArgument.text !== endpointsConstantName) {
|
|
163
|
+
problems.push({ filePath, message: `${calleeName} must be passed ${endpointsConstantName}: ${callLabel}.` });
|
|
164
|
+
}
|
|
165
|
+
const name = readStringProperty(declaration, 'name');
|
|
166
|
+
const scriptId = readStringProperty(declaration, 'scriptId');
|
|
167
|
+
const deployId = readStringProperty(declaration, 'deployId');
|
|
168
|
+
for (const [property, value] of [['name', name], ['scriptId', scriptId], ['deployId', deployId]]) {
|
|
169
|
+
if (value === undefined)
|
|
170
|
+
problems.push({ filePath, message: `the script declaration needs '${property}' as a string literal; the generator reads it from the source.` });
|
|
171
|
+
}
|
|
172
|
+
if (name !== undefined && name !== controllerName) {
|
|
173
|
+
problems.push({ filePath, message: `the script declaration says name: '${name}' but the file is ${controllerName}Controller.ts; the name is the file name without Controller.` });
|
|
174
|
+
}
|
|
175
|
+
const browser = readBooleanProperty(declaration, 'browser');
|
|
176
|
+
if (browser === undefined && declaration.properties.some((property) => ts.isPropertyAssignment(property) && ts.isIdentifier(property.name) && property.name.text === 'browser')) {
|
|
177
|
+
problems.push({ filePath, message: "'browser' must be the literal true or false." });
|
|
178
|
+
}
|
|
179
|
+
if (problems.length > 0 || scriptId === undefined || deployId === undefined)
|
|
180
|
+
return { problems };
|
|
181
|
+
return { script: { kind: entryPoint.kind, scriptId, deployId, browser: browser ?? true }, problems };
|
|
182
|
+
}
|
|
183
|
+
export function readControllerContract(filePath, source, options) {
|
|
184
|
+
const problems = [];
|
|
185
|
+
const fileName = nodePath.basename(toPosixPath(filePath));
|
|
186
|
+
const nameMatch = controllerFileNamePattern.exec(fileName);
|
|
187
|
+
if (!nameMatch) {
|
|
188
|
+
return { problems: [{ filePath, message: 'a controller file is named <name>Controller.ts, with <name> in camelCase.' }] };
|
|
189
|
+
}
|
|
190
|
+
const name = nameMatch[1];
|
|
191
|
+
const endpointsConstantName = `${name}Endpoints`;
|
|
192
|
+
const sourceFile = ts.createSourceFile(filePath, source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
|
|
193
|
+
const typeImports = [];
|
|
194
|
+
const typeDeclarations = [];
|
|
195
|
+
const endpoints = [];
|
|
196
|
+
let endpointsFound = false;
|
|
197
|
+
let script;
|
|
198
|
+
let entryPointFound = false;
|
|
199
|
+
for (const statement of sourceFile.statements) {
|
|
200
|
+
if (ts.isImportDeclaration(statement)) {
|
|
201
|
+
const result = readTypeImport(statement, filePath, options);
|
|
202
|
+
problems.push(...result.problems);
|
|
203
|
+
if (result.typeImport)
|
|
204
|
+
typeImports.push(result.typeImport);
|
|
205
|
+
}
|
|
206
|
+
else if (ts.isInterfaceDeclaration(statement) || ts.isTypeAliasDeclaration(statement) || ts.isEnumDeclaration(statement)) {
|
|
207
|
+
if (isTypeQueryAlias(statement)) {
|
|
208
|
+
const queried = statement.type;
|
|
209
|
+
if (ts.isIdentifier(queried.exprName) && queried.exprName.text === endpointsConstantName)
|
|
210
|
+
continue;
|
|
211
|
+
problems.push({ filePath, message: `type '${statement.name.text}' is a typeof; a wire shape is written out as an interface or a type alias.` });
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
if (!hasExportModifier(statement)) {
|
|
215
|
+
problems.push({ filePath, message: `'${statement.name.text}' is not exported; every type in a controller is a wire shape, so export it (or move it below the controller).` });
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
const jsDoc = readLeadingJsDoc(statement, sourceFile);
|
|
219
|
+
typeDeclarations.push({ name: statement.name.text, text: `${jsDoc ? `${jsDoc}\n` : ''}${statement.getText(sourceFile)}` });
|
|
220
|
+
}
|
|
221
|
+
else if (ts.isVariableStatement(statement)) {
|
|
222
|
+
const endpointsCall = findCall(statement, ['defineEndpoints']);
|
|
223
|
+
if (endpointsCall) {
|
|
224
|
+
if (endpointsCall.declarationName !== endpointsConstantName) {
|
|
225
|
+
problems.push({ filePath, message: `the endpoints are declared as 'export const ${endpointsConstantName} = defineEndpoints({ ... })', not '${endpointsCall.declarationName}'.` });
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
endpointsFound = true;
|
|
229
|
+
const argument = endpointsCall.call.arguments[0];
|
|
230
|
+
if (!argument || !ts.isObjectLiteralExpression(argument)) {
|
|
231
|
+
problems.push({ filePath, message: `${endpointsConstantName} must be defineEndpoints({ ... }) with the endpoints written inline.` });
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
for (const property of argument.properties) {
|
|
235
|
+
const result = readEndpoint(property, filePath, sourceFile);
|
|
236
|
+
problems.push(...result.problems);
|
|
237
|
+
if (result.endpoint)
|
|
238
|
+
endpoints.push(result.endpoint);
|
|
239
|
+
}
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
const entryPointCall = findCall(statement, Object.keys(entryPointByFunction));
|
|
243
|
+
if (entryPointCall) {
|
|
244
|
+
if (entryPointFound) {
|
|
245
|
+
problems.push({ filePath, message: 'a controller has one entry point; a second defineRestlet or defineSuitelet call is a second script.' });
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
entryPointFound = true;
|
|
249
|
+
const result = readScriptDeclaration(entryPointCall, statement, name, endpointsConstantName, source, filePath);
|
|
250
|
+
problems.push(...result.problems);
|
|
251
|
+
script = result.script;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
if (!endpointsFound) {
|
|
256
|
+
problems.push({ filePath, message: `must declare 'export const ${endpointsConstantName} = defineEndpoints({ ... })'.` });
|
|
257
|
+
}
|
|
258
|
+
if (!entryPointFound) {
|
|
259
|
+
problems.push({ filePath, message: `must end with 'export const post = defineRestlet({ name, scriptId, deployId }, ${endpointsConstantName})' or 'export const onRequest = defineSuitelet(...)'.` });
|
|
260
|
+
}
|
|
261
|
+
if (problems.length > 0 || !script)
|
|
262
|
+
return { problems };
|
|
263
|
+
return {
|
|
264
|
+
contract: { name, filePath, endpointsTypeName: `${toPascalCase(name)}Endpoints`, clientName: `${name}Api`, script, typeImports, typeDeclarations, endpoints },
|
|
265
|
+
problems,
|
|
266
|
+
};
|
|
267
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { ControllerContract } from './controllerReader.js';
|
|
2
|
+
/**
|
|
3
|
+
* Writes the generated modules. The client module holds the app declarations, every controller's
|
|
4
|
+
* wire shapes and endpoint type, and a typed client for each controller the browser calls: one file,
|
|
5
|
+
* so a DTO that references another controller's type resolves in place and the client never imports
|
|
6
|
+
* from the api. The scripts module is the server-side map a repository passes to createSuiteletClient.
|
|
7
|
+
*/
|
|
8
|
+
export interface EmittedController {
|
|
9
|
+
contract: ControllerContract;
|
|
10
|
+
/** Where the file's header points a reader: the controller's path relative to the project. */
|
|
11
|
+
sourceLabel: string;
|
|
12
|
+
}
|
|
13
|
+
export interface EmitClientModuleOptions {
|
|
14
|
+
clientModule: string;
|
|
15
|
+
/** How the header names the sources: `api/src/controllers`. */
|
|
16
|
+
controllersLabel: string;
|
|
17
|
+
}
|
|
18
|
+
export interface EmitScriptsModuleOptions {
|
|
19
|
+
wireModule: string;
|
|
20
|
+
controllersLabel: string;
|
|
21
|
+
}
|
|
22
|
+
export interface EmitAppModuleOptions {
|
|
23
|
+
/** How the header names the app file: `netsuite.ts`. */
|
|
24
|
+
appLabel: string;
|
|
25
|
+
}
|
|
26
|
+
export declare function emitClientModule(controllers: EmittedController[], options: EmitClientModuleOptions): string;
|
|
27
|
+
/** The app file as the client sees it: its exported declarations, verbatim. */
|
|
28
|
+
export declare function emitAppModule(appDeclarations: string[], options: EmitAppModuleOptions): string;
|
|
29
|
+
export declare function emitScriptsModule(controllers: EmittedController[], options: EmitScriptsModuleOptions): string;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
const INDENT = ' ';
|
|
2
|
+
const EDIT_NOTICE = 'Do not edit: change the controller and run `npm run generate`.';
|
|
3
|
+
function indentJsDoc(jsDoc) {
|
|
4
|
+
return jsDoc
|
|
5
|
+
.split('\n')
|
|
6
|
+
.map((line, index) => (index === 0 ? `${INDENT}${line.trim()}` : `${INDENT} ${line.trim()}`))
|
|
7
|
+
.join('\n');
|
|
8
|
+
}
|
|
9
|
+
function emitEndpointMember(endpoint) {
|
|
10
|
+
const parameter = endpoint.requestType === undefined ? '' : `request${endpoint.requestOptional ? '?' : ''}: ${endpoint.requestType}`;
|
|
11
|
+
const member = `${INDENT}${endpoint.name}: (${parameter}) => ${endpoint.responseType};`;
|
|
12
|
+
return endpoint.jsDoc ? `${indentJsDoc(endpoint.jsDoc)}\n${member}` : member;
|
|
13
|
+
}
|
|
14
|
+
function emitTypeImports(controllers) {
|
|
15
|
+
const namesByModule = new Map();
|
|
16
|
+
for (const { contract } of controllers) {
|
|
17
|
+
for (const typeImport of contract.typeImports) {
|
|
18
|
+
const names = namesByModule.get(typeImport.moduleSpecifier) ?? new Set();
|
|
19
|
+
typeImport.names.forEach((name) => names.add(name));
|
|
20
|
+
namesByModule.set(typeImport.moduleSpecifier, names);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return Array.from(namesByModule.keys())
|
|
24
|
+
.sort()
|
|
25
|
+
.map((moduleSpecifier) => `import type { ${Array.from(namesByModule.get(moduleSpecifier) ?? []).sort().join(', ')} } from '${moduleSpecifier}';`);
|
|
26
|
+
}
|
|
27
|
+
/** The script reference as a literal, as the client module and the scripts module write it. */
|
|
28
|
+
function emitScriptRef(script) {
|
|
29
|
+
const browser = script.browser ? '' : ', browser: false';
|
|
30
|
+
return `{ kind: '${script.kind}', scriptId: '${script.scriptId}', deployId: '${script.deployId}'${browser} }`;
|
|
31
|
+
}
|
|
32
|
+
function emitController({ contract, sourceLabel }) {
|
|
33
|
+
const browser = contract.script.browser;
|
|
34
|
+
const sections = [`// ${contract.name} (${sourceLabel})${browser ? '' : ': called by server code only, so types only'}`];
|
|
35
|
+
for (const declaration of contract.typeDeclarations)
|
|
36
|
+
sections.push(declaration.text);
|
|
37
|
+
const members = contract.endpoints.map(emitEndpointMember);
|
|
38
|
+
// A type alias, not an interface: only an object type literal satisfies the Endpoints index signature.
|
|
39
|
+
sections.push(`export type ${contract.endpointsTypeName} = {\n${members.join('\n')}\n};`);
|
|
40
|
+
if (browser) {
|
|
41
|
+
const rawEndpoints = contract.endpoints.filter((endpoint) => endpoint.raw).map((endpoint) => `'${endpoint.name}'`);
|
|
42
|
+
const clientOptions = rawEndpoints.length > 0 ? `, { rawEndpoints: [${rawEndpoints.join(', ')}] }` : '';
|
|
43
|
+
sections.push(`/** One typed function per endpoint of the ${contract.name} controller: \`${contract.clientName}.${contract.endpoints[0]?.name ?? 'endpoint'}(...)\`. */\n` +
|
|
44
|
+
`export const ${contract.clientName} = createApiClient<${contract.endpointsTypeName}>(${emitScriptRef({ ...contract.script, browser: true })}${clientOptions});`);
|
|
45
|
+
}
|
|
46
|
+
return sections.join('\n\n');
|
|
47
|
+
}
|
|
48
|
+
function sortControllers(controllers) {
|
|
49
|
+
return [...controllers].sort((left, right) => left.contract.name.localeCompare(right.contract.name));
|
|
50
|
+
}
|
|
51
|
+
export function emitClientModule(controllers, options) {
|
|
52
|
+
const ordered = sortControllers(controllers);
|
|
53
|
+
const anyBrowserFacing = ordered.some((controller) => controller.contract.script.browser);
|
|
54
|
+
const header = [`// Generated by netsuite-api generate from ${options.controllersLabel}. ${EDIT_NOTICE}`, '/* eslint-disable */'];
|
|
55
|
+
const imports = [...(anyBrowserFacing ? [`import { createApiClient } from '${options.clientModule}';`] : []), ...emitTypeImports(ordered)];
|
|
56
|
+
const body = ordered.map(emitController);
|
|
57
|
+
return `${[header.join('\n'), imports.join('\n'), ...body].filter((section) => section !== '').join('\n\n')}\n`;
|
|
58
|
+
}
|
|
59
|
+
/** The app file as the client sees it: its exported declarations, verbatim. */
|
|
60
|
+
export function emitAppModule(appDeclarations, options) {
|
|
61
|
+
const header = [`// Generated by netsuite-api generate from ${options.appLabel}. Do not edit: change ${options.appLabel} and run \`npm run generate\`.`, '/* eslint-disable */'];
|
|
62
|
+
return `${[header.join('\n'), ...appDeclarations].join('\n\n')}\n`;
|
|
63
|
+
}
|
|
64
|
+
export function emitScriptsModule(controllers, options) {
|
|
65
|
+
const ordered = sortControllers(controllers);
|
|
66
|
+
const entries = ordered.map(({ contract }) => `${INDENT}${contract.name}: ${emitScriptRef(contract.script)},`);
|
|
67
|
+
return [
|
|
68
|
+
`// Generated by netsuite-api generate from ${options.controllersLabel}. ${EDIT_NOTICE}`,
|
|
69
|
+
'/* eslint-disable */',
|
|
70
|
+
'',
|
|
71
|
+
`import type { ScriptRef } from '${options.wireModule}';`,
|
|
72
|
+
'',
|
|
73
|
+
'/** Every script the controllers declare, by controller name: what a repository passes to createSuiteletClient. */',
|
|
74
|
+
'export const scripts = {',
|
|
75
|
+
...entries,
|
|
76
|
+
'} as const satisfies Record<string, ScriptRef>;',
|
|
77
|
+
'',
|
|
78
|
+
].join('\n');
|
|
79
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** The file system surface the generator uses, so tests run fully in memory. */
|
|
2
|
+
export interface FileSystemAdapter {
|
|
3
|
+
readTextFile(filePath: string): string;
|
|
4
|
+
writeTextFile(filePath: string, content: string): void;
|
|
5
|
+
fileExists(filePath: string): boolean;
|
|
6
|
+
ensureDirectory(directoryPath: string): void;
|
|
7
|
+
/** Lists the absolute paths of the files directly inside a directory. Returns [] when it does not exist. */
|
|
8
|
+
listFiles(directoryPath: string): string[];
|
|
9
|
+
}
|
|
10
|
+
export declare function toPosixPath(filePath: string): string;
|
|
11
|
+
export declare function createNodeFileSystemAdapter(): FileSystemAdapter;
|
|
12
|
+
export interface InMemoryFileSystemAdapter extends FileSystemAdapter {
|
|
13
|
+
readonly files: Map<string, string>;
|
|
14
|
+
}
|
|
15
|
+
export declare function createInMemoryFileSystemAdapter(initialFiles?: Record<string, string>): InMemoryFileSystemAdapter;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import * as nodeFileSystem from 'node:fs';
|
|
2
|
+
import * as nodePath from 'node:path';
|
|
3
|
+
export function toPosixPath(filePath) {
|
|
4
|
+
return filePath.replace(/\\/g, '/');
|
|
5
|
+
}
|
|
6
|
+
export function createNodeFileSystemAdapter() {
|
|
7
|
+
return {
|
|
8
|
+
readTextFile: (filePath) => nodeFileSystem.readFileSync(filePath, 'utf8'),
|
|
9
|
+
writeTextFile: (filePath, content) => nodeFileSystem.writeFileSync(filePath, content, 'utf8'),
|
|
10
|
+
fileExists: (filePath) => nodeFileSystem.existsSync(filePath),
|
|
11
|
+
ensureDirectory: (directoryPath) => nodeFileSystem.mkdirSync(directoryPath, { recursive: true }),
|
|
12
|
+
listFiles: (directoryPath) => {
|
|
13
|
+
if (!nodeFileSystem.existsSync(directoryPath) || !nodeFileSystem.statSync(directoryPath).isDirectory())
|
|
14
|
+
return [];
|
|
15
|
+
return nodeFileSystem
|
|
16
|
+
.readdirSync(directoryPath, { withFileTypes: true })
|
|
17
|
+
.filter((entry) => entry.isFile())
|
|
18
|
+
.map((entry) => nodePath.join(directoryPath, entry.name))
|
|
19
|
+
.sort();
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
function normalizeKey(filePath) {
|
|
24
|
+
return toPosixPath(nodePath.resolve(filePath));
|
|
25
|
+
}
|
|
26
|
+
export function createInMemoryFileSystemAdapter(initialFiles = {}) {
|
|
27
|
+
const files = new Map();
|
|
28
|
+
const directories = new Set();
|
|
29
|
+
for (const [filePath, content] of Object.entries(initialFiles))
|
|
30
|
+
files.set(normalizeKey(filePath), content);
|
|
31
|
+
return {
|
|
32
|
+
files,
|
|
33
|
+
readTextFile: (filePath) => {
|
|
34
|
+
const content = files.get(normalizeKey(filePath));
|
|
35
|
+
if (content === undefined)
|
|
36
|
+
throw new Error(`File not found: ${filePath}`);
|
|
37
|
+
return content;
|
|
38
|
+
},
|
|
39
|
+
writeTextFile: (filePath, content) => {
|
|
40
|
+
files.set(normalizeKey(filePath), content);
|
|
41
|
+
},
|
|
42
|
+
fileExists: (filePath) => files.has(normalizeKey(filePath)) || directories.has(normalizeKey(filePath)),
|
|
43
|
+
ensureDirectory: (directoryPath) => {
|
|
44
|
+
directories.add(normalizeKey(directoryPath));
|
|
45
|
+
},
|
|
46
|
+
listFiles: (directoryPath) => {
|
|
47
|
+
const prefix = `${normalizeKey(directoryPath)}/`;
|
|
48
|
+
return Array.from(files.keys())
|
|
49
|
+
.filter((filePath) => filePath.startsWith(prefix) && !filePath.slice(prefix.length).includes('/'))
|
|
50
|
+
.sort();
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { ResolvedClientGeneratorConfig } from './config.js';
|
|
2
|
+
import type { ControllerProblem } from './controllerReader.js';
|
|
3
|
+
import type { FileSystemAdapter } from './file-system.js';
|
|
4
|
+
export interface GenerateClientOptions {
|
|
5
|
+
config: ResolvedClientGeneratorConfig;
|
|
6
|
+
fileSystem: FileSystemAdapter;
|
|
7
|
+
}
|
|
8
|
+
export interface PlannedController {
|
|
9
|
+
name: string;
|
|
10
|
+
filePath: string;
|
|
11
|
+
kind: 'restlet' | 'suitelet';
|
|
12
|
+
browser: boolean;
|
|
13
|
+
endpointCount: number;
|
|
14
|
+
}
|
|
15
|
+
export interface PlannedFile {
|
|
16
|
+
/** Absolute path. */
|
|
17
|
+
path: string;
|
|
18
|
+
content: string;
|
|
19
|
+
}
|
|
20
|
+
export interface ClientGenerationPlan {
|
|
21
|
+
/** The modules to write and the files to copy; empty when there are problems. */
|
|
22
|
+
files: PlannedFile[];
|
|
23
|
+
controllers: PlannedController[];
|
|
24
|
+
problems: ControllerProblem[];
|
|
25
|
+
}
|
|
26
|
+
export interface ClientGenerationResult extends ClientGenerationPlan {
|
|
27
|
+
writtenFiles: string[];
|
|
28
|
+
unchangedFiles: string[];
|
|
29
|
+
}
|
|
30
|
+
export interface ClientGenerationCheck extends ClientGenerationPlan {
|
|
31
|
+
missingFiles: string[];
|
|
32
|
+
staleFiles: string[];
|
|
33
|
+
}
|
|
34
|
+
export declare function planClientGeneration({ config, fileSystem }: GenerateClientOptions): ClientGenerationPlan;
|
|
35
|
+
export declare function runClientGeneration(options: GenerateClientOptions): ClientGenerationResult;
|
|
36
|
+
export declare function checkClientGeneration(options: GenerateClientOptions): ClientGenerationCheck;
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import * as nodePath from 'node:path';
|
|
2
|
+
import { readAppDeclarations } from './appReader.js';
|
|
3
|
+
import { isControllerFileName, readControllerContract } from './controllerReader.js';
|
|
4
|
+
import { emitAppModule, emitClientModule, emitScriptsModule } from './emit.js';
|
|
5
|
+
import { toPosixPath } from './file-system.js';
|
|
6
|
+
function relativeLabel(rootDirectory, filePath) {
|
|
7
|
+
return toPosixPath(nodePath.relative(rootDirectory, filePath));
|
|
8
|
+
}
|
|
9
|
+
function findDuplicateNames(controllers) {
|
|
10
|
+
const owners = new Map();
|
|
11
|
+
const problems = [];
|
|
12
|
+
const claim = (name, filePath, what) => {
|
|
13
|
+
const owner = owners.get(name);
|
|
14
|
+
if (owner !== undefined && owner !== filePath) {
|
|
15
|
+
problems.push({ filePath, message: `${what} '${name}' is also declared by ${owner}; the generated module holds every controller, so names are unique across them.` });
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
owners.set(name, filePath);
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
for (const { contract } of controllers) {
|
|
22
|
+
for (const declaration of contract.typeDeclarations)
|
|
23
|
+
claim(declaration.name, contract.filePath, 'type');
|
|
24
|
+
claim(contract.endpointsTypeName, contract.filePath, 'type');
|
|
25
|
+
claim(contract.clientName, contract.filePath, 'client');
|
|
26
|
+
}
|
|
27
|
+
return problems;
|
|
28
|
+
}
|
|
29
|
+
function findDuplicateScriptIds(controllers) {
|
|
30
|
+
const problems = [];
|
|
31
|
+
for (const property of ['scriptId', 'deployId']) {
|
|
32
|
+
const owners = new Map();
|
|
33
|
+
for (const { contract } of controllers) {
|
|
34
|
+
const id = contract.script[property];
|
|
35
|
+
const owner = owners.get(id);
|
|
36
|
+
if (owner !== undefined)
|
|
37
|
+
problems.push({ filePath: contract.filePath, message: `${property} '${id}' is also declared by ${owner}; every controller is its own script.` });
|
|
38
|
+
else
|
|
39
|
+
owners.set(id, contract.filePath);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return problems;
|
|
43
|
+
}
|
|
44
|
+
export function planClientGeneration({ config, fileSystem }) {
|
|
45
|
+
const resolve = (relativePath) => nodePath.resolve(config.rootDirectory, relativePath);
|
|
46
|
+
const label = (absolutePath) => relativeLabel(config.rootDirectory, absolutePath);
|
|
47
|
+
const controllersDirectory = resolve(config.controllers);
|
|
48
|
+
const appFile = resolve(config.appFile);
|
|
49
|
+
const problems = [];
|
|
50
|
+
const controllerFiles = fileSystem.listFiles(controllersDirectory).filter((filePath) => isControllerFileName(nodePath.basename(filePath)));
|
|
51
|
+
if (controllerFiles.length === 0) {
|
|
52
|
+
problems.push({ filePath: label(controllersDirectory), message: 'holds no <name>Controller.ts file.' });
|
|
53
|
+
}
|
|
54
|
+
const emitted = [];
|
|
55
|
+
for (const filePath of controllerFiles) {
|
|
56
|
+
const result = readControllerContract(label(filePath), fileSystem.readTextFile(filePath), { typeImports: config.typeImports });
|
|
57
|
+
problems.push(...result.problems);
|
|
58
|
+
if (result.contract)
|
|
59
|
+
emitted.push({ contract: result.contract, sourceLabel: label(filePath) });
|
|
60
|
+
}
|
|
61
|
+
problems.push(...findDuplicateNames(emitted), ...findDuplicateScriptIds(emitted));
|
|
62
|
+
let appDeclarations = [];
|
|
63
|
+
if (fileSystem.fileExists(appFile)) {
|
|
64
|
+
const app = readAppDeclarations(label(appFile), fileSystem.readTextFile(appFile));
|
|
65
|
+
problems.push(...app.problems);
|
|
66
|
+
appDeclarations = app.declarations;
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
problems.push({ filePath: label(appFile), message: 'the app file does not exist.' });
|
|
70
|
+
}
|
|
71
|
+
const copies = [];
|
|
72
|
+
for (const [source, destination] of Object.entries(config.copyFiles)) {
|
|
73
|
+
const sourcePath = resolve(source);
|
|
74
|
+
if (!fileSystem.fileExists(sourcePath)) {
|
|
75
|
+
problems.push({ filePath: label(sourcePath), message: 'the file to copy into the client does not exist; run the model generator first.' });
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
copies.push({ path: resolve(destination), content: fileSystem.readTextFile(sourcePath) });
|
|
79
|
+
}
|
|
80
|
+
const controllers = emitted.map(({ contract }) => ({
|
|
81
|
+
name: contract.name,
|
|
82
|
+
filePath: contract.filePath,
|
|
83
|
+
kind: contract.script.kind,
|
|
84
|
+
browser: contract.script.browser,
|
|
85
|
+
endpointCount: contract.endpoints.length,
|
|
86
|
+
}));
|
|
87
|
+
if (problems.length > 0)
|
|
88
|
+
return { files: [], controllers, problems };
|
|
89
|
+
const controllersLabel = label(controllersDirectory);
|
|
90
|
+
const files = [
|
|
91
|
+
{ path: resolve(config.outFile), content: emitClientModule(emitted, { clientModule: config.clientModule, controllersLabel }) },
|
|
92
|
+
{ path: resolve(config.appOutFile), content: emitAppModule(appDeclarations, { appLabel: label(appFile) }) },
|
|
93
|
+
{ path: resolve(config.scriptsOutFile), content: emitScriptsModule(emitted, { wireModule: config.wireModule, controllersLabel }) },
|
|
94
|
+
...copies,
|
|
95
|
+
];
|
|
96
|
+
return { files, controllers, problems };
|
|
97
|
+
}
|
|
98
|
+
export function runClientGeneration(options) {
|
|
99
|
+
const plan = planClientGeneration(options);
|
|
100
|
+
const writtenFiles = [];
|
|
101
|
+
const unchangedFiles = [];
|
|
102
|
+
if (plan.problems.length > 0)
|
|
103
|
+
return { ...plan, writtenFiles, unchangedFiles };
|
|
104
|
+
const { fileSystem } = options;
|
|
105
|
+
for (const file of plan.files) {
|
|
106
|
+
if (fileSystem.fileExists(file.path) && fileSystem.readTextFile(file.path) === file.content) {
|
|
107
|
+
unchangedFiles.push(file.path);
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
fileSystem.ensureDirectory(nodePath.dirname(file.path));
|
|
111
|
+
fileSystem.writeTextFile(file.path, file.content);
|
|
112
|
+
writtenFiles.push(file.path);
|
|
113
|
+
}
|
|
114
|
+
return { ...plan, writtenFiles, unchangedFiles };
|
|
115
|
+
}
|
|
116
|
+
export function checkClientGeneration(options) {
|
|
117
|
+
const plan = planClientGeneration(options);
|
|
118
|
+
const { fileSystem } = options;
|
|
119
|
+
const missingFiles = [];
|
|
120
|
+
const staleFiles = [];
|
|
121
|
+
for (const file of plan.files) {
|
|
122
|
+
if (!fileSystem.fileExists(file.path))
|
|
123
|
+
missingFiles.push(file.path);
|
|
124
|
+
else if (fileSystem.readTextFile(file.path) !== file.content)
|
|
125
|
+
staleFiles.push(file.path);
|
|
126
|
+
}
|
|
127
|
+
return { ...plan, missingFiles, staleFiles };
|
|
128
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** The generator as a library: what the `netsuite-api` command runs, for a build tool or a test to call directly. */
|
|
2
|
+
export { DEFAULT_CONFIG_FILE_NAME, ClientGeneratorConfigError, defaultClientGeneratorConfig, loadClientGeneratorConfig } from './config.js';
|
|
3
|
+
export type { ClientGeneratorConfig, ResolvedClientGeneratorConfig } from './config.js';
|
|
4
|
+
export { RAW_RESPONSE_TYPE_NAME, isControllerFileName, readControllerContract, readLeadingJsDoc, toPascalCase } from './controllerReader.js';
|
|
5
|
+
export type { CarriedTypeImport, ControllerContract, ControllerKind, ControllerProblem, ControllerReadResult, DeclaredScript, EndpointSignature, ReadControllerOptions, TypeDeclaration } from './controllerReader.js';
|
|
6
|
+
export { readAppDeclarations } from './appReader.js';
|
|
7
|
+
export type { AppDeclarations } from './appReader.js';
|
|
8
|
+
export { emitAppModule, emitClientModule, emitScriptsModule } from './emit.js';
|
|
9
|
+
export type { EmitAppModuleOptions, EmitClientModuleOptions, EmitScriptsModuleOptions, EmittedController } from './emit.js';
|
|
10
|
+
export { checkClientGeneration, planClientGeneration, runClientGeneration } from './generate.js';
|
|
11
|
+
export type { ClientGenerationCheck, ClientGenerationPlan, ClientGenerationResult, GenerateClientOptions, PlannedController, PlannedFile } from './generate.js';
|
|
12
|
+
export { createInMemoryFileSystemAdapter, createNodeFileSystemAdapter, toPosixPath } from './file-system.js';
|
|
13
|
+
export type { FileSystemAdapter, InMemoryFileSystemAdapter } from './file-system.js';
|
|
14
|
+
export { CLI_USAGE, EXIT_PROBLEMS, EXIT_SUCCESS, EXIT_USAGE, runCli } from './cli/main.js';
|
|
15
|
+
export type { CliEnvironment } from './cli/main.js';
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** The generator as a library: what the `netsuite-api` command runs, for a build tool or a test to call directly. */
|
|
2
|
+
export { DEFAULT_CONFIG_FILE_NAME, ClientGeneratorConfigError, defaultClientGeneratorConfig, loadClientGeneratorConfig } from './config.js';
|
|
3
|
+
export { RAW_RESPONSE_TYPE_NAME, isControllerFileName, readControllerContract, readLeadingJsDoc, toPascalCase } from './controllerReader.js';
|
|
4
|
+
export { readAppDeclarations } from './appReader.js';
|
|
5
|
+
export { emitAppModule, emitClientModule, emitScriptsModule } from './emit.js';
|
|
6
|
+
export { checkClientGeneration, planClientGeneration, runClientGeneration } from './generate.js';
|
|
7
|
+
export { createInMemoryFileSystemAdapter, createNodeFileSystemAdapter, toPosixPath } from './file-system.js';
|
|
8
|
+
export { CLI_USAGE, EXIT_PROBLEMS, EXIT_SUCCESS, EXIT_USAGE, runCli } from './cli/main.js';
|
|
@@ -0,0 +1,17 @@
|
|
|
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;
|