├── .prettierrc ├── icon.png ├── .gitignore ├── .vscodeignore ├── src ├── sdf-config.ts ├── environment.ts ├── sdf-cli-json.ts ├── cli-command.ts ├── test │ ├── extension.test.ts │ └── index.ts ├── provider-commands.ts ├── sdf-provider.ts ├── extension.ts ├── custom-object.ts └── netsuite-sdf.ts ├── CHANGELOG.md ├── tsconfig.json ├── .vscode ├── settings.json ├── tasks.json └── launch.json ├── .github └── workflows │ └── compile.yml ├── resources ├── light │ ├── document.svg │ ├── folder.svg │ ├── boolean.svg │ ├── dependency.svg │ ├── refresh.svg │ ├── number.svg │ └── string.svg └── dark │ ├── document.svg │ ├── folder.svg │ ├── boolean.svg │ ├── dependency.svg │ ├── refresh.svg │ ├── number.svg │ └── string.svg ├── LICENSE ├── webpack.config.js ├── README.md └── package.json /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "printWidth": 120 4 | } 5 | -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/christopherwxyz/NetSuiteSDF/HEAD/icon.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | out 2 | node_modules 3 | .vscode-test/ 4 | *.vsix 5 | *.log 6 | package-lock.json 7 | .DS_Store 8 | yarn.lock 9 | dist/ 10 | -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | .vscode-test/** 3 | out/test/** 4 | out/**/*.map 5 | src/** 6 | .gitignore 7 | tsconfig.json 8 | vsc-extension-quickstart.md 9 | -------------------------------------------------------------------------------- /src/sdf-config.ts: -------------------------------------------------------------------------------- 1 | import { Environment } from './environment'; 2 | 3 | export interface SDFConfig { 4 | projectName: string; 5 | environments: Environment[]; 6 | } 7 | -------------------------------------------------------------------------------- /src/environment.ts: -------------------------------------------------------------------------------- 1 | export interface Environment { 2 | name: string; 3 | account: string; 4 | url: string; 5 | role: number; 6 | email: string; 7 | authid: string; 8 | } 9 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | All notable changes to the "netsuitesdf" extension will be documented in this file. 3 | 4 | Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how to structure this file. 5 | 6 | ## [Unreleased] 7 | - Initial release -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es6", 5 | "outDir": "out", 6 | "lib": ["es6", "es2016.array.include", "es2017.object"], 7 | "sourceMap": true, 8 | "rootDir": "src" 9 | }, 10 | "exclude": ["node_modules", ".vscode-test"] 11 | } 12 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "files.exclude": { 4 | "out": false // set this to true to hide the "out" folder with the compiled JS files 5 | }, 6 | "search.exclude": { 7 | "out": true // set this to false to include "out" folder in search results 8 | } 9 | } -------------------------------------------------------------------------------- /.github/workflows/compile.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - master 5 | name: Deploy Extension 6 | jobs: 7 | deploy: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v2 11 | - run: npm install 12 | - uses: lannonbr/vsce-action@master 13 | with: 14 | args: "publish -p $VSCE_TOKEN" 15 | env: 16 | VSCE_TOKEN: ${{ secrets.VSCE_TOKEN }} 17 | -------------------------------------------------------------------------------- /src/sdf-cli-json.ts: -------------------------------------------------------------------------------- 1 | export const SdfCliJson = `{ 2 | "projectName": "ProjectName", 3 | "environments": [ 4 | { 5 | "name": "Sandbox", 6 | "account": "00000000", 7 | "url": "system.sandbox.netsuite.com", 8 | "role": 3, 9 | "email": "email@address.com" 10 | }, 11 | { 12 | "name": "Production", 13 | "account": "00000000", 14 | "url": "system.netsuite.com", 15 | "role": 3, 16 | "email": "email@address.com" 17 | } 18 | ] 19 | }`; 20 | -------------------------------------------------------------------------------- /resources/light/document.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // See https://go.microsoft.com/fwlink/?LinkId=733558 2 | // for the documentation about the tasks.json format 3 | { 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "type": "npm", 8 | "script": "watch", 9 | "problemMatcher": "$tsc-watch", 10 | "isBackground": true, 11 | "presentation": { 12 | "reveal": "never" 13 | }, 14 | "group": { 15 | "kind": "build", 16 | "isDefault": true 17 | } 18 | } 19 | ] 20 | } -------------------------------------------------------------------------------- /resources/dark/document.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /resources/light/folder.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /resources/dark/folder.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/cli-command.ts: -------------------------------------------------------------------------------- 1 | export enum CLICommand { 2 | AddDependencies = 'adddependencies', 3 | CreateProject = 'createproject', 4 | Deploy = 'deploy', 5 | ImportBundle = 'importbundle', 6 | ImportFiles = 'importfiles', 7 | ImportObjects = 'importobjects', 8 | IssueToken = 'issuetoken', 9 | ListBundles = 'listbundles', 10 | ListConfiguration = 'listconfiguration', 11 | ListFiles = 'listfiles', 12 | ListMissingDependencies = 'listmissingdependencies', 13 | ListObjects = 'listobjects', 14 | Preview = 'preview', 15 | RevokeToken = 'revoketoken', 16 | SaveToken = 'authenticate', 17 | Update = 'update', 18 | UpdateCustomRecordsWithInstances = 'updatecustomrecordwithinstances', 19 | // UploadFolders = 'uploadfolders', 20 | Validate = 'validate' 21 | } 22 | -------------------------------------------------------------------------------- /resources/dark/boolean.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /resources/dark/dependency.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /resources/light/boolean.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /resources/light/dependency.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/test/extension.test.ts: -------------------------------------------------------------------------------- 1 | // 2 | // Note: This example test is leveraging the Mocha test framework. 3 | // Please refer to their documentation on https://mochajs.org/ for help. 4 | // 5 | 6 | // The module 'assert' provides assertion methods from node 7 | import * as assert from 'assert'; 8 | 9 | // You can import and use all API from the 'vscode' module 10 | // as well as import your extension to test it 11 | import * as vscode from 'vscode'; 12 | import * as myExtension from '../extension'; 13 | 14 | // Defines a Mocha test suite to group tests of similar kind together 15 | suite("Extension Tests", () => { 16 | 17 | // Defines a Mocha unit test 18 | test("Something 1", () => { 19 | assert.equal(-1, [1, 2, 3].indexOf(5)); 20 | assert.equal(-1, [1, 2, 3].indexOf(0)); 21 | }); 22 | }); -------------------------------------------------------------------------------- /src/provider-commands.ts: -------------------------------------------------------------------------------- 1 | import * as _ from 'lodash'; 2 | 3 | import { NetSuiteSDF } from './netsuite-sdf'; 4 | import { SDFObjectFolder, SDFObject, SDFProvider } from './sdf-provider'; 5 | 6 | export let _importObject = (sdf: NetSuiteSDF) => (context?: SDFObject) => { 7 | sdf._importObjects(context.type, [context.label], context.path); 8 | }; 9 | 10 | export let _importFile = (sdf: NetSuiteSDF) => (context?: SDFObject) => { 11 | sdf._importFiles([context.path]); 12 | }; 13 | 14 | export let _importObjectFolder = (sdf: NetSuiteSDF) => async (context?: SDFObjectFolder) => { 15 | const children = await context.getChildren(); 16 | const scriptIds = _.map(children, (sdfObject: SDFObject) => sdfObject.label); 17 | sdf._importObjects(context.object.type, scriptIds, context.object.destination); 18 | }; 19 | 20 | export let _refresh = (provider: SDFProvider) => async (context?: any) => { 21 | console.log(provider); 22 | console.log(context); 23 | }; 24 | -------------------------------------------------------------------------------- /resources/dark/refresh.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /resources/light/refresh.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that compiles the extension and then opens it inside a new window 2 | { 3 | "version": "0.1.0", 4 | "configurations": [ 5 | { 6 | "name": "Extension", 7 | "type": "extensionHost", 8 | "request": "launch", 9 | "runtimeExecutable": "${execPath}", 10 | "args": ["--extensionDevelopmentPath=${workspaceRoot}"], 11 | "stopOnEntry": false, 12 | "sourceMaps": true, 13 | "outFiles": ["${workspaceRoot}/dist/**/*.js"], 14 | "preLaunchTask": "npm: webpack" 15 | }, 16 | { 17 | "name": "Extension Tests", 18 | "type": "extensionHost", 19 | "request": "launch", 20 | "runtimeExecutable": "${execPath}", 21 | "args": ["--extensionDevelopmentPath=${workspaceRoot}", "--extensionTestsPath=${workspaceRoot}/out/test"], 22 | "stopOnEntry": false, 23 | "sourceMaps": true, 24 | "outFiles": ["${workspaceRoot}/out/dist/**/*.js"], 25 | "preLaunchTask": "npm: webpack" 26 | } 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /src/test/index.ts: -------------------------------------------------------------------------------- 1 | // 2 | // PLEASE DO NOT MODIFY / DELETE UNLESS YOU KNOW WHAT YOU ARE DOING 3 | // 4 | // This file is providing the test runner to use when running extension tests. 5 | // By default the test runner in use is Mocha based. 6 | // 7 | // You can provide your own test runner if you want to override it by exporting 8 | // a function run(testRoot: string, clb: (error:Error) => void) that the extension 9 | // host can call to run the tests. The test runner is expected to use console.log 10 | // to report the results back to the caller. When the tests are finished, return 11 | // a possible error to the callback or null if none. 12 | 13 | import * as testRunner from 'vscode/lib/testrunner'; 14 | 15 | // You can directly control Mocha options by uncommenting the following lines 16 | // See https://github.com/mochajs/mocha/wiki/Using-mocha-programmatically#set-options for more info 17 | testRunner.configure({ 18 | ui: 'tdd', // the TDD UI is being used in extension.test.ts (suite, test, etc.) 19 | useColors: true // colored output from test results 20 | }); 21 | 22 | module.exports = testRunner; -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 mark-keaton 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | //@ts-check 7 | 8 | 'use strict'; 9 | 10 | const path = require('path'); 11 | 12 | /**@type {import('webpack').Configuration}*/ 13 | const config = { 14 | target: 'node', // vscode extensions run in a Node.js-context 📖 -> https://webpack.js.org/configuration/node/ 15 | 16 | entry: './src/extension.ts', // the entry point of this extension, 📖 -> https://webpack.js.org/configuration/entry-context/ 17 | output: { 18 | // the bundle is stored in the 'dist' folder (check package.json), 📖 -> https://webpack.js.org/configuration/output/ 19 | path: path.resolve(__dirname, 'dist'), 20 | filename: 'extension.js', 21 | libraryTarget: 'commonjs2', 22 | devtoolModuleFilenameTemplate: '../[resource-path]' 23 | }, 24 | devtool: 'source-map', 25 | externals: { 26 | vscode: 'commonjs vscode' // the vscode-module is created on-the-fly and must be excluded. Add other modules that cannot be webpack'ed, 📖 -> https://webpack.js.org/configuration/externals/ 27 | }, 28 | resolve: { 29 | // support reading TypeScript and JavaScript files, 📖 -> https://github.com/TypeStrong/ts-loader 30 | extensions: ['.ts', '.js'] 31 | }, 32 | module: { 33 | rules: [ 34 | { 35 | test: /\.ts$/, 36 | exclude: /node_modules/, 37 | use: [ 38 | { 39 | loader: 'ts-loader', 40 | options: { 41 | compilerOptions: { 42 | module: 'commonjs', 43 | target: 'es6', 44 | outDir: 'out', 45 | lib: ['es6', 'es2016.array.include', 'es2017.object'], 46 | sourceMap: true, 47 | rootDir: 'src' 48 | } 49 | } 50 | } 51 | ] 52 | } 53 | ] 54 | } 55 | }; 56 | 57 | module.exports = config; 58 | -------------------------------------------------------------------------------- /resources/dark/number.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /resources/light/number.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /resources/dark/string.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /resources/light/string.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/sdf-provider.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | 3 | import * as _ from 'lodash'; 4 | import { NetSuiteSDF } from './netsuite-sdf'; 5 | import { CustomObjects, CustomObject } from './custom-object'; 6 | import { CLICommand } from './cli-command'; 7 | 8 | export class SDFProvider implements vscode.TreeDataProvider { 9 | private _onDidChangeTreeData: vscode.EventEmitter = new vscode.EventEmitter< 10 | SDFFile | undefined 11 | >(); 12 | readonly onDidChangeTreeData: vscode.Event = this._onDidChangeTreeData.event; 13 | 14 | constructor(private sdf: NetSuiteSDF) {} 15 | 16 | refresh(event?: any): void { 17 | console.log(event); 18 | console.log(event); 19 | } 20 | 21 | getTreeItem(element: SDFFile): vscode.TreeItem { 22 | return element; 23 | } 24 | 25 | async getChildren(element?: SDFFile | SDFFolder | SDFObjectFolder) { 26 | if (element && element.label === 'SuiteScripts') { 27 | await this.getSuiteScripts(element); 28 | return (element).getChildren(); 29 | } else if (element) { 30 | return (element).getChildren(); 31 | } else { 32 | return [this.getObjectFolders(), new SDFFolder('SuiteScripts')]; 33 | } 34 | } 35 | 36 | private async getSuiteScripts(suiteScriptsFolder: SDFFolder) { 37 | this.sdf.doAddProjectParameter = false; 38 | this.sdf.doReturnData = true; 39 | 40 | const collectedData = await this.sdf.listFiles(); 41 | if (collectedData) { 42 | const reducedData = _(collectedData) 43 | .filter(path => path) 44 | .map(path => path.split('/').slice(2)) 45 | .reduce((acc, pathParts, index) => { 46 | const path = collectedData[index]; 47 | const fileName = pathParts[pathParts.length - 1]; 48 | 49 | if (pathParts.length === 1) { 50 | acc.children.push(new SDFFile(fileName, path)); 51 | } else { 52 | const dirParts = pathParts.slice(0, -1); 53 | 54 | let dirObj = acc; 55 | while (!_.isEmpty(dirParts)) { 56 | let dir = dirParts.shift(); 57 | if (!_.has(dirObj, dir)) { 58 | dirObj[dir] = new SDFFolder(dir); 59 | } 60 | dirObj = dirObj[dir]; 61 | } 62 | 63 | dirObj['children'].push(new SDFFile(fileName, path)); 64 | } 65 | return acc; 66 | }, suiteScriptsFolder); 67 | } 68 | } 69 | 70 | private getObjectFolders() { 71 | const objects = new SDFFolder('Objects'); 72 | const objectsMap = _.reduce( 73 | CustomObjects, 74 | (acc, customObject) => { 75 | const folderName = customObject._destination[0]; 76 | let child: SDFFolder | SDFObjectFolder; 77 | if (customObject._destination.length > 1) { 78 | child = folderName in acc ? acc[folderName] : new SDFFolder(folderName); 79 | const grandchild = customObject._destination[1]; 80 | (child).children.push(new SDFObjectFolder(this.sdf, grandchild, customObject)); 81 | } else { 82 | child = new SDFObjectFolder(this.sdf, folderName, customObject); 83 | } 84 | acc[folderName] = child; 85 | return acc; 86 | }, 87 | {} 88 | ); 89 | objects.children = _.sortBy(Object.values(objectsMap), 'label'); 90 | return objects; 91 | } 92 | } 93 | 94 | export class SDFFile extends vscode.TreeItem { 95 | constructor(public readonly label: string, private path: string) { 96 | super(label, vscode.TreeItemCollapsibleState.None); 97 | } 98 | 99 | get tooltip(): string { 100 | return `${this.label}`; 101 | } 102 | 103 | contextValue = 'sdf-file'; 104 | } 105 | 106 | export class SDFObject extends vscode.TreeItem { 107 | constructor(public readonly label: string, public path: string, public type: string) { 108 | super(label, vscode.TreeItemCollapsibleState.None); 109 | } 110 | 111 | get tooltip(): string { 112 | return `${this.label}`; 113 | } 114 | 115 | contextValue = 'sdf-object'; 116 | } 117 | 118 | export class SDFFolder extends vscode.TreeItem { 119 | children: Array = []; 120 | 121 | constructor(public label: string) { 122 | super(label, vscode.TreeItemCollapsibleState.Collapsed); 123 | } 124 | 125 | get tooltip(): string { 126 | return `${this.label}`; 127 | } 128 | 129 | getChildren() { 130 | const keys = Object.keys(this); 131 | const folderKeys = _.filter(keys, key => !['collapsibleState', 'label', 'contextValue', 'children'].includes(key)); 132 | const folders = _.map(folderKeys, folderKey => this[folderKey]); 133 | const concatted = folders.concat(this.children); 134 | return concatted; 135 | } 136 | 137 | contextValue = 'sdf-folder'; 138 | } 139 | 140 | export class SDFObjectFolder extends vscode.TreeItem { 141 | constructor(public sdf: NetSuiteSDF, public label: string, public object: CustomObject) { 142 | super(label, vscode.TreeItemCollapsibleState.Collapsed); 143 | } 144 | 145 | get tooltip(): string { 146 | return `${this.label}`; 147 | } 148 | 149 | async getChildren() { 150 | this.sdf.doAddProjectParameter = false; 151 | this.sdf.doReturnData = true; 152 | 153 | await this.sdf.getConfig(); 154 | if (this.sdf.sdfConfig) { 155 | const files = await this.sdf.runCommand(CLICommand.ListObjects, `-type ${this.object.type}`); 156 | return _.map(files, (file: string) => new SDFObject(file, this.object.destination, this.object.type)); 157 | } 158 | } 159 | 160 | contextValue = 'sdf-object-folder'; 161 | } 162 | -------------------------------------------------------------------------------- /src/extension.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | // The module 'vscode' contains the VS Code extensibility API 3 | // Import the module and reference it with the alias vscode in your code below 4 | import * as vscode from 'vscode'; 5 | 6 | import { NetSuiteSDF } from './netsuite-sdf'; 7 | import { SDFProvider } from './sdf-provider'; 8 | import { _importObject, _importObjectFolder, _refresh, _importFile } from './provider-commands'; 9 | 10 | export async function activate(context: vscode.ExtensionContext) { 11 | const netsuiteSdf = new NetSuiteSDF(context); 12 | 13 | const sdfProvider = new SDFProvider(netsuiteSdf); 14 | vscode.window.registerTreeDataProvider('netsuitesdf', sdfProvider); 15 | 16 | let importFolderFunc = _importObjectFolder(netsuiteSdf); 17 | let importObjectFunc = _importObject(netsuiteSdf); 18 | let importFileFunc = _importFile(netsuiteSdf); 19 | // let refreshFunc = _refresh(sdfProvider); 20 | let importFolder = vscode.commands.registerCommand('extension.importFolder', importFolderFunc); 21 | let importObject = vscode.commands.registerCommand('extension.importObject', importObjectFunc); 22 | let importFile = vscode.commands.registerCommand('extension.importFile', importFileFunc); 23 | // let refresh = vscode.commands.registerCommand('extension.refresh', refreshFunc); 24 | 25 | let addDependencies = vscode.commands.registerCommand( 26 | 'extension.addDependencies', 27 | netsuiteSdf.addDependencies.bind(netsuiteSdf) 28 | ); 29 | let createResetDeploy = vscode.commands.registerCommand( 30 | 'extension.createResetDeploy', 31 | netsuiteSdf.createResetDeploy.bind(netsuiteSdf) 32 | ); 33 | let backupRestoreDeploy = vscode.commands.registerCommand( 34 | 'extension.backupRestoreDeploy', 35 | netsuiteSdf.backupRestoreDeploy.bind(netsuiteSdf) 36 | ); 37 | let addFileToDeploy = vscode.commands.registerCommand( 38 | 'extension.addFileToDeploy', 39 | netsuiteSdf.addFileToDeploy.bind(netsuiteSdf) 40 | ); 41 | let deploy = vscode.commands.registerCommand('extension.deploy', netsuiteSdf.deploy.bind(netsuiteSdf)); 42 | let importBundle = vscode.commands.registerCommand( 43 | 'extension.importBundle', 44 | netsuiteSdf.importBundle.bind(netsuiteSdf) 45 | ); 46 | let importFiles = vscode.commands.registerCommand('extension.importFiles', netsuiteSdf.importFiles.bind(netsuiteSdf)); 47 | let importObjects = vscode.commands.registerCommand( 48 | 'extension.importObjects', 49 | netsuiteSdf.importObjects.bind(netsuiteSdf) 50 | ); 51 | let issueToken = vscode.commands.registerCommand('extension.issueToken', netsuiteSdf.issueToken.bind(netsuiteSdf)); 52 | let listBundles = vscode.commands.registerCommand('extension.listBundles', netsuiteSdf.listBundles.bind(netsuiteSdf)); 53 | let listConfiguration = vscode.commands.registerCommand( 54 | 'extension.listConfiguration', 55 | netsuiteSdf.listConfiguration.bind(netsuiteSdf) 56 | ); 57 | let listFiles = vscode.commands.registerCommand('extension.listFiles', netsuiteSdf.listFiles.bind(netsuiteSdf)); 58 | let listMissingDependencies = vscode.commands.registerCommand( 59 | 'extension.listMissingDependencies', 60 | netsuiteSdf.listMissingDependencies.bind(netsuiteSdf) 61 | ); 62 | let listObjects = vscode.commands.registerCommand('extension.listObjects', netsuiteSdf.listObjects.bind(netsuiteSdf)); 63 | let preview = vscode.commands.registerCommand('extension.preview', netsuiteSdf.preview.bind(netsuiteSdf)); 64 | let refreshConfig = vscode.commands.registerCommand( 65 | 'extension.refreshConfig', 66 | netsuiteSdf.refreshConfig.bind(netsuiteSdf) 67 | ); 68 | let removeFolders = vscode.commands.registerCommand( 69 | 'extension.removeFolders', 70 | netsuiteSdf.removeFolders.bind(netsuiteSdf) 71 | ); 72 | let resetPassword = vscode.commands.registerCommand( 73 | 'extension.resetPassword', 74 | netsuiteSdf.resetPassword.bind(netsuiteSdf) 75 | ); 76 | let revokeToken = vscode.commands.registerCommand('extension.revokeToken', netsuiteSdf.revokeToken.bind(netsuiteSdf)); 77 | let saveToken = vscode.commands.registerCommand('extension.saveToken', netsuiteSdf.saveToken.bind(netsuiteSdf)); 78 | let selectEnvironment = vscode.commands.registerCommand( 79 | 'extension.selectEnvironment', 80 | netsuiteSdf.selectEnvironment.bind(netsuiteSdf) 81 | ); 82 | let sync = vscode.commands.registerCommand('extension.sync', netsuiteSdf.sync.bind(netsuiteSdf)); 83 | let update = vscode.commands.registerCommand('extension.update', netsuiteSdf.update.bind(netsuiteSdf)); 84 | let updateCustomRecordWithInstances = vscode.commands.registerCommand( 85 | 'extension.updateCustomRecordWithInstances', 86 | netsuiteSdf.updateCustomRecordWithInstances.bind(netsuiteSdf) 87 | ); 88 | // let uploadFolders = vscode.commands.registerCommand( 89 | // 'extension.uploadFolders', 90 | // netsuiteSdf.uploadFolders.bind(netsuiteSdf) 91 | // ); 92 | let validate = vscode.commands.registerCommand('extension.validate', netsuiteSdf.validate.bind(netsuiteSdf)); 93 | 94 | context.subscriptions.push(importFolder); 95 | context.subscriptions.push(importObject); 96 | context.subscriptions.push(importFile); 97 | // context.subscriptions.push(refresh); 98 | 99 | context.subscriptions.push(addDependencies); 100 | context.subscriptions.push(createResetDeploy); 101 | context.subscriptions.push(backupRestoreDeploy); 102 | context.subscriptions.push(addFileToDeploy); 103 | context.subscriptions.push(deploy); 104 | context.subscriptions.push(importBundle); 105 | context.subscriptions.push(importFiles); 106 | context.subscriptions.push(importObjects); 107 | context.subscriptions.push(issueToken); 108 | context.subscriptions.push(listBundles); 109 | context.subscriptions.push(listConfiguration); 110 | context.subscriptions.push(listFiles); 111 | context.subscriptions.push(listMissingDependencies); 112 | context.subscriptions.push(listObjects); 113 | context.subscriptions.push(preview); 114 | context.subscriptions.push(refreshConfig); 115 | context.subscriptions.push(removeFolders); 116 | context.subscriptions.push(resetPassword); 117 | context.subscriptions.push(revokeToken); 118 | context.subscriptions.push(saveToken); 119 | context.subscriptions.push(selectEnvironment); 120 | context.subscriptions.push(sync); 121 | context.subscriptions.push(update); 122 | context.subscriptions.push(updateCustomRecordWithInstances); 123 | // context.subscriptions.push(uploadFolders); 124 | context.subscriptions.push(validate); 125 | } 126 | 127 | // this method is called when your extension is deactivated 128 | export function deactivate() {} 129 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NetSuiteSDF Extension 2 | 3 | ## Notice 4 | 5 | Beginning with 2020.2, you will need to upgrade your sdfcli version. You can use homebrew to do this, or follow the NetSuite help docs. 6 | 7 | You will also need to upgrade to Java 11. This project officially supports Amazon Corretto (https://corretto.aws/) instead of OpenJDK or the official Java SDK. 8 | 9 | Also starting with 2020.2, this project no longer supports username and password authentication for the plugin. You will need to use the save token option in the command palette to setup your environments. Make sure to add `authid` in your `.sdfcli.json` config file. 10 | 11 | Here's a sample .sdfcli.json: 12 | 13 | ``` 14 | { 15 | "projectName": "SDF", 16 | "environments": [ 17 | { 18 | "name": "SANDBOX 1", 19 | "account": "123456_SB1", 20 | "url": "system.netsuite.com", 21 | "role": 3, 22 | "email": "your@email.com", 23 | "authid": "DEV - ADMINISTRATOR" 24 | } 25 | ] 26 | } 27 | ``` 28 | 29 | ## Introduction 30 | 31 | This is a Visual Studio Code wrapper around the NetSuite SDF command-line interface. It also provides a series of defaults for the SDF CLI to make utilizing the CLI more user-friendly. 32 | 33 | ## Inspiration 34 | 35 | This is a port of the functionality provided by [tjtyrrell](https://github.com/tjtyrrell)'s Sublime SDF plugin (). And we are borrowing the work he did in getting the Windows and Mac SDFCLI installers working. 36 | 37 | ## Features 38 | 39 | - Currently updated to work with 2020.2. 40 | - Wraps SDF CLI commands 41 | - Environment (Sandbox, Production, etc.) selector 42 | - Output window integrated with VS Code 43 | - _Now webpacked to speed up VS Code load time_ 44 | - Quick Deploy option available in Extension Preferences 45 | 46 | ## Status 47 | 48 | All commands can be found with the `SDF` prefix in the Command Palette (Win: Ctrl+Shift+P, Mac: Cmd+Shift+P). 49 | 50 | ### SDF CLI Commands 51 | 52 | | _Command_ | _Implemented_ | _Shortcut_ | 53 | | ------------------------------- | ---------------------- | ---------- | 54 | | adddependencies | ✔ | 55 | | deploy | ✔ | 56 | | importbundle | | 57 | | importconfiguration | | 58 | | importfiles | ✔ | 59 | | importobjects | ✔ | 60 | | issuetoken | ✔ | 61 | | listbundles | ✔ | 62 | | listconfiguration | ✔ | 63 | | listfiles | ✔ | 64 | | listmissingdependencies | ✔ | 65 | | listObjects | ✔ | 66 | | preview | ✔ | 67 | | project | (Handled by extension) | 68 | | revoketoken | ✔ | 69 | | savetoken | ✔ | 70 | | update | ✔ | 71 | | updatecustomrecordwithinstances | ✔ | 72 | | validate | ✔ | 73 | 74 | ### VS Code Commands 75 | 76 | | _Command_ | _Description_ | 77 | | ----------------- | ----------------------------------------------------------------------------------------------- | 78 | | Refresh Config | Force the extension to re-read .sdfcli.json | 79 | | resetPassword | Enter password for use with environment | 80 | | selectEnvironment | Select active environment from list in .sdfcli.json. If only one, will automatically select it. | 81 | | sync | Grabs all available customizations from NetSuite that is normally possible by the plugin. | 82 | | remove folders | Removes all created folders from the current directory. Used in conjunction with sync. | 83 | 84 | ### Hotkeys 85 | 86 | _Note: All hotkeys are preceded by Ctrl-; on Windows or Cmd-; on Mac._ 87 | For example, if I wanted to run the command `addDependencies` on a Mac, I would press Cmd-;, then press a. 88 | `

