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 */
};