Scaffolding and boilerplate for using TypeScript v7+ in SuiteCloud Account Customization Projects (ACP).
Additionally, this project demonstrates Option 3 of the folder structure alternatives proposed in oracle/netsuite-suitecloud-sdk#976.
/
├ .github/workflows # github actions
├ __tests__/ # jest tests
├ lib/ # entry points for Zod and other 3rd-party libs for bundling
├ src/ # folder used as `defaultProjectFolder` in suitecloud.config.js
│ ├ FileCabinet/ # standard folder expected by the SuiteCloud CLI
│ │ └ ... # folders expected by the SuiteCloud CLI except SuiteScripts (i.e. Templates)
│ ├ Objects/ # SuiteCloud XML objects
│ ├ SuiteScripts/ # TS and JS source files, not deployed to NetSuite
│ ├ deploy.xml
│ └ manifest.xml
└ ... # project, build, bundle and SuiteCloud configuration files
TypeScript source files sit inside defaultProjectFolder (i.e. src/) but outside FileCabinet/.
The TypeScript compiler outputs compiled JS directly into src/FileCabinet/SuiteScripts/, which is
what gets deployed to the File Cabinet, and is ignored from Git.
- TypeScript and JavaScript files are compiled into the native
FileCabinetfolder expected by the CLI - TypeScript and JavaScript source co-exist in the same directory
- Native support for
object:importof XML object files - TypeScript files aren't deployed to the File Cabinet
- Compiled JavaScript files are ignored from Git
- Developer is warned whenever a JavaScript file may be imported/created in the ignored folder
- Supports import of JavaScript files from TypeScript, allowing incremental adoption of TypeScript into existing JavaScript projects
- Includes NetSuite types via 3rd-party
@hitc/netsuite-typespackage - TypeScript v7 for better performance and long-term support
- Support bundling third-party NPM libraries into SuiteScript-compatible AMD modules
- ESLint with TypeScript support and
requirejsrules for plain JavaScript files - Prettier formatting
- Includes GitHub Action for Pull Request validation
- Pre-commit hooks for linting, format and conventional commit message
- NVM support via
.nvmrcfile - Nix flake configuration with
direnvsupport for dev shell
All of these are enabled by default, but can be left unused if desired. See Disabling Quality-of-life Features.
After the initial Setup, run deployments and other SuiteCloud CLI commands as usual. The build runs automatically before each command.
suitecloud project:deploy| Script | Description |
|---|---|
npm run build |
Compile TypeScript and copy static files to FileCabinet |
npm run bundle:lib |
Bundle third-party libraries into src/SuiteScripts/lib/ |
npm run clean |
Remove compiled output from FileCabinet/SuiteScripts |
npm run lint / lint:fix |
Lint the project or auto-fix linting issues |
npm run format / format:check |
Format or check format with Prettier |
npm test |
Run unit tests with Jest |
-
(Optional) If using
nixanddirenv, make sure flakes are enabled, and run:direnv allow
-
If not using
nix, make sure that the following are installed globally:- Node.js v22
- Oracle JDK or OpenJDK v21
@oracle/suitecloud-cliNPM package
-
Install dependencies using
npm:npm install
Every quality-of-life feature is enabled by default, but none of them are required. They stay out
of the build: build doesn't invoke any of them, and the SuiteCloud CLI hooks only call
clean, build and test. Leaving one unused costs nothing, and builds and deployments keep
working either way.
If any feature is not needed, it can be disabled or ignored by following the instructions described below for each.
Don't run npm run lint. The only other things that invoke it are the pre-commit hook and the
GitHub Action, both covered below.
To silence it in an editor that lints automatically, add paths to the ignores array in
eslint.config.mjs, or disable the ESLint extension for this workspace.
Don't run npm run format. Formatting is enforced by npm run lint, the pre-commit hook and the
GitHub Action, since ESLint reports Prettier violations as lint errors through
eslint-plugin-prettier.
To exempt specific files, add them to .prettierignore.
The workflow only triggers on pull_request, so it never runs locally.
To drop an individual check, remove its step from .github/workflows/validate.yaml. The
Check Format, Lint and Test steps can each go on their own; the rest set up the job.
If the repository will be hosted in GitHub, but no GitHub Actions are needed at all,
delete .github/workflows/validate.yaml. If hosted elsewhere, then the GitHub Actions will be
ignored.
The pre-commit hook runs lint-staged and the commit-msg hook runs commitlint. They are
independent, so either can be disabled on its own.
Disable a hook by deleting its file:
rm .husky/pre-commit # lint and format staged files
rm .husky/commit-msg # conventional commit message validationDeleting both is the recommended way to turn off git hooks entirely. Husky can stay installed with
no hook files present: its wrapper exits early when a hook has no matching file, so commits run
without it, and npm install leaves the deletions alone. Restoring a hook later is a matter of
writing the file back.
.nvmrc is only read when you run nvm use, so ignoring it means not running that command. Any
Node.js v22 install works, whether from nvm, nix, Homebrew or a system package.
The GitHub Action reads the same file via node-version-file, which is what keeps CI aligned
with local development.
Don't run direnv allow, and the flake is never evaluated. If the shell is already active, run
direnv deny to unload it.
In that case, install the appropriate Node.js and OpenJDK versions by whatever means you prefer, as described in Setup. The flake files can stay in the repository unused.
suitecloud.config.js hooks into several SuiteCloud CLI commands via beforeExecuting to automate
the build and keep the developer experience consistent:
| Command | Hook behavior |
|---|---|
project:deploy |
Runs build and tests |
project:validate |
Runs build |
project:package |
Runs build |
file:upload |
Runs build |
file:create |
Prints a note to move generated JS files |
file:import |
Prints a note to move generated JS files |
object:import |
Prints a note to move generated JS files |
object:update |
Prints a note to move generated JS files |
Commands that write files into FileCabinet (file:create, file:import, object:import, object:update)
print a reminder to move any downloaded JS files into src/SuiteScripts/ so they are managed by the
build pipeline rather overritten by the next build.
SuiteScript files must be delivered as AMD modules, but TypeScript 7 dropped the
module: "amd" compiler option.
The pipeline works around this by having tsc emit ESNext modules into an intermediate
build/ directory, then passing that output through Rollup to produce the AMD bundles
that NetSuite expects.
The npm run build command runs build:ts and build:js concurrently. build:ts
chains two sequential steps; build:js runs independently in parallel:
flowchart TD
START["src/SuiteScripts/**/*.{js,ts}\n(TS and AMD)"]
TS["src/SuiteScripts/**/*.ts\n(TS)"]
JS["src/SuiteScripts/**/*.js\n(AMD)"]
BUILD["build/**/*.js\n(ESNext)"]
FC["src/FileCabinet/SuiteScripts/**/*.js\n(AMD)"]
START -->|"build:ts"| TS
START -->|"build:js"| JS
TS -->|"build:ts:compile\ntsc"| BUILD
BUILD -->|"build:ts:bundle\nrollup → AMD"| FC
JS -->|"build:js\ncopyfiles"| FC
TypeScript 7 compiles src/SuiteScripts/**/*.ts into build/ using tsconfig.build.json.
The output format is ESNext with ES modules (module: "esnext"), producing clean
intermediate JS before any bundling. NetSuite's N/* module paths are left as bare imports at this stage.
Note: TypeScript 7 is installed as
typescript7(aliased fromnpm:typescript@^7) to avoid conflicting with thetypescriptpackage, which remains at v6 so thattypescript-eslint(which does not yet support TypeScript 7) continues to work.
Runs after build:ts:compile. Rollup picks up every file in build/ and outputs
AMD modules into src/FileCabinet/SuiteScripts/, preserving the original module
structure. Several inline plugins handle NetSuite-specific concerns:
- Mark all
N/*imports as external so Rollup does not attempt to bundle them. - Rewrite
import * as x from 'N/...'toimport x from 'N/...'so Rollup can emit clean AMD dependencies without interop boilerplate. - Mark relative imports that resolve to plain JS AMD files (not compiled by tsc) as external so they are not inlined.Tthose files are handled by the static copy step instead.
- Moves
@NApiVersion/@NScriptTypeJSDoc comments back to the top of each file, because Rollup's AMD wrapper would place them insidedefine().
Runs concurrently with build:ts. Plain JavaScript files under src/SuiteScripts/
(existing AMD scripts not managed by tsc) are copied directly into src/FileCabinet/SuiteScripts/
with copyfiles.
Third-party npm packages cannot be loaded directly in SuiteScript, since it expects AMD modules served from the File Cabinet. The library bundler pre-bundles selected packages into self-contained AMD files that can be uploaded and imported like any other SuiteScript file.
Each library gets a small entrypoint in lib/ that re-exports the public API, for example:
// lib/zod.ts
export { z as default } from 'zod';Running npm run bundle:lib processes every entrypoint in lib/ through Rollup
(rollup.config.lib.mjs) and writes two output files per library into src/SuiteScripts/lib/:
<package>.js— the full library bundled as an AMD module, ready for the File Cabinet<package>.d.ts— bundled type declarations for use during TypeScript development
Both output files should be committed to the repository. They are consumed directly by the TypeScript build pipeline, and no build step is required for day-to-day development after the initial bundle.
-
Install the package as a dev dependency:
npm install --save-dev <package>
-
Create an entrypoint in
lib/that exports the API your scripts will use, for example:// lib/<package>.ts export { something as default } from '<package>';
-
Add two entries to the
export defaultarray inrollup.config.lib.mjs: one for the JS bundle and one for the type declarations:// JS bundle { input: `${dirs.entrypoints}/<package>.ts`, output: { file: `${dirs.output}/<package>.js`, format: 'amd' }, plugins: [resolve()], }, // Type declarations { input: `${dirs.entrypoints}/<package>.ts`, output: { file: `${dirs.output}/<package>.d.ts`, format: 'es' }, plugins: [resolve(), dts({ respectExternal: true })], },
-
Run the bundler and commit the output:
npm run bundle:lib git add src/SuiteScripts/lib/<package>.js src/SuiteScripts/lib/<package>.d.ts
-
In your SuiteScript files, import the bundled library using its relative path:
import name from './lib/<package>';