-
Notifications
You must be signed in to change notification settings - Fork 615
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
fix: direct download of extensions (#4206)
- Loading branch information
Showing
35 changed files
with
783 additions
and
348 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
155 changes: 155 additions & 0 deletions
155
packages/cli-plugin-scaffold-extensions/src/downloadAndLinkExtension.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,155 @@ | ||
import os from "os"; | ||
import path from "path"; | ||
import fs from "node:fs"; | ||
import fsAsync from "node:fs/promises"; | ||
import { CliCommandScaffoldCallableArgs } from "@webiny/cli-plugin-scaffold/types"; | ||
import { setTimeout } from "node:timers/promises"; | ||
import { WEBINY_DEV_VERSION } from "~/utils/constants"; | ||
import { linkAllExtensions } from "./utils/linkAllExtensions"; | ||
import { Input } from "./types"; | ||
import { downloadFolderFromS3 } from "./downloadAndLinkExtension/downloadFolderFromS3"; | ||
import { setWebinyPackageVersions } from "~/utils/setWebinyPackageVersions"; | ||
import { runYarnInstall } from "@webiny/cli-plugin-scaffold/utils"; | ||
import { getDownloadedExtensionType } from "~/downloadAndLinkExtension/getDownloadedExtensionType"; | ||
import chalk from "chalk"; | ||
import { Extension } from "./extensions/Extension"; | ||
|
||
const EXTENSIONS_ROOT_FOLDER = "extensions"; | ||
|
||
const S3_BUCKET_NAME = "webiny-examples"; | ||
const S3_BUCKET_REGION = "us-east-1"; | ||
|
||
const getVersionFromVersionFolders = async ( | ||
versionFoldersList: string[], | ||
currentWebinyVersion: string | ||
) => { | ||
const availableVersions = versionFoldersList.map(v => v.replace(".x", ".0")).sort(); | ||
|
||
let versionToUse = ""; | ||
|
||
// When developing Webiny, we want to use the latest version. | ||
if (currentWebinyVersion === WEBINY_DEV_VERSION) { | ||
versionToUse = availableVersions[availableVersions.length - 1]; | ||
} else { | ||
for (const availableVersion of availableVersions) { | ||
if (currentWebinyVersion >= availableVersion) { | ||
versionToUse = availableVersion; | ||
} else { | ||
break; | ||
} | ||
} | ||
} | ||
|
||
return versionToUse.replace(".0", ".x"); | ||
}; | ||
|
||
export const downloadAndLinkExtension = async ({ | ||
input, | ||
ora, | ||
context | ||
}: CliCommandScaffoldCallableArgs<Input>) => { | ||
const currentWebinyVersion = context.version; | ||
|
||
const downloadExtensionSource = input.templateArgs!; | ||
|
||
try { | ||
ora.start(`Downloading extension...`); | ||
|
||
const randomId = String(Date.now()); | ||
const downloadFolderPath = path.join(os.tmpdir(), `wby-ext-${randomId}`); | ||
|
||
await downloadFolderFromS3({ | ||
bucketName: S3_BUCKET_NAME, | ||
bucketRegion: S3_BUCKET_REGION, | ||
bucketFolderKey: downloadExtensionSource, | ||
downloadFolderPath | ||
}); | ||
|
||
ora.text = `Copying extension...`; | ||
await setTimeout(1000); | ||
|
||
let extensionsFolderToCopyPath = path.join(downloadFolderPath, "extensions"); | ||
|
||
// If we have `extensions` folder in the root of the downloaded extension. | ||
// it means the example extension is not versioned, and we can just copy it. | ||
const extensionsFolderExistsInRoot = fs.existsSync(extensionsFolderToCopyPath); | ||
const versionedExtension = !extensionsFolderExistsInRoot; | ||
|
||
if (versionedExtension) { | ||
// If we have `x.x.x` folders in the root of the downloaded | ||
// extension, we need to find the right version to use. | ||
|
||
// This can be `5.40.x`, `5.41.x`, etc. | ||
const versionFolders = await fsAsync.readdir(downloadFolderPath); | ||
|
||
const versionToUse = await getVersionFromVersionFolders( | ||
versionFolders, | ||
currentWebinyVersion | ||
); | ||
|
||
extensionsFolderToCopyPath = path.join(downloadFolderPath, versionToUse, "extensions"); | ||
} | ||
|
||
await fsAsync.cp(extensionsFolderToCopyPath, EXTENSIONS_ROOT_FOLDER, { | ||
recursive: true | ||
}); | ||
|
||
ora.text = `Linking extension...`; | ||
|
||
// Retrieve extensions folders in the root of the downloaded extension. We use this | ||
// later to run additional setup tasks on each extension. | ||
const extensionsFolderNames = await fsAsync.readdir(extensionsFolderToCopyPath); | ||
const downloadedExtensions: Extension[] = []; | ||
|
||
for (const extensionsFolderName of extensionsFolderNames) { | ||
const folderPath = path.join(EXTENSIONS_ROOT_FOLDER, extensionsFolderName); | ||
const extensionType = await getDownloadedExtensionType(folderPath); | ||
|
||
downloadedExtensions.push( | ||
new Extension({ | ||
name: extensionsFolderName, | ||
type: extensionType, | ||
location: folderPath, | ||
|
||
// We don't care about the package name here. | ||
packageName: extensionsFolderName | ||
}) | ||
); | ||
} | ||
|
||
for (const downloadedExtension of downloadedExtensions) { | ||
await setWebinyPackageVersions(downloadedExtension, currentWebinyVersion); | ||
} | ||
|
||
await linkAllExtensions(); | ||
await runYarnInstall(); | ||
|
||
if (downloadedExtensions.length === 1) { | ||
const [downloadedExtension] = downloadedExtensions; | ||
ora.succeed( | ||
`Extension downloaded in ${context.success.hl(downloadedExtension.getLocation())}.` | ||
); | ||
|
||
const nextSteps = downloadedExtension.getNextSteps(); | ||
|
||
console.log(); | ||
console.log(chalk.bold("Next Steps")); | ||
nextSteps.forEach(message => { | ||
console.log(`‣ ${message}`); | ||
}); | ||
} else { | ||
const paths = downloadedExtensions.map(ext => ext.getLocation()); | ||
ora.succeed(`Extensions downloaded in ${context.success.hl(paths.join(", "))}.`); | ||
} | ||
} catch (e) { | ||
switch (e.code) { | ||
case "NO_OBJECTS_FOUND": | ||
ora.fail("Could not download extension. Looks like the extension does not exist."); | ||
break; | ||
default: | ||
ora.fail("Could not create extension. Please check the logs below."); | ||
console.log(); | ||
console.log(e); | ||
} | ||
} | ||
}; |
73 changes: 73 additions & 0 deletions
73
packages/cli-plugin-scaffold-extensions/src/downloadAndLinkExtension/downloadFolderFromS3.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
import { S3Client, ListObjectsV2Command, GetObjectCommand } from "@webiny/aws-sdk/client-s3"; | ||
import fs from "fs"; | ||
import path from "path"; | ||
import { WebinyError } from "@webiny/error"; | ||
|
||
interface DownloadFolderFromS3Params { | ||
bucketName: string; | ||
bucketRegion: string; | ||
bucketFolderKey: string; | ||
downloadFolderPath: string; | ||
} | ||
|
||
export const downloadFolderFromS3 = async (params: DownloadFolderFromS3Params) => { | ||
const { bucketName, bucketRegion, bucketFolderKey, downloadFolderPath } = params; | ||
|
||
// Configure the S3 client | ||
const s3Client = new S3Client({ region: bucketRegion }); | ||
|
||
// List all objects in the specified S3 folder | ||
const listObjects = async (bucket: string, folderKey: string) => { | ||
const command = new ListObjectsV2Command({ | ||
Bucket: bucket, | ||
Prefix: folderKey | ||
}); | ||
const response = await s3Client.send(command); | ||
return response.Contents; | ||
}; | ||
|
||
// Download an individual file from S3 | ||
const downloadFile = async (bucket: string, key: string, localPath: string) => { | ||
const command = new GetObjectCommand({ | ||
Bucket: bucket, | ||
Key: key | ||
}); | ||
|
||
const response = await s3Client.send(command); | ||
|
||
return new Promise((resolve, reject) => { | ||
const fileStream = fs.createWriteStream(localPath); | ||
// @ts-expect-error | ||
response.Body.pipe(fileStream); | ||
// @ts-expect-error | ||
response.Body.on("error", reject); | ||
fileStream.on("finish", resolve); | ||
}); | ||
}; | ||
|
||
const objects = (await listObjects(bucketName, bucketFolderKey)) || []; | ||
if (!objects.length) { | ||
throw new WebinyError(`No objects found in the specified S3 folder.`, "NO_OBJECTS_FOUND"); | ||
} | ||
|
||
for (const object of objects) { | ||
const s3Key = object.Key!; | ||
const relativePath = path.relative(bucketFolderKey, s3Key); | ||
const localFilePath = path.join(downloadFolderPath, relativePath); | ||
|
||
if (s3Key.endsWith("/")) { | ||
// It's a directory, create it if it doesn't exist. | ||
if (!fs.existsSync(localFilePath)) { | ||
fs.mkdirSync(localFilePath, { recursive: true }); | ||
} | ||
} else { | ||
// It's a file, download it. | ||
const localDirPath = path.dirname(localFilePath); | ||
if (!fs.existsSync(localDirPath)) { | ||
fs.mkdirSync(localDirPath, { recursive: true }); | ||
} | ||
|
||
await downloadFile(bucketName, s3Key, localFilePath); | ||
} | ||
} | ||
}; |
19 changes: 19 additions & 0 deletions
19
...cli-plugin-scaffold-extensions/src/downloadAndLinkExtension/getDownloadedExtensionType.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
import loadJson from "load-json-file"; | ||
import { PackageJson } from "@webiny/cli-plugin-scaffold/types"; | ||
import path from "node:path"; | ||
|
||
export const getDownloadedExtensionType = async (downloadedExtensionRootPath: string) => { | ||
const pkgJsonPath = path.join(downloadedExtensionRootPath, "package.json"); | ||
const pkgJson = await loadJson<PackageJson>(pkgJsonPath); | ||
|
||
const keywords = pkgJson.keywords; | ||
if (Array.isArray(keywords)) { | ||
for (const keyword of keywords) { | ||
if (keyword.startsWith("webiny-extension-type:")) { | ||
return keyword.replace("webiny-extension-type:", ""); | ||
} | ||
} | ||
} | ||
|
||
throw new Error(`Could not determine the extension type from the downloaded extension.`); | ||
}; |
34 changes: 34 additions & 0 deletions
34
packages/cli-plugin-scaffold-extensions/src/extensions/AbstractExtension.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
export interface ExtensionTypeConstructorParams { | ||
name: string; | ||
type: string; | ||
location: string; | ||
packageName: string; | ||
} | ||
|
||
export abstract class AbstractExtension { | ||
protected params: ExtensionTypeConstructorParams; | ||
|
||
constructor(params: ExtensionTypeConstructorParams) { | ||
this.params = params; | ||
} | ||
|
||
abstract generate(): Promise<void>; | ||
|
||
abstract getNextSteps(): string[]; | ||
|
||
getPackageJsonPath(): string { | ||
return `${this.params.location}/package.json`; | ||
} | ||
|
||
getLocation(): string { | ||
return this.params.location; | ||
} | ||
|
||
getPackageName(): string { | ||
return this.params.packageName; | ||
} | ||
|
||
getName(): string { | ||
return this.params.name; | ||
} | ||
} |
Oops, something went wrong.