@ossy/platform 1.14.2 → 1.15.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": "@ossy/platform",
3
- "version": "1.14.2",
3
+ "version": "1.15.0",
4
4
  "description": "Ossy application server runtime",
5
5
  "repository": {
6
6
  "type": "git",
@@ -22,17 +22,15 @@
22
22
  "author": "Ossy <yourfriends@ossy.se> (https://ossy.se)",
23
23
  "license": "MIT",
24
24
  "dependencies": {
25
- "@ossy/connected-components": "^1.15.2",
26
- "@ossy/router": "^1.15.2",
27
- "@ossy/sdk": "^1.15.2",
25
+ "@ossy/router": "^1.16.0",
26
+ "@ossy/sdk": "^1.16.0",
28
27
  "cookie-parser": "^1.4.7",
28
+ "dotenv": ">=16.0.0 <17.0.0",
29
29
  "express": ">=5.0.0 <6.0.0",
30
- "morgan": ">=1.10.1 <2.0.0",
31
- "react": ">=19.0.0 <20.0.0",
32
- "react-dom": ">=19.0.0 <20.0.0"
30
+ "morgan": ">=1.10.1 <2.0.0"
33
31
  },
34
32
  "files": [
35
33
  "src"
36
34
  ],
37
- "gitHead": "6a15f3ac4cafdb190beed11e4a93adc0ba28c245"
35
+ "gitHead": "db2ab43d6b3266c59eb53c8046811e0acb3f87f1"
38
36
  }
package/src/server.js CHANGED
@@ -6,12 +6,9 @@ import morgan from 'morgan'
6
6
  import { Router as OssyRouter } from '@ossy/router'
7
7
  import cookieParser from 'cookie-parser'
8
8
  import { ProxyInternal } from './proxy-internal.js'
9
- import getPlatformFiles, {
10
- PAGE_FILE_PATTERN,
11
- API_FILE_PATTERN,
12
- } from './getPlatformFiles.task.js'
13
9
 
14
10
  const DEFAULT_PORT = 3000
11
+ const MANIFEST_FILE = 'manifest.json'
15
12
 
