diff --git a/package.json b/package.json index 31e8272..1374213 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,8 @@ "scripts": { "dev": "vite", "build": "vite build", - "preview": "vite preview" + "preview": "vite preview", + "test:netlify-build": "node scripts/verify-netlify-build.mjs" }, "devDependencies": { "vite": "^7.1.0" diff --git a/scripts/verify-netlify-build.mjs b/scripts/verify-netlify-build.mjs new file mode 100644 index 0000000..18535d5 --- /dev/null +++ b/scripts/verify-netlify-build.mjs @@ -0,0 +1,48 @@ +import { existsSync, readdirSync, statSync } from 'node:fs'; +import path from 'node:path'; + +const root = process.cwd(); +const dist = path.join(root, 'dist'); +const excludedDirs = new Set(['.git', 'dist', 'node_modules']); + +function collectFiles(dir, predicate = () => true) { + const entries = readdirSync(dir, { withFileTypes: true }); + const files = []; + + for (const entry of entries) { + const fullPath = path.join(dir, entry.name); + + if (entry.isDirectory()) { + if (!excludedDirs.has(entry.name)) { + files.push(...collectFiles(fullPath, predicate)); + } + continue; + } + + if (entry.isFile() && predicate(fullPath)) { + files.push(fullPath); + } + } + + return files; +} + +if (!existsSync(dist) || !statSync(dist).isDirectory()) { + throw new Error('Missing dist directory. Run `npm run build` first.'); +} + +const missingHtmlFiles = collectFiles(root, (file) => file.endsWith('.html')) + .map((file) => path.relative(root, file)) + .filter((relativePath) => !existsSync(path.join(dist, relativePath))); + +const missingAssetFiles = collectFiles(path.join(root, 'assets')) + .map((file) => path.relative(root, file)) + .filter((relativePath) => !existsSync(path.join(dist, relativePath))); + +const missingFiles = [...missingHtmlFiles, ...missingAssetFiles]; + +if (missingFiles.length > 0) { + throw new Error(`Missing built files:\n${missingFiles.join('\n')}`); +} + +console.log('All source HTML and static asset files are present in dist.'); diff --git a/vite.config.js b/vite.config.js new file mode 100644 index 0000000..891b74f --- /dev/null +++ b/vite.config.js @@ -0,0 +1,57 @@ +import { cpSync, existsSync, readdirSync } from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { defineConfig } from 'vite'; + +const root = path.dirname(fileURLToPath(import.meta.url)); +const excludedDirs = new Set(['.git', 'dist', 'node_modules']); + +function collectHtmlInputs(dir) { + const inputs = {}; + const entries = readdirSync(dir, { withFileTypes: true }); + + for (const entry of entries) { + const fullPath = path.join(dir, entry.name); + + if (entry.isDirectory()) { + if (!excludedDirs.has(entry.name)) { + Object.assign(inputs, collectHtmlInputs(fullPath)); + } + continue; + } + + if (entry.isFile() && entry.name.endsWith('.html')) { + const relativePath = path.relative(root, fullPath); + const inputName = relativePath + .replace(/\.html$/, '') + .replace(/[^a-zA-Z0-9_-]+/g, '_'); + + inputs[inputName] = fullPath; + } + } + + return inputs; +} + +function copyStaticAssets() { + return { + name: 'copy-static-assets', + closeBundle() { + const source = path.join(root, 'assets'); + const destination = path.join(root, 'dist', 'assets'); + + if (existsSync(source)) { + cpSync(source, destination, { recursive: true }); + } + }, + }; +} + +export default defineConfig({ + plugins: [copyStaticAssets()], + build: { + rollupOptions: { + input: collectHtmlInputs(root), + }, + }, +});