export async function generate()

in packages/aws-arch/scripts/aws-arch/generate-assets.ts [85:392]


export async function generate () {
  await fs.ensureDir(ASSET_PACKAGE_DIR);
  await fs.emptyDir(ASSET_PACKAGE_DIR);
  await fs.ensureDir(path.dirname(AWS_ASSETS_TS));

  const assetFiles: string[] = [];

  // copy static assets
  const groups = new Set<[string, string]>();
  for(const staticFile of await listDirFiles(path.join(STATIC_ASSETS))) {
    if (staticFile.match(/\/groups\/\w+\.(png|svg)/)) {
      // TODO: support svg groups - need to find the assets
      const group = path.basename(staticFile).split('.')[0];
      group !== "" && groups.add([group, `groups/${group}`]);
    }
    assetFiles.push(staticFile.replace(STATIC_ASSETS + '/', ''));
  }
  await fs.copy(STATIC_ASSETS, ASSET_PACKAGE_DIR, { overwrite: true });

  const downloadPath = path.join(TMP_DIR, ASSET_PACKAGE_ZIP_FILENAME);

  if (!await fs.pathExists(downloadPath)) {
    const response = await fetch(ASSET_PACKAGE_ZIP_URL);
    if (!response.ok) throw new Error(`unexpected response ${response.statusText}`)
    await util.promisify(stream.pipeline)(response.body, fs.createWriteStream(downloadPath))
  }

  const fullNameLookup: {[key: string]: string} = {};

  const categories = new Set<[string, string]>();
  const services = new Set<[string, string]>();
  const resources = new Set<[string, string]>();
  const generalIcons = new Set<[string, string]>();
  const instanceTypes = new Set<[string, string]>();
  const rdsInstanceTypes = new Set<[string, string]>();
  const iotThings = new Set<[string, string]>();

  // Unzip all .svg files (png resources are low-res of 48px only, so we generate them from svg)
  const zip = fs.createReadStream(downloadPath).pipe(ZipParse());
  // async for...of causes premature close in node 18+
  // https://github.com/ZJONSSON/node-unzipper/issues/228#issuecomment-1294451911
  await zip.on("entry", (entry) => {
    const entryPath: string = patchZipEntryPath(entry.path);
    let fileName = path.basename(entryPath);
    const ext = path.extname(fileName);
    const type = entry.type; // 'Directory' or 'File'

    let extract = false;

    if (type === 'File' && !fileName.startsWith('.')) {
      if (entryPath.match(ASSET_PATTERNS.RDS_INSTANCE)) {
        let { category, instanceType, alt, theme } = entryPath.match(ASSET_PATTERNS.RDS_INSTANCE)!.groups! as { category: string, theme: string, instanceType: string, alt?: string };
        category = findAwsCategoryDefinition(normalizeIdentifier(category)).id;
        instanceType = normalizeIdentifier(instanceType);
        theme = normalizeIdentifier(theme);
        const serviceName = resolveServiceName("rds");

        // All db asset icons start with some variation of "Amazon-Aurora" prefix, even though they do not belong under Aurora
        // Example: Res_Amazon-Aurora-Oracle-Instance_48_Light, Res_Amazon-Aurora-Oracle-Instance_48_Light
        if (instanceType === "rds") {
          instanceType = "instance";
        } else if (instanceType !== "aurora") {
          // only treat the actual "aurora" type as aurora and all others as "rds" types
          instanceType = instanceType.replace("aurora_", "");
          instanceType = instanceType.replace("rds_", "");
          // squash value to align with resource engine values (eg: sql_server => sqlserver)
          // https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-rds-dbinstance.html#cfn-rds-dbinstance-engine
          instanceType = instanceType.replace("_", "");
        }

        if (alt) {
          instanceType += "_alt"
        }

        let key: string;

        // Treat default RDS instance as resource of rds, rather than an instance type
        if (instanceType === "instance" || instanceType === "instance_alt") {
          key = path.join(category, serviceName, instanceType);

          if (!alt && theme === DEFAULT_THEME) {
            resources.add(["rds_instance", key]);
            fullNameLookup["rds_instance"] = "RDS Instance";
          }
        } else {
          key = path.join(category, serviceName, "instance", instanceType);

          if (theme === DEFAULT_THEME) {
            rdsInstanceTypes.add([instanceType, key]);
          }
        }
        if (theme !== DEFAULT_THEME) {
          key += `.${theme}`;
        }

        fileName = key + ext;
        extract = true;
      } else if (entryPath.match(ASSET_PATTERNS.RESOURCE)) {
        let { category, serviceName, resourceName, theme } = entryPath.match(ASSET_PATTERNS.RESOURCE)!.groups! as { category: string, serviceName: string, theme: string, resourceName: string };
        const serviceFullName = interpolateFullName(serviceName);
        const resourceFullName = interpolateFullName(resourceName);
        category = findAwsCategoryDefinition(normalizeIdentifier(category)).id;
        serviceName = resolveServiceName(serviceName);
        resourceName = normalizeIdentifier(resourceName);
        theme = normalizeIdentifier(theme);

        let key = path.join(category, serviceName, resourceName);
        let nestedResource = false;
        if (key.match(EC2_INSTANCE_PATTERN)) {
          const { instanceType } = key.match(EC2_INSTANCE_PATTERN)!.groups!;
          key = path.join(category, serviceName, 'instance', instanceType);
          instanceTypes.add([instanceType, key]);
          nestedResource = true;
        }
        if (key.match(IOT_THING_PATTERN)) {
          const { thingType } = key.match(IOT_THING_PATTERN)!.groups!;
          key = path.join(category, 'thing', thingType);
          iotThings.add([thingType, key]);
          nestedResource = true;
        }

        if (theme === DEFAULT_THEME) {
          if (!nestedResource) {
            if (resourceName === 'service') {
              services.add([serviceName, key]);
              fullNameLookup[serviceName] = serviceFullName;
            }
            resourceName = `${serviceName}_${resourceName}`;
            resources.add([resourceName, key]);
            fullNameLookup[resourceName] = resourceFullName;
          }
        } else {
          key += `.${theme}`;
        }
        fileName = key + ext;
        extract = true;
      } else if (entryPath.match(ASSET_PATTERNS.SERVICE)) {
        let { category, serviceName } = entryPath.match(ASSET_PATTERNS.SERVICE)!.groups! as { category: string, serviceName: string };
        const serviceFullName = interpolateFullName(serviceName);
        serviceName = resolveServiceName(serviceName);
        category = findAwsCategoryDefinition(normalizeIdentifier(category)).id;

        const key = path.join(category, serviceName, SERVICE_ICON);
        fileName = key + ext;
        extract = true;
        services.add([serviceName, key]);
        fullNameLookup[serviceName] = serviceFullName;
      } else if (entryPath.match(ASSET_PATTERNS.CATEGORY)) {
        let { category } = entryPath.match(ASSET_PATTERNS.CATEGORY)!.groups! as { category: string };
        const categoryFullName = interpolateFullName(category);
        category = normalizeIdentifier(category);

        const key = path.join(category, CATEGORY_ICON);
        fileName = key + ext;
        extract = true;
        categories.add([category, key]);
        fullNameLookup[category] = categoryFullName;
      } else if (entryPath.match(ASSET_PATTERNS.GENERAL)) {
        let { theme, generalIcon } = entryPath.match(ASSET_PATTERNS.GENERAL)!.groups! as { theme: string, generalIcon: string };
        theme = normalizeIdentifier(theme);
        generalIcon = normalizeIdentifier(generalIcon);

        let key = path.join(GENERAL_CATEGORY_ID, generalIcon);
        if (theme === DEFAULT_THEME) {
          generalIcons.add([generalIcon, key]);
        } else {
          key += `.${theme}`;
        }
        fileName = key + ext;
        extract = true;
      } else if (entryPath.includes("_64") && entryPath.endsWith(".svg")) {
        throw new Error(`Failed to match expected path: ${entryPath}`);
      }
    }

    if (extract) {
      const filePath = path.join(ASSET_PACKAGE_DIR, fileName);
      assetFiles.push(fileName);
      fs.ensureDirSync(path.dirname(filePath));
      if (fs.pathExistsSync(filePath)) {
        console.debug(entryPath)
        throw new Error(`Asset path arealdy exists: ${filePath}`)
      }
      entry.pipe(fs.createWriteStream(filePath));
    } else {
      entry.autodrain();
    }
  }).promise();

  // some dark instance types do not have "-Instance" prefix, so need to move them
  const computeEC2Dir = path.join(ASSET_PACKAGE_DIR, 'compute', 'ec2');
  const instanceDir = path.join(computeEC2Dir, 'instance');
  const instanceTypeNames = new Set<string>(Array.from(instanceTypes).map(([key]) => key));
  const resolvedInstanceTypes: string[] = [];
  for(const _ec2File of (await listDirFiles(computeEC2Dir, false))) {
    const _basename = path.basename(_ec2File);
    if (instanceTypeNames.has(_basename.split(".")[0])) {
      resolvedInstanceTypes.push(_basename)
      await fs.move(_ec2File, path.join(instanceDir, _basename))
    }
  }
  console.debug("Resolved instance icons:", resolvedInstanceTypes.join(', '));

  // create png for all svg assets (that don't already have png)
  for(const svgFile of (await listDirFiles(ASSET_PACKAGE_DIR)).filter(filePath => path.extname(filePath) === EXT_SVG)) {
    const pngFile = svgFile.replace(EXT_SVG, EXT_PNG);
    if (!(await fs.pathExists(pngFile))) {
      await sharp(svgFile).resize({
        width: PNG_ASSET_SIZE,
        height: PNG_ASSET_SIZE,
        background: {r:1,g:1,b:1,alpha:0}, // transparent
        fit: 'contain',
      }).toFormat('png').toFile(pngFile);
      assetFiles.push(path.relative(ASSET_PACKAGE_DIR, pngFile));
    }
  }

  const aliasable: Set<string> = new Set<string>([
    ...Array.from(resources.values()).map(([_k,_v]) => _v),
    ...Array.from(generalIcons.values()).map(([_k,_v]) => _v),
  ])
  for (const [alias, assetKey] of Object.entries(GENERAL_ALIASES)) {
    if (!aliasable.has(assetKey)) {
      throw new Error(`Invalid general alias: ${alias} => ${assetKey}`)
    }
    generalIcons.add([alias, assetKey]);
  }

  assetFiles.sort();

  /** eslint-disable */
  await fs.writeFile(AWS_ASSETS_TS, `// AUTO-GENERATED - DO NOT EDIT
/* eslint-disable */
export namespace AwsAsset {
  export const Categories = ${JSON.stringify(sortedObjectFromEntries(categories), null, 4)} as const;

  export type Category = keyof typeof Categories;

  export const Services = ${JSON.stringify(sortedObjectFromEntries(services), null, 4)} as const;

  export type Service = keyof typeof Services;

  export const Resources = ${JSON.stringify(sortedObjectFromEntries(resources), null, 4)} as const;

  export type Resource = keyof typeof Resources;

  export const InstanceTypes = ${JSON.stringify(sortedObjectFromEntries(instanceTypes), null, 4)} as const;

  export type InstanceType = keyof typeof InstanceTypes;

  export const RdsInstanceTypes = ${JSON.stringify(sortedObjectFromEntries(rdsInstanceTypes), null, 4)} as const;

  export type RdsInstanceType = keyof typeof RdsInstanceTypes;

  export const IotThings = ${JSON.stringify(sortedObjectFromEntries(iotThings), null, 4)} as const;

  export type IotThing  = keyof typeof IotThings;

  export const GeneralIcons = ${JSON.stringify(sortedObjectFromEntries(generalIcons), null, 4)} as const;

  export type GeneralIcon = keyof typeof GeneralIcons;

  export const Groups = ${JSON.stringify(sortedObjectFromEntries(groups), null, 4)} as const;

  export type Group = keyof typeof Groups;

  export const AssetFullNameLookup = ${JSON.stringify(sortedObjectFromEntries(fullNameLookup), null, 4)} as const;

  /**
   * Static list of all avaliable asset files - stored as const to prevent async io at runtime
   * @internal
   **/
  export const AssetFiles = new Set<string>(${JSON.stringify(assetFiles, null, 4)});
}
`)

  const assetTree = (await tree({ base: path.relative(process.cwd(), ASSET_PACKAGE_DIR), l: 10, f: true })).report.replace(/\S+/, '/');

  await fs.writeFile(ASSETS_MARKDOWN, `<!-- AUTO-GENERATED - DO NOT EDIT -->
## AWS Architecture Icons

This package generates a normalized asset dictionary from [AWS Architecture Icons](https://aws.amazon.com/architecture/icons/).

Asset paths follow \`<category>/<service>/<resource>[.<theme>].<ext>\` structure.

> eg: storage/s3/bucket.png, storage/s3/bucket.dark.png

**Category** icons follow \`<category>/category_icon.<ext>\` structure.

> eg: storage/category_icon.png

**Service** icons follow \`<category>/<service>/service_icon.<ext>\` structure.

> eg: storage/s3/service_icon.png

The _default theme_ is **light**.

---

### Available assets

\`\`\`
${assetTree}
\`\`\`

`)
  /** eslint-enable */
};