16
13
  function parsePortFromArgv (argv) {
17
14
  const flagIdx = argv.findIndex((a) => a === '--port' || a === '-p')
@@ -27,92 +24,42 @@ function resolvePort (explicit) {
27
24
  return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_PORT
28
25
  }
29
26
 
30
- async function importModule (absPath) {
31
- return import(pathToFileURL(absPath).href)
32
- }
33
-
34
- async function loadRoutes (buildDir) {
35
- const files = await getPlatformFiles(buildDir)
36
- const pages = []
37
- const apis = []
38
- const pageModules = new Map()
39
- const apiModules = new Map()
40
-
41
- for (const file of files) {
42
- const basename = path.basename(file)
43
- const isPage = PAGE_FILE_PATTERN.test(basename)
44
- const isApi = API_FILE_PATTERN.test(basename)
45
- if (!isPage && !isApi) continue
46
-
47
- const mod = await importModule(file)
48
- const metadata = mod.metadata || {}
49
- const id = metadata.id
50
- const routePath = metadata.path
51
- if (!id || !routePath) continue
52
-
53
- const entry = { id, path: routePath, title: metadata.title }
54
- if (isPage) {
55
- pages.push(entry)
56
- pageModules.set(id, mod)
57
- } else {
58
- apis.push(entry)
59
- apiModules.set(id, mod)
60
- }
27
+ function loadManifest (buildDir) {
28
+ const manifestPath = path.join(buildDir, MANIFEST_FILE)
29
+ if (!fs.existsSync(manifestPath)) {
30
+ throw new Error(`[@ossy/platform][server] Build manifest not found: ${manifestPath}. Run \`app build\` first.`)
31
+ }
32
+ const raw = fs.readFileSync(manifestPath, 'utf8')
33
+ const manifest = JSON.parse(raw)
34
+ // The manifest is a flat `entries` array discriminated by `type`. Bucket
35
+ // them once at boot so the per-request hot path stays a simple lookup.
36
+ const entries = Array.isArray(manifest.entries) ? manifest.entries : []
37
+ const pages = entries.filter((e) => e.type === 'page')
38
+ const apis = entries.filter((e) => e.type === 'api')
39
+ const tasks = entries.filter((e) => e.type === 'task')
40
+ return {
41
+ entries,
42
+ pages,
43
+ apis,
44
+ tasks,
45
+ config: manifest.config || {},
61
46
  }
62
-
63
- return { pages, apis, pageModules, apiModules }
64
47
  }
65
48
 
66
- async function loadConfig (buildDir) {
67
- const configPath = path.resolve(buildDir, 'config.js')
68
- if (!fs.existsSync(configPath)) return {}
69
- const mod = await importModule(configPath)
70
- return mod.default || {}
71
- }
72
-
73
- async function loadMiddleware (buildDir) {
74
- const mwPath = path.resolve(buildDir, 'middleware.js')
75
- if (!fs.existsSync(mwPath)) return []
76
- const mod = await importModule(mwPath)
77
- const value = mod.default
78
- if (Array.isArray(value)) return value
79
- if (typeof value === 'function') return [value]
80
- return []
49
+ // `entry` URLs in the manifest are public-relative (`/static/<file>`). Translate
50
+ // them to local file URLs for `import()` while passing absolute URLs (e.g. CDN
51
+ // hosted bundles) through unchanged.
52
+ export function resolveEntryUrl (entryUrl, buildDir) {
53
+ if (!entryUrl) throw new Error('[@ossy/platform][server] Entry URL missing in manifest entry')
54
+ if (/^[a-z][a-z0-9+.-]*:\/\//i.test(entryUrl)) return entryUrl
55
+ const absolute = path.join(buildDir, 'public', entryUrl.startsWith('/') ? entryUrl.slice(1) : entryUrl)
56
+ return pathToFileURL(absolute).href
81
57
  }
82
58
 
83
59
  function cloneSerializable (value) {
84
60
  return value == null ? value : JSON.parse(JSON.stringify(value))
85
61
  }
86
62
 
87
- async function renderPage ({ Component, metadata, appConfig }) {
88
- const [{ createElement }, { renderToString }, { App }] = await Promise.all([
89
- import('react'),
90
- import('react-dom/server'),
91
- import('@ossy/connected-components'),
92
- ])
93
-
94
- const appProps = {
95
- ...appConfig,
96
- theme: cloneSerializable(appConfig.theme),
97
- themes: cloneSerializable(appConfig.themes),
98
- resourceTemplates: cloneSerializable(appConfig.resourceTemplates),
99
- }
100
-
101
- const element = createElement(
102
- 'html',
103
- { lang: appProps.htmlLang || appProps.defaultLanguage || 'en' },
104
- createElement(
105
- 'head',
106
- null,
107
- createElement('meta', { charSet: 'utf-8' }),
108
- createElement('title', null, metadata?.title || appProps.documentTitle || ''),
109
- ),
110
- createElement(App, appProps, createElement(Component, appProps)),
111
- )
112
-
113
- return '<!doctype html>' + renderToString(element)
114
- }
115
-
116
63
  export async function startServer (options = {}) {
117
64
  const cwd = options.cwd ? path.resolve(options.cwd) : process.cwd()
118
65
  const buildDir = path.resolve(cwd, options.buildDir || 'build')
@@ -123,16 +70,30 @@ export async function startServer (options = {}) {
123
70
  throw new Error(`[@ossy/platform][server] Build directory not found: ${buildDir}. Run \`app build\` first.`)
124
71
  }
125
72
 
126
- const [config, userMiddleware, routes] = await Promise.all([
127
- loadConfig(buildDir),
128
- loadMiddleware(buildDir),
129
- loadRoutes(buildDir),
130
- ])
73
+ const manifest = loadManifest(buildDir)
74
+ const config = manifest.config
131
75
 
132
- const { pages, apis, pageModules, apiModules } = routes
76
+ const supportedLanguages = Array.isArray(config.supportedLanguages) ? config.supportedLanguages : []
77
+ const defaultLanguage = config.defaultLanguage
133
78
 
134
- const app = express()
79
+ const pageRouter = OssyRouter.of({
80
+ pages: manifest.pages,
81
+ supportedLanguages,
82
+ defaultLanguage,
83
+ })
84
+ const apiRouter = OssyRouter.of({ pages: manifest.apis })
85
+
86
+ const moduleCache = new Map()
87
+ async function loadEntry (entryUrl) {
88
+ if (moduleCache.has(entryUrl)) return moduleCache.get(entryUrl)
89
+ const promise = import(resolveEntryUrl(entryUrl, buildDir))
90
+ moduleCache.set(entryUrl, promise)
91
+ return promise
92
+ }
93
+
94
+ const userMiddleware = await loadMiddleware(buildDir)
135
95
 
96
+ const app = express()
136
97
  app.use(morgan('tiny'))
137
98
  app.use(express.json({ strict: false }))
138
99
  app.use(cookieParser(process.env.OSSY_COOKIE_SECRET || 'default_secret'))
@@ -150,23 +111,18 @@ export async function startServer (options = {}) {
150
111
  if (fs.existsSync(publicDir)) app.use(express.static(publicDir))
151
112
  app.use(ProxyInternal())
152
113
 
153
- const apiRouter = OssyRouter.of({ pages: apis })
154
- const pageRouter = OssyRouter.of({
155
- pages,
156
- supportedLanguages: Array.isArray(config.supportedLanguages) ? config.supportedLanguages : [],
157
- defaultLanguage: config.defaultLanguage,
158
- })
159
-
160
114
  app.all('*all', async (req, res) => {
161
115
  const requestUrl = req.originalUrl || '/'
162
116
  try {
163
117
  const apiRoute = apiRouter.getPageByUrl(requestUrl)
164
118
  if (apiRoute) {
165
- const mod = apiModules.get(apiRoute.id)
166
- const handler = mod?.default
167
- if (typeof handler === 'function') {
168
- await handler(req, res)
169
- return
119
+ const apiEntry = manifest.apis.find((a) => a.id === apiRoute.id)
120
+ if (apiEntry) {
121
+ const mod = await loadEntry(apiEntry.entry)
122
+ if (typeof mod.handle === 'function') {
123
+ await mod.handle(req, res)
124
+ return
125
+ }
170
126
  }
171
127
  }
172
128
 
@@ -177,28 +133,28 @@ export async function startServer (options = {}) {
177
133
 
178
134
  const pageRoute = pageRouter.getPageByUrl(requestUrl)
179
135
  if (pageRoute) {
180
- const mod = pageModules.get(pageRoute.id)
181
- const Component = mod?.default
182
- if (typeof Component !== 'function') {
136
+ const pageEntry = manifest.pages.find((p) => p.id === pageRoute.id)
137
+ if (!pageEntry) {
138
+ res.status(404).send('Not found')
139
+ return
140
+ }
141
+ const mod = await loadEntry(pageEntry.entry)
142
+ if (typeof mod.render !== 'function') {
183
143
  res.status(503).type('text').send('SSR runtime unavailable')
184
144
  return
185
145
  }
186
- const appConfig = {
146
+ const props = {
187
147
  ...config,
148
+ theme: cloneSerializable(config?.theme),
149
+ themes: cloneSerializable(config?.themes),
150
+ resourceTemplates: cloneSerializable(config?.resourceTemplates),
188
151
  url: requestUrl,
189
- theme: config?.theme || 'light',
190
152
  isAuthenticated: !!req.isAuthenticated,
191
- workspaceId: config?.workspaceId,
192
- apiUrl: config?.apiUrl,
193
- pages: pages.map((page) => ({ id: page?.id, path: page?.path })),
153
+ pages: manifest.pages.map((page) => ({ id: page.id, path: page.path })),
194
154
  pageId: pageRoute.id,
195
155
  sidebarPrimaryCollapsed: false,
196
156
  }
197
- const html = await renderPage({
198
- Component,
199
- metadata: mod.metadata,
200
- appConfig,
201
- })
157
+ const html = await mod.render(props)
202
158
  res.status(200).type('html').send(html)
203
159
  return
204
160
  }
@@ -239,8 +195,22 @@ export async function startServer (options = {}) {
239
195
  process.on('SIGTERM', () => handleShutdown('SIGTERM'))
240
196
 
241
197
  const lifetime = new Promise(() => {})
242
-
243
198
  return { app, server, port, close: closeServer, lifetime }
244
199
  }
245
200
 
201
+ async function loadMiddleware (buildDir) {
202
+ const candidates = [
203
+ path.resolve(buildDir, 'public', 'static', 'middleware.js'),
204
+ path.resolve(buildDir, 'middleware.js'),
205
+ ]
206
+ for (const candidate of candidates) {
207
+ if (!fs.existsSync(candidate)) continue
208
+ const mod = await import(pathToFileURL(candidate).href)
209
+ const value = mod.default
210
+ if (Array.isArray(value)) return value
211
+ if (typeof value === 'function') return [value]
212
+ }
213
+ return []
214
+ }
215
+
246
216
  export default startServer
@@ -4,7 +4,14 @@ import path from 'node:path'
4
4
  import { fileURLToPath } from 'node:url'
5
5
  import { runWorkerScheduler } from './worker-runtime.js'
6
6
 
7
- const __ossyDir = path.dirname(fileURLToPath(import.meta.url)) + '/.ossy'
8
- const tasks = JSON.parse(fs.readFileSync(path.join(__ossyDir, 'tasks.generated.json'), 'utf8')) ?? []
7
+ const __dirname = path.dirname(fileURLToPath(import.meta.url))
8
+ const buildDir = process.env.OSSY_BUILD_DIR
9
+ ? path.resolve(process.env.OSSY_BUILD_DIR)
10
+ : path.resolve(__dirname, '..', '..', '..', 'build')
11
+ const manifestPath = path.join(buildDir, 'manifest.json')
9
12
 
10
- runWorkerScheduler(tasks, __ossyDir)
13
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'))
14
+ const entries = Array.isArray(manifest.entries) ? manifest.entries : []
15
+ const tasks = entries.filter((e) => e.type === 'task')
16
+
17
+ runWorkerScheduler(tasks, buildDir)
@@ -2,11 +2,18 @@ import path from 'node:path'
2
2
  import { pathToFileURL } from 'node:url'
3
3
  import { SDK } from '@ossy/sdk'
4
4
 
5
+ function resolveTaskEntryUrl (entryUrl, buildDir) {
6
+ if (!entryUrl) throw new Error('[@ossy/platform][worker] Task entry URL missing')
7
+ if (/^[a-z][a-z0-9+.-]*:\/\//i.test(entryUrl)) return entryUrl
8
+ const trimmed = entryUrl.startsWith('/') ? entryUrl.slice(1) : entryUrl
9
+ return pathToFileURL(path.join(buildDir, 'public', trimmed)).href
10
+ }
11
+
5
12
  /**
6
- * @param {Array<{ type: string, module: string }>} tasks
7
- * @param {string} ossyDir - absolute path to the .ossy directory
13
+ * @param {Array<{ id: string, entry: string }>} tasks
14
+ * @param {string} buildDir - absolute path to the build directory
8
15
  */
9
- export function runWorkerScheduler (tasks, ossyDir) {
16
+ export function runWorkerScheduler (tasks, buildDir) {
10
17
  const sdk = SDK.of({
11
18
  workspaceId: process.env.OSSY_WORKSPACE_ID,
12
19
  apiUrl: process.env.OSSY_API_URL,
@@ -92,20 +99,25 @@ export function runWorkerScheduler (tasks, ossyDir) {
92
99
  console.log(`Processing ${jobList.length} jobs`)
93
100
  for (const job of jobList) {
94
101
  console.log(`Processing job ${job.id}`)
95
- const task = tasks.find((t) => t.type === job.type)
102
+ const task = tasks.find((t) => t.id === job.type)
96
103
  if (!task) {
97
104
  console.log('No handler found for job', job.id)
98
105
  continue
99
106
  }
100
107
 
101
- console.log(`Handler found for ${task.type}`)
108
+ console.log(`Handler found for ${task.id}`)
102
109
  try {
103
- const mod = await import(pathToFileURL(path.resolve(ossyDir, task.module)).href)
110
+ const mod = await import(resolveTaskEntryUrl(task.entry, buildDir))
104
111
  const jobSdk = SDK.of({
105
112
  workspaceId: job.belongsTo,
106
113
  authorization: process.env.OSSY_API_TOKEN,
107
114
  })
108
- await mod.default({ sdk: jobSdk, job }).catch(() => {})
115
+ const runner = typeof mod.run === 'function' ? mod.run : mod.default
116
+ if (typeof runner !== 'function') {
117
+ console.log(`Task ${task.id} has no callable handler`)
118
+ continue
119
+ }
120
+ await runner({ sdk: jobSdk, job }).catch(() => {})
109
121
  } catch (error) {
110
122
  console.error(error)
111
123
  console.log('Failed to processing job')
@@ -1,50 +0,0 @@
1
- import fs from 'node:fs'
2
- import path from 'node:path'
3
-
4
- export const PAGE_FILE_PATTERN = /\.page\.(jsx?|tsx?)$/
5
- export const API_FILE_PATTERN = /\.api\.(mjs|cjs|js)$/
6
- export const TASK_FILE_PATTERN = /\.task\.(mjs|cjs|js)$/
7
- export const RESOURCE_TEMPLATE_FILE_PATTERN = /\.resource\.(mjs|cjs|js)$/
8
-
9
- function discoverFilesByPattern (srcDir, filePattern) {
10
- const dir = path.resolve(srcDir)
11
- if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) {
12
- return []
13
- }
14
- const files = []
15
- const walk = (d) => {
16
- const entries = fs.readdirSync(d, { withFileTypes: true })
17
- for (const e of entries) {
18
- const full = path.join(d, e.name)
19
- if (e.isDirectory()) walk(full)
20
- else if (filePattern.test(e.name)) files.push(full)
21
- }
22
- }
23
- walk(dir)
24
- return files
25
- }
26
-
27
-
28
- export default async function getPlatformFiles(_folder) {
29
- const cwd = process.cwd()
30
- const folder = path.resolve(cwd, _folder)
31
-
32
- const configPath = fs.existsSync(path.resolve(folder, 'config.js')) ? path.resolve(folder, 'config.js') : null
33
- const middlewarePath = fs.existsSync(path.resolve(folder, 'middleware.js')) ? path.resolve(folder, 'middleware.js') : null
34
-
35
- const pageFiles = discoverFilesByPattern(folder, PAGE_FILE_PATTERN)
36
- const apiFiles = discoverFilesByPattern(folder, API_FILE_PATTERN)
37
- const taskFiles = discoverFilesByPattern(folder, TASK_FILE_PATTERN)
38
- const resourceTemplatesFiles = discoverFilesByPattern(folder, TASK_FILE_PATTERN)
39
-
40
- const entries = [
41
- ...(middlewarePath ? [middlewarePath] : []),
42
- ...(configPath ? [configPath] : []),
43
- ...pageFiles,
44
- ...apiFiles,
45
- ...taskFiles,
46
- ...resourceTemplatesFiles,
47
- ]
48
-
49
- return entries
50
- }
package/src/http.js DELETED
@@ -1,85 +0,0 @@
1
- import path from 'path';
2
- import url from 'url'
3
- import express from 'express'
4
- import morgan from 'morgan'
5
- import { Router as OssyRouter } from '@ossy/router'
6
- import cookieParser from 'cookie-parser'
7
-
8
- const app = express();
9
-
10
- const currentDir = path.dirname(url.fileURLToPath(import.meta.url))
11
-
12
- function parsePortFromArgv(argv) {
13
- // Supports: --port 4000, --port=4000, -p 4000
14
- const idx = argv.findIndex(a => a === '--port' || a === '-p')
15
- if (idx !== -1 && argv[idx + 1]) return argv[idx + 1]
16
-
17
- const eq = argv.find(a => a.startsWith('--port='))
18
- if (eq) return eq.split('=')[1]
19
-
20
- return undefined
21
- }
22
-
23
- function normalizePort(value, fallback) {
24
- if (value === undefined || value === null || value === '') return fallback
25
- const n = Number.parseInt(String(value), 10)
26
- if (!Number.isFinite(n) || n <= 0) return fallback
27
- return n
28
- }
29
-
30
- const DEFAULT_PORT = 3000
31
- const port = normalizePort(parsePortFromArgv(process.argv) ?? process.env.PORT, DEFAULT_PORT)
32
-
33
- const middleware = [
34
- morgan('tiny'),
35
- express.json({ strict: false }),
36
- cookieParser(process.env.OSSY_COOKIE_SECRET || 'default_secret'),
37
- ]
38
-
39
- app.use(middleware)
40
-
41
- // const PageRegistry = OssyRouter.of({ pages: [] })
42
- // const ApiRegistry = OssyRouter.of({ api: [] })
43
- // const TaskRegistry = OssyRouter.of({ tasks: [] })
44
- // const StaticRegistry = OssyRouter.of({ static: [] })
45
-
46
- const Registry = OssyRouter.of({ pages: [
47
- {
48
- id: 'home',
49
- type: '@ossy/platform/page',
50
- path: '/',
51
- content: '<h1>Hello World from page route</h1>'
52
- },
53
- {
54
- id: 'home',
55
- type: '@ossy/platform/api',
56
- path: '/api',
57
- handle: (req, res) => {
58
- res.send({ message: 'Hello World from api route' })
59
- }
60
- }
61
- ]})
62
-
63
- app.all('*all', (req, res) => {
64
- const pathname = req.originalUrl
65
-
66
- // Will be replaced with fetching the resource from ossy storage
67
- const resource = Registry.getPageByUrl(pathname)
68
-
69
- if (resource.type === '@ossy/platform/page') {
70
- res.send(resource.content)
71
- }
72
-
73
- else if (resource.type === '@ossy/platform/api') {
74
- resource.handle(req, res)
75
- }
76
-
77
- else {
78
- res.status(404).send('Not found')
79
- }
80
-
81
- })
82
-
83
- app.listen(port, () => {
84
- console.log(`[@ossy/app][server] Running on http://localhost:${port}`);
85
- });