UNPKG

itk-wasm

Version:

High-performance spatial analysis in a web browser, Node.js, and reproducible execution across programming languages and hardware architectures.

611 lines (590 loc) 25.8 kB
import path from 'path' import { markdownTable } from 'markdown-table' import camelCase from '../camel-case.js' import interfaceJsonTypeToInterfaceType from '../interface-json-type-to-interface-type.js' import interfaceJsonTypeToTypeScriptType from './interface-json-type-to-typescript-type.js' import writeIfOverrideNotPresent from '../write-if-override-not-present.js' function readFileIfNotInterfaceType( forNode, interfaceType, varName, indent, isArray ) { if (forNode) { if (isArray) { return `${indent}${varName}.forEach((p) => mountDirs.add(path.dirname(p) as string))\n` } else { return `${indent}mountDirs.add(path.dirname(${varName} as string))\n` } } else { if (interfaceType === 'TextFile') { if (isArray) { return `${indent}const ${varName}File = await Promise.all(${varName}.map(async (v) => {\n${indent}${indent}let vFile = v\n${indent}${indent}if (v instanceof File) {\n${indent}${indent} const vBuffer = await v.arrayBuffer()\n${indent}${indent} vFile = { path: v.name, data: new TextDecoder().decode(vBuffer) }\n${indent}${indent}}\n${indent}${indent}return vFile\n${indent}}))\n` } else { return `${indent}let ${varName}File = ${varName}\n${indent}if (${varName} instanceof File) {\n${indent} const ${varName}Buffer = await ${varName}.arrayBuffer()\n${indent} ${varName}File = { path: ${varName}.name, data: new TextDecoder().decode(${varName}Buffer) }\n${indent}}\n` } } else { if (isArray) { return `${indent}const ${varName}File = await Promise.all(${varName}.map(async (v) => {\n${indent}let vFile = v\n${indent}${indent}if (v instanceof File) {\n${indent}${indent} const vBuffer = await v.arrayBuffer()\n${indent}${indent} vFile = { path: v.name, data: new Uint8Array(vBuffer) }\n${indent}${indent}}${indent}${indent}\nreturn vFile\n${indent}}))\n` } else { return `${indent}let ${varName}File = ${varName}\n${indent}if (${varName} instanceof File) {\n${indent} const ${varName}Buffer = await ${varName}.arrayBuffer()\n${indent} ${varName}File = { path: ${varName}.name, data: new Uint8Array(${varName}Buffer) }\n${indent}}\n` } } } } function functionModule( srcOutputDir, forNode, interfaceJson, modulePascalCase, moduleKebabCase, moduleCamelCase, nodeTextCamel, nodeTextKebab, haveOptions ) { let readmeFunction = '' let functionContent = `import {\n` const usedInterfaceTypes = new Set() let needMountDirs = false const pipelineComponents = ['inputs', 'outputs', 'parameters'] pipelineComponents.forEach((pipelineComponent) => { interfaceJson[pipelineComponent].forEach((value) => { if (interfaceJsonTypeToInterfaceType.has(value.type)) { const interfaceType = interfaceJsonTypeToInterfaceType.get(value.type) if (interfaceType.includes('File') && forNode) { needMountDirs = true return } if ( !(pipelineComponent === 'inputs' && interfaceType === 'BinaryStream') ) { usedInterfaceTypes.add(interfaceType) } } }) }) usedInterfaceTypes.forEach((interfaceType) => { functionContent += ` ${interfaceType},\n` }) if (usedInterfaceTypes.size) { functionContent += ' InterfaceTypes,\n' } functionContent += ' PipelineOutput,\n' functionContent += ' PipelineInput,\n' if (forNode) { functionContent += ' runPipelineNode\n' } else { functionContent += ' runPipeline\n' } functionContent += "} from 'itk-wasm'\n\n" if (haveOptions) { functionContent += `import ${modulePascalCase}${nodeTextCamel}Options from './${moduleKebabCase}${nodeTextKebab}-options.js'\n` } functionContent += `import ${modulePascalCase}${nodeTextCamel}Result from './${moduleKebabCase}${nodeTextKebab}-result.js'\n\n` if (forNode) { functionContent += "import path from 'path'\n" functionContent += "import { fileURLToPath } from 'url'\n\n" } else { functionContent += "import { getPipelinesBaseUrl } from './pipelines-base-url.js'\n" functionContent += "import { getPipelineWorkerUrl } from './pipeline-worker-url.js'\n\n" functionContent += "import { getDefaultWebWorker } from './default-web-worker.js'\n\n" } const readmeParametersTable = [['Parameter', 'Type', 'Description']] functionContent += `/**\n * ${interfaceJson.description}\n *\n` interfaceJson.inputs.forEach((input) => { if (!interfaceJsonTypeToTypeScriptType.has(input.type)) { console.error(`Unexpected input type: ${input.type}`) process.exit(1) } let typescriptType = interfaceJsonTypeToTypeScriptType.get(input.type) const isArray = input.itemsExpectedMax > 1 ? '[]' : '' const fileType = forNode ? 'string' : 'File' if (typescriptType === 'TextFile' || typescriptType === 'BinaryFile') { if (forNode) { typescriptType = `${fileType}${isArray}` } else { typescriptType = `${fileType}${isArray} | ${typescriptType}${isArray}` } } else { typescriptType = `${typescriptType}${isArray}` } functionContent += ` * @param {${typescriptType}} ${camelCase(input.name)} - ${input.description}\n` readmeParametersTable.push([ `\`${camelCase(input.name)}\``, `*${typescriptType}*`, input.description ]) }) const outputFiles = interfaceJson.outputs.filter((o) => { return o.type.includes('FILE') }) outputFiles.forEach((output) => { const isArray = output.itemsExpectedMax > 1 ? '[]' : '' const typescriptType = `string${isArray}` functionContent += ` * @param {${typescriptType}} ${camelCase(output.name)} - ${output.description}\n` readmeParametersTable.push([ `\`${camelCase(output.name)}\``, `*${typescriptType.replaceAll('|', 'or')}*`, output.description ]) }) if (haveOptions) { functionContent += ` * @param {${modulePascalCase}${nodeTextCamel}Options} options - options object\n` } functionContent += ` *\n * @returns {Promise<${modulePascalCase}${nodeTextCamel}Result>} - result object\n` functionContent += ` */\n` readmeFunction += `\n#### ${moduleCamelCase}${nodeTextCamel}\n\n` let functionCall = '' functionCall += `async function ${moduleCamelCase}${nodeTextCamel}(\n` interfaceJson.inputs.forEach((input, index) => { let typescriptType = interfaceJsonTypeToTypeScriptType.get(input.type) const end = index === interfaceJson.inputs.length - 1 && !haveOptions && !outputFiles.length ? '\n' : ',\n' const isArray = input.itemsExpectedMax > 1 ? '[]' : '' const fileType = forNode ? 'string' : 'File' if (typescriptType === 'TextFile' || typescriptType === 'BinaryFile') { if (forNode) { typescriptType = `${fileType}${isArray}` } else { typescriptType = `${fileType}${isArray} | ${typescriptType}${isArray}` } } else { typescriptType = `${typescriptType}${isArray}` } functionCall += ` ${camelCase(input.name)}: ${typescriptType}${end}` }) outputFiles.forEach((output, index) => { const end = index === interfaceJson.outputs.length - 1 && !haveOptions ? '\n' : ',\n' const isArray = output.itemsExpectedMax > 1 ? '[]' : '' const typescriptType = `string${isArray}` functionCall += ` ${camelCase(output.name)}: ${typescriptType}${end}` }) if (haveOptions) { let requiredOptions = '' interfaceJson.parameters.forEach((parameter) => { if (parameter.required) { if ( parameter.itemsExpectedMax > 1 && parameter.type !== 'FLOAT' && parameter.type !== 'INT' && parameter.type !== 'UINT' ) { const typescriptType = interfaceJsonTypeToTypeScriptType.get( parameter.type ) let arrayType = typescriptType === 'TextFile' || typescriptType === 'BinaryFile' ? `${typescriptType}[] | File[] | string[]` : `${typescriptType}[]` if (forNode) { arrayType = typescriptType === 'TextFile' || typescriptType === 'BinaryFile' ? 'string[]' : `${typescriptType}[]` } requiredOptions += ` ${camelCase(parameter.name)}: [] as ${arrayType},` } else { if ( parameter.type === 'FLOAT' || parameter.type === 'INT' || parameter.type === 'UINT' ) { if (!parameter.default || parameter.default === '{}') { requiredOptions += ` ${camelCase(parameter.name)}: [] as number[],` } else { requiredOptions += ` ${camelCase(parameter.name)}: ${parameter.default},` } } } } }) if (requiredOptions.length > 0) { requiredOptions += ' ' } functionCall += ` options: ${modulePascalCase}${nodeTextCamel}Options = {${requiredOptions}}\n) : Promise<${modulePascalCase}${nodeTextCamel}Result>` } else { functionCall += `) : Promise<${modulePascalCase}${nodeTextCamel}Result>` } readmeFunction += `*${interfaceJson.description}*\n\n` readmeFunction += `\`\`\`ts\n${functionCall}\n\`\`\`\n\n` readmeFunction += markdownTable(readmeParametersTable, { align: ['c', 'c', 'l'] }) + '\n' functionContent += functionCall functionContent += ' {\n\n' if (needMountDirs) { functionContent += ' const mountDirs: Set<string> = new Set()\n\n' } if (!forNode) { interfaceJson.outputs.forEach((output) => { if (interfaceJsonTypeToInterfaceType.has(output.type)) { const interfaceType = interfaceJsonTypeToInterfaceType.get(output.type) const isArray = output.itemsExpectedMax > 1 if (interfaceType.includes('File')) { const camel = camelCase(output.name) if (isArray) { const defaultData = interfaceType === 'BinaryFile' ? 'new Uint8Array()' : "''" functionContent += ` const ${camel}PipelineOutputs = ${camel}.map((p) => { return { type: InterfaceTypes.${interfaceType}, data: { path: p, data: ${defaultData} }}})\n` } } } }) } let haveArray = false functionContent += ' const desiredOutputs: Array<PipelineOutput> = [\n' interfaceJson.outputs.forEach((output) => { if (interfaceJsonTypeToInterfaceType.has(output.type)) { const interfaceType = interfaceJsonTypeToInterfaceType.get(output.type) if (!forNode && interfaceType.includes('File')) { const camel = camelCase(output.name) const defaultData = interfaceType === 'BinaryFile' ? 'new Uint8Array()' : "''" const isArray = output.itemsExpectedMax > 1 if (isArray) { haveArray = true functionContent += ` ...${camel}PipelineOutputs,\n` } else { functionContent += ` { type: InterfaceTypes.${interfaceType}, data: { path: ${camel}, data: ${defaultData} }},\n` } } else if (!interfaceType.includes('File')) { functionContent += ` { type: InterfaceTypes.${interfaceType} },\n` } } }) functionContent += ' ]\n\n' if (haveArray) { functionContent += ` let outputIndex = 0\n` interfaceJson.outputs.forEach((output) => { if (interfaceJsonTypeToInterfaceType.has(output.type)) { const interfaceType = interfaceJsonTypeToInterfaceType.get(output.type) if (!forNode && interfaceType.includes('File')) { const camel = camelCase(output.name) const isArray = output.itemsExpectedMax > 1 if (isArray) { functionContent += ` const ${camel}Start = outputIndex\n` functionContent += ` outputIndex += ${camel}.length\n` functionContent += ` const ${camel}End = outputIndex\n` } else { functionContent += ` const ${camel}Index = outputIndex\n` functionContent += ` ++outputIndex\n` } } else if (!interfaceType.includes('File')) { functionContent += ` const ${camel}Index = outputIndex\n` functionContent += ` ++outputIndex\n` } } }) functionContent += '\n' } interfaceJson.inputs.forEach((input) => { if (interfaceJsonTypeToInterfaceType.has(input.type)) { const interfaceType = interfaceJsonTypeToInterfaceType.get(input.type) if (interfaceType.includes('File')) { const camel = camelCase(input.name) const isArray = input.itemsExpectedMax > 1 functionContent += readFileIfNotInterfaceType( forNode, interfaceType, camel, ' ', isArray ) if (!forNode && isArray) { functionContent += ` const ${camel}PipelineInputs = ${camel}File.map(i => { return { type: InterfaceTypes.${interfaceType}, data: i as ${interfaceType} }})\n` } } } }) functionContent += ' const inputs: Array<PipelineInput> = [\n' interfaceJson.inputs.forEach((input) => { if (interfaceJsonTypeToInterfaceType.has(input.type)) { const interfaceType = interfaceJsonTypeToInterfaceType.get(input.type) const camel = camelCase(input.name) const isArray = input.itemsExpectedMax > 1 if (interfaceType.includes('File')) { if (!forNode) { if (isArray) { functionContent += ` ...${camel}PipelineInputs,\n` } else { functionContent += ` { type: InterfaceTypes.${interfaceType}, data: ${camel}File as ${interfaceType} },\n` } } } else { let data = camel if (interfaceType.includes('Stream')) { data = `{ data: ${camel} } ` } else if (interfaceType === 'JsonCompatible') { data = `${camel} as JsonCompatible ` } functionContent += ` { type: InterfaceTypes.${interfaceType}, data: ${data} },\n` } } }) functionContent += ' ]\n\n' let inputCount = 0 functionContent += ' const args = []\n' functionContent += ' // Inputs\n' interfaceJson.inputs.forEach((input) => { const camel = camelCase(input.name) if (interfaceJsonTypeToInterfaceType.has(input.type)) { const interfaceType = interfaceJsonTypeToInterfaceType.get(input.type) const isArray = input.itemsExpectedMax > 1 let name = ` const ${camel}Name = '${inputCount.toString()}'\n` if (interfaceType.includes('File')) { if (isArray) { name = '' } else { if (forNode) { name = ` const ${camel}Name = ${camel}\n` } else { name = ` const ${camel}Name = (${camel}File as ${interfaceType}).path\n` } } } functionContent += name if (isArray) { if (forNode) { functionContent += ` ${camel}.forEach((p) => args.push(p))\n` } else { functionContent += ` ${camel}File.forEach((p) => args.push((p as ${interfaceType}).path))\n` } if (forNode && interfaceType.includes('File')) { functionContent += ` ${camel}.forEach((p) => mountDirs.add(path.dirname(p)))\n` } } else { functionContent += ` args.push(${camel}Name)\n` if (forNode && interfaceType.includes('File')) { functionContent += ` mountDirs.add(path.dirname(${camel}Name))\n` } } functionContent += '\n' inputCount++ } else { functionContent += ` args.push(${camel}.toString())\n\n` } }) let outputCount = 0 functionContent += ' // Outputs\n' interfaceJson.outputs.forEach((output) => { const camel = camelCase(output.name) if (interfaceJsonTypeToInterfaceType.has(output.type)) { const interfaceType = interfaceJsonTypeToInterfaceType.get(output.type) let name = ` const ${camel}Name = '${outputCount.toString()}'\n` const isArray = output.itemsExpectedMax > 1 if (interfaceType.includes('File')) { if (isArray) { name = '' } else { name = ` const ${camel}Name = ${camel}\n` } } functionContent += name if (isArray) { functionContent += ` ${camel}.forEach((p) => args.push(p))\n` if (forNode && interfaceType.includes('File')) { functionContent += ` ${camel}.forEach((p) => mountDirs.add(path.dirname(p)))\n` } } else { functionContent += ` args.push(${camel}Name)\n` if (forNode && interfaceType.includes('File')) { functionContent += ` mountDirs.add(path.dirname(${camel}Name))\n` } } functionContent += '\n' if (!(forNode && interfaceType.includes('File'))) { outputCount++ } } else { functionContent += ` args.push(${camel}.toString())\n\n` } }) functionContent += ' // Options\n' functionContent += " args.push('--memory-io')\n" interfaceJson.parameters.forEach((parameter) => { if (parameter.name === 'memory-io' || parameter.name === 'version') { // Internal return } const camel = camelCase(parameter.name) functionContent += ` if (options.${camel}) {\n` if (parameter.type === 'BOOL') { functionContent += ` options.${camel} && args.push('--${parameter.name}')\n` } else if (parameter.itemsExpectedMax > 1) { functionContent += ` if(options.${camel}.length < ${parameter.itemsExpectedMin}) {\n` functionContent += ` throw new Error('"${parameter.name}" option must have a length > ${parameter.itemsExpectedMin}')\n` functionContent += ' }\n' functionContent += ` args.push('--${parameter.name}')\n\n` if (forNode) { functionContent += ` options.${camel}.forEach((value) => {\n` } else { functionContent += ` await Promise.all(options.${camel}.map(async (value) => {\n` } if (interfaceJsonTypeToInterfaceType.has(parameter.type)) { const interfaceType = interfaceJsonTypeToInterfaceType.get( parameter.type ) if (interfaceType.includes('File')) { // for files if (forNode) { functionContent += ` mountDirs.add(path.dirname(value as string))\n` functionContent += ' args.push(value as string)\n' } else { functionContent += readFileIfNotInterfaceType( forNode, interfaceType, 'value', ' ', false ) functionContent += ` inputs.push({ type: InterfaceTypes.${interfaceType}, data: valueFile as ${interfaceType} })\n` functionContent += ` const name = value instanceof File ? value.name : (valueFile as ${interfaceType}).path\n` functionContent += ' args.push(name)\n' } } else if (interfaceType.includes('Stream')) { // for Streams functionContent += ' const inputCountString = inputs.length.toString()\n' functionContent += ` inputs.push({ type: InterfaceTypes.${interfaceType}, data: { data: value } })\n` functionContent += ' args.push(inputCountString)\n\n' } else { // Image, Mesh, PointSet, PolyData, TransformList, JsonCompatible functionContent += ' const inputCountString = inputs.length.toString()\n' functionContent += ` inputs.push({ type: InterfaceTypes.${interfaceType}, data: value as ${interfaceType} })\n` functionContent += ' args.push(inputCountString)\n\n' } } else { if (parameter.type.startsWith('TEXT:{')) { const choices = parameter.type.split('{')[1].split('}')[0].split(',') functionContent += ` if (![${choices.map((c) => `'${c}'`).join(', ')}].includes(options.${camel})) {\n` functionContent += ` throw new Error('"${parameter.name}" option must be one of ${choices.join(', ')}')\n` functionContent += ' }\n' } functionContent += ' args.push(value.toString())\n' } functionContent += forNode ? ' })\n' : ' }))\n' } else { if (interfaceJsonTypeToInterfaceType.has(parameter.type)) { const interfaceType = interfaceJsonTypeToInterfaceType.get( parameter.type ) if (interfaceType.includes('File')) { // for files functionContent += ` const ${camel} = options.${camel}\n` functionContent += readFileIfNotInterfaceType( forNode, interfaceType, camel, ' ', false ) functionContent += ` args.push('--${parameter.name}')\n\n` let name = ` const name = ${camel} as string\n` if (!forNode) { name = ` const name = ${camel} instanceof File ? ${camel}.name : (${camel} as ${interfaceType}).path\n` functionContent += ` inputs.push({ type: InterfaceTypes.${interfaceType}, data: ${camel}File as ${interfaceType} })\n` } functionContent += name functionContent += ' args.push(name)\n\n' } else if (interfaceType.includes('Stream')) { // for Streams functionContent += ' const inputCountString = inputs.length.toString()\n' functionContent += ` inputs.push({ type: InterfaceTypes.${interfaceType}, data: { data: options.${camel} } })\n` functionContent += ` args.push('--${parameter.name}', inputCountString)\n\n` } else { // Image, Mesh, PointSet, PolyData, TransformList, JsonCompatible functionContent += ' const inputCountString = inputs.length.toString()\n' functionContent += ` inputs.push({ type: InterfaceTypes.${interfaceType}, data: options.${camel} as ${interfaceType} })\n` functionContent += ` args.push('--${parameter.name}', inputCountString)\n\n` } } else { if (parameter.type.startsWith('TEXT:{')) { const choices = parameter.type.split('{')[1].split('}')[0].split(',') functionContent += ` if (![${choices.map((c) => `'${c}'`).join(', ')}].includes(options.${camel})) {\n` functionContent += ` throw new Error('"${parameter.name}" option must be one of ${choices.join(', ')}')\n` functionContent += ' }\n' } functionContent += ` args.push('--${parameter.name}', options.${camel}.toString())\n\n` } } functionContent += ' }\n' }) const outputsVar = interfaceJson.outputs.filter( (o) => !o.type.includes('FILE') || !forNode ).length ? ' outputs\n' : '' if (forNode) { functionContent += `\n const pipelinePath = path.join(path.dirname(fileURLToPath(import.meta.url)), 'pipelines', '${moduleKebabCase}')\n\n` const mountDirsArg = needMountDirs ? ', mountDirs' : '' functionContent += ` const {\n returnValue,\n stderr,\n${outputsVar} } = await runPipelineNode(pipelinePath, args, desiredOutputs, inputs${mountDirsArg})\n` } else { functionContent += `\n const pipelinePath = '${moduleKebabCase}'\n\n` functionContent += ` let workerToUse = options?.webWorker\n if (workerToUse === undefined) {\n workerToUse = await getDefaultWebWorker()\n }\n` functionContent += ` const {\n webWorker: usedWebWorker,\n returnValue,\n stderr,\n${outputsVar} } = await runPipeline(pipelinePath, args, desiredOutputs, inputs, { pipelineBaseUrl: getPipelinesBaseUrl(), pipelineWorkerUrl: getPipelineWorkerUrl(), webWorker: workerToUse, noCopy: options?.noCopy })\n` } functionContent += ' if (returnValue !== 0 && stderr !== "") {\n throw new Error(stderr)\n }\n\n' functionContent += ' const result = {\n' if (!forNode) { functionContent += ' webWorker: usedWebWorker as Worker,\n' } outputCount = 0 interfaceJson.outputs.forEach((output) => { const camel = camelCase(output.name) const interfaceType = interfaceJsonTypeToInterfaceType.get(output.type) const outputIndex = haveArray ? `${camel}Index` : outputCount.toString() if ( interfaceType.includes('TextStream') || interfaceType.includes('BinaryStream') ) { if (haveArray) { const isArray = output.itemsExpectedMax > 1 if (isArray) { functionContent += ` ${camel}: (outputs.slice(${camel}Start, ${camel}End).map(o => (o?.data as ${interfaceType})?.data)),\n` } else { functionContent += ` ${camel}: (outputs[${camel}Index]?.data as ${interfaceType}).data),\n` } } else { functionContent += ` ${camel}: (outputs[${outputIndex}]?.data as ${interfaceType}).data,\n` } } else if (forNode && interfaceType.includes('File')) { // Written to disk } else { if (haveArray) { const isArray = output.itemsExpectedMax > 1 if (isArray) { functionContent += ` ${camel}: outputs.slice(${camel}Start, ${camel}End).map(o => (o?.data as ${interfaceType})),\n` } else { functionContent += ` ${camel}: outputs[${camel}Index]?.data as ${interfaceType},\n` } } else { functionContent += ` ${camel}: outputs[${outputIndex}]?.data as ${interfaceType},\n` } } if (!(forNode && interfaceType.includes('File'))) { outputCount++ } }) functionContent += ' }\n' functionContent += ' return result\n' functionContent += `}\n\nexport default ${moduleCamelCase}${nodeTextCamel}\n` writeIfOverrideNotPresent( path.join(srcOutputDir, `${moduleKebabCase}${nodeTextKebab}.ts`), functionContent ) return { readmeFunction, usedInterfaceTypes } } export default functionModule