@ossy/app 1.24.1 → 1.25.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/cli/build.task.js CHANGED
@@ -14,10 +14,11 @@ import getPlatformFiles, {
14
14
  API_FILE_PATTERN,
15
15
  TASK_FILE_PATTERN,
16
16
  RESOURCE_FILE_PATTERN,
17
+ COMPONENT_FILE_PATTERN,
17
18
  } from './get-platform-files.task.js'
18
19
  import { manifestPlugin } from './manifest-plugin.js'
19
20
 
20
- export { PAGE_FILE_PATTERN, API_FILE_PATTERN, TASK_FILE_PATTERN, RESOURCE_FILE_PATTERN }
21
+ export { PAGE_FILE_PATTERN, API_FILE_PATTERN, TASK_FILE_PATTERN, RESOURCE_FILE_PATTERN, COMPONENT_FILE_PATTERN }
21
22
 
22
23
  // Generated entry stubs live under `build/.ossy/entries/`. Putting them
23
24
  // inside `build/` means they're cleaned automatically with every build,
@@ -67,7 +68,7 @@ function entryNameFromSource (sourcePath, srcDir, packageName) {
67
68
  }
68
69
 
69
70
  function stubFileNameFor (kind, sourcePath, srcDir, packageName) {
70
- const ext = kind === 'page' ? '.entry.jsx' : '.entry.js'
71
+ const ext = (kind === 'page' || kind === 'component') ? '.entry.jsx' : '.entry.js'
71
72
  return entryNameFromSource(sourcePath, srcDir, packageName) + ext
72
73
  }
73
74
 
@@ -133,10 +134,23 @@ function generateResourceStub ({ stubAbs, sourceAbs }) {
133
134
  ].join('\n')
134
135
  }
135
136
 
137
+ // Components are re-exported as-is — the manifest plugin reads `metadata.id`
138
+ // from the bundled output and records the entry URL. Components don't get
139
+ // route stubs or hydration bootstrapping; they're loaded on-demand at runtime.
140
+ function generateComponentStub ({ stubAbs, sourceAbs }) {
141
+ const importPath = relImport(stubAbs, sourceAbs)
142
+ return [
143
+ '// Generated by @ossy/app — do not edit',
144
+ `export { default, metadata } from '${importPath}'`,
145
+ '',
146
+ ].join('\n')
147
+ }
148
+
136
149
  function stubFor (kind, args) {
137
150
  if (kind === 'page') return generatePageStub(args)
138
151
  if (kind === 'api') return generateApiStub(args)
139
152
  if (kind === 'resource') return generateResourceStub(args)
153
+ if (kind === 'component') return generateComponentStub(args)
140
154
  return generateTaskStub(args)
141
155
  }
142
156
 
@@ -177,6 +191,7 @@ export async function build (cliArgs = []) {
177
191
  ...platformFiles.apis,
178
192
  ...platformFiles.tasks,
179
193
  ...platformFiles.resources,
194
+ ...platformFiles.components,
180
195
  ]
181
196
 
182
197
  const entriesByStub = new Map()
@@ -5,9 +5,10 @@ export const PAGE_FILE_PATTERN = /\.page\.(jsx?|tsx?)$/
5
5
  export const API_FILE_PATTERN = /\.api\.(mjs|cjs|js)$/
6
6
  export const TASK_FILE_PATTERN = /\.task\.(mjs|cjs|js)$/
7
7
  export const RESOURCE_FILE_PATTERN = /\.resource\.(mjs|cjs|js)$/
8
+ export const COMPONENT_FILE_PATTERN = /\.component\.(jsx?|tsx?)$/
8
9
 
