Skip to content

Commit

Permalink
DOP-5128 and DOP-5167 (#34)
Browse files Browse the repository at this point in the history
* initial commit. boilerplate

* startup commits

* make copy of offline snooty and build

* fix syntax

* run format

* biome check --write

* test upload to S3

* remove jsdom for now

* remove jsdom. write comments for todo with 5167

* update build command

* remove jsdom

* add logic to derive filename and bucket from previous extension

* merge conflicts

* run lint

* update package lock

* new extension format

* run format

* add logic for updating html

* remove directory filepaths that are empty

* add logs

* add logs

* add logs

* test without converting

* test upload without converting

* testing

* test

* update logs

* fix promise awaits

* clean logs

* remove recursive log

* recursive readdir

* keep log of dirs

* close happy dom before returning

* fix parent pathing since we are using new filename

* fix relative path since not recursive

* address comments

* address comments
  • Loading branch information
seungpark authored Nov 18, 2024
1 parent 78ae0c0 commit d09b19b
Show file tree
Hide file tree
Showing 10 changed files with 1,124 additions and 31 deletions.
4 changes: 4 additions & 0 deletions extensions/offline-snooty/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,21 @@
"dev": "netlify-extension dev --open"
},
"dependencies": {
"@aws-sdk/client-s3": "^3.685.0",
"@netlify/functions": "^2.8.2",
"@netlify/sdk": "^2.8.1",
"@tanstack/react-query": "^5.59.16",
"@trpc/client": "^11.0.0-rc.477",
"@trpc/react-query": "^11.0.0-rc.477",
"@trpc/server": "^11.0.0-rc.477",
"happy-dom": "^15.10.1",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"tar": "^7.4.3",
"zod": "^3.23.8"
},
"devDependencies": {
"@netlify/build": "^29.56.0",
"@netlify/netlify-plugin-netlify-extension": "^1.0.3",
"@tsconfig/node18": "^18.2.4",
"@tsconfig/recommended": "^1.0.8",
Expand Down
46 changes: 46 additions & 0 deletions extensions/offline-snooty/src/convertGatsbyToHtml/fileHandler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { HTMLAnchorElement, HTMLImageElement, Node, Window } from 'happy-dom';
import { promises as fsPromises } from 'node:fs';

function updateToRelativePaths(nodeList: Node[], prefix: string) {
// for links: href = relativePath + href + index.html
// for images: src = relativePath + src
for (const node of nodeList) {
if (node instanceof HTMLAnchorElement && node['href'].startsWith('/')) {
// TODO: strip hash and query portions
const targetHref = (prefix + node['href'] + '/index.html').replaceAll(
/\/+/g,
'/',
);
node.setAttribute('href', targetHref);
} else if (
node instanceof HTMLImageElement &&
node['src'].startsWith('/')
) {
node['src'] = (prefix + node['src']).replace(/\/+/, '/');
}
}
}

export const handleHtmlFile = async (
filepath: string,
relativePath: string,
) => {
console.log('handlehtmlfile ', filepath);
// update the DOM. change paths for links and images
// first open the file. as a DOM string.
const html = (await fsPromises.readFile(filepath)).toString();
const window = new Window();
const document = window.document;
document.write(html);

const links = document.querySelectorAll('a');
const images = document.querySelectorAll('img');
// TODO: should handle background-image url as well
updateToRelativePaths([...links, ...images], relativePath ?? './');

console.log('writing file html ', filepath);

await fsPromises.writeFile(filepath, document.documentElement.innerHTML);

await window.happyDOM.close();
};
125 changes: 125 additions & 0 deletions extensions/offline-snooty/src/convertGatsbyToHtml/imageExtensions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
export const IMAGE_EXT = new Set<string>([
'ase',
'art',
'bmp',
'blp',
'cd5',
'cit',
'cpt',
'cr2',
'cut',
'dds',
'dib',
'djvu',
'egt',
'exif',
'gif',
'gpl',
'grf',
'icns',
'ico',
'iff',
'jng',
'jpeg',
'jpg',
'jfif',
'jp2',
'jps',
'lbm',
'max',
'miff',
'mng',
'msp',
'nef',
'nitf',
'ota',
'pbm',
'pc1',
'pc2',
'pc3',
'pcf',
'pcx',
'pdn',
'pgm',
'PI1',
'PI2',
'PI3',
'pict',
'pct',
'pnm',
'pns',
'ppm',
'psb',
'psd',
'pdd',
'psp',
'px',
'pxm',
'pxr',
'qfx',
'raw',
'rle',
'sct',
'sgi',
'rgb',
'int',
'bw',
'tga',
'tiff',
'tif',
'vtf',
'xbm',
'xcf',
'xpm',
'3dv',
'amf',
'ai',
'awg',
'cgm',
'cdr',
'cmx',
'dxf',
'e2d',
'egt',
'eps',
'fs',
'gbr',
'odg',
'svg',
'stl',
'vrml',
'x3d',
'sxd',
'v2d',
'vnd',
'wmf',
'emf',
'art',
'xar',
'png',
'webp',
'jxr',
'hdp',
'wdp',
'cur',
'ecw',
'iff',
'lbm',
'liff',
'nrrd',
'pam',
'pcx',
'pgf',
'sgi',
'rgb',
'rgba',
'bw',
'int',
'inta',
'sid',
'ras',
'sun',
'tga',
'heic',
'heif',
]);
101 changes: 98 additions & 3 deletions extensions/offline-snooty/src/convertGatsbyToHtml/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,103 @@
* @param path full directory path of gatsby output
*/

import { type Gzip, createGzip } from 'node:zlib';
import { existsSync, promises as fsPromises } from 'node:fs';
import { join, dirname } from 'node:path';
import { create } from 'tar';
import { handleHtmlFile } from './fileHandler';
import { IMAGE_EXT } from './imageExtensions';
import { PUBLIC_OUTPUT_PATH } from '..';

export const convertGatsbyToHtml = async (path: string): Promise<Gzip> => {
return createGzip();
type FileUpdateLog = {
processedHtmlFiles: string[];
removedFiles: string[];
filePathsPerDir: { [key: string]: string[] };
};

const log: FileUpdateLog = {
processedHtmlFiles: [],
removedFiles: [],
filePathsPerDir: {},
};

// get all full directory pathnames leading up to current path
function getParentPaths(filename: string): string[] {
const res: string[] = [];
let currentDirectory = dirname(filename);
let isRoot = currentDirectory === PUBLIC_OUTPUT_PATH;
while (!isRoot) {
res.push(currentDirectory);
const currentParts = currentDirectory.split('/');
currentDirectory = currentParts.slice(0, -1).join('/');
// note: can update this to be read from original rootfilename of scanFileTree.
isRoot = currentDirectory === PUBLIC_OUTPUT_PATH;
}

return res;
}

// traverses into a directory, and handles each file.
// each file type handler should handle what to do with current file type
async function scanFileTree(directoryPath: string) {
if (!existsSync(directoryPath)) {
console.error(`no directory at ${directoryPath}`);
return;
}
if (!log.filePathsPerDir[directoryPath]) {
log.filePathsPerDir[directoryPath] = [];
}

const files = await fsPromises.readdir(directoryPath, { recursive: true });
for (const file of files) {
const filename = join(directoryPath, file);
const stat = await fsPromises.stat(filename);
const extName = filename.split('.').pop() ?? '';

if (stat.isDirectory() || IMAGE_EXT.has(extName)) {
continue;
} else if (extName.endsWith('html')) {
const allParentPaths = getParentPaths(filename);
const pathBackToRoot = '../'.repeat(allParentPaths.length);
await handleHtmlFile(filename, pathBackToRoot || './');
for (const parentPath of allParentPaths) {
if (!log.filePathsPerDir[parentPath]) {
log.filePathsPerDir[parentPath] = [];
}
log.filePathsPerDir[parentPath].push(filename);
}
} else {
// delete the file
await fsPromises.rm(filename);
console.log('removing file ', filename);

log.removedFiles.push(filename);
}
}
}

export const convertGatsbyToHtml = async (
gatsbyOutputPath: string,
fileName: string,
): Promise<void> => {
await scanFileTree(gatsbyOutputPath);
console.log('>>>>>>>>>> converted gatsby results <<<<<<<<<<<<<');
console.log(JSON.stringify(log));

// remove empty directories
await Promise.all(
Object.entries(log.filePathsPerDir).map(async ([path, filenames]) => {
if (!filenames.length && path !== PUBLIC_OUTPUT_PATH) {
return fsPromises.rm(path, { recursive: true, force: true });
}
}),
);

await create(
{
gzip: true,
file: fileName,
cwd: gatsbyOutputPath,
},
['./'],
);
};
24 changes: 24 additions & 0 deletions extensions/offline-snooty/src/createSnootyCopy/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import type { NetlifyPluginUtils } from '@netlify/build';

export const createSnootyCopy = async (
run: NetlifyPluginUtils['run'],
targetPath: string,
) => {
await run.command(
`rsync -av ${process.cwd()}/snooty ${targetPath} --exclude public --exclude node_modules`,
);

const offlineSnootyPath = `${targetPath}/snooty`;

await run.command('npm ci --legacy-peer-deps', {
cwd: offlineSnootyPath,
});

await run.command('npm run clean', {
cwd: offlineSnootyPath,
});

await run.command('npm run build:no-prefix', {
cwd: offlineSnootyPath,
});
};
64 changes: 52 additions & 12 deletions extensions/offline-snooty/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,55 @@
// Documentation: https://sdk.netlify.com
import { envVarToBool, Extension } from 'util/extension';
const extension = new Extension({
isEnabled: envVarToBool(process.env.OFFLINE_SNOOTY_ENABLED),
});

extension.addBuildEventHandler('onPreBuild', () => {
// If the build event handler is not enabled, return early
if (!process.env.OFFLINE_SNOOTY_ENABLED) {
return;
}
console.log('Hello there.');
});
import { NetlifyExtension } from '@netlify/sdk';
import { convertGatsbyToHtml } from './convertGatsbyToHtml';
import { createSnootyCopy } from './createSnootyCopy';
import { destroyClient, uploadToS3 } from './uploadToS3';
import {
type BranchEntry,
type DocsetsDocument,
type ReposBranchesDocument,
readEnvConfigs,
} from './uploadToS3/utils';

const extension = new NetlifyExtension();
const NEW_SNOOTY_PATH = `${process.cwd()}/snooty-offline`;
export const PUBLIC_OUTPUT_PATH = `${NEW_SNOOTY_PATH}/snooty/public`;

// run this extension after the build and deploy are successful
extension.addBuildEventHandler(
'onSuccess',
async ({ netlifyConfig, utils: { run } }) => {
// If the build event handler is not enabled, return early
if (!process.env.OFFLINE_SNOOTY_ENABLED) {
return;
}

const environment = netlifyConfig.build.environment as Record<
string,
string | DocsetsDocument | ReposBranchesDocument | BranchEntry
>;
const { bucketName, fileName } = readEnvConfigs({
env: (environment.ENV as string) ?? '',
docsetEntry: (environment.DOCSET_ENTRY as DocsetsDocument) ?? {},
repoEntry: (environment.REPO_ENTRY as ReposBranchesDocument) ?? {},
branchEntry: (environment.BRANCH_ENTRY as BranchEntry) ?? {},
});

try {
console.log('... creating snooty copy');
await createSnootyCopy(run, NEW_SNOOTY_PATH);
console.log('... converting gatsby to html');
await convertGatsbyToHtml(PUBLIC_OUTPUT_PATH, fileName);
console.log('... uploading to AWS S3 ', bucketName, fileName);
await uploadToS3(`${process.cwd()}/${fileName}`, bucketName, fileName);
console.log('... uploaded to AWS S3');
// TODO: update atlas collection repos_branches to signal offline availability
} catch (e) {
console.error(e);
throw e;
} finally {
destroyClient();
}
},
);

export { extension };
Loading

0 comments on commit d09b19b

Please sign in to comment.