"use strict"; /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ Object.defineProperty(exports, "__esModule", { value: true }); const child_process_1 = require("child_process"); const fs_1 = require("fs"); const path = require("path"); const byline = require("byline"); const ripgrep_1 = require("@vscode/ripgrep"); const Parser = require("tree-sitter"); const { typescript } = require('tree-sitter-typescript'); const product = require('../../product.json'); const packageJson = require('../../package.json'); function isNlsString(value) { return value ? typeof value !== 'string' : false; } function isStringArray(value) { return !value.some(s => isNlsString(s)); } function isNlsStringArray(value) { return value.every(s => isNlsString(s)); } var PolicyType; (function (PolicyType) { PolicyType[PolicyType["StringEnum"] = 0] = "StringEnum"; })(PolicyType || (PolicyType = {})); function renderADMLString(prefix, moduleName, nlsString, translations) { let value; if (translations) { const moduleTranslations = translations[moduleName]; if (moduleTranslations) { value = moduleTranslations[nlsString.nlsKey]; } } if (!value) { value = nlsString.value; } return `${value}`; } class BasePolicy { policyType; name; category; minimumVersion; description; moduleName; constructor(policyType, name, category, minimumVersion, description, moduleName) { this.policyType = policyType; this.name = name; this.category = category; this.minimumVersion = minimumVersion; this.description = description; this.moduleName = moduleName; } renderADMLString(nlsString, translations) { return renderADMLString(this.name, this.moduleName, nlsString, translations); } renderADMX(regKey) { return [ ``, ` `, ` `, ` `, ...this.renderADMXElements(), ` `, `` ]; } renderADMLStrings(translations) { return [ `${this.name}`, this.renderADMLString(this.description, translations) ]; } renderADMLPresentation() { return `${this.renderADMLPresentationContents()}`; } } class BooleanPolicy extends BasePolicy { static from(name, category, minimumVersion, description, moduleName, settingNode) { const type = getStringProperty(settingNode, 'type'); if (type !== 'boolean') { return undefined; } return new BooleanPolicy(name, category, minimumVersion, description, moduleName); } constructor(name, category, minimumVersion, description, moduleName) { super(PolicyType.StringEnum, name, category, minimumVersion, description, moduleName); } renderADMXElements() { return [ ``, ` `, `` ]; } renderADMLPresentationContents() { return `${this.name}`; } } class IntPolicy extends BasePolicy { defaultValue; static from(name, category, minimumVersion, description, moduleName, settingNode) { const type = getStringProperty(settingNode, 'type'); if (type !== 'number') { return undefined; } const defaultValue = getIntProperty(settingNode, 'default'); if (typeof defaultValue === 'undefined') { throw new Error(`Missing required 'default' property.`); } return new IntPolicy(name, category, minimumVersion, description, moduleName, defaultValue); } constructor(name, category, minimumVersion, description, moduleName, defaultValue) { super(PolicyType.StringEnum, name, category, minimumVersion, description, moduleName); this.defaultValue = defaultValue; } renderADMXElements() { return [ `` // `` ]; } renderADMLPresentationContents() { return `${this.name}`; } } class StringPolicy extends BasePolicy { static from(name, category, minimumVersion, description, moduleName, settingNode) { const type = getStringProperty(settingNode, 'type'); if (type !== 'string') { return undefined; } return new StringPolicy(name, category, minimumVersion, description, moduleName); } constructor(name, category, minimumVersion, description, moduleName) { super(PolicyType.StringEnum, name, category, minimumVersion, description, moduleName); } renderADMXElements() { return [``]; } renderADMLPresentationContents() { return ``; } } class StringEnumPolicy extends BasePolicy { enum_; enumDescriptions; static from(name, category, minimumVersion, description, moduleName, settingNode) { const type = getStringProperty(settingNode, 'type'); if (type !== 'string') { return undefined; } const enum_ = getStringArrayProperty(settingNode, 'enum'); if (!enum_) { return undefined; } if (!isStringArray(enum_)) { throw new Error(`Property 'enum' should not be localized.`); } const enumDescriptions = getStringArrayProperty(settingNode, 'enumDescriptions'); if (!enumDescriptions) { throw new Error(`Missing required 'enumDescriptions' property.`); } else if (!isNlsStringArray(enumDescriptions)) { throw new Error(`Property 'enumDescriptions' should be localized.`); } return new StringEnumPolicy(name, category, minimumVersion, description, moduleName, enum_, enumDescriptions); } constructor(name, category, minimumVersion, description, moduleName, enum_, enumDescriptions) { super(PolicyType.StringEnum, name, category, minimumVersion, description, moduleName); this.enum_ = enum_; this.enumDescriptions = enumDescriptions; } renderADMXElements() { return [ ``, ...this.enum_.map((value, index) => ` ${value}`), `` ]; } renderADMLStrings(translations) { return [ ...super.renderADMLStrings(translations), ...this.enumDescriptions.map(e => this.renderADMLString(e, translations)) ]; } renderADMLPresentationContents() { return ``; } } const IntQ = { Q: `(number) @value`, value(matches) { const match = matches[0]; if (!match) { return undefined; } const value = match.captures.filter(c => c.name === 'value')[0]?.node.text; if (!value) { throw new Error(`Missing required 'value' property.`); } return parseInt(value); } }; const StringQ = { Q: `[ (string (string_fragment) @value) (call_expression function: (identifier) @localizeFn arguments: (arguments (string (string_fragment) @nlsKey) (string (string_fragment) @value)) (#eq? @localizeFn localize)) ]`, value(matches) { const match = matches[0]; if (!match) { return undefined; } const value = match.captures.filter(c => c.name === 'value')[0]?.node.text; if (!value) { throw new Error(`Missing required 'value' property.`); } const nlsKey = match.captures.filter(c => c.name === 'nlsKey')[0]?.node.text; if (nlsKey) { return { value, nlsKey }; } else { return value; } } }; const StringArrayQ = { Q: `(array ${StringQ.Q})`, value(matches) { if (matches.length === 0) { return undefined; } return matches.map(match => { return StringQ.value([match]); }); } }; function getProperty(qtype, node, key) { const query = new Parser.Query(typescript, `( (pair key: [(property_identifier)(string)] @key value: ${qtype.Q} ) (#eq? @key ${key}) )`); return qtype.value(query.matches(node)); } function getIntProperty(node, key) { return getProperty(IntQ, node, key); } function getStringProperty(node, key) { return getProperty(StringQ, node, key); } function getStringArrayProperty(node, key) { return getProperty(StringArrayQ, node, key); } // TODO: add more policy types const PolicyTypes = [ BooleanPolicy, IntPolicy, StringEnumPolicy, StringPolicy, ]; function getPolicy(moduleName, configurationNode, settingNode, policyNode, categories) { const name = getStringProperty(policyNode, 'name'); if (!name) { throw new Error(`Missing required 'name' property.`); } else if (isNlsString(name)) { throw new Error(`Property 'name' should be a literal string.`); } const categoryName = getStringProperty(configurationNode, 'title'); if (!categoryName) { throw new Error(`Missing required 'title' property.`); } else if (!isNlsString(categoryName)) { throw new Error(`Property 'title' should be localized.`); } const categoryKey = `${categoryName.nlsKey}:${categoryName.value}`; let category = categories.get(categoryKey); if (!category) { category = { moduleName, name: categoryName }; categories.set(categoryKey, category); } const minimumVersion = getStringProperty(policyNode, 'minimumVersion'); if (!minimumVersion) { throw new Error(`Missing required 'minimumVersion' property.`); } else if (isNlsString(minimumVersion)) { throw new Error(`Property 'minimumVersion' should be a literal string.`); } const description = getStringProperty(settingNode, 'description'); if (!description) { throw new Error(`Missing required 'description' property.`); } if (!isNlsString(description)) { throw new Error(`Property 'description' should be localized.`); } let result; for (const policyType of PolicyTypes) { if (result = policyType.from(name, category, minimumVersion, description, moduleName, settingNode)) { break; } } if (!result) { throw new Error(`Failed to parse policy '${name}'.`); } return result; } function getPolicies(moduleName, node) { const query = new Parser.Query(typescript, ` ( (call_expression function: (member_expression property: (property_identifier) @registerConfigurationFn) (#eq? @registerConfigurationFn registerConfiguration) arguments: (arguments (object (pair key: [(property_identifier)(string)] @propertiesKey (#eq? @propertiesKey properties) value: (object (pair key: [(property_identifier)(string)] value: (object (pair key: [(property_identifier)(string)] @policyKey (#eq? @policyKey policy) value: (object) @policy )) @setting )) )) @configuration) ) ) `); const categories = new Map(); return query.matches(node).map(m => { const configurationNode = m.captures.filter(c => c.name === 'configuration')[0].node; const settingNode = m.captures.filter(c => c.name === 'setting')[0].node; const policyNode = m.captures.filter(c => c.name === 'policy')[0].node; return getPolicy(moduleName, configurationNode, settingNode, policyNode, categories); }); } async function getFiles(root) { return new Promise((c, e) => { const result = []; const rg = (0, child_process_1.spawn)(ripgrep_1.rgPath, ['-l', 'registerConfiguration\\(', '-g', 'src/**/*.ts', '-g', '!src/**/test/**', root]); const stream = byline(rg.stdout.setEncoding('utf8')); stream.on('data', path => result.push(path)); stream.on('error', err => e(err)); stream.on('end', () => c(result)); }); } function renderADMX(regKey, versions, categories, policies) { versions = versions.map(v => v.replace(/\./g, '_')); return ` ${versions.map(v => ``).join(`\n `)} ${categories.map(c => ``).join(`\n `)} ${policies.map(p => p.renderADMX(regKey)).flat().join(`\n `)} `; } function renderADML(appName, versions, categories, policies, translations) { return ` ${appName} ${versions.map(v => `${appName} >= ${v}`)} ${categories.map(c => renderADMLString('Category', c.moduleName, c.name, translations))} ${policies.map(p => p.renderADMLStrings(translations)).flat().join(`\n `)} ${policies.map(p => p.renderADMLPresentation()).join(`\n `)} `; } function renderGP(policies, translations) { const appName = product.nameLong; const regKey = product.win32RegValueName; const versions = [...new Set(policies.map(p => p.minimumVersion)).values()].sort(); const categories = [...new Set(policies.map(p => p.category))]; return { admx: renderADMX(regKey, versions, categories, policies), adml: [ { languageId: 'en-us', contents: renderADML(appName, versions, categories, policies) }, ...translations.map(({ languageId, languageTranslations }) => ({ languageId, contents: renderADML(appName, versions, categories, policies, languageTranslations) })) ] }; } const Languages = { 'fr': 'fr-fr', 'it': 'it-it', 'de': 'de-de', 'es': 'es-es', 'ru': 'ru-ru', 'zh-hans': 'zh-cn', 'zh-hant': 'zh-tw', 'ja': 'ja-jp', 'ko': 'ko-kr', 'cs': 'cs-cz', 'pt-br': 'pt-br', 'tr': 'tr-tr', 'pl': 'pl-pl', }; async function getSpecificNLS(resourceUrlTemplate, languageId, version) { const resource = { publisher: 'ms-ceintl', name: `vscode-language-pack-${languageId}`, version: `${version[0]}.${version[1]}.${version[2]}`, path: 'extension/translations/main.i18n.json' }; const url = resourceUrlTemplate.replace(/\{([^}]+)\}/g, (_, key) => resource[key]); const res = await fetch(url); if (res.status !== 200) { throw new Error(`[${res.status}] Error downloading language pack ${languageId}@${version}`); } const { contents: result } = await res.json(); return result; } function parseVersion(version) { const [, major, minor, patch] = /^(\d+)\.(\d+)\.(\d+)/.exec(version); return [parseInt(major), parseInt(minor), parseInt(patch)]; } function compareVersions(a, b) { if (a[0] !== b[0]) { return a[0] - b[0]; } if (a[1] !== b[1]) { return a[1] - b[1]; } return a[2] - b[2]; } async function queryVersions(serviceUrl, languageId) { const res = await fetch(`${serviceUrl}/extensionquery`, { method: 'POST', headers: { 'Accept': 'application/json;api-version=3.0-preview.1', 'Content-Type': 'application/json', 'User-Agent': 'VS Code Build', }, body: JSON.stringify({ filters: [{ criteria: [{ filterType: 7, value: `ms-ceintl.vscode-language-pack-${languageId}` }] }], flags: 0x1 }) }); if (res.status !== 200) { throw new Error(`[${res.status}] Error querying for extension: ${languageId}`); } const result = await res.json(); return result.results[0].extensions[0].versions.map(v => parseVersion(v.version)).sort(compareVersions); } async function getNLS(extensionGalleryServiceUrl, resourceUrlTemplate, languageId, version) { const versions = await queryVersions(extensionGalleryServiceUrl, languageId); const nextMinor = [version[0], version[1] + 1, 0]; const compatibleVersions = versions.filter(v => compareVersions(v, nextMinor) < 0); const latestCompatibleVersion = compatibleVersions.at(-1); // order is newest to oldest if (!latestCompatibleVersion) { throw new Error(`No compatible language pack found for ${languageId} for version ${version}`); } return await getSpecificNLS(resourceUrlTemplate, languageId, latestCompatibleVersion); } async function parsePolicies() { const parser = new Parser(); parser.setLanguage(typescript); const files = await getFiles(process.cwd()); const base = path.join(process.cwd(), 'src'); const policies = []; for (const file of files) { const moduleName = path.relative(base, file).replace(/\.ts$/i, '').replace(/\\/g, '/'); const contents = await fs_1.promises.readFile(file, { encoding: 'utf8' }); const tree = parser.parse(contents); policies.push(...getPolicies(moduleName, tree.rootNode)); } return policies; } async function getTranslations() { const extensionGalleryServiceUrl = product.extensionsGallery?.serviceUrl; if (!extensionGalleryServiceUrl) { console.warn(`Skipping policy localization: No 'extensionGallery.serviceUrl' found in 'product.json'.`); return []; } const resourceUrlTemplate = product.extensionsGallery?.resourceUrlTemplate; if (!resourceUrlTemplate) { console.warn(`Skipping policy localization: No 'resourceUrlTemplate' found in 'product.json'.`); return []; } const version = parseVersion(packageJson.version); const languageIds = Object.keys(Languages); return await Promise.all(languageIds.map(languageId => getNLS(extensionGalleryServiceUrl, resourceUrlTemplate, languageId, version) .then(languageTranslations => ({ languageId, languageTranslations })))); } async function main() { const [policies, translations] = await Promise.all([parsePolicies(), getTranslations()]); const { admx, adml } = await renderGP(policies, translations); const root = '.build/policies/win32'; await fs_1.promises.rm(root, { recursive: true, force: true }); await fs_1.promises.mkdir(root, { recursive: true }); await fs_1.promises.writeFile(path.join(root, `${product.win32RegValueName}.admx`), admx.replace(/\r?\n/g, '\n')); for (const { languageId, contents } of adml) { const languagePath = path.join(root, languageId === 'en-us' ? 'en-us' : Languages[languageId]); await fs_1.promises.mkdir(languagePath, { recursive: true }); await fs_1.promises.writeFile(path.join(languagePath, `${product.win32RegValueName}.adml`), contents.replace(/\r?\n/g, '\n')); } } if (require.main === module) { main().catch(err => { console.error(err); process.exit(1); }); } //# sourceMappingURL=policies.js.map