refactor: Convert the build scripts to TypeScript and group them (#2696)
- Closes: #2695
This commit is contained in:
+1
-1
@@ -178,7 +178,7 @@
|
||||
"docs-update-images": "./scripts/docs-update-images.sh",
|
||||
"docs-update-dependencies": "./scripts/docs-update-dependencies.sh",
|
||||
"docs-update-uml": "docker run --init --rm -u \"$(id -u):$(id -g)\" -e HOME=/tmp -e JAVA_TOOL_OPTIONS=-Duser.home=/tmp -v \"$(pwd):/data\" -w /data plantuml/plantuml -tsvg docs/uml/call-sequence.puml -o /data/docs/images",
|
||||
"lint": "eslint '{src,tests}/**/*.ts'",
|
||||
"lint": "eslint '{scripts,src,tests}/**/*.ts'",
|
||||
"typecheck": "tsc --noEmit -p tsconfig.json",
|
||||
"format": "prettier --write .",
|
||||
"format-check": "prettier --check .",
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
import { init, parse } from 'es-module-lexer';
|
||||
|
||||
const DIST_DIRECTORY = 'dist';
|
||||
|
||||
const listFiles = () =>
|
||||
existsSync(DIST_DIRECTORY)
|
||||
? readdirSync(DIST_DIRECTORY, { withFileTypes: true })
|
||||
.filter((entry) => entry.isFile())
|
||||
.map((entry) => entry.name)
|
||||
: [];
|
||||
|
||||
/**
|
||||
* What a file imports, split into static imports, which the browser fetches
|
||||
* before the file runs, and dynamic ones, which it fetches only when reached.
|
||||
*/
|
||||
const getImports = async (source) => {
|
||||
await init;
|
||||
const [imports] = parse(source);
|
||||
|
||||
// `n` is the file named in the import, and is absent when that name is put
|
||||
// together at runtime, which names no one file.
|
||||
const named = imports.filter((entry) => entry.n);
|
||||
|
||||
// `d` says how it is imported: -1 static, -2 `import.meta`, otherwise the
|
||||
// position of the `import(`.
|
||||
return {
|
||||
staticImports: named.filter((entry) => entry.d === -1).map((entry) => entry.n),
|
||||
dynamicImports: named.filter((entry) => entry.d >= 0).map((entry) => entry.n),
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Facts about the built output, gathered in Node and handed to a test running
|
||||
* in the browser.
|
||||
*
|
||||
* The imports are parsed rather than searched for: the card holds its own
|
||||
* filenames as plain strings, which a text search would mistake for imports.
|
||||
*
|
||||
* @type {Record<string, import('vitest/node').BrowserCommand<[], unknown>>}
|
||||
*/
|
||||
export const distCommands = {
|
||||
listDistFiles: () => listFiles(),
|
||||
|
||||
getDistImportGraph: async () => {
|
||||
const graph = {};
|
||||
for (const file of listFiles().filter((name) => name.endsWith('.js'))) {
|
||||
graph[file] = await getImports(
|
||||
readFileSync(path.resolve(DIST_DIRECTORY, file), 'utf8'),
|
||||
);
|
||||
}
|
||||
return graph;
|
||||
},
|
||||
};
|
||||
@@ -1,12 +1,12 @@
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import { readFileSync } from 'node:fs';
|
||||
|
||||
import { BUILD_DATE_PLACEHOLDER } from './build-date-plugin.js';
|
||||
import { BUILD_DATE_PLACEHOLDER } from './plugins/build-date.js';
|
||||
|
||||
/**
|
||||
* Asks git something or gives back nothing when it cannot be asked.
|
||||
*/
|
||||
const askGit = (...args) => {
|
||||
const askGit = (...args: string[]): string => {
|
||||
try {
|
||||
return execFileSync('git', args, {
|
||||
encoding: 'utf-8',
|
||||
@@ -17,8 +17,25 @@ const askGit = (...args) => {
|
||||
}
|
||||
};
|
||||
|
||||
const getPackageVersion = () =>
|
||||
JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf-8')).version;
|
||||
const getPackageVersion = (): string => {
|
||||
const contents: unknown = JSON.parse(
|
||||
readFileSync(new URL('../../package.json', import.meta.url), 'utf-8'),
|
||||
);
|
||||
if (
|
||||
typeof contents !== 'object' ||
|
||||
contents === null ||
|
||||
!('version' in contents) ||
|
||||
typeof contents.version !== 'string'
|
||||
) {
|
||||
throw new Error('package.json must have a string version');
|
||||
}
|
||||
return contents.version;
|
||||
};
|
||||
|
||||
interface BuildDefinesOptions {
|
||||
dev: boolean;
|
||||
releaseVersion?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* What the build stamps into the card, as names for the bundler to substitute.
|
||||
@@ -33,7 +50,10 @@ const getPackageVersion = () =>
|
||||
*
|
||||
* The values are JSON so that a bundler can drop them in as written.
|
||||
*/
|
||||
export const getBuildDefines = ({ dev, releaseVersion }) => {
|
||||
export const getBuildDefines = ({
|
||||
dev,
|
||||
releaseVersion,
|
||||
}: BuildDefinesOptions): Record<string, string> => {
|
||||
const gitHash = askGit('rev-parse', '--short', 'HEAD');
|
||||
const developmentVersion = gitHash ? `dev+${gitHash}` : 'dev';
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
/**
|
||||
* Vite plugin: writes the date of the build into the card.
|
||||
* Writes the date of the build into the card.
|
||||
*/
|
||||
|
||||
import type { Plugin } from 'vite';
|
||||
|
||||
// Strict length requirement: See below.
|
||||
export const BUILD_DATE_PLACEHOLDER = '__BUILD_DATE_SENTINEL___';
|
||||
|
||||
@@ -11,10 +13,8 @@ export const BUILD_DATE_PLACEHOLDER = '__BUILD_DATE_SENTINEL___';
|
||||
* As such, every rebuild would report the time the watcher started rather than
|
||||
* the actual build time. Output is generated afresh for each build, so the date
|
||||
* is written here instead.
|
||||
*
|
||||
* @type {() => import('vite').Plugin}
|
||||
*/
|
||||
export const buildDate = () => ({
|
||||
export const buildDate = (): Plugin => ({
|
||||
name: 'build-date',
|
||||
|
||||
renderChunk(code) {
|
||||
@@ -1,19 +1,19 @@
|
||||
import { readdirSync, rmSync } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
import type { Plugin } from 'vite';
|
||||
|
||||
/**
|
||||
* Vite plugin: removes build artifacts an earlier build left behind, which
|
||||
* otherwise accumulate indefinitely as the hashed names change.
|
||||
* Removes build artifacts an earlier build left behind, which otherwise accumulate
|
||||
* indefinitely as the hashed names change.
|
||||
*
|
||||
* Runs once the new output is on disk, and keeps whatever this build just
|
||||
* wrote, rather than emptying the directory beforehand. This ensures the card
|
||||
* is never briefly missing from a directory Home Assistant is potentially
|
||||
* serving out of. Only the build's own kind of file is removed, by extension
|
||||
* and without recursing, so anything else there survives.
|
||||
*
|
||||
* @type {() => import('vite').Plugin}
|
||||
*/
|
||||
export const cleanDist = () => ({
|
||||
export const cleanDist = (): Plugin => ({
|
||||
name: 'clean-dist',
|
||||
|
||||
writeBundle(options, bundle) {
|
||||
@@ -1,3 +1,9 @@
|
||||
import type { Plugin } from 'vite';
|
||||
|
||||
interface FacadeEntryOptions {
|
||||
publicFileNames: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Emits the files a dashboard resource can name, each a re-export of the hashed
|
||||
* chunk holding the actual card.
|
||||
@@ -8,10 +14,8 @@
|
||||
* run a second time, and defining its elements twice will throw an error.
|
||||
* Keeping the card itself in a hashed chunk that nothing outside can name is
|
||||
* what prevents that, and the check below is what keeps it true.
|
||||
*
|
||||
* @type {(options: { publicFileNames: string[] }) => import('vite').Plugin}
|
||||
*/
|
||||
export const facadeEntry = ({ publicFileNames }) => ({
|
||||
export const facadeEntry = ({ publicFileNames }: FacadeEntryOptions): Plugin => ({
|
||||
name: 'facade-entry',
|
||||
|
||||
generateBundle(_options, bundle) {
|
||||
@@ -1,13 +1,12 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
|
||||
import type { Plugin } from 'vite';
|
||||
|
||||
/**
|
||||
* Vite plugin: importing an SVG yields `{ path, viewBox }` extracted at build
|
||||
* time, the shape a Home Assistant custom iconset serves. The SVG must be a
|
||||
* single-path icon.
|
||||
*
|
||||
* @type {() => import('vite').Plugin}
|
||||
* Importing an SVG yields `{ path, viewBox }` extracted at build time, the shape a
|
||||
* Home Assistant custom iconset serves. The SVG must be a single-path icon.
|
||||
*/
|
||||
export const svgPath = () => ({
|
||||
export const svgPath = (): Plugin => ({
|
||||
name: 'svg-path',
|
||||
|
||||
// Run before Vite's builtin asset handling.
|
||||
@@ -0,0 +1,74 @@
|
||||
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
import { init, parse } from 'es-module-lexer';
|
||||
import type { BrowserCommand } from 'vitest/node';
|
||||
|
||||
const DIST_DIRECTORY = 'dist';
|
||||
|
||||
const listFiles = (): string[] =>
|
||||
existsSync(DIST_DIRECTORY)
|
||||
? readdirSync(DIST_DIRECTORY, { withFileTypes: true })
|
||||
.filter((entry) => entry.isFile())
|
||||
.map((entry) => entry.name)
|
||||
: [];
|
||||
|
||||
interface FileImports {
|
||||
staticImports: string[];
|
||||
dynamicImports: string[];
|
||||
}
|
||||
|
||||
const STATIC_IMPORT = -1;
|
||||
|
||||
/**
|
||||
* What a file imports, split into static imports, which the browser fetches
|
||||
* before the file runs, and dynamic ones, which it fetches only when reached.
|
||||
*
|
||||
* es-module-lexer names its fields tersely. `n` is the imported path, and is
|
||||
* absent when a dynamic import builds its path at runtime (e.g. `import('./' +
|
||||
* name)`), so no one file can be recorded for it. `d` is the position of a
|
||||
* dynamic import's `import(`, or -1 for a static import and -2 for
|
||||
* `import.meta`, which is not an import of a file at all.
|
||||
*/
|
||||
const getImports = async (source: string): Promise<FileImports> => {
|
||||
await init;
|
||||
const [imports] = parse(source);
|
||||
|
||||
const staticImports: string[] = [];
|
||||
const dynamicImports: string[] = [];
|
||||
|
||||
for (const { n: importedPath, d: dynamicImportPosition } of imports) {
|
||||
if (importedPath === undefined) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (dynamicImportPosition === STATIC_IMPORT) {
|
||||
staticImports.push(importedPath);
|
||||
} else if (dynamicImportPosition >= 0) {
|
||||
dynamicImports.push(importedPath);
|
||||
}
|
||||
}
|
||||
|
||||
return { staticImports, dynamicImports };
|
||||
};
|
||||
|
||||
/**
|
||||
* Facts about the built output, gathered in Node and handed to a test running
|
||||
* in the browser.
|
||||
*
|
||||
* The imports are parsed rather than searched for: the card holds its own
|
||||
* filenames as plain strings, which a text search would mistake for imports.
|
||||
*/
|
||||
export const distCommands: Record<string, BrowserCommand<[], unknown>> = {
|
||||
listDistFiles: () => listFiles(),
|
||||
|
||||
getDistImportGraph: async () => {
|
||||
const graph: Record<string, FileImports> = {};
|
||||
for (const file of listFiles().filter((name) => name.endsWith('.js'))) {
|
||||
graph[file] = await getImports(
|
||||
readFileSync(path.resolve(DIST_DIRECTORY, file), 'utf8'),
|
||||
);
|
||||
}
|
||||
return graph;
|
||||
},
|
||||
};
|
||||
Vendored
+1
-1
@@ -1,7 +1,7 @@
|
||||
import { beforeAll, describe, expect, it } from 'vitest';
|
||||
import { commands } from 'vitest/browser';
|
||||
|
||||
import { PUBLIC_ENTRIES, PUBLIC_ENTRY } from '../../scripts/public-entries.js';
|
||||
import { PUBLIC_ENTRIES, PUBLIC_ENTRY } from '../../scripts/vite/public-entries.js';
|
||||
import type { RawAdvancedCameraCardConfig } from '../../src/config/types';
|
||||
import type { FakeHASS } from '../browser/fake-hass';
|
||||
import { defineHAElementStubs } from '../browser/ha-element-stubs';
|
||||
|
||||
+4
-1
@@ -60,5 +60,8 @@
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
// `allowJs` would otherwise parse the build output.
|
||||
"exclude": ["dist", "node_modules"]
|
||||
}
|
||||
|
||||
+6
-6
@@ -1,12 +1,12 @@
|
||||
import { visualizer } from 'rollup-plugin-visualizer';
|
||||
import { defineConfig } from 'vite';
|
||||
|
||||
import { buildDate } from './scripts/build-date-plugin.js';
|
||||
import { getBuildDefines } from './scripts/build-defines.js';
|
||||
import { cleanDist } from './scripts/clean-dist-plugin.js';
|
||||
import { facadeEntry } from './scripts/facade-entry-plugin.js';
|
||||
import { PUBLIC_ENTRIES } from './scripts/public-entries.js';
|
||||
import { svgPath } from './scripts/svg-path-plugin.js';
|
||||
import { getBuildDefines } from './scripts/vite/build-defines.js';
|
||||
import { buildDate } from './scripts/vite/plugins/build-date.js';
|
||||
import { cleanDist } from './scripts/vite/plugins/clean-dist.js';
|
||||
import { facadeEntry } from './scripts/vite/plugins/facade-entry.js';
|
||||
import { svgPath } from './scripts/vite/plugins/svg-path.js';
|
||||
import { PUBLIC_ENTRIES } from './scripts/vite/public-entries.js';
|
||||
|
||||
// `yarn start` passes `--mode development`; a plain `vite build` is
|
||||
// `production`.
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { playwright } from '@vitest/browser-playwright';
|
||||
import { defineConfig } from 'vitest/config';
|
||||
|
||||
import { getBrowsers, type Browser } from './scripts/browsers.js';
|
||||
import { svgPath } from './scripts/svg-path-plugin.js';
|
||||
import { svgPath } from './scripts/vite/plugins/svg-path.js';
|
||||
import { getBrowsers, type Browser } from './scripts/vitest/browsers.js';
|
||||
|
||||
// Browser tests mount the real card in a browser. Their own config rather than
|
||||
// a fourth project in `vitest.config.ts`, because `vitest run` executes every
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@ import { join } from 'path';
|
||||
|
||||
import { defineConfig } from 'vitest/config';
|
||||
|
||||
import { svgPath } from './scripts/svg-path-plugin.js';
|
||||
import { svgPath } from './scripts/vite/plugins/svg-path.js';
|
||||
|
||||
const EXCLUSIONS = [
|
||||
'.eslintrc.cjs',
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { playwright } from '@vitest/browser-playwright';
|
||||
import { defineConfig } from 'vitest/config';
|
||||
|
||||
import { getBrowsers, type Browser } from './scripts/browsers.js';
|
||||
import { distCommands } from './scripts/dist-commands.js';
|
||||
import { getBrowsers, type Browser } from './scripts/vitest/browsers.js';
|
||||
import { distCommands } from './scripts/vitest/dist-commands.js';
|
||||
|
||||
// This is the only test suite that runs against the built bundle rather than
|
||||
// `src/`. They need a build to already exist: run `yarn run build` first.
|
||||
|
||||
Reference in New Issue
Block a user