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-images": "./scripts/docs-update-images.sh",
|
||||||
"docs-update-dependencies": "./scripts/docs-update-dependencies.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",
|
"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",
|
"typecheck": "tsc --noEmit -p tsconfig.json",
|
||||||
"format": "prettier --write .",
|
"format": "prettier --write .",
|
||||||
"format-check": "prettier --check .",
|
"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 { execFileSync } from 'node:child_process';
|
||||||
import { readFileSync } from 'node:fs';
|
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.
|
* Asks git something or gives back nothing when it cannot be asked.
|
||||||
*/
|
*/
|
||||||
const askGit = (...args) => {
|
const askGit = (...args: string[]): string => {
|
||||||
try {
|
try {
|
||||||
return execFileSync('git', args, {
|
return execFileSync('git', args, {
|
||||||
encoding: 'utf-8',
|
encoding: 'utf-8',
|
||||||
@@ -17,8 +17,25 @@ const askGit = (...args) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getPackageVersion = () =>
|
const getPackageVersion = (): string => {
|
||||||
JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf-8')).version;
|
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.
|
* 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.
|
* 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 gitHash = askGit('rev-parse', '--short', 'HEAD');
|
||||||
const developmentVersion = gitHash ? `dev+${gitHash}` : 'dev';
|
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.
|
// Strict length requirement: See below.
|
||||||
export const BUILD_DATE_PLACEHOLDER = '__BUILD_DATE_SENTINEL___';
|
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
|
* 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
|
* the actual build time. Output is generated afresh for each build, so the date
|
||||||
* is written here instead.
|
* is written here instead.
|
||||||
*
|
|
||||||
* @type {() => import('vite').Plugin}
|
|
||||||
*/
|
*/
|
||||||
export const buildDate = () => ({
|
export const buildDate = (): Plugin => ({
|
||||||
name: 'build-date',
|
name: 'build-date',
|
||||||
|
|
||||||
renderChunk(code) {
|
renderChunk(code) {
|
||||||
@@ -1,19 +1,19 @@
|
|||||||
import { readdirSync, rmSync } from 'node:fs';
|
import { readdirSync, rmSync } from 'node:fs';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
|
|
||||||
|
import type { Plugin } from 'vite';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Vite plugin: removes build artifacts an earlier build left behind, which
|
* Removes build artifacts an earlier build left behind, which otherwise accumulate
|
||||||
* otherwise accumulate indefinitely as the hashed names change.
|
* indefinitely as the hashed names change.
|
||||||
*
|
*
|
||||||
* Runs once the new output is on disk, and keeps whatever this build just
|
* 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
|
* wrote, rather than emptying the directory beforehand. This ensures the card
|
||||||
* is never briefly missing from a directory Home Assistant is potentially
|
* 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
|
* serving out of. Only the build's own kind of file is removed, by extension
|
||||||
* and without recursing, so anything else there survives.
|
* and without recursing, so anything else there survives.
|
||||||
*
|
|
||||||
* @type {() => import('vite').Plugin}
|
|
||||||
*/
|
*/
|
||||||
export const cleanDist = () => ({
|
export const cleanDist = (): Plugin => ({
|
||||||
name: 'clean-dist',
|
name: 'clean-dist',
|
||||||
|
|
||||||
writeBundle(options, bundle) {
|
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
|
* Emits the files a dashboard resource can name, each a re-export of the hashed
|
||||||
* chunk holding the actual card.
|
* chunk holding the actual card.
|
||||||
@@ -8,10 +14,8 @@
|
|||||||
* run a second time, and defining its elements twice will throw an error.
|
* 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
|
* 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.
|
* 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',
|
name: 'facade-entry',
|
||||||
|
|
||||||
generateBundle(_options, bundle) {
|
generateBundle(_options, bundle) {
|
||||||
@@ -1,13 +1,12 @@
|
|||||||
import { readFileSync } from 'node:fs';
|
import { readFileSync } from 'node:fs';
|
||||||
|
|
||||||
|
import type { Plugin } from 'vite';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Vite plugin: importing an SVG yields `{ path, viewBox }` extracted at build
|
* Importing an SVG yields `{ path, viewBox }` extracted at build time, the shape a
|
||||||
* time, the shape a Home Assistant custom iconset serves. The SVG must be a
|
* Home Assistant custom iconset serves. The SVG must be a single-path icon.
|
||||||
* single-path icon.
|
|
||||||
*
|
|
||||||
* @type {() => import('vite').Plugin}
|
|
||||||
*/
|
*/
|
||||||
export const svgPath = () => ({
|
export const svgPath = (): Plugin => ({
|
||||||
name: 'svg-path',
|
name: 'svg-path',
|
||||||
|
|
||||||
// Run before Vite's builtin asset handling.
|
// 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 { beforeAll, describe, expect, it } from 'vitest';
|
||||||
import { commands } from 'vitest/browser';
|
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 { RawAdvancedCameraCardConfig } from '../../src/config/types';
|
||||||
import type { FakeHASS } from '../browser/fake-hass';
|
import type { FakeHASS } from '../browser/fake-hass';
|
||||||
import { defineHAElementStubs } from '../browser/ha-element-stubs';
|
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 { visualizer } from 'rollup-plugin-visualizer';
|
||||||
import { defineConfig } from 'vite';
|
import { defineConfig } from 'vite';
|
||||||
|
|
||||||
import { buildDate } from './scripts/build-date-plugin.js';
|
import { getBuildDefines } from './scripts/vite/build-defines.js';
|
||||||
import { getBuildDefines } from './scripts/build-defines.js';
|
import { buildDate } from './scripts/vite/plugins/build-date.js';
|
||||||
import { cleanDist } from './scripts/clean-dist-plugin.js';
|
import { cleanDist } from './scripts/vite/plugins/clean-dist.js';
|
||||||
import { facadeEntry } from './scripts/facade-entry-plugin.js';
|
import { facadeEntry } from './scripts/vite/plugins/facade-entry.js';
|
||||||
import { PUBLIC_ENTRIES } from './scripts/public-entries.js';
|
import { svgPath } from './scripts/vite/plugins/svg-path.js';
|
||||||
import { svgPath } from './scripts/svg-path-plugin.js';
|
import { PUBLIC_ENTRIES } from './scripts/vite/public-entries.js';
|
||||||
|
|
||||||
// `yarn start` passes `--mode development`; a plain `vite build` is
|
// `yarn start` passes `--mode development`; a plain `vite build` is
|
||||||
// `production`.
|
// `production`.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { playwright } from '@vitest/browser-playwright';
|
import { playwright } from '@vitest/browser-playwright';
|
||||||
import { defineConfig } from 'vitest/config';
|
import { defineConfig } from 'vitest/config';
|
||||||
|
|
||||||
import { getBrowsers, type Browser } from './scripts/browsers.js';
|
import { svgPath } from './scripts/vite/plugins/svg-path.js';
|
||||||
import { svgPath } from './scripts/svg-path-plugin.js';
|
import { getBrowsers, type Browser } from './scripts/vitest/browsers.js';
|
||||||
|
|
||||||
// Browser tests mount the real card in a browser. Their own config rather than
|
// 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
|
// 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 { defineConfig } from 'vitest/config';
|
||||||
|
|
||||||
import { svgPath } from './scripts/svg-path-plugin.js';
|
import { svgPath } from './scripts/vite/plugins/svg-path.js';
|
||||||
|
|
||||||
const EXCLUSIONS = [
|
const EXCLUSIONS = [
|
||||||
'.eslintrc.cjs',
|
'.eslintrc.cjs',
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { playwright } from '@vitest/browser-playwright';
|
import { playwright } from '@vitest/browser-playwright';
|
||||||
import { defineConfig } from 'vitest/config';
|
import { defineConfig } from 'vitest/config';
|
||||||
|
|
||||||
import { getBrowsers, type Browser } from './scripts/browsers.js';
|
import { getBrowsers, type Browser } from './scripts/vitest/browsers.js';
|
||||||
import { distCommands } from './scripts/dist-commands.js';
|
import { distCommands } from './scripts/vitest/dist-commands.js';
|
||||||
|
|
||||||
// This is the only test suite that runs against the built bundle rather than
|
// 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.
|
// `src/`. They need a build to already exist: run `yarn run build` first.
|
||||||
|
|||||||
Reference in New Issue
Block a user