@amerilux/netsuite-api 0.2.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 CHANGED
@@ -8,7 +8,7 @@ The API layer for a NetSuite single-page app. The app's server side is SuiteScri
8
8
  - **`netsuite-api generate`**: reads the controllers and writes the client's whole view of the backend, one module per controller and an index re-exporting them, plus the server-side map of scripts. The client never imports from the server tree.
9
9
  - **`@amerilux/netsuite-api`** (the root): the wire itself. The envelope, the endpoint types, `ScriptDeclaration`, `ScriptRef`.
10
10
 
11
- The layout it assumes is the one `create-netsuite-project` scaffolds: `api/` (SuiteScript) and `client/` (React) as workspaces, and `netsuite.ts` at the root holding the application's names.
11
+ The layout it assumes is the one `create-netsuite-project` scaffolds: `api/` (SuiteScript) and `client/` (React) as workspaces.
12
12
 
13
13
  ## A controller
14
14
 
@@ -102,16 +102,10 @@ export * as user from './user.gen';
102
102
 
103
103
  A hook imports `{ customer }` from it, calls `customer.api.search({ search: 'acme' })` and gets a `Promise<customer.CustomerSummary[]>`. The second argument carries an `AbortSignal`.
104
104
 
105
- **`client/src/app.gen.ts`**, a verbatim copy of the app file, outside the api folder so a page or a component may import `app` without touching a client.
106
-
107
105
  **`api/src/scripts.gen.ts`**, the server-side map of every declared script by controller name. A repository passes an entry to `createSuiteletClient`; nothing else needs it.
108
106
 
109
107
  `netsuite-api check` exits non-zero when any generated file is missing, out of date or left over, for CI. `netsuite-api generate --dry-run` prints every file instead of writing anything.
110
108
 
111
- ### The app file
112
-
113
- `netsuite.ts` at the project root holds the application's names and any id no controller or model owns. Both sides use it, so the client gets a verbatim copy: the file is exported constants and types only, with no imports and nothing that runs.
114
-
115
109
  ### Configuration
116
110
 
117
111
  `netsuite-api.config.json` at the project root, every setting optional. The defaults:
@@ -119,9 +113,7 @@ A hook imports `{ customer }` from it, calls `customer.api.search({ search: 'acm
119
113
  ```json
