diff --git a/craco.config.js b/craco.config.js index 27f0bf3..b0e9bf9 100644 --- a/craco.config.js +++ b/craco.config.js @@ -1,42 +1,51 @@ +const path = require("path"); const { ModuleFederationPlugin } = require("webpack").container; const deps = require("./package.json").dependencies; -// Import the remote configurations -// Note: Since this is a craco.config.js (Node.js environment), we use require. -const remotes = require("./src/remotes/mf-remotes.config.js").default; +// The remote list is no longer needed at build time: containers are resolved and +// fetched in the browser by src/remotes/loadRemote.js, so nothing here has to +// know their URLs. module.exports = { webpack: { configure: (webpackConfig) => { webpackConfig.output.publicPath = "auto"; - // Dynamically generate the remotes object based on the environment - const isProduction = process.env.NODE_ENV === "production"; - const remoteEntries = remotes.reduce((acc, remote) => { - const url = isProduction ? remote.prodUrl : remote.devUrl; - acc[remote.name] = `${remote.name}@${url}`; - return acc; - }, {}); - webpackConfig.plugins.push( new ModuleFederationPlugin({ name: "host", - remotes: remoteEntries, // Use the dynamically generated object + // Deliberately no `remotes` option. + // + // A statically-declared remote gets an `initExternal()` call emitted + // into webpack's share-scope initialiser, which runs at app startup — + // so all four remoteEntry.js files were fetched on every route, + // including the landing page, which renders none of them. + // + // Containers are loaded on demand instead, by src/remotes/loadRemote.js. + // The `shared` scope below is still required: it is what the runtime + // hands to each container's init() so React stays a singleton. + // Only share what actually runs in the browser and is worth + // deduplicating with the remotes. Spreading every entry of + // `dependencies` here also shares build-only packages + // (react-scripts, @craco/craco, @testing-library/*), which webpack + // then has to resolve into the share scope for no benefit. + // + // These are NOT `eager: true`: src/index.js already provides the + // async boundary (`import("./bootstrap")`) that eager consumption + // exists to work around. Keeping them lazy lets webpack emit React + // and the router as their own long-lived chunks instead of inlining + // them into main.js on every build. shared: { - ...deps, react: { singleton: true, - eager: true, requiredVersion: deps.react, }, "react-dom": { singleton: true, - eager: true, requiredVersion: deps["react-dom"], }, "react-router-dom": { singleton: true, - eager: true, requiredVersion: deps["react-router-dom"], }, }, @@ -54,4 +63,30 @@ module.exports = { devServer: { historyApiFallback: true, }, + jest: { + configure: (jestConfig) => { + // CRA 5 pins jest 27, whose resolver predates `exports` maps entirely. + // react-router 7 relies on them: + // + // - react-router-dom declares `main: "./dist/main.js"`, a file it does + // not ship (only dist/index.js exists), so the `main` fallback fails + // outright — this is what made every router-importing test die with + // "Cannot find module 'react-router-dom'". + // - react-router/dom is exports-only, with no `main` to fall back to, + // and Node's own resolution picks the .mjs build that jest 27 cannot + // parse. + // + // Map both to the CJS builds. webpack resolves these correctly on its + // own, so this affects tests only. + const cjs = (p) => path.resolve(__dirname, "node_modules", p); + jestConfig.moduleNameMapper = { + ...jestConfig.moduleNameMapper, + "^react-router-dom$": cjs("react-router-dom/dist/index.js"), + "^react-router/dom$": cjs( + "react-router/dist/development/dom-export.js" + ), + }; + return jestConfig; + }, + }, }; diff --git a/netlify.toml b/netlify.toml new file mode 100644 index 0000000..4289170 --- /dev/null +++ b/netlify.toml @@ -0,0 +1,98 @@ +[build] + command = "npm run build" + publish = "build" + +# --------------------------------------------------------------------------- +# Caching +# --------------------------------------------------------------------------- +# Netlify's default for this site was `public, max-age=0, must-revalidate` on +# everything, including webpack's content-hashed assets. Those filenames change +# whenever their contents change, so revalidating them costs a round-trip per +# asset per visit and can never return anything but 304. +[[headers]] + for = "/static/*" + [headers.values] + Cache-Control = "public, max-age=31536000, immutable" + +# index.html is the only file whose URL is stable while its contents change — +# it must never be cached, or visitors keep booting a stale asset manifest. +[[headers]] + for = "/index.html" + [headers.values] + Cache-Control = "public, max-age=0, must-revalidate" + +[[headers]] + for = "/" + [headers.values] + Cache-Control = "public, max-age=0, must-revalidate" + +# Unhashed public/ assets: cache briefly, but let them be replaced same-day. +[[headers]] + for = "/*.webp" + [headers.values] + Cache-Control = "public, max-age=86400" + +[[headers]] + for = "/*.png" + [headers.values] + Cache-Control = "public, max-age=86400" + +[[headers]] + for = "/favicon.svg" + [headers.values] + Cache-Control = "public, max-age=86400" + +[[headers]] + for = "/Devi_R_Senior_Frontend_Engineer_Resume.pdf" + [headers.values] + Cache-Control = "public, max-age=86400" + +# --------------------------------------------------------------------------- +# Security headers +# --------------------------------------------------------------------------- +# This shell executes JavaScript fetched from four Render origins and frames two +# Vercel origins at runtime. A CSP is the one place that trust boundary is +# written down and enforced; without it any injected script can pull code from +# anywhere. The allowlist below is the architecture, stated explicitly. +[[headers]] + for = "/*" + [headers.values] + X-Content-Type-Options = "nosniff" + Referrer-Policy = "strict-origin-when-cross-origin" + Permissions-Policy = "camera=(), microphone=(), geolocation=(), interest-cohort=()" + Content-Security-Policy = """ + default-src 'self'; \ + script-src 'self' 'unsafe-inline' \ + https://react-post-login-dashboard.onrender.com \ + https://react-ecommerce-catalogue-page.onrender.com \ + https://react-syntax-highlighter.onrender.com \ + https://react-webgl-paint-splatter.onrender.com \ + https://cdn.jsdelivr.net \ + https://www.googletagmanager.com \ + https://www.google-analytics.com; \ + style-src 'self' 'unsafe-inline' \ + https://react-post-login-dashboard.onrender.com \ + https://react-ecommerce-catalogue-page.onrender.com \ + https://react-syntax-highlighter.onrender.com \ + https://react-webgl-paint-splatter.onrender.com; \ + img-src 'self' data: blob: https:; \ + font-src 'self' data:; \ + worker-src 'self' blob: \ + https://react-syntax-highlighter.onrender.com; \ + connect-src 'self' \ + https://react-post-login-dashboard.onrender.com \ + https://react-ecommerce-catalogue-page.onrender.com \ + https://react-syntax-highlighter.onrender.com \ + https://react-webgl-paint-splatter.onrender.com \ + https://express-mock-server-rose.vercel.app \ + https://cdn.jsdelivr.net \ + https://www.google-analytics.com \ + https://region1.google-analytics.com; \ + frame-src 'self' \ + https://nextjs-portfolio-blogs.vercel.app \ + https://nextjs-fullstack-ai-fe-system-desig.vercel.app; \ + frame-ancestors 'none'; \ + base-uri 'self'; \ + form-action 'self'; \ + object-src 'none' + """ diff --git a/package.json b/package.json index fcbb225..45fe957 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ }, "scripts": { "start": "PORT=3000 craco start", - "build": "craco build", + "build": "craco build && node scripts/generate-seo.js", "test": "craco test", "eject": "react-scripts eject" }, diff --git a/public/_redirects b/public/_redirects index f08da56..c9bbf30 100644 --- a/public/_redirects +++ b/public/_redirects @@ -1,3 +1,10 @@ +# Fallback only. +# +# `npm run build` overwrites this file in build/ via scripts/generate-seo.js, +# which emits an explicit 200 rule per known route plus a catch-all that returns +# a real 404 status for everything else. This blanket rule stays here so that a +# bare `craco build` (without the generate step) still produces a working SPA +# rather than 404ing every deep link. + # SPA fallback rule (must be last) /* /index.html 200 - diff --git a/public/index.html b/public/index.html index e7aac2e..c89cd16 100644 --- a/public/index.html +++ b/public/index.html @@ -12,7 +12,7 @@ - + - + +