@inglorious/ssx 1.8.0 → 1.10.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inglorious/ssx",
3
- "version": "1.8.0",
3
+ "version": "1.10.0",
4
4
  "description": "Server-Side-X. Xecution? Xperience? Who knows.",
5
5
  "author": "IceOnFire <antony.mistretta@gmail.com> (https://ingloriouscoderz.it)",
6
6
  "license": "MIT",
@@ -48,6 +48,7 @@
48
48
  },
49
49
  "sideEffects": false,
50
50
  "dependencies": {
51
+ "@inglorious/logo": "^2.1.0",
51
52
  "@lit-labs/ssr": "^4.0.0",
52
53
  "commander": "^14.0.2",
53
54
  "connect": "^3.7.0",
@@ -64,7 +65,7 @@
64
65
  "svgo": "^4.0.0",
65
66
  "vite": "^7.1.3",
66
67
  "vite-plugin-image-optimizer": "^2.0.3",
67
- "@inglorious/web": "4.3.0"
68
+ "@inglorious/web": "4.4.0"
68
69
  },
69
70
  "devDependencies": {
70
71
  "prettier": "^3.6.2",
@@ -56,7 +56,12 @@ describe("build", () => {
56
56
  loadManifest.mockResolvedValue(null) // First build
57
57
  getPages.mockResolvedValue([{ path: "/" }])
58
58
  hashEntities.mockResolvedValue("hash")
59
- generateStore.mockResolvedValue({})
59
+ generateStore.mockResolvedValue({
60
+ store: {},
61
+ entities: {},
62
+ hasTypesFile: false,
63
+ hasEntitiesFile: false,
64
+ })
60
65
  generatePages
61
66
  .mockResolvedValueOnce([{ path: "/", html: "<html></html>" }])
62
67
  .mockResolvedValueOnce([])
@@ -140,7 +140,11 @@ export async function build(options = {}) {
140
140
  // 8. Always regenerate client-side JavaScript (it's cheap and ensures consistency)
141
141
  console.log("\nšŸ“ Generating client scripts...\n")
142
142
 
143
- const app = generateApp(allPages, { ...mergedOptions, isDev: false })
143
+ const app = await generateApp(
144
+ allPages,
145
+ { ...mergedOptions, isDev: false },
146
+ loader,
147
+ )
144
148
  await fs.writeFile(path.join(outDir, "main.js"), app, "utf-8")
145
149
  console.log(` āœ“ main.js\n`)
146
150
 
@@ -14,11 +14,16 @@ import { extractPageMetadata } from "./metadata.js"
14
14
  * @param {Object} [options] - Generation options.
15
15
  * @param {boolean} [options.shouldGenerateHtml=true] - Whether to generate HTML.
16
16
  * @param {boolean} [options.shouldGenerateMetadata=true] - Whether to generate metadata.
17
+ * @param {Object} [options.originalEntities] - Original entities from entities.js.
17
18
  * @param {Function} [loader] - Optional loader function.
18
19
  * @returns {Promise<Array<Object>>} The processed pages with `html` and `metadata` properties added.
19
20
  */
20
21
  export async function generatePages(store, pages, options = {}, loader) {
21
- const { shouldGenerateHtml = true, shouldGenerateMetadata = true } = options
22
+ const {
23
+ shouldGenerateHtml = true,
24
+ shouldGenerateMetadata = true,
25
+ originalEntities = {},
26
+ } = options
22
27
  const load = loader || ((p) => import(pathToFileURL(path.resolve(p))))
23
28
 
24
29
  const api = store._api
@@ -43,6 +48,7 @@ export async function generatePages(store, pages, options = {}, loader) {
43
48
  if (shouldGenerateHtml) {
44
49
  const html = await renderPage(store, page, entity, {
45
50
  ...options,
51
+ originalEntities,
46
52
  wrap: true,
47
53
  })
48
54
  page.html = html
package/src/dev/index.js CHANGED
@@ -80,7 +80,11 @@ export async function dev(options = {}) {
80
80
  }
81
81
 
82
82
  // Generate and update the virtual app file BEFORE rendering
83
- const app = generateApp(pages, { ...mergedOptions, isDev: true })
83
+ const app = await generateApp(
84
+ pages,
85
+ { ...mergedOptions, isDev: true },
86
+ loader,
87
+ )
84
88
  virtualFiles.set("/main.js", app)
85
89
 
86
90
  // Invalidate the virtual module to ensure Vite picks up changes
@@ -41,10 +41,10 @@ export async function toHTML(store, renderFn, options = {}) {
41
41
  const layout = options.layout ?? defaultLayout
42
42
  let html = layout(finalHTML, options)
43
43
 
44
- if (options.ssxEntity) {
44
+ if (options.ssxPage) {
45
45
  html = html.replace(
46
46
  /<body[^>]*>/,
47
- `$&<script type="application/json" id="__SSX_ENTITY__">${JSON.stringify(options.ssxEntity)}</script>`,
47
+ `$&<script type="application/json" id="__SSX_PAGE__">${JSON.stringify(options.ssxPage)}</script>`,
48
48
  )
49
49
  }
50
50
 
@@ -52,6 +52,6 @@ export async function renderPage(store, page, entity, options = {}) {
52
52
  styles,
53
53
  head,
54
54
  scripts,
55
- ssxEntity: { [moduleName]: entity },
55
+ ssxPage: { [moduleName]: entity },
56
56
  })
57
57
  }
@@ -1,15 +1,24 @@
1
+ import { getStoreStuff } from "../store/stuff.js"
2
+
1
3
  /**
2
4
  * Generates the client-side entry point script.
3
5
  * This script hydrates the store with the initial state (entities) and sets up the router.
4
6
  *
5
7
  * @param {Array<Object>} pages - List of page objects to generate routes for.
6
8
  * @param {Object} [options] - Runtime options.
9
+ * @param {boolean} [options.hasTypesFile] - Whether src/store/types.js exists.
10
+ * @param {boolean} [options.hasEntitiesFile] - Whether src/store/entities.js exists.
7
11
  * @returns {string} The generated JavaScript code for the client entry point.
8
12
  */
9
- export function generateApp(pages, options = {}) {
13
+ export async function generateApp(pages, options = {}, loader) {
10
14
  const i18n = options.i18n || inferI18nFromPages(pages)
11
15
  const isDev = Boolean(options.isDev)
12
16
 
17
+ const types = await getStoreStuff("types", options, loader)
18
+ const hasTypesFile = Object.keys(types).length
19
+ const entities = await getStoreStuff("entities", options, loader)
20
+ const hasEntitiesFile = Object.keys(entities).length
21
+
13
22
  // Build client route map, including localized patterns (e.g. /it/about).
14
23
  const routesByPattern = new Map()
15
24
  for (const page of pages) {
@@ -22,10 +31,30 @@ export function generateApp(pages, options = {}) {
22
31
  }
23
32
  const routes = [...routesByPattern.values()]
24
33
 
25
- return `import { createDevtools, createStore, mount } from "@inglorious/web"
34
+ const typesImport = hasTypesFile
35
+ ? `import { types as additionalTypes } from "@/store/types.js"`
36
+ : ""
37
+
38
+ const entitiesImport = hasEntitiesFile
39
+ ? `import { entities as additionalEntities } from "@/store/entities.js"`
40
+ : ""
41
+
42
+ const typesAssignment = hasTypesFile
43
+ ? `Object.assign(types, additionalTypes)`
44
+ : ""
45
+
46
+ const entitiesAssignment = hasEntitiesFile
47
+ ? `Object.assign(entities, additionalEntities)`
48
+ : ""
49
+
50
+ return `import "@inglorious/web/hydrate"
51
+ import { createDevtools, createStore, mount } from "@inglorious/web"
26
52
  import { getRoute, router, setRoutes } from "@inglorious/web/router"
27
53
  import { getLocaleFromPath } from "@inglorious/ssx/i18n"
28
54
 
55
+ ${typesImport}
56
+ ${entitiesImport}
57
+
29
58
  const normalizePathname = (path = "/") => path.split("?")[0].split("#")[0]
30
59
  const normalizeRoutePath = (path = "/") => {
31
60
  const pathname = normalizePathname(path)
@@ -49,24 +78,18 @@ const path = normalizeRoutePath(window.location.pathname)
49
78
  const page = pages.find((page) => normalizeRoutePath(page.path) === path)
50
79
 
51
80
  const types = { router }
81
+ ${typesAssignment}
52
82
 
53
83
  const i18n = ${JSON.stringify(i18n, null, 2)}
54
84
  const isDev = ${JSON.stringify(isDev)}
55
85
 
56
- const ssxEntity = JSON.parse(document.getElementById("__SSX_ENTITY__").textContent)
57
-
58
86
  const entities = {
59
- router: {
60
- type: "router",
61
- path,
62
- route: page?.moduleName,
63
- },
64
- i18n: {
65
- type: "i18n",
66
- ...i18n,
67
- },
68
- ...ssxEntity,
87
+ router: { type: "router", path, route: page?.moduleName },
88
+ i18n: { type: "i18n", ...i18n },
69
89
  }
90
+ ${entitiesAssignment}
91
+ Object.assign(entities, JSON.parse(document.getElementById("__SSX_PAGE__").textContent))
92
+
70
93
 
71
94
  const middlewares = []
72
95
  if (isDev) {
@@ -75,7 +75,7 @@ describe("generateApp", () => {
75
75
  { ...page, path: "/pt/hello", locale: "pt" },
76
76
  ]
77
77
 
78
- const app = generateApp(localizedPages)
78
+ const app = await generateApp(localizedPages)
79
79
 
80
80
  expect(app).toContain(`"/hello": () => import("@/pages/hello.js")`)
81
81
  expect(app).toContain(`"/it/hello": () => import("@/pages/hello.js")`)
@@ -1,15 +1,15 @@
1
- import { existsSync } from "node:fs"
2
- import path from "node:path"
3
1
  import { pathToFileURL } from "node:url"
4
2
 
5
3
  import { createStore } from "@inglorious/web"
6
4
 
7
5
  import { getModuleName } from "../utils/module.js"
6
+ import { getStoreStuff } from "./stuff.js"
8
7
 
9
8
  /**
10
9
  * Generates the application store based on the provided pages and configuration.
11
10
  * It loads page modules to register their exported entities as store types.
12
- * It also attempts to load initial entities from an `entities.js` file in the root directory.
11
+ * It also attempts to load initial entities from an `entities.js` file and
12
+ * additional types from a `types.js` file in the store directory.
13
13
  *
14
14
  * @param {Array<Object>} pages - List of page objects containing file paths.
15
15
  * @param {Object} options - Configuration options.
@@ -18,34 +18,17 @@ import { getModuleName } from "../utils/module.js"
18
18
  * @returns {Promise<Object>} The initialized store instance.
19
19
  */
20
20
  export async function generateStore(pages = [], options = {}, loader) {
21
- const { rootDir = "." } = options
22
- const srcDir = path.join(rootDir, "src")
23
-
24
21
  const load = loader || ((p) => import(pathToFileURL(p)))
25
22
 
26
- const types = {}
23
+ const types = await getStoreStuff("types", options, loader)
24
+
27
25
  for (const page of pages) {
28
26
  const pageModule = await load(page.filePath)
29
27
  const name = getModuleName(pageModule)
30
28
  types[name] = pageModule[name]
31
29
  }
32
30
 
33
- let entities = {}
34
- const extensions = ["js", "ts"]
35
-
36
- for (const ext of extensions) {
37
- const fullPath = path.join(srcDir, "store", `entities.${ext}`)
38
-
39
- if (existsSync(fullPath)) {
40
- try {
41
- const module = await load(fullPath)
42
- entities = module.entities
43
- break
44
- } catch {
45
- // ignore and try next extension
46
- }
47
- }
48
- }
31
+ const entities = await getStoreStuff("entities", options, loader)
49
32
 
50
33
  const store = createStore({ types, entities, autoCreateEntities: true })
51
34
  return store
@@ -0,0 +1,87 @@
1
+ import { existsSync } from "node:fs"
2
+ import path from "node:path"
3
+ import { pathToFileURL } from "node:url"
4
+
5
+ export async function getStoreStuff(name, options, loader) {
6
+ const { rootDir = "." } = options
7
+ const srcDir = path.join(rootDir, "src")
8
+
9
+ const load = loader || ((p) => import(pathToFileURL(p)))
10
+
11
+ const stuff = {}
12
+ const extensions = ["js", "ts"]
13
+
14
+ for (const ext of extensions) {
15
+ const stuffPath = path.join(srcDir, "store", `${name}.${ext}`)
16
+
17
+ if (existsSync(stuffPath)) {
18
+ try {
19
+ const module = await load(stuffPath)
20
+ if (module[name]) {
21
+ Object.assign(stuff, module[name])
22
+ }
23
+ break
24
+ } catch {
25
+ // ignore and try next extension
26
+ }
27
+ }
28
+ }
29
+
30
+ return stuff
31
+ }
32
+
33
+ export async function getTypes(options, loader) {
34
+ const { rootDir = "." } = options
35
+ const srcDir = path.join(rootDir, "src")
36
+
37
+ const load = loader || ((p) => import(pathToFileURL(p)))
38
+
39
+ const types = {}
40
+ const extensions = ["js", "ts"]
41
+
42
+ for (const ext of extensions) {
43
+ const typesPath = path.join(srcDir, "store", `types.${ext}`)
44
+
45
+ if (existsSync(typesPath)) {
46
+ try {
47
+ const module = await load(typesPath)
48
+ if (module.types) {
49
+ Object.assign(types, module.types)
50
+ }
51
+ break
52
+ } catch {
53
+ // ignore and try next extension
54
+ }
55
+ }
56
+ }
57
+
58
+ return types
59
+ }
60
+
61
+ export async function getEntities(options, loader) {
62
+ const { rootDir = "." } = options
63
+ const srcDir = path.join(rootDir, "src")
64
+
65
+ const load = loader || ((p) => import(pathToFileURL(p)))
66
+
67
+ const entities = {}
68
+ const extensions = ["js", "ts"]
69
+
70
+ for (const ext of extensions) {
71
+ const entitiesPath = path.join(srcDir, "store", `entities.${ext}`)
72
+
73
+ if (existsSync(entitiesPath)) {
74
+ try {
75
+ const module = await load(entitiesPath)
76
+ if (module.entities) {
77
+ Object.assign(entities, module.entities)
78
+ }
79
+ break
80
+ } catch {
81
+ // ignore and try next extension
82
+ }
83
+ }
84
+ }
85
+
86
+ return entities
87
+ }