chore: add windmill-utils-internal package (#6299)

* add utils package

* naming

* cleaning

* add docs

* remove log

* use autogenerated types

* remove old

* fix

* cleaning

* add docs
This commit is contained in:
centdix
2025-07-31 02:10:58 +02:00
committed by GitHub
parent cbd3fecae2
commit 4aaf2cd34d
15 changed files with 794 additions and 0 deletions
+86
View File
@@ -0,0 +1,86 @@
# Dependencies
node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Build outputs
dist/
build/
*.tsbuildinfo
# Generated client
src/gen/
# Environment variables
.env
.env.local
.env.*.local
# IDE/Editor files
.vscode/
.idea/
*.swp
*.swo
*~
# OS generated files
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
# Logs
*.log
logs/
# Runtime data
pids/
*.pid
*.seed
*.pid.lock
# Coverage directory used by tools like istanbul
coverage/
*.lcov
# nyc test coverage
.nyc_output
# Dependency directories
jspm_packages/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# TypeScript cache
*.tsbuildinfo
# Temporary folders
tmp/
temp/
+26
View File
@@ -0,0 +1,26 @@
#!/usr/bin/env bash
brew install gnu-sed
script_dirpath="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
output_dirpath="${script_dirpath}/src/gen"
rm -rf "${output_dirpath}"
npx --yes @hey-api/openapi-ts@0.53.1 --input "${script_dirpath}/../../backend/windmill-api/openapi.yaml" --output "${output_dirpath}" --useOptions --client legacy/fetch --schemas false
cat <<EOF - "${output_dirpath}/core/OpenAPI.ts" > temp_file && mv temp_file "${output_dirpath}/core/OpenAPI.ts"
const getEnv = (key: string) => {
return process.env[key]
};
const baseUrl = getEnv("BASE_INTERNAL_URL") ?? getEnv("BASE_URL") ?? "http://localhost:8000";
const baseUrlApi = (baseUrl ?? '') + "/api";
EOF
gsed -i 's/WITH_CREDENTIALS: false/WITH_CREDENTIALS: true/g' "${output_dirpath}/core/OpenAPI.ts"
gsed -i 's/TOKEN: undefined/TOKEN: getEnv("WM_TOKEN")/g' "${output_dirpath}/core/OpenAPI.ts"
gsed -i "s/BASE: '\/api'/BASE: baseUrlApi/g" "${output_dirpath}/core/OpenAPI.ts"
find "${output_dirpath}" -name "*.ts" -exec gsed -i -E "s/(import.*from[[:space:]]*['\"][^'\"]+)(['\"])/\1.ts\2/g" {} \;
find "${output_dirpath}" -name "*.ts" -exec gsed -i -E "s/(export.*from[[:space:]]*['\"][^'\"]+)(['\"])/\1.ts\2/g" {} \;
+24
View File
@@ -0,0 +1,24 @@
#!/usr/bin/env bash
script_dirpath="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
output_dirpath="${script_dirpath}/src/gen"
rm -rf "${output_dirpath}"
npx --yes @hey-api/openapi-ts@0.53.1 --input "${script_dirpath}/../../backend/windmill-api/openapi.yaml" --output "${output_dirpath}" --useOptions --client legacy/fetch --schemas false
cat <<EOF - "${output_dirpath}/core/OpenAPI.ts" > temp_file && mv temp_file "${output_dirpath}/core/OpenAPI.ts"
const getEnv = (key: string) => {
return process.env[key]
};
const baseUrl = getEnv("BASE_INTERNAL_URL") ?? getEnv("BASE_URL") ?? "http://localhost:8000";
const baseUrlApi = (baseUrl ?? '') + "/api";
EOF
sed -i 's/WITH_CREDENTIALS: false/WITH_CREDENTIALS: true/g' "${output_dirpath}/core/OpenAPI.ts"
sed -i 's/TOKEN: undefined/TOKEN: getEnv("WM_TOKEN")/g' "${output_dirpath}/core/OpenAPI.ts"
sed -i "s/BASE: '\/api'/BASE: baseUrlApi/g" "${output_dirpath}/core/OpenAPI.ts"
find "${output_dirpath}" -name "*.ts" -exec sed -i -E "s/(import.*from[[:space:]]*['\"][^'\"]+)(['\"])/\1.ts\2/g" {} \;
find "${output_dirpath}" -name "*.ts" -exec sed -i -E "s/(export.*from[[:space:]]*['\"][^'\"]+)(['\"])/\1.ts\2/g" {} \;
+48
View File
@@ -0,0 +1,48 @@
{
"name": "windmill-utils-internal",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "windmill-utils-internal",
"version": "1.0.0",
"license": "Apache 2.0",
"devDependencies": {
"@types/node": "^24.1.0",
"typescript": "^5.0.0"
}
},
"node_modules/@types/node": {
"version": "24.1.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.1.0.tgz",
"integrity": "sha512-ut5FthK5moxFKH2T1CUOC6ctR67rQRvvHdFLCD2Ql6KXmMuCrjsSsRI9UsLCm9M18BMwClv4pn327UvB7eeO1w==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~7.8.0"
}
},
"node_modules/typescript": {
"version": "5.8.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/undici-types": {
"version": "7.8.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz",
"integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==",
"dev": true,
"license": "MIT"
}
}
}
+24
View File
@@ -0,0 +1,24 @@
{
"name": "windmill-utils-internal",
"version": "1.0.0",
"description": "Internal utility functions for Windmill",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
"build": "./gen_wm_client.sh && tsc",
"build-mac": "./gen_wm_client-mac.sh && tsc",
"prepublishOnly": "npm run build"
},
"keywords": [
"windmill"
],
"author": "Ruben Fiszel",
"license": "Apache 2.0",
"devDependencies": {
"@types/node": "^24.1.0",
"typescript": "^5.0.0"
},
"files": [
"dist/**/*"
]
}
@@ -0,0 +1,47 @@
// constants.ts
/**
* Cross-platform path separator constants
* Compatible with both Node.js and Deno environments
*/
/**
* Detects if the current platform is Windows by checking various environment indicators.
* Uses multiple fallback methods to ensure compatibility across Node.js, Deno, and browser environments.
*
* @returns True if running on Windows, false otherwise
*/
const isWindows = (() => {
// Try Node.js process.platform first (most reliable)
if (typeof process !== "undefined" && process.platform) {
return process.platform === "win32";
}
// Try Deno specific detection via globalThis
if (typeof globalThis !== "undefined" && "Deno" in globalThis) {
return (globalThis as any).Deno?.build?.os === "windows";
}
// Try navigator.platform (browser/some Deno contexts)
// @ts-ignore
if (typeof navigator !== "undefined" && navigator.platform) {
// @ts-ignore
return navigator.platform.toLowerCase().includes("win");
}
// Fallback - assume Unix-like (safest default)
return false;
})();
/**
* Path separator constant - equivalent to Deno's SEPARATOR or Node's path.sep
* On Windows: "\"
* On Unix-like systems: "/"
*/
export const SEP = isWindows ? "\\" : "/";
/**
* Path delimiter constant for environment variables like PATH
* On Windows: ";"
* On Unix-like systems: ":"
*/
export const DELIMITER = isWindows ? ";" : ":";
+14
View File
@@ -0,0 +1,14 @@
/**
* @fileoverview Main entry point for windmill-utils-internal package
*
* This module provides utilities for handling Windmill flows, scripts, and schemas:
* - Inline script extraction and replacement
* - Path utilities for different programming languages
* - Schema parsing and conversion utilities
* - Cross-platform path constants
*/
export * from "./inline-scripts";
export * from "./path-utils";
export * from "./parse";
export { SEP, DELIMITER } from "./constants";
@@ -0,0 +1,107 @@
import { assignPath } from "../path-utils";
import { SEP } from "../constants";
import { FlowModule } from "../gen/types.gen";
/**
* Represents an inline script extracted from a flow module
*/
interface InlineScript {
/** File path where the script content should be written */
path: string;
/** The actual script content */
content: string;
}
/**
* Extracts inline scripts from flow modules, converting them to separate files
* and replacing the original content with file references.
*
* @param modules - Array of flow modules to process
* @param mapping - Optional mapping of module IDs to custom file paths
* @param defaultTs - Default TypeScript runtime to use ("bun" or "deno")
* @returns Array of inline scripts with their paths and content
*/
export function extractInlineScripts(
modules: FlowModule[],
mapping: Record<string, string> = {},
defaultTs?: "bun" | "deno"
): InlineScript[] {
return modules.flatMap((m) => {
if (m.value.type == "rawscript") {
const [basePath, ext] = assignPath(
m.id,
m.value.language,
defaultTs
);
const path = mapping[m.id] ?? basePath + ext;
const content = m.value.content;
const r = [{ path: path, content: content }];
m.value.content = "!inline " + path.replace(SEP, "/");
const lock = m.value.lock;
if (lock && lock != "") {
const lockPath = basePath + "lock";
m.value.lock = "!inline " + lockPath.replace(SEP, "/");
r.push({ path: lockPath, content: lock });
}
return r;
} else if (m.value.type == "forloopflow") {
return extractInlineScripts(m.value.modules, mapping, defaultTs);
} else if (m.value.type == "branchall") {
return m.value.branches.flatMap((b) =>
extractInlineScripts(b.modules, mapping, defaultTs)
);
} else if (m.value.type == "whileloopflow") {
return extractInlineScripts(m.value.modules, mapping, defaultTs);
} else if (m.value.type == "branchone") {
return [
...m.value.branches.flatMap((b) =>
extractInlineScripts(b.modules, mapping, defaultTs)
),
...extractInlineScripts(m.value.default, mapping, defaultTs),
];
} else {
return [];
}
});
}
/**
* Extracts the current mapping of module IDs to file paths from flow modules
* by analyzing existing inline script references.
*
* @param modules - Array of flow modules to analyze (can be undefined)
* @param mapping - Existing mapping to extend (defaults to empty object)
* @returns Record mapping module IDs to their corresponding file paths
*/
export function extractCurrentMapping(
modules: FlowModule[] | undefined,
mapping: Record<string, string> = {}
): Record<string, string> {
if (!modules || !Array.isArray(modules)) {
return mapping;
}
modules.forEach((m) => {
if (!m?.value?.type) {
return;
}
if (m.value.type === "rawscript") {
if (m.value.content && m.value.content.startsWith("!inline ")) {
mapping[m.id] = m.value.content.trim().split(" ")[1];
}
} else if (
m.value.type === "forloopflow" ||
m.value.type === "whileloopflow"
) {
extractCurrentMapping(m.value.modules, mapping);
} else if (m.value.type === "branchall") {
m.value.branches.forEach((b) => extractCurrentMapping(b.modules, mapping));
} else if (m.value.type === "branchone") {
m.value.branches.forEach((b) => extractCurrentMapping(b.modules, mapping));
extractCurrentMapping(m.value.default, mapping);
}
});
return mapping;
}
@@ -0,0 +1,2 @@
export * from "./replacer";
export * from "./extractor";
@@ -0,0 +1,69 @@
import { FlowModule } from "../gen/types.gen.ts";
/**
* Replaces inline script references with actual file content from the filesystem.
* This function recursively processes all flow modules and their nested structures.
*
* @param modules - Array of flow modules to process
* @param fileReader - Function to read file content (typically fs.readFile or similar)
* @param logger - Optional logger object with info and error methods
* @param localPath - Base path for resolving relative file paths
* @param removeLocks - Optional array of paths for which to remove lock files
* @returns Promise that resolves when all inline scripts have been replaced
*/
export async function replaceInlineScripts(
modules: FlowModule[],
fileReader: (...args: any[]) => any,
logger: {
info: (...args: any[]) => void,
error: (...args: any[]) => void,
} = {
info: () => {},
error: () => {},
},
localPath: string,
removeLocks?: string[]
): Promise<void> {
await Promise.all(modules.map(async (module) => {
if (!module.value) {
throw new Error(`Module value is undefined for module ${module.id}`);
}
if (module.value.type === "rawscript") {
if (module.value.content.startsWith("!inline")) {
const path = module.value.content.split(" ")[1];
try {
module.value.content = await fileReader(path);
} catch {
logger.error(`Script file ${path} not found`);
}
const lock = module.value.lock;
if (removeLocks && removeLocks.includes(path)) {
module.value.lock = undefined;
} else if (
lock &&
typeof lock == "string" &&
lock.trimStart().startsWith("!inline ")
) {
const path = lock.split(" ")[1];
try {
module.value.lock = await fileReader(path);
} catch {
logger.error(`Lock file ${path} not found`);
}
}
}
} else if (module.value.type === "forloopflow" || module.value.type === "whileloopflow") {
await replaceInlineScripts(module.value.modules, fileReader, logger, localPath, removeLocks);
} else if (module.value.type === "branchall") {
await Promise.all(module.value.branches.map(async (branch) => {
await replaceInlineScripts(branch.modules, fileReader, logger, localPath, removeLocks);
}));
} else if (module.value.type === "branchone") {
await Promise.all(module.value.branches.map(async (branch) => {
await replaceInlineScripts(branch.modules, fileReader, logger, localPath, removeLocks);
}));
await replaceInlineScripts(module.value.default, fileReader, logger, localPath, removeLocks);
}
}));
}
@@ -0,0 +1 @@
export * from "./parse-schema";
@@ -0,0 +1,243 @@
/**
* Type alias for enum values - can be an array of strings or undefined
*/
export type EnumType = string[] | undefined
/**
* Represents a property in a JSON schema with various validation and display options
*/
export interface SchemaProperty {
type: string | undefined
description?: string
pattern?: string
default?: any
enum?: EnumType
contentEncoding?: 'base64' | 'binary'
format?: string
items?: {
type?: 'string' | 'number' | 'bytes' | 'object' | 'resource'
contentEncoding?: 'base64'
enum?: string[]
resourceType?: string
properties?: { [name: string]: SchemaProperty }
}
min?: number
max?: number
currency?: string
currencyLocale?: string
multiselect?: boolean
customErrorMessage?: string
properties?: { [name: string]: SchemaProperty }
required?: string[]
showExpr?: string
password?: boolean
order?: string[]
nullable?: boolean
dateFormat?: string
title?: string
placeholder?: string
oneOf?: SchemaProperty[]
originalType?: string
}
/**
* Converts argument signature types to JSON schema properties.
* This function handles various Windmill-specific types and converts them
* to standard JSON schema format while preserving existing property metadata.
*
* @param t - The argument signature type definition (can be string or complex object types)
* @param oldS - Existing schema property to update with new type information
*/
export function argSigToJsonSchemaType(
t:
| string
| { resource: string | null }
| {
list:
| (string | { name?: string; props?: { key: string; typ: any }[] })
| { str: any }
| { object: { name?: string; props?: { key: string; typ: any }[] } }
| null
}
| { dynselect: string }
| { str: string[] | null }
| { object: { name?: string; props?: { key: string; typ: any }[] } }
| {
oneof: {
label: string
properties: { key: string; typ: any }[]
}[]
},
oldS: SchemaProperty
): void {
const newS: SchemaProperty = { type: '' }
if (t === 'int') {
newS.type = 'integer'
} else if (t === 'float') {
newS.type = 'number'
} else if (t === 'bool') {
newS.type = 'boolean'
} else if (t === 'email') {
newS.type = 'string'
newS.format = 'email'
} else if (t === 'sql') {
newS.type = 'string'
newS.format = 'sql'
} else if (t === 'yaml') {
newS.type = 'string'
newS.format = 'yaml'
} else if (t === 'bytes') {
newS.type = 'string'
newS.contentEncoding = 'base64'
newS.originalType = 'bytes'
} else if (t === 'datetime') {
newS.type = 'string'
newS.format = 'date-time'
} else if (typeof t !== 'string' && 'oneof' in t) {
newS.type = 'object'
if (t.oneof) {
newS.oneOf = t.oneof.map((obj) => {
const oldObjS = oldS.oneOf?.find((o) => o?.title === obj.label) ?? undefined
const properties: Record<string, any> = {}
for (const prop of obj.properties) {
if (oldObjS?.properties && prop.key in oldObjS?.properties) {
properties[prop.key] = oldObjS?.properties[prop.key]
} else {
properties[prop.key] = { description: '', type: '' }
}
argSigToJsonSchemaType(prop.typ, properties[prop.key])
}
return {
type: 'object',
title: obj.label,
properties,
order: oldObjS?.order ?? undefined
}
})
}
} else if (typeof t !== 'string' && `object` in t) {
newS.type = 'object'
if (t.object.name) {
newS.format = `resource-${t.object.name}`
}
if (t.object.props) {
const properties: Record<string, any> = {}
for (const prop of t.object.props) {
if (oldS.properties && prop.key in oldS.properties) {
properties[prop.key] = oldS.properties[prop.key]
} else {
properties[prop.key] = { description: '', type: '' }
}
argSigToJsonSchemaType(prop.typ, properties[prop.key])
}
newS.properties = properties
}
} else if (typeof t !== 'string' && `str` in t) {
newS.type = 'string'
if (t.str) {
newS.originalType = 'enum'
newS.enum = t.str
} else if (oldS.originalType == 'string' && oldS.enum) {
newS.originalType = 'string'
newS.enum = oldS.enum
} else {
newS.originalType = 'string'
newS.enum = undefined
}
} else if (typeof t !== 'string' && `resource` in t) {
newS.type = 'object'
newS.format = `resource-${t.resource}`
} else if (typeof t !== 'string' && `dynselect` in t) {
newS.type = 'object'
newS.format = `dynselect-${t.dynselect}`
} else if (typeof t !== 'string' && `list` in t) {
newS.type = 'array'
if (t.list === 'int' || t.list === 'float') {
newS.items = { type: 'number' }
newS.originalType = 'number[]'
} else if (t.list === 'bytes') {
newS.items = { type: 'string', contentEncoding: 'base64' }
newS.originalType = 'bytes[]'
} else if (t.list && typeof t.list == 'object' && 'str' in t.list && t.list.str) {
newS.items = { type: 'string', enum: t.list.str }
newS.originalType = 'enum[]'
} else if (t.list == 'string' || (t.list && typeof t.list == 'object' && 'str' in t.list)) {
newS.items = { type: 'string', enum: oldS.items?.enum }
newS.originalType = 'string[]'
} else if (t.list && typeof t.list == 'object' && 'resource' in t.list && t.list.resource) {
newS.items = {
type: 'resource',
resourceType: t.list.resource as string
}
newS.originalType = 'resource[]'
} else if (t.list && typeof t.list == 'object' && 'object' in t.list && t.list.object) {
if (t.list.object.name) {
newS.format = `resource-${t.list.object.name}`
}
if (t.list.object.props && t.list.object.props.length > 0) {
const properties: Record<string, any> = {}
for (const prop of t.list.object.props) {
properties[prop.key] = { description: '', type: '' }
argSigToJsonSchemaType(prop.typ, properties[prop.key])
}
newS.items = { type: 'object', properties: properties }
} else {
newS.items = { type: 'object' }
}
newS.originalType = 'record[]'
} else {
newS.items = { type: 'object' }
newS.originalType = 'object[]'
}
} else {
newS.type = 'object'
}
const preservedFields = [
'description',
'pattern',
'min',
'max',
'currency',
'currencyLocale',
'multiselect',
'customErrorMessage',
'required',
'showExpr',
'password',
'order',
'dateFormat',
'title',
'placeholder'
]
preservedFields.forEach((field) => {
// @ts-ignore
if (oldS[field] !== undefined) {
// @ts-ignore
newS[field] = oldS[field]
}
})
if (oldS.type != newS.type) {
for (const prop of Object.getOwnPropertyNames(newS)) {
if (prop != 'description') {
// @ts-ignore
delete oldS[prop]
}
}
} else if ((oldS.format == 'date' || oldS.format === 'date-time') && newS.format == 'string') {
newS.format = oldS.format
} else if (newS.format == 'date-time' && oldS.format == 'date') {
newS.format = 'date'
} else if (oldS.items?.type != newS.items?.type) {
delete oldS.items
}
if (oldS.format && !newS.format) {
oldS.format = undefined
}
Object.assign(oldS, newS)
}
@@ -0,0 +1 @@
export * from "./path-assigner";
@@ -0,0 +1,71 @@
import { RawScript } from "../gen/types.gen";
/**
* Union type of all supported programming languages in Windmill
*/
export type SupportedLanguage = RawScript["language"] | "frontend" | "bunnative" | "oracledb" | "rust" | "csharp" | "nu" | "ansible" | "java" | "duckdb";
/**
* Mapping of supported languages to their file extensions
*/
export const LANGUAGE_EXTENSIONS: Record<SupportedLanguage, string> = {
python3: "py",
bun: "bun.ts",
deno: "deno.ts",
go: "go",
bash: "sh",
powershell: "ps1",
postgresql: "pg.sql",
mysql: "my.sql",
bigquery: "bq.sql",
oracledb: "odb.sql",
snowflake: "sf.sql",
mssql: "ms.sql",
graphql: "gql",
nativets: "native.ts",
frontend: "frontend.js",
php: "php",
rust: "rs",
csharp: "cs",
nu: "nu",
ansible: "playbook.yml",
java: "java",
duckdb: "duckdb.sql",
bunnative: "ts"
};
/**
* Gets the appropriate file extension for a given programming language.
* Handles special cases for TypeScript variants based on the default runtime.
*
* @param language - The programming language to get extension for
* @param defaultTs - Default TypeScript runtime ("bun" or "deno")
* @returns File extension string (without the dot)
*/
export function getLanguageExtension(
language: SupportedLanguage,
defaultTs: "bun" | "deno" = "bun"
): string {
if (language === defaultTs || language === "bunnative") {
return "ts";
}
return LANGUAGE_EXTENSIONS[language] || "no_ext";
}
/**
* Assigns a file path for an inline script based on its ID and language.
* Returns both the base path and extension as separate components.
*
* @param id - Unique identifier for the script
* @param language - Programming language of the script
* @param defaultTs - Default TypeScript runtime ("bun" or "deno")
* @returns Tuple containing [basePath, extension]
*/
export function assignPath(
id: string,
language: SupportedLanguage,
defaultTs: "bun" | "deno" = "bun"
): [string, string] {
const ext = getLanguageExtension(language, defaultTs);
return [`${id}.inline_script.`, ext];
}
+31
View File
@@ -0,0 +1,31 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"lib": ["ES2020"],
"declaration": true,
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"noUnusedLocals": false,
"noUnusedParameters": false,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"moduleResolution": "node",
"baseUrl": "./",
"esModuleInterop": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"allowImportingTsExtensions": true
},
"include": [
"src/**/*"
],
"exclude": [
"node_modules",
"dist"
]
}