diff --git a/utils/generate_dotnet_channels.js b/utils/generate_dotnet_channels.js index 6d0c462bc4..71e35aa5a9 100644 --- a/utils/generate_dotnet_channels.js +++ b/utils/generate_dotnet_channels.js @@ -25,141 +25,7 @@ const channels = new Set(); const inherits = new Map(); const mixins = new Map(); -function raise(item) { - throw new Error('Invalid item: ' + JSON.stringify(item, null, 2)); -} - -function titleCase(name) { - return name[0].toUpperCase() + name.substring(1); -} - -function mapType(type) { - if (type === 'SerializedValue') - return 'System.Text.Json.JsonElement'; - if (type === 'boolean') - return 'bool'; - if (type === 'number') - return 'int'; - // TODO: keep the same names in .NET as upstream - if (type === 'ResourceTiming') - return 'RequestTimingResult'; - if (type === 'LifecycleEvent') - return 'WaitUntilState'; - if (type === 'NameValue') - return 'HeaderEntry'; - return type; -} - -function nullableSuffix(inner) { - if (['int', 'boolean'].includes(inner.ts)) - return inner.optional ? '?' : ''; - return ''; -} - -function inlineType(type, indent = '', wrapEnums = false) { - if (typeof type === 'string') { - const optional = type.endsWith('?'); - if (optional) - type = type.substring(0, type.length - 1); - if (type === 'binary') - return { ts: 'byte[]', scheme: 'tArray(tByte)', optional }; - if (type === 'json') - return { ts: 'any', scheme: 'tAny', optional }; - if (['string', 'boolean', 'number', 'undefined'].includes(type)) { - return { ts: mapType(type), scheme: `t${titleCase(type)}`, optional }; - } - if (channels.has(type)) - return { ts: `Core.${type}`, scheme: `tChannel('${type}')` , optional }; - if (type === 'Channel') - return { ts: `Channel`, scheme: `tChannel('*')`, optional }; - return { ts: mapType(type), scheme: `tType('${type}')`, optional }; - } - if (type.type.startsWith('array')) { - const optional = type.type.endsWith('?'); - const inner = inlineType(type.items, indent, true); - return { ts: `List<${inner.ts}>`, scheme: `tArray(${inner.scheme})`, optional }; - } - if (type.type.startsWith('enum')) { - if (type.literals.includes('networkidle')) - return { ts: 'LoadState', scheme: `tString`, optional: false }; - return { ts: 'string', scheme: `tString`, optional: false }; - } - if (type.type.startsWith('object')) { - const optional = type.type.endsWith('?'); - const custom = processCustomType(type, optional); - if (custom) - return custom; - const inner = properties(type.properties, indent + ' '); - return { - ts: `{\n${inner.ts}\n${indent}}`, - scheme: `tObject({\n${inner.scheme}\n${indent}})`, - optional - }; - } - raise(type); -} - -function properties(properties, indent, onlyOptional) { - const ts = []; - const scheme = []; - const visitProperties = props => { - for (const [name, value] of Object.entries(props)) { - if (name === 'android' || name === 'electron') - continue; - if (name.startsWith('$mixin')) { - visitProperties(mixins.get(value).properties); - continue; - } - const inner = inlineType(value, indent); - if (onlyOptional && !inner.optional) - continue; - ts.push(''); - ts.push(`${indent}[JsonPropertyName("${name}")]`) - ts.push(`${indent}public ${inner.ts}${nullableSuffix(inner)} ${toTitleCase(name)} { get; set; }`); - const wrapped = inner.optional ? `tOptional(${inner.scheme})` : inner.scheme; - scheme.push(`${indent}${name}: ${wrapped},`); - } - }; - visitProperties(properties); - return { ts: ts.join('\n'), scheme: scheme.join('\n') }; -} - -function objectType(props, indent, onlyOptional = false) { - if (!Object.entries(props).length) - return { ts: `${indent}{\n${indent}}`, scheme: `tObject({})` }; - const inner = properties(props, indent + ' ', onlyOptional); - return { ts: `${indent}{${inner.ts}\n${indent}}`, scheme: `tObject({\n${inner.scheme}\n${indent}})` }; -} - -const yml = fs.readFileSync(path.join(__dirname, '..', 'packages', 'playwright-core', 'src', 'protocol', 'protocol.yml'), 'utf-8'); -const protocol = yaml.parse(yml); - -for (const [name, value] of Object.entries(protocol)) { - if (value.type === 'interface') { - channels.add(name); - if (value.extends) - inherits.set(name, value.extends); - } - if (value.type === 'mixin') - mixins.set(name, value); -} - -if (!process.argv[2]) { - console.error('.NET repository needs to be specified as an argument.\n'+ `Usage: node ${path.relative(process.cwd(), __filename)} ../playwright-dotnet/src/Playwright/`); - process.exit(1); -} - -const dir = path.join(process.argv[2], 'Transport', 'Protocol', 'Generated') -fs.mkdirSync(dir, { recursive: true }); - -for (const [name, item] of Object.entries(protocol)) { - if (item.type === 'interface') { - const channelName = name; - const channels_ts = []; - const init = objectType(item.initializer || {}, ' '); - const initializerName = channelName + 'Initializer'; - const superName = inherits.get(name); - channels_ts.push(`/* +const COPYRIGHT_HEADER = `/* * MIT License * * Copyright (c) Microsoft Corporation. @@ -182,18 +48,174 @@ for (const [name, item] of Object.entries(protocol)) { * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ -`) - channels_ts.push('using System.Collections.Generic;'); - channels_ts.push('using System.Text.Json.Serialization;') - channels_ts.push(``); - channels_ts.push(`namespace Microsoft.Playwright.Transport.Protocol`); - channels_ts.push(`{`); - channels_ts.push(` internal class ${initializerName}${superName ? ' : ' + superName + 'Initializer' : ''}`); - channels_ts.push(init.ts); - channels_ts.push(`}`); - channels_ts.push(``); - writeFile(`${initializerName}.cs`, channels_ts.join('\n')); +`; + +function raise(item) { + throw new Error('Invalid item: ' + JSON.stringify(item, null, 2)); +} + +function titleCase(name) { + return name[0].toUpperCase() + name.substring(1); +} + +function mapType(type) { + if (type === 'SerializedValue') + return 'System.Text.Json.JsonElement'; + if (type === 'boolean') + return 'bool'; + if (type === 'number') + return 'int'; + // TODO: keep the same names in .NET as upstream + if (type === 'ResourceTiming') + return 'RequestTimingResult'; + if (type === 'LifecycleEvent') + return 'WaitUntilState'; + return type; +} + +function nullableSuffix(inner) { + if (['int', 'boolean'].includes(inner.ts)) + return inner.optional ? '?' : ''; + return ''; +} + +function inlineType(type, indent = '', name, level) { + if (typeof type === 'string') { + const optional = type.endsWith('?'); + if (optional) + type = type.substring(0, type.length - 1); + if (type === 'binary') + return { ts: 'byte[]', scheme: 'tArray(tByte)', optional }; + if (type === 'json') + return { ts: 'any', scheme: 'tAny', optional }; + if (['string', 'boolean', 'number', 'undefined'].includes(type)) + return { ts: mapType(type), scheme: `t${titleCase(type)}`, optional }; + if (channels.has(type)) + return { ts: `Core.${type}`, scheme: `tChannel('${type}')` , optional }; + if (type === 'Channel') + return { ts: `Channel`, scheme: `tChannel('*')`, optional }; + return { ts: mapType(type), scheme: `tType('${type}')`, optional }; } + if (type.type.startsWith('array')) { + const optional = type.type.endsWith('?'); + const inner = inlineType(type.items, indent, name, level); + return { ts: `List<${inner.ts}>`, scheme: `tArray(${inner.scheme})`, optional }; + } + if (type.type.startsWith('enum')) { + if (type.literals.includes('networkidle')) + return { ts: 'LoadState', scheme: `tString`, optional: false }; + return { ts: 'string', scheme: `tString`, optional: false }; + } + if (type.type.startsWith('object')) { + const optional = type.type.endsWith('?'); + + const custom = processCustomType(type, optional); + if (custom) + return custom; + if (level >= 1) { + const inner = properties(type.properties, ' ', false, name, level); + writeCSharpClass(name, null, ' {' + inner.ts + '\n }'); + return { ts: name, scheme: 'tObject()', optional }; + } + + const inner = properties(type.properties, indent + ' ', false, name, level); + return { + ts: `{\n${inner.ts}\n${indent}}`, + scheme: `tObject({\n${inner.scheme}\n${indent}})`, + optional + }; + } + raise(type); +} + +function properties(properties, indent, onlyOptional, parentName, level) { + const ts = []; + const scheme = []; + const visitProperties = (props, parentName) => { + for (const [name, value] of Object.entries(props)) { + if (name === 'android' || name === 'electron') + continue; + if (name.startsWith('$mixin')) { + visitProperties(mixins.get(value).properties, parentName + toTitleCase(name)); + continue; + } + const inner = inlineType(value, indent, parentName + toTitleCase(name), level + 1); + if (onlyOptional && !inner.optional) + continue; + ts.push(''); + ts.push(`${indent}[JsonPropertyName("${name}")]`); + ts.push(`${indent}public ${inner.ts}${nullableSuffix(inner)} ${toTitleCase(name)} { get; set; }`); + const wrapped = inner.optional ? `tOptional(${inner.scheme})` : inner.scheme; + scheme.push(`${indent}${name}: ${wrapped},`); + } + }; + visitProperties(properties, parentName); + return { ts: ts.join('\n'), scheme: scheme.join('\n') }; +} + +function objectType(props, indent, onlyOptional = false, parentName = '') { + if (!Object.entries(props).length) + return { ts: `${indent}{\n${indent}}`, scheme: `tObject({})` }; + const inner = properties(props, indent + ' ', onlyOptional, parentName, 0); + return { ts: `${indent}{${inner.ts}\n${indent}}`, scheme: `tObject({\n${inner.scheme}\n${indent}})` }; +} + +const yml = fs.readFileSync(path.join(__dirname, '..', 'packages', 'playwright-core', 'src', 'protocol', 'protocol.yml'), 'utf-8'); +const protocol = yaml.parse(yml); + +for (const [name, value] of Object.entries(protocol)) { + if (value.type === 'interface') { + channels.add(name); + if (value.extends) + inherits.set(name, value.extends); + } + if (value.type === 'mixin') + mixins.set(name, value); +} + +if (!process.argv[2]) { + console.error('.NET repository needs to be specified as an argument.\n' + `Usage: node ${path.relative(process.cwd(), __filename)} ../playwright-dotnet/src/Playwright/`); + process.exit(1); +} + +const dir = path.join(process.argv[2], 'Transport', 'Protocol', 'Generated'); +fs.mkdirSync(dir, { recursive: true }); + +for (const [name, item] of Object.entries(protocol)) { + if (item.type === 'interface') { + const init = objectType(item.initializer || {}, ' '); + const initializerName = name + 'Initializer'; + const superName = inherits.has(name) ? inherits.get(name) + 'Initializer' : null; + writeCSharpClass(initializerName, superName, init.ts); + } else if (item.type === 'object') { + if (Object.keys(item.properties).length === 0) + continue; + const init = objectType(item.properties, ' ', false, name); + writeCSharpClass(name, null, init.ts); + } +} + +/** + * + * @param {string} className + * @param {string|undefined} inheritFrom + * @param {any} serializedProperties + */ +function writeCSharpClass(className, inheritFrom, serializedProperties) { + if (className === 'SerializedArgument') + return; + const channels_ts = []; + channels_ts.push(COPYRIGHT_HEADER); + channels_ts.push('using System.Collections.Generic;'); + channels_ts.push('using System.Text.Json.Serialization;'); + channels_ts.push(``); + channels_ts.push(`namespace Microsoft.Playwright.Transport.Protocol`); + channels_ts.push(`{`); + channels_ts.push(` internal class ${className}${inheritFrom ? ' : ' + inheritFrom : ''}`); + channels_ts.push(serializedProperties); + channels_ts.push(`}`); + channels_ts.push(``); + writeFile(`${className}.cs`, channels_ts.join('\n')); } function writeFile(file, content) { @@ -212,24 +234,24 @@ function processCustomType(type, optional) { if (type.properties.name && type.properties.value && inlineType(type.properties.name).ts === 'string' - && inlineType(type.properties.value).ts === 'string') { + && inlineType(type.properties.value).ts === 'string') return { ts: 'HeaderEntry', scheme: 'tObject()', optional }; - } + if (type.properties.width && type.properties.height && inlineType(type.properties.width).ts === 'int' - && inlineType(type.properties.height).ts === 'int') { + && inlineType(type.properties.height).ts === 'int') return { ts: 'ViewportSize', scheme: 'tObject()', optional }; - } + if (type.properties.url && type.properties.lineNumber && inlineType(type.properties.url).ts === 'string' - && inlineType(type.properties.lineNumber).ts === 'int') { + && inlineType(type.properties.lineNumber).ts === 'int') return { ts: 'ConsoleMessageLocation', scheme: 'tObject()', optional }; - } + if (type.properties.name && type.properties.descriptor - && inlineType(type.properties.name).ts === 'string') { + && inlineType(type.properties.name).ts === 'string') return { ts: 'DeviceDescriptorEntry', scheme: 'tObject()', optional }; - } + }