Typescript src/assets (#53110)
This commit is contained in:
27
package-lock.json
generated
27
package-lock.json
generated
@@ -38,7 +38,7 @@
|
|||||||
"express": "4.21.1",
|
"express": "4.21.1",
|
||||||
"express-rate-limit": "7.4.0",
|
"express-rate-limit": "7.4.0",
|
||||||
"fastest-levenshtein": "1.0.16",
|
"fastest-levenshtein": "1.0.16",
|
||||||
"file-type": "19.4.1",
|
"file-type": "19.6.0",
|
||||||
"flat": "^6.0.1",
|
"flat": "^6.0.1",
|
||||||
"github-slugger": "^2.0.0",
|
"github-slugger": "^2.0.0",
|
||||||
"glob": "11.0.0",
|
"glob": "11.0.0",
|
||||||
@@ -6880,12 +6880,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/file-type": {
|
"node_modules/file-type": {
|
||||||
"version": "19.4.1",
|
"version": "19.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/file-type/-/file-type-19.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/file-type/-/file-type-19.6.0.tgz",
|
||||||
"integrity": "sha512-RuWzwF2L9tCHS76KR/Mdh+DwJZcFCzrhrPXpOw6MlEfl/o31fjpTikzcKlYuyeV7e7ftdCGVJTNOCzkYD/aLbw==",
|
"integrity": "sha512-VZR5I7k5wkD0HgFnMsq5hOsSc710MJMu5Nc5QYsbe38NN5iPV/XTObYLc/cpttRTf6lX538+5uO1ZQRhYibiZQ==",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"get-stream": "^9.0.1",
|
"get-stream": "^9.0.1",
|
||||||
"strtok3": "^8.1.0",
|
"strtok3": "^9.0.1",
|
||||||
"token-types": "^6.0.0",
|
"token-types": "^6.0.0",
|
||||||
"uint8array-extras": "^1.3.0"
|
"uint8array-extras": "^1.3.0"
|
||||||
},
|
},
|
||||||
@@ -11546,9 +11547,10 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/peek-readable": {
|
"node_modules/peek-readable": {
|
||||||
"version": "5.1.4",
|
"version": "5.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-5.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-5.3.1.tgz",
|
||||||
"integrity": "sha512-E7mY2VmKqw9jYuXrSWGHFuPCW2SLQenzXLF3amGaY6lXXg4/b3gj5HVM7h8ZjCO/nZS9ICs0Cz285+32FvNd/A==",
|
"integrity": "sha512-GVlENSDW6KHaXcd9zkZltB7tCLosKB/4Hg0fqBJkAoBgYG2Tn1xtMgXtSUuMU9AK/gCm/tTdT8mgAeF4YNeeqw==",
|
||||||
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14.16"
|
"node": ">=14.16"
|
||||||
},
|
},
|
||||||
@@ -13836,12 +13838,13 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/strtok3": {
|
"node_modules/strtok3": {
|
||||||
"version": "8.1.0",
|
"version": "9.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/strtok3/-/strtok3-8.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/strtok3/-/strtok3-9.0.1.tgz",
|
||||||
"integrity": "sha512-ExzDvHYPj6F6QkSNe/JxSlBxTh3OrI6wrAIz53ulxo1c4hBJ1bT9C/JrAthEKHWG9riVH3Xzg7B03Oxty6S2Lw==",
|
"integrity": "sha512-ERPW+XkvX9W2A+ov07iy+ZFJpVdik04GhDA4eVogiG9hpC97Kem2iucyzhFxbFRvQ5o2UckFtKZdp1hkGvnrEw==",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tokenizer/token": "^0.3.0",
|
"@tokenizer/token": "^0.3.0",
|
||||||
"peek-readable": "^5.1.4"
|
"peek-readable": "^5.3.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=16"
|
"node": ">=16"
|
||||||
|
|||||||
@@ -33,7 +33,7 @@
|
|||||||
"delete-orphan-translation-files": "tsx src/workflows/delete-orphan-translation-files.ts",
|
"delete-orphan-translation-files": "tsx src/workflows/delete-orphan-translation-files.ts",
|
||||||
"deleted-features-pr-comment": "tsx src/data-directory/scripts/deleted-features-pr-comment.ts",
|
"deleted-features-pr-comment": "tsx src/data-directory/scripts/deleted-features-pr-comment.ts",
|
||||||
"dev": "cross-env npm start",
|
"dev": "cross-env npm start",
|
||||||
"find-orphaned-assets": "node src/assets/scripts/find-orphaned-assets.js",
|
"find-orphaned-assets": "tsx src/assets/scripts/find-orphaned-assets.ts",
|
||||||
"find-orphaned-features": "tsx src/data-directory/scripts/find-orphaned-features/index.ts",
|
"find-orphaned-features": "tsx src/data-directory/scripts/find-orphaned-features/index.ts",
|
||||||
"find-past-built-pr": "tsx src/workflows/find-past-built-pr.ts",
|
"find-past-built-pr": "tsx src/workflows/find-past-built-pr.ts",
|
||||||
"find-unused-variables": "tsx src/content-linter/scripts/find-unsed-variables.ts",
|
"find-unused-variables": "tsx src/content-linter/scripts/find-unsed-variables.ts",
|
||||||
@@ -259,7 +259,7 @@
|
|||||||
"express": "4.21.1",
|
"express": "4.21.1",
|
||||||
"express-rate-limit": "7.4.0",
|
"express-rate-limit": "7.4.0",
|
||||||
"fastest-levenshtein": "1.0.16",
|
"fastest-levenshtein": "1.0.16",
|
||||||
"file-type": "19.4.1",
|
"file-type": "19.6.0",
|
||||||
"flat": "^6.0.1",
|
"flat": "^6.0.1",
|
||||||
"github-slugger": "^2.0.0",
|
"github-slugger": "^2.0.0",
|
||||||
"glob": "11.0.0",
|
"glob": "11.0.0",
|
||||||
|
|||||||
1
src/assets/lib/image-density.d.ts
vendored
Normal file
1
src/assets/lib/image-density.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export const IMAGE_DENSITY: Record<string, string>
|
||||||
@@ -18,14 +18,19 @@
|
|||||||
// [end-readme]
|
// [end-readme]
|
||||||
|
|
||||||
import { program } from 'commander'
|
import { program } from 'commander'
|
||||||
import main from './deleted-assets-pr-comment.js'
|
import main from './deleted-assets-pr-comment'
|
||||||
|
|
||||||
program
|
program
|
||||||
.description('If applicable, print a snippet of Markdown about deleted assets')
|
.description('If applicable, print a snippet of Markdown about deleted assets')
|
||||||
.arguments('owner repo base_sha head_sha', 'Simulate what the Actions workflow does')
|
.arguments('owner repo base_sha head_sha')
|
||||||
.parse(process.argv)
|
.parse(process.argv)
|
||||||
|
|
||||||
const opts = program.opts()
|
type MainArgs = {
|
||||||
const args = program.args
|
owner: string
|
||||||
|
repo: string
|
||||||
|
baseSHA: string
|
||||||
|
headSHA: string
|
||||||
|
}
|
||||||
|
const opts = program.opts() as MainArgs
|
||||||
|
|
||||||
console.log(await main(...args, { ...opts }))
|
console.log(await main(opts))
|
||||||
@@ -13,16 +13,22 @@ if (!GITHUB_TOKEN) {
|
|||||||
// When this file is invoked directly from action as opposed to being imported
|
// When this file is invoked directly from action as opposed to being imported
|
||||||
if (import.meta.url.endsWith(process.argv[1])) {
|
if (import.meta.url.endsWith(process.argv[1])) {
|
||||||
const owner = context.repo.owner
|
const owner = context.repo.owner
|
||||||
const repo = context.payload.repository.name
|
const repo = context.payload.repository?.name || ''
|
||||||
const baseSHA = context.payload.pull_request.base.sha
|
const baseSHA = context.payload.pull_request?.base.sha
|
||||||
const headSHA = context.payload.pull_request.head.sha
|
const headSHA = context.payload.pull_request?.head.sha
|
||||||
|
|
||||||
const markdown = await main(owner, repo, baseSHA, headSHA)
|
const markdown = await main({ owner, repo, baseSHA, headSHA })
|
||||||
core.setOutput('markdown', markdown)
|
core.setOutput('markdown', markdown)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function main(owner, repo, baseSHA, headSHA) {
|
type MainArgs = {
|
||||||
const octokit = github.getOctokit(GITHUB_TOKEN)
|
owner: string
|
||||||
|
repo: string
|
||||||
|
baseSHA: string
|
||||||
|
headSHA: string
|
||||||
|
}
|
||||||
|
async function main({ owner, repo, baseSHA, headSHA }: MainArgs) {
|
||||||
|
const octokit = github.getOctokit(GITHUB_TOKEN as string)
|
||||||
// get the list of file changes from the PR
|
// get the list of file changes from the PR
|
||||||
const response = await octokit.rest.repos.compareCommitsWithBasehead({
|
const response = await octokit.rest.repos.compareCommitsWithBasehead({
|
||||||
owner,
|
owner,
|
||||||
@@ -32,6 +38,10 @@ async function main(owner, repo, baseSHA, headSHA) {
|
|||||||
|
|
||||||
const { files } = response.data
|
const { files } = response.data
|
||||||
|
|
||||||
|
if (!files) {
|
||||||
|
throw new Error('No files found in the PR')
|
||||||
|
}
|
||||||
|
|
||||||
const oldFilenames = []
|
const oldFilenames = []
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
const { filename, status } = file
|
const { filename, status } = file
|
||||||
@@ -32,7 +32,7 @@ const EXCEPTIONS = new Set([
|
|||||||
'assets/images/site/apple-touch-icon-76x76.png',
|
'assets/images/site/apple-touch-icon-76x76.png',
|
||||||
])
|
])
|
||||||
|
|
||||||
function isExceptionPath(imagePath) {
|
function isExceptionPath(imagePath: string) {
|
||||||
// We also check for .DS_Store because any macOS user that has opened
|
// We also check for .DS_Store because any macOS user that has opened
|
||||||
// a folder with images will have this on disk. It won't get added
|
// a folder with images will have this on disk. It won't get added
|
||||||
// to git anyway thanks to our .DS_Store.
|
// to git anyway thanks to our .DS_Store.
|
||||||
@@ -53,9 +53,15 @@ program
|
|||||||
.option('--exclude-translations', "Don't search in translations/")
|
.option('--exclude-translations', "Don't search in translations/")
|
||||||
.parse(process.argv)
|
.parse(process.argv)
|
||||||
|
|
||||||
main(program.opts(), program.args)
|
type MainOptions = {
|
||||||
|
json: boolean
|
||||||
|
verbose: boolean
|
||||||
|
exit: boolean
|
||||||
|
excludeTranslations: boolean
|
||||||
|
}
|
||||||
|
main(program.opts())
|
||||||
|
|
||||||
async function main(opts) {
|
async function main(opts: MainOptions) {
|
||||||
const { json, verbose, exit, excludeTranslations } = opts
|
const { json, verbose, exit, excludeTranslations } = opts
|
||||||
|
|
||||||
const walkOptions = {
|
const walkOptions = {
|
||||||
@@ -164,7 +170,7 @@ async function main(opts) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getTotalDiskSize(filePaths) {
|
function getTotalDiskSize(filePaths: Set<string>) {
|
||||||
let sum = 0
|
let sum = 0
|
||||||
for (const filePath of filePaths) {
|
for (const filePath of filePaths) {
|
||||||
sum += fs.statSync(filePath).size
|
sum += fs.statSync(filePath).size
|
||||||
@@ -10,32 +10,26 @@ import { fileURLToPath } from 'url'
|
|||||||
import path from 'path'
|
import path from 'path'
|
||||||
import walk from 'walk-sync'
|
import walk from 'walk-sync'
|
||||||
import sharp from 'sharp'
|
import sharp from 'sharp'
|
||||||
import { chain } from 'lodash-es'
|
|
||||||
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||||
|
|
||||||
const imagesPath = path.join(__dirname, '../assets/images')
|
const imagesPath = path.join(__dirname, '../assets/images')
|
||||||
const imagesExtensions = ['.jpg', '.jpeg', '.png', '.gif']
|
const imagesExtensions = ['.jpg', '.jpeg', '.png', '.gif']
|
||||||
|
|
||||||
const files = chain(walk(imagesPath, { directories: false })).filter((relativePath) => {
|
const files = walk(imagesPath, { directories: false }).filter((relativePath) => {
|
||||||
return imagesExtensions.includes(path.extname(relativePath.toLowerCase()))
|
return imagesExtensions.includes(path.extname(relativePath.toLowerCase()))
|
||||||
})
|
})
|
||||||
const infos = await Promise.all(
|
const images = await Promise.all(
|
||||||
files.map(async (relativePath) => {
|
files.map(async (relativePath) => {
|
||||||
const fullPath = path.join(imagesPath, relativePath)
|
const fullPath = path.join(imagesPath, relativePath)
|
||||||
const image = sharp(fullPath)
|
const image = sharp(fullPath)
|
||||||
const { width, height } = await image.metadata()
|
const { width, height } = await image.metadata()
|
||||||
const size = width * height
|
const size = (width || 0) * (height || 0)
|
||||||
return { relativePath, width, height, size }
|
return { relativePath, width, height, size }
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
const images = files
|
images
|
||||||
.map((relativePath, i) => {
|
.sort((a, b) => b.size - a.size)
|
||||||
return { relativePath, ...infos[i] }
|
.forEach((image) => {
|
||||||
|
const { relativePath, width, height } = image
|
||||||
|
console.log(`${width} x ${height} - ${relativePath}`)
|
||||||
})
|
})
|
||||||
.orderBy('size', 'desc')
|
|
||||||
.value()
|
|
||||||
|
|
||||||
images.forEach((image) => {
|
|
||||||
const { relativePath, width, height } = image
|
|
||||||
console.log(`${width} x ${height} - ${relativePath}`)
|
|
||||||
})
|
|
||||||
@@ -19,6 +19,7 @@ import path from 'path'
|
|||||||
import { program } from 'commander'
|
import { program } from 'commander'
|
||||||
import chalk from 'chalk'
|
import chalk from 'chalk'
|
||||||
import cheerio from 'cheerio'
|
import cheerio from 'cheerio'
|
||||||
|
// @ts-ignore see https://github.com/sindresorhus/file-type/issues/652
|
||||||
import { fileTypeFromFile } from 'file-type'
|
import { fileTypeFromFile } from 'file-type'
|
||||||
import walk from 'walk-sync'
|
import walk from 'walk-sync'
|
||||||
import isSVG from 'is-svg'
|
import isSVG from 'is-svg'
|
||||||
@@ -43,7 +44,7 @@ const EXPECT = {
|
|||||||
'.ico': 'image/x-icon',
|
'.ico': 'image/x-icon',
|
||||||
'.pdf': 'application/pdf',
|
'.pdf': 'application/pdf',
|
||||||
'.webp': 'image/webp',
|
'.webp': 'image/webp',
|
||||||
}
|
} as Record<string, string>
|
||||||
|
|
||||||
const CRITICAL = 'critical'
|
const CRITICAL = 'critical'
|
||||||
const WARNING = 'warning'
|
const WARNING = 'warning'
|
||||||
@@ -56,7 +57,7 @@ program
|
|||||||
|
|
||||||
main(program.opts())
|
main(program.opts())
|
||||||
|
|
||||||
async function main(opts) {
|
async function main(opts: { dryRun: boolean; verbose: boolean }) {
|
||||||
let errors = 0
|
let errors = 0
|
||||||
|
|
||||||
const files = walk(ASSETS_ROOT, { includeBasePath: true, directories: false }).filter(
|
const files = walk(ASSETS_ROOT, { includeBasePath: true, directories: false }).filter(
|
||||||
@@ -71,7 +72,11 @@ async function main(opts) {
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
const results = (await Promise.all(files.map(checkFile))).filter(Boolean)
|
const results = (await Promise.all(files.map(checkFile))).filter(Boolean) as [
|
||||||
|
level: string,
|
||||||
|
filePath: string,
|
||||||
|
error: string,
|
||||||
|
][]
|
||||||
for (const [level, filePath, error] of results) {
|
for (const [level, filePath, error] of results) {
|
||||||
console.log(
|
console.log(
|
||||||
level === CRITICAL ? chalk.red(level) : chalk.yellow(level),
|
level === CRITICAL ? chalk.red(level) : chalk.yellow(level),
|
||||||
@@ -94,7 +99,7 @@ async function main(opts) {
|
|||||||
process.exitCode = errors
|
process.exitCode = errors
|
||||||
}
|
}
|
||||||
|
|
||||||
async function checkFile(filePath) {
|
async function checkFile(filePath: string) {
|
||||||
const ext = path.extname(filePath)
|
const ext = path.extname(filePath)
|
||||||
|
|
||||||
const { size } = await fs.stat(filePath)
|
const { size } = await fs.stat(filePath)
|
||||||
@@ -113,7 +118,7 @@ async function checkFile(filePath) {
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
checkSVGContent(content)
|
checkSVGContent(content)
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
return [CRITICAL, filePath, error.message]
|
return [CRITICAL, filePath, error.message]
|
||||||
}
|
}
|
||||||
} else if (EXPECT[ext]) {
|
} else if (EXPECT[ext]) {
|
||||||
@@ -135,15 +140,15 @@ async function checkFile(filePath) {
|
|||||||
// All is well. Nothing to complain about.
|
// All is well. Nothing to complain about.
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkSVGContent(content) {
|
function checkSVGContent(content: string) {
|
||||||
const $ = cheerio.load(content)
|
const $ = cheerio.load(content)
|
||||||
const disallowedTagNames = new Set(['script', 'object', 'iframe', 'embed'])
|
const disallowedTagNames = new Set(['script', 'object', 'iframe', 'embed'])
|
||||||
$('*').each((i, element) => {
|
$('*').each((i, element) => {
|
||||||
const { tagName } = element
|
const { tagName } = $(element).get(0)
|
||||||
if (disallowedTagNames.has(tagName)) {
|
if (disallowedTagNames.has(tagName)) {
|
||||||
throw new Error(`contains a <${tagName}> tag`)
|
throw new Error(`contains a <${tagName}> tag`)
|
||||||
}
|
}
|
||||||
for (const key in element.attribs) {
|
for (const key in $(element).get(0).attribs) {
|
||||||
// Looks for suspicious event handlers on tags.
|
// Looks for suspicious event handlers on tags.
|
||||||
// For example `<path oNload="alert(1)"" d="M28 0l4.59 4.59-9.76`
|
// For example `<path oNload="alert(1)"" d="M28 0l4.59 4.59-9.76`
|
||||||
// We don't need to do a case-sensitive regex here because cheerio
|
// We don't need to do a case-sensitive regex here because cheerio
|
||||||
@@ -6,7 +6,7 @@ import { describe, expect, test, vi } from 'vitest'
|
|||||||
import { get } from '#src/tests/helpers/e2etest.js'
|
import { get } from '#src/tests/helpers/e2etest.js'
|
||||||
import { checkCachingHeaders } from '#src/tests/helpers/caching-headers.js'
|
import { checkCachingHeaders } from '#src/tests/helpers/caching-headers.js'
|
||||||
|
|
||||||
function getNextStaticAsset(directory) {
|
function getNextStaticAsset(directory: string) {
|
||||||
const root = path.join('.next', 'static', directory)
|
const root = path.join('.next', 'static', directory)
|
||||||
const files = fs.readdirSync(root)
|
const files = fs.readdirSync(root)
|
||||||
if (!files.length) throw new Error(`Can't find any files in ${root}`)
|
if (!files.length) throw new Error(`Can't find any files in ${root}`)
|
||||||
@@ -1,11 +1,7 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "ES2022",
|
"target": "ES2022",
|
||||||
"lib": [
|
"lib": ["dom", "dom.iterable", "esnext"],
|
||||||
"dom",
|
|
||||||
"dom.iterable",
|
|
||||||
"esnext"
|
|
||||||
],
|
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
@@ -30,9 +26,5 @@
|
|||||||
"docs-internal-data",
|
"docs-internal-data",
|
||||||
"src/code-scanning/scripts/generate-code-scanning-query-list.ts"
|
"src/code-scanning/scripts/generate-code-scanning-query-list.ts"
|
||||||
],
|
],
|
||||||
"include": [
|
"include": ["*.d.ts", "**/*.ts", "**/*.tsx"]
|
||||||
"*.d.ts",
|
|
||||||
"**/*.ts",
|
|
||||||
"**/*.tsx"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user