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

Ensure static assets coming from slices have the slice name prefix in dest URL #18

Closed
wants to merge 9 commits into from
Closed
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
5 changes: 1 addition & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,5 @@ jobs:
- name: Install dependencies
run: npm ci

- name: Lint
run: npx prettier . --check

- name: Test
run: make test
run: make ci
7 changes: 6 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,15 @@
BUILD_DIR := dist

# Targets
ci: build lint test

.PHONY: test
test: build
test:
npm test

build:
rm -rf $(BUILD_DIR)
npm run build

lint:
npx prettier . --check
30 changes: 26 additions & 4 deletions dist/esbuild-plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ const hanamiEsbuild = (options = { ...defaults }) => {
build.onEnd(async (result) => {
const outputs = result.metafile?.outputs;
const assetsManifest = {};
const extractSliceName = (dirPath) => {
const regex = /^slices\/([^\/]+)/;
const match = dirPath.match(regex);
return match ? match[1] : null;
};
const calulateSourceUrl = (str) => {
return normalizeUrl(str)
.replace(/\/assets\//, "")
Expand All @@ -45,6 +50,9 @@ const hanamiEsbuild = (options = { ...defaults }) => {
const result = crypto.createHash("sha256").update(hashBytes).digest("hex");
return result.slice(0, 8).toUpperCase();
};
const compactArray = (arr) => {
return arr.filter((token) => token !== null);
};
function extractEsbuildInputs(inputData) {
const inputs = {};
for (const key in inputData) {
Expand Down Expand Up @@ -75,19 +83,29 @@ const hanamiEsbuild = (options = { ...defaults }) => {
};
const processAssetDirectory = (pattern, inputs, options) => {
const dirPath = path.dirname(pattern);
const files = fs.readdirSync(dirPath);
const files = fs.readdirSync(dirPath, { recursive: true });
const assets = [];
files.forEach((file) => {
const srcPath = path.join(dirPath, file);
const srcPath = path.join(dirPath, file.toString());
// Skip if the file is already processed by esbuild
if (inputs.hasOwnProperty(srcPath)) {
return;
}
// Skip directories and any other non-files
if (!fs.statSync(srcPath).isFile()) {
return;
}
const fileHash = calculateHash(fs.readFileSync(srcPath), options.hash);
const fileExtension = path.extname(srcPath);
const baseName = path.basename(srcPath, fileExtension);
const destFileName = [baseName, fileHash].filter((item) => item !== null).join("-") + fileExtension;
const destPath = path.join(options.destDir, path.relative(dirPath, srcPath).replace(file, destFileName));
const sliceName = extractSliceName(dirPath);
const pathTokens = compactArray([
options.destDir,
sliceName,
path.relative(dirPath, srcPath).replace(path.basename(file.toString()), destFileName),
]);
const destPath = path.join(...pathTokens);
if (fs.lstatSync(srcPath).isDirectory()) {
assets.push(...processAssetDirectory(destPath, inputs, options));
}
Expand Down Expand Up @@ -123,8 +141,12 @@ const hanamiEsbuild = (options = { ...defaults }) => {
}
assetsManifest[sourceUrl] = asset;
}
// FIXME: Need a mutex around this
// const existingManifest = fs.readJsonSync(manifest);
// const resultManifest = { ...existingManifest, ...assetsManifest };
const resultManifest = assetsManifest;
// Write assets manifest to the destination directory
await fs.writeJson(manifest, assetsManifest, { spaces: 2 });
await fs.writeJson(manifest, resultManifest, { spaces: 2 });
});
},
};
Expand Down
2 changes: 1 addition & 1 deletion dist/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,5 @@ interface RunOptions {
esbuildOptionsFn?: EsbuildOptionsFn;
}
type EsbuildOptionsFn = (args: Args, esbuildOptions: EsbuildOptions) => EsbuildOptions;
export declare const run: (options?: RunOptions) => Promise<BuildContext | void>;
export declare const run: (options?: RunOptions) => Promise<BuildContext[] | void>;
export {};
112 changes: 103 additions & 9 deletions dist/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import path from "path";
import esbuild from "esbuild";
import { parseArgs } from "./args.js";
import { buildOptions, watchOptions } from "./esbuild.js";
import cloneDeep from "lodash.clonedeep";
export const run = async function (options) {
const { root = process.cwd(), argv = process.argv, esbuildOptionsFn = null } = options || {};
const args = parseArgs(argv);
Expand All @@ -12,23 +13,116 @@ export const run = async function (options) {
if (esbuildOptionsFn) {
esbuildOptions = esbuildOptionsFn(args, esbuildOptions);
}
const errorHandler = (err) => {
console.log(err);
process.exit(1);
};
touchManifest(root);
// const ctx = await esbuild.context(esbuildOptions);
// await ctx.watch().catch(errorHandler);
// return [ctx];
if (args.watch) {
touchManifest(root);
const ctx = await esbuild.context(esbuildOptions);
await ctx.watch().catch(errorHandler);
return ctx;
const contexts = [];
const splitOptions = splitEsbuildOptions(esbuildOptions);
for (const options of splitOptions) {
const ctx = await esbuild.context(options);
contexts.push(ctx);
await ctx.watch().catch(errorHandler);
}
return contexts;
}
else {
await esbuild.build(esbuildOptions).catch(errorHandler);
await esbuildMultipleBuilds(esbuildOptions);
}
};
const errorHandler = (err) => {
console.log(err);
process.exit(1);
};
const touchManifest = (root) => {
const manifestPath = path.join(root, "public", "assets.json");
const manifestDir = path.dirname(manifestPath);
fs.ensureDirSync(manifestDir);
fs.writeFileSync(manifestPath, JSON.stringify({}, null, 2));
};
const esbuildMultipleBuilds = async function (esbuildOptions) {
const builds = splitEsbuildOptions(esbuildOptions);
for (const build of builds) {
await esbuild.build(build).catch(errorHandler);
}
};
const splitEsbuildOptions = (esbuildOptions) => {
const entryPoints = extractEntryPoints(esbuildOptions);
const slices = extractSlices(entryPoints);
const result = slices.map((slice) => {
const sliceName = extractSliceName(slice);
const sliceOptions = cloneDeep(esbuildOptions);
const entryPoints = extractEntryPoints(sliceOptions);
const external = extractExternal(sliceOptions);
sliceOptions.entryPoints = entryPoints.filter((entryPoint) => entryPoint.startsWith(slice));
sliceOptions.external = external.filter((ext) => ext.startsWith(slice));
if (sliceName) {
if (false) {
sliceOptions.entryNames = [sliceName, "[dir]", "[name]-[hash]"].join("/");
sliceOptions.assetNames = [sliceName, "[name]-[hash]"].join("/");
}
else {
sliceOptions.entryNames = [sliceName, "[dir]", "[name]"].join("/");
sliceOptions.assetNames = [sliceName, "[name]"].join("/");
}
}
return sliceOptions;
});
return result;
};
const extractEntryPoints = (esbuildOptions) => {
let entryPoints = [];
if (esbuildOptions.entryPoints &&
typeof esbuildOptions.entryPoints === "object" &&
!Array.isArray(esbuildOptions.entryPoints)) {
entryPoints = Object.values(esbuildOptions.entryPoints);
}
else if (Array.isArray(esbuildOptions.entryPoints)) {
// Handle the case where entryPoints is an array
// Assuming you want to flatten it to a string array
entryPoints = esbuildOptions.entryPoints.flatMap((ep) => typeof ep === "string" ? ep : [ep.in, ep.out]);
}
else if (typeof esbuildOptions.entryPoints === "string") {
entryPoints = [esbuildOptions.entryPoints];
}
return entryPoints;
};
const extractExternal = (esbuildOptions) => {
let external = [];
if (esbuildOptions.external &&
typeof esbuildOptions.external === "object" &&
!Array.isArray(esbuildOptions.external)) {
external = Object.values(esbuildOptions.external);
}
else if (Array.isArray(esbuildOptions.external)) {
// Handle the case where external is an array
// Assuming you want to flatten it to a string array
external = esbuildOptions.external.flatMap((ep) => (typeof ep === "string" ? ep : [ep]));
}
else if (typeof esbuildOptions.external === "string") {
external = [esbuildOptions.external];
}
return external;
};
const extractSlices = (entryPoints) => {
const result = entryPoints.map((entryPoint) => {
return extractSliceOrAppName(entryPoint);
});
return [...new Set(result)];
};
const extractSliceOrAppName = (entryPoint) => {
if (entryPoint.startsWith("app")) {
return "app";
}
const sliceName = extractSliceName(entryPoint);
if (!sliceName) {
throw new Error("Could not extract slice name from entry point: " + entryPoint);
}
return path.join("slices", sliceName);
};
const extractSliceName = (name) => {
const regex = /^slices\/([^\/]+)/;
const match = name.match(regex);
return match ? match[1] : null;
};
24 changes: 23 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"@jest/globals": "^29.6.1",
"@types/fs-extra": "^11.0.1",
"@types/jest": "^29.5.3",
"@types/lodash.clonedeep": "^4.5.9",
"@types/node": "^20.4.5",
"@types/react": "^18.2.16",
"@types/react-dom": "^18.2.7",
Expand All @@ -48,6 +49,7 @@
"dependencies": {
"esbuild": "^0.19.0",
"fs-extra": "^11.1.0",
"glob": "^10.3.3"
"glob": "^10.3.3",
"lodash.clonedeep": "^4.5.0"
}
}
35 changes: 29 additions & 6 deletions src/esbuild-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,12 @@ const hanamiEsbuild = (options: PluginOptions = { ...defaults }): Plugin => {
const outputs = result.metafile?.outputs;
const assetsManifest: Record<string, Asset> = {};

const extractSliceName = (dirPath: string): string | null => {
const regex = /^slices\/([^\/]+)/;
const match = dirPath.match(regex);
return match ? match[1] : null;
};

const calulateSourceUrl = (str: string): string => {
return normalizeUrl(str)
.replace(/\/assets\//, "")
Expand Down Expand Up @@ -78,6 +84,10 @@ const hanamiEsbuild = (options: PluginOptions = { ...defaults }): Plugin => {
return result.slice(0, 8).toUpperCase();
};

const compactArray = (arr: Array<string | null>): Array<string> => {
return arr.filter((token): token is string => token !== null);
};

function extractEsbuildInputs(inputData: Record<string, any>): Record<string, boolean> {
const inputs: Record<string, boolean> = {};

Expand Down Expand Up @@ -121,26 +131,34 @@ const hanamiEsbuild = (options: PluginOptions = { ...defaults }): Plugin => {
options: PluginOptions,
): string[] => {
const dirPath = path.dirname(pattern);
const files = fs.readdirSync(dirPath);
const files = fs.readdirSync(dirPath, { recursive: true });
const assets: string[] = [];

files.forEach((file) => {
const srcPath = path.join(dirPath, file);
const srcPath = path.join(dirPath, file.toString());

// Skip if the file is already processed by esbuild
if (inputs.hasOwnProperty(srcPath)) {
return;
}

// Skip directories and any other non-files
if (!fs.statSync(srcPath).isFile()) {
return;
}

const fileHash = calculateHash(fs.readFileSync(srcPath), options.hash);
const fileExtension = path.extname(srcPath);
const baseName = path.basename(srcPath, fileExtension);
const destFileName =
[baseName, fileHash].filter((item) => item !== null).join("-") + fileExtension;
const destPath = path.join(
const sliceName = extractSliceName(dirPath);
const pathTokens = compactArray([
options.destDir,
path.relative(dirPath, srcPath).replace(file, destFileName),
);
sliceName,
path.relative(dirPath, srcPath).replace(path.basename(file.toString()), destFileName),
]);
const destPath = path.join(...pathTokens);

if (fs.lstatSync(srcPath).isDirectory()) {
assets.push(...processAssetDirectory(destPath, inputs, options));
Expand Down Expand Up @@ -187,8 +205,13 @@ const hanamiEsbuild = (options: PluginOptions = { ...defaults }): Plugin => {
assetsManifest[sourceUrl] = asset;
}

// FIXME: Need a mutex around this
// const existingManifest = fs.readJsonSync(manifest);
// const resultManifest = { ...existingManifest, ...assetsManifest };
const resultManifest = assetsManifest;

// Write assets manifest to the destination directory
await fs.writeJson(manifest, assetsManifest, { spaces: 2 });
await fs.writeJson(manifest, resultManifest, { spaces: 2 });
});
},
};
Expand Down
Loading
Loading