Emit hashed static assets in Bridgeable builds
This commit is contained in:
@@ -53,6 +53,7 @@ The CLI preserves the current Bridgeable Components production contract:
|
||||
- Section CSS and JS are emitted as standalone hashed assets.
|
||||
- Component CSS is expected to be imported by global CSS or section CSS.
|
||||
- Component JS is emitted only when it self-registers with `window.initBlock`.
|
||||
- Static image, font, and video assets are emitted as hashed files and added to `manifest.json`.
|
||||
- `*.twig` and `*.component.json` files are copied into the output.
|
||||
- Storybook-only files, fixture data, docs, build config, `node_modules`, `.git`, and previous build output are excluded.
|
||||
- Vite emits `manifest.json`, which the WordPress Bridgeable Components plugin uses to resolve hashed asset filenames.
|
||||
|
||||
+13
-10
@@ -5,6 +5,18 @@ export const normalizePath = (path) => path.replaceAll('\\', '/');
|
||||
|
||||
const styleExtensions = ['.scss', '.css'];
|
||||
|
||||
export const staticAssetExtensions = [
|
||||
'.gif',
|
||||
'.jpeg',
|
||||
'.jpg',
|
||||
'.mp4',
|
||||
'.png',
|
||||
'.svg',
|
||||
'.webp',
|
||||
'.woff',
|
||||
'.woff2'
|
||||
];
|
||||
|
||||
export const shouldSkipDirectory = (name) =>
|
||||
['.git', 'dist', 'docs', 'node_modules', 'storybook-static'].includes(name);
|
||||
|
||||
@@ -34,18 +46,9 @@ export const shouldCopyFile = (name) => {
|
||||
'.css',
|
||||
'.scss',
|
||||
'.gz',
|
||||
'.gif',
|
||||
'.jpeg',
|
||||
'.jpg',
|
||||
'.js',
|
||||
'.mp4',
|
||||
'.png',
|
||||
'.svg',
|
||||
'.webp',
|
||||
'.woff',
|
||||
'.woff2',
|
||||
'.zip'
|
||||
].includes(extname(name));
|
||||
].concat(staticAssetExtensions).includes(extname(name));
|
||||
};
|
||||
|
||||
export const walkFiles = (directory, callback) => {
|
||||
|
||||
+109
-2
@@ -1,7 +1,112 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import { createHash } from 'node:crypto';
|
||||
import { cpSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
||||
import { basename, dirname, extname, join, relative, resolve } from 'node:path';
|
||||
import { collectEntries, normalizePath, shouldCopyFile, walkFiles } from './file-rules.js';
|
||||
import { collectEntries, normalizePath, shouldCopyFile, staticAssetExtensions, walkFiles } from './file-rules.js';
|
||||
|
||||
const staticAssetExtensionSet = new Set(staticAssetExtensions);
|
||||
|
||||
const isStaticAssetFile = (absolutePath) => staticAssetExtensionSet.has(extname(absolutePath).toLowerCase());
|
||||
|
||||
const hashedAssetPath = (relativePath, contents) => {
|
||||
const extension = extname(relativePath);
|
||||
const stem = relativePath.slice(0, -extension.length);
|
||||
const hash = createHash('sha256').update(contents).digest('base64url').slice(0, 8);
|
||||
|
||||
return `${stem}.${hash}${extension}`;
|
||||
};
|
||||
|
||||
const emitStaticAssetsWithManifest = ({ sourceDir, outDir }) => {
|
||||
const manifestPath = join(outDir, 'manifest.json');
|
||||
const manifest = existsSync(manifestPath) ? JSON.parse(readFileSync(manifestPath, 'utf8')) : {};
|
||||
let changed = false;
|
||||
|
||||
walkFiles(sourceDir, (absolutePath) => {
|
||||
if (!isStaticAssetFile(absolutePath)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const relativePath = normalizePath(relative(sourceDir, absolutePath));
|
||||
if (manifest[relativePath]?.file) {
|
||||
return;
|
||||
}
|
||||
|
||||
const contents = readFileSync(absolutePath);
|
||||
const file = hashedAssetPath(relativePath, contents);
|
||||
const destination = join(outDir, file);
|
||||
|
||||
mkdirSync(dirname(destination), { recursive: true });
|
||||
writeFileSync(destination, contents);
|
||||
|
||||
manifest[relativePath] = {
|
||||
file,
|
||||
src: relativePath
|
||||
};
|
||||
changed = true;
|
||||
});
|
||||
|
||||
if (changed) {
|
||||
writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
|
||||
}
|
||||
};
|
||||
|
||||
const manifestRelativeAssetFromUrl = (assetUrl, manifest) => {
|
||||
const cleanUrl = assetUrl.split('#')[0].split('?')[0].replaceAll('\\', '/');
|
||||
const path = cleanUrl.startsWith('/') ? cleanUrl.slice(1) : cleanUrl;
|
||||
|
||||
if (manifest[path]?.file) {
|
||||
return path;
|
||||
}
|
||||
|
||||
const bridgeableMatch = path.match(/(?:^|\/)bridgeable-components\/(.+)$/);
|
||||
if (bridgeableMatch?.[1] && manifest[bridgeableMatch[1]]?.file) {
|
||||
return bridgeableMatch[1];
|
||||
}
|
||||
|
||||
for (const prefix of ['assets/', 'components/', 'sections/', 'styles/', 'tokens/']) {
|
||||
if (path.startsWith(prefix) && manifest[path]?.file) {
|
||||
return path;
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
};
|
||||
|
||||
const rewriteCssManifestAssetUrls = (outDir) => {
|
||||
const manifestPath = join(outDir, 'manifest.json');
|
||||
if (!existsSync(manifestPath)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
|
||||
|
||||
walkFiles(outDir, (absolutePath) => {
|
||||
if (!absolutePath.endsWith('.css')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const cssDir = dirname(absolutePath);
|
||||
const contents = readFileSync(absolutePath, 'utf8');
|
||||
const rewritten = contents.replace(
|
||||
/url\((['"]?)(?!data:|https?:|\/\/)([^'")]+)\1\)/g,
|
||||
(match, quote, assetUrl) => {
|
||||
const relativeAsset = manifestRelativeAssetFromUrl(assetUrl, manifest);
|
||||
const builtFile = relativeAsset ? manifest[relativeAsset]?.file : '';
|
||||
if (!builtFile) {
|
||||
return match;
|
||||
}
|
||||
|
||||
const relativeUrl = normalizePath(relative(cssDir, join(outDir, builtFile)));
|
||||
|
||||
return `url(${quote}${relativeUrl}${quote})`;
|
||||
}
|
||||
);
|
||||
|
||||
if (rewritten !== contents) {
|
||||
writeFileSync(absolutePath, rewritten);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const normalizeScssManifestEntries = (outDir) => {
|
||||
const manifestPath = join(outDir, 'manifest.json');
|
||||
@@ -69,7 +174,7 @@ const copyServerFilesPlugin = ({ sourceDir, outDir }) => ({
|
||||
name: 'copy-bridgeable-server-files',
|
||||
buildStart() {
|
||||
walkFiles(sourceDir, (absolutePath) => {
|
||||
if (shouldCopyFile(basename(absolutePath))) {
|
||||
if (shouldCopyFile(basename(absolutePath)) || isStaticAssetFile(absolutePath)) {
|
||||
this.addWatchFile(absolutePath);
|
||||
}
|
||||
});
|
||||
@@ -89,6 +194,8 @@ const copyServerFilesPlugin = ({ sourceDir, outDir }) => ({
|
||||
});
|
||||
|
||||
normalizeScssManifestEntries(outDir);
|
||||
emitStaticAssetsWithManifest({ sourceDir, outDir });
|
||||
rewriteCssManifestAssetUrls(outDir);
|
||||
normalizeCssAssetUrls(outDir);
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user