9
10
  /**
10
- * @typedef {'page' | 'api' | 'task' | 'resource'} EntryKind
11
+ * @typedef {'page' | 'api' | 'task' | 'resource' | 'component'} EntryKind
11
12
  *
12
13
  * @typedef {object} PlatformEntry
13
14
  * @property {EntryKind} kind Which bucket this entry lives in.
@@ -16,10 +17,11 @@ export const RESOURCE_FILE_PATTERN = /\.resource\.(mjs|cjs|js)$/
16
17
  * @property {string} [packageSrcDir] Absolute path to the package's `src/` dir, set only for installed-package entries.
17
18
  *
18
19
  * @typedef {object} PlatformFiles
19
- * @property {PlatformEntry[]} pages Discovered `*.page.{jsx,tsx,...}` entries.
20
- * @property {PlatformEntry[]} apis Discovered `*.api.{js,mjs,cjs}` entries.
21
- * @property {PlatformEntry[]} tasks Discovered `*.task.{js,mjs,cjs}` entries.
22
- * @property {PlatformEntry[]} resources Discovered `*.resource.{js,mjs,cjs}` entries.
20
+ * @property {PlatformEntry[]} pages Discovered `*.page.{jsx,tsx,...}` entries.
21
+ * @property {PlatformEntry[]} apis Discovered `*.api.{js,mjs,cjs}` entries.
22
+ * @property {PlatformEntry[]} tasks Discovered `*.task.{js,mjs,cjs}` entries.
23
+ * @property {PlatformEntry[]} resources Discovered `*.resource.{js,mjs,cjs}` entries.
24
+ * @property {PlatformEntry[]} components Discovered `*.component.{jsx,tsx,...}` entries.
23
25
  */
