lightnet 3.12.2 → 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.
Files changed (110) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/__e2e__/basics-fixture.ts +9 -63
  3. package/__e2e__/fixtures/basics/astro.config.mjs +7 -12
  4. package/__e2e__/fixtures/basics/node_modules/.bin/astro +4 -4
  5. package/__e2e__/fixtures/basics/node_modules/.bin/tailwind +2 -2
  6. package/__e2e__/fixtures/basics/node_modules/.bin/tailwindcss +2 -2
  7. package/__e2e__/fixtures/basics/node_modules/.bin/tsc +2 -2
  8. package/__e2e__/fixtures/basics/node_modules/.bin/tsserver +2 -2
  9. package/__e2e__/fixtures/basics/package.json +6 -5
  10. package/__e2e__/fixtures/basics/src/content/categories/christian-living.json +4 -1
  11. package/__e2e__/fixtures/basics/src/content/categories/teens.json +4 -1
  12. package/__e2e__/fixtures/basics/src/content/categories/theology.json +4 -1
  13. package/__e2e__/fixtures/basics/src/content/media/faithful-freestyle--en.json +6 -2
  14. package/__e2e__/fixtures/basics/src/content/media/how-to-kickflip--de.json +6 -1
  15. package/__e2e__/fixtures/basics/src/content/media/skate-sounds--en.json +8 -2
  16. package/__e2e__/fixtures/basics/src/content/media-collections/how-to-articles.json +5 -1
  17. package/__e2e__/fixtures/basics/src/content/media-types/audio.json +5 -2
  18. package/__e2e__/fixtures/basics/src/content/media-types/book.json +10 -4
  19. package/__e2e__/fixtures/basics/src/content/media-types/video.json +5 -2
  20. package/__e2e__/fixtures/basics/src/pages/[locale]/index.astro +0 -1
  21. package/__e2e__/fixtures/basics/src/translations/de.yml +0 -8
  22. package/__e2e__/fixtures/basics/src/translations/en.yml +0 -8
  23. package/__e2e__/global.teardown.ts +2 -2
  24. package/__tests__/astro-integration/config.spec.ts +364 -0
  25. package/__tests__/astro-integration/integration.spec.ts +125 -0
  26. package/__tests__/astro-integration/tailwind.spec.ts +36 -0
  27. package/__tests__/content/content-schema.spec.ts +109 -0
  28. package/__tests__/content/get-media-collections.spec.ts +72 -0
  29. package/__tests__/content/query-media-items.spec.ts +213 -0
  30. package/__tests__/i18n/resolve-current-locale.spec.ts +65 -0
  31. package/__tests__/i18n/translate-map.spec.ts +19 -0
  32. package/__tests__/i18n/translate.spec.ts +91 -0
  33. package/__tests__/pages/details-page/create-content-metadata.spec.ts +43 -25
  34. package/__tests__/pages/details-page/get-translations.spec.ts +56 -0
  35. package/__tests__/utils/paths.spec.ts +116 -0
  36. package/__tests__/utils/urls.spec.ts +9 -4
  37. package/exports/content.ts +7 -2
  38. package/exports/i18n.ts +0 -1
  39. package/exports/index.ts +1 -5
  40. package/exports/utils.ts +0 -1
  41. package/package.json +16 -12
  42. package/src/astro-integration/config.ts +60 -49
  43. package/src/astro-integration/integration.ts +13 -24
  44. package/src/astro-integration/tailwind.ts +86 -0
  45. package/src/astro-integration/validators/validate-inline-translations.ts +51 -0
  46. package/src/astro-integration/validators/validate-languages.ts +39 -0
  47. package/src/astro-integration/virtual.d.ts +8 -6
  48. package/src/astro-integration/vite-plugin-lightnet-config.ts +29 -9
  49. package/src/components/CarouselSection.astro +7 -11
  50. package/src/components/CategoriesSection.astro +2 -2
  51. package/src/components/HighlightSection.astro +4 -7
  52. package/src/components/Icon.tsx +2 -2
  53. package/src/components/MediaGallerySection.astro +88 -68
  54. package/src/components/MediaList.astro +9 -7
  55. package/src/components/SearchInput.astro +7 -4
  56. package/src/components/Section.astro +7 -5
  57. package/src/components/VideoPlayer.astro +2 -3
  58. package/src/content/content-schema.ts +129 -142
  59. package/src/content/get-categories.ts +52 -28
  60. package/src/content/get-languages.ts +29 -8
  61. package/src/content/get-media-collections.ts +43 -0
  62. package/src/content/get-media-types.ts +41 -7
  63. package/src/content/query-media-items.ts +23 -13
  64. package/src/i18n/bcp-47.ts +8 -0
  65. package/src/i18n/get-locale-paths.ts +1 -3
  66. package/src/i18n/locals.d.ts +21 -3
  67. package/src/i18n/locals.ts +18 -11
  68. package/src/i18n/resolve-current-locale.ts +18 -0
  69. package/src/i18n/resolve-language.ts +10 -5
  70. package/src/i18n/translate-map.ts +70 -0
  71. package/src/i18n/translate.ts +68 -47
  72. package/src/layouts/Page.astro +5 -3
  73. package/src/layouts/components/LanguagePicker.astro +22 -17
  74. package/src/layouts/components/Menu.astro +2 -5
  75. package/src/layouts/components/MenuItem.astro +1 -1
  76. package/src/layouts/components/PageNavigation.astro +29 -29
  77. package/src/layouts/components/PageTitle.astro +23 -7
  78. package/src/pages/404Route.astro +2 -1
  79. package/src/pages/RootRoute.astro +6 -1
  80. package/src/pages/details-page/DefaultDetailsPage.astro +9 -2
  81. package/src/pages/details-page/DetailsPageRoute.astro +1 -2
  82. package/src/pages/details-page/components/AudioPanel.astro +7 -3
  83. package/src/pages/details-page/components/AudioPlayer.astro +2 -2
  84. package/src/pages/details-page/components/ContentSection.astro +67 -44
  85. package/src/pages/details-page/components/MediaCollection.astro +8 -4
  86. package/src/pages/details-page/components/MediaCollectionsSection.astro +3 -6
  87. package/src/pages/details-page/components/main-details/EditButton.astro +22 -10
  88. package/src/pages/details-page/components/main-details/OpenButton.astro +17 -12
  89. package/src/pages/details-page/components/main-details/ShareButton.astro +3 -2
  90. package/src/pages/details-page/components/more-details/Categories.astro +5 -3
  91. package/src/pages/details-page/components/more-details/Languages.astro +12 -7
  92. package/src/pages/details-page/utils/create-content-metadata.ts +24 -9
  93. package/src/pages/details-page/utils/get-translations.ts +6 -0
  94. package/src/pages/search-page/components/LoadingSkeleton.tsx +6 -5
  95. package/src/pages/search-page/components/SearchFilter.astro +10 -21
  96. package/src/pages/search-page/components/SearchFilter.tsx +2 -2
  97. package/src/pages/search-page/components/SearchList.astro +10 -7
  98. package/src/pages/search-page/components/SearchListItem.tsx +5 -4
  99. package/src/pages/search-page/hooks/use-search.ts +5 -2
  100. package/src/utils/lazy.ts +20 -0
  101. package/src/utils/paths.ts +40 -3
  102. package/src/utils/urls.ts +1 -2
  103. package/src/utils/verify-schema.ts +12 -10
  104. package/tailwind.config.ts +1 -25
  105. package/vitest.config.js +18 -2
  106. package/src/astro-integration/project-context.ts +0 -5
  107. package/src/content/compare-media-collection-items.ts +0 -24
  108. package/src/i18n/resolve-default-locale.ts +0 -19
  109. package/src/i18n/resolve-locales.ts +0 -5
  110. 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 projectContext from "virtual:lightnet/project-context"
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 projectContext.site as internal", () => {
14
- expect(isExternalUrl(`${projectContext.site}/page`)).toBe(false)
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
@@ -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
- ) => queryMediaItems(getCollection("media"), query ?? {})
18
+ ) =>
19
+ queryMediaItems(
20
+ getCollection("media"),
21
+ getCollection("media-collections"),
22
+ query ?? {},
23
+ )
package/exports/i18n.ts CHANGED
@@ -1,2 +1 @@
1
1
  export { getLocalePaths } from "../src/i18n/get-locale-paths"
