lightnet 3.12.1 → 4.0.0
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/CHANGELOG.md +55 -0
- package/__e2e__/basics-fixture.ts +9 -63
- package/__e2e__/fixtures/basics/astro.config.mjs +7 -12
- package/__e2e__/fixtures/basics/node_modules/.bin/astro +4 -4
- package/__e2e__/fixtures/basics/node_modules/.bin/tailwind +2 -2
- package/__e2e__/fixtures/basics/node_modules/.bin/tailwindcss +2 -2
- package/__e2e__/fixtures/basics/node_modules/.bin/tsc +2 -2
- package/__e2e__/fixtures/basics/node_modules/.bin/tsserver +2 -2
- package/__e2e__/fixtures/basics/package.json +6 -5
- package/__e2e__/fixtures/basics/src/content/categories/christian-living.json +4 -1
- package/__e2e__/fixtures/basics/src/content/categories/teens.json +4 -1
- package/__e2e__/fixtures/basics/src/content/categories/theology.json +4 -1
- package/__e2e__/fixtures/basics/src/content/media/faithful-freestyle--en.json +6 -2
- package/__e2e__/fixtures/basics/src/content/media/how-to-kickflip--de.json +6 -1
- package/__e2e__/fixtures/basics/src/content/media/skate-sounds--en.json +8 -2
- package/__e2e__/fixtures/basics/src/content/media-collections/how-to-articles.json +5 -1
- package/__e2e__/fixtures/basics/src/content/media-types/audio.json +5 -2
- package/__e2e__/fixtures/basics/src/content/media-types/book.json +10 -4
- package/__e2e__/fixtures/basics/src/content/media-types/video.json +5 -2
- package/__e2e__/fixtures/basics/src/pages/[locale]/index.astro +0 -1
- package/__e2e__/fixtures/basics/src/translations/de.yml +0 -8
- package/__e2e__/fixtures/basics/src/translations/en.yml +0 -8
- package/__e2e__/global.teardown.ts +2 -2
- package/__tests__/astro-integration/config.spec.ts +364 -0
- package/__tests__/astro-integration/integration.spec.ts +125 -0
- package/__tests__/astro-integration/tailwind.spec.ts +36 -0
- package/__tests__/content/content-schema.spec.ts +109 -0
- package/__tests__/content/get-media-collections.spec.ts +72 -0
- package/__tests__/content/query-media-items.spec.ts +213 -0
- package/__tests__/i18n/resolve-current-locale.spec.ts +65 -0
- package/__tests__/i18n/translate-map.spec.ts +19 -0
- package/__tests__/i18n/translate.spec.ts +91 -0
- package/__tests__/pages/details-page/create-content-metadata.spec.ts +43 -25
- package/__tests__/pages/details-page/get-translations.spec.ts +56 -0
- package/__tests__/utils/paths.spec.ts +116 -0
- package/__tests__/utils/urls.spec.ts +9 -4
- package/exports/content.ts +7 -2
- package/exports/i18n.ts +0 -1
- package/exports/index.ts +1 -5
- package/exports/utils.ts +0 -1
- package/package.json +16 -12
- package/src/astro-integration/config.ts +60 -49
- package/src/astro-integration/integration.ts +13 -24
- package/src/astro-integration/tailwind.ts +86 -0
- package/src/astro-integration/validators/validate-inline-translations.ts +51 -0
- package/src/astro-integration/validators/validate-languages.ts +39 -0
- package/src/astro-integration/virtual.d.ts +8 -6
- package/src/astro-integration/vite-plugin-lightnet-config.ts +29 -9
- package/src/components/CarouselSection.astro +7 -11
- package/src/components/CategoriesSection.astro +2 -2
- package/src/components/HighlightSection.astro +4 -7
- package/src/components/Icon.tsx +2 -2
- package/src/components/MediaGallerySection.astro +88 -68
- package/src/components/MediaList.astro +9 -7
- package/src/components/SearchInput.astro +7 -4
- package/src/components/Section.astro +7 -5
- package/src/components/VideoPlayer.astro +2 -3
- package/src/content/content-schema.ts +129 -149
- package/src/content/get-categories.ts +52 -28
- package/src/content/get-languages.ts +29 -8
- package/src/content/get-media-collections.ts +43 -0
- package/src/content/get-media-types.ts +41 -7
- package/src/content/query-media-items.ts +23 -13
- package/src/i18n/bcp-47.ts +8 -0
- package/src/i18n/get-locale-paths.ts +1 -3
- package/src/i18n/locals.d.ts +21 -3
- package/src/i18n/locals.ts +18 -11
- package/src/i18n/resolve-current-locale.ts +18 -0
- package/src/i18n/resolve-language.ts +10 -5
- package/src/i18n/translate-map.ts +70 -0
- package/src/i18n/translate.ts +68 -47
- package/src/layouts/Page.astro +5 -3
- package/src/layouts/components/LanguagePicker.astro +22 -17
- package/src/layouts/components/Menu.astro +2 -5
- package/src/layouts/components/MenuItem.astro +1 -1
- package/src/layouts/components/PageNavigation.astro +29 -29
- package/src/layouts/components/PageTitle.astro +23 -7
- package/src/pages/404Route.astro +2 -1
- package/src/pages/RootRoute.astro +6 -1
- package/src/pages/details-page/DefaultDetailsPage.astro +9 -2
- package/src/pages/details-page/DetailsPageRoute.astro +1 -2
- package/src/pages/details-page/components/AudioPanel.astro +7 -3
- package/src/pages/details-page/components/AudioPlayer.astro +2 -2
- package/src/pages/details-page/components/ContentSection.astro +67 -44
- package/src/pages/details-page/components/MediaCollection.astro +8 -4
- package/src/pages/details-page/components/MediaCollectionsSection.astro +3 -6
- package/src/pages/details-page/components/main-details/EditButton.astro +22 -10
- package/src/pages/details-page/components/main-details/OpenButton.astro +17 -12
- package/src/pages/details-page/components/main-details/ShareButton.astro +3 -2
- package/src/pages/details-page/components/more-details/Categories.astro +5 -3
- package/src/pages/details-page/components/more-details/Languages.astro +12 -7
- package/src/pages/details-page/utils/create-content-metadata.ts +24 -9
- package/src/pages/details-page/utils/get-translations.ts +6 -0
- package/src/pages/search-page/components/LoadingSkeleton.tsx +6 -5
- package/src/pages/search-page/components/SearchFilter.astro +10 -21
- package/src/pages/search-page/components/SearchFilter.tsx +2 -2
- package/src/pages/search-page/components/SearchList.astro +10 -7
- package/src/pages/search-page/components/SearchListItem.tsx +5 -4
- package/src/pages/search-page/hooks/use-search.ts +5 -2
- package/src/utils/lazy.ts +20 -0
- package/src/utils/paths.ts +40 -3
- package/src/utils/urls.ts +1 -2
- package/src/utils/verify-schema.ts +12 -10
- package/tailwind.config.ts +1 -25
- package/vitest.config.js +18 -2
- package/src/astro-integration/project-context.ts +0 -5
- package/src/content/compare-media-collection-items.ts +0 -24
- package/src/i18n/resolve-default-locale.ts +0 -19
- package/src/i18n/resolve-locales.ts +0 -5
- package/src/pages/details-page/utils/get-collection-items.ts +0 -29
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { afterEach, expect, test, vi } from "vitest"
|
|
2
|
+
|
|
3
|
+
afterEach(() => {
|
|
4
|
+
vi.unstubAllEnvs()
|
|
5
|
+
vi.resetModules()
|
|
6
|
+
})
|
|
7
|
+
|
|
8
|
+
test("Should build localized paths at root base", async () => {
|
|
9
|
+
vi.stubEnv("BASE_URL", "/")
|
|
10
|
+
|
|
11
|
+
const { detailsPagePath, localizePath, searchPagePath } =
|
|
12
|
+
await import("../../src/utils/paths")
|
|
13
|
+
|
|
14
|
+
expect(detailsPagePath("en", { id: "my-book" })).toBe("/en/media/my-book")
|
|
15
|
+
expect(searchPagePath("en", { category: "comics" })).toBe(
|
|
16
|
+
"/en/media?category=comics",
|
|
17
|
+
)
|
|
18
|
+
expect(localizePath("en", "/about")).toBe("/en/about")
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
test("Should keep internal paths unchanged at root base", async () => {
|
|
22
|
+
vi.stubEnv("BASE_URL", "/")
|
|
23
|
+
|
|
24
|
+
const { pathWithBase } = await import("../../src/utils/paths")
|
|
25
|
+
|
|
26
|
+
expect(pathWithBase("/en/about")).toBe("/en/about")
|
|
27
|
+
expect(pathWithBase("/en/media?category=comics#results")).toBe(
|
|
28
|
+
"/en/media?category=comics#results",
|
|
29
|
+
)
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
test("Should prefix localized paths with Astro base", async () => {
|
|
33
|
+
vi.stubEnv("BASE_URL", "/docs/")
|
|
34
|
+
|
|
35
|
+
const { detailsPagePath, localizePath, searchPagePath } =
|
|
36
|
+
await import("../../src/utils/paths")
|
|
37
|
+
|
|
38
|
+
expect(detailsPagePath("en", { id: "my-book" })).toBe(
|
|
39
|
+
"/docs/en/media/my-book",
|
|
40
|
+
)
|
|
41
|
+
expect(searchPagePath("en", { category: "comics" })).toBe(
|
|
42
|
+
"/docs/en/media?category=comics",
|
|
43
|
+
)
|
|
44
|
+
expect(localizePath("en", "/about")).toBe("/docs/en/about")
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
test("Should prefix internal API paths with Astro base", async () => {
|
|
48
|
+
vi.stubEnv("BASE_URL", "/docs/")
|
|
49
|
+
|
|
50
|
+
const { pathWithBase } = await import("../../src/utils/paths")
|
|
51
|
+
|
|
52
|
+
expect(pathWithBase("/api/internal/search.json")).toBe(
|
|
53
|
+
"/docs/api/internal/search.json",
|
|
54
|
+
)
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
test("Should strip Astro base from pathname", async () => {
|
|
58
|
+
vi.stubEnv("BASE_URL", "/docs/")
|
|
59
|
+
|
|
60
|
+
const { pathWithoutBase } = await import("../../src/utils/paths")
|
|
61
|
+
|
|
62
|
+
expect(pathWithoutBase("/docs/en/media")).toBe("/en/media")
|
|
63
|
+
expect(pathWithoutBase("/docs")).toBe("/")
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
test("Should leave root-base pathname unchanged", async () => {
|
|
67
|
+
vi.stubEnv("BASE_URL", "/")
|
|
68
|
+
|
|
69
|
+
const { pathWithoutBase } = await import("../../src/utils/paths")
|
|
70
|
+
|
|
71
|
+
expect(pathWithoutBase("/en/media")).toBe("/en/media")
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
test("Should support explicit base when stripping pathname", async () => {
|
|
75
|
+
const { pathWithoutBase } = await import("../../src/utils/paths")
|
|
76
|
+
|
|
77
|
+
expect(pathWithoutBase("/custom/en/media", "/custom/")).toBe("/en/media")
|
|
78
|
+
expect(pathWithoutBase("/en/media", "/custom/")).toBe("/en/media")
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
test("Should preserve root path under Astro base", async () => {
|
|
82
|
+
vi.stubEnv("BASE_URL", "/docs/")
|
|
83
|
+
|
|
84
|
+
const { pathWithBase, localizePath } = await import("../../src/utils/paths")
|
|
85
|
+
|
|
86
|
+
expect(pathWithBase("/")).toBe("/docs/")
|
|
87
|
+
expect(localizePath(undefined, "/")).toBe("/docs/")
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
test("Should preserve root path at root base", async () => {
|
|
91
|
+
vi.stubEnv("BASE_URL", "/")
|
|
92
|
+
|
|
93
|
+
const { pathWithBase, localizePath } = await import("../../src/utils/paths")
|
|
94
|
+
|
|
95
|
+
expect(pathWithBase("/")).toBe("/")
|
|
96
|
+
expect(localizePath(undefined, "/")).toBe("/")
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
test("Should preserve query strings and hashes", async () => {
|
|
100
|
+
vi.stubEnv("BASE_URL", "/docs/")
|
|
101
|
+
|
|
102
|
+
const { pathWithBase } = await import("../../src/utils/paths")
|
|
103
|
+
|
|
104
|
+
expect(pathWithBase("/en/media?category=comics#results")).toBe(
|
|
105
|
+
"/docs/en/media?category=comics#results",
|
|
106
|
+
)
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
test("Should normalize slashes between base and path", async () => {
|
|
110
|
+
vi.stubEnv("BASE_URL", "/docs/")
|
|
111
|
+
|
|
112
|
+
const { pathWithBase } = await import("../../src/utils/paths")
|
|
113
|
+
|
|
114
|
+
expect(pathWithBase("/en/about")).toBe("/docs/en/about")
|
|
115
|
+
expect(pathWithBase("en/about")).toBe("/docs/en/about")
|
|
116
|
+
})
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import config from "virtual:lightnet/config"
|
|
2
|
-
import
|
|
3
|
-
import { expect, test } from "vitest"
|
|
2
|
+
import { afterEach, expect, test, vi } from "vitest"
|
|
4
3
|
|
|
5
4
|
import { isExternalUrl } from "../../src/utils/urls"
|
|
6
5
|
|
|
@@ -9,9 +8,15 @@ test("Should treat relative paths as internal", () => {
|
|
|
9
8
|
expect(isExternalUrl("/page")).toBe(false)
|
|
10
9
|
})
|
|
11
10
|
|
|
11
|
+
afterEach(() => {
|
|
12
|
+
vi.unstubAllEnvs()
|
|
13
|
+
})
|
|
14
|
+
|
|
12
15
|
// absolute url that matches the configured site should be internal
|
|
13
|
-
test("Should treat URLs matching
|
|
14
|
-
|
|
16
|
+
test("Should treat URLs matching import.meta.env.SITE as internal", () => {
|
|
17
|
+
vi.stubEnv("SITE", "https://sk8-ministries.dev")
|
|
18
|
+
|
|
19
|
+
expect(isExternalUrl("https://sk8-ministries.dev/page")).toBe(false)
|
|
15
20
|
})
|
|
16
21
|
|
|
17
22
|
// domains listed in internalDomains should be treated as internal
|
package/exports/content.ts
CHANGED
|
@@ -13,6 +13,11 @@ import {
|
|
|
13
13
|
queryMediaItems,
|
|
14
14
|
} from "../src/content/query-media-items"
|
|
15
15
|
|
|
16
|
-
export const getMediaItems = (
|
|
16
|
+
export const getMediaItems = async (
|
|
17
17
|
query?: MediaItemQuery<CollectionEntry<"media">>,
|
|
18
|
-
) =>
|
|
18
|
+
) =>
|
|
19
|
+
queryMediaItems(
|
|
20
|
+
getCollection("media"),
|
|
21
|
+
getCollection("media-collections"),
|
|
22
|
+
query ?? {},
|
|
23
|
+
)
|
package/exports/i18n.ts
CHANGED
package/exports/index.ts
CHANGED
package/exports/utils.ts
CHANGED
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"description": "LightNet makes it easy to run your own digital media library.",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
|
-
"version": "
|
|
6
|
+
"version": "4.0.0",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
9
9
|
"url": "https://github.com/LightNetDev/lightnet",
|
|
@@ -35,38 +35,42 @@
|
|
|
35
35
|
"./api/versions.ts": "./src/api/versions.ts"
|
|
36
36
|
},
|
|
37
37
|
"peerDependencies": {
|
|
38
|
-
"astro": "^
|
|
38
|
+
"astro": "^6.0.0",
|
|
39
39
|
"react": "^19.0.0",
|
|
40
40
|
"react-dom": "^19.0.0",
|
|
41
41
|
"tailwindcss": ">=3.4.0 <4.0.0"
|
|
42
42
|
},
|
|
43
43
|
"dependencies": {
|
|
44
|
-
"@astrojs/react": "^
|
|
45
|
-
"@
|
|
44
|
+
"@astrojs/react": "^5.0.2",
|
|
45
|
+
"@iconify-json/lucide": "^1.2.100",
|
|
46
46
|
"@iconify-json/mdi": "^1.2.3",
|
|
47
47
|
"@iconify/tailwind": "^1.2.0",
|
|
48
48
|
"@tailwindcss/typography": "^0.5.19",
|
|
49
|
-
"@tanstack/react-virtual": "^3.13.
|
|
50
|
-
"
|
|
49
|
+
"@tanstack/react-virtual": "^3.13.23",
|
|
50
|
+
"autoprefixer": "^10.4.27",
|
|
51
51
|
"embla-carousel": "^8.6.0",
|
|
52
52
|
"embla-carousel-wheel-gestures": "^8.1.0",
|
|
53
53
|
"fuse.js": "^7.1.0",
|
|
54
|
-
"i18next": "^25.
|
|
55
|
-
"
|
|
56
|
-
"
|
|
54
|
+
"i18next": "^25.10.10",
|
|
55
|
+
"lucide-react": "^1.7.0",
|
|
56
|
+
"marked": "^17.0.5",
|
|
57
|
+
"postcss": "^8.5.8",
|
|
58
|
+
"postcss-load-config": "^6.0.1",
|
|
59
|
+
"yaml": "^2.8.3"
|
|
57
60
|
},
|
|
58
61
|
"devDependencies": {
|
|
62
|
+
"@internal/e2e-test-utils": "^0.0.1",
|
|
59
63
|
"@playwright/test": "^1.58.2",
|
|
60
|
-
"@types/node": "^22.19.11",
|
|
61
64
|
"@types/react": "^19.2.14",
|
|
65
|
+
"astro": "^6.1.1",
|
|
62
66
|
"typescript": "^5.9.3",
|
|
63
|
-
"vitest": "^4.
|
|
67
|
+
"vitest": "^4.1.2"
|
|
64
68
|
},
|
|
65
69
|
"engines": {
|
|
66
70
|
"node": ">=22"
|
|
67
71
|
},
|
|
68
72
|
"scripts": {
|
|
69
73
|
"test": "vitest",
|
|
70
|
-
"e2e": "playwright
|
|
74
|
+
"e2e": "playwright test"
|
|
71
75
|
}
|
|
72
76
|
}
|
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
import { z } from "astro/zod"
|
|
2
2
|
|
|
3
|
+
import { isBcp47 } from "../i18n/bcp-47"
|
|
4
|
+
import { validateInlineTranslations } from "./validators/validate-inline-translations"
|
|
5
|
+
import { validateLanguages } from "./validators/validate-languages"
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Translations by BCP-47 tags.
|
|
9
|
+
* We can only do basic validation here because we cannot access locales
|
|
10
|
+
* from the same config.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* {
|
|
14
|
+
* de: "Hallo",
|
|
15
|
+
* en: "Hello"
|
|
16
|
+
* }
|
|
17
|
+
*/
|
|
18
|
+
export const inlineTranslationSchema = z.record(
|
|
19
|
+
z.string(),
|
|
20
|
+
z.string().nonempty(),
|
|
21
|
+
)
|
|
22
|
+
|
|
3
23
|
/**
|
|
4
24
|
* Link Schema.
|
|
5
25
|
*/
|
|
@@ -12,9 +32,10 @@ const linkSchema = z.object({
|
|
|
12
32
|
href: z.string(),
|
|
13
33
|
/**
|
|
14
34
|
* Label to be used for the link.
|
|
15
|
-
*
|
|
35
|
+
* Must define a value for the default site locale.
|
|
36
|
+
* Other configured site locales are optional.
|
|
16
37
|
*/
|
|
17
|
-
label:
|
|
38
|
+
label: inlineTranslationSchema,
|
|
18
39
|
/**
|
|
19
40
|
* If this is set to true the currentLocale will be appended to
|
|
20
41
|
* the href path. Eg. for href="/about"
|
|
@@ -27,63 +48,35 @@ const linkSchema = z.object({
|
|
|
27
48
|
requiresLocale: z.boolean().default(true),
|
|
28
49
|
})
|
|
29
50
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
51
|
+
const bcp47Schema = z.string().refine(isBcp47, {
|
|
52
|
+
message: "Invalid BCP-47 language code",
|
|
53
|
+
})
|
|
54
|
+
|
|
33
55
|
const languageSchema = z
|
|
34
56
|
.object({
|
|
35
57
|
/**
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
* This will be the identifier of this language and will
|
|
39
|
-
* also appear on the URL paths of the website.
|
|
58
|
+
* BCP-47 code for this language.
|
|
40
59
|
*/
|
|
41
|
-
code:
|
|
60
|
+
code: bcp47Schema,
|
|
42
61
|
/**
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
* Can either be a fixed string or a translation key.
|
|
62
|
+
* Display name for this language.
|
|
46
63
|
*/
|
|
47
|
-
label:
|
|
64
|
+
label: inlineTranslationSchema,
|
|
48
65
|
/**
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
* Make sure to provide translations inside the `src/translations/` folder.
|
|
52
|
-
*
|
|
53
|
-
* Default is `false`
|
|
66
|
+
* Whether this language should be exposed as a site UI language.
|
|
54
67
|
*/
|
|
55
68
|
isSiteLanguage: z.boolean().default(false),
|
|
56
69
|
/**
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
* The default language will be used as a fallback when translations are missing
|
|
60
|
-
* also this will be the language selected when a user visits the site on the `/` path.
|
|
61
|
-
*
|
|
62
|
-
* Setting this to `true` will also set `isSiteLanguage` to `true`.
|
|
63
|
-
*
|
|
64
|
-
* Default is `false`
|
|
70
|
+
* Whether this language is the default site language.
|
|
65
71
|
*/
|
|
66
72
|
isDefaultSiteLanguage: z.boolean().default(false),
|
|
67
73
|
/**
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
* This is used when no translation key is defined for this language.
|
|
71
|
-
* The system will iterate over this array in order and use the first language for which a
|
|
72
|
-
* matching translation key is found.
|
|
73
|
-
*
|
|
74
|
-
* If no match is found from the fallback languages, the system will
|
|
75
|
-
* attempt the translation using the default site language.
|
|
76
|
-
*
|
|
77
|
-
* If the translation still cannot be resolved, it will then fall back to the English
|
|
78
|
-
* translation as a final resort.
|
|
79
|
-
*
|
|
80
|
-
* @example ["fr", "it"]
|
|
74
|
+
* Ordered fallback language codes used when a translation key is missing.
|
|
81
75
|
*/
|
|
82
|
-
fallbackLanguages: z.
|
|
76
|
+
fallbackLanguages: z.array(bcp47Schema).default([]),
|
|
83
77
|
})
|
|
84
78
|
.transform((language) => ({
|
|
85
79
|
...language,
|
|
86
|
-
// if language is default site language also set is site language to true.
|
|
87
80
|
isSiteLanguage: language.isDefaultSiteLanguage || language.isSiteLanguage,
|
|
88
81
|
}))
|
|
89
82
|
|
|
@@ -123,11 +116,11 @@ export const configSchema = z.object({
|
|
|
123
116
|
/**
|
|
124
117
|
* Title of the web site.
|
|
125
118
|
*/
|
|
126
|
-
title:
|
|
119
|
+
title: inlineTranslationSchema,
|
|
127
120
|
/**
|
|
128
|
-
*
|
|
121
|
+
* Languages supported by this site.
|
|
129
122
|
*/
|
|
130
|
-
languages:
|
|
123
|
+
languages: z.array(languageSchema).min(1).superRefine(validateLanguages),
|
|
131
124
|
/**
|
|
132
125
|
* Favicons for your site.
|
|
133
126
|
*/
|
|
@@ -155,9 +148,10 @@ export const configSchema = z.object({
|
|
|
155
148
|
src: z.string(),
|
|
156
149
|
/**
|
|
157
150
|
* Alt attribute to add for screen reader etc.
|
|
158
|
-
*
|
|
151
|
+
* Must define a value for the default site locale.
|
|
152
|
+
* Other configured site locales are optional.
|
|
159
153
|
*/
|
|
160
|
-
alt:
|
|
154
|
+
alt: inlineTranslationSchema.optional(),
|
|
161
155
|
/**
|
|
162
156
|
* Size in px to use for the logo on the header bar.
|
|
163
157
|
* The size will be applied to the shorter side of your logo image.
|
|
@@ -223,8 +217,25 @@ export const configSchema = z.object({
|
|
|
223
217
|
experimental: z.object({}).optional(),
|
|
224
218
|
})
|
|
225
219
|
|
|
226
|
-
export
|
|
220
|
+
export const extendedConfigSchema = configSchema.transform((config, ctx) => {
|
|
221
|
+
const locales = config.languages
|
|
222
|
+
.filter((language) => language.isSiteLanguage)
|
|
223
|
+
.map((language) => language.code)
|
|
224
|
+
const defaultLocale =
|
|
225
|
+
config.languages.find((language) => language.isDefaultSiteLanguage)?.code ??
|
|
226
|
+
""
|
|
227
|
+
|
|
228
|
+
validateInlineTranslations(config, locales, defaultLocale, ctx)
|
|
229
|
+
|
|
230
|
+
return {
|
|
231
|
+
...config,
|
|
232
|
+
locales,
|
|
233
|
+
defaultLocale,
|
|
234
|
+
}
|
|
235
|
+
})
|
|
236
|
+
|
|
227
237
|
export type Link = z.input<typeof linkSchema>
|
|
238
|
+
export type Language = z.output<typeof languageSchema>
|
|
228
239
|
|
|
229
240
|
export type LightnetConfig = z.input<typeof configSchema>
|
|
230
|
-
export type
|
|
241
|
+
export type ExtendedLightnetConfig = z.output<typeof extendedConfigSchema>
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
/// <reference path="../i18n/locals.d.ts" />
|
|
2
2
|
import react from "@astrojs/react"
|
|
3
|
-
import tailwind from "@astrojs/tailwind"
|
|
4
3
|
import type { AstroIntegration } from "astro"
|
|
4
|
+
import { AstroError } from "astro/errors"
|
|
5
5
|
|
|
6
|
-
import { resolveDefaultLocale } from "../i18n/resolve-default-locale"
|
|
7
|
-
import { resolveLocales } from "../i18n/resolve-locales"
|
|
8
6
|
import { verifySchema } from "../utils/verify-schema"
|
|
9
|
-
import {
|
|
7
|
+
import { extendedConfigSchema, type LightnetConfig } from "./config"
|
|
8
|
+
import tailwind from "./tailwind"
|
|
10
9
|
import { vitePluginLightnetConfig } from "./vite-plugin-lightnet-config"
|
|
11
10
|
|
|
12
11
|
export function lightnet(lightnetConfig: LightnetConfig): AstroIntegration {
|
|
@@ -20,11 +19,18 @@ export function lightnet(lightnetConfig: LightnetConfig): AstroIntegration {
|
|
|
20
19
|
logger,
|
|
21
20
|
addMiddleware,
|
|
22
21
|
}) => {
|
|
22
|
+
if (!astroConfig.site) {
|
|
23
|
+
throw new AstroError(
|
|
24
|
+
"Invalid LightNet configuration",
|
|
25
|
+
"Set `site` in your Astro config. LightNet requires Astro `site` to be configured.",
|
|
26
|
+
)
|
|
27
|
+
}
|
|
28
|
+
|
|
23
29
|
const config = verifySchema(
|
|
24
|
-
|
|
30
|
+
extendedConfigSchema,
|
|
25
31
|
lightnetConfig,
|
|
26
32
|
"Invalid LightNet configuration",
|
|
27
|
-
"Fix these errors on the LightNet configuration
|
|
33
|
+
"Fix these errors on the LightNet configuration:",
|
|
28
34
|
)
|
|
29
35
|
|
|
30
36
|
injectRoute({
|
|
@@ -65,28 +71,11 @@ export function lightnet(lightnetConfig: LightnetConfig): AstroIntegration {
|
|
|
65
71
|
|
|
66
72
|
addMiddleware({ entrypoint: "lightnet/locals", order: "pre" })
|
|
67
73
|
|
|
68
|
-
astroConfig.integrations.push(
|
|
69
|
-
tailwind({ applyBaseStyles: false }),
|
|
70
|
-
react(),
|
|
71
|
-
)
|
|
72
|
-
|
|
73
74
|
updateConfig({
|
|
75
|
+
integrations: [tailwind(), react()],
|
|
74
76
|
vite: {
|
|
75
77
|
plugins: [vitePluginLightnetConfig(config, astroConfig, logger)],
|
|
76
78
|
},
|
|
77
|
-
i18n: {
|
|
78
|
-
defaultLocale: resolveDefaultLocale(config),
|
|
79
|
-
locales: resolveLocales(config),
|
|
80
|
-
routing: {
|
|
81
|
-
redirectToDefaultLocale: false,
|
|
82
|
-
// We need to set this to false to allow for
|
|
83
|
-
// admin paths without locale. But actually
|
|
84
|
-
// the default locale will be prefixed for regular
|
|
85
|
-
// LightNet pages.
|
|
86
|
-
prefixDefaultLocale: false,
|
|
87
|
-
fallbackType: "rewrite",
|
|
88
|
-
},
|
|
89
|
-
},
|
|
90
79
|
})
|
|
91
80
|
},
|
|
92
81
|
},
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { fileURLToPath } from "node:url"
|
|
2
|
+
|
|
3
|
+
import type { AstroIntegration } from "astro"
|
|
4
|
+
import autoprefixerPlugin from "autoprefixer"
|
|
5
|
+
import type { AcceptedPlugin, ProcessOptions } from "postcss"
|
|
6
|
+
import postcssrc from "postcss-load-config"
|
|
7
|
+
import tailwindPlugin from "tailwindcss"
|
|
8
|
+
|
|
9
|
+
type PostcssInlineOptions =
|
|
10
|
+
| undefined
|
|
11
|
+
| string
|
|
12
|
+
| (ProcessOptions & {
|
|
13
|
+
plugins?: AcceptedPlugin[]
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
function isInlinePostCssOptions(
|
|
17
|
+
postcssInlineOptions: PostcssInlineOptions,
|
|
18
|
+
): postcssInlineOptions is Exclude<PostcssInlineOptions, undefined | string> {
|
|
19
|
+
return (
|
|
20
|
+
typeof postcssInlineOptions === "object" && postcssInlineOptions !== null
|
|
21
|
+
)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function getPostCssConfig(
|
|
25
|
+
root: string,
|
|
26
|
+
postcssInlineOptions: PostcssInlineOptions,
|
|
27
|
+
) {
|
|
28
|
+
let postcssConfigResult
|
|
29
|
+
if (!isInlinePostCssOptions(postcssInlineOptions)) {
|
|
30
|
+
const searchPath =
|
|
31
|
+
typeof postcssInlineOptions === "string" ? postcssInlineOptions : root
|
|
32
|
+
try {
|
|
33
|
+
postcssConfigResult = await postcssrc({}, searchPath)
|
|
34
|
+
} catch {
|
|
35
|
+
postcssConfigResult = null
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return postcssConfigResult
|
|
39
|
+
}
|
|
40
|
+
async function getViteConfiguration(
|
|
41
|
+
root: string,
|
|
42
|
+
postcssInlineOptions: PostcssInlineOptions,
|
|
43
|
+
) {
|
|
44
|
+
const postcssConfigResult = await getPostCssConfig(root, postcssInlineOptions)
|
|
45
|
+
const inlinePostcssOptions = isInlinePostCssOptions(postcssInlineOptions)
|
|
46
|
+
? postcssInlineOptions
|
|
47
|
+
: null
|
|
48
|
+
const postcssOptions = inlinePostcssOptions
|
|
49
|
+
? { ...inlinePostcssOptions }
|
|
50
|
+
: (postcssConfigResult?.options ?? {})
|
|
51
|
+
const postcssPlugins =
|
|
52
|
+
inlinePostcssOptions?.plugins?.slice() ??
|
|
53
|
+
postcssConfigResult?.plugins?.slice() ??
|
|
54
|
+
[]
|
|
55
|
+
postcssPlugins.push(tailwindPlugin())
|
|
56
|
+
postcssPlugins.push(autoprefixerPlugin())
|
|
57
|
+
return {
|
|
58
|
+
css: {
|
|
59
|
+
postcss: {
|
|
60
|
+
...postcssOptions,
|
|
61
|
+
plugins: postcssPlugins,
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Astro v6 no longer supports Tailwind CSS v3 through `@astrojs/tailwind`,
|
|
68
|
+
// so LightNet provides its own minimal integration for now.
|
|
69
|
+
// Remove this in the next major release once Tailwind v4 is the baseline,
|
|
70
|
+
// and then also remove `autoprefixer`, `postcss`, and `postcss-load-config`.
|
|
71
|
+
function tailwindIntegration(): AstroIntegration {
|
|
72
|
+
return {
|
|
73
|
+
name: "@lightnet/tailwind",
|
|
74
|
+
hooks: {
|
|
75
|
+
"astro:config:setup": async ({ config, updateConfig }) => {
|
|
76
|
+
updateConfig({
|
|
77
|
+
vite: await getViteConfiguration(
|
|
78
|
+
fileURLToPath(config.root),
|
|
79
|
+
config.vite.css?.postcss,
|
|
80
|
+
),
|
|
81
|
+
})
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
export { tailwindIntegration as default }
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { z } from "astro/zod"
|
|
2
|
+
|
|
3
|
+
export const validateInlineTranslations = (
|
|
4
|
+
config: {
|
|
5
|
+
title: Record<string, string>
|
|
6
|
+
languages: { label: Record<string, string> }[]
|
|
7
|
+
mainMenu?: { label: Record<string, string> }[]
|
|
8
|
+
logo?: { alt?: Record<string, string> }
|
|
9
|
+
},
|
|
10
|
+
locales: string[],
|
|
11
|
+
defaultLocale: string,
|
|
12
|
+
ctx: z.RefinementCtx,
|
|
13
|
+
) => {
|
|
14
|
+
const validateInlineTranslation = (
|
|
15
|
+
inlineTranslation: Record<string, string> | undefined,
|
|
16
|
+
path: (string | number)[],
|
|
17
|
+
) => {
|
|
18
|
+
if (!inlineTranslation) {
|
|
19
|
+
return
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (!(defaultLocale in inlineTranslation)) {
|
|
23
|
+
ctx.addIssue({
|
|
24
|
+
code: "custom",
|
|
25
|
+
message: `Missing translation for default locale "${defaultLocale}"`,
|
|
26
|
+
path: [...path, defaultLocale],
|
|
27
|
+
})
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
for (const locale of Object.keys(inlineTranslation)) {
|
|
31
|
+
if (locales.includes(locale)) {
|
|
32
|
+
continue
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
ctx.addIssue({
|
|
36
|
+
code: "custom",
|
|
37
|
+
message: `Invalid locale "${locale}". Inline translations only support configured site locales: ${locales.join(", ")}`,
|
|
38
|
+
path: [...path, locale],
|
|
39
|
+
})
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
validateInlineTranslation(config.title, ["title"])
|
|
44
|
+
for (const [index, language] of config.languages.entries()) {
|
|
45
|
+
validateInlineTranslation(language.label, ["languages", index, "label"])
|
|
46
|
+
}
|
|
47
|
+
for (const [index, link] of (config.mainMenu ?? []).entries()) {
|
|
48
|
+
validateInlineTranslation(link.label, ["mainMenu", index, "label"])
|
|
49
|
+
}
|
|
50
|
+
validateInlineTranslation(config.logo?.alt, ["logo", "alt"])
|
|
51
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { z } from "astro/zod"
|
|
2
|
+
|
|
3
|
+
type Language = {
|
|
4
|
+
code: string
|
|
5
|
+
isDefaultSiteLanguage?: boolean
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export const validateLanguages = (
|
|
9
|
+
languages: Language[],
|
|
10
|
+
ctx: z.RefinementCtx,
|
|
11
|
+
) => {
|
|
12
|
+
const seen = new Set<string>()
|
|
13
|
+
let defaultCount = 0
|
|
14
|
+
|
|
15
|
+
for (const [index, language] of languages.entries()) {
|
|
16
|
+
const { code, isDefaultSiteLanguage } = language
|
|
17
|
+
|
|
18
|
+
if (!seen.has(code)) {
|
|
19
|
+
seen.add(code)
|
|
20
|
+
} else {
|
|
21
|
+
ctx.addIssue({
|
|
22
|
+
code: "custom",
|
|
23
|
+
message: `Duplicate language code "${code}"`,
|
|
24
|
+
path: [index, "code"],
|
|
25
|
+
})
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (isDefaultSiteLanguage) {
|
|
29
|
+
defaultCount += 1
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (defaultCount !== 1) {
|
|
34
|
+
ctx.addIssue({
|
|
35
|
+
code: "custom",
|
|
36
|
+
message: "Exactly one language must define isDefaultSiteLanguage: true",
|
|
37
|
+
})
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
declare module "virtual:lightnet/config" {
|
|
2
|
-
const config: import("./config").
|
|
2
|
+
const config: import("./config").ExtendedLightnetConfig
|
|
3
3
|
export default config
|
|
4
4
|
}
|
|
5
5
|
|
|
@@ -8,11 +8,6 @@ declare module "virtual:lightnet/logo" {
|
|
|
8
8
|
export default logo
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
declare module "virtual:lightnet/project-context" {
|
|
12
|
-
const context: import("./project-context").ProjectContext
|
|
13
|
-
export default context
|
|
14
|
-
}
|
|
15
|
-
|
|
16
11
|
declare module "virtual:lightnet/components/CustomHead" {
|
|
17
12
|
const CustomHead: ((props: Record<string, any>) => any) | undefined
|
|
18
13
|
export default CustomHead
|
|
@@ -22,3 +17,10 @@ declare module "virtual:lightnet/components/CustomFooter" {
|
|
|
22
17
|
const CustomFooter: ((props: Record<string, any>) => any) | undefined
|
|
23
18
|
export default CustomFooter
|
|
24
19
|
}
|
|
20
|
+
|
|
21
|
+
declare module "virtual:lightnet/components/media-item-edit-button-controller" {
|
|
22
|
+
const mediaItemEditButtonController:
|
|
23
|
+
| { shouldShow: () => boolean; createHref: (mediaId: string) => string }
|
|
24
|
+
| undefined
|
|
25
|
+
export default mediaItemEditButtonController
|
|
26
|
+
}
|