). 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 |
--------------------------------------------------------------------------------