Skip to content

Commit

Permalink
Merge remote-tracking branch 'upstream/main' into enhance-css-formal-…
Browse files Browse the repository at this point in the history
…syntax
  • Loading branch information
fiji-flo committed Apr 25, 2024
2 parents f03d482 + 81ec2a7 commit 2834464
Show file tree
Hide file tree
Showing 20 changed files with 253 additions and 279 deletions.
2 changes: 2 additions & 0 deletions .env.testing
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,5 @@ BUILD_ALWAYS_ALLOW_ROBOTS=true
REACT_APP_ENABLE_PLUS=true

REACT_APP_FXA_SIGNIN_URL=/users/fxa/login/authenticate/

BASE_URL="https://developer.mozilla.org"
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ yarn-error.log*
/server/*.js.map
/ssr/dist/
/ssr/*.js
!/ssr/mozilla.dnthelper.min.js
!/ssr/webpack.config.js
/ssr/*.js.map
/tool/*.js
Expand Down
2 changes: 1 addition & 1 deletion client/config/webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ function config(webpackEnv) {
// as %PUBLIC_URL% in `index.html` and `process.env.PUBLIC_URL` in JavaScript.
// Omit trailing slash as %PUBLIC_URL%/xyz looks better than %PUBLIC_URL%xyz.
// Get environment variables to inject into our app.
const env = getClientEnvironment(paths.publicUrlOrPath.slice(0, -1));
const env = getClientEnvironment(paths.publicUrlOrPath.replace(/\/$/, ""));

const shouldUseReactRefresh = env.raw.FAST_REFRESH;

Expand Down
8 changes: 4 additions & 4 deletions client/public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,13 @@
of the file that has a hash in it.
-->

<link rel="icon" href="%PUBLIC_URL%/favicon-48x48.png" />
<link rel="icon" href="/favicon-48x48.png" />

<link rel="apple-touch-icon" href="%PUBLIC_URL%/apple-touch-icon.png" />
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />

<meta name="theme-color" content="#ffffff" />

<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<link rel="manifest" href="/manifest.json" />

<link
rel="search"
Expand Down Expand Up @@ -58,7 +58,7 @@
property="og:description"
content="The MDN Web Docs site provides information about Open Web technologies including HTML, CSS, and APIs for both Web sites and progressive web apps."
/>
<meta property="og:image" content="%PUBLIC_URL%/mdn-social-share.png" />
<meta property="og:image" content="/mdn-social-share.png" />
<meta property="og:image:type" content="image/png" />
<meta property="og:image:height" content="1080" />
<meta property="og:image:width" content="1920" />
Expand Down
12 changes: 12 additions & 0 deletions client/scripts/build.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import printBuildError from "react-dev-utils/printBuildError.js";

import configFactory from "../config/webpack.config.js";
import paths from "../config/paths.js";
import { hashSomeStaticFilesForClientBuild } from "./postprocess-client-build.js";

// Makes the script crash on unhandled rejections instead of silently
// ignoring them. In the future, promise rejections that are not handled will
Expand Down Expand Up @@ -120,6 +121,17 @@ checkBrowsers(paths.appPath, isInteractive)
}
}
)
.then(async () => {
const { results } = await hashSomeStaticFilesForClientBuild(paths.appBuild);
console.log(
chalk.green(
`Hashed ${results.length} files in ${path.join(
paths.appBuild,
"index.html"
)}`
)
);
})
.catch((err) => {
if (err && err.message) {
console.log(err.message);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import path from "node:path";
import cheerio from "cheerio";
import md5File from "md5-file";

export async function runOptimizeClientBuild(buildRoot) {
export async function hashSomeStaticFilesForClientBuild(buildRoot) {
const indexHtmlFilePath = path.join(buildRoot, "index.html");
const indexHtml = fs.readFileSync(indexHtmlFilePath, "utf-8");

Expand All @@ -26,17 +26,11 @@ export async function runOptimizeClientBuild(buildRoot) {
if (element.attribs.property !== "og:image") {
return;
}
// This is a can of worms. Using from environment for now.
// We need to use an absolute URL for "og:image".
hrefPrefix = process.env.BASE_URL || "";
href = element.attribs.content;
attributeKey = "content";
// This is an unfortunate hack. The value for the
// <meta property=og:image content=...> needs to be an absolute URL.
// We tested with a relative URL and it seems it doesn't work in Twitter.
// So we hardcode the URL to be our production domain so the URL is
// always absolute.
// Yes, this makes it a bit weird to use a build of this on a dev,
// stage, preview, or a local build. Especially if the hashed URL doesn't
// always work. But it's a fair price to pay.
hrefPrefix = "https://developer.mozilla.org";
} else {
href = element.attribs.href;
if (!href) {
Expand Down Expand Up @@ -75,7 +69,7 @@ export async function runOptimizeClientBuild(buildRoot) {
const splitName = filePath.split(extName);
const hashedFilePath = `${splitName[0]}.${hash}${extName}`;
fs.copyFileSync(filePath, hashedFilePath);
const hashedHref = filePathToHref(buildRoot, hashedFilePath);
const hashedHref = filePathToHref(buildRoot, hashedFilePath, href);
results.push({
filePath,
href,
Expand All @@ -101,8 +95,19 @@ export async function runOptimizeClientBuild(buildRoot) {
}

// Turn 'C:\Path\to\client\build\favicon.ico' to '/favicon.ico'
function filePathToHref(root, filePath) {
return `${filePath.replace(root, "").replace(path.sep, "/")}`;
// or 'https://foo.bar/favicon.ico' if href is an absolute URL.
function filePathToHref(root, filePath, href) {
let dummyOrExistingUrl = new URL(href, "http://localhost.example");
dummyOrExistingUrl.pathname = "";
let url = new URL(
`${filePath.replace(root, "").replace(path.sep, "/")}`,
dummyOrExistingUrl
);
if (url.hostname === "localhost.example") {
return url.pathname;
} else {
return url.href;
}
}

// Turn '/favicon.ico' to 'C:\Path\to\client\build\favicon.ico'
Expand Down
2 changes: 1 addition & 1 deletion client/scripts/start.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ process.on("unhandledRejection", (err) => {

const appPackageJson = JSON.parse(fs.readFileSync(paths.appPackageJson));

const env = getClientEnvironment(paths.publicUrlOrPath.slice(0, -1));
const env = getClientEnvironment(paths.publicUrlOrPath.replace(/\/$/, ""));
const useYarn = fs.existsSync(paths.yarnLockFile);
const isInteractive = process.stdout.isTTY;

Expand Down
4 changes: 1 addition & 3 deletions client/src/homepage/contributor-spotlight/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,7 @@ import { Icon } from "../../ui/atoms/icon";
import Mandala from "../../ui/molecules/mandala";

import "./index.scss";
const contributorGraphic = `${
process.env.PUBLIC_URL || ""
}/assets/mdn_contributor.png`;
const contributorGraphic = "/assets/mdn_contributor.png";

export function ContributorSpotlight(props: HydrationData<any>) {
const fallbackData = props.hyData ? props : undefined;
Expand Down
2 changes: 1 addition & 1 deletion client/src/ui/atoms/avatar/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export const Avatar = ({ userData }: { userData: UserData }) => {
// If we have user data and the user is logged in, show their
// profile pic, defaulting to the dino head if the avatar
// URL doesn't work.
const avatarImage = `${process.env.PUBLIC_URL || ""}/assets/avatar.png`;
const avatarImage = "/assets/avatar.png";

return (
<div
Expand Down
14 changes: 7 additions & 7 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@
"build:curriculum": "cross-env NODE_ENV=production NODE_OPTIONS='--no-warnings=ExperimentalWarning --loader ts-node/esm' node build/build-curriculum.ts",
"build:dist": "tsc -p tsconfig.dist.json",
"build:glean": "cd client && cross-env VIRTUAL_ENV=venv glean translate src/telemetry/metrics.yaml src/telemetry/pings.yaml -f typescript -o src/telemetry/generated",
"build:prepare": "yarn build:client && yarn build:ssr && yarn tool optimize-client-build && yarn tool google-analytics-code && yarn tool popularities && yarn tool spas && yarn tool gather-git-history && yarn tool build-robots-txt",
"build:ssr": "cd ssr && webpack --mode=production",
"build:prepare": "yarn build:client && yarn build:ssr && yarn tool popularities && yarn tool spas && yarn tool gather-git-history && yarn tool build-robots-txt",
"build:ssr": "cross-env NODE_ENV=production NODE_OPTIONS='--no-warnings=ExperimentalWarning --loader ts-node/esm' node ssr/prepare.ts && cd ssr && webpack --mode=production",
"build:sw": "cd client/pwa && yarn && yarn build:prod",
"build:sw-dev": "cd client/pwa && yarn && yarn build",
"check:tsc": "find . -name 'tsconfig.json' ! -wholename '**/node_modules/**' -print0 | xargs -n1 -P 2 -0 sh -c 'cd `dirname $0` && echo \"🔄 $(pwd)\" && npx tsc --noEmit && echo \"☑️ $(pwd)\" || exit 255'",
Expand Down Expand Up @@ -73,8 +73,8 @@
"@mdn/bcd-utils-api": "^0.0.7",
"@mdn/browser-compat-data": "^5.5.23",
"@mozilla/glean": "5.0.0",
"@sentry/integrations": "^7.112.1",
"@sentry/node": "^7.112.1",
"@sentry/integrations": "^7.112.2",
"@sentry/node": "^7.112.2",
"@stripe/stripe-js": "^3.3.0",
"@use-it/interval": "^1.0.0",
"@vscode/ripgrep": "^1.15.9",
Expand Down Expand Up @@ -119,7 +119,7 @@
"mdn-data": "^2.6.1",
"open": "^10.1.0",
"open-editor": "^4.1.1",
"openai": "^4.38.3",
"openai": "^4.38.5",
"pg": "^8.11.5",
"pgvector": "^0.1.8",
"prism-svelte": "^0.5.0",
Expand All @@ -145,7 +145,7 @@
"unified": "^11.0.4",
"unist-builder": "^4.0.0",
"unist-util-visit": "^5.0.0",
"web-features": "^0.6.4",
"web-features": "^0.7.0",
"web-specs": "^3.8.0"
},
"devDependencies": {
Expand Down Expand Up @@ -190,7 +190,7 @@
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-jest": "^28.2.0",
"eslint-plugin-jsx-a11y": "^6.8.0",
"eslint-plugin-n": "^17.2.1",
"eslint-plugin-n": "^17.3.1",
"eslint-plugin-react": "^7.34.1",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-unicorn": "^52.0.0",
Expand Down
1 change: 1 addition & 0 deletions ssr/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
include.ts
47 changes: 47 additions & 0 deletions ssr/ga.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import fs from "node:fs";
import {
BUILD_OUT_ROOT,
GOOGLE_ANALYTICS_MEASUREMENT_ID,
} from "../libs/env/index.js";
import path from "node:path";

export async function generateGA() {
const outFile = path.join(BUILD_OUT_ROOT, "static", "js", "gtag.js");
const measurementIds =
GOOGLE_ANALYTICS_MEASUREMENT_ID.split(",").filter(Boolean);
if (measurementIds.length) {
const dntHelperCode = fs
.readFileSync(
new URL("mozilla.dnthelper.min.js", import.meta.url),
"utf-8"
)
.trim();

const firstMeasurementId = measurementIds[0];
const gaScriptURL = `https://www.googletagmanager.com/gtag/js?id=${encodeURIComponent(firstMeasurementId)}`;

const code = `
// Mozilla DNT Helper
${dntHelperCode}
// Load GA unless DNT is enabled.
if (Mozilla && !Mozilla.dntEnabled()) {
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
${measurementIds
.map((id) => `gtag('config', '${id}', { 'anonymize_ip': true });`)
.join("\n ")}
var gaScript = document.createElement('script');
gaScript.async = true;
gaScript.src = '${gaScriptURL}';
document.head.appendChild(gaScript);
}`.trim();
fs.writeFileSync(outFile, `${code}\n`, "utf-8");
console.log(
`Generated ${outFile} for SSR rendering using ${GOOGLE_ANALYTICS_MEASUREMENT_ID}.`
);
} else {
console.log("No Google Analytics code file generated");
}
}
4 changes: 4 additions & 0 deletions ssr/include.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export const WEBFONT_TAGS: string;
export const GTAG_PATH: null | string;
export const BASE_URL: string;
export const ALWAYS_ALLOW_ROBOTS: boolean;
10 changes: 0 additions & 10 deletions ssr/index.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,9 @@
import path from "node:path";
import { fileURLToPath } from "node:url";

import * as dotenv from "dotenv";
import React from "react";
import { StaticRouter } from "react-router-dom/server";

import { App } from "../client/src/app";
import render from "./render";

const dirname = fileURLToPath(new URL(".", import.meta.url));

dotenv.config({
path: path.join(dirname, "..", process.env.ENV_FILE || ".env"),
});

export function renderHTML(url, context) {
return render(
React.createElement(
Expand Down
File renamed without changes.
73 changes: 73 additions & 0 deletions ssr/prepare.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import {
ALWAYS_ALLOW_ROBOTS,
BUILD_OUT_ROOT,
BASE_URL,
} from "../libs/env/index.js";
import { generateGA } from "./ga.js";

const dirname = path.dirname(fileURLToPath(new URL(".", import.meta.url)));
const clientBuildRoot = path.resolve(dirname, "client/build");

function extractWebFontURLs() {
const urls: string[] = [];
const manifest = JSON.parse(
fs.readFileSync(path.join(clientBuildRoot, "asset-manifest.json"), "utf-8")
);
for (const entrypoint of manifest.entrypoints) {
if (!entrypoint.endsWith(".css")) continue;
const css = fs.readFileSync(
path.join(clientBuildRoot, entrypoint),
"utf-8"
);
const generator = extractCSSURLs(css, (url) => url.endsWith(".woff2"));
urls.push(...generator);
}
return [...new Set(urls)];
}

function* extractCSSURLs(css, filterFunction) {
for (const match of css.matchAll(/url\((.*?)\)/g)) {
const url = match[1];
if (filterFunction(url)) {
yield url;
}
}
}

function webfontTags(webfontURLs): string {
return webfontURLs
.map(
(url) =>
`<link rel="preload" as="font" type="font/woff2" href="${url}" crossorigin>`
)
.join("");
}

function gtagScriptPath(relPath = "/static/js/gtag.js") {
const filePath = relPath.split("/").slice(1).join(path.sep);
if (fs.existsSync(path.join(BUILD_OUT_ROOT, filePath))) {
return relPath;
}
return null;
}

function prepare() {
const webfontURLs = extractWebFontURLs();
const tags = webfontTags(webfontURLs);
const gtagPath = gtagScriptPath();

fs.writeFileSync(
path.join(dirname, "ssr", "include.ts"),
`
export const WEBFONT_TAGS = ${JSON.stringify(tags)};
export const GTAG_PATH = ${JSON.stringify(gtagPath)};
export const BASE_URL = ${JSON.stringify(BASE_URL)};
export const ALWAYS_ALLOW_ROBOTS = ${JSON.stringify(ALWAYS_ALLOW_ROBOTS)};
`
);
}

generateGA().then(() => prepare());
Loading

0 comments on commit 2834464

Please sign in to comment.