Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft: Generate s3 file listing after each upload #1980

Open
wants to merge 8 commits into
base: 4.x
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24,548 changes: 14,009 additions & 10,539 deletions package-lock.json
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you update all deps? Seems to have lots of changes...

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@
"uglify-js": "^3.1.4"
},
"devDependencies": {
"@aws-sdk/client-s3": "^3.385.0",
"@playwright/test": "^1.17.1",
"aws-sdk": "^2.1.49",
"babel-loader": "^5.0.0",
"babel-runtime": "^5.1.10",
"benchmark": "~1.0",
Expand Down
127 changes: 127 additions & 0 deletions tasks/aws-s3-builds-page/fileList.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
<!doctype html>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe activate prettier formatting for this file?

<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<style>
table {
column-gap: 1rem;
}
thead {
position: sticky;
top: 0;
background: #efefef;
}

th:not(:last-child), td:not(:last-child) {
margin-right: 1rem;
}

th {
text-align: left;
}
</style>
<title>Handlebars.js Builds</title>
</head>
<body>
<h1>Handlebars.js builds</h1>
<p>See <a href="https://handlebarsjs.com">https://handlebarsjs.com</a> for documentation.</p>
<p>Machine-readable version: <a href="{{jsonListUrl}}">{{jsonListUrl}}</a></p>
<table>
<thead>
<tr>
<th data-col="key"><a href="#" onclick="return toggleSort('key')">Name</a></th>
<th data-col="size"><a href="#" onclick="return toggleSort('size')">Size</a></th>
<th data-col="lastModified"><a href="#" onclick="return toggleSort('lastModified')">Last-Modified</a></th>
</tr>
</thead>
<tbody id="files">
{{#each fileList as | file |}}
<tr>
<td data-col="key"><a href="{{file.key}}">{{key}}</a></td>
<td data-col="size">{{file.size}}</td>
<td data-col="lastModified">{{file.lastModified}}</td>
</tr>
{{/each}}
</tbody>

</table>
<script type="application/javascript">
const files = {{{json fileList}}};
const fileElements = Array.from(document.querySelectorAll("#files > tr"));


applyNewOrder()

function getSearchParams() {
return new URLSearchParams(window.location.hash.slice(1));
}

function toggleSort(newSortProperty) {
const params = getSearchParams()

const oldSortProperty = params.get("sort");
if (oldSortProperty === newSortProperty) {
const newDir = params.get("dir") === "asc" ? "desc" : "asc"
window.location.hash = "sort=" + newSortProperty + "&dir=" + newDir
} else {
window.location.hash = "sort=" + newSortProperty

}
setTimeout(() => applyNewOrder())


return false
}

function applyNewOrder() {
const params = getSearchParams()
const sortProperty = params.get("sort") ?? "lastModified"
const ascending = params.get("dir") === "asc"
sortFilesArray(sortProperty, ascending);
updateRows();
}

function sortFilesArray(propertyName, ascending) {

files.sort(compareByProp(propertyName))
if (!ascending) {
files.reverse()
}
}

function compareByProp(propertyName) {
return (file1, file2) => {
if (file1[propertyName] === file2[propertyName]) {
return 0
}
if (file1[propertyName] > file2[propertyName]) {
return 1
}
return -1
}
}

function updateRows() {
let index = 0;
for (const rowElement of fileElements) {
update(rowElement, files[index++])
}
}

function update(rowElement, file) {
const link = rowElement.querySelector('[data-col="key"] a')
link.setAttribute("href", file.key)
link.innerText = file.key

const size = rowElement.querySelector('[data-col="size"]')
size.innerText = file.size

const lastModified = rowElement.querySelector('[data-col="lastModified"]')
lastModified.innerText = file.lastModified
}
</script>
</body>
</html>
31 changes: 31 additions & 0 deletions tasks/aws-s3-builds-page/generateFileList-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
const crypto = require('crypto');

const { runTest } = require('./test-utils/runTest');
const { createS3Client } = require('./s3client');
const { generateFileList } = require('./generateFileList');
const assert = require('node:assert');

// This is a test file. It is intended to be run manually with the proper environment variables set
//
// Run it from the project root using "node tasks/aws-s3-builds-page/generateFileList-test.js"

const s3Client = createS3Client();

runTest(async ({ log }) => {
log('Generate file list');
const filename = `test-file-list-${crypto.randomUUID()}`;
await generateFileList(filename);

log(`Checking JSON at ${s3Client.fileUrl(`${filename}.json`)}`);
const jsonList = JSON.parse(await s3Client.fetchFile(`${filename}.json`));
assert(jsonList.find(s3obj => s3obj.key === 'handlebars-v4.7.7.js'));

log(`Checking HTML at ${s3Client.fileUrl(`${filename}.html`)}`);
const htmlList = await s3Client.fetchFile(`${filename}.html`);
assert(htmlList.includes('handlebars-v4.7.7.js'));
assert(htmlList.includes('handlebarsjs.com'));
assert(!htmlList.includes('index.html'));

log(`Deleting file ${filename}.json`);
await s3Client.deleteFile(`${filename}.json`);
});
39 changes: 39 additions & 0 deletions tasks/aws-s3-builds-page/generateFileList.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/* eslint-disable no-console */
const { createS3Client } = require('./s3client');
const Handlebars = require('../..');
const fs = require('node:fs/promises');
const path = require('path');

async function generateFileList(nameWithoutExtension) {
const s3Client = createS3Client();
const fileList = await s3Client.listFiles();
const relevantFiles = fileList.filter(s3obj => s3obj.key.endsWith('.js'));

await uploadJson(s3Client, relevantFiles, nameWithoutExtension);
await uploadHtml(s3Client, relevantFiles, nameWithoutExtension);
}

async function uploadJson(s3Client, fileList, nameWithoutExtension) {
const fileListJson = JSON.stringify(fileList, null, 2);
await s3Client.uploadData(fileListJson, nameWithoutExtension + '.json', {
contentType: 'application/json'
});
}

async function uploadHtml(s3Client, fileList, nameWithoutExtension) {
const templateStr = await fs.readFile(
path.join(__dirname, 'fileList.hbs'),
'utf-8'
);
const template = Handlebars.compile(templateStr);
Handlebars.registerHelper('json', obj => JSON.stringify(obj));
const fileListHtml = template({
fileList,
jsonListUrl: nameWithoutExtension + '.json'
});
await s3Client.uploadData(fileListHtml, nameWithoutExtension + '.html', {
contentType: 'text/html'
});
}

module.exports = { generateFileList };
47 changes: 47 additions & 0 deletions tasks/aws-s3-builds-page/publish-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
const crypto = require('crypto');
const { publishWithSuffixes } = require('./publish');
const { runTest } = require('./test-utils/runTest');
const { createS3Client } = require('./s3client');
const fs = require('node:fs/promises');

// This is a test file. It is intended to be run manually with the proper environment variables set
//
// Run it from the project root using "node tasks/aws-s3-builds-page/publish-test.js"

const s3Client = createS3Client();

runTest(async ({ log }) => {
const suffix1 = `-test-file-` + crypto.randomUUID();
const suffix2 = `-test-file-` + crypto.randomUUID();
log(`Publish ${suffix1} and ${suffix2}`);
await publishWithSuffixes([suffix1, suffix2]);
await compareAndDeleteFiles(suffix1, log);
await compareAndDeleteFiles(suffix2, log);
});

async function compareAndDeleteFiles(suffix, log) {
const pairs = [
['dist/handlebars.js', `handlebars${suffix}.js`],
['dist/handlebars.min.js', `handlebars.min${suffix}.js`],
['dist/handlebars.runtime.js', `handlebars.runtime${suffix}.js`],
['dist/handlebars.runtime.min.js', `handlebars.runtime.min${suffix}.js`]
];
for (const [localFile, remoteFile] of pairs) {
await expectSameContents(localFile, remoteFile, log);
log(`Deleting "${remoteFile}"`);
await s3Client.deleteFile(remoteFile);
}
}

async function expectSameContents(localFile, remoteFile, log) {
log(
`Checking file contents "${localFile}" vs "${s3Client.fileUrl(remoteFile)}"`
);
const remoteContents = await s3Client.fetchFile(remoteFile);
const localContents = await fs.readFile(localFile, 'utf-8');
if (remoteContents !== localContents) {
throw new Error(
`Files do not match: ${localFile}" vs "${s3Client.fileUrl(remoteFile)}"`
);
}
}
37 changes: 37 additions & 0 deletions tasks/aws-s3-builds-page/publish.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/* eslint-disable no-console */
const { createS3Client } = require('./s3client');

const filenames = [
'handlebars.js',
'handlebars.min.js',
'handlebars.runtime.js',
'handlebars.runtime.min.js'
];

async function publishWithSuffixes(suffixes) {
const s3Client = createS3Client();
const publishPromises = suffixes.map(suffix =>
publishSuffix(s3Client, suffix)
);
return Promise.all(publishPromises);
}

async function publishSuffix(s3client, suffix) {
const publishPromises = filenames.map(async filename => {
const nameInBucket = getNameInBucket(filename, suffix);
const localFile = getLocalFile(filename);
await s3client.uploadFile(localFile, nameInBucket);
console.log(`Published ${localFile} to build server (${nameInBucket})`);
});
return Promise.all(publishPromises);
}

function getNameInBucket(filename, suffix) {
return filename.replace(/\.js$/, suffix + '.js');
}

function getLocalFile(filename) {
return 'dist/' + filename;
}

module.exports = { publishWithSuffixes };
11 changes: 11 additions & 0 deletions tasks/aws-s3-builds-page/s3client/deleteFile.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
const { DeleteObjectCommand } = require('@aws-sdk/client-s3');

async function deleteFile(s3Client, bucket, remoteName) {
const command = new DeleteObjectCommand({
Bucket: bucket,
Key: remoteName
});
await s3Client.send(command);
}

module.exports = { deleteFile };
10 changes: 10 additions & 0 deletions tasks/aws-s3-builds-page/s3client/fetchFile.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
async function fetchFile(bucket, remoteName) {
return (await fetch(fileUrl(bucket, remoteName))).text();
}

function fileUrl(bucket, remoteName) {
const bucketUrl = `https://s3.amazonaws.com/${bucket}`;
return `${bucketUrl}/${remoteName}`;
}

module.exports = { fetchFile, fileUrl };
42 changes: 42 additions & 0 deletions tasks/aws-s3-builds-page/s3client/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
const { listFiles } = require('./listFiles');
const { uploadFile, uploadData } = require('./uploadFile');
const { deleteFile } = require('./deleteFile');
const { S3Client } = require('@aws-sdk/client-s3');
const { requireEnvVar } = require('./requireEnvVar');
const { fetchFile, fileUrl } = require('./fetchFile');

module.exports = { createS3Client };

function createS3Client() {
// https://docs.aws.amazon.com/sdk-for-javascript/v2/developer-guide/loading-node-credentials-environment.html
requireEnvVar('AWS_ACCESS_KEY_ID');
requireEnvVar('AWS_SECRET_ACCESS_KEY');

const bucket = requireEnvVar('S3_BUCKET_NAME');
const s3Client = new S3Client({
region: 'us-east-1'
});

return {
async listFiles() {
return listFiles(s3Client, bucket);
},
async uploadFile(localName, remoteName, { contentType } = {}) {
await uploadFile(s3Client, bucket, localName, remoteName, {
contentType
});
},
async uploadData(data, remoteName, { contentType } = {}) {
await uploadData(s3Client, bucket, data, remoteName, { contentType });
},
async deleteFile(remoteName) {
await deleteFile(s3Client, bucket, remoteName);
},
async fetchFile(remoteName) {
return fetchFile(bucket, remoteName);
},
fileUrl(remoteName) {
return fileUrl(bucket, remoteName);
}
};
}
32 changes: 32 additions & 0 deletions tasks/aws-s3-builds-page/s3client/listFiles.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
const { ListObjectsV2Command } = require('@aws-sdk/client-s3');

async function listFiles(s3Client, bucket) {
const command = new ListObjectsV2Command({
Bucket: bucket
});

let isTruncated = true;
const files = [];

while (isTruncated) {
const {
Contents,
IsTruncated,
NextContinuationToken
} = await s3Client.send(command);
files.push(...Contents.map(dataFromS3Object));
isTruncated = IsTruncated;
command.input.ContinuationToken = NextContinuationToken;
}
return files;
}

function dataFromS3Object(s3obj) {
return {
key: s3obj.Key,
size: s3obj.Size,
lastModified: s3obj.LastModified.toISOString()
};
}

module.exports = { listFiles };
Loading
Loading