2
- export { resolveDefaultLocale } from "../src/i18n/resolve-default-locale"
package/exports/index.ts CHANGED
@@ -1,6 +1,2 @@
1
- export {
2
- type Language,
3
- type LightnetConfig,
4
- type Link,
5
- } from "../src/astro-integration/config"
1
+ export { type LightnetConfig, type Link } from "../src/astro-integration/config"
6
2
  export { lightnet as default } from "../src/astro-integration/integration"
package/exports/utils.ts CHANGED
@@ -1,2 +1 @@
1
1
  export { detailsPagePath, searchPagePath } from "../src/utils/paths"
2
- export { verifySchema, verifySchemaAsync } from "../src/utils/verify-schema"
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": "3.12.2",
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": "^5.1.0",
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": "^4.4.2",
45
- "@astrojs/tailwind": "^6.0.2",
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.18",
50
- "daisyui": "^4.12.24",
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.8.5",
55
- "marked": "^16.4.2",
56
- "yaml": "^2.8.2"
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.0.18"
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 install --with-deps chromium && playwright test"
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
- * Can either be a translation key or a fixed string.
35
+ * Must define a value for the default site locale.
36
+ * Other configured site locales are optional.
16
37
  */
17
- label: z.string(),
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
- * Language Schema.
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
- * IETF BCP-47 language tag for this language.
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: z.string(),
60
+ code: bcp47Schema,
42
61
  /**
43
- * The name of the language that will be shown on the Website.
44
- *
45
- * Can either be a fixed string or a translation key.
62
+ * Display name for this language.
46
63
  */
47
- label: z.string(),
64
+ label: inlineTranslationSchema,
48
65
  /**
49
- * Should this language be used as a site language?
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
- * Should this language be used as the default site language?
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
- * An array of fallback language codes.
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.string().array().default([]),
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: z.string(),
119
+ title: inlineTranslationSchema,
127
120
  /**
128
- * All languages: content languages and site languages.
121
+ * Languages supported by this site.
129
122
  */
130
- languages: languageSchema.array(),
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
- * This can be a fixed string or a translation key.
151
+ * Must define a value for the default site locale.
152
+ * Other configured site locales are optional.
159
153
  */
160
- alt: z.string().optional(),
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 type Language = z.input<typeof languageSchema>
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 PreparedLightnetConfig = z.output<typeof configSchema>
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 { configSchema, type LightnetConfig } from "./config"
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
- configSchema,
30
+ extendedConfigSchema,
25
31
  lightnetConfig,
26
32
  "Invalid LightNet configuration",
27
- "Fix these errors on the LightNet configuration inside astro.config.mjs:",
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").PreparedLightnetConfig
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
+ }