` below stands for that OS-specific prefix. These commands are case sensitive. 89 | 90 | | _Command_ | _Shortcut_ | 91 | | ------------------------------- | ---------- | 92 | | addDependencies | `

a` | 93 | | addFileToDeploy | `

+` | 94 | | deploy | `

d` | 95 | | importBundle | `

d` | 96 | | importFiles | `

F` | 97 | | importObjects | `

O` | 98 | | issueToken | `

t` | 99 | | listBundles | `

b` | 100 | | listConfiguration | `

c` | 101 | | listFiles | `

f` | 102 | | listMissingDependencies | `

m` | 103 | | listObjects | `

o` | 104 | | preview | `

p` | 105 | | refreshConfig | `

r` | 106 | | revokeToken | `

R` | 107 | | saveToken | `

T` | 108 | | selectEnvironment | `

s` | 109 | | update | `

u` | 110 | | updateCustomRecordWithInstances | `

U` | 111 | | validate | `

v` | 112 | | resetPassword | `

P` | 113 | 114 | ### Quick Deploy 115 | 116 | Enable this feature in VS Code settings. Currently it is opt-in, as it is a beta feature. 117 | 118 | If enabled, when a `Deploy` is triggered, the files and objects listed in the `deploy.xml` will be copied to a subdirectory along with the `manifest.xml`, and the deploy will be triggered from there. 119 | 120 | To facilitate more rapid development, there is a command to add a file directly to your `deploy.xml`. Either through selecting it in the File Browser, right-clicking, and selecting `Add File to Deploy.xml`, or by running the command while the file is being edited to add it. 121 | 122 | Pros: 123 | 124 | - Avoids the node_modules issue 125 | - Allows for larger SDF projects 126 | - Reduction in deploy time from 8-10 minutes down to 8-10 seconds 127 | 128 | Cons: 129 | 130 | - Untested on Windows 131 | 132 | ### ToDo 133 | 134 | | _Command_ | _Description_ | 135 | | ----------- | ----------------------------------------------------------------------------------- | 136 | | New Project | Will generate SDF project file structure in the same manner as sdfcli-createproject | 137 | | Update .sdf | Automatically update .sdf with active environment information | 138 | 139 | ## Installation 140 | 141 | 1. Install SDFCLI. Either use the SDF documentation or tjtyrrell's _brew_ or _chocolatey_. I recommend tjtyrell's. 142 | 143 | #### Windows 144 | 145 | Install via [Chocolatey](https://chocolatey.org) 146 | 147 | ```bash 148 | choco install sdfcli # This installs Java 8 and Maven 3.5 149 | ``` 150 | 151 | #### Mac 152 | 153 | Install via [Homebrew](https://brew.sh) 154 | 155 | _Warning: the Brew Cask has been renamed. If you used it previously as `sdfcli`, please untap the cask and uninstall:_ 156 | 157 | ```bash 158 | brew untap limebox/netsuite 159 | brew uninstall sdfcli 160 | ``` 161 | 162 | Install SDFSDK: 163 | 164 | ```bash 165 | brew cask install corretto 166 | brew install limebox/netsuite/sdfsdk 167 | ``` 168 | 169 | 2. The plugin is activated when a project is opened that has a `.sdf` or `.sdfcli.json` file in the root directory. So open a SDF project folder that contains a `.sdf` file. 170 | 171 | 3) If the Extension is activated, you should see a `SDF` button in the bottom left status bar. Click the button to open up the Select Environment inputs. This will generate a .sdfcli.json in your root directory of your project. 172 | 173 | 4) Fill in your environments that you want to be able to switch between inside of the extension inside of the .sdfcli.json. 174 | 175 | 5) Hit Ctrl+Shift+P for Windows, or Cmd+Shift+P to bring up the command palette again, and type `SDF` to see all the options. 176 | -------------------------------------------------------------------------------- /src/custom-object.ts: -------------------------------------------------------------------------------- 1 | import { QuickPickItem } from 'vscode'; 2 | 3 | export interface ICustomObject extends QuickPickItem { 4 | _destination: string[]; 5 | type: string; 6 | } 7 | 8 | export class CustomObject implements ICustomObject { 9 | label: string; 10 | type: string; 11 | _destination: string[]; 12 | detail: string; 13 | description: string; 14 | 15 | constructor(customObject: ICustomObject) { 16 | return Object.assign(this, customObject); 17 | } 18 | 19 | get destination() { 20 | return `/Objects/${this._destination.join('/')}`; 21 | } 22 | } 23 | 24 | export const CustomObjects: CustomObject[] = [ 25 | new CustomObject({ 26 | label: 'Address Form', 27 | type: 'addressform', 28 | _destination: ['Forms', 'AddressForms'], 29 | detail: 'customscript', 30 | description: '', 31 | }), 32 | new CustomObject({ 33 | label: 'Advanced PDF Template', 34 | type: 'advancedpdftemplate', 35 | _destination: ['Templates', 'AdvancedPDFs'], 36 | detail: 'custtmpl', 37 | description: '', 38 | }), 39 | new CustomObject({ 40 | label: 'Bank Statement Parser Plugin', 41 | type: 'bankstatementparserplugin', 42 | _destination: ['BankStatementParserPlugin'], 43 | detail: 'customscript', 44 | description: '', 45 | }), 46 | new CustomObject({ 47 | label: 'Bundle Installation Script', 48 | type: 'bundleinstallationscript', 49 | _destination: ['BundleInstallation'], 50 | detail: 'customscript', 51 | description: '', 52 | }), 53 | new CustomObject({ 54 | label: 'Centers', 55 | type: 'center', 56 | _destination: ['CentersAndTabs', 'Centers'], 57 | detail: 'custcenter', 58 | description: '', 59 | }), 60 | new CustomObject({ 61 | label: 'Center Categories', 62 | type: 'centercategory', 63 | _destination: ['CentersAndTabs', 'Categories'], 64 | detail: 'custcentercategory', 65 | description: '', 66 | }), 67 | new CustomObject({ 68 | label: 'Center Tabs', 69 | type: 'centertab', 70 | _destination: ['CentersAndTabs', 'Tab'], 71 | detail: 'custcentertab', 72 | description: '', 73 | }), 74 | new CustomObject({ 75 | label: 'Client Scripts', 76 | type: 'clientscript', 77 | _destination: ['Scripts', 'Client'], 78 | detail: 'customscript', 79 | description: '', 80 | }), 81 | new CustomObject({ 82 | label: 'CMS Content Type', 83 | type: 'cmscontenttype', 84 | _destination: ['CMS', 'ContentType'], 85 | detail: 'custcontenttype', 86 | description: '', 87 | }), 88 | new CustomObject({ 89 | label: 'CRM Custom Fields', 90 | type: 'crmcustomfield', 91 | _destination: ['Fields', 'CRM'], 92 | detail: 'custevent', 93 | description: '', 94 | }), 95 | new CustomObject({ 96 | label: 'Custom Plugins', 97 | type: 'customglplugin', 98 | _destination: ['Plugins', 'Custom'], 99 | detail: 'customscript', 100 | description: '', 101 | }), 102 | new CustomObject({ 103 | label: 'Custom Lists', 104 | type: 'customlist', 105 | _destination: ['Lists'], 106 | detail: 'customlist', 107 | description: '', 108 | }), 109 | new CustomObject({ 110 | label: 'Custom Records', 111 | type: 'customrecordtype', 112 | _destination: ['Records'], 113 | detail: 'customrecord', 114 | description: '', 115 | }), 116 | new CustomObject({ 117 | label: 'Custom Segment', 118 | type: 'customsegment', 119 | _destination: ['CustomSegments'], 120 | detail: 'cseg', 121 | description: '', 122 | }), 123 | new CustomObject({ 124 | label: 'Custom Transactions', 125 | type: 'customtransactiontype', 126 | _destination: ['CustomTransactions'], 127 | detail: 'customtransaction', 128 | description: '', 129 | }), 130 | new CustomObject({ 131 | label: 'Dataset', 132 | type: 'dataset', 133 | _destination: ['Dataset'], 134 | detail: 'custdataset', 135 | description: '', 136 | }), 137 | new CustomObject({ 138 | label: 'Email Capture Plugins', 139 | type: 'emailcaptureplugin', 140 | _destination: ['Plugins', 'Email'], 141 | detail: 'customscript', 142 | description: '', 143 | }), 144 | new CustomObject({ 145 | label: 'Email Template', 146 | type: 'emailtemplate', 147 | _destination: ['Templates', 'Email'], 148 | detail: 'custemailtmpl', 149 | description: '', 150 | }), 151 | new CustomObject({ 152 | label: 'Entity Custom Fields', 153 | type: 'entitycustomfield', 154 | _destination: ['Fields', 'Entity'], 155 | detail: 'custentity', 156 | description: '', 157 | }), 158 | new CustomObject({ 159 | label: 'Entity Forms', 160 | type: 'entryForm', 161 | _destination: ['Forms', 'EntryForm'], 162 | detail: 'custform', 163 | description: '', 164 | }), 165 | new CustomObject({ 166 | label: 'Financial Layouts', 167 | type: 'financiallayout', 168 | _destination: ['Reports', 'FinancialLayout'], 169 | detail: 'customlayout', 170 | description: '', 171 | }), 172 | new CustomObject({ 173 | label: 'Item Custom Fields', 174 | type: 'itemcustomfield', 175 | _destination: ['Fields', 'Item'], 176 | detail: 'custitem', 177 | description: '', 178 | }), 179 | new CustomObject({ 180 | label: 'Item Number Custom Fields', 181 | type: 'itemnumbercustomfield', 182 | _destination: ['Fields', 'ItemNumber'], 183 | detail: 'custitem', 184 | description: '', 185 | }), 186 | new CustomObject({ 187 | label: 'Item Option Custom Fields', 188 | type: 'itemoptioncustomfield', 189 | _destination: ['Fields', 'ItemOption'], 190 | detail: 'custitemoption', 191 | description: '', 192 | }), 193 | new CustomObject({ 194 | label: 'KPI Scorecard', 195 | type: 'kpiscorecard', 196 | _destination: ['KPIScorecards'], 197 | detail: 'custkpiscorecard', 198 | description: '', 199 | }), 200 | new CustomObject({ 201 | label: 'Map Reduce Script', 202 | type: 'mapreducescript', 203 | _destination: ['Scripts', 'MapReduce'], 204 | detail: 'customscript', 205 | description: '', 206 | }), 207 | new CustomObject({ 208 | label: 'Mass Update Script', 209 | type: 'massupdatescript', 210 | _destination: ['Scripts', 'MassUpdate'], 211 | detail: 'customscript', 212 | description: '', 213 | }), 214 | new CustomObject({ 215 | label: 'Other Custom Field', 216 | type: 'othercustomfield', 217 | _destination: ['Fields', 'Other'], 218 | detail: 'custrecord', 219 | description: '', 220 | }), 221 | new CustomObject({ 222 | label: 'Plugin Implementation', 223 | type: 'pluginimplementation', 224 | _destination: ['PluginImplementations'], 225 | detail: 'customscript', 226 | description: '', 227 | }), 228 | 229 | new CustomObject({ 230 | label: 'Plugin Type', 231 | type: 'plugintype', 232 | _destination: ['PluginTypes'], 233 | detail: 'customscript', 234 | description: '', 235 | }), 236 | new CustomObject({ 237 | label: 'Portlets', 238 | type: 'portlet', 239 | _destination: ['Scripts', 'Portlet'], 240 | detail: 'customscript', 241 | description: '', 242 | }), 243 | new CustomObject({ 244 | label: 'Promotions Plugins', 245 | type: 'promotionsplugin', 246 | _destination: ['Plugins', 'Promotions'], 247 | detail: 'customscript', 248 | description: '', 249 | }), 250 | new CustomObject({ 251 | label: 'Publish Dashboards', 252 | type: 'publisheddashboard', 253 | _destination: ['PublishDashboards'], 254 | detail: 'custpubdashboard', 255 | description: '', 256 | }), 257 | new CustomObject({ 258 | label: 'Restlets', 259 | type: 'restlet', 260 | _destination: ['Scripts', 'Restlet'], 261 | detail: 'customscript', 262 | description: '', 263 | }), 264 | new CustomObject({ 265 | label: 'Report Definitions', 266 | type: 'reportdefinition', 267 | _destination: ['Reports', 'ReportDefinition'], 268 | detail: 'customreport', 269 | description: '', 270 | }), 271 | new CustomObject({ 272 | label: 'Roles', 273 | type: 'role', 274 | _destination: ['Roles'], 275 | detail: 'customrole', 276 | description: '', 277 | }), 278 | new CustomObject({ 279 | label: 'Saved CSV Import', 280 | type: 'csvimport', 281 | _destination: ['CSVImports'], 282 | detail: 'custimport', 283 | description: '', 284 | }), 285 | new CustomObject({ 286 | label: 'Saved Searches', 287 | type: 'savedsearch', 288 | _destination: ['SavedSearches'], 289 | detail: 'customsearch', 290 | description: '', 291 | }), 292 | new CustomObject({ 293 | label: 'Scheduled Scripts', 294 | type: 'scheduledscript', 295 | _destination: ['Scripts', 'Scheduled'], 296 | detail: 'customscript', 297 | description: '', 298 | }), 299 | new CustomObject({ 300 | label: 'SDF Installation Script', 301 | type: 'sdfinstallationscript', 302 | _destination: ['Scripts', 'SDFInstallation'], 303 | detail: 'customscript', 304 | description: '', 305 | }), 306 | new CustomObject({ 307 | label: 'SSP Applications', 308 | type: 'sspapplication', 309 | _destination: ['SSPApplications'], 310 | detail: 'webapp', 311 | description: '', 312 | }), 313 | new CustomObject({ 314 | label: 'Sublists', 315 | type: 'sublist', 316 | _destination: ['Sublists'], 317 | detail: 'custsublist', 318 | description: '', 319 | }), 320 | new CustomObject({ 321 | label: 'SubTabs', 322 | type: 'subtab', 323 | _destination: ['CentersAndTabs', 'SubTab'], 324 | detail: 'custtab', 325 | description: '', 326 | }), 327 | new CustomObject({ 328 | label: 'Suitelet', 329 | type: 'suitelet', 330 | _destination: ['Scripts', 'Suitelet'], 331 | detail: 'customscript', 332 | description: '', 333 | }), 334 | new CustomObject({ 335 | label: 'Transaction Forms', 336 | type: 'transactionForm', 337 | _destination: ['Forms', 'TransactionForm'], 338 | detail: 'custform', 339 | description: '', 340 | }), 341 | new CustomObject({ 342 | label: 'Transaction Body Custom Field', 343 | type: 'transactionbodycustomfield', 344 | _destination: ['Fields', 'TransactionBody'], 345 | detail: 'transactionbodycustomfield', 346 | description: '', 347 | }), 348 | new CustomObject({ 349 | label: 'Transaction Column Custom Field', 350 | type: 'transactioncolumncustomfield', 351 | _destination: ['Fields', 'TransactionColumn'], 352 | detail: 'custcol', 353 | description: '', 354 | }), 355 | new CustomObject({ 356 | label: 'Translation Collection', 357 | type: 'translationcollection', 358 | _destination: ['TranslationCollection'], 359 | detail: 'custcollection', 360 | description: '', 361 | }), 362 | new CustomObject({ 363 | label: 'User Event Script', 364 | type: 'usereventscript', 365 | _destination: ['Scripts', 'UserEvent'], 366 | detail: 'customscript', 367 | description: '', 368 | }), 369 | new CustomObject({ 370 | label: 'Workbooks', 371 | type: 'workbook', 372 | _destination: ['Workbooks'], 373 | detail: 'custworkbook', 374 | description: '', 375 | }), 376 | new CustomObject({ 377 | label: 'Workflows', 378 | type: 'workflow', 379 | _destination: ['Workflows'], 380 | detail: 'customworkflow', 381 | description: '', 382 | }), 383 | new CustomObject({ 384 | label: 'Workflow Action Scripts', 385 | type: 'workflowactionscript', 386 | _destination: ['Scripts', 'WorkflowAction'], 387 | detail: 'customscript', 388 | description: '', 389 | }), 390 | ]; 391 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "netsuitesdf", 3 | "displayName": "NetSuiteSDF", 4 | "description": "Plugin to integrate NetSuite SDF CLI", 5 | "version": "2022.11.16", 6 | "publisher": "christopherwxyz", 7 | "license": "MIT", 8 | "repository": "https://github.com/christopherwxyz/NetSuiteSDF", 9 | "engines": { 10 | "vscode": "^1.35.0" 11 | }, 12 | "categories": [ 13 | "Other" 14 | ], 15 | "icon": "icon.png", 16 | "galleryBanner": { 17 | "color": "#C80000", 18 | "theme": "dark" 19 | }, 20 | "activationEvents": [ 21 | "workspaceContains:**/.sdf", 22 | "workspaceContains:**/.sdfcli.json", 23 | "onCommand:extension.addDependencies", 24 | "onCommand:extension.deploy", 25 | "onCommand:extension.importBundle", 26 | "onCommand:extension.importFiles", 27 | "onCommand:extension.importObjects", 28 | "onCommand:extension.issueToken", 29 | "onCommand:extension.listBundles", 30 | "onCommand:extension.listFiles", 31 | "onCommand:extension.listMissingDependencies", 32 | "onCommand:extension.listObjects", 33 | "onCommand:extension.preview", 34 | "onCommand:extension.refreshConfig", 35 | "onCommand:extension.removeFolders", 36 | "onCommand:extension.resetPassword", 37 | "onCommand:extension.revokeToken", 38 | "onCommand:extension.saveToken", 39 | "onCommand:extension.selectEnvironment", 40 | "onCommand:extension.sync", 41 | "onCommand:extension.update", 42 | "onCommand:extension.updateCustomRecordsWithInstances", 43 | "onCommand:extension.validate" 44 | ], 45 | "main": "./dist/extension", 46 | "contributes": { 47 | "configuration": { 48 | "type": "object", 49 | "title": "NetSuiteSDF", 50 | "properties": { 51 | "netsuitesdf.useQuickDeploy": { 52 | "type": "boolean", 53 | "default": true, 54 | "description": "Enable Quick Deployments" 55 | }, 56 | "netsuitesdf.addMatchingJavascriptWhenAddingTypescriptToDeployXML": { 57 | "type": "boolean", 58 | "default": true, 59 | "description": "When adding a TypeScript file to deploy.xml, its matching compiled JavaScript file will be added in its place." 60 | }, 61 | "netsuitesdf.onBackupResetDeployXML": { 62 | "type": "boolean", 63 | "default": true, 64 | "description": "After creating a backup of deploy.xml, reset deploy.xml." 65 | }, 66 | "netsuitesdf.onRestoreDeleteBackupDeployXML": { 67 | "type": "boolean", 68 | "default": true, 69 | "description": "After restoring a backup of deploy.xml, delete the backup of deploy.xml." 70 | } 71 | } 72 | }, 73 | "views": { 74 | "explorer": [ 75 | { 76 | "id": "netsuitesdf", 77 | "name": "NetSuiteSDF" 78 | } 79 | ] 80 | }, 81 | "keybindings": [ 82 | { 83 | "command": "extension.addDependencies", 84 | "key": "ctrl+; a", 85 | "mac": "cmd+; a" 86 | }, 87 | { 88 | "command": "extension.createResetDeploy", 89 | "key": "ctrl+; shift+-", 90 | "mac": "cmd+; shift+-" 91 | }, 92 | { 93 | "command": "extension.addFileToDeploy", 94 | "key": "ctrl+; shift+=", 95 | "mac": "cmd+; shift+=" 96 | }, 97 | { 98 | "command": "extension.deploy", 99 | "key": "ctrl+; d", 100 | "mac": "cmd+; d" 101 | }, 102 | { 103 | "command": "extension.importBundle", 104 | "key": "ctrl+; shift+b", 105 | "mac": "cmd+; shift+b" 106 | }, 107 | { 108 | "command": "extension.importFiles", 109 | "key": "ctrl+; shift+f", 110 | "mac": "cmd+; shift+f" 111 | }, 112 | { 113 | "command": "extension.importObjects", 114 | "key": "ctrl+; shift+o", 115 | "mac": "cmd+; shift+o" 116 | }, 117 | { 118 | "command": "extension.issueToken", 119 | "key": "ctrl+; t", 120 | "mac": "cmd+; t" 121 | }, 122 | { 123 | "command": "extension.listBundles", 124 | "key": "ctrl+; b", 125 | "mac": "cmd+; b" 126 | }, 127 | { 128 | "command": "extension.listConfiguration", 129 | "key": "ctrl+; c", 130 | "mac": "cmd+; c" 131 | }, 132 | { 133 | "command": "extension.listFiles", 134 | "key": "ctrl+; f", 135 | "mac": "cmd+; f" 136 | }, 137 | { 138 | "command": "extension.listMissingDependencies", 139 | "key": "ctrl+; m", 140 | "mac": "cmd+; m" 141 | }, 142 | { 143 | "command": "extension.listObjects", 144 | "key": "ctrl+; o", 145 | "mac": "cmd+; o" 146 | }, 147 | { 148 | "command": "extension.preview", 149 | "key": "ctrl+; p", 150 | "mac": "cmd+; p" 151 | }, 152 | { 153 | "command": "extension.refreshConfig", 154 | "key": "ctrl+; r", 155 | "mac": "cmd+; r" 156 | }, 157 | { 158 | "command": "extension.revokeToken", 159 | "key": "ctrl+; shift+r", 160 | "mac": "cmd+; shift+r" 161 | }, 162 | { 163 | "command": "extension.saveToken", 164 | "key": "ctrl+; shift+t", 165 | "mac": "cmd+; shift+t" 166 | }, 167 | { 168 | "command": "extension.selectEnvironment", 169 | "key": "ctrl+; s", 170 | "mac": "cmd+; s" 171 | }, 172 | { 173 | "command": "extension.update", 174 | "key": "ctrl+; u", 175 | "mac": "cmd+; u" 176 | }, 177 | { 178 | "command": "extension.updateCustomRecordWithInstances", 179 | "key": "ctrl+; shift+u", 180 | "mac": "cmd+; shift+u" 181 | }, 182 | { 183 | "command": "extension.validate", 184 | "key": "ctrl+; v", 185 | "mac": "cmd+; v" 186 | }, 187 | { 188 | "command": "extension.resetPassword", 189 | "key": "ctrl+; shift+p", 190 | "mac": "cmd+; shift+p" 191 | } 192 | ], 193 | "commands": [ 194 | { 195 | "command": "extension.addDependencies", 196 | "title": "Add Dependencies to Manifest", 197 | "category": "SDF" 198 | }, 199 | { 200 | "command": "extension.createResetDeploy", 201 | "title": "Create/Reset deploy.xml", 202 | "category": "SDF" 203 | }, 204 | { 205 | "command": "extension.backupRestoreDeploy", 206 | "title": "Backup/Restore deploy.xml", 207 | "category": "SDF" 208 | }, 209 | { 210 | "command": "extension.addFileToDeploy", 211 | "title": "Add current/selected file to deploy.xml", 212 | "category": "SDF" 213 | }, 214 | { 215 | "command": "extension.deploy", 216 | "title": "Deploy to Account", 217 | "category": "SDF" 218 | }, 219 | { 220 | "command": "extension.importFolder", 221 | "title": "Import Folder", 222 | "category": "SDF" 223 | }, 224 | { 225 | "command": "extension.importBundle", 226 | "title": "Import Bundle", 227 | "category": "SDF" 228 | }, 229 | { 230 | "command": "extension.importFiles", 231 | "title": "Import Files", 232 | "category": "SDF" 233 | }, 234 | { 235 | "command": "extension.importObjects", 236 | "title": "Import Objects", 237 | "category": "SDF" 238 | }, 239 | { 240 | "command": "extension.issueToken", 241 | "title": "Issue Token", 242 | "category": "SDF" 243 | }, 244 | { 245 | "command": "extension.listBundles", 246 | "title": "List Bundles", 247 | "category": "SDF" 248 | }, 249 | { 250 | "command": "extension.listConfiguration", 251 | "title": "List Configuration", 252 | "category": "SDF" 253 | }, 254 | { 255 | "command": "extension.listFiles", 256 | "title": "List Files", 257 | "category": "SDF" 258 | }, 259 | { 260 | "command": "extension.listMissingDependencies", 261 | "title": "List Missing Dependencies", 262 | "category": "SDF" 263 | }, 264 | { 265 | "command": "extension.listObjects", 266 | "title": "List Objects", 267 | "category": "SDF" 268 | }, 269 | { 270 | "command": "extension.preview", 271 | "title": "Preview", 272 | "category": "SDF" 273 | }, 274 | { 275 | "command": "extension.refreshConfig", 276 | "title": "Refresh Config", 277 | "category": "SDF" 278 | }, 279 | { 280 | "command": "extension.removeFolders", 281 | "title": "Remove folders", 282 | "category": "SDF" 283 | }, 284 | { 285 | "command": "extension.revokeToken", 286 | "title": "Revoke Token", 287 | "category": "SDF" 288 | }, 289 | { 290 | "command": "extension.saveToken", 291 | "title": "Save Token", 292 | "category": "SDF" 293 | }, 294 | { 295 | "command": "extension.selectEnvironment", 296 | "title": "Select Environment", 297 | "category": "SDF" 298 | }, 299 | { 300 | "command": "extension.sync", 301 | "title": "Run Customization Sync", 302 | "category": "SDF" 303 | }, 304 | { 305 | "command": "extension.update", 306 | "title": "Update", 307 | "category": "SDF" 308 | }, 309 | { 310 | "command": "extension.updateCustomRecordWithInstances", 311 | "title": "Update Custom Record with Instances", 312 | "category": "SDF" 313 | }, 314 | { 315 | "command": "extension.validate", 316 | "title": "Validate Project", 317 | "category": "SDF" 318 | }, 319 | { 320 | "command": "extension.resetPassword", 321 | "title": "Reset Password", 322 | "category": "SDF" 323 | } 324 | ], 325 | "menus": { 326 | "view/item/context": [ 327 | { 328 | "command": "extension.importFolder", 329 | "when": "view == netsuitesdf && viewItem == sdf-object-folder" 330 | }, 331 | { 332 | "command": "extension.importObjects", 333 | "when": "view == netsuitesdf && viewItem == sdf-object" 334 | }, 335 | { 336 | "command": "extension.importFiles", 337 | "when": "view == netsuitesdf && viewItem == sdf-file" 338 | } 339 | ], 340 | "explorer/context": [ 341 | { 342 | "command": "extension.createResetDeploy", 343 | "group": "z_commands@1" 344 | }, 345 | { 346 | "when": "resourceLangId == xml && resourceFilename =~ /(.+\\.)?deploy.xml/", 347 | "command": "extension.backupRestoreDeploy", 348 | "group": "z_commands@2" 349 | }, 350 | { 351 | "command": "extension.addFileToDeploy", 352 | "group": "z_commands@3" 353 | } 354 | ] 355 | } 356 | }, 357 | "scripts": { 358 | "compile": "tsc -p ./", 359 | "watch": "tsc -watch -p ./", 360 | "test": "npm run compile && node ./node_modules/vscode/bin/test", 361 | "vscode:prepublish": "webpack --mode production", 362 | "webpack": "webpack --mode development", 363 | "webpack-dev": "webpack --mode development --watch", 364 | "test-compile": "tsc -p ./", 365 | "postinstall": "node ./node_modules/vscode/bin/install" 366 | }, 367 | "dependencies": { 368 | "@types/lodash": "^4.14.122", 369 | "bluebird": "^3.7.2", 370 | "fs-extra": "^7.0.1", 371 | "fstream": "^1.0.12", 372 | "lodash": "^4.17.20", 373 | "rimraf": "^2.7.1", 374 | "rxjs": "^6.6.3", 375 | "rxjs-compat": "^6.6.3", 376 | "spawn-rx": "~2.0.12", 377 | "tar": "^4.4.13", 378 | "tmp": "0.0.33", 379 | "xml2js": "^0.4.23" 380 | }, 381 | "devDependencies": { 382 | "@types/fs-extra": "^7.0.0", 383 | "@types/mocha": "^2.2.42", 384 | "@types/node": "^12.0.4", 385 | "@types/tmp": "0.0.34", 386 | "@types/xml2js": "^0.4.3", 387 | "ts-loader": "^5.3.3", 388 | "typescript": "^3.9.7", 389 | "vscode": "^1.1.34", 390 | "webpack": "^4.44.1", 391 | "webpack-cli": "^3.3.12" 392 | } 393 | } 394 | -------------------------------------------------------------------------------- /src/netsuite-sdf.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | 3 | import * as fs from 'fs-extra'; 4 | import * as path from 'path'; 5 | import * as util from 'util'; 6 | 7 | import * as _ from 'lodash'; 8 | import * as glob from 'glob'; 9 | import * as rimraf from 'rimraf'; 10 | import * as tmp from 'tmp'; 11 | import * as xml2js from 'xml2js'; 12 | import { Observable, Subject } from 'rxjs/Rx'; 13 | import 'rxjs/add/operator/do'; 14 | import 'rxjs/add/operator/filter'; 15 | import 'rxjs/add/operator/map'; 16 | import 'rxjs/add/operator/toPromise'; 17 | 18 | import { spawn } from 'spawn-rx'; 19 | 20 | import { Environment } from './environment'; 21 | import { SDFConfig } from './sdf-config'; 22 | import { SdfCliJson } from './sdf-cli-json'; 23 | import { CLICommand } from './cli-command'; 24 | import { CustomObjects, CustomObject } from './custom-object'; 25 | 26 | const Bluebird = require('bluebird'); 27 | const globAsync = util.promisify(glob); 28 | 29 | export class NetSuiteSDF { 30 | activeEnvironment: Environment; 31 | addDefaultParameters = true; 32 | collectedData: string[] = []; 33 | currentObject: CustomObject; 34 | doAddProjectParameter = true; 35 | doReturnData = false; 36 | doSendPassword = true; 37 | doShowOutput = true; 38 | intervalId; 39 | outputChannel: vscode.OutputChannel; 40 | password: string; 41 | rootPath: string; 42 | savedStatus: string; 43 | sdfcli: any; 44 | sdfConfig: SDFConfig; 45 | sdfCliIsInstalled = true; // Prevents error messages while Code is testing SDFCLI is installed. 46 | statusBar: vscode.StatusBarItem; 47 | tempDir: tmp.SynchrounousResult; 48 | hasSdfCache: boolean; 49 | xmlBuilder = new xml2js.Builder({ headless: true }); 50 | 51 | constructor(private context: vscode.ExtensionContext) { 52 | this.checkSdfCliIsInstalled().then(() => { 53 | if (this.sdfCliIsInstalled) { 54 | this.initializeStatusBar(); 55 | this.outputChannel = vscode.window.createOutputChannel('SDF'); 56 | } 57 | }); 58 | } 59 | 60 | private initializeStatusBar() { 61 | this.statusBar = vscode.window.createStatusBarItem(); 62 | this.statusBar.text = this.statusBarDefault; 63 | this.statusBar.tooltip = 'Click here to select your NetSuite environment'; 64 | this.statusBar.command = 'extension.selectEnvironment'; 65 | this.statusBar.show(); 66 | vscode.window.showWarningMessage( 67 | '!!! Starting January 1st, the NetSuiteSDF plugin will have a major update, you will need to ' + 68 | 'install suitecloud-cli and refactor your project layout to use this plugin. !!!' 69 | ); 70 | } 71 | 72 | get statusBarDefault() { 73 | if (this.activeEnvironment) { 74 | return `SDF (${this.activeEnvironment.name})`; 75 | } else { 76 | return 'SDF'; 77 | } 78 | } 79 | 80 | /*********************/ 81 | /** SDF CLI Commands */ 82 | /*********************/ 83 | 84 | async addDependencies() { 85 | if (!this.sdfCliIsInstalled) { 86 | vscode.window.showErrorMessage("'sdfcli' not found in path. Please restart VS Code if you installed it."); 87 | return; 88 | } 89 | 90 | this.doSendPassword = false; 91 | this.addDefaultParameters = false; 92 | 93 | await this.getConfig(); 94 | const projectName = this.sdfConfig.projectName || 'PROJECT_NAME_MISSING'; 95 | const defaultXml = ` 96 | 97 | ${projectName} 98 | 1.0 99 | 100 | `; 101 | fs.writeFile(path.join(this.rootPath, 'manifest.xml'), defaultXml, function (err) { 102 | if (err) throw err; 103 | }); 104 | await this.runCommand(CLICommand.AddDependencies, '-all'); 105 | } 106 | 107 | async createProject() { 108 | if (!this.sdfCliIsInstalled) { 109 | vscode.window.showErrorMessage("'sdfcli' not found in path. Please restart VS Code if you installed it."); 110 | return; 111 | } 112 | 113 | this.doSendPassword = false; 114 | this.addDefaultParameters = false; 115 | 116 | const pathPrompt = `Please enter your the parent directory to create the Project in`; 117 | const outputPath = await vscode.window.showInputBox({ 118 | prompt: pathPrompt, 119 | ignoreFocusOut: true, 120 | }); 121 | if (outputPath) { 122 | const projectNamePrompt = `Please enter your project's name`; 123 | const projectName = await vscode.window.showInputBox({ 124 | prompt: projectNamePrompt, 125 | ignoreFocusOut: true, 126 | }); 127 | if (projectName) { 128 | await this.runCommand( 129 | CLICommand.CreateProject, 130 | `-pd ${outputPath}`, 131 | `-pn ${projectName}`, 132 | '-t ACCOUNTCUSTOMIZATION' 133 | ); 134 | } 135 | } 136 | } 137 | 138 | async _generateTempDeployDirectory() { 139 | const deployPath = path.join(this.rootPath, 'deploy.xml'); 140 | const deployXmlExists = await this.fileExists(deployPath); 141 | if (!deployXmlExists) { 142 | this.setDefaultDeployXml(); 143 | } 144 | const deployXml = await this.openFile(deployPath); 145 | const deployJs = await this.parseXml(deployXml); 146 | 147 | const files = _.get(deployJs, 'deploy.files[0].path', []); 148 | const objects = _.get(deployJs, 'deploy.objects[0].path', []); 149 | const allFilePatterns = files.concat(objects).concat(['/deploy.xml', '/manifest.xml', '/.sdf']); 150 | const allFilePaths = await this.getFilePaths(allFilePatterns); 151 | 152 | this.tempDir = tmp.dirSync({ unsafeCleanup: true, keep: false }); 153 | try { 154 | for (let filepath of allFilePaths) { 155 | const fromPath = path.join(filepath); 156 | const toPath = path.join(this.tempDir.name, filepath); 157 | await this.copyFile(fromPath, toPath); 158 | } 159 | } catch (e) { 160 | console.log(e); 161 | } 162 | } 163 | 164 | async deploy() { 165 | if (!this.sdfCliIsInstalled) { 166 | vscode.window.showErrorMessage("'sdfcli' not found in path. Please restart VS Code if you installed it."); 167 | return; 168 | } 169 | await this.getConfig(); 170 | 171 | let config = vscode.workspace.getConfiguration('netsuitesdf'); 172 | const useQuickDeploy = config.get('useQuickDeploy'); 173 | if (useQuickDeploy) { 174 | await this._generateTempDeployDirectory(); 175 | 176 | await this.runCommand(CLICommand.Deploy); 177 | 178 | await rimraf(this.rootPath + '/var', (err: Error) => { 179 | vscode.window.showErrorMessage(err.message); 180 | }); 181 | } else { 182 | await this.runCommand(CLICommand.Deploy); 183 | } 184 | } 185 | 186 | importBundle() { 187 | if (!this.sdfCliIsInstalled) { 188 | vscode.window.showErrorMessage("'sdfcli' not found in path. Please restart VS Code if you installed it."); 189 | return; 190 | } 191 | 192 | // TODO? 193 | this.doAddProjectParameter = false; 194 | this.runCommand(CLICommand.ImportBundle); 195 | } 196 | 197 | // TODO 198 | // importConfiguration 199 | 200 | async importFiles() { 201 | if (!this.sdfCliIsInstalled) { 202 | vscode.window.showErrorMessage("'sdfcli' not found in path. Please restart VS Code if you installed it."); 203 | return; 204 | } 205 | 206 | this.doAddProjectParameter = false; 207 | this.doReturnData = true; 208 | 209 | const collectedData = await this.listFiles(); 210 | if (collectedData) { 211 | const filteredData = collectedData.filter((data) => data.indexOf('SuiteScripts') >= 0); 212 | if (filteredData.length > 0) { 213 | const selectedFiles = await vscode.window.showQuickPick(filteredData, { 214 | canPickMany: true, 215 | ignoreFocusOut: true, 216 | }); 217 | if (selectedFiles && selectedFiles.length > 0) { 218 | this._importFiles(selectedFiles); 219 | } 220 | } 221 | } 222 | } 223 | 224 | async _importFiles(files: string[]) { 225 | const cleanedFiles = _.map(files, (file) => `${file}`); 226 | const fileString = cleanedFiles.join(' '); 227 | return this.runCommand(CLICommand.ImportFiles, `-paths`, `${fileString}`); 228 | } 229 | 230 | async importObjects(context?: any) { 231 | if (!this.sdfCliIsInstalled) { 232 | vscode.window.showErrorMessage("'sdfcli' not found in path. Please restart VS Code if you installed it."); 233 | return; 234 | } 235 | 236 | const collectedData = await this.listObjects(); 237 | if (collectedData) { 238 | const filteredData = collectedData.filter((data) => data.indexOf('cust') >= 0); 239 | if (filteredData.length > 0) { 240 | const selectedObjects = await vscode.window.showQuickPick(filteredData, { 241 | canPickMany: true, 242 | ignoreFocusOut: true, 243 | }); 244 | if (selectedObjects && selectedObjects.length > 0) { 245 | this.createPath(this.currentObject.destination); 246 | this._importObjects(this.currentObject.type, selectedObjects, this.currentObject.destination); 247 | } 248 | } 249 | } 250 | } 251 | 252 | async _importObjects(scriptType: string, scriptIds: string[], destination: string) { 253 | this.createPath(destination); 254 | const scriptIdString = scriptIds.join(' '); 255 | return this.runCommand( 256 | CLICommand.ImportObjects, 257 | `-scriptid`, 258 | scriptIdString, 259 | `-type`, 260 | `${scriptType}`, 261 | `-destinationfolder`, 262 | `${destination}` 263 | ); 264 | } 265 | 266 | issueToken() { 267 | if (!this.sdfCliIsInstalled) { 268 | vscode.window.showErrorMessage("'sdfcli' not found in path. Please restart VS Code if you installed it."); 269 | return; 270 | } 271 | 272 | this.doAddProjectParameter = false; 273 | this.runCommand(CLICommand.IssueToken); 274 | } 275 | 276 | listBundles() { 277 | if (!this.sdfCliIsInstalled) { 278 | vscode.window.showErrorMessage("'sdfcli' not found in path. Please restart VS Code if you installed it."); 279 | return; 280 | } 281 | 282 | this.doAddProjectParameter = false; 283 | this.runCommand(CLICommand.ListBundles); 284 | } 285 | 286 | listConfiguration() { 287 | if (!this.sdfCliIsInstalled) { 288 | vscode.window.showErrorMessage("'sdfcli' not found in path. Please restart VS Code if you installed it."); 289 | return; 290 | } 291 | 292 | this.doAddProjectParameter = false; 293 | this.runCommand(CLICommand.ListConfiguration); 294 | } 295 | 296 | listFiles() { 297 | if (!this.sdfCliIsInstalled) { 298 | vscode.window.showErrorMessage("'sdfcli' not found in path. Please restart VS Code if you installed it."); 299 | return; 300 | } 301 | 302 | this.doAddProjectParameter = false; 303 | return this.runCommand(CLICommand.ListFiles, `-folder`, `/SuiteScripts`); 304 | } 305 | 306 | listMissingDependencies() { 307 | if (!this.sdfCliIsInstalled) { 308 | vscode.window.showErrorMessage("'sdfcli' not found in path. Please restart VS Code if you installed it."); 309 | return; 310 | } 311 | 312 | this.doSendPassword = false; 313 | this.runCommand(CLICommand.ListMissingDependencies); 314 | } 315 | 316 | async listObjects() { 317 | if (!this.sdfCliIsInstalled) { 318 | vscode.window.showErrorMessage("'sdfcli' not found in path. Please restart VS Code if you installed it."); 319 | return; 320 | } 321 | 322 | this.doAddProjectParameter = false; 323 | this.doReturnData = true; 324 | 325 | await this.getConfig(); 326 | if (this.sdfConfig) { 327 | this.currentObject = await vscode.window.showQuickPick(CustomObjects, { 328 | ignoreFocusOut: true, 329 | }); 330 | if (this.currentObject) { 331 | return this.runCommand(CLICommand.ListObjects, `-type`, `${this.currentObject.type}`); 332 | } 333 | } 334 | } 335 | 336 | preview() { 337 | if (!this.sdfCliIsInstalled) { 338 | vscode.window.showErrorMessage("'sdfcli' not found in path. Please restart VS Code if you installed it."); 339 | return; 340 | } 341 | 342 | this.runCommand(CLICommand.Preview); 343 | } 344 | 345 | revokeToken() { 346 | if (!this.sdfCliIsInstalled) { 347 | vscode.window.showErrorMessage("'sdfcli' not found in path. Please restart VS Code if you installed it."); 348 | return; 349 | } 350 | 351 | this.doAddProjectParameter = false; 352 | this.runCommand(CLICommand.RevokeToken); 353 | } 354 | 355 | async saveToken() { 356 | if (!this.sdfCliIsInstalled) { 357 | vscode.window.showErrorMessage("'sdfcli' not found in path. Please restart VS Code if you installed it."); 358 | return; 359 | } 360 | const authid = await vscode.window.showInputBox({ 361 | prompt: 'Please enter a unique authid to tie your token keys and secrets.', 362 | ignoreFocusOut: true, 363 | }); 364 | 365 | const account = await vscode.window.showInputBox({ 366 | prompt: 'Please enter your account associated with your token keys and secrets.', 367 | ignoreFocusOut: true, 368 | }); 369 | 370 | const tokenKey = await vscode.window.showInputBox({ 371 | prompt: 'Please enter your token key associated with your SuiteCloud IDE & CLI integration:', 372 | ignoreFocusOut: true, 373 | }); 374 | if (tokenKey) { 375 | const tokenSecret = await vscode.window.showInputBox({ 376 | prompt: 'Please enter your token secret associated with your SuiteCloud IDE & CLI integration:', 377 | ignoreFocusOut: true, 378 | }); 379 | if (tokenSecret) { 380 | this.doAddProjectParameter = false; 381 | this.addDefaultParameters = false; 382 | this.runCommand( 383 | CLICommand.SaveToken, 384 | `-account`, 385 | `${account}`, 386 | `-authid`, 387 | `${authid}`, 388 | `-savetoken`, 389 | `-tokenid`, 390 | `${tokenKey}`, 391 | `-tokensecret`, 392 | `${tokenSecret}` 393 | ); 394 | } 395 | } 396 | } 397 | 398 | async getFiles() { 399 | await this.getConfig(); 400 | this.doAddProjectParameter = true; 401 | if (this.sdfConfig) { 402 | const files = await this.listFiles(); 403 | if (files) { 404 | vscode.window.showInformationMessage('Synchronizing SuiteScript folder.'); 405 | await this._importFiles(files); 406 | } 407 | } else { 408 | return; 409 | } 410 | } 411 | 412 | async getObjectFunc(object: CustomObject) { 413 | await this.getConfig(); 414 | // Ephermeral data customizations should not be supported at this time. 415 | if ( 416 | object.type === 'savedsearch' || 417 | object.type === 'csvimport' || 418 | object.type === 'dataset' || 419 | object.type === 'financiallayout' || 420 | object.type === 'reportdefinition' || 421 | object.type === 'translationcollection' || 422 | object.type === 'workbook' 423 | ) 424 | return; 425 | 426 | this.doAddProjectParameter = true; 427 | if (this.sdfConfig) { 428 | await this._importObjects(object.type, ['ALL'], object.destination); 429 | } else { 430 | return; 431 | } 432 | } 433 | 434 | async update() { 435 | if (!this.sdfCliIsInstalled) { 436 | vscode.window.showErrorMessage("'sdfcli' not found in path. Please restart VS Code if you installed it."); 437 | return; 438 | } 439 | 440 | await this.getConfig(); 441 | const objectsRecordPath = path.join(this.rootPath, 'Objects'); 442 | const pathExists = await this.fileExists(objectsRecordPath); 443 | 444 | if (pathExists) { 445 | const filePathList = await this.getXMLFileList(['Objects'], this.rootPath); 446 | 447 | if (filePathList.length > 0) { 448 | const shortNames = filePathList.map((file) => file.path.substr(file.path.indexOf('Objects') + 8)); 449 | const selectionArr = await vscode.window.showQuickPick(shortNames, { 450 | canPickMany: true, 451 | }); 452 | 453 | if (selectionArr && selectionArr.length > 0) { 454 | const selectedFile = filePathList.filter((file) => { 455 | for (const selection of selectionArr) { 456 | if (file.path.indexOf(selection) >= 0) { 457 | return true; 458 | } 459 | } 460 | }); 461 | const selectionStr = selectedFile 462 | .map((file) => file.scriptid.substring(0, file.scriptid.indexOf('.'))) 463 | .join(' '); 464 | this.runCommand(CLICommand.Update, `-scriptid`, `${selectionStr}`); 465 | } 466 | } 467 | } 468 | } 469 | 470 | async updateCustomRecordWithInstances() { 471 | if (!this.sdfCliIsInstalled) { 472 | vscode.window.showErrorMessage("'sdfcli' not found in path. Please restart VS Code if you installed it."); 473 | return; 474 | } 475 | 476 | await this.getConfig(); 477 | const customRecordPath = path.join(this.rootPath, '/Objects/Records'); 478 | const pathExists = await this.fileExists(customRecordPath); 479 | if (pathExists) { 480 | const rawFileList = await this.ls(customRecordPath); 481 | const fileList = rawFileList.map((filename: string) => filename.slice(0, -4)); 482 | 483 | if (fileList) { 484 | const objectId = await vscode.window.showQuickPick(fileList, { 485 | ignoreFocusOut: true, 486 | }); 487 | if (objectId) { 488 | this.runCommand(CLICommand.UpdateCustomRecordsWithInstances, `-scriptid`, `${objectId}`); 489 | } 490 | } 491 | } else { 492 | vscode.window.showErrorMessage( 493 | 'No custom records found in /Objects/Records. Import Objects before updating with custom records.' 494 | ); 495 | } 496 | } 497 | 498 | validate() { 499 | if (!this.sdfCliIsInstalled) { 500 | vscode.window.showErrorMessage("'sdfcli' not found in path. Please restart VS Code if you installed it."); 501 | return; 502 | } 503 | 504 | this.runCommand(CLICommand.Validate); 505 | } 506 | 507 | /************************/ 508 | /** Extension Commands **/ 509 | /************************/ 510 | 511 | async createResetDeploy(context?: any) { 512 | await this.getConfig(); 513 | this.setDefaultDeployXml(); 514 | vscode.window.showInformationMessage('Reset deploy.xml.'); 515 | } 516 | 517 | async backupRestoreDeploy(context?: any) { 518 | if (context && context.scheme !== 'file') { 519 | vscode.window.showWarningMessage(`Unknown file type '${context.scheme}' to backup/restore.`); 520 | return; 521 | } 522 | await this.getConfig(); 523 | 524 | let currentFile: string; 525 | if (context && context.fsPath) { 526 | currentFile = fs.lstatSync(context.fsPath).isDirectory() ? `${context.fsPath}${path.sep}*` : context.fsPath; 527 | } else { 528 | currentFile = vscode.window.activeTextEditor.document.fileName; 529 | } 530 | 531 | const currentFileName = path.basename(currentFile); 532 | const isDeployXML = _.includes(currentFileName, 'deploy.xml'); 533 | if (!isDeployXML) { 534 | vscode.window.showErrorMessage('File does not appear to be a valid deploy.xml file.'); 535 | return; 536 | } 537 | 538 | let config = vscode.workspace.getConfiguration('netsuitesdf'); 539 | const onBackupResetDeployXML = config.get('onBackupResetDeployXML'); 540 | const onRestoreDeleteBackupDeployXML = config.get('onRestoreDeleteBackupDeployXML'); 541 | 542 | if (currentFileName === 'deploy.xml') { 543 | const prompt = 544 | 'Enter filename prefix (i.e. PREFIX.deploy.xml). Entering no value will use current date and time.'; 545 | let filenamePrefix = await vscode.window.showInputBox({ 546 | prompt: prompt, 547 | ignoreFocusOut: true, 548 | }); 549 | const now = new Date(); 550 | filenamePrefix = 551 | filenamePrefix || 552 | `${now.toISOString().slice(0, 10).replace(/-/g, '')}_${('0' + now.getHours()).slice(-2)}${( 553 | '0' + now.getMinutes() 554 | ).slice(-2)}${('0' + now.getSeconds()).slice(-2)}`; 555 | await fs.copyFile(path.join(this.rootPath, 'deploy.xml'), path.join(this.rootPath, `${filenamePrefix}.deploy.xml`)); 556 | if (onBackupResetDeployXML) { 557 | await this.createResetDeploy(context); 558 | } 559 | vscode.window.showInformationMessage(`Backed up deploy.xml to ${filenamePrefix}.deploy.xml`); 560 | } else { 561 | let answer: string; 562 | const deployXMLExists = await fs.pathExists(path.join(this.rootPath, 'deploy.xml')); 563 | if (deployXMLExists) { 564 | const prompt = 'Deploy.xml already exists. Type OK to overwrite the existing file.'; 565 | answer = await vscode.window.showInputBox({ 566 | prompt: prompt, 567 | ignoreFocusOut: true, 568 | }); 569 | } else answer = 'OK'; 570 | if (answer === 'OK') { 571 | await fs.copyFile(currentFile, path.join(this.rootPath, 'deploy.xml')); 572 | if (onRestoreDeleteBackupDeployXML) { 573 | await fs.remove(currentFile); 574 | } 575 | vscode.window.showInformationMessage(`Restored ${currentFileName} to deploy.xml`); 576 | } 577 | } 578 | } 579 | 580 | async addFileToDeploy(context?: any) { 581 | if (context && context.scheme !== 'file') { 582 | vscode.window.showWarningMessage(`Unknown file type '${context.scheme}' to add to deploy.xml`); 583 | return; 584 | } 585 | await this.getConfig(); 586 | const deployPath = path.join(this.rootPath, 'deploy.xml'); 587 | 588 | let currentFile: string; 589 | if (context && context.fsPath) { 590 | currentFile = fs.lstatSync(context.fsPath).isDirectory() ? `${context.fsPath}${path.sep}*` : context.fsPath; 591 | } else { 592 | currentFile = vscode.window.activeTextEditor.document.fileName; 593 | } 594 | 595 | let config = vscode.workspace.getConfiguration('netsuitesdf'); 596 | const addMatchingJSWhenAddingTSToDeployXML = config.get('addMatchingJavascriptWhenAddingTypescriptToDeployXML'); 597 | 598 | const isFileInFileCabinet = _.includes(currentFile, path.join(this.rootPath, '/FileCabinet/SuiteScripts')); 599 | let isJavaScript = isFileInFileCabinet && _.includes(currentFile, '.js'); 600 | const isTypeScript = _.includes(currentFile, '.ts'); 601 | const isObject = _.includes(currentFile, path.join(this.rootPath, '/Objects')) && _.includes(currentFile, '.xml'); 602 | let matchedJavaScriptFile: string; 603 | 604 | if (!isFileInFileCabinet && !isJavaScript && !isObject) { 605 | if (isTypeScript && addMatchingJSWhenAddingTSToDeployXML) { 606 | const matchedJavaScriptFiles: string[] = []; 607 | const currentFileName = path.basename(currentFile); 608 | 609 | const getFiles = async (dir: string): Promise => { 610 | const subdirs = (await util.promisify(fs.readdir)(dir)) as string[]; 611 | const f = await Promise.all( 612 | subdirs.map(async (subdir) => { 613 | const res = path.resolve(dir, subdir); 614 | return (await fs.stat(res)).isDirectory() ? getFiles(res) : res; 615 | }) 616 | ); 617 | return Array.prototype.concat.apply([], f); 618 | }; 619 | 620 | const files: string[] = await getFiles(path.join(this.rootPath, '/FileCabinet/SuiteScripts')); 621 | for (const file of files) { 622 | const fileName = path.basename(file); 623 | if (fileName.replace(/\.[^/.]+$/, '') === currentFileName.replace(/\.[^/.]+$/, '')) { 624 | matchedJavaScriptFiles.push(file); 625 | } 626 | } 627 | 628 | if (matchedJavaScriptFiles.length) { 629 | isJavaScript = true; 630 | const currentFileParentDir = path.basename(path.dirname(currentFile)); 631 | for (const file of matchedJavaScriptFiles) { 632 | const fileParentDir = path.basename(path.dirname(file)); 633 | if (fileParentDir === currentFileParentDir) { 634 | matchedJavaScriptFile = file; 635 | break; 636 | } 637 | } 638 | if (!matchedJavaScriptFile && matchedJavaScriptFiles.length === 1) 639 | matchedJavaScriptFile = matchedJavaScriptFiles[0]; 640 | if (matchedJavaScriptFile) currentFile = matchedJavaScriptFile; 641 | else { 642 | vscode.window.showErrorMessage( 643 | 'No matching compiled JavaScript file found in FileCabinet/SuiteScripts/**.' 644 | ); 645 | return; 646 | } 647 | } else { 648 | vscode.window.showErrorMessage('No matching compiled JavaScript file found in FileCabinet/SuiteScripts/**.'); 649 | return; 650 | } 651 | } else { 652 | vscode.window.showErrorMessage('Invalid file to add to deploy.xml. File is not a Script or an Object.'); 653 | return; 654 | } 655 | } 656 | 657 | const xmlPath = isFileInFileCabinet || isJavaScript ? 'deploy.files[0].path' : 'deploy.objects[0].path'; 658 | const relativePath = _.replace(currentFile, this.rootPath, '~').replace(/\\/gi, '/'); 659 | 660 | const deployXmlExists = await this.fileExists(deployPath); 661 | if (!deployXmlExists) { 662 | this.setDefaultDeployXml(); 663 | } 664 | const deployXml = await this.openFile(deployPath); 665 | const deployJs = await this.parseXml(deployXml); 666 | const elements = _.get(deployJs, xmlPath, []); 667 | if (_.includes(elements, relativePath)) { 668 | vscode.window.showInformationMessage(`${isObject ? 'Object' : 'File'} already exists in deploy.xml.`); 669 | } else { 670 | elements.push(relativePath); 671 | _.set(deployJs, xmlPath, elements); 672 | 673 | const newXml = this.xmlBuilder.buildObject(deployJs); 674 | fs.writeFile(deployPath, newXml, function (err) { 675 | if (err) throw err; 676 | vscode.window.showInformationMessage( 677 | `Added ${matchedJavaScriptFile ? 'matching compiled JavaScript' : ''} ${ 678 | isObject ? 'object' : 'file' 679 | } to deploy.xml.` 680 | ); 681 | }); 682 | } 683 | } 684 | 685 | refreshConfig() { 686 | this.getConfig({ force: true }); 687 | } 688 | 689 | async removeFolders() { 690 | await this.getConfig(); 691 | 692 | if (this.sdfConfig) { 693 | vscode.window.showInformationMessage('Emptying: ' + this.rootPath + '/Objects/'); 694 | await rimraf(this.rootPath + '/Objects/*', (err: Error) => { 695 | vscode.window.showErrorMessage(err.message); 696 | }); 697 | vscode.window.showInformationMessage('Emptying: ' + this.rootPath + '/FileCabinet/SuiteScripts/'); 698 | await rimraf(this.rootPath + '/FileCabinet/SuiteScripts/*', (err: Error) => { 699 | vscode.window.showErrorMessage(err.message); 700 | }); 701 | } 702 | } 703 | 704 | async resetPassword() { 705 | if (!this.sdfCliIsInstalled) { 706 | vscode.window.showErrorMessage("'sdfcli' not found in path. Please restart VS Code if you installed it."); 707 | return; 708 | } 709 | 710 | const _resetPassword = async () => { 711 | const prompt = `Please enter your password for your ${this.activeEnvironment.name} account.`; 712 | const password = await vscode.window.showInputBox({ 713 | prompt: prompt, 714 | password: true, 715 | ignoreFocusOut: true, 716 | }); 717 | this.password = password; 718 | }; 719 | 720 | if (this.sdfConfig) { 721 | await _resetPassword(); 722 | } else { 723 | await this.getConfig({ force: true }); 724 | await _resetPassword(); 725 | } 726 | } 727 | 728 | async selectEnvironment() { 729 | if (!this.sdfCliIsInstalled) { 730 | vscode.window.showErrorMessage("'sdfcli' not found in path. Please restart VS Code if you installed it."); 731 | return; 732 | } 733 | 734 | const _selectEnvironment = async () => { 735 | try { 736 | const environments = this.sdfConfig.environments.reduce((acc, curr: Environment) => { 737 | acc[curr.name] = curr; 738 | return acc; 739 | }, {}); 740 | const environmentNames = Object.keys(environments); 741 | if (environmentNames.length === 1) { 742 | const environmentName = environmentNames[0]; 743 | this.activeEnvironment = environments[environmentName]; 744 | this.statusBar.text = this.statusBarDefault; 745 | vscode.window.showInformationMessage(`Found only one environment. Using ${environmentName}`); 746 | } else { 747 | const environmentName = await vscode.window.showQuickPick(environmentNames, { ignoreFocusOut: true }); 748 | if (environmentName) { 749 | this.activeEnvironment = environments[environmentName]; 750 | if (this.activeEnvironment.account === '00000000') { 751 | vscode.window.showErrorMessage( 752 | '.sdfcli.json account number appears to be wrong. Are you still using the blank template?' 753 | ); 754 | this.sdfConfig = undefined; 755 | this.activeEnvironment = undefined; 756 | this.clearStatus(); 757 | } else { 758 | this.statusBar.text = this.statusBarDefault; 759 | } 760 | } 761 | } 762 | } catch (e) { 763 | vscode.window.showErrorMessage( 764 | 'Unable to parse .sdfcli.json environments. Please check repo for .sdfcli.json formatting.' 765 | ); 766 | this.clearStatus(); 767 | } 768 | }; 769 | 770 | if (this.sdfConfig) { 771 | await _selectEnvironment(); 772 | } else { 773 | await this.getConfig({ force: true }); 774 | } 775 | } 776 | 777 | setDefaultDeployXml() { 778 | const defaultXml = ``; 779 | fs.writeFile(path.join(this.rootPath, 'deploy.xml'), defaultXml, function (err) { 780 | if (err) throw err; 781 | }); 782 | } 783 | 784 | async sync() { 785 | if (!this.sdfCliIsInstalled) { 786 | vscode.window.showErrorMessage("'sdfcli' not found in path. Please restart VS Code if you installed it."); 787 | return; 788 | } 789 | const prompt = 'Warning! Syncing to NetSuite will delete File Cabinet and Object contents. Type OK to proceed.'; 790 | const answer = await vscode.window.showInputBox({ 791 | prompt: prompt, 792 | ignoreFocusOut: true, 793 | }); 794 | if (answer === 'OK') { 795 | vscode.window.showInformationMessage('Beginning sync.'); 796 | } else { 797 | this.outputChannel.append('Cancelling sync.\n'); 798 | return; 799 | } 800 | 801 | await this.getConfig(); 802 | await this.removeFolders(); 803 | try { 804 | if (this.sdfConfig) { 805 | await Bluebird.map([this.getFiles.bind(this)], (func) => func(), { concurrency: 5 }); 806 | const objectCommands = _.map(CustomObjects, (object: CustomObject) => this.getObjectFunc(object)); 807 | await Bluebird.map([objectCommands], (func) => func(), { concurrency: 5 }); 808 | 809 | vscode.window.showInformationMessage('Synchronization complete!'); 810 | } 811 | } catch (e) { 812 | } finally { 813 | this.cleanup(); 814 | } 815 | } 816 | 817 | /*********************/ 818 | /** VS Code Helpers **/ 819 | /*********************/ 820 | 821 | async checkSdfCliIsInstalled() { 822 | try { 823 | // Don't like this. There must be a better way. 824 | await spawn('sdfcli').toPromise(); 825 | this.sdfCliIsInstalled = true; 826 | } catch (e) { 827 | this.sdfCliIsInstalled = false; 828 | if (e.code === 'ENOENT') { 829 | vscode.window.showErrorMessage("'sdfcli' not found in path! Check repo for install directions."); 830 | } else { 831 | throw e; 832 | } 833 | } 834 | } 835 | 836 | cleanup() { 837 | // Clean up default instance variables (or other matters) after thread closes. 838 | if (!this.doReturnData) { 839 | this.collectedData = []; 840 | this.currentObject = undefined; 841 | } 842 | clearInterval(this.intervalId); 843 | this.clearStatus(); 844 | 845 | this.doAddProjectParameter = true; 846 | this.doReturnData = false; 847 | this.doSendPassword = true; 848 | this.intervalId = undefined; 849 | this.sdfcli = undefined; 850 | this.doShowOutput = true; 851 | if (this.tempDir && this.tempDir.name !== '') { 852 | this.tempDir.removeCallback(); 853 | } 854 | this.tempDir = undefined; 855 | this.addDefaultParameters = true; 856 | } 857 | 858 | clearStatus() { 859 | if (this.savedStatus) { 860 | this.statusBar.text = this.savedStatus; 861 | this.savedStatus = undefined; 862 | } else { 863 | this.statusBar.text = this.statusBarDefault; 864 | } 865 | } 866 | 867 | async getConfig({ force = false }: { force?: boolean } = {}) { 868 | if (!this.sdfCliIsInstalled) { 869 | vscode.window.showErrorMessage("'sdfcli' not found in path. Please restart VS Code if you installed it."); 870 | return; 871 | } 872 | 873 | if (force || !this.sdfConfig) { 874 | const workspaceFolders = vscode.workspace.workspaceFolders; 875 | if (workspaceFolders) { 876 | this.rootPath = workspaceFolders[0].uri.fsPath; 877 | 878 | const sdfTokenPath = path.join(this.rootPath, '.clicache'); 879 | const sdfCacheExists = await this.fileExists(sdfTokenPath); 880 | 881 | if (sdfCacheExists) { 882 | this.hasSdfCache = true; 883 | } 884 | 885 | const sdfPath = path.join(this.rootPath, '.sdfcli.json'); 886 | const sdfPathExists = await this.fileExists(sdfPath); 887 | if (sdfPathExists) { 888 | const buffer = await this.openFile(path.join(this.rootPath, '.sdfcli.json')); 889 | const jsonString = buffer.toString(); 890 | try { 891 | this.sdfConfig = JSON.parse(jsonString); 892 | await this.selectEnvironment(); 893 | } catch (e) { 894 | vscode.window.showErrorMessage(`Unable to parse .sdfcli.json file found at project root: ${this.rootPath}`); 895 | } 896 | } else { 897 | fs.writeFileSync(path.join(this.rootPath, '.sdfcli.json'), SdfCliJson); 898 | vscode.window.showErrorMessage( 899 | `No .sdfcli.json file found at project root: ${this.rootPath}. Generated a blank .sdfcli.json template.` 900 | ); 901 | } 902 | } else { 903 | vscode.window.showErrorMessage( 904 | 'No workspace folder found. SDF plugin cannot work without a workspace folder root containing a .sdfcli.json file.' 905 | ); 906 | } 907 | } else if (!this.activeEnvironment) { 908 | await this.selectEnvironment(); 909 | } 910 | } 911 | 912 | handlePassword(line: string, command: CLICommand, stdinSubject: Subject) { 913 | if (line.startsWith('Enter password:')) { 914 | line = line.substring(15); 915 | } 916 | if (line.includes('You have entered an invalid email address or password. Please try again.')) { 917 | this.password = undefined; 918 | vscode.window.showErrorMessage('Invalid email or password. Be careful! Too many attempts will lock you out!'); 919 | } 920 | return line; 921 | } 922 | 923 | async handleStdIn(line: string, command: CLICommand, stdinSubject: Subject) { 924 | switch (true) { 925 | case line.includes('Using user credentials.') && this.doSendPassword: 926 | if (!this.password) { 927 | await this.resetPassword(); 928 | } 929 | stdinSubject.next(`${this.password}\n`); 930 | break; 931 | case line.includes('WARNING! You are deploying to a Production account, enter YES to continue'): 932 | const prompt = "Please type 'Deploy' to deploy to production."; 933 | const answer = await vscode.window.showInputBox({ 934 | prompt: prompt, 935 | ignoreFocusOut: true, 936 | }); 937 | if (answer === 'Deploy') { 938 | stdinSubject.next('YES\n'); 939 | } else { 940 | this.outputChannel.append('Cancelling deployment.\n'); 941 | stdinSubject.next('NO\n'); 942 | } 943 | break; 944 | case line.includes('Type YES to continue'): 945 | case line.includes('enter YES to continue'): 946 | case line.includes('Type YES to update the manifest file'): 947 | case line.includes('Proceed with deploy?'): 948 | case line.includes('Type Yes (Y) to continue.'): 949 | stdinSubject.next('YES\n'); 950 | break; 951 | default: 952 | break; 953 | } 954 | } 955 | 956 | async handleStdOut(line: string, command: CLICommand) { 957 | switch (true) { 958 | case line.includes('That record does not exist.'): 959 | break; 960 | case line.includes('does not exist.'): 961 | vscode.window.showErrorMessage('Custom record does not exist for updating. Please Import Object first.'); 962 | break; 963 | case line.includes('Installation COMPLETE'): 964 | vscode.window.showInformationMessage('Installation of deployment was completed.'); 965 | break; 966 | default: 967 | break; 968 | } 969 | } 970 | 971 | mapCommandOutput(command: CLICommand, line: string) { 972 | switch (command) { 973 | case CLICommand.ListObjects: 974 | return line.includes(':') ? line.split(':')[1] : line; 975 | default: 976 | return line; 977 | } 978 | } 979 | 980 | async runCommand(command: CLICommand, ...args): Promise { 981 | await this.getConfig(); 982 | if (this.sdfConfig && this.activeEnvironment) { 983 | const workspaceFolders = vscode.workspace.workspaceFolders; 984 | if (this.doShowOutput) { 985 | this.outputChannel.show(); 986 | } 987 | 988 | let workPath = this.rootPath; 989 | if (this.tempDir) { 990 | workPath = path.join(workPath, this.tempDir.name); 991 | } 992 | 993 | let commandArray: string[] = [command]; 994 | if (this.addDefaultParameters) { 995 | commandArray = commandArray.concat([ 996 | /** 997 | * As of 2020.2, this plugin will no longer support 998 | * username and password for authentication. 999 | * Please use the sdfcli manageauth -savetoken option 1000 | * to add accounts to your environment. 1001 | */ 1002 | `-authid`, 1003 | `${this.activeEnvironment.authid}`, 1004 | ]); 1005 | } 1006 | 1007 | if (this.doAddProjectParameter) { 1008 | commandArray.push(`-p`, `${workPath}`); 1009 | } 1010 | for (let arg of args) { 1011 | let argArray = arg.split(' '); 1012 | argArray.map((a) => commandArray.push(`${a}`)); 1013 | } 1014 | 1015 | const stdinSubject = new Subject(); 1016 | 1017 | this.sdfcli = spawn('sdfcli', commandArray, { 1018 | cwd: workPath, 1019 | stdin: stdinSubject, 1020 | windowsVerbatimArguments: true, 1021 | }); 1022 | 1023 | this.showStatus(); 1024 | 1025 | let streamWrapper: Observable = new Observable((observer) => { 1026 | let acc = ''; 1027 | 1028 | return this.sdfcli.subscribe( 1029 | (value) => { 1030 | acc = acc + value; 1031 | let lines = acc.split('\n'); 1032 | 1033 | // Check if the last line is a password entry line - this is only an issue with Object and File imports 1034 | const endingPhrases = ['Enter password:']; 1035 | const endingLine = lines.filter((line) => { 1036 | for (let phrase of endingPhrases) { 1037 | return line === phrase; 1038 | } 1039 | }); 1040 | for (let line of lines.slice(0, -1).concat(endingLine)) { 1041 | observer.next(line); 1042 | } 1043 | acc = endingLine.length > 0 ? '' : lines[lines.length - 1]; 1044 | }, 1045 | (error) => observer.error(error), 1046 | () => observer.complete() 1047 | ); 1048 | }); 1049 | 1050 | const collectedData = await streamWrapper 1051 | .map((line) => this.handlePassword(line, command, stdinSubject)) 1052 | .do((line) => (this.doShowOutput ? this.outputChannel.append(`${line}\n`) : null)) 1053 | .do((line) => this.handleStdIn(line, command, stdinSubject)) 1054 | .do((line) => this.handleStdOut(line, command)) 1055 | .filter( 1056 | (line) => 1057 | !( 1058 | !line || 1059 | line.startsWith('[INFO]') || 1060 | line.startsWith('SuiteCloud Development Framework CLI') || 1061 | line.startsWith('Done.') || 1062 | line.startsWith('Using ') 1063 | ) 1064 | ) 1065 | .map((line) => this.mapCommandOutput(command, line)) 1066 | .reduce((acc: string[], curr: string) => acc.concat([curr]), []) 1067 | .toPromise() 1068 | .catch((err) => this.cleanup()); 1069 | 1070 | this.cleanup(); 1071 | return collectedData; 1072 | } 1073 | } 1074 | 1075 | showStatus() { 1076 | this.savedStatus = this.statusBar.text; 1077 | const mode1 = ' [= ]'; 1078 | const mode2 = ' [ =]'; 1079 | let currentMode = mode1; 1080 | this.intervalId = setInterval(() => { 1081 | currentMode = currentMode === mode1 ? mode2 : mode1; 1082 | this.statusBar.text = this.savedStatus + currentMode; 1083 | }, 500); 1084 | } 1085 | 1086 | /**************/ 1087 | /*** UTILS ****/ 1088 | /**************/ 1089 | 1090 | async copyFile(relativeFrom: string, relativeTo: string) { 1091 | const toDir = relativeTo.split('/').slice(0, -1).join('/'); 1092 | this.createPath(toDir); 1093 | const from = path.join(this.rootPath, relativeFrom); 1094 | const to = path.join(this.rootPath, relativeTo); 1095 | return fs.copyFile(from, to); 1096 | } 1097 | 1098 | createPath(targetDir) { 1099 | // Strip leading '/' 1100 | targetDir = targetDir.substring(1); 1101 | const initDir = this.rootPath; 1102 | const baseDir = this.rootPath; 1103 | 1104 | targetDir.split('/').reduce((parentDir, childDir) => { 1105 | const curDir = path.resolve(baseDir, parentDir, childDir); 1106 | try { 1107 | fs.mkdirSync(curDir); 1108 | } catch (err) { 1109 | if (err.code !== 'EEXIST') { 1110 | throw err; 1111 | } 1112 | } 1113 | 1114 | return curDir; 1115 | }, initDir); 1116 | } 1117 | 1118 | async getFilePaths(filePatterns: string[]): Promise { 1119 | const globPromises = filePatterns.map((filePattern) => { 1120 | filePattern = filePattern.replace('~', ''); 1121 | filePattern = filePattern.replace('*', '**'); // NetSuite's * glob pattern functions the same as a traditional ** pattern 1122 | return globAsync(path.join(this.rootPath, filePattern), { nodir: true }); 1123 | }); 1124 | const matchArr = await Promise.all(globPromises); 1125 | const filePaths: string[] = matchArr.reduce((filePathAccum: string[], matches) => { 1126 | for (const match of matches) { 1127 | // Make sure there are no duplicates 1128 | if (filePathAccum.indexOf(match) === -1) { 1129 | filePathAccum.push(match); 1130 | } 1131 | } 1132 | return filePathAccum; 1133 | }, []); 1134 | const relativeFilePaths = filePaths.map((fullPath) => `${path.sep}${path.relative(this.rootPath, fullPath)}`); 1135 | return relativeFilePaths; 1136 | } 1137 | 1138 | fileExists(p: string): Promise { 1139 | return new Promise((resolve, reject) => { 1140 | try { 1141 | fs.exists(p, (exists) => resolve(exists)); 1142 | } catch (e) { 1143 | reject(e); 1144 | } 1145 | }); 1146 | } 1147 | 1148 | openFile(p: string): Promise { 1149 | return new Promise((resolve, reject) => { 1150 | fs.readFile(p, (err, data) => { 1151 | if (err) { 1152 | reject(err); 1153 | } 1154 | resolve(data); 1155 | }); 1156 | }); 1157 | } 1158 | 1159 | ls(p: string): Promise { 1160 | return new Promise((resolve, reject) => { 1161 | fs.readdir(p, (err, items) => { 1162 | if (err) { 1163 | reject(err); 1164 | } 1165 | resolve(items); 1166 | }); 1167 | }); 1168 | } 1169 | 1170 | parseXml(xml: string): Promise<{ [key: string]: any }> { 1171 | return new Promise((resolve, reject) => { 1172 | xml2js.parseString(xml, function (err, result) { 1173 | if (err) { 1174 | reject(err); 1175 | } 1176 | resolve(result); 1177 | }); 1178 | }); 1179 | } 1180 | 1181 | async getXMLFileList(dirList: string[], root: string): Promise<{ path: string; scriptid: string }[]> { 1182 | const fileList: { path: string; scriptid: string }[] = []; 1183 | const traverseFolders = async (folders: string[], root: string) => { 1184 | if (folders.length > 0) { 1185 | for (const folder of folders) { 1186 | const rawFileList = await this.ls(path.join(root, folder)); 1187 | const dirList: string[] = []; 1188 | for (const fileName of rawFileList) { 1189 | const lstat = fs.lstatSync(path.join(root, folder, fileName)); 1190 | if (lstat.isDirectory()) { 1191 | dirList.push(fileName); 1192 | } else { 1193 | if (fileName.slice(fileName.length - 4) === '.xml') { 1194 | fileList.push({ 1195 | path: path.join(root, folder, fileName), 1196 | scriptid: fileName, 1197 | }); 1198 | } 1199 | } 1200 | } 1201 | await traverseFolders(dirList, path.join(root, folder)); 1202 | } 1203 | } else { 1204 | return folders; 1205 | } 1206 | }; 1207 | try { 1208 | await traverseFolders(dirList, root); 1209 | return fileList; 1210 | } catch (err) { 1211 | vscode.window.showErrorMessage('Unable to get file list: ', err.message); 1212 | } 1213 | } 1214 | } 1215 | --------------------------------------------------------------------------------