Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/build/lib/policies.ts
3520 views
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
import { spawn } from 'child_process';
7
import { promises as fs } from 'fs';
8
import path from 'path';
9
import byline from 'byline';
10
import { rgPath } from '@vscode/ripgrep';
11
import Parser from 'tree-sitter';
12
const { typescript } = require('tree-sitter-typescript');
13
const product = require('../../product.json');
14
const packageJson = require('../../package.json');
15
16
type NlsString = { value: string; nlsKey: string };
17
18
function isNlsString(value: string | NlsString | undefined): value is NlsString {
19
return value ? typeof value !== 'string' : false;
20
}
21
22
function isStringArray(value: (string | NlsString)[]): value is string[] {
23
return !value.some(s => isNlsString(s));
24
}
25
26
function isNlsStringArray(value: (string | NlsString)[]): value is NlsString[] {
27
return value.every(s => isNlsString(s));
28
}
29
30
interface Category {
31
readonly moduleName: string;
32
readonly name: NlsString;
33
}
34
35
enum PolicyType {
36
Boolean = 'boolean',
37
Number = 'number',
38
Object = 'object',
39
String = 'string',
40
StringEnum = 'stringEnum',
41
}
42
43
interface Policy {
44
readonly name: string;
45
readonly type: PolicyType;
46
readonly category: Category;
47
readonly minimumVersion: string;
48
renderADMX(regKey: string): string[];
49
renderADMLStrings(translations?: LanguageTranslations): string[];
50
renderADMLPresentation(): string;
51
renderProfile(): string[];
52
// https://github.com/ProfileManifests/ProfileManifests/wiki/Manifest-Format
53
renderProfileManifest(translations?: LanguageTranslations): string;
54
}
55
56
function renderADMLString(prefix: string, moduleName: string, nlsString: NlsString, translations?: LanguageTranslations): string {
57
let value: string | undefined;
58
59
if (translations) {
60
const moduleTranslations = translations[moduleName];
61
62
if (moduleTranslations) {
63
value = moduleTranslations[nlsString.nlsKey];
64
}
65
}
66
67
if (!value) {
68
value = nlsString.value;
69
}
70
71
return `<string id="${prefix}_${nlsString.nlsKey.replace(/\./g, '_')}">${value}</string>`;
72
}
73
74
function renderProfileString(_prefix: string, moduleName: string, nlsString: NlsString, translations?: LanguageTranslations): string {
75
let value: string | undefined;
76
77
if (translations) {
78
const moduleTranslations = translations[moduleName];
79
80
if (moduleTranslations) {
81
value = moduleTranslations[nlsString.nlsKey];
82
}
83
}
84
85
if (!value) {
86
value = nlsString.value;
87
}
88
89
return value;
90
}
91
92
abstract class BasePolicy implements Policy {
93
constructor(
94
readonly type: PolicyType,
95
readonly name: string,
96
readonly category: Category,
97
readonly minimumVersion: string,
98
protected description: NlsString,
99
protected moduleName: string,
100
) { }
101
102
protected renderADMLString(nlsString: NlsString, translations?: LanguageTranslations): string {
103
return renderADMLString(this.name, this.moduleName, nlsString, translations);
104
}
105
106
renderADMX(regKey: string) {
107
return [
108
`<policy name="${this.name}" class="Both" displayName="$(string.${this.name})" explainText="$(string.${this.name}_${this.description.nlsKey.replace(/\./g, '_')})" key="Software\\Policies\\Microsoft\\${regKey}" presentation="$(presentation.${this.name})">`,
109
` <parentCategory ref="${this.category.name.nlsKey}" />`,
110
` <supportedOn ref="Supported_${this.minimumVersion.replace(/\./g, '_')}" />`,
111
` <elements>`,
112
...this.renderADMXElements(),
113
` </elements>`,
114
`</policy>`
115
];
116
}
117
118
protected abstract renderADMXElements(): string[];
119
120
renderADMLStrings(translations?: LanguageTranslations) {
121
return [
122
`<string id="${this.name}">${this.name}</string>`,
123
this.renderADMLString(this.description, translations)
124
];
125
}
126
127
renderADMLPresentation(): string {
128
return `<presentation id="${this.name}">${this.renderADMLPresentationContents()}</presentation>`;
129
}
130
131
protected abstract renderADMLPresentationContents(): string;
132
133
renderProfile() {
134
return [`<key>${this.name}</key>`, this.renderProfileValue()];
135
}
136
137
renderProfileManifest(translations?: LanguageTranslations): string {
138
return `<dict>
139
${this.renderProfileManifestValue(translations)}
140
</dict>`;
141
}
142
143
abstract renderProfileValue(): string;
144
abstract renderProfileManifestValue(translations?: LanguageTranslations): string;
145
}
146
147
class BooleanPolicy extends BasePolicy {
148
149
static from(
150
name: string,
151
category: Category,
152
minimumVersion: string,
153
description: NlsString,
154
moduleName: string,
155
settingNode: Parser.SyntaxNode
156
): BooleanPolicy | undefined {
157
const type = getStringProperty(moduleName, settingNode, 'type');
158
159
if (type !== 'boolean') {
160
return undefined;
161
}
162
163
return new BooleanPolicy(name, category, minimumVersion, description, moduleName);
164
}
165
166
private constructor(
167
name: string,
168
category: Category,
169
minimumVersion: string,
170
description: NlsString,
171
moduleName: string,
172
) {
173
super(PolicyType.Boolean, name, category, minimumVersion, description, moduleName);
174
}
175
176
protected renderADMXElements(): string[] {
177
return [
178
`<boolean id="${this.name}" valueName="${this.name}">`,
179
` <trueValue><decimal value="1" /></trueValue><falseValue><decimal value="0" /></falseValue>`,
180
`</boolean>`
181
];
182
}
183
184
renderADMLPresentationContents() {
185
return `<checkBox refId="${this.name}">${this.name}</checkBox>`;
186
}
187
188
renderProfileValue(): string {
189
return `<false/>`;
190
}
191
192
renderProfileManifestValue(translations?: LanguageTranslations): string {
193
return `<key>pfm_default</key>
194
<false/>
195
<key>pfm_description</key>
196
<string>${renderProfileString(this.name, this.moduleName, this.description, translations)}</string>
197
<key>pfm_name</key>
198
<string>${this.name}</string>
199
<key>pfm_title</key>
200
<string>${this.name}</string>
201
<key>pfm_type</key>
202
<string>boolean</string>`;
203
}
204
}
205
206
class ParseError extends Error {
207
constructor(message: string, moduleName: string, node: Parser.SyntaxNode) {
208
super(`${message}. ${moduleName}.ts:${node.startPosition.row + 1}`);
209
}
210
}
211
212
class NumberPolicy extends BasePolicy {
213
214
static from(
215
name: string,
216
category: Category,
217
minimumVersion: string,
218
description: NlsString,
219
moduleName: string,
220
settingNode: Parser.SyntaxNode
221
): NumberPolicy | undefined {
222
const type = getStringProperty(moduleName, settingNode, 'type');
223
224
if (type !== 'number') {
225
return undefined;
226
}
227
228
const defaultValue = getNumberProperty(moduleName, settingNode, 'default');
229
230
if (typeof defaultValue === 'undefined') {
231
throw new ParseError(`Missing required 'default' property.`, moduleName, settingNode);
232
}
233
234
return new NumberPolicy(name, category, minimumVersion, description, moduleName, defaultValue);
235
}
236
237
private constructor(
238
name: string,
239
category: Category,
240
minimumVersion: string,
241
description: NlsString,
242
moduleName: string,
243
protected readonly defaultValue: number,
244
) {
245
super(PolicyType.StringEnum, name, category, minimumVersion, description, moduleName);
246
}
247
248
protected renderADMXElements(): string[] {
249
return [
250
`<decimal id="${this.name}" valueName="${this.name}" />`
251
// `<decimal id="Quarantine_PurgeItemsAfterDelay" valueName="PurgeItemsAfterDelay" minValue="0" maxValue="10000000" />`
252
];
253
}
254
255
renderADMLPresentationContents() {
256
return `<decimalTextBox refId="${this.name}" defaultValue="${this.defaultValue}">${this.name}</decimalTextBox>`;
257
}
258
259
renderProfileValue() {
260
return `<integer>${this.defaultValue}</integer>`;
261
}
262
263
renderProfileManifestValue(translations?: LanguageTranslations) {
264
return `<key>pfm_default</key>
265
<integer>${this.defaultValue}</integer>
266
<key>pfm_description</key>
267
<string>${renderProfileString(this.name, this.moduleName, this.description, translations)}</string>
268
<key>pfm_name</key>
269
<string>${this.name}</string>
270
<key>pfm_title</key>
271
<string>${this.name}</string>
272
<key>pfm_type</key>
273
<string>integer</string>`;
274
}
275
}
276
277
class StringPolicy extends BasePolicy {
278
279
static from(
280
name: string,
281
category: Category,
282
minimumVersion: string,
283
description: NlsString,
284
moduleName: string,
285
settingNode: Parser.SyntaxNode
286
): StringPolicy | undefined {
287
const type = getStringProperty(moduleName, settingNode, 'type');
288
289
if (type !== 'string') {
290
return undefined;
291
}
292
293
return new StringPolicy(name, category, minimumVersion, description, moduleName);
294
}
295
296
private constructor(
297
name: string,
298
category: Category,
299
minimumVersion: string,
300
description: NlsString,
301
moduleName: string,
302
) {
303
super(PolicyType.String, name, category, minimumVersion, description, moduleName);
304
}
305
306
protected renderADMXElements(): string[] {
307
return [`<text id="${this.name}" valueName="${this.name}" required="true" />`];
308
}
309
310
renderADMLPresentationContents() {
311
return `<textBox refId="${this.name}"><label>${this.name}:</label></textBox>`;
312
}
313
314
renderProfileValue(): string {
315
return `<string></string>`;
316
}
317
318
renderProfileManifestValue(translations?: LanguageTranslations): string {
319
return `<key>pfm_default</key>
320
<string></string>
321
<key>pfm_description</key>
322
<string>${renderProfileString(this.name, this.moduleName, this.description, translations)}</string>
323
<key>pfm_name</key>
324
<string>${this.name}</string>
325
<key>pfm_title</key>
326
<string>${this.name}</string>
327
<key>pfm_type</key>
328
<string>string</string>`;
329
}
330
}
331
332
class ObjectPolicy extends BasePolicy {
333
334
static from(
335
name: string,
336
category: Category,
337
minimumVersion: string,
338
description: NlsString,
339
moduleName: string,
340
settingNode: Parser.SyntaxNode
341
): ObjectPolicy | undefined {
342
const type = getStringProperty(moduleName, settingNode, 'type');
343
344
if (type !== 'object' && type !== 'array') {
345
return undefined;
346
}
347
348
return new ObjectPolicy(name, category, minimumVersion, description, moduleName);
349
}
350
351
private constructor(
352
name: string,
353
category: Category,
354
minimumVersion: string,
355
description: NlsString,
356
moduleName: string,
357
) {
358
super(PolicyType.Object, name, category, minimumVersion, description, moduleName);
359
}
360
361
protected renderADMXElements(): string[] {
362
return [`<multiText id="${this.name}" valueName="${this.name}" required="true" />`];
363
}
364
365
renderADMLPresentationContents() {
366
return `<multiTextBox refId="${this.name}" />`;
367
}
368
369
renderProfileValue(): string {
370
return `<string></string>`;
371
}
372
373
renderProfileManifestValue(translations?: LanguageTranslations): string {
374
return `<key>pfm_default</key>
375
<string></string>
376
<key>pfm_description</key>
377
<string>${renderProfileString(this.name, this.moduleName, this.description, translations)}</string>
378
<key>pfm_name</key>
379
<string>${this.name}</string>
380
<key>pfm_title</key>
381
<string>${this.name}</string>
382
<key>pfm_type</key>
383
<string>string</string>
384
`;
385
}
386
}
387
388
class StringEnumPolicy extends BasePolicy {
389
390
static from(
391
name: string,
392
category: Category,
393
minimumVersion: string,
394
description: NlsString,
395
moduleName: string,
396
settingNode: Parser.SyntaxNode
397
): StringEnumPolicy | undefined {
398
const type = getStringProperty(moduleName, settingNode, 'type');
399
400
if (type !== 'string') {
401
return undefined;
402
}
403
404
const enum_ = getStringArrayProperty(moduleName, settingNode, 'enum');
405
406
if (!enum_) {
407
return undefined;
408
}
409
410
if (!isStringArray(enum_)) {
411
throw new ParseError(`Property 'enum' should not be localized.`, moduleName, settingNode);
412
}
413
414
const enumDescriptions = getStringArrayProperty(moduleName, settingNode, 'enumDescriptions');
415
416
if (!enumDescriptions) {
417
throw new ParseError(`Missing required 'enumDescriptions' property.`, moduleName, settingNode);
418
} else if (!isNlsStringArray(enumDescriptions)) {
419
throw new ParseError(`Property 'enumDescriptions' should be localized.`, moduleName, settingNode);
420
}
421
422
return new StringEnumPolicy(name, category, minimumVersion, description, moduleName, enum_, enumDescriptions);
423
}
424
425
private constructor(
426
name: string,
427
category: Category,
428
minimumVersion: string,
429
description: NlsString,
430
moduleName: string,
431
protected enum_: string[],
432
protected enumDescriptions: NlsString[],
433
) {
434
super(PolicyType.StringEnum, name, category, minimumVersion, description, moduleName);
435
}
436
437
protected renderADMXElements(): string[] {
438
return [
439
`<enum id="${this.name}" valueName="${this.name}">`,
440
...this.enum_.map((value, index) => ` <item displayName="$(string.${this.name}_${this.enumDescriptions[index].nlsKey})"><value><string>${value}</string></value></item>`),
441
`</enum>`
442
];
443
}
444
445
renderADMLStrings(translations?: LanguageTranslations) {
446
return [
447
...super.renderADMLStrings(translations),
448
...this.enumDescriptions.map(e => this.renderADMLString(e, translations))
449
];
450
}
451
452
renderADMLPresentationContents() {
453
return `<dropdownList refId="${this.name}" />`;
454
}
455
456
renderProfileValue() {
457
return `<string>${this.enum_[0]}</string>`;
458
}
459
460
renderProfileManifestValue(translations?: LanguageTranslations): string {
461
return `<key>pfm_default</key>
462
<string>${this.enum_[0]}</string>
463
<key>pfm_description</key>
464
<string>${renderProfileString(this.name, this.moduleName, this.description, translations)}</string>
465
<key>pfm_name</key>
466
<string>${this.name}</string>
467
<key>pfm_title</key>
468
<string>${this.name}</string>
469
<key>pfm_type</key>
470
<string>string</string>
471
<key>pfm_range_list</key>
472
<array>
473
${this.enum_.map(e => `<string>${e}</string>`).join('\n ')}
474
</array>`;
475
}
476
}
477
478
interface QType<T> {
479
Q: string;
480
value(matches: Parser.QueryMatch[]): T | undefined;
481
}
482
483
const NumberQ: QType<number> = {
484
Q: `(number) @value`,
485
486
value(matches: Parser.QueryMatch[]): number | undefined {
487
const match = matches[0];
488
489
if (!match) {
490
return undefined;
491
}
492
493
const value = match.captures.filter(c => c.name === 'value')[0]?.node.text;
494
495
if (!value) {
496
throw new Error(`Missing required 'value' property.`);
497
}
498
499
return parseInt(value);
500
}
501
};
502
503
const StringQ: QType<string | NlsString> = {
504
Q: `[
505
(string (string_fragment) @value)
506
(call_expression
507
function: [
508
(identifier) @localizeFn (#eq? @localizeFn localize)
509
(member_expression
510
object: (identifier) @nlsObj (#eq? @nlsObj nls)
511
property: (property_identifier) @localizeFn (#eq? @localizeFn localize)
512
)
513
]
514
arguments: (arguments (string (string_fragment) @nlsKey) (string (string_fragment) @value))
515
)
516
]`,
517
518
value(matches: Parser.QueryMatch[]): string | NlsString | undefined {
519
const match = matches[0];
520
521
if (!match) {
522
return undefined;
523
}
524
525
const value = match.captures.filter(c => c.name === 'value')[0]?.node.text;
526
527
if (!value) {
528
throw new Error(`Missing required 'value' property.`);
529
}
530
531
const nlsKey = match.captures.filter(c => c.name === 'nlsKey')[0]?.node.text;
532
533
if (nlsKey) {
534
return { value, nlsKey };
535
} else {
536
return value;
537
}
538
}
539
};
540
541
const StringArrayQ: QType<(string | NlsString)[]> = {
542
Q: `(array ${StringQ.Q})`,
543
544
value(matches: Parser.QueryMatch[]): (string | NlsString)[] | undefined {
545
if (matches.length === 0) {
546
return undefined;
547
}
548
549
return matches.map(match => {
550
return StringQ.value([match]) as string | NlsString;
551
});
552
}
553
};
554
555
function getProperty<T>(qtype: QType<T>, moduleName: string, node: Parser.SyntaxNode, key: string): T | undefined {
556
const query = new Parser.Query(
557
typescript,
558
`(
559
(pair
560
key: [(property_identifier)(string)] @key
561
value: ${qtype.Q}
562
)
563
(#any-of? @key "${key}" "'${key}'")
564
)`
565
);
566
567
try {
568
const matches = query.matches(node).filter(m => m.captures[0].node.parent?.parent === node);
569
return qtype.value(matches);
570
} catch (e) {
571
throw new ParseError(e.message, moduleName, node);
572
}
573
}
574
575
function getNumberProperty(moduleName: string, node: Parser.SyntaxNode, key: string): number | undefined {
576
return getProperty(NumberQ, moduleName, node, key);
577
}
578
579
function getStringProperty(moduleName: string, node: Parser.SyntaxNode, key: string): string | NlsString | undefined {
580
return getProperty(StringQ, moduleName, node, key);
581
}
582
583
function getStringArrayProperty(moduleName: string, node: Parser.SyntaxNode, key: string): (string | NlsString)[] | undefined {
584
return getProperty(StringArrayQ, moduleName, node, key);
585
}
586
587
// TODO: add more policy types
588
const PolicyTypes = [
589
BooleanPolicy,
590
NumberPolicy,
591
StringEnumPolicy,
592
StringPolicy,
593
ObjectPolicy
594
];
595
596
function getPolicy(
597
moduleName: string,
598
configurationNode: Parser.SyntaxNode,
599
settingNode: Parser.SyntaxNode,
600
policyNode: Parser.SyntaxNode,
601
categories: Map<string, Category>
602
): Policy {
603
const name = getStringProperty(moduleName, policyNode, 'name');
604
605
if (!name) {
606
throw new ParseError(`Missing required 'name' property`, moduleName, policyNode);
607
} else if (isNlsString(name)) {
608
throw new ParseError(`Property 'name' should be a literal string`, moduleName, policyNode);
609
}
610
611
const categoryName = getStringProperty(moduleName, configurationNode, 'title');
612
613
if (!categoryName) {
614
throw new ParseError(`Missing required 'title' property`, moduleName, configurationNode);
615
} else if (!isNlsString(categoryName)) {
616
throw new ParseError(`Property 'title' should be localized`, moduleName, configurationNode);
617
}
618
619
const categoryKey = `${categoryName.nlsKey}:${categoryName.value}`;
620
let category = categories.get(categoryKey);
621
622
if (!category) {
623
category = { moduleName, name: categoryName };
624
categories.set(categoryKey, category);
625
}
626
627
const minimumVersion = getStringProperty(moduleName, policyNode, 'minimumVersion');
628
629
if (!minimumVersion) {
630
throw new ParseError(`Missing required 'minimumVersion' property.`, moduleName, policyNode);
631
} else if (isNlsString(minimumVersion)) {
632
throw new ParseError(`Property 'minimumVersion' should be a literal string.`, moduleName, policyNode);
633
}
634
635
const description = getStringProperty(moduleName, policyNode, 'description') ?? getStringProperty(moduleName, settingNode, 'description');
636
637
if (!description) {
638
throw new ParseError(`Missing required 'description' property.`, moduleName, settingNode);
639
} if (!isNlsString(description)) {
640
throw new ParseError(`Property 'description' should be localized.`, moduleName, settingNode);
641
}
642
643
let result: Policy | undefined;
644
645
for (const policyType of PolicyTypes) {
646
if (result = policyType.from(name, category, minimumVersion, description, moduleName, settingNode)) {
647
break;
648
}
649
}
650
651
if (!result) {
652
throw new ParseError(`Failed to parse policy '${name}'.`, moduleName, settingNode);
653
}
654
655
return result;
656
}
657
658
function getPolicies(moduleName: string, node: Parser.SyntaxNode): Policy[] {
659
const query = new Parser.Query(typescript, `
660
(
661
(call_expression
662
function: (member_expression property: (property_identifier) @registerConfigurationFn) (#eq? @registerConfigurationFn registerConfiguration)
663
arguments: (arguments (object (pair
664
key: [(property_identifier)(string)] @propertiesKey (#any-of? @propertiesKey "properties" "'properties'")
665
value: (object (pair
666
key: [(property_identifier)(string)(computed_property_name)]
667
value: (object (pair
668
key: [(property_identifier)(string)] @policyKey (#any-of? @policyKey "policy" "'policy'")
669
value: (object) @policy
670
)) @setting
671
))
672
)) @configuration)
673
)
674
)
675
`);
676
677
const categories = new Map<string, Category>();
678
679
return query.matches(node).map(m => {
680
const configurationNode = m.captures.filter(c => c.name === 'configuration')[0].node;
681
const settingNode = m.captures.filter(c => c.name === 'setting')[0].node;
682
const policyNode = m.captures.filter(c => c.name === 'policy')[0].node;
683
return getPolicy(moduleName, configurationNode, settingNode, policyNode, categories);
684
});
685
}
686
687
async function getFiles(root: string): Promise<string[]> {
688
return new Promise((c, e) => {
689
const result: string[] = [];
690
const rg = spawn(rgPath, ['-l', 'registerConfiguration\\(', '-g', 'src/**/*.ts', '-g', '!src/**/test/**', root]);
691
const stream = byline(rg.stdout.setEncoding('utf8'));
692
stream.on('data', path => result.push(path));
693
stream.on('error', err => e(err));
694
stream.on('end', () => c(result));
695
});
696
}
697
698
function renderADMX(regKey: string, versions: string[], categories: Category[], policies: Policy[]) {
699
versions = versions.map(v => v.replace(/\./g, '_'));
700
701
return `<?xml version="1.0" encoding="utf-8"?>
702
<policyDefinitions revision="1.1" schemaVersion="1.0">
703
<policyNamespaces>
704
<target prefix="${regKey}" namespace="Microsoft.Policies.${regKey}" />
705
</policyNamespaces>
706
<resources minRequiredRevision="1.0" />
707
<supportedOn>
708
<definitions>
709
${versions.map(v => `<definition name="Supported_${v}" displayName="$(string.Supported_${v})" />`).join(`\n `)}
710
</definitions>
711
</supportedOn>
712
<categories>
713
<category displayName="$(string.Application)" name="Application" />
714
${categories.map(c => `<category displayName="$(string.Category_${c.name.nlsKey})" name="${c.name.nlsKey}"><parentCategory ref="Application" /></category>`).join(`\n `)}
715
</categories>
716
<policies>
717
${policies.map(p => p.renderADMX(regKey)).flat().join(`\n `)}
718
</policies>
719
</policyDefinitions>
720
`;
721
}
722
723
function renderADML(appName: string, versions: string[], categories: Category[], policies: Policy[], translations?: LanguageTranslations) {
724
return `<?xml version="1.0" encoding="utf-8"?>
725
<policyDefinitionResources revision="1.0" schemaVersion="1.0">
726
<displayName />
727
<description />
728
<resources>
729
<stringTable>
730
<string id="Application">${appName}</string>
731
${versions.map(v => `<string id="Supported_${v.replace(/\./g, '_')}">${appName} &gt;= ${v}</string>`).join(`\n `)}
732
${categories.map(c => renderADMLString('Category', c.moduleName, c.name, translations)).join(`\n `)}
733
${policies.map(p => p.renderADMLStrings(translations)).flat().join(`\n `)}
734
</stringTable>
735
<presentationTable>
736
${policies.map(p => p.renderADMLPresentation()).join(`\n `)}
737
</presentationTable>
738
</resources>
739
</policyDefinitionResources>
740
`;
741
}
742
743
function renderProfileManifest(appName: string, bundleIdentifier: string, _versions: string[], _categories: Category[], policies: Policy[], translations?: LanguageTranslations) {
744
745
const requiredPayloadFields = `
746
<dict>
747
<key>pfm_default</key>
748
<string>Configure ${appName}</string>
749
<key>pfm_name</key>
750
<string>PayloadDescription</string>
751
<key>pfm_title</key>
752
<string>Payload Description</string>
753
<key>pfm_type</key>
754
<string>string</string>
755
</dict>
756
<dict>
757
<key>pfm_default</key>
758
<string>${appName}</string>
759
<key>pfm_name</key>
760
<string>PayloadDisplayName</string>
761
<key>pfm_require</key>
762
<string>always</string>
763
<key>pfm_title</key>
764
<string>Payload Display Name</string>
765
<key>pfm_type</key>
766
<string>string</string>
767
</dict>
768
<dict>
769
<key>pfm_default</key>
770
<string>${bundleIdentifier}</string>
771
<key>pfm_name</key>
772
<string>PayloadIdentifier</string>
773
<key>pfm_require</key>
774
<string>always</string>
775
<key>pfm_title</key>
776
<string>Payload Identifier</string>
777
<key>pfm_type</key>
778
<string>string</string>
779
</dict>
780
<dict>
781
<key>pfm_default</key>
782
<string>${bundleIdentifier}</string>
783
<key>pfm_name</key>
784
<string>PayloadType</string>
785
<key>pfm_require</key>
786
<string>always</string>
787
<key>pfm_title</key>
788
<string>Payload Type</string>
789
<key>pfm_type</key>
790
<string>string</string>
791
</dict>
792
<dict>
793
<key>pfm_default</key>
794
<string></string>
795
<key>pfm_name</key>
796
<string>PayloadUUID</string>
797
<key>pfm_require</key>
798
<string>always</string>
799
<key>pfm_title</key>
800
<string>Payload UUID</string>
801
<key>pfm_type</key>
802
<string>string</string>
803
</dict>
804
<dict>
805
<key>pfm_default</key>
806
<integer>1</integer>
807
<key>pfm_name</key>
808
<string>PayloadVersion</string>
809
<key>pfm_range_list</key>
810
<array>
811
<integer>1</integer>
812
</array>
813
<key>pfm_require</key>
814
<string>always</string>
815
<key>pfm_title</key>
816
<string>Payload Version</string>
817
<key>pfm_type</key>
818
<string>integer</string>
819
</dict>
820
<dict>
821
<key>pfm_default</key>
822
<string>Microsoft</string>
823
<key>pfm_name</key>
824
<string>PayloadOrganization</string>
825
<key>pfm_title</key>
826
<string>Payload Organization</string>
827
<key>pfm_type</key>
828
<string>string</string>
829
</dict>`;
830
831
const profileManifestSubkeys = policies.map(policy => {
832
return policy.renderProfileManifest(translations);
833
}).join('');
834
835
return `<?xml version="1.0" encoding="UTF-8"?>
836
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
837
<plist version="1.0">
838
<dict>
839
<key>pfm_app_url</key>
840
<string>https://code.visualstudio.com/</string>
841
<key>pfm_description</key>
842
<string>${appName} Managed Settings</string>
843
<key>pfm_documentation_url</key>
844
<string>https://code.visualstudio.com/docs/setup/enterprise</string>
845
<key>pfm_domain</key>
846
<string>${bundleIdentifier}</string>
847
<key>pfm_format_version</key>
848
<integer>1</integer>
849
<key>pfm_interaction</key>
850
<string>combined</string>
851
<key>pfm_last_modified</key>
852
<date>${new Date().toISOString().replace(/\.\d+Z$/, 'Z')}</date>
853
<key>pfm_platforms</key>
854
<array>
855
<string>macOS</string>
856
</array>
857
<key>pfm_subkeys</key>
858
<array>
859
${requiredPayloadFields}
860
${profileManifestSubkeys}
861
</array>
862
<key>pfm_title</key>
863
<string>${appName}</string>
864
<key>pfm_unique</key>
865
<true/>
866
<key>pfm_version</key>
867
<integer>1</integer>
868
</dict>
869
</plist>`;
870
}
871
872
function renderMacOSPolicy(policies: Policy[], translations: Translations) {
873
const appName = product.nameLong;
874
const bundleIdentifier = product.darwinBundleIdentifier;
875
const payloadUUID = product.darwinProfilePayloadUUID;
876
const UUID = product.darwinProfileUUID;
877
878
const versions = [...new Set(policies.map(p => p.minimumVersion)).values()].sort();
879
const categories = [...new Set(policies.map(p => p.category))];
880
881
const policyEntries =
882
policies.map(policy => policy.renderProfile())
883
.flat()
884
.map(entry => `\t\t\t\t${entry}`)
885
.join('\n');
886
887
888
return {
889
profile: `<?xml version="1.0" encoding="UTF-8"?>
890
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
891
<plist version="1.0">
892
<dict>
893
<key>PayloadContent</key>
894
<array>
895
<dict>
896
<key>PayloadDisplayName</key>
897
<string>${appName}</string>
898
<key>PayloadIdentifier</key>
899
<string>${bundleIdentifier}.${UUID}</string>
900
<key>PayloadType</key>
901
<string>${bundleIdentifier}</string>
902
<key>PayloadUUID</key>
903
<string>${UUID}</string>
904
<key>PayloadVersion</key>
905
<integer>1</integer>
906
${policyEntries}
907
</dict>
908
</array>
909
<key>PayloadDescription</key>
910
<string>This profile manages ${appName}. For more information see https://code.visualstudio.com/docs/setup/enterprise</string>
911
<key>PayloadDisplayName</key>
912
<string>${appName}</string>
913
<key>PayloadIdentifier</key>
914
<string>${bundleIdentifier}</string>
915
<key>PayloadOrganization</key>
916
<string>Microsoft</string>
917
<key>PayloadType</key>
918
<string>Configuration</string>
919
<key>PayloadUUID</key>
920
<string>${payloadUUID}</string>
921
<key>PayloadVersion</key>
922
<integer>1</integer>
923
<key>TargetDeviceType</key>
924
<integer>5</integer>
925
</dict>
926
</plist>`,
927
manifests: [{ languageId: 'en-us', contents: renderProfileManifest(appName, bundleIdentifier, versions, categories, policies) },
928
...translations.map(({ languageId, languageTranslations }) =>
929
({ languageId, contents: renderProfileManifest(appName, bundleIdentifier, versions, categories, policies, languageTranslations) }))
930
]
931
};
932
}
933
934
function renderGP(policies: Policy[], translations: Translations) {
935
const appName = product.nameLong;
936
const regKey = product.win32RegValueName;
937
938
const versions = [...new Set(policies.map(p => p.minimumVersion)).values()].sort();
939
const categories = [...Object.values(policies.reduce((acc, p) => ({ ...acc, [p.category.name.nlsKey]: p.category }), {}))] as Category[];
940
941
return {
942
admx: renderADMX(regKey, versions, categories, policies),
943
adml: [
944
{ languageId: 'en-us', contents: renderADML(appName, versions, categories, policies) },
945
...translations.map(({ languageId, languageTranslations }) =>
946
({ languageId, contents: renderADML(appName, versions, categories, policies, languageTranslations) }))
947
]
948
};
949
}
950
951
const Languages = {
952
'fr': 'fr-fr',
953
'it': 'it-it',
954
'de': 'de-de',
955
'es': 'es-es',
956
'ru': 'ru-ru',
957
'zh-hans': 'zh-cn',
958
'zh-hant': 'zh-tw',
959
'ja': 'ja-jp',
960
'ko': 'ko-kr',
961
'cs': 'cs-cz',
962
'pt-br': 'pt-br',
963
'tr': 'tr-tr',
964
'pl': 'pl-pl',
965
};
966
967
type LanguageTranslations = { [moduleName: string]: { [nlsKey: string]: string } };
968
type Translations = { languageId: string; languageTranslations: LanguageTranslations }[];
969
970
type Version = [number, number, number];
971
972
async function getSpecificNLS(resourceUrlTemplate: string, languageId: string, version: Version) {
973
const resource = {
974
publisher: 'ms-ceintl',
975
name: `vscode-language-pack-${languageId}`,
976
version: `${version[0]}.${version[1]}.${version[2]}`,
977
path: 'extension/translations/main.i18n.json'
978
};
979
980
const url = resourceUrlTemplate.replace(/\{([^}]+)\}/g, (_, key) => resource[key as keyof typeof resource]);
981
const res = await fetch(url);
982
983
if (res.status !== 200) {
984
throw new Error(`[${res.status}] Error downloading language pack ${languageId}@${version}`);
985
}
986
987
const { contents: result } = await res.json() as { contents: LanguageTranslations };
988
return result;
989
}
990
991
function parseVersion(version: string): Version {
992
const [, major, minor, patch] = /^(\d+)\.(\d+)\.(\d+)/.exec(version)!;
993
return [parseInt(major), parseInt(minor), parseInt(patch)];
994
}
995
996
function compareVersions(a: Version, b: Version): number {
997
if (a[0] !== b[0]) { return a[0] - b[0]; }
998
if (a[1] !== b[1]) { return a[1] - b[1]; }
999
return a[2] - b[2];
1000
}
1001
1002
async function queryVersions(serviceUrl: string, languageId: string): Promise<Version[]> {
1003
const res = await fetch(`${serviceUrl}/extensionquery`, {
1004
method: 'POST',
1005
headers: {
1006
'Accept': 'application/json;api-version=3.0-preview.1',
1007
'Content-Type': 'application/json',
1008
'User-Agent': 'VS Code Build',
1009
},
1010
body: JSON.stringify({
1011
filters: [{ criteria: [{ filterType: 7, value: `ms-ceintl.vscode-language-pack-${languageId}` }] }],
1012
flags: 0x1
1013
})
1014
});
1015
1016
if (res.status !== 200) {
1017
throw new Error(`[${res.status}] Error querying for extension: ${languageId}`);
1018
}
1019
1020
const result = await res.json() as { results: [{ extensions: { versions: { version: string }[] }[] }] };
1021
return result.results[0].extensions[0].versions.map(v => parseVersion(v.version)).sort(compareVersions);
1022
}
1023
1024
async function getNLS(extensionGalleryServiceUrl: string, resourceUrlTemplate: string, languageId: string, version: Version) {
1025
const versions = await queryVersions(extensionGalleryServiceUrl, languageId);
1026
const nextMinor: Version = [version[0], version[1] + 1, 0];
1027
const compatibleVersions = versions.filter(v => compareVersions(v, nextMinor) < 0);
1028
const latestCompatibleVersion = compatibleVersions.at(-1)!; // order is newest to oldest
1029
1030
if (!latestCompatibleVersion) {
1031
throw new Error(`No compatible language pack found for ${languageId} for version ${version}`);
1032
}
1033
1034
return await getSpecificNLS(resourceUrlTemplate, languageId, latestCompatibleVersion);
1035
}
1036
1037
async function parsePolicies(): Promise<Policy[]> {
1038
const parser = new Parser();
1039
parser.setLanguage(typescript);
1040
1041
const files = await getFiles(process.cwd());
1042
const base = path.join(process.cwd(), 'src');
1043
const policies = [];
1044
1045
for (const file of files) {
1046
const moduleName = path.relative(base, file).replace(/\.ts$/i, '').replace(/\\/g, '/');
1047
const contents = await fs.readFile(file, { encoding: 'utf8' });
1048
const tree = parser.parse(contents);
1049
policies.push(...getPolicies(moduleName, tree.rootNode));
1050
}
1051
1052
return policies;
1053
}
1054
1055
async function getTranslations(): Promise<Translations> {
1056
const extensionGalleryServiceUrl = product.extensionsGallery?.serviceUrl;
1057
1058
if (!extensionGalleryServiceUrl) {
1059
console.warn(`Skipping policy localization: No 'extensionGallery.serviceUrl' found in 'product.json'.`);
1060
return [];
1061
}
1062
1063
const resourceUrlTemplate = product.extensionsGallery?.resourceUrlTemplate;
1064
1065
if (!resourceUrlTemplate) {
1066
console.warn(`Skipping policy localization: No 'resourceUrlTemplate' found in 'product.json'.`);
1067
return [];
1068
}
1069
1070
const version = parseVersion(packageJson.version);
1071
const languageIds = Object.keys(Languages);
1072
1073
return await Promise.all(languageIds.map(
1074
languageId => getNLS(extensionGalleryServiceUrl, resourceUrlTemplate, languageId, version)
1075
.then(languageTranslations => ({ languageId, languageTranslations }))
1076
));
1077
}
1078
1079
async function windowsMain(policies: Policy[], translations: Translations) {
1080
const root = '.build/policies/win32';
1081
const { admx, adml } = await renderGP(policies, translations);
1082
1083
await fs.rm(root, { recursive: true, force: true });
1084
await fs.mkdir(root, { recursive: true });
1085
1086
await fs.writeFile(path.join(root, `${product.win32RegValueName}.admx`), admx.replace(/\r?\n/g, '\n'));
1087
1088
for (const { languageId, contents } of adml) {
1089
const languagePath = path.join(root, languageId === 'en-us' ? 'en-us' : Languages[languageId as keyof typeof Languages]);
1090
await fs.mkdir(languagePath, { recursive: true });
1091
await fs.writeFile(path.join(languagePath, `${product.win32RegValueName}.adml`), contents.replace(/\r?\n/g, '\n'));
1092
}
1093
}
1094
1095
async function darwinMain(policies: Policy[], translations: Translations) {
1096
const bundleIdentifier = product.darwinBundleIdentifier;
1097
if (!bundleIdentifier || !product.darwinProfilePayloadUUID || !product.darwinProfileUUID) {
1098
throw new Error(`Missing required product information.`);
1099
}
1100
const root = '.build/policies/darwin';
1101
const { profile, manifests } = await renderMacOSPolicy(policies, translations);
1102
1103
await fs.rm(root, { recursive: true, force: true });
1104
await fs.mkdir(root, { recursive: true });
1105
await fs.writeFile(path.join(root, `${bundleIdentifier}.mobileconfig`), profile.replace(/\r?\n/g, '\n'));
1106
1107
for (const { languageId, contents } of manifests) {
1108
const languagePath = path.join(root, languageId === 'en-us' ? 'en-us' : Languages[languageId as keyof typeof Languages]);
1109
await fs.mkdir(languagePath, { recursive: true });
1110
await fs.writeFile(path.join(languagePath, `${bundleIdentifier}.plist`), contents.replace(/\r?\n/g, '\n'));
1111
}
1112
}
1113
1114
async function main() {
1115
const [policies, translations] = await Promise.all([parsePolicies(), getTranslations()]);
1116
const platform = process.argv[2];
1117
1118
if (platform === 'darwin') {
1119
await darwinMain(policies, translations);
1120
} else if (platform === 'win32') {
1121
await windowsMain(policies, translations);
1122
} else {
1123
console.error(`Usage: node build/lib/policies <darwin|win32>`);
1124
process.exit(1);
1125
}
1126
}
1127
1128
if (require.main === module) {
1129
main().catch(err => {
1130
if (err instanceof ParseError) {
1131
console.error(`Parse Error:`, err.message);
1132
} else {
1133
console.error(err);
1134
}
1135
process.exit(1);
1136
});
1137
}
1138
1139