@abreen/tada 1.15.8 → 1.15.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/build/bundle.ts CHANGED
@@ -19,6 +19,19 @@ import type { SiteVariables } from './types';
19
19
  import timezones from '../src/timezone/timezones.json' with { type: 'json' };
20
20
  import pkg from '../package.json' with { type: 'json' };
21
21
 
22
+ interface BunBuildPlugin {
23
+ name: string;
24
+ setup(build: PluginBuilder): void;
25
+ }
26
+
27
+ interface CoverageHooks {
28
+ createBundlePlugin(): BunBuildPlugin;
29
+ }
30
+
31
+ interface CoverageGlobal {
32
+ __tadaCoverage?: CoverageHooks;
33
+ }
34
+
22
35
  export function getBundleNaming(): string {
23
36
  const version = pkg.version.replace(/[^a-zA-Z0-9.-]/g, '-');
24
37
  return `[name].bundle.tada-${version}.[ext]`;
@@ -92,9 +105,7 @@ function createDefine(
92
105
  };
93
106
  }
94
107
 
95
- function createScssPlugin(siteVariables: SiteVariables) {
96
- const themeDir = renderThemeScss(siteVariables);
97
-
108
+ function createScssPlugin(themeDir: string) {
98
109
  return {
99
110
  name: 'scss',
100
111
  setup(build: PluginBuilder) {
@@ -108,6 +119,12 @@ function createScssPlugin(siteVariables: SiteVariables) {
108
119
  };
109
120
  }
110
121
 
122
+ function getCoverageBundlePlugin(): BunBuildPlugin | null {
123
+ return (
124
+ (globalThis as CoverageGlobal).__tadaCoverage?.createBundlePlugin() ?? null
125
+ );
126
+ }
127
+
111
128
  export async function bundle(
112
129
  siteVariables: SiteVariables,
113
130
  { mode = 'development', distDir }: { mode?: string; distDir?: string } = {},
@@ -121,30 +138,42 @@ export async function bundle(
121
138
  path.resolve(packageDir, 'src/critical.scss'),
122
139
  ];
123
140
 
124
- const result = await Bun.build({
125
- entrypoints,
126
- outdir: resolvedDistDir,
127
- naming: getBundleNaming(),
128
- minify: mode === 'production',
129
- sourcemap: isDev ? 'inline' : 'none',
130
- define: createDefine(siteVariables, isDev),
131
- external: ['*.woff2'],
132
- plugins: [createScssPlugin(siteVariables)],
133
- });
141
+ const themeDir = renderThemeScss(siteVariables);
134
142
 
135
- if (!result.success) {
136
- const messages = result.logs
137
- .filter(log => log.level === 'error')
138
- .map(log => log.message || String(log));
139
- throw new Error(`Bundle failed:\n${messages.join('\n')}`);
143
+ try {
144
+ const plugins = [createScssPlugin(themeDir)];
145
+ const coverageBundlePlugin = getCoverageBundlePlugin();
146
+ if (coverageBundlePlugin) {
147
+ plugins.push(coverageBundlePlugin);
148
+ }
149
+
150
+ const result = await Bun.build({
151
+ entrypoints,
152
+ outdir: resolvedDistDir,
153
+ naming: getBundleNaming(),
154
+ minify: mode === 'production',
155
+ sourcemap: isDev ? 'inline' : 'none',
156
+ define: createDefine(siteVariables, isDev),
157
+ external: ['*.woff2'],
158
+ plugins,
159
+ });
160
+
161
+ if (!result.success) {
162
+ const messages = result.logs
163
+ .filter(log => log.level === 'error')
164
+ .map(log => log.message || String(log));
165
+ throw new Error(`Bundle failed:\n${messages.join('\n')}`);
166
+ }
167
+
168
+ // Return the output filenames for asset tag injection
169
+ const assetFiles = result.outputs.map(output =>
170
+ toPosix(path.relative(resolvedDistDir, output.path)),
171
+ );
172
+
173
+ return assetFiles;
174
+ } finally {
175
+ fs.rmSync(themeDir, { recursive: true, force: true });
140
176
  }
141
-
142
- // Return the output filenames for asset tag injection
143
- const assetFiles = result.outputs.map(output =>
144
- toPosix(path.relative(resolvedDistDir, output.path)),
145
- );
146
-
147
- return assetFiles;
148
177
  }
149
178
 
150
- export { renderThemeScss, createDefine };
179
+ export { renderThemeScss };
package/build/colors.ts CHANGED
@@ -12,11 +12,11 @@ export const R = style(['red']);
12
12
  export const G = style(['green']);
13
13
  export const B = style(['blue']);
14
14
  export const Y = style(['yellow']);
15
- export const L = style(['blackBright']);
15
+ export const L = style(['gray']);
16
16
  export const P = style(['magenta']);
17
17
  export const I = style(['italic', 'bold']);
18
18
 
19
19
  export const Ri = style(['inverse', 'red']);
20
20
  export const Gi = style(['inverse', 'green']);
21
21
  export const Yi = style(['inverse', 'yellow']);
22
- export const Li = style(['inverse', 'blackBright']);
22
+ export const Li = style(['inverse', 'gray']);
@@ -5,7 +5,7 @@ export type SiteEnv = 'dev' | 'prod';
5
5
  export type SiteConfigName = 'site.dev' | 'site.prod';
6
6
  export type ProjectConfigName = 'nav' | 'authors';
7
7
  export type ConfigName = SiteConfigName | ProjectConfigName;
8
- export type ProjectConfigExtension = '.yaml' | '.yml' | '.json';
8
+ type ProjectConfigExtension = '.yaml' | '.yml' | '.json';
9
9
 
10
10
  export interface ResolvedProjectConfigFile<
11
11
  Name extends ConfigName = ConfigName,
@@ -18,21 +18,21 @@ export interface ResolvedProjectConfigFile<
18
18
  required: boolean;
19
19
  }
20
20
 
21
- export const CONFIG_FILE_EXTENSIONS = ['.yaml', '.yml', '.json'] as const;
22
- export const DEFAULT_CONFIG_FILE_EXTENSION = '.yaml' as const;
21
+ const CONFIG_FILE_EXTENSIONS = ['.yaml', '.yml', '.json'] as const;
22
+ const DEFAULT_CONFIG_FILE_EXTENSION = '.yaml' as const;
23
23
 
24
- export const SITE_CONFIG_BASE_NAMES = {
24
+ const SITE_CONFIG_BASE_NAMES = {
25
25
  dev: 'site.dev',
26
26
  prod: 'site.prod',
27
27
  } as const satisfies Record<SiteEnv, SiteConfigName>;
28
28
 
29
- export const PROJECT_CONFIG_BASE_NAMES = {
29
+ const PROJECT_CONFIG_BASE_NAMES = {
30
30
  nav: 'nav',
31
31
  authors: 'authors',
32
32
  } as const satisfies Record<ProjectConfigName, ProjectConfigName>;
33
33
 
34
- export const REQUIRED_PROJECT_CONFIG_NAMES = ['nav'] as const;
35
- export const OPTIONAL_PROJECT_CONFIG_NAMES = ['authors'] as const;
34
+ const REQUIRED_PROJECT_CONFIG_NAMES = ['nav'] as const;
35
+ const OPTIONAL_PROJECT_CONFIG_NAMES = ['authors'] as const;
36
36
  export const PROJECT_CONFIG_NAMES = [
37
37
  ...REQUIRED_PROJECT_CONFIG_NAMES,
38
38
  ...OPTIONAL_PROJECT_CONFIG_NAMES,
@@ -66,7 +66,7 @@ export function getProjectConfigBaseName(
66
66
  return PROJECT_CONFIG_BASE_NAMES[name];
67
67
  }
68
68
 
69
- export function getSupportedConfigFileNames(baseName: string): string[] {
69
+ function getSupportedConfigFileNames(baseName: string): string[] {
70
70
  return CONFIG_FILE_EXTENSIONS.map(ext => `${baseName}${ext}`);
71
71
  }
72
72
 
@@ -83,21 +83,17 @@ export function getSupportedConfigFileNamesText(baseName: string): string {
83
83
  return joinWithOr(getSupportedConfigFileNames(baseName));
84
84
  }
85
85
 
86
- export function getDefaultConfigFileName(baseName: string): string {
86
+ function getDefaultConfigFileName(baseName: string): string {
87
87
  return `${baseName}${DEFAULT_CONFIG_FILE_EXTENSION}`;
88
88
  }
89
89
 
90
- export function getDefaultConfigFilePath(
90
+ function getDefaultConfigFilePath(
91
91
  projectDir: string,
92
92
  baseName: string,
93
93
  ): string {
94
94
  return path.join(projectDir, getDefaultConfigFileName(baseName));
95
95
  }
96
96
 
97
- export function getDefaultSiteConfigFileName(env: SiteEnv): string {
98
- return getDefaultConfigFileName(getSiteConfigBaseName(env));
99
- }
100
-
101
97
  export function getDefaultSiteConfigPath(
102
98
  projectDir: string,
103
99
  env: SiteEnv,
@@ -105,12 +101,6 @@ export function getDefaultSiteConfigPath(
105
101
  return getDefaultConfigFilePath(projectDir, getSiteConfigBaseName(env));
106
102
  }
107
103
 
108
- export function getDefaultProjectConfigFileName(
109
- name: ProjectConfigName,
110
- ): string {
111
- return getDefaultConfigFileName(getProjectConfigBaseName(name));
112
- }
113
-
114
104
  export function getDefaultProjectConfigFilePath(
115
105
  projectDir: string,
116
106
  name: ProjectConfigName,
@@ -118,7 +108,7 @@ export function getDefaultProjectConfigFilePath(
118
108
  return getDefaultConfigFilePath(projectDir, getProjectConfigBaseName(name));
119
109
  }
120
110
 
121
- export function resolveConfigFile<Name extends ConfigName = ConfigName>(
111
+ function resolveConfigFile<Name extends ConfigName = ConfigName>(
122
112
  projectDir: string,
123
113
  {
124
114
  name,
@@ -115,7 +115,7 @@ function interpolateConfigValue(
115
115
  return value;
116
116
  }
117
117
 
118
- export function loadResolvedConfigFile<
118
+ function loadResolvedConfigFile<
119
119
  T = unknown,
120
120
  Name extends ConfigName = ConfigName,
121
121
  >(
@@ -175,6 +175,7 @@ export default function externalLinks(
175
175
  const newClassAttr = classAttr ? `${classAttr} external` : 'external';
176
176
  token.attrSet('class', newClassAttr);
177
177
  token.attrSet('target', '_blank');
178
+ token.attrSet('rel', 'noopener noreferrer');
178
179
  log.debug`${href} -> "${newClassAttr}"`;
179
180
 
180
181
  const closeIdx = findMatchingClose(children, i);
@@ -158,5 +158,3 @@ export async function generateFavicons(
158
158
  );
159
159
  fs.writeFileSync(path.join(distDir, `${filenameBase}.ico`), icoBuffer);
160
160
  }
161
-
162
- export { createFaviconSvg };
@@ -67,5 +67,3 @@ export function generateWebAppManifest(
67
67
  JSON.stringify(manifest),
68
68
  );
69
69
  }
70
-
71
- export { createManifest };
package/build/pdf-text.ts CHANGED
@@ -8,12 +8,12 @@ interface MutoolOutput {
8
8
  stderr: string;
9
9
  }
10
10
 
11
- export interface PdfPage {
11
+ interface PdfPage {
12
12
  pageNumber: number;
13
13
  content: string;
14
14
  }
15
15
 
16
- export interface PdfExtractResult {
16
+ interface PdfExtractResult {
17
17
  pages: PdfPage[];
18
18
  hasExtractedText: boolean;
19
19
  }
@@ -37,7 +37,7 @@ function resolveTargetToAssetTarget(target: string): string | null {
37
37
  return path.posix.extname(normalizedTarget) ? normalizedTarget : null;
38
38
  }
39
39
 
40
- export function collectOutgoingHtmlAssetPaths(
40
+ function collectOutgoingHtmlAssetPaths(
41
41
  analysis: HtmlOutputAnalysis,
42
42
  knownHtmlAssetPaths: Set<string>,
43
43
  ): string[] {
@@ -1,6 +1,6 @@
1
1
  import { bundledLanguages } from 'shiki';
2
2
  import type { BundledLanguage } from 'shiki';
3
- import { getSiteConfigBaseName, type SiteEnv } from './config-files';
3
+ import type { SiteEnv } from './config-files';
4
4
  import { loadSiteConfig } from './config-loader';
5
5
  import { compile as compileJsonSchema, doValidation } from './json-schema';
6
6
  import { getProjectDir } from './utils/paths';
@@ -163,8 +163,6 @@ export function getProdSiteVariables(): SiteVariables {
163
163
  return getSiteVariables('prod');
164
164
  }
165
165
 
166
- export { getSiteConfigBaseName };
167
-
168
166
  function getValidatedSiteConfigRoot(
169
167
  value: unknown,
170
168
  fileName: string,
@@ -23,6 +23,7 @@ interface TemplateGlobals {
23
23
  readableDate: (date: string | Date | null | undefined) => string;
24
24
  classNames: (obj: Record<string, unknown>) => string;
25
25
  cx: (obj: Record<string, unknown>) => string;
26
+ encodeAuthoredUrl: (value: string) => string;
26
27
  renderTimeZoneChooser: () => string;
27
28
  }
28
29
 
@@ -51,10 +52,16 @@ export default function createTemplateGlobals(
51
52
  readableDate,
52
53
  classNames,
53
54
  cx: classNames,
55
+ encodeAuthoredUrl,
54
56
  renderTimeZoneChooser,
55
57
  };
56
58
  }
57
59
 
60
+ // Encode raw authored URL text, but preserve percent escapes authors already wrote.
61
+ export function encodeAuthoredUrl(value: string): string {
62
+ return encodeURI(value).replace(/%25([0-9a-fA-F]{2})/g, '%$1');
63
+ }
64
+
58
65
  function isoDate(str: string | null | undefined): string | null {
59
66
  if (str == null || str == '') {
60
67
  return null;
@@ -144,4 +144,4 @@ function compileAndSetValidator(
144
144
  validators[name] = compileJsonSchema(schema);
145
145
  }
146
146
 
147
- export { getHtmlTemplatesDir, getProjectConfigDir };
147
+ export { getProjectConfigDir };
package/build/util.ts CHANGED
@@ -1,37 +1,16 @@
1
- export {
2
- addGeneratedRouteAliases,
3
- getProcessedExts,
4
- getSourceOutputPaths,
5
- getSourceTargetPaths,
6
- isBuildContentSource,
7
- } from './source-model';
8
- export {
9
- getBuildContentFiles,
10
- getContentFiles,
11
- getContentOutputRelPaths,
12
- getContentSourceOutputRelPaths,
13
- getFilesByExtensions,
14
- getValidInternalTargets,
15
- shouldSkipContentFile,
16
- } from './utils/content-files';
17
- export { extensionIsMarkdown, isPartial } from './utils/file-types';
18
- export { createMarkdown } from './utils/markdown';
1
+ export { getFilesByExtensions } from './utils/content-files';
19
2
  export {
20
3
  createApplyBasePath,
21
4
  getContentDir,
22
5
  getDistDir,
23
6
  getPackageDir,
24
- getProjectDir,
25
7
  getPublicDir,
26
8
  normalizeOutputPath,
27
9
  toPosix,
28
10
  } from './utils/paths';
29
11
  export {
30
- injectAssetTags,
31
- preparePageTemplateHtml,
32
12
  renderCodePageAsset,
33
13
  renderCopiedContentAsset,
34
14
  renderLiterateJavaPageAsset,
35
15
  renderPlainTextPageAsset,
36
16
  } from './utils/render';
37
- export { parseFrontMatter } from './utils/front-matter';
@@ -32,7 +32,7 @@ export function runJavac(
32
32
  }
33
33
  }
34
34
 
35
- export function getExecError(err: unknown): string {
35
+ function getExecError(err: unknown): string {
36
36
  const execErr = err as { stderr?: Buffer; message: string };
37
37
  return execErr.stderr ? execErr.stderr.toString() : execErr.message;
38
38
  }
@@ -23,7 +23,7 @@ interface MarkdownPartialsEnv {
23
23
  [PARTIAL_CONTEXT_STACK_KEY]?: MarkdownPartialContext[];
24
24
  }
25
25
 
26
- export interface MarkdownPartialsOptions {
26
+ interface MarkdownPartialsOptions {
27
27
  filePath: string;
28
28
  templateParams: Record<string, unknown>;
29
29
  dependencyCollector?: RenderDependencyCollector;
@@ -64,10 +64,7 @@ export async function initHighlighter(
64
64
  }
65
65
  }
66
66
 
67
- export function getHighlighter(): HighlighterGeneric<
68
- BundledLanguage,
69
- BundledTheme
70
- > {
67
+ function getHighlighter(): HighlighterGeneric<BundledLanguage, BundledTheme> {
71
68
  if (!highlighter) {
72
69
  throw new Error('Shiki highlighter not initialized');
73
70
  }
@@ -33,7 +33,7 @@ const MIN_VALUE_WIDTH = 40;
33
33
  const MIN_ARRAY_CELL_WIDTH = 48;
34
34
 
35
35
  /** Information collected about a heap object across all trace steps. */
36
- export interface ObjectInfo {
36
+ interface ObjectInfo {
37
37
  id: string;
38
38
  type: string;
39
39
  firstStep: number;
@@ -61,7 +61,7 @@ export interface ObjectInfo {
61
61
  }
62
62
 
63
63
  /** A tree node used to build the hierarchy for d3-flextree. */
64
- export interface TreeNode {
64
+ interface TreeNode {
65
65
  id: string;
66
66
  width: number;
67
67
  height: number;
@@ -1,4 +1,5 @@
1
1
  import path from 'path';
2
+ import { encodeAuthoredUrl } from './template-globals';
2
3
  import { normalizeOutputPath } from './utils/paths';
3
4
 
4
5
  interface NavLink {
@@ -24,6 +25,51 @@ function requireRootRelativePath(
24
25
  return `${errorPrefix} must start with "/": "${value}"`;
25
26
  }
26
27
 
28
+ const SPECIAL_URL_SCHEMES = new Set([
29
+ 'file',
30
+ 'ftp',
31
+ 'http',
32
+ 'https',
33
+ 'ws',
34
+ 'wss',
35
+ ]);
36
+
37
+ function isValidAbsoluteAuthorUrl(value: string): boolean {
38
+ if (value.trim() !== value) {
39
+ return false;
40
+ }
41
+
42
+ const schemeMatch = value.match(/^([a-zA-Z][a-zA-Z0-9+.-]*):/);
43
+ if (!schemeMatch) {
44
+ return false;
45
+ }
46
+
47
+ const scheme = schemeMatch[1].toLowerCase();
48
+ if (
49
+ SPECIAL_URL_SCHEMES.has(scheme) &&
50
+ !value.startsWith(`${schemeMatch[1]}://`)
51
+ ) {
52
+ return false;
53
+ }
54
+
55
+ try {
56
+ new URL(encodeAuthoredUrl(value));
57
+ return true;
58
+ } catch {
59
+ return false;
60
+ }
61
+ }
62
+
63
+ function getInternalHrefTarget(href: string): string {
64
+ const { pathname } = splitHref(href);
65
+ const normalized = normalizeOutputPath(pathname);
66
+ try {
67
+ return normalizeOutputPath(decodeURIComponent(normalized));
68
+ } catch {
69
+ return normalized;
70
+ }
71
+ }
72
+
27
73
  export function validateNavLinks(
28
74
  navData: unknown,
29
75
  validTargets: Set<string>,
@@ -97,6 +143,10 @@ export function validateAuthorLinks(
97
143
  }
98
144
 
99
145
  if (author.url) {
146
+ if (isValidAbsoluteAuthorUrl(author.url)) {
147
+ continue;
148
+ }
149
+
100
150
  const urlRootRelativeError = requireRootRelativePath(
101
151
  author.url,
102
152
  `${fileName}: url for "${key}"`,
@@ -104,7 +154,7 @@ export function validateAuthorLinks(
104
154
  if (urlRootRelativeError) {
105
155
  errors.push(urlRootRelativeError);
106
156
  } else {
107
- const urlPath = normalizeOutputPath(author.url);
157
+ const urlPath = getInternalHrefTarget(author.url);
108
158
  if (!validTargets.has(urlPath)) {
109
159
  errors.push(`${fileName}: broken url for "${key}": "${author.url}"`);
110
160
  }
@@ -6,7 +6,7 @@ import {
6
6
  } from '../config-files';
7
7
  import { getProjectConfigDir } from '../templates';
8
8
 
9
- export type WatchConfigPathKind = 'site' | 'nav' | 'authors';
9
+ type WatchConfigPathKind = 'site' | 'nav' | 'authors';
10
10
 
11
11
  function getConfigPathEntries(): Array<{
12
12
  kind: WatchConfigPathKind;
@@ -14,7 +14,7 @@ export interface TadaIncrementalWatchPlan {
14
14
  publicToRemove: Set<string>;
15
15
  }
16
16
 
17
- export type TadaWatchPlan = { kind: 'full' } | TadaIncrementalWatchPlan;
17
+ type TadaWatchPlan = { kind: 'full' } | TadaIncrementalWatchPlan;
18
18
 
19
19
  export function diffAuthorKeys(previous: unknown, next: unknown): Set<string> {
20
20
  const previousMap =
@@ -1,11 +1,8 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
- import { normalizeOutputPath } from '../util';
4
3
  import {
5
4
  type TadaProjectScan,
6
5
  assertNoOutputPathConflicts,
7
- scanProject,
8
- updateProjectScan,
9
6
  } from '../source-model';
10
7
  import type { Asset, HtmlOutputAnalysis, SiteVariables } from '../types';
11
8
  import type { TadaBuildMeta } from './compiler-types';
@@ -60,7 +57,7 @@ function buildReverseMap(
60
57
  return reverse;
61
58
  }
62
59
 
63
- export function collectOutputOwners(snapshot: {
60
+ function collectOutputOwners(snapshot: {
64
61
  contentRecords: Map<string, TadaSourceRecord>;
65
62
  publicRecords: Map<string, TadaSourceRecord>;
66
63
  }): Map<string, TadaOutputOwner> {
@@ -164,7 +161,7 @@ export function collectSourceHtmlAnalysis(
164
161
  return htmlAnalysisByOutputPath;
165
162
  }
166
163
 
167
- export function collectHtmlAssetsByPath(
164
+ function collectHtmlAssetsByPath(
168
165
  contentRecords: Map<string, TadaSourceRecord>,
169
166
  ): Map<string, string> {
170
167
  const htmlAssetsByPath = new Map<string, string>();
@@ -178,7 +175,7 @@ export function collectHtmlAssetsByPath(
178
175
  return htmlAssetsByPath;
179
176
  }
180
177
 
181
- export function collectHtmlAnalysisByPath(
178
+ function collectHtmlAnalysisByPath(
182
179
  contentRecords: Map<string, TadaSourceRecord>,
183
180
  ): Map<string, HtmlOutputAnalysis> {
184
181
  const htmlAnalysisByPath = new Map<string, HtmlOutputAnalysis>();
@@ -201,13 +198,4 @@ export function createBuildMeta(snapshot: TadaSnapshot): TadaBuildMeta {
201
198
  };
202
199
  }
203
200
 
204
- export function normalizeInternalTarget(target: string): string {
205
- return normalizeOutputPath(target);
206
- }
207
-
208
- export {
209
- assertNoOutputPathConflicts,
210
- scanProject,
211
- updateProjectScan,
212
- type TadaProjectScan,
213
- };
201
+ export { assertNoOutputPathConflicts, type TadaProjectScan };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abreen/tada",
3
- "version": "1.15.8",
3
+ "version": "1.15.10",
4
4
  "type": "module",
5
5
  "description": "A static site generator",
6
6
  "license": "MIT",
@@ -33,7 +33,7 @@
33
33
  "check-deps": "bun run scripts/check-deps.ts",
34
34
  "init-example": "bun run bin/tada.ts init example --no-interactive",
35
35
  "iew": "bun run scripts/init-example-watch.ts",
36
- "lint": "eslint src/ build/ bin/",
36
+ "lint": "eslint src/ build/ bin/ templates/*.test.ts",
37
37
  "lint:sass": "bun run scripts/lint-sass.ts",
38
38
  "typecheck": "tsc --noEmit",
39
39
  "format": "prettier --write \"**/*.{ts,js,scss,json}\"",
@@ -61,7 +61,6 @@
61
61
  "chokidar": "^4.0.3",
62
62
  "culori": "^4.0.2",
63
63
  "d3-flextree": "^2.1.2",
64
- "d3-hierarchy": "^3.1.2",
65
64
  "fontkit": "^2.0.4",
66
65
  "front-matter": "^4.0.2",
67
66
  "java-parser": "^3.0.1",
@@ -101,7 +100,6 @@
101
100
  "@types/bun": "^1.3.12",
102
101
  "@types/culori": "^4.0.1",
103
102
  "@types/d3-flextree": "^2.1.4",
104
- "@types/d3-hierarchy": "^3.1.7",
105
103
  "@types/fontkit": "^2.0.9",
106
104
  "@types/istanbul-lib-coverage": "^2.0.6",
107
105
  "@types/istanbul-lib-instrument": "^1.7.8",
@@ -109,12 +107,14 @@
109
107
  "@types/istanbul-reports": "^3.0.4",
110
108
  "@types/lodash": "^4.17.24",
111
109
  "@types/markdown-it-container": "^4.0.0",
110
+ "@types/node": "^25.6.0",
112
111
  "eslint": "^10.2.1",
113
112
  "husky": "^9.1.7",
114
113
  "istanbul-lib-coverage": "^3.2.2",
115
114
  "istanbul-lib-instrument": "^6.0.3",
116
115
  "istanbul-lib-report": "^3.0.1",
117
116
  "istanbul-reports": "^3.2.0",
117
+ "knip": "^6.11.0",
118
118
  "lint-staged": "^16.4.0",
119
119
  "postcss-scss": "^4.0.9",
120
120
  "prettier": "^3.8.3",
@@ -122,7 +122,7 @@
122
122
  "stylelint": "^17.8.0",
123
123
  "stylelint-config-recess-order": "^7.7.0",
124
124
  "stylelint-config-standard-scss": "^17.0.0",
125
- "typescript": "^5.9.3",
125
+ "typescript": "^6.0.3",
126
126
  "typescript-eslint": "^8.58.2"
127
127
  }
128
128
  }
@@ -6,7 +6,7 @@
6
6
  "properties": {
7
7
  "name": { "type": "string" },
8
8
  "avatar": { "type": "string", "pattern": "^\\/[-a-zA-Z0-9_./#]*$" },
9
- "url": { "type": "string", "pattern": "^\\/[-a-zA-Z0-9_./#]*$" }
9
+ "url": { "type": "string", "minLength": 1 }
10
10
  },
11
11
  "additionalProperties": false
12
12
  }
@@ -45,7 +45,14 @@ export function isEligibleLink(
45
45
  return false;
46
46
  }
47
47
 
48
- if (!url.pathname.startsWith(basePath)) {
48
+ const normalizedBasePath = basePath.endsWith('/')
49
+ ? basePath.slice(0, -1) || '/'
50
+ : basePath;
51
+ if (
52
+ normalizedBasePath !== '/' &&
53
+ url.pathname !== normalizedBasePath &&
54
+ !url.pathname.startsWith(`${normalizedBasePath}/`)
55
+ ) {
49
56
  return false;
50
57
  }
51
58