diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 29fba5bab..f00dec533 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -2,7 +2,7 @@ name: ci
on:
push:
branches: [master, next]
- paths-ignore: ['**.md']
+ paths-ignore: ["**.md"]
pull_request: {}
workflow_dispatch: {}
concurrency:
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index 4b049852d..d54ca6dc7 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -5,7 +5,7 @@ on:
pull_request: {}
workflow_dispatch: {}
schedule:
- - cron: '28 6 * * 4'
+ - cron: "28 6 * * 4"
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.head.label || github.run_id }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
diff --git a/.github/workflows/semgrep-analysis.yml b/.github/workflows/semgrep-analysis.yml
index fe659b69f..1ae40099b 100644
--- a/.github/workflows/semgrep-analysis.yml
+++ b/.github/workflows/semgrep-analysis.yml
@@ -5,7 +5,7 @@ on:
pull_request: {}
workflow_dispatch: {}
schedule:
- - cron: '28 6 * * 4'
+ - cron: "28 6 * * 4"
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.head.label || github.run_id }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
diff --git a/biome.jsonc b/biome.jsonc
index 118c702fc..ebafabd54 100644
--- a/biome.jsonc
+++ b/biome.jsonc
@@ -117,6 +117,9 @@
},
"style": {
"noNamespaceImport": "off"
+ },
+ "nursery": {
+ "noDynamicNamespaceImportAccess": "off"
}
}
},
diff --git a/build.ts b/build.ts
index 4fb63ed30..8bab4378b 100644
--- a/build.ts
+++ b/build.ts
@@ -48,11 +48,9 @@ function compileCSS(src: string, from: string) {
}
/**
- * Construct HTML and CSS files and save them to disk.
+ * Construct HTML file and save it to disk.
*/
-async function makeHTML(pageName: string, stylePath: string) {
- const styleSrc = await Bun.file(stylePath).text();
- const css = compileCSS(styleSrc, stylePath);
+async function makeHTML(pageName: string) {
const template = `
@@ -64,10 +62,18 @@ async function makeHTML(pageName: string, stylePath: string) {
.trim()
.replaceAll(/\n\s+/g, '\n'); // remove leading whitespace
- await Bun.write(`dist/${pageName}.css`, css);
await Bun.write(`dist/${pageName}.html`, template);
}
+/**
+ * Construct CSS file and save it to disk.
+ */
+async function makeCSS(pageName: string, cssEntrypoint: string) {
+ const cssSource = await Bun.file(cssEntrypoint).text();
+ const css = compileCSS(cssSource, cssEntrypoint);
+ await Bun.write(`dist/${pageName}.css`, css);
+}
+
/**
* Compile all themes, combine into a single JSON file, and save it to disk.
*/
@@ -86,29 +92,37 @@ async function makeThemes() {
await Bun.write('dist/themes.json', JSON.stringify(themes));
}
-async function minifyJS(artifact: BuildArtifact) {
- let source = await artifact.text();
-
- // Improve collapsing variables; terser doesn't do this so we do it manually.
- source = source.replaceAll('const ', 'let ');
-
- const result = await terser.minify(source, {
- ecma: 2020,
- module: true,
- compress: {
- reduce_funcs: false, // prevent functions being inlined
- // XXX: Comment out to keep performance markers for debugging.
- pure_funcs: ['performance.mark', 'performance.measure'],
- passes: 3,
- },
- mangle: {
- properties: {
- regex: /^\$\$|^__click$/,
- },
- },
- });
-
- await Bun.write(artifact.path, result.code ?? '');
+async function minifyJS(artifacts: BuildArtifact[]) {
+ for (const artifact of artifacts) {
+ if (artifact.kind === 'entry-point' || artifact.kind === 'chunk') {
+ const source = await artifact.text();
+ const result = await terser.minify(source, {
+ ecma: 2020,
+ module: true,
+ compress: {
+ comparisons: false,
+ negate_iife: false,
+ reduce_funcs: false, // prevent function inlining
+ passes: 3,
+ // XXX: Comment out to keep performance markers for debugging.
+ pure_funcs: ['performance.mark', 'performance.measure'],
+ },
+ mangle: {
+ properties: {
+ regex: /^\$\$|^__click$/,
+ },
+ },
+ format: {
+ wrap_func_args: true,
+ wrap_iife: true,
+ },
+ });
+
+ if (result.code) {
+ await Bun.write(artifact.path, result.code);
+ }
+ }
+ }
}
console.time('prebuild');
@@ -121,29 +135,35 @@ console.time('manifest');
await Bun.write('dist/manifest.json', JSON.stringify(createManifest()));
console.timeEnd('manifest');
-console.time('html+css');
-await makeHTML('newtab', 'src/css/newtab.xcss');
-await makeHTML('settings', 'src/css/settings.xcss');
-console.timeEnd('html+css');
+console.time('html');
+await makeHTML('newtab');
+await makeHTML('settings');
+console.timeEnd('html');
+
+console.time('css');
+await makeCSS('newtab', 'src/css/newtab.xcss');
+await makeCSS('settings', 'src/css/settings.xcss');
+console.timeEnd('css');
console.time('themes');
await makeThemes();
console.timeEnd('themes');
// New Tab & Settings apps
-console.time('build');
-const out = await Bun.build({
+console.time('build:1');
+const out1 = await Bun.build({
entrypoints: ['src/newtab.ts', 'src/settings.ts'],
outdir: 'dist',
target: 'browser',
minify: !dev,
sourcemap: dev ? 'linked' : 'none',
});
-console.timeEnd('build');
-console.log(out);
+console.timeEnd('build:1');
+console.log(out1.outputs);
+if (!out1.success) throw new AggregateError(out1.logs, 'Build failed');
// Background service worker script
-console.time('build2');
+console.time('build:2');
const out2 = await Bun.build({
entrypoints: ['src/sw.ts'],
outdir: 'dist',
@@ -151,13 +171,13 @@ const out2 = await Bun.build({
minify: !dev,
sourcemap: dev ? 'linked' : 'none',
});
-console.timeEnd('build2');
-console.log(out2);
+console.timeEnd('build:2');
+console.log(out2.outputs);
+if (!out2.success) throw new AggregateError(out2.logs, 'Build failed');
if (!dev) {
- console.time('minify');
- await minifyJS(out.outputs[0]);
- await minifyJS(out.outputs[1]);
- await minifyJS(out2.outputs[0]);
- console.timeEnd('minify');
+ console.time('minify:js');
+ await minifyJS(out1.outputs);
+ await minifyJS(out2.outputs);
+ console.timeEnd('minify:js');
}
diff --git a/bun.lockb b/bun.lockb
index e26e5438b..30fb9bf1c 100755
Binary files a/bun.lockb and b/bun.lockb differ
diff --git a/manifest.config.ts b/manifest.config.ts
index 893ee0788..4d42ccc60 100644
--- a/manifest.config.ts
+++ b/manifest.config.ts
@@ -17,6 +17,7 @@ function gitRef() {
}
// FIXME: Remove these once @types/chrome is updated.
+// https://developer.chrome.com/docs/extensions/mv3/cross-origin-isolation/
interface ManifestExtra {
/** https://developer.chrome.com/docs/extensions/mv3/manifest/cross_origin_embedder_policy/ */
cross_origin_embedder_policy?: {
@@ -35,7 +36,7 @@ export const createManifest = (
name: 'New Tab',
description: pkg.description,
homepage_url: pkg.homepage,
- version: pkg.version,
+ version: pkg.version.split('-')[0],
// shippable releases should not have a named version
version_name: debug ? gitRef() : undefined,
minimum_chrome_version: '123', // for light-dark() CSS function
@@ -68,10 +69,10 @@ export const createManifest = (
content_security_policy: {
extension_pages: [
"default-src 'none'",
+ "base-uri 'none'",
"script-src 'self'",
"style-src 'self'",
"img-src 'self'",
- "base-uri 'none'",
'', // include trailing semicolon
].join(';'),
},
diff --git a/package.json b/package.json
index eacc820fa..c36bd24ac 100644
--- a/package.json
+++ b/package.json
@@ -2,7 +2,8 @@
"name": "new-tab",
"version": "0.23.0",
"description": "⚡ A high-performance new tab page that gets you where you need to go faster.",
- "repository": "maxmilton/new-tab",
+ "repository": "github:maxmilton/new-tab",
+ "bugs": "https://github.com/maxmilton/new-tab/issues",
"homepage": "https://github.com/maxmilton/new-tab",
"author": "Max Milton ",
"license": "MIT",
@@ -19,27 +20,27 @@
"test:e2e": "playwright test"
},
"dependencies": {
- "stage1": "0.8.0-next.13"
+ "stage1": "0.8.0-next.16"
},
"devDependencies": {
"@biomejs/biome": "1.9.4",
"@ekscss/plugin-import": "0.0.15",
- "@eslint/js": "9.14.0",
- "@maxmilton/eslint-config": "0.0.7",
+ "@eslint/js": "9.17.0",
+ "@maxmilton/eslint-config": "0.0.8",
"@maxmilton/stylelint-config": "0.1.2",
"@maxmilton/test-utils": "0.0.6",
- "@playwright/test": "1.48.2",
- "@types/bun": "1.1.13",
- "@types/chrome": "0.0.280",
+ "@playwright/test": "1.49.1",
+ "@types/bun": "1.1.14",
+ "@types/chrome": "0.0.287",
"ekscss": "0.0.20",
- "eslint": "9.14.0",
- "eslint-plugin-unicorn": "56.0.0",
- "happy-dom": "15.11.0",
- "lightningcss": "1.28.1",
- "stylelint": "16.10.0",
+ "eslint": "9.17.0",
+ "eslint-plugin-unicorn": "56.0.1",
+ "happy-dom": "15.11.7",
+ "lightningcss": "1.28.2",
+ "stylelint": "16.11.0",
"stylelint-config-standard": "36.0.1",
- "terser": "5.36.0",
- "typescript": "5.6.3",
- "typescript-eslint": "8.13.0"
+ "terser": "5.37.0",
+ "typescript": "5.7.2",
+ "typescript-eslint": "8.18.0"
}
}
diff --git a/src/components/BookmarkBar.ts b/src/components/BookmarkBar.ts
index 0e474be6d..a993f0ebf 100644
--- a/src/components/BookmarkBar.ts
+++ b/src/components/BookmarkBar.ts
@@ -48,6 +48,7 @@ export const BookmarkBar = (): BookmarkBarComponent => {
let index = 0;
let node: ReturnType;
+ // Add one bookmark at a time until we overflow the max width
for (; index < len; index++) {
node = append(BookmarkNode(bookmarks[index]), root);
width += node.clientWidth;
@@ -131,3 +132,8 @@ export const BookmarkBar = (): BookmarkBarComponent => {
return root;
};
+
+// // Improve performance of lookups on DOM nodes
+// // @ts-expect-error -- add new properties to HTMLElement
+// // eslint-disable-next-line no-multi-assign
+// Element.prototype.__mouseover = Element.prototype.__mouseout = undefined;
diff --git a/src/components/SearchResult.ts b/src/components/SearchResult.ts
index 41235ca50..ef599cc11 100644
--- a/src/components/SearchResult.ts
+++ b/src/components/SearchResult.ts
@@ -94,9 +94,7 @@ export const SearchResult = (
root.$$filter = (text) =>
renderList(
rawData.filter((item) =>
- (item.title + '[' + item.url)
- .toLowerCase()
- .includes(text.toLowerCase()),
+ (item.title + item.url).toLowerCase().includes(text.toLowerCase()),
),
);
diff --git a/src/newtab.ts b/src/newtab.ts
index 3b889713a..ffdcc9867 100644
--- a/src/newtab.ts
+++ b/src/newtab.ts
@@ -1,7 +1,7 @@
// Theme loader code must run first
import './theme';
-import { append, createFragment } from 'stage1';
+import { append, fragment } from 'stage1';
import { BookmarkBar } from './components/BookmarkBar';
import { Menu } from './components/Menu';
import { Search } from './components/Search';
@@ -9,16 +9,16 @@ import { handleClick, storage } from './utils';
performance.mark('Initialise Components');
-const frag = createFragment();
+const container = fragment();
// Create Search component first because it has asynchronous calls that can
// execute while the remaining components are constructed
-append(Search(), frag);
+append(Search(), container);
// Create BookmarkBar component near last because, after an async call, it needs
// to synchronously and sequentially calculate DOM layout multiple times and
// could cause reflow in extreme situations, so paint the rest of the app first
-if (!storage.b) append(BookmarkBar(), frag);
-append(Menu(), frag);
-append(frag, document.body);
+if (!storage.b) append(BookmarkBar(), container);
+append(Menu(), container);
+append(container, document.body);
performance.measure('Initialise Components', 'Initialise Components');
diff --git a/src/settings.ts b/src/settings.ts
index 8a9dbd115..63501ce70 100644
--- a/src/settings.ts
+++ b/src/settings.ts
@@ -14,6 +14,7 @@ interface SettingsState {
type ItemIndex = [listIndex: 0 | 1, itemIndex: number];
+/** Section drag-and-drop helper functions */
interface SectionScope {
indexOf(list: 0 | 1, item: SectionOrderItem): number;
moveItem(from: ItemIndex, to: ItemIndex): void;
@@ -24,7 +25,7 @@ const DEFAULT_THEME = 'auto';
// eslint-disable-next-line unicorn/prefer-top-level-await
const themesData = fetch('themes.json').then(
- (res) => res.json() as Promise,
+ (response) => response.json() as Promise,
);
type SectionComponent = HTMLLIElement;
@@ -137,7 +138,7 @@ const meta = compile(`