120
114
  {
121
115
  "controllers": "api/src/controllers",
122
- "appFile": "netsuite.ts",
123
116
  "outDir": "client/src/api",
124
- "appOutFile": "client/src/app.gen.ts",
125
117
  "scriptsOutFile": "api/src/scripts.gen.ts",
126
118
  "clientModule": "@amerilux/netsuite-api/client",
127
119
  "wireModule": "@amerilux/netsuite-api",
@@ -6,12 +6,8 @@ import type { FileSystemAdapter } from './file-system.js';
6
6
  export interface ClientGeneratorConfig {
7
7
  /** Directory holding the controllers: one `<name>Controller.ts` per script. */
8
8
  controllers: string;
9
- /** The file declaring `app` (and any other id no controller or model owns), copied into the client verbatim. */
10
- appFile: string;
11
9
  /** The client's generated directory: one `<name>.gen.ts` per controller (its wire shapes, its endpoint type, its client) and `index.gen.ts` re-exporting each under the controller's name. Nothing else lives there. */
12
10
  outDir: string;
13
- /** The generated copy of the app file for the client, outside the api folder so pages and components may import it. */
14
- appOutFile: string;
15
11
  /** The generated server-side `scripts` map: what a repository passes to createSuiteletClient. */
16
12
  scriptsOutFile: string;
17
13
  /** The specifier the controller modules import `createApiClient` from. */
@@ -2,9 +2,7 @@ import * as nodePath from 'node:path';
2
2
  export const DEFAULT_CONFIG_FILE_NAME = 'netsuite-api.config.json';
3
3
  export const defaultClientGeneratorConfig = {
4
4
  controllers: 'api/src/controllers',
5
- appFile: 'netsuite.ts',
6
5
  outDir: 'client/src/api',
7
- appOutFile: 'client/src/app.gen.ts',
8
6
  scriptsOutFile: 'api/src/scripts.gen.ts',
9
7
  clientModule: '@amerilux/netsuite-api/client',
10
8
  wireModule: '@amerilux/netsuite-api',
@@ -21,7 +19,7 @@ export class ClientGeneratorConfigError extends Error {
21
19
  this.name = 'ClientGeneratorConfigError';
22
20
  }
23
21
  }
24
- const stringSettings = ['controllers', 'appFile', 'outDir', 'appOutFile', 'scriptsOutFile', 'clientModule', 'wireModule'];
22
+ const stringSettings = ['controllers', 'outDir', 'scriptsOutFile', 'clientModule', 'wireModule'];
25
23
  const mapSettings = ['typeImports', 'inlineTypes'];
26
24
  function isStringMap(value) {
27
25
  return !!value && typeof value === 'object' && !Array.isArray(value) && Object.values(value).every((entry) => typeof entry === 'string' && entry !== '');
@@ -29,10 +29,6 @@ export interface EmitScriptsModuleOptions {
29
29
  wireModule: string;
30
30
  controllersLabel: string;
31
31
  }
32
- export interface EmitAppModuleOptions {
33
- /** How the header names the app file: `netsuite.ts`. */
34
- appLabel: string;
35
- }
36
32
  /** The index module of the client, next to the controller modules: what a hook imports. */
37
33
  export declare const CLIENT_INDEX_FILE_NAME = "index.gen.ts";
38
34
  /** The client module of a controller: `user.gen.ts` for the user controller. */
@@ -41,6 +37,4 @@ export declare function emitControllerModule({ contract, sourceLabel, inlinedTyp
41
37
  export declare function sortControllers(controllers: EmittedController[]): EmittedController[];
42
38
  /** The index module: every controller module re-exported under the controller's name. */
43
39
  export declare function emitClientIndexModule(controllers: EmittedController[], options: EmitClientIndexModuleOptions): string;
44
- /** The app file as the client sees it: its exported declarations, verbatim. */
45
- export declare function emitAppModule(appDeclarations: string[], options: EmitAppModuleOptions): string;
46
40
  export declare function emitScriptsModule(controllers: EmittedController[], options: EmitScriptsModuleOptions): string;
@@ -86,11 +86,6 @@ export function emitClientIndexModule(controllers, options) {
86
86
  }
87
87
  return `${lines.join('\n')}\n`;
88
88
  }
89
- /** The app file as the client sees it: its exported declarations, verbatim. */
90
- export function emitAppModule(appDeclarations, options) {
91
- const header = [`// Generated by netsuite-api generate from ${options.appLabel}. Do not edit: change ${options.appLabel} and run \`npm run generate\`.`, ESLINT_DISABLE];
92
- return `${[header.join('\n'), ...appDeclarations].join('\n\n')}\n`;
93
- }
94
89
  export function emitScriptsModule(controllers, options) {
95
90
  const ordered = sortControllers(controllers);
96
91
  const entries = ordered.map(({ contract }) => `${INDENT}${contract.name}: ${emitScriptRef(contract.script)},`);
@@ -18,7 +18,7 @@ export interface PlannedFile {
18
18
  content: string;
19
19
  }
20
20
  export interface ClientGenerationPlan {
21
- /** The modules to write: one per controller, the client index, the app module, the scripts map; empty when there are problems. */
21
+ /** The modules to write: one per controller, the client index, the scripts map; empty when there are problems. */
22
22
  files: PlannedFile[];
23
23
  /** Generated files in the client's output directory the plan does not write: a removed controller's module, or the copy of the entity types an earlier version made. Absolute paths. */
24
24
  leftoverFiles: string[];
@@ -1,7 +1,6 @@
1
1
  import * as nodePath from 'node:path';
2
- import { readAppDeclarations } from './appReader.js';
3
2
  import { isControllerFileName, readControllerContract } from './controllerReader.js';
4
- import { CLIENT_INDEX_FILE_NAME, controllerModuleFileName, emitAppModule, emitClientIndexModule, emitControllerModule, emitScriptsModule, sortControllers } from './emit.js';
3
+ import { CLIENT_INDEX_FILE_NAME, controllerModuleFileName, emitClientIndexModule, emitControllerModule, emitScriptsModule, sortControllers } from './emit.js';
5
4
  import { toPosixPath } from './file-system.js';
6
5
  import { readInlinableTypesFile, selectInlinedTypes } from './typesFileReader.js';
7
6
  const generatedFileNamePattern = /\.gen\.ts$/;
@@ -34,7 +33,6 @@ export function planClientGeneration({ config, fileSystem }) {
34
33
  const resolve = (relativePath) => nodePath.resolve(config.rootDirectory, relativePath);
35
34
  const label = (absolutePath) => relativeLabel(config.rootDirectory, absolutePath);
36
35
  const controllersDirectory = resolve(config.controllers);
37
- const appFile = resolve(config.appFile);
38
36
  const outDirectory = resolve(config.outDir);
39
37
  const problems = [];
40
38
  const controllerFiles = fileSystem.listFiles(controllersDirectory).filter((filePath) => isControllerFileName(nodePath.basename(filePath)));
@@ -87,15 +85,6 @@ export function planClientGeneration({ config, fileSystem }) {
87
85
  emitted.push({ contract, sourceLabel: controllerLabel, inlinedTypes });
88
86
  }
89
87
  problems.push(...findDuplicateScriptIds(emitted));
90
- let appDeclarations = [];
91
- if (fileSystem.fileExists(appFile)) {
92
- const app = readAppDeclarations(label(appFile), fileSystem.readTextFile(appFile));
93
- problems.push(...app.problems);
94
- appDeclarations = app.declarations;
95
- }
96
- else {
97
- problems.push({ filePath: label(appFile), message: 'the app file does not exist.' });
98
- }
99
88
  const controllers = emitted.map(({ contract }) => ({
100
89
  name: contract.name,
101
90
  filePath: contract.filePath,
@@ -112,7 +101,6 @@ export function planClientGeneration({ config, fileSystem }) {
112
101
  content: emitControllerModule(controller, { clientModule: config.clientModule }),
113
102
  })),
114
103
  { path: nodePath.join(outDirectory, CLIENT_INDEX_FILE_NAME), content: emitClientIndexModule(emitted, { controllersLabel }) },
115
- { path: resolve(config.appOutFile), content: emitAppModule(appDeclarations, { appLabel: label(appFile) }) },
116
104
  { path: resolve(config.scriptsOutFile), content: emitScriptsModule(emitted, { wireModule: config.wireModule, controllersLabel }) },
117
105
  ];
118
106
  const leftoverFiles = findLeftoverFiles(fileSystem, outDirectory, files.map((file) => file.path));
@@ -5,10 +5,8 @@ export { GENERATED_CLIENT_NAME, GENERATED_ENDPOINTS_TYPE_NAME, RAW_RESPONSE_TYPE
5
5
  export type { CarriedTypeImport, ControllerContract, ControllerKind, ControllerProblem, ControllerReadResult, ControllerTypeImport, DeclaredScript, EndpointSignature, InlinedTypeImport, ReadControllerOptions, TypeDeclaration, TypeImportName } from './controllerReader.js';
6
6
  export { readInlinableTypesFile, selectInlinedTypes } from './typesFileReader.js';
7
7
  export type { InlinableTypeDeclaration, InlinableTypesFile, SelectedInlinedTypes } from './typesFileReader.js';
8
- export { readAppDeclarations } from './appReader.js';
9
- export type { AppDeclarations } from './appReader.js';
10
- export { CLIENT_INDEX_FILE_NAME, controllerModuleFileName, emitAppModule, emitClientIndexModule, emitControllerModule, emitScriptsModule } from './emit.js';
11
- export type { EmitAppModuleOptions, EmitClientIndexModuleOptions, EmitControllerModuleOptions, EmitScriptsModuleOptions, EmittedController, InlinedTypeSection } from './emit.js';
8
+ export { CLIENT_INDEX_FILE_NAME, controllerModuleFileName, emitClientIndexModule, emitControllerModule, emitScriptsModule } from './emit.js';
9
+ export type { EmitClientIndexModuleOptions, EmitControllerModuleOptions, EmitScriptsModuleOptions, EmittedController, InlinedTypeSection } from './emit.js';
12
10
  export { checkClientGeneration, planClientGeneration, runClientGeneration } from './generate.js';
13
11
  export type { ClientGenerationCheck, ClientGenerationPlan, ClientGenerationResult, GenerateClientOptions, PlannedController, PlannedFile } from './generate.js';
14
12
  export { createInMemoryFileSystemAdapter, createNodeFileSystemAdapter, toPosixPath } from './file-system.js';
@@ -2,8 +2,7 @@
2
2
  export { DEFAULT_CONFIG_FILE_NAME, ClientGeneratorConfigError, defaultClientGeneratorConfig, loadClientGeneratorConfig } from './config.js';
3
3
  export { GENERATED_CLIENT_NAME, GENERATED_ENDPOINTS_TYPE_NAME, RAW_RESPONSE_TYPE_NAME, isControllerFileName, readControllerContract, readLeadingJsDoc } from './controllerReader.js';
4
4
  export { readInlinableTypesFile, selectInlinedTypes } from './typesFileReader.js';
5
- export { readAppDeclarations } from './appReader.js';
6
- export { CLIENT_INDEX_FILE_NAME, controllerModuleFileName, emitAppModule, emitClientIndexModule, emitControllerModule, emitScriptsModule } from './emit.js';
5
+ export { CLIENT_INDEX_FILE_NAME, controllerModuleFileName, emitClientIndexModule, emitControllerModule, emitScriptsModule } from './emit.js';
7
6
  export { checkClientGeneration, planClientGeneration, runClientGeneration } from './generate.js';
8
7
  export { createInMemoryFileSystemAdapter, createNodeFileSystemAdapter, toPosixPath } from './file-system.js';
9
8
  export { CLI_USAGE, EXIT_PROBLEMS, EXIT_SUCCESS, EXIT_USAGE, runCli } from './cli/main.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@amerilux/netsuite-api",
3
- "version": "0.2.0",
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
- }