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.
|
- Section CSS and JS are emitted as standalone hashed assets.
|
||||||
- Component CSS is expected to be imported by global CSS or section CSS.
|
- 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`.
|
- 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.
|
- `*.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.
|
- 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.
|
- 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'];
|
const styleExtensions = ['.scss', '.css'];
|
||||||
|
|
||||||
|
export const staticAssetExtensions = [
|
||||||
|
'.gif',
|
||||||
|
'.jpeg',
|
||||||
|
'.jpg',
|
||||||
|
'.mp4',
|
||||||
|
'.png',
|
||||||
|
'.svg',
|
||||||
|
'.webp',
|
||||||
|
'.woff',
|
||||||
|
'.woff2'
|
||||||
|
];
|
||||||
|
|
||||||
export const shouldSkipDirectory = (name) =>
|
export const shouldSkipDirectory = (name) =>
|
||||||
['.git', 'dist', 'docs', 'node_modules', 'storybook-static'].includes(name);
|
['.git', 'dist', 'docs', 'node_modules', 'storybook-static'].includes(name);
|
||||||
|
|
||||||
@@ -34,18 +46,9 @@ export const shouldCopyFile = (name) => {
|
|||||||
'.css',
|
'.css',
|
||||||
'.scss',
|
'.scss',
|
||||||
'.gz',
|
'.gz',
|
||||||
'.gif',
|
|
||||||
'.jpeg',
|
|
||||||
'.jpg',
|
|
||||||
'.js',
|
'.js',
|
||||||
'.mp4',
|
|
||||||
'.png',
|
|
||||||
'.svg',
|
|
||||||
'.webp',
|
|
||||||
'.woff',
|
|
||||||
'.woff2',
|
|
||||||
'.zip'
|
'.zip'
|
||||||
].includes(extname(name));
|
].concat(staticAssetExtensions).includes(extname(name));
|
||||||
};
|
};
|
||||||
|
|
||||||
export const walkFiles = (directory, callback) => {
|
export const walkFiles = (directory, callback) => {
|
||||||
|
|||||||
+109
-2
@@ -1,7 +1,112 @@
|
|||||||
import { defineConfig } from 'vite';
|
import { defineConfig } from 'vite';
|
||||||
|
import { createHash } from 'node:crypto';
|
||||||
import { cpSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
import { cpSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
||||||
import { basename, dirname, extname, join, relative, resolve } from 'node:path';
|
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 normalizeScssManifestEntries = (outDir) => {
|
||||||
const manifestPath = join(outDir, 'manifest.json');
|
const manifestPath = join(outDir, 'manifest.json');
|
||||||
@@ -69,7 +174,7 @@ const copyServerFilesPlugin = ({ sourceDir, outDir }) => ({
|
|||||||
name: 'copy-bridgeable-server-files',
|
name: 'copy-bridgeable-server-files',
|
||||||
buildStart() {
|
buildStart() {
|
||||||
walkFiles(sourceDir, (absolutePath) => {
|
walkFiles(sourceDir, (absolutePath) => {
|
||||||
if (shouldCopyFile(basename(absolutePath))) {
|
if (shouldCopyFile(basename(absolutePath)) || isStaticAssetFile(absolutePath)) {
|
||||||
this.addWatchFile(absolutePath);
|
this.addWatchFile(absolutePath);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -89,6 +194,8 @@ const copyServerFilesPlugin = ({ sourceDir, outDir }) => ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
normalizeScssManifestEntries(outDir);
|
normalizeScssManifestEntries(outDir);
|
||||||
|
emitStaticAssetsWithManifest({ sourceDir, outDir });
|
||||||
|
rewriteCssManifestAssetUrls(outDir);
|
||||||
normalizeCssAssetUrls(outDir);
|
normalizeCssAssetUrls(outDir);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user