24
26
  export function discoverFilesByPattern (srcDir, filePattern) {
25
27
  const dir = path.resolve(srcDir)
@@ -42,6 +44,7 @@ function classifyFile (absPath) {
42
44
  if (API_FILE_PATTERN.test(base)) return 'api'
43
45
  if (TASK_FILE_PATTERN.test(base)) return 'task'
44
46
  if (RESOURCE_FILE_PATTERN.test(base)) return 'resource'
47
+ if (COMPONENT_FILE_PATTERN.test(base)) return 'component'
45
48
  return null
46
49
  }
47
50
 
@@ -58,6 +61,7 @@ export function metadataIdFromFile (absPath, srcDir) {
58
61
  .replace(PAGE_FILE_PATTERN, '')
59
62
  .replace(API_FILE_PATTERN, '')
60
63
  .replace(TASK_FILE_PATTERN, '')
64
+ .replace(COMPONENT_FILE_PATTERN, '')
61
65
  if (noExt === 'home' || noExt === 'index') return 'home'
62
66
  return noExt.replace(/\//g, '-')
63
67
  }
@@ -109,6 +113,7 @@ export function discoverInstalledPackageEntries (projectRoot) {
109
113
  ...discoverFilesByPattern(packageSrcDir, API_FILE_PATTERN),
110
114
  ...discoverFilesByPattern(packageSrcDir, TASK_FILE_PATTERN),
111
115
  ...discoverFilesByPattern(packageSrcDir, RESOURCE_FILE_PATTERN),
116
+ ...discoverFilesByPattern(packageSrcDir, COMPONENT_FILE_PATTERN),
112
117
  ]
113
118
  for (const sourcePath of files) {
114
119
  const stat = fs.statSync(sourcePath)
@@ -152,14 +157,15 @@ export function discoverInstalledPackageEntries (projectRoot) {
152
157
  * @returns {Promise<PlatformFiles>}
153
158
  */
154
159
  export default async function getPlatformFiles (srcDir) {
155
- const out = { pages: [], apis: [], tasks: [], resources: [] }
156
- const bucketByKind = { page: 'pages', api: 'apis', task: 'tasks', resource: 'resources' }
160
+ const out = { pages: [], apis: [], tasks: [], resources: [], components: [] }
161
+ const bucketByKind = { page: 'pages', api: 'apis', task: 'tasks', resource: 'resources', component: 'components' }
157
162
 
158
163
  const localFiles = [
159
164
  ...discoverFilesByPattern(srcDir, PAGE_FILE_PATTERN),
160
165
  ...discoverFilesByPattern(srcDir, API_FILE_PATTERN),
161
166
  ...discoverFilesByPattern(srcDir, TASK_FILE_PATTERN),
162
167
  ...discoverFilesByPattern(srcDir, RESOURCE_FILE_PATTERN),
168
+ ...discoverFilesByPattern(srcDir, COMPONENT_FILE_PATTERN),
163
169
  ]
164
170
  for (const sourcePath of localFiles) {
165
171
  const stat = fs.statSync(sourcePath)
@@ -38,8 +38,13 @@ import { metadataIdFromFile, defaultPageRoute } from './get-platform-files.task.
38
38
  * @property {string} entry URL the platform serves the bundle from
39
39
  * (e.g. `/static/home.page-7f2a.js`).
40
40
  *
41
+ * @typedef {object} ComponentManifestEntry
42
+ * @property {string} id Unique component id (from `metadata.id`).
43
+ * @property {string} entry URL the platform serves the component bundle from.
44
+ *
41
45
  * @typedef {object} Manifest
42
46
  * @property {ManifestEntry[]} entries Flat, ordered list of every routable thing.
47
+ * @property {ComponentManifestEntry[]} components Discovered `*.component.*` entries, keyed by id.
43
48
  * @property {object[]} resourceTemplates Each `*.resource.js` file's default-exported template object.
44
49
  * @property {object} config Inlined `src/config.js` default export.
45
50
  */
@@ -86,7 +91,9 @@ export function manifestPlugin ({ entriesByStub, srcDir, staticOutDir, configVal
86
91
  const entries = []
87
92
  /** @type {object[]} */
88
93
  const resourceTemplates = []
89
- const seenIds = { page: new Set(), api: new Set(), task: new Set() }
94
+ /** @type {import('./get-platform-files.task.js').ComponentManifestEntry[]} */
95
+ const components = []
96
+ const seenIds = { page: new Set(), api: new Set(), task: new Set(), component: new Set() }
90
97
  const seenResourceIds = new Set()
91
98
 
92
99
  for (const fileName of Object.keys(bundle)) {
@@ -147,6 +154,11 @@ export function manifestPlugin ({ entriesByStub, srcDir, staticOutDir, configVal
147
154
  }
148
155
  seenIds[entryInfo.kind].add(id)
149
156
 
157
+ if (entryInfo.kind === 'component') {
158
+ components.push({ id, entry: url })
159
+ continue
160
+ }
161
+
150
162
  if (entryInfo.kind === 'page') {
151
163
  const pagePath = rawMeta.path !== undefined ? rawMeta.path : defaultPageRoute(id)
152
164
  entries.push({ type: 'page', id, path: pagePath, title: rawMeta.title, entry: url })
@@ -169,7 +181,7 @@ export function manifestPlugin ({ entriesByStub, srcDir, staticOutDir, configVal
169
181
  }
170
182
 
171
183
  /** @type {Manifest} */
172
- const manifest = { entries, resourceTemplates, config: configValue || {} }
184
+ const manifest = { entries, components, resourceTemplates, config: configValue || {} }
173
185
  fs.mkdirSync(path.dirname(manifestPath), { recursive: true })
174
186
  fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2), 'utf8')
175
187
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ossy/app",
3
- "version": "1.24.1",
3
+ "version": "1.25.0",
4
4
  "description": "",
5
5
  "source": "./src/index.js",
6
6
  "main": "./src/index.js",
@@ -37,15 +37,15 @@
37
37
  "@babel/eslint-parser": "^7.15.8",
38
38
  "@babel/preset-react": "^7.26.3",
39
39
  "@babel/register": "^7.25.9",
40
- "@ossy/connected-components": "^1.24.1",
41
- "@ossy/design-system": "^1.24.1",
40
+ "@ossy/connected-components": "^1.25.0",
41
+ "@ossy/design-system": "^1.25.0",
42
42
  "@ossy/pages": "^1.23.0",
43
- "@ossy/platform": "^1.23.1",
44
- "@ossy/router": "^1.24.1",
45
- "@ossy/router-react": "^1.24.1",
46
- "@ossy/sdk": "^1.24.1",
47
- "@ossy/sdk-react": "^1.24.1",
48
- "@ossy/themes": "^1.24.1",
43
+ "@ossy/platform": "^1.24.0",
44
+ "@ossy/router": "^1.25.0",
45
+ "@ossy/router-react": "^1.25.0",
46
+ "@ossy/sdk": "^1.25.0",
47
+ "@ossy/sdk-react": "^1.25.0",
48
+ "@ossy/themes": "^1.25.0",
49
49
  "@rollup/plugin-alias": "^6.0.0",
50
50
  "@rollup/plugin-babel": "^7.0.0",
51
51
  "@rollup/plugin-commonjs": "^29.0.0",
@@ -79,5 +79,5 @@
79
79
  "README.md",
80
80
  "tsconfig.json"
81
81
  ],
82
- "gitHead": "c4dfd4a2cc4a6977ce00dc223c91dbaecc3348ba"
82
+ "gitHead": "9b116b6a4fadd570c1e6c0c6d9a4867448f7410c"
83
83
  }
@@ -1,6 +1,31 @@
1
1
  import { createElement } from 'react'
2
2
  import { App } from '@ossy/connected-components'
3
3
 
4
+ /**
5
+ * Dynamically imports each component bundle listed in `entries` and returns a
6
+ * `Record<id, ComponentType>` suitable for passing to `App` as `components`.
7
+ *
8
+ * Entries that fail to import (network error, missing export, etc.) are silently
9
+ * skipped so a broken component never prevents the page from rendering.
10
+ *
11
+ * @param {Array<{ id: string, entry: string }>} entries
12
+ * @returns {Promise<Record<string, import('react').ComponentType>>}
13
+ */
14
+ export async function loadComponents (entries = []) {
15
+ const map = {}
16
+ await Promise.all(
17
+ entries.map(async ({ id, entry }) => {
18
+ try {
19
+ const mod = await import(entry)
20
+ if (mod && mod.default) map[id] = mod.default
21
+ } catch {
22
+ // silently skip broken component bundles
23
+ }
24
+ }),
25
+ )
26
+ return map
27
+ }
28
+
4
29
  function buildTree ({ Component, metadata, props }) {
5
30
  const lang = props.htmlLang || props.defaultLanguage || 'en'
6
31
  return createElement(
@@ -50,7 +75,9 @@ export function createPageEntry (pageModule, options = {}) {
50
75
  import('node:stream'),
51
76
  ])
52
77
 
53
- const tree = buildTree({ Component, metadata, props })
78
+ const { componentEntries = [], ...pageProps } = props
79
+ const components = await loadComponents(componentEntries)
80
+ const tree = buildTree({ Component, metadata, props: { ...pageProps, components } })
54
81
  const bootstrapUrl = toBootstrapUrl(entryUrl)
55
82
  const bootstrapModules = bootstrapUrl ? [bootstrapUrl] : []
56
83
  const bootstrapScriptContent =
@@ -82,8 +109,12 @@ export function createPageEntry (pageModule, options = {}) {
82
109
  if (typeof document === 'undefined' || typeof window === 'undefined') return
83
110
  hydrated = true
84
111
  const props = window.__OSSY__ || {}
85
- import('react-dom/client').then(({ hydrateRoot }) => {
86
- hydrateRoot(document, buildTree({ Component, metadata, props }))
112
+ const { componentEntries = [], ...pageProps } = props
113
+ Promise.all([
114
+ import('react-dom/client'),
115
+ loadComponents(componentEntries),
116
+ ]).then(([{ hydrateRoot }, components]) => {
117
+ hydrateRoot(document, buildTree({ Component, metadata, props: { ...pageProps, components } }))
87
118
  })
88
119
  }
89
120