diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c18efb0..1f25628 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 diff --git a/Makefile b/Makefile index dddaa2d..dffdd09 100644 --- a/Makefile +++ b/Makefile @@ -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 diff --git a/dist/esbuild-plugin.js b/dist/esbuild-plugin.js index 44209e3..15f0198 100644 --- a/dist/esbuild-plugin.js +++ b/dist/esbuild-plugin.js @@ -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\//, "") @@ -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) { @@ -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)); } @@ -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 }); }); }, }; diff --git a/dist/index.d.ts b/dist/index.d.ts index 0d2540c..8834d74 100644 --- a/dist/index.d.ts +++ b/dist/index.d.ts @@ -8,5 +8,5 @@ interface RunOptions { esbuildOptionsFn?: EsbuildOptionsFn; } type EsbuildOptionsFn = (args: Args, esbuildOptions: EsbuildOptions) => EsbuildOptions; -export declare const run: (options?: RunOptions) => Promise; +export declare const run: (options?: RunOptions) => Promise; export {}; diff --git a/dist/index.js b/dist/index.js index 947313d..34a404d 100644 --- a/dist/index.js +++ b/dist/index.js @@ -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); @@ -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; +}; diff --git a/package-lock.json b/package-lock.json index bceab78..e2a55cc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,12 +11,14 @@ "dependencies": { "esbuild": "^0.19.0", "fs-extra": "^11.1.0", - "glob": "^10.3.3" + "glob": "^10.3.3", + "lodash.clonedeep": "^4.5.0" }, "devDependencies": { "@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", @@ -1630,6 +1632,21 @@ "@types/node": "*" } }, + "node_modules/@types/lodash": { + "version": "4.14.202", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.202.tgz", + "integrity": "sha512-OvlIYQK9tNneDlS0VN54LLd5uiPCBOp7gS5Z0f1mjoJYBrtStzgmJBxONW3U6OZqdtNzZPmn9BS/7WI7BFFcFQ==", + "dev": true + }, + "node_modules/@types/lodash.clonedeep": { + "version": "4.5.9", + "resolved": "https://registry.npmjs.org/@types/lodash.clonedeep/-/lodash.clonedeep-4.5.9.tgz", + "integrity": "sha512-19429mWC+FyaAhOLzsS8kZUsI+/GmBAQ0HFiCPsKGU+7pBXOQWhyrY6xNNDwUSX8SMZMJvuFVMF9O5dQOlQK9Q==", + "dev": true, + "dependencies": { + "@types/lodash": "*" + } + }, "node_modules/@types/node": { "version": "20.9.0", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.9.0.tgz", @@ -3639,6 +3656,11 @@ "node": ">=8" } }, + "node_modules/lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==" + }, "node_modules/lodash.memoize": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", diff --git a/package.json b/package.json index 00f661e..e46e550 100644 --- a/package.json +++ b/package.json @@ -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", @@ -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" } } diff --git a/src/esbuild-plugin.ts b/src/esbuild-plugin.ts index a59a427..cdc738d 100644 --- a/src/esbuild-plugin.ts +++ b/src/esbuild-plugin.ts @@ -46,6 +46,12 @@ const hanamiEsbuild = (options: PluginOptions = { ...defaults }): Plugin => { const outputs = result.metafile?.outputs; const assetsManifest: Record = {}; + 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\//, "") @@ -78,6 +84,10 @@ const hanamiEsbuild = (options: PluginOptions = { ...defaults }): Plugin => { return result.slice(0, 8).toUpperCase(); }; + const compactArray = (arr: Array): Array => { + return arr.filter((token): token is string => token !== null); + }; + function extractEsbuildInputs(inputData: Record): Record { const inputs: Record = {}; @@ -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)); @@ -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 }); }); }, }; diff --git a/src/index.ts b/src/index.ts index 202fb87..885817a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,9 +2,10 @@ import fs from "fs-extra"; import path from "path"; -import esbuild, { BuildContext } from "esbuild"; +import esbuild, { BuildContext, BuildResult } from "esbuild"; import { Args, parseArgs } from "./args.js"; import { EsbuildOptions, buildOptions, watchOptions } from "./esbuild.js"; +import cloneDeep from "lodash.clonedeep"; interface RunOptions { root?: string; @@ -14,7 +15,7 @@ interface RunOptions { type EsbuildOptionsFn = (args: Args, esbuildOptions: EsbuildOptions) => EsbuildOptions; -export const run = async function (options?: RunOptions): Promise { +export const run = async function (options?: RunOptions): Promise { const { root = process.cwd(), argv = process.argv, esbuildOptionsFn = null } = options || {}; const args = parseArgs(argv); @@ -25,23 +26,33 @@ export const run = async function (options?: RunOptions): Promise { - 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 contexts: BuildContext[] = []; + const splitOptions = splitEsbuildOptions(esbuildOptions); - const ctx = await esbuild.context(esbuildOptions); - await ctx.watch().catch(errorHandler); + for (const options of splitOptions) { + const ctx = await esbuild.context(options); + contexts.push(ctx); - return ctx; + await ctx.watch().catch(errorHandler); + } + + return contexts; } else { - await esbuild.build(esbuildOptions).catch(errorHandler); + await esbuildMultipleBuilds(esbuildOptions); } }; +const errorHandler = (err: any): void => { + console.log(err); + process.exit(1); +}; + const touchManifest = (root: string): void => { const manifestPath = path.join(root, "public", "assets.json"); const manifestDir = path.dirname(manifestPath); @@ -50,3 +61,110 @@ const touchManifest = (root: string): void => { fs.writeFileSync(manifestPath, JSON.stringify({}, null, 2)); }; + +const esbuildMultipleBuilds = async function ( + esbuildOptions: EsbuildOptions, +): Promise | void> { + const builds = splitEsbuildOptions(esbuildOptions); + + for (const build of builds) { + await esbuild.build(build).catch(errorHandler); + } +}; + +const splitEsbuildOptions = (esbuildOptions: EsbuildOptions): 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: EsbuildOptions): string[] => { + let entryPoints: string[] = []; + + 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: EsbuildOptions): string[] => { + let external: string[] = []; + + 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: string[]): string[] => { + const result = entryPoints.map((entryPoint) => { + return extractSliceOrAppName(entryPoint); + }); + + return [...new Set(result)]; +}; + +const extractSliceOrAppName = (entryPoint: string): string => { + 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: string): string | null => { + const regex = /^slices\/([^\/]+)/; + const match = name.match(regex); + return match ? match[1] : null; +}; diff --git a/test/fixtures/todo/app/assets/images/logo.png b/test/fixtures/todo/app/assets/images/logo.png new file mode 100644 index 0000000..6a64ac6 Binary files /dev/null and b/test/fixtures/todo/app/assets/images/logo.png differ diff --git a/test/fixtures/todo/slices/admin/assets/css/app.css b/test/fixtures/todo/slices/admin/assets/css/app.css new file mode 100644 index 0000000..6c666f0 --- /dev/null +++ b/test/fixtures/todo/slices/admin/assets/css/app.css @@ -0,0 +1,3 @@ +body { + background-image: url("../images/background.jpg"); +} diff --git a/test/fixtures/todo/slices/admin/assets/images/background.jpg b/test/fixtures/todo/slices/admin/assets/images/background.jpg new file mode 100644 index 0000000..b41faf8 Binary files /dev/null and b/test/fixtures/todo/slices/admin/assets/images/background.jpg differ diff --git a/test/fixtures/todo/slices/admin/assets/images/nested/image.jpg b/test/fixtures/todo/slices/admin/assets/images/nested/image.jpg new file mode 100644 index 0000000..4359309 Binary files /dev/null and b/test/fixtures/todo/slices/admin/assets/images/nested/image.jpg differ diff --git a/test/fixtures/todo/slices/admin/assets/js/app.js b/test/fixtures/todo/slices/admin/assets/js/app.js index 358a8b5..7cbc5e1 100644 --- a/test/fixtures/todo/slices/admin/assets/js/app.js +++ b/test/fixtures/todo/slices/admin/assets/js/app.js @@ -1 +1,3 @@ +import "../css/app.css"; // Import main *slice* stylesheet + console.log("admin"); diff --git a/test/hanami-assets.test.ts b/test/hanami-assets.test.ts index ce53203..d799999 100644 --- a/test/hanami-assets.test.ts +++ b/test/hanami-assets.test.ts @@ -71,13 +71,13 @@ describe("hanami-assets", () => { // Check if the manifest contains the correct file paths expect(manifest).toEqual({ "admin/app.js": { - url: "/assets/admin/app-NLRESL5A.js", + url: "/assets/admin/app-NKKNBRXX.js", }, "app.js": { url: "/assets/app-JLSTK5SN.js", }, "metrics/app.js": { - url: "/assets/metrics/app-27Z7ZALS.js", + url: "/assets/metrics/app-Q7BVC6SS.js", }, }); }); @@ -126,6 +126,14 @@ describe("hanami-assets", () => { url: "/assets/background-UU2XY655.jpg", sri: ["sha384-M7QyKTUfzyVWNC4FoMYq0ypu7LDifAYWEtXRT5d6M3Prpau9t5wavW1216HhvCJc"], }, + "font.otf": { + url: "/assets/font-E1A70B27.otf", + sri: ["sha384-Lpm/oUsCQkOg41WyENyyB1zjaX/FB522VWlU44JKakwzwBxvu11le0ILkiPsR73K"], + }, + "logo.png": { + url: "/assets/logo-C1EF77E4.png", + sri: ["sha384-7q5x+ZjZrCoWwyV0BTyc8HUPf1xr+n9l77gwxmwywPWSe0PtopZj1T8NTUPFo0FI"], + }, "app.css": { url: "/assets/app-4HPGUYGF.css", sri: ["sha384-KsEObWWMvw+PouA5LgKpXohYpsOO4h9dL9pv7LwznkIg83/n1gkJo+S/oU/9Qb8Q"], @@ -135,18 +143,32 @@ describe("hanami-assets", () => { sri: ["sha384-z0TVeAyYeMsyiCnAqNu/OYs+IxvLwkTocy2uchAChAHmXaV68xYonUUzn1wJ4myH"], }, "admin/app.js": { - url: "/assets/admin/app-H646WNEB.js", - sri: ["sha384-noZH9am6sCla+CnG7l+IGxBlTqo68Wz891fhqfIF1U2kgafUrRzZewAt0yA6jl15"], + url: "/assets/admin/app-RFBN5M7F.js", + sri: ["sha384-dW855HdCFw/XLP/popj7E2Xy76dQvWs/JGbMIs1hPRl1IzbTYUdj5CWwiMViVMIo"], }, - "font.otf": { - url: "/assets/font-E1A70B27.otf", - sri: ["sha384-Lpm/oUsCQkOg41WyENyyB1zjaX/FB522VWlU44JKakwzwBxvu11le0ILkiPsR73K"], + "admin/app.css": { + url: "/assets/admin/app-L3S2NK76.css", + sri: ["sha384-h5eqXASqe7pRAil3oO5d73/rbGxsPEq145MHtA/+D/zsMbhXFfmRKfHo++nzYQs6"], }, - "logo.png": { - url: "/assets/logo-C1EF77E4.png", + "admin/logo.png": { + url: "/assets/admin/logo-C1EF77E4.png", sri: ["sha384-7q5x+ZjZrCoWwyV0BTyc8HUPf1xr+n9l77gwxmwywPWSe0PtopZj1T8NTUPFo0FI"], }, + "admin/nested/image.jpg": { + url: "/assets/admin/nested/image-FA518447.jpg", + sri: ["sha384-RbjZmXXoS+NuWIfaT/8A1JWcmmNaSVcy78V5zMKFKIXcEDKLKinkGNG/N3EqyOKa"], + }, + "admin/background.jpg": { + url: "/assets/admin/background-SB6NFNOQ.jpg", + sri: ["sha384-KzOZwTAlOBMvr/np+ZObBUuYlE9fiVV7FN9HJGhwXKTKY3/joV+LAI31fqNvx9C6"], + }, }); + + const adminCss = await fs.readFile( + path.join(dest, "public/assets/admin/app-L3S2NK76.css"), + "utf-8", + ); + expect(adminCss).toMatch("./background-SB6NFNOQ.jpg"); }); test( @@ -165,8 +187,14 @@ describe("hanami-assets", () => { const appAsset = path.join(dest, "public", "assets", "app.js"); const imageAsset = path.join(dest, "public", "assets", "background.jpg"); + const entryPoint2 = path.join(dest, "slices/admin/assets/js/app.js"); + const entryPoint2Asset = path.join(dest, "public", "assets", "admin", "app.js"); + const entryPoint3 = path.join(dest, "slices/metrics/assets/js/app.ts"); + await fs.writeFile(entryPoint2, "console.log('Hello, Admin!');"); + await fs.writeFile(entryPoint3, "console.log('Hello, Metrics!');"); + // Watch for asset changes - let ctx = await assets.run({ root: dest, argv: ["--watch"] }); + let ctxs = await assets.run({ root: dest, argv: ["--watch"] }); await fs.writeFile(entryPoint, "console.log('Hello, Watch!');"); @@ -190,7 +218,7 @@ describe("hanami-assets", () => { }); }; - const found = await appAssetExists(); + let found = await appAssetExists(); expect(found).toBe(true); expect(fs.existsSync(imageAsset)).toBe(true); @@ -201,18 +229,45 @@ describe("hanami-assets", () => { // Check if the asset has the expected contents expect(assetContent).toMatch('console.log("Hello, Watch!");'); - const manifestExists = await fs.pathExists(path.join(dest, "public/assets.json")); - expect(manifestExists).toBe(true); - + // const manifestExists = await fs.pathExists(path.join(dest, "public/assets.json")); + // expect(manifestExists).toBe(true); // Read and parse the manifest file - const manifestContent = await fs.readFile(path.join(dest, "public/assets.json"), "utf-8"); - const manifest = JSON.parse(manifestContent); + // const manifestContent = await fs.readFile(path.join(dest, "public/assets.json"), "utf-8"); + // const manifest = JSON.parse(manifestContent); + // expect(manifest["background.jpg"]).toEqual({ + // url: "/assets/background.jpg", + // }); - expect(manifest["background.jpg"]).toEqual({ - url: "/assets/background.jpg", - }); + const entryPoint2AssetExists = (timeout = watchTimeout): Promise => { + return new Promise((resolve, reject) => { + let elapsedTime = 0; + const intervalTime = 100; + + const interval = setInterval(() => { + if (fs.existsSync(entryPoint2Asset)) { + clearInterval(interval); + resolve(true); + } + + elapsedTime += intervalTime; + if (elapsedTime >= timeout) { + clearInterval(interval); + reject(false); + } + }, intervalTime); + }); + }; + + await fs.writeFile(entryPoint2, "console.log('Hello, Admin, from Watch!');"); + found = await entryPoint2AssetExists(); + expect(found).toBe(true); + expect(fs.existsSync(entryPoint2Asset)).toBe(true); + const entryPoint2Content = await fs.readFile(entryPoint2Asset, "utf-8"); + expect(entryPoint2Content).toMatch('console.log("Hello, Admin, from Watch!");'); - await ctx!.dispose(); + for (const ctx of ctxs!) { + await ctx.dispose(); + } }, watchTimeout + 1000, );