@abreen/tada 1.7.0 → 1.7.1
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/bin/tada.ts +4 -11
- package/build/apply-base-path-plugin.ts +11 -19
- package/build/colors.ts +19 -22
- package/build/content-watch.ts +3 -16
- package/build/copy.ts +42 -20
- package/build/deflist-id-plugin.ts +3 -16
- package/build/features.ts +3 -6
- package/build/generate-content-assets.ts +9 -5
- package/build/generate-favicon.ts +5 -15
- package/build/generate-web-app-manifest.ts +19 -57
- package/build/globals.ts +19 -24
- package/build/log.ts +30 -46
- package/build/pagefind.ts +2 -2
- package/build/reachability.ts +1 -1
- package/build/serve.ts +1 -5
- package/build/site-variables.ts +2 -2
- package/build/templates.ts +2 -2
- package/build/text-to-id.ts +6 -0
- package/build/types.d.ts +1 -7
- package/build/utils/code.ts +3 -7
- package/build/utils/link.ts +8 -0
- package/build/utils/literate-java.ts +42 -33
- package/build/utils/markdown.ts +13 -17
- package/build/utils/paths.ts +3 -11
- package/build/utils/render.ts +25 -34
- package/build/utils/trace-layout.ts +308 -246
- package/build/utils/trace-svg.ts +1 -6
- package/build/utils/trace.ts +22 -47
- package/build/validate-internal-links-plugin.ts +2 -9
- package/build/watch-reload-client.ts +4 -6
- package/build/watch.ts +67 -116
- package/package.json +2 -2
- package/src/_base.scss +1 -0
- package/src/literate/style.scss +1 -0
- package/src/print/style.scss +16 -1
- package/templates/_theme.scss +0 -1
package/bin/tada.ts
CHANGED
|
@@ -607,21 +607,14 @@ try {
|
|
|
607
607
|
await initCommand(process.argv.slice(3));
|
|
608
608
|
break;
|
|
609
609
|
|
|
610
|
-
case 'dev':
|
|
611
|
-
requireSiteConfig('dev');
|
|
612
|
-
const { runPipeline } = await import(
|
|
613
|
-
path.join(packageDir, 'build/pipeline.ts')
|
|
614
|
-
);
|
|
615
|
-
await runPipeline('development');
|
|
616
|
-
break;
|
|
617
|
-
}
|
|
618
|
-
|
|
610
|
+
case 'dev':
|
|
619
611
|
case 'prod': {
|
|
620
|
-
|
|
612
|
+
const mode = command === 'dev' ? 'development' : 'production';
|
|
613
|
+
requireSiteConfig(command);
|
|
621
614
|
const { runPipeline } = await import(
|
|
622
615
|
path.join(packageDir, 'build/pipeline.ts')
|
|
623
616
|
);
|
|
624
|
-
await runPipeline(
|
|
617
|
+
await runPipeline(mode);
|
|
625
618
|
break;
|
|
626
619
|
}
|
|
627
620
|
|
|
@@ -3,6 +3,7 @@ import type MarkdownIt from 'markdown-it';
|
|
|
3
3
|
import type Token from 'markdown-it/lib/token.mjs';
|
|
4
4
|
import { createApplyBasePath } from './utils/paths';
|
|
5
5
|
import { isFeatureEnabled } from './features';
|
|
6
|
+
import { isInternalLink } from './utils/link';
|
|
6
7
|
import { makeLogger } from './log';
|
|
7
8
|
import type { SiteVariables } from './types';
|
|
8
9
|
|
|
@@ -26,6 +27,11 @@ export default function applyBasePathPlugin(
|
|
|
26
27
|
const validTargets = pluginOptions.validTargets;
|
|
27
28
|
|
|
28
29
|
function rewriteInternalHref(href: string): string {
|
|
30
|
+
const resolveAgainstSource = (p: string): string =>
|
|
31
|
+
p.startsWith('/')
|
|
32
|
+
? p
|
|
33
|
+
: path.posix.join(path.posix.dirname(sourceUrlPath!), p);
|
|
34
|
+
|
|
29
35
|
const match = href.match(/^([^?#]*)(.*)$/);
|
|
30
36
|
const pathname = match ? match[1] : href;
|
|
31
37
|
const suffix = match ? match[2] : '';
|
|
@@ -35,12 +41,7 @@ export default function applyBasePathPlugin(
|
|
|
35
41
|
for (const ext of Object.keys(siteVariables.codeLanguages ?? {})) {
|
|
36
42
|
if (modifiedPath.endsWith(`.${ext}`)) {
|
|
37
43
|
if (literateJavaOutputPaths && sourceUrlPath) {
|
|
38
|
-
const resolved = modifiedPath
|
|
39
|
-
? modifiedPath
|
|
40
|
-
: path.posix.join(
|
|
41
|
-
path.posix.dirname(sourceUrlPath),
|
|
42
|
-
modifiedPath,
|
|
43
|
-
);
|
|
44
|
+
const resolved = resolveAgainstSource(modifiedPath);
|
|
44
45
|
if (literateJavaOutputPaths.has(resolved)) {
|
|
45
46
|
break;
|
|
46
47
|
}
|
|
@@ -48,12 +49,7 @@ export default function applyBasePathPlugin(
|
|
|
48
49
|
// Only rewrite if the .html version exists. Public files with
|
|
49
50
|
// code extensions are copied as-is and have no .html page.
|
|
50
51
|
if (validTargets && sourceUrlPath) {
|
|
51
|
-
const resolved = modifiedPath
|
|
52
|
-
? modifiedPath
|
|
53
|
-
: path.posix.join(
|
|
54
|
-
path.posix.dirname(sourceUrlPath),
|
|
55
|
-
modifiedPath,
|
|
56
|
-
);
|
|
52
|
+
const resolved = resolveAgainstSource(modifiedPath);
|
|
57
53
|
if (!validTargets.has(`${resolved}.html`)) {
|
|
58
54
|
break;
|
|
59
55
|
}
|
|
@@ -79,11 +75,7 @@ export default function applyBasePathPlugin(
|
|
|
79
75
|
const afterApply = applyBasePath(modifiedHref);
|
|
80
76
|
log.debug`Applying base path: ${href} -> ${afterApply}`;
|
|
81
77
|
token.attrSet('href', afterApply);
|
|
82
|
-
} else if (
|
|
83
|
-
!href.startsWith('#') &&
|
|
84
|
-
!href.startsWith('//') &&
|
|
85
|
-
!/^[a-zA-Z][a-zA-Z\d+.-]*:/.test(href)
|
|
86
|
-
) {
|
|
78
|
+
} else if (isInternalLink(href)) {
|
|
87
79
|
// Relative link: only rewrite code extensions, don't apply base path
|
|
88
80
|
const modifiedHref = rewriteInternalHref(href);
|
|
89
81
|
if (modifiedHref !== href) {
|
|
@@ -120,10 +112,10 @@ export default function applyBasePathPlugin(
|
|
|
120
112
|
);
|
|
121
113
|
}
|
|
122
114
|
|
|
123
|
-
token.children?.
|
|
115
|
+
token.children?.forEach(checkAndApplyBasePath);
|
|
124
116
|
}
|
|
125
117
|
|
|
126
118
|
md.core.ruler.push('apply_base_path', state => {
|
|
127
|
-
state.tokens.
|
|
119
|
+
state.tokens.forEach(checkAndApplyBasePath);
|
|
128
120
|
});
|
|
129
121
|
}
|
package/build/colors.ts
CHANGED
|
@@ -1,25 +1,22 @@
|
|
|
1
1
|
import util from 'util';
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
export const G = (str: TemplateStringsArray, ...args: unknown[]): string =>
|
|
6
|
-
util.styleText(['green'], String.raw(str, ...args));
|
|
7
|
-
export const B = (str: TemplateStringsArray, ...args: unknown[]): string =>
|
|
8
|
-
util.styleText(['blue'], String.raw(str, ...args));
|
|
9
|
-
export const Y = (str: TemplateStringsArray, ...args: unknown[]): string =>
|
|
10
|
-
util.styleText(['yellow'], String.raw(str, ...args));
|
|
11
|
-
export const L = (str: TemplateStringsArray, ...args: unknown[]): string =>
|
|
12
|
-
util.styleText(['blackBright'], String.raw(str, ...args));
|
|
13
|
-
export const P = (str: TemplateStringsArray, ...args: unknown[]): string =>
|
|
14
|
-
util.styleText(['magenta'], String.raw(str, ...args));
|
|
15
|
-
export const I = (str: TemplateStringsArray, ...args: unknown[]): string =>
|
|
16
|
-
util.styleText(['italic', 'bold'], String.raw(str, ...args));
|
|
3
|
+
type StyleFormat = Parameters<typeof util.styleText>[0];
|
|
4
|
+
type TagFn = (str: TemplateStringsArray, ...args: unknown[]) => string;
|
|
17
5
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
export const
|
|
25
|
-
|
|
6
|
+
const style =
|
|
7
|
+
(styles: StyleFormat): TagFn =>
|
|
8
|
+
(str, ...args) =>
|
|
9
|
+
util.styleText(styles, String.raw(str, ...args));
|
|
10
|
+
|
|
11
|
+
export const R = style(['red']);
|
|
12
|
+
export const G = style(['green']);
|
|
13
|
+
export const B = style(['blue']);
|
|
14
|
+
export const Y = style(['yellow']);
|
|
15
|
+
export const L = style(['blackBright']);
|
|
16
|
+
export const P = style(['magenta']);
|
|
17
|
+
export const I = style(['italic', 'bold']);
|
|
18
|
+
|
|
19
|
+
export const Ri = style(['inverse', 'red']);
|
|
20
|
+
export const Gi = style(['inverse', 'green']);
|
|
21
|
+
export const Yi = style(['inverse', 'yellow']);
|
|
22
|
+
export const Li = style(['inverse', 'blackBright']);
|
package/build/content-watch.ts
CHANGED
|
@@ -2,7 +2,6 @@ import path from 'path';
|
|
|
2
2
|
import type { SiteVariables } from './types';
|
|
3
3
|
import { getContentDir, getBuildContentFiles, isPartial } from './util';
|
|
4
4
|
import { compileTemplates, getJsonDataDir, JSON_DATA_FILES } from './templates';
|
|
5
|
-
import { getProjectDir } from './utils/paths';
|
|
6
5
|
import { B } from './colors';
|
|
7
6
|
import { makeLogger } from './log';
|
|
8
7
|
|
|
@@ -18,18 +17,16 @@ interface ChangeDetectionResult {
|
|
|
18
17
|
|
|
19
18
|
export class ContentChangeDetector {
|
|
20
19
|
private siteVariables: SiteVariables;
|
|
21
|
-
private siteConfigPath: string;
|
|
22
20
|
private lastSig: string | null;
|
|
23
21
|
|
|
24
22
|
constructor(siteVariables: SiteVariables) {
|
|
25
23
|
this.siteVariables = siteVariables;
|
|
26
|
-
this.siteConfigPath = path.resolve(getProjectDir(), 'site.dev.json');
|
|
27
24
|
this.lastSig = null;
|
|
28
25
|
}
|
|
29
26
|
|
|
30
27
|
detectChanges(modifiedFiles: Iterable<string>): ChangeDetectionResult {
|
|
31
28
|
const resolvedFiles = new Set(
|
|
32
|
-
[...
|
|
29
|
+
[...modifiedFiles].map(filePath => path.resolve(filePath)),
|
|
33
30
|
);
|
|
34
31
|
|
|
35
32
|
const jsonDataDir = getJsonDataDir();
|
|
@@ -75,16 +72,14 @@ export class ContentChangeDetector {
|
|
|
75
72
|
jsonDataPaths.includes(filePath),
|
|
76
73
|
);
|
|
77
74
|
const jsonDataChanged = changedJsonDataPaths.length > 0;
|
|
78
|
-
const
|
|
75
|
+
const changedPartials = [...changedContentFiles].filter(f => isPartial(f));
|
|
76
|
+
const partialsChanged = changedPartials.length > 0;
|
|
79
77
|
|
|
80
78
|
for (const filePath of changedJsonDataPaths) {
|
|
81
79
|
log.event`${B`${path.basename(filePath)}`} changed, rebuilding`;
|
|
82
80
|
}
|
|
83
81
|
|
|
84
82
|
if (partialsChanged) {
|
|
85
|
-
const changedPartials = [...changedContentFiles].filter(f =>
|
|
86
|
-
isPartial(f),
|
|
87
|
-
);
|
|
88
83
|
for (const filePath of changedPartials) {
|
|
89
84
|
log.event`Partial ${B`${path.basename(filePath)}`} changed, rebuilding`;
|
|
90
85
|
}
|
|
@@ -97,14 +92,6 @@ export class ContentChangeDetector {
|
|
|
97
92
|
log.event`${B`${path.basename(filePath)}`} changed, re-running traces`;
|
|
98
93
|
}
|
|
99
94
|
|
|
100
|
-
// Check if site config changed
|
|
101
|
-
const siteConfigChanged = resolvedFiles.has(
|
|
102
|
-
path.resolve(this.siteConfigPath),
|
|
103
|
-
);
|
|
104
|
-
if (siteConfigChanged) {
|
|
105
|
-
needsRestart = true;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
95
|
return {
|
|
109
96
|
templateError: null,
|
|
110
97
|
needsRestart,
|
package/build/copy.ts
CHANGED
|
@@ -15,8 +15,11 @@ function collectFiles(dir: string): CollectedFile[] {
|
|
|
15
15
|
let entries: fs.Dirent[];
|
|
16
16
|
try {
|
|
17
17
|
entries = fs.readdirSync(dir, { withFileTypes: true, recursive: true });
|
|
18
|
-
} catch {
|
|
19
|
-
|
|
18
|
+
} catch (err: unknown) {
|
|
19
|
+
if ((err as NodeJS.ErrnoException).code === 'ENOENT') {
|
|
20
|
+
return [];
|
|
21
|
+
}
|
|
22
|
+
throw err;
|
|
20
23
|
}
|
|
21
24
|
return entries
|
|
22
25
|
.filter(entry => entry.isFile())
|
|
@@ -59,7 +62,7 @@ export function copyContentAssets(
|
|
|
59
62
|
continue;
|
|
60
63
|
}
|
|
61
64
|
contentAssetRelPaths.add(rel);
|
|
62
|
-
if (publicRelPaths
|
|
65
|
+
if (publicRelPaths.has(rel)) {
|
|
63
66
|
conflicts.push(rel);
|
|
64
67
|
}
|
|
65
68
|
const dest = path.join(distDir, rel);
|
|
@@ -78,21 +81,41 @@ export function copyContentAssets(
|
|
|
78
81
|
return contentAssetRelPaths;
|
|
79
82
|
}
|
|
80
83
|
|
|
81
|
-
|
|
82
|
-
|
|
84
|
+
function copySingleFile(
|
|
85
|
+
sourceDir: string,
|
|
83
86
|
distDir: string,
|
|
84
87
|
filePath: string,
|
|
85
|
-
|
|
88
|
+
sourceLabel: string,
|
|
89
|
+
conflictLabel: string,
|
|
90
|
+
conflictSet?: Set<string>,
|
|
86
91
|
): void {
|
|
87
|
-
const rel = toPosix(path.relative(
|
|
88
|
-
if (
|
|
89
|
-
log.error
|
|
90
|
-
throw new Error(
|
|
92
|
+
const rel = toPosix(path.relative(sourceDir, filePath));
|
|
93
|
+
if (conflictSet?.has(rel)) {
|
|
94
|
+
log.error`${sourceLabel}/${B`${rel}`} conflicts with ${conflictLabel}/${B`${rel}`}`;
|
|
95
|
+
throw new Error(
|
|
96
|
+
`${sourceLabel}/${rel} and ${conflictLabel}/${rel} have the same path`,
|
|
97
|
+
);
|
|
91
98
|
}
|
|
92
99
|
const dest = path.join(distDir, rel);
|
|
93
100
|
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
94
101
|
fs.copyFileSync(filePath, dest);
|
|
95
|
-
log.info`Copying
|
|
102
|
+
log.info`Copying ${sourceLabel} file ${B`${rel}`}`;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function copyPublicFile(
|
|
106
|
+
publicDir: string,
|
|
107
|
+
distDir: string,
|
|
108
|
+
filePath: string,
|
|
109
|
+
contentAssetRelPaths?: Set<string>,
|
|
110
|
+
): void {
|
|
111
|
+
copySingleFile(
|
|
112
|
+
publicDir,
|
|
113
|
+
distDir,
|
|
114
|
+
filePath,
|
|
115
|
+
'public',
|
|
116
|
+
'content',
|
|
117
|
+
contentAssetRelPaths,
|
|
118
|
+
);
|
|
96
119
|
}
|
|
97
120
|
|
|
98
121
|
export function copyContentFile(
|
|
@@ -101,13 +124,12 @@ export function copyContentFile(
|
|
|
101
124
|
filePath: string,
|
|
102
125
|
publicRelPaths?: Set<string>,
|
|
103
126
|
): void {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
log.info`Copying content file ${B`${rel}`}`;
|
|
127
|
+
copySingleFile(
|
|
128
|
+
contentDir,
|
|
129
|
+
distDir,
|
|
130
|
+
filePath,
|
|
131
|
+
'content',
|
|
132
|
+
'public',
|
|
133
|
+
publicRelPaths,
|
|
134
|
+
);
|
|
113
135
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type MarkdownIt from 'markdown-it';
|
|
2
|
-
import textToId from './text-to-id';
|
|
2
|
+
import textToId, { deduplicateId } from './text-to-id';
|
|
3
3
|
|
|
4
4
|
export default function deflistIdPlugin(md: MarkdownIt): void {
|
|
5
5
|
md.core.ruler.push('deflist_id_injector', function (state) {
|
|
@@ -7,21 +7,8 @@ export default function deflistIdPlugin(md: MarkdownIt): void {
|
|
|
7
7
|
const used = new Map<string, number>();
|
|
8
8
|
|
|
9
9
|
function slugify(str: string): string {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
if (!slug) {
|
|
13
|
-
slug = 'term';
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
if (used.has(slug)) {
|
|
17
|
-
const n = (used.get(slug) as number) + 1;
|
|
18
|
-
used.set(slug, n);
|
|
19
|
-
slug = `${slug}-${n}`;
|
|
20
|
-
} else {
|
|
21
|
-
used.set(slug, 1);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
return slug;
|
|
10
|
+
const base = textToId(str) || 'term';
|
|
11
|
+
return deduplicateId(used, base);
|
|
25
12
|
}
|
|
26
13
|
|
|
27
14
|
for (let i = 0; i < tokens.length; i++) {
|
package/build/features.ts
CHANGED
|
@@ -1,11 +1,8 @@
|
|
|
1
|
-
import type { SiteVariables } from './types';
|
|
1
|
+
import type { FeatureConfig, SiteVariables } from './types';
|
|
2
2
|
|
|
3
3
|
export function isFeatureEnabled(
|
|
4
4
|
siteVariables: SiteVariables,
|
|
5
|
-
featureName:
|
|
5
|
+
featureName: keyof FeatureConfig,
|
|
6
6
|
): boolean {
|
|
7
|
-
return
|
|
8
|
-
siteVariables.features?.[featureName as keyof SiteVariables['features']] !==
|
|
9
|
-
false
|
|
10
|
-
);
|
|
7
|
+
return siteVariables.features[featureName] !== false;
|
|
11
8
|
}
|
|
@@ -44,12 +44,16 @@ export class ContentRenderer {
|
|
|
44
44
|
> = new Map();
|
|
45
45
|
|
|
46
46
|
constructor(siteVariables: SiteVariables) {
|
|
47
|
-
this.siteVariables = siteVariables
|
|
47
|
+
this.siteVariables = siteVariables;
|
|
48
48
|
this.loggedCodeDisabled = false;
|
|
49
49
|
this.sourceFileCache = new Map();
|
|
50
50
|
this.lastBuildFiles = new Set();
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
private getCachedAssets(filePath: string): Asset[] {
|
|
54
|
+
return this.sourceFileCache.get(filePath) ?? [];
|
|
55
|
+
}
|
|
56
|
+
|
|
53
57
|
async initHighlighter(): Promise<void> {
|
|
54
58
|
const langs = [
|
|
55
59
|
...new Set([
|
|
@@ -210,7 +214,7 @@ export class ContentRenderer {
|
|
|
210
214
|
changedHtmlAssetPaths: Set<string>,
|
|
211
215
|
removedHtmlAssetPaths: Set<string>,
|
|
212
216
|
): void {
|
|
213
|
-
const previousAssets = this.
|
|
217
|
+
const previousAssets = this.getCachedAssets(filePath);
|
|
214
218
|
const nextAssetPaths = new Set(assets.map(asset => asset.assetPath));
|
|
215
219
|
|
|
216
220
|
for (const asset of previousAssets) {
|
|
@@ -233,7 +237,7 @@ export class ContentRenderer {
|
|
|
233
237
|
|
|
234
238
|
writeCachedAssets(distDir: string, buildContentFiles: string[]): void {
|
|
235
239
|
for (const filePath of buildContentFiles) {
|
|
236
|
-
const assets = this.
|
|
240
|
+
const assets = this.getCachedAssets(filePath);
|
|
237
241
|
for (const asset of assets) {
|
|
238
242
|
const outPath = path.join(distDir, asset.assetPath);
|
|
239
243
|
fs.mkdirSync(path.dirname(outPath), { recursive: true });
|
|
@@ -251,7 +255,7 @@ export class ContentRenderer {
|
|
|
251
255
|
continue;
|
|
252
256
|
}
|
|
253
257
|
|
|
254
|
-
const previousAssets = this.
|
|
258
|
+
const previousAssets = this.getCachedAssets(filePath);
|
|
255
259
|
for (const asset of previousAssets) {
|
|
256
260
|
if (asset.assetPath.endsWith('.html')) {
|
|
257
261
|
removedHtmlAssetPaths.add(asset.assetPath);
|
|
@@ -352,7 +356,7 @@ export class ContentRenderer {
|
|
|
352
356
|
// Collect HTML asset content for Pagefind
|
|
353
357
|
const htmlAssetsByPath = new Map<string, string>();
|
|
354
358
|
for (const filePath of buildContentFiles) {
|
|
355
|
-
const assets = this.
|
|
359
|
+
const assets = this.getCachedAssets(filePath);
|
|
356
360
|
for (const asset of assets) {
|
|
357
361
|
if (asset.assetPath.endsWith('.html')) {
|
|
358
362
|
htmlAssetsByPath.set(asset.assetPath, asset.content as string);
|
|
@@ -10,6 +10,8 @@ import type { SiteVariables } from './types';
|
|
|
10
10
|
|
|
11
11
|
const log = makeLogger(import.meta.url);
|
|
12
12
|
|
|
13
|
+
export const FAVICON_SIZES = [16, 32, 48, 64, 128, 192, 256, 512, 1024];
|
|
14
|
+
|
|
13
15
|
const FONT_PATH = path.join(
|
|
14
16
|
getPackageDir(),
|
|
15
17
|
'fonts',
|
|
@@ -110,24 +112,12 @@ export async function generateFavicons(
|
|
|
110
112
|
siteVariables: SiteVariables,
|
|
111
113
|
distDir: string,
|
|
112
114
|
): Promise<void> {
|
|
113
|
-
const color = siteVariables.faviconColor
|
|
114
|
-
const symbol = siteVariables.faviconSymbol
|
|
115
|
+
const color = siteVariables.faviconColor!;
|
|
116
|
+
const symbol = siteVariables.faviconSymbol!;
|
|
115
117
|
const fontWeight = siteVariables.faviconFontWeight || 700;
|
|
116
|
-
const sizes = [16, 32, 48, 64, 128, 192, 256, 512, 1024];
|
|
117
118
|
const svgSize = 512;
|
|
118
119
|
const filenameBase = 'favicon';
|
|
119
120
|
|
|
120
|
-
if (!symbol) {
|
|
121
|
-
throw new Error(
|
|
122
|
-
'Favicon generation requires "symbol" in site config (or "faviconSymbol")',
|
|
123
|
-
);
|
|
124
|
-
}
|
|
125
|
-
if (!color) {
|
|
126
|
-
throw new Error(
|
|
127
|
-
'Favicon generation requires "themeColor" in site config (or "faviconColor")',
|
|
128
|
-
);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
121
|
const { themeColorLight, textOnThemeLight } = deriveTheme(color);
|
|
132
122
|
|
|
133
123
|
log.info`Generating favicons`;
|
|
@@ -145,7 +135,7 @@ export async function generateFavicons(
|
|
|
145
135
|
fs.writeFileSync(path.join(distDir, `${filenameBase}.svg`), svgMarkup);
|
|
146
136
|
|
|
147
137
|
const pngBuffers = await Promise.all(
|
|
148
|
-
|
|
138
|
+
FAVICON_SIZES.map(async size => {
|
|
149
139
|
const svgForSize = createFaviconSvg(
|
|
150
140
|
size,
|
|
151
141
|
themeColorLight,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import { createApplyBasePath } from './util';
|
|
4
|
+
import { FAVICON_SIZES } from './generate-favicon';
|
|
4
5
|
import type { SiteVariables } from './types';
|
|
5
6
|
|
|
6
7
|
interface ManifestIcon {
|
|
@@ -19,6 +20,20 @@ interface WebAppManifest {
|
|
|
19
20
|
|
|
20
21
|
function createManifest(siteVariables: SiteVariables): WebAppManifest {
|
|
21
22
|
const applyBasePath = createApplyBasePath(siteVariables);
|
|
23
|
+
const filenameBase = 'favicon';
|
|
24
|
+
|
|
25
|
+
const pngIcons: ManifestIcon[] = [...FAVICON_SIZES]
|
|
26
|
+
.sort((a, b) => b - a)
|
|
27
|
+
.map(size => ({
|
|
28
|
+
src: `${filenameBase}-${size}.png`,
|
|
29
|
+
sizes: `${size}x${size}`,
|
|
30
|
+
type: 'image/png',
|
|
31
|
+
purpose: size >= 128 ? 'any maskable' : 'any',
|
|
32
|
+
}));
|
|
33
|
+
|
|
34
|
+
const icoSizes = FAVICON_SIZES.filter(s => s <= 256)
|
|
35
|
+
.map(s => `${s}x${s}`)
|
|
36
|
+
.join(' ');
|
|
22
37
|
|
|
23
38
|
return {
|
|
24
39
|
name: siteVariables.title,
|
|
@@ -26,68 +41,15 @@ function createManifest(siteVariables: SiteVariables): WebAppManifest {
|
|
|
26
41
|
display: 'minimal-ui',
|
|
27
42
|
icons: [
|
|
28
43
|
{
|
|
29
|
-
src:
|
|
44
|
+
src: `${filenameBase}.svg`,
|
|
30
45
|
sizes: 'any',
|
|
31
46
|
type: 'image/svg+xml',
|
|
32
47
|
purpose: 'any maskable',
|
|
33
48
|
},
|
|
49
|
+
...pngIcons,
|
|
34
50
|
{
|
|
35
|
-
src:
|
|
36
|
-
sizes:
|
|
37
|
-
type: 'image/png',
|
|
38
|
-
purpose: 'any maskable',
|
|
39
|
-
},
|
|
40
|
-
{
|
|
41
|
-
src: 'favicon-512.png',
|
|
42
|
-
sizes: '512x512',
|
|
43
|
-
type: 'image/png',
|
|
44
|
-
purpose: 'any maskable',
|
|
45
|
-
},
|
|
46
|
-
{
|
|
47
|
-
src: 'favicon-256.png',
|
|
48
|
-
sizes: '256x256',
|
|
49
|
-
type: 'image/png',
|
|
50
|
-
purpose: 'any maskable',
|
|
51
|
-
},
|
|
52
|
-
{
|
|
53
|
-
src: 'favicon-192.png',
|
|
54
|
-
sizes: '192x192',
|
|
55
|
-
type: 'image/png',
|
|
56
|
-
purpose: 'any maskable',
|
|
57
|
-
},
|
|
58
|
-
{
|
|
59
|
-
src: 'favicon-128.png',
|
|
60
|
-
sizes: '128x128',
|
|
61
|
-
type: 'image/png',
|
|
62
|
-
purpose: 'any maskable',
|
|
63
|
-
},
|
|
64
|
-
{
|
|
65
|
-
src: 'favicon-64.png',
|
|
66
|
-
sizes: '64x64',
|
|
67
|
-
type: 'image/png',
|
|
68
|
-
purpose: 'any',
|
|
69
|
-
},
|
|
70
|
-
{
|
|
71
|
-
src: 'favicon-48.png',
|
|
72
|
-
sizes: '48x48',
|
|
73
|
-
type: 'image/png',
|
|
74
|
-
purpose: 'any',
|
|
75
|
-
},
|
|
76
|
-
{
|
|
77
|
-
src: 'favicon-32.png',
|
|
78
|
-
sizes: '32x32',
|
|
79
|
-
type: 'image/png',
|
|
80
|
-
purpose: 'any',
|
|
81
|
-
},
|
|
82
|
-
{
|
|
83
|
-
src: 'favicon-16.png',
|
|
84
|
-
sizes: '16x16',
|
|
85
|
-
type: 'image/png',
|
|
86
|
-
purpose: 'any',
|
|
87
|
-
},
|
|
88
|
-
{
|
|
89
|
-
src: 'favicon.ico',
|
|
90
|
-
sizes: '16x16 32x32 48x48 64x64 128x128 192x192 256x256',
|
|
51
|
+
src: `${filenameBase}.ico`,
|
|
52
|
+
sizes: icoSizes,
|
|
91
53
|
type: 'image/x-icon',
|
|
92
54
|
purpose: 'any maskable',
|
|
93
55
|
},
|
package/build/globals.ts
CHANGED
|
@@ -1,6 +1,22 @@
|
|
|
1
1
|
import timezones from '../src/timezone/timezones.json' with { type: 'json' };
|
|
2
2
|
import type { SiteVariables } from './types';
|
|
3
3
|
|
|
4
|
+
const MONTHS = [
|
|
5
|
+
'',
|
|
6
|
+
'January',
|
|
7
|
+
'February',
|
|
8
|
+
'March',
|
|
9
|
+
'April',
|
|
10
|
+
'May',
|
|
11
|
+
'June',
|
|
12
|
+
'July',
|
|
13
|
+
'August',
|
|
14
|
+
'September',
|
|
15
|
+
'October',
|
|
16
|
+
'November',
|
|
17
|
+
'December',
|
|
18
|
+
];
|
|
19
|
+
|
|
4
20
|
interface Globals {
|
|
5
21
|
isHomePage: boolean;
|
|
6
22
|
isoDate: (str: string | null | undefined) => string | null;
|
|
@@ -58,31 +74,10 @@ function readableDate(date: string | Date | null | undefined): string {
|
|
|
58
74
|
|
|
59
75
|
const str = date.toISOString();
|
|
60
76
|
const year = str.slice(0, 4);
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
month = month[1];
|
|
64
|
-
}
|
|
65
|
-
let day: string = str.slice(8, 10);
|
|
66
|
-
if (day[0] === '0') {
|
|
67
|
-
day = day[1];
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
const months: Record<string, string> = {
|
|
71
|
-
1: 'January',
|
|
72
|
-
2: 'February',
|
|
73
|
-
3: 'March',
|
|
74
|
-
4: 'April',
|
|
75
|
-
5: 'May',
|
|
76
|
-
6: 'June',
|
|
77
|
-
7: 'July',
|
|
78
|
-
8: 'August',
|
|
79
|
-
9: 'September',
|
|
80
|
-
10: 'October',
|
|
81
|
-
11: 'November',
|
|
82
|
-
12: 'December',
|
|
83
|
-
};
|
|
77
|
+
const month = Number(str.slice(5, 7));
|
|
78
|
+
const day = Number(str.slice(8, 10));
|
|
84
79
|
|
|
85
|
-
return `${
|
|
80
|
+
return `${MONTHS[month]} ${day}, ${year}`;
|
|
86
81
|
}
|
|
87
82
|
|
|
88
83
|
export function classNames(obj: Record<string, unknown>): string {
|