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

Compile within single slice only #20

Merged
merged 26 commits into from
Feb 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
a666d2e
Path slice path via args and look for assets in single slice only
timriley Jan 9, 2024
16e2c9a
Remove publicDir plugin option
timriley Jan 10, 2024
2228a4d
Use --target arg to determine output directory
timriley Jan 10, 2024
1458b9c
Fix argv format in tests
timriley Jan 10, 2024
8a3294a
Write manifest file inside target dir
timriley Jan 10, 2024
6916c34
Process individual slices in isolation (code is messy)
timriley Jan 13, 2024
d941afd
Refactor
timriley Jan 14, 2024
15440ac
Isolate handling of external directories
timriley Jan 15, 2024
1c811ac
Fix SRI handling
timriley Jan 15, 2024
cc86929
Include fonts in tests
timriley Jan 15, 2024
fed9b7b
Remove "full app" scenario
timriley Jan 15, 2024
3abccdc
Fix watch test
timriley Jan 15, 2024
6ba5309
Clean up after tests again
timriley Jan 15, 2024
425f947
Prettier
timriley Jan 15, 2024
3ee71a5
Remove stray console.log lines
timriley Jan 15, 2024
6512383
Use extension from compiled asset in manifest keys
timriley Jan 23, 2024
246826b
Put CSS, etc. back in the manifest
timriley Jan 23, 2024
a620554
Fix prettier issue
timriley Feb 5, 2024
cfb376b
Rename --target to --dest
timriley Feb 6, 2024
506b532
Pass root into the plugin options from the outside
timriley Feb 6, 2024
87b28aa
Rename baseDir to sourceDir for clarity
timriley Feb 6, 2024
c029f8a
Extract “assets” strings into constants
timriley Feb 6, 2024
827d434
Remove unneeded shebang line
timriley Feb 6, 2024
1bd2ecc
Remove stale comment
timriley Feb 6, 2024
1ffd9a6
Shift regexp up a level
timriley Feb 6, 2024
aafddf5
Use interface for copied assets; clarify naming
timriley Feb 6, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions dist/args.d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
export interface Args {
path: string;
dest: string;
watch: Boolean;
sri: string[] | null;
}
Expand Down
2 changes: 2 additions & 0 deletions dist/args.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ export const parseArgs = (args) => {
result[key] = value;
});
return {
path: result["path"],
dest: result["dest"],
watch: result.hasOwnProperty("watch"),
sri: result["sri"]?.split(","),
};
Expand Down
7 changes: 3 additions & 4 deletions dist/esbuild-plugin.d.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import { Plugin } from "esbuild";
export interface PluginOptions {
root: string;
publicDir: string;
sourceDir: string;
destDir: string;
manifestPath: string;
sriAlgorithms: Array<string>;
hash: boolean;
}
export declare const defaults: Pick<PluginOptions, "root" | "publicDir" | "destDir" | "manifestPath" | "sriAlgorithms" | "hash">;
declare const hanamiEsbuild: (options?: PluginOptions) => Plugin;
export declare const defaults: Pick<PluginOptions, "sriAlgorithms" | "hash">;
declare const hanamiEsbuild: (options: PluginOptions) => Plugin;
export default hanamiEsbuild;
117 changes: 72 additions & 45 deletions dist/esbuild-plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,29 +3,20 @@ import path from "path";
import crypto from "node:crypto";
const URL_SEPARATOR = "/";
export const defaults = {
root: "",
publicDir: "public",
destDir: path.join("public", "assets"),
manifestPath: path.join("public", "assets.json"),
sriAlgorithms: [],
hash: true,
};
const hanamiEsbuild = (options = { ...defaults }) => {
const assetsDirName = "assets";
const hanamiEsbuild = (options) => {
return {
name: "hanami-esbuild",
setup(build) {
build.initialOptions.metafile = true;
options.root = options.root || process.cwd();
const manifest = path.join(options.root, options.manifestPath);
const manifestPath = path.join(options.root, options.destDir, "assets.json");
const externalDirs = build.initialOptions.external || [];
build.onEnd(async (result) => {
const outputs = result.metafile?.outputs;
const assetsManifest = {};
const calulateSourceUrl = (str) => {
return normalizeUrl(str)
.replace(/\/assets\//, "")
.replace(/-[A-Z0-9]{8}/, "");
};
const calulateDestinationUrl = (str) => {
return normalizeUrl(str).replace(/public/, "");
};
Expand All @@ -45,17 +36,34 @@ const hanamiEsbuild = (options = { ...defaults }) => {
const result = crypto.createHash("sha256").update(hashBytes).digest("hex");
return result.slice(0, 8).toUpperCase();
};
function extractEsbuildInputs(inputData) {
const inputs = {};
for (const key in inputData) {
const entry = inputData[key];
if (entry.inputs) {
for (const inputKey in entry.inputs) {
inputs[inputKey] = true;
}
// Transforms the esbuild metafile outputs into an object containing mappings of outputs
// generated from entryPoints only.
//
// Converts this:
//
// {
// 'public/assets/admin/app-ITGLRDE7.js': {
// imports: [],
// exports: [],
// entryPoint: 'slices/admin/assets/js/app.js',
// inputs: { 'slices/admin/assets/js/app.js': [Object] },
// bytes: 95
// }
// }
//
// To this:
//
// {
// 'public/assets/admin/app-ITGLRDE7.js': true
// }
function extractEsbuildCompiledEntrypoints(esbuildOutputs) {
const entryPoints = {};
for (const key in esbuildOutputs) {
if (!key.endsWith(".map")) {
entryPoints[key] = true;
}
}
return inputs;
return entryPoints;
}
// TODO: profile the current implementation vs blindly copying the asset
const copyAsset = (srcPath, destPath) => {
Expand All @@ -73,62 +81,81 @@ const hanamiEsbuild = (options = { ...defaults }) => {
fs.copyFileSync(srcPath, destPath);
return;
};
const processAssetDirectory = (pattern, inputs, options) => {
const processAssetDirectory = (pattern, compiledEntryPoints, options) => {
const dirPath = path.dirname(pattern);
const files = fs.readdirSync(dirPath, { recursive: true });
const assets = [];
files.forEach((file) => {
const srcPath = path.join(dirPath, file.toString());
const sourcePath = path.join(dirPath, file.toString());
// Skip if the file is already processed by esbuild
if (inputs.hasOwnProperty(srcPath)) {
if (compiledEntryPoints.hasOwnProperty(sourcePath)) {
return;
}
// Skip directories and any other non-files
if (!fs.statSync(srcPath).isFile()) {
if (!fs.statSync(sourcePath).isFile()) {
return;
}
const fileHash = calculateHash(fs.readFileSync(srcPath), options.hash);
const fileExtension = path.extname(srcPath);
const baseName = path.basename(srcPath, fileExtension);
const fileHash = calculateHash(fs.readFileSync(sourcePath), options.hash);
const fileExtension = path.extname(sourcePath);
const baseName = path.basename(sourcePath, fileExtension);
const destFileName = [baseName, fileHash].filter((item) => item !== null).join("-") + fileExtension;
const destPath = path.join(options.destDir, path.relative(dirPath, srcPath).replace(path.basename(file.toString()), destFileName));
if (fs.lstatSync(srcPath).isDirectory()) {
assets.push(...processAssetDirectory(destPath, inputs, options));
const destPath = path.join(options.destDir, path
.relative(dirPath, sourcePath)
.replace(path.basename(file.toString()), destFileName));
if (fs.lstatSync(sourcePath).isDirectory()) {
assets.push(...processAssetDirectory(destPath, compiledEntryPoints, options));
}
else {
copyAsset(srcPath, destPath);
assets.push(destPath);
copyAsset(sourcePath, destPath);
assets.push({ sourcePath: sourcePath, destPath: destPath });
}
});
return assets;
};
if (typeof outputs === "undefined") {
return;
}
const inputs = extractEsbuildInputs(outputs);
const compiledEntryPoints = extractEsbuildCompiledEntrypoints(outputs);
const copiedAssets = [];
externalDirs.forEach((pattern) => {
copiedAssets.push(...processAssetDirectory(pattern, inputs, options));
copiedAssets.push(...processAssetDirectory(pattern, compiledEntryPoints, options));
});
const assetsToProcess = Object.keys(outputs).concat(copiedAssets);
for (const assetToProcess of assetsToProcess) {
if (assetToProcess.endsWith(".map")) {
continue;
}
const destinationUrl = calulateDestinationUrl(assetToProcess);
const sourceUrl = calulateSourceUrl(destinationUrl);
function prepareAsset(assetPath, destinationUrl) {
var asset = { url: destinationUrl };
if (options.sriAlgorithms.length > 0) {
asset.sri = [];
for (const algorithm of options.sriAlgorithms) {
const subresourceIntegrity = calculateSubresourceIntegrity(algorithm, assetToProcess);
const subresourceIntegrity = calculateSubresourceIntegrity(algorithm, path.join(options.root, assetPath));
asset.sri.push(subresourceIntegrity);
}
}
assetsManifest[sourceUrl] = asset;
return asset;
}
// Process entrypoints
const fileHashRegexp = /(-[A-Z0-9]{8})(\.\S+)$/;
for (const compiledEntryPoint in compiledEntryPoints) {
// Convert "public/assets/app-2TLUHCQ6.js" to "app.js"
let sourceUrl = compiledEntryPoint
.replace(options.destDir + "/", "")
.replace(fileHashRegexp, "$2");
const destinationUrl = calulateDestinationUrl(compiledEntryPoint);
assetsManifest[sourceUrl] = prepareAsset(compiledEntryPoint, destinationUrl);
}
// Process copied assets
for (const copiedAsset of copiedAssets) {
// TODO: I wonder if we can skip .map files earlier
if (copiedAsset.sourcePath.endsWith(".map")) {
continue;
}
const destinationUrl = calulateDestinationUrl(copiedAsset.destPath);
// Take the full path of the copied asset and remove everything up to (and including) the "assets/" dir
var sourceUrl = copiedAsset.sourcePath.replace(path.join(options.root, options.sourceDir, assetsDirName) + "/", "");
// Then remove the first subdir (e.g. "images/"), since we do not include those in the asset paths
sourceUrl = sourceUrl.substring(sourceUrl.indexOf("/") + 1);
assetsManifest[sourceUrl] = prepareAsset(copiedAsset.destPath, destinationUrl);
}
// Write assets manifest to the destination directory
await fs.writeJson(manifest, assetsManifest, { spaces: 2 });
await fs.writeJson(manifestPath, assetsManifest, { spaces: 2 });
});
},
};
Expand Down
46 changes: 22 additions & 24 deletions dist/esbuild.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,36 +18,28 @@ const loader = {
".eot": "file",
".ttf": "file",
};
const assetsDirName = "assets";
const entryPointExtensions = "app.{js,ts,mjs,mts,tsx,jsx}";
// FIXME: make cross platform
const entryPointsMatcher = /(app\/assets\/js\/|slices\/(.*\/)assets\/js\/)/;
const findEntryPoints = (root) => {
const findEntryPoints = (sliceRoot) => {
const result = {};
// TODO: should this be done explicitly within the root?
const entryPoints = globSync([
path.join("app", "assets", "js", "**", entryPointExtensions),
path.join("slices", "*", "assets", "js", "**", entryPointExtensions),
path.join(sliceRoot, assetsDirName, "js", "**", entryPointExtensions),
]);
entryPoints.forEach((entryPoint) => {
let modifiedPath = entryPoint.replace(entryPointsMatcher, "$2");
const relativePath = path.relative(root, modifiedPath);
const { dir, name } = path.parse(relativePath);
let entryPointPath = entryPoint.replace(sliceRoot + "/assets/js/", "");
const { dir, name } = path.parse(entryPointPath);
if (dir) {
modifiedPath = path.join(dir, name);
entryPointPath = path.join(dir, name);
}
else {
modifiedPath = name;
entryPointPath = name;
}
result[modifiedPath] = entryPoint;
result[entryPointPath] = entryPoint;
});
return result;
};
// TODO: feels like this really should be passed a root too, to become the cwd for globSync
const externalDirectories = () => {
const assetDirsPattern = [
path.join("app", "assets", "*"),
path.join("slices", "*", "assets", "*"),
];
const findExternalDirectories = (basePath) => {
const assetDirsPattern = [path.join(basePath, assetsDirName, "*")];
const excludeDirs = ["js", "css"];
try {
const dirs = globSync(assetDirsPattern, { nodir: false });
Expand All @@ -66,41 +58,47 @@ const externalDirectories = () => {
export const buildOptions = (root, args) => {
const pluginOptions = {
...pluginDefaults,
root: root,
sourceDir: args.path,
destDir: args.dest,
sriAlgorithms: args.sri || [],
};
const plugin = esbuildPlugin(pluginOptions);
const options = {
bundle: true,
outdir: path.join(root, "public", "assets"),
outdir: args.dest,
absWorkingDir: root,
loader: loader,
external: externalDirectories(),
external: findExternalDirectories(path.join(root, args.path)),
logLevel: "info",
minify: true,
sourcemap: true,
entryNames: "[dir]/[name]-[hash]",
entryPoints: findEntryPoints(root),
entryPoints: findEntryPoints(path.join(root, args.path)),
plugins: [plugin],
};
return options;
};
export const watchOptions = (root, args) => {
const pluginOptions = {
...pluginDefaults,
root: root,
sourceDir: args.path,
destDir: args.dest,
hash: false,
};
const plugin = esbuildPlugin(pluginOptions);
const options = {
bundle: true,
outdir: path.join(root, "public", "assets"),
outdir: args.dest,
absWorkingDir: root,
loader: loader,
external: externalDirectories(),
external: findExternalDirectories(path.join(root, args.path)),
logLevel: "info",
minify: false,
sourcemap: false,
entryNames: "[dir]/[name]",
entryPoints: findEntryPoints(root),
entryPoints: findEntryPoints(path.join(root, args.path)),
plugins: [plugin],
};
return options;
Expand Down
1 change: 0 additions & 1 deletion dist/index.d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
#!/usr/bin/env node
import { BuildContext } from "esbuild";
import { Args } from "./args.js";
import { EsbuildOptions } from "./esbuild.js";
Expand Down
2 changes: 1 addition & 1 deletion dist/index.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
#!/usr/bin/env node
import fs from "fs-extra";
import path from "path";
import esbuild from "esbuild";
import { parseArgs } from "./args.js";
import { buildOptions, watchOptions } from "./esbuild.js";
export const run = async function (options) {
// TODO: Allow root to be provided (optionally) as a --root arg
const { root = process.cwd(), argv = process.argv, esbuildOptionsFn = null } = options || {};
const args = parseArgs(argv);
// TODO: make nicer
Expand Down
4 changes: 4 additions & 0 deletions src/args.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
export interface Args {
path: string;
dest: string;
watch: Boolean;
sri: string[] | null;
}
Expand All @@ -12,6 +14,8 @@ export const parseArgs = (args: string[]): Args => {
});

return {
path: result["path"],
dest: result["dest"],
watch: result.hasOwnProperty("watch"),
sri: result["sri"]?.split(","),
};
Expand Down