@ossy/app 1.16.0 → 1.16.1
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 +4 -2
- package/cli/get-platform-files.task.js +21 -107
- package/cli/manifest-plugin.js +46 -16
- package/package.json +11 -11
package/cli/build.task.js
CHANGED
|
@@ -147,7 +147,7 @@ export async function build (cliArgs = []) {
|
|
|
147
147
|
const stubInputMap = {}
|
|
148
148
|
|
|
149
149
|
for (const entry of allEntries) {
|
|
150
|
-
const { kind, sourcePath
|
|
150
|
+
const { kind, sourcePath } = entry
|
|
151
151
|
const stubFileName = stubFileNameFor(kind, sourcePath, srcDir)
|
|
152
152
|
const stubAbs = path.join(stubDir, stubFileName)
|
|
153
153
|
fs.writeFileSync(stubAbs, stubFor(kind, { stubAbs, sourceAbs: sourcePath }), 'utf8')
|
|
@@ -157,7 +157,7 @@ export async function build (cliArgs = []) {
|
|
|
157
157
|
throw new Error(`[@ossy/app][build] Duplicate entry name "${inputName}" between ${stubInputMap[inputName]} and ${stubAbs}.`)
|
|
158
158
|
}
|
|
159
159
|
stubInputMap[inputName] = stubAbs
|
|
160
|
-
entriesByStub.set(stubAbs, { kind, sourcePath
|
|
160
|
+
entriesByStub.set(stubAbs, { kind, sourcePath })
|
|
161
161
|
}
|
|
162
162
|
|
|
163
163
|
const configPath = path.resolve(srcDir, 'config.js')
|
|
@@ -197,6 +197,8 @@ export async function build (cliArgs = []) {
|
|
|
197
197
|
}),
|
|
198
198
|
manifestPlugin({
|
|
199
199
|
entriesByStub,
|
|
200
|
+
srcDir,
|
|
201
|
+
staticOutDir,
|
|
200
202
|
configValue,
|
|
201
203
|
manifestPath: path.join(buildPath, 'manifest.json'),
|
|
202
204
|
}),
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import fs from 'node:fs'
|
|
2
2
|
import path from 'node:path'
|
|
3
|
-
import { parse as babelParse } from '@babel/parser'
|
|
4
3
|
|
|
5
4
|
export const PAGE_FILE_PATTERN = /\.page\.(jsx?|tsx?)$/
|
|
6
5
|
export const API_FILE_PATTERN = /\.api\.(mjs|cjs|js)$/
|
|
@@ -10,9 +9,8 @@ export const TASK_FILE_PATTERN = /\.task\.(mjs|cjs|js)$/
|
|
|
10
9
|
* @typedef {'page' | 'api' | 'task'} EntryKind
|
|
11
10
|
*
|
|
12
11
|
* @typedef {object} PlatformEntry
|
|
13
|
-
* @property {EntryKind} kind
|
|
14
|
-
* @property {string} sourcePath
|
|
15
|
-
* @property {object} metadata Normalized metadata (always has `id`; pages always have `path`).
|
|
12
|
+
* @property {EntryKind} kind Which bucket this entry lives in.
|
|
13
|
+
* @property {string} sourcePath Absolute path to the user-authored source file.
|
|
16
14
|
*
|
|
17
15
|
* @typedef {object} PlatformFiles
|
|
18
16
|
* @property {PlatformEntry[]} pages Discovered `*.page.{jsx,tsx,...}` entries.
|
|
@@ -43,7 +41,14 @@ function classifyFile (absPath) {
|
|
|
43
41
|
return null
|
|
44
42
|
}
|
|
45
43
|
|
|
46
|
-
|
|
44
|
+
/**
|
|
45
|
+
* Default `id` derived from a source file's location relative to `src/`.
|
|
46
|
+
* Used when a source module doesn't export `metadata.id`.
|
|
47
|
+
* `src/home.page.jsx` -> `home`
|
|
48
|
+
* `src/index.page.jsx` -> `home`
|
|
49
|
+
* `src/blog/post.page.jsx` -> `blog-post`
|
|
50
|
+
*/
|
|
51
|
+
export function metadataIdFromFile (absPath, srcDir) {
|
|
47
52
|
const rel = path.relative(srcDir, absPath).replace(/\\/g, '/')
|
|
48
53
|
const noExt = rel
|
|
49
54
|
.replace(PAGE_FILE_PATTERN, '')
|
|
@@ -53,110 +58,22 @@ function metadataIdFromFile (absPath, srcDir) {
|
|
|
53
58
|
return noExt.replace(/\//g, '-')
|
|
54
59
|
}
|
|
55
60
|
|
|
56
|
-
|
|
61
|
+
/**
|
|
62
|
+
* Default URL path for a page when `metadata.path` isn't set.
|
|
63
|
+
* `home` is a special case — it lives at `/`.
|
|
64
|
+
*/
|
|
65
|
+
export function defaultPageRoute (id) {
|
|
57
66
|
return id === 'home' ? '/' : '/' + id
|
|
58
67
|
}
|
|
59
68
|
|
|
60
|
-
// Parses the static `metadata` export from a source file without executing it.
|
|
61
|
-
// Returns a plain JS-able shape: strings, numbers, booleans, arrays, and
|
|
62
|
-
// nested objects with string keys. Arbitrary expressions (function calls,
|
|
63
|
-
// references) are not supported here — pages must declare their metadata as
|
|
64
|
-
// a literal object expression so the build can read it without bundling.
|
|
65
|
-
function parseMetadataFromSource (absPath) {
|
|
66
|
-
const source = fs.readFileSync(absPath, 'utf8')
|
|
67
|
-
const ext = path.extname(absPath)
|
|
68
|
-
const plugins = ['importMeta']
|
|
69
|
-
if (ext === '.tsx' || ext === '.ts') plugins.push('typescript')
|
|
70
|
-
if (ext === '.jsx' || ext === '.tsx') plugins.push('jsx')
|
|
71
|
-
|
|
72
|
-
let ast
|
|
73
|
-
try {
|
|
74
|
-
ast = babelParse(source, {
|
|
75
|
-
sourceType: 'module',
|
|
76
|
-
allowImportExportEverywhere: true,
|
|
77
|
-
plugins,
|
|
78
|
-
})
|
|
79
|
-
} catch (err) {
|
|
80
|
-
throw new Error(`[@ossy/app][build] Failed to parse ${absPath}: ${err.message}`)
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
for (const node of ast.program.body) {
|
|
84
|
-
if (node.type !== 'ExportNamedDeclaration') continue
|
|
85
|
-
const decl = node.declaration
|
|
86
|
-
if (!decl || decl.type !== 'VariableDeclaration') continue
|
|
87
|
-
for (const declarator of decl.declarations) {
|
|
88
|
-
if (declarator.id?.type !== 'Identifier') continue
|
|
89
|
-
if (declarator.id.name !== 'metadata') continue
|
|
90
|
-
const literal = literalFromNode(declarator.init)
|
|
91
|
-
if (literal === undefined) {
|
|
92
|
-
throw new Error(`[@ossy/app][build] metadata in ${absPath} must be a literal object expression.`)
|
|
93
|
-
}
|
|
94
|
-
return literal
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
return null
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function literalFromNode (node) {
|
|
101
|
-
if (!node) return undefined
|
|
102
|
-
switch (node.type) {
|
|
103
|
-
case 'StringLiteral':
|
|
104
|
-
case 'NumericLiteral':
|
|
105
|
-
case 'BooleanLiteral':
|
|
106
|
-
return node.value
|
|
107
|
-
case 'NullLiteral':
|
|
108
|
-
return null
|
|
109
|
-
case 'TemplateLiteral':
|
|
110
|
-
if (node.expressions.length === 0) return node.quasis.map((q) => q.value.cooked).join('')
|
|
111
|
-
return undefined
|
|
112
|
-
case 'ArrayExpression': {
|
|
113
|
-
const out = []
|
|
114
|
-
for (const el of node.elements) {
|
|
115
|
-
if (el === null) { out.push(null); continue }
|
|
116
|
-
const value = literalFromNode(el)
|
|
117
|
-
if (value === undefined) return undefined
|
|
118
|
-
out.push(value)
|
|
119
|
-
}
|
|
120
|
-
return out
|
|
121
|
-
}
|
|
122
|
-
case 'ObjectExpression': {
|
|
123
|
-
const out = {}
|
|
124
|
-
for (const prop of node.properties) {
|
|
125
|
-
if (prop.type !== 'ObjectProperty') return undefined
|
|
126
|
-
let key
|
|
127
|
-
if (prop.key.type === 'Identifier') key = prop.key.name
|
|
128
|
-
else if (prop.key.type === 'StringLiteral') key = prop.key.value
|
|
129
|
-
else return undefined
|
|
130
|
-
const value = literalFromNode(prop.value)
|
|
131
|
-
if (value === undefined) return undefined
|
|
132
|
-
out[key] = value
|
|
133
|
-
}
|
|
134
|
-
return out
|
|
135
|
-
}
|
|
136
|
-
default:
|
|
137
|
-
return undefined
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
function ensureMetadataForKind ({ kind, sourcePath, parsed, srcDir }) {
|
|
142
|
-
const fallbackId = metadataIdFromFile(sourcePath, srcDir)
|
|
143
|
-
const meta = { ...(parsed || {}) }
|
|
144
|
-
if (!meta.id) meta.id = fallbackId
|
|
145
|
-
if (kind === 'page' && meta.path === undefined) meta.path = defaultPageRoute(meta.id)
|
|
146
|
-
if (kind === 'task' && !meta.id && meta.type) meta.id = meta.type
|
|
147
|
-
return meta
|
|
148
|
-
}
|
|
149
|
-
|
|
150
69
|
/**
|
|
151
|
-
* Walks `srcDir` and returns the user-authored entries
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
* placeholders the user is mid-creating.
|
|
70
|
+
* Walks `srcDir` and returns the user-authored entries the platform cares
|
|
71
|
+
* about, bucketed by kind. Empty (zero-byte) source files are skipped — we
|
|
72
|
+
* treat them as placeholders the user is mid-creating.
|
|
155
73
|
*
|
|
156
|
-
*
|
|
157
|
-
*
|
|
158
|
-
*
|
|
159
|
-
* without triggering Rollup).
|
|
74
|
+
* Metadata isn't read here on purpose: the build pipeline reads it from the
|
|
75
|
+
* bundled output instead, which lets users compute `metadata.path` (and
|
|
76
|
+
* anything else) from imports, config, env vars, etc.
|
|
160
77
|
*
|
|
161
78
|
* @param {string} srcDir Absolute path to the project's `src/` directory.
|
|
162
79
|
* @returns {Promise<PlatformFiles>}
|
|
@@ -173,12 +90,9 @@ export default async function getPlatformFiles (srcDir) {
|
|
|
173
90
|
if (!stat.isFile() || stat.size === 0) continue
|
|
174
91
|
const kind = classifyFile(sourcePath)
|
|
175
92
|
if (!kind) continue
|
|
176
|
-
const parsed = parseMetadataFromSource(sourcePath)
|
|
177
|
-
const metadata = ensureMetadataForKind({ kind, sourcePath, parsed, srcDir })
|
|
178
93
|
out[kind === 'page' ? 'pages' : kind === 'api' ? 'apis' : 'tasks'].push({
|
|
179
94
|
kind,
|
|
180
95
|
sourcePath,
|
|
181
|
-
metadata,
|
|
182
96
|
})
|
|
183
97
|
}
|
|
184
98
|
return out
|
package/cli/manifest-plugin.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import fs from 'node:fs'
|
|
2
2
|
import path from 'node:path'
|
|
3
|
+
import { pathToFileURL } from 'node:url'
|
|
4
|
+
|
|
5
|
+
import { metadataIdFromFile, defaultPageRoute } from './get-platform-files.task.js'
|
|
3
6
|
|
|
4
7
|
/**
|
|
5
8
|
* @typedef {import('./get-platform-files.task.js').PlatformEntry} PlatformEntry
|
|
@@ -7,9 +10,16 @@ import path from 'node:path'
|
|
|
7
10
|
* @typedef {object} ManifestPluginOptions
|
|
8
11
|
* @property {Map<string, PlatformEntry>} entriesByStub
|
|
9
12
|
* Maps each generated stub's absolute path (Rollup's `facadeModuleId`) to
|
|
10
|
-
* the `PlatformEntry` it was created for.
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
+
* the `PlatformEntry` it was created for. Used to recover the entry's
|
|
14
|
+
* `kind` and source location after Rollup has bundled and hashed each
|
|
15
|
+
* chunk.
|
|
16
|
+
* @property {string} srcDir
|
|
17
|
+
* Absolute path to the project's `src/` — used to derive fallback `id`s
|
|
18
|
+
* from source file paths when `metadata.id` is missing.
|
|
19
|
+
* @property {string} staticOutDir
|
|
20
|
+
* Absolute path to Rollup's `output.dir` (typically
|
|
21
|
+
* `build/public/static/`). Bundled chunks live here, and we dynamically
|
|
22
|
+
* `import()` them to read their exported metadata.
|
|
13
23
|
* @property {object} configValue
|
|
14
24
|
* The serializable config object (typically `src/config.js`'s default
|
|
15
25
|
* export). Inlined under `manifest.config` so the platform server doesn't
|
|
@@ -44,6 +54,13 @@ import path from 'node:path'
|
|
|
44
54
|
* top-level key because it isn't an entry — it's the runtime context the
|
|
45
55
|
* platform threads into every page render.
|
|
46
56
|
*
|
|
57
|
+
* Metadata is read from the **bundled** output, not from the source file.
|
|
58
|
+
* Each entry stub re-exports `metadata` from the user's module, so once
|
|
59
|
+
* Rollup writes the chunk we can `await import(chunkUrl)` and read
|
|
60
|
+
* `mod.metadata` directly. This lets users compute metadata however they
|
|
61
|
+
* like (function calls, `Object.fromEntries`, values derived from imported
|
|
62
|
+
* config, etc.) — anything that runs at module load in Node will work.
|
|
63
|
+
*
|
|
47
64
|
* The plugin runs in the `writeBundle` hook (rather than `generateBundle` +
|
|
48
65
|
* `emitFile`) for two reasons:
|
|
49
66
|
*
|
|
@@ -51,22 +68,19 @@ import path from 'node:path'
|
|
|
51
68
|
* `output.dir` (`build/public/static/`). Rollup forbids relative paths
|
|
52
69
|
* in `emitFile({ type: 'asset', fileName })`, so we write the file
|
|
53
70
|
* directly with `fs` once Rollup has finished writing the chunks.
|
|
54
|
-
* 2.
|
|
55
|
-
*
|
|
71
|
+
* 2. We need the chunks on disk to dynamically `import()` them and read
|
|
72
|
+
* their exported `metadata`.
|
|
56
73
|
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
* `manifest.entries` and stamps `entry: '/static/<chunk-name>'`.
|
|
60
|
-
* Duplicate ids within a `type` abort the build — routers key on `id`
|
|
61
|
-
* and would otherwise silently dispatch to the wrong entry.
|
|
74
|
+
* Duplicate ids within a `type` abort the build — routers key on `id` and
|
|
75
|
+
* would otherwise silently dispatch to the wrong entry.
|
|
62
76
|
*
|
|
63
77
|
* @param {ManifestPluginOptions} options
|
|
64
78
|
* @returns {import('rollup').Plugin}
|
|
65
79
|
*/
|
|
66
|
-
export function manifestPlugin ({ entriesByStub, configValue, manifestPath }) {
|
|
80
|
+
export function manifestPlugin ({ entriesByStub, srcDir, staticOutDir, configValue, manifestPath }) {
|
|
67
81
|
return {
|
|
68
82
|
name: 'ossy-manifest',
|
|
69
|
-
writeBundle (_, bundle) {
|
|
83
|
+
async writeBundle (_, bundle) {
|
|
70
84
|
/** @type {ManifestEntry[]} */
|
|
71
85
|
const entries = []
|
|
72
86
|
const seenIds = { page: new Set(), api: new Set(), task: new Set() }
|
|
@@ -80,8 +94,23 @@ export function manifestPlugin ({ entriesByStub, configValue, manifestPath }) {
|
|
|
80
94
|
if (!entryInfo) continue
|
|
81
95
|
|
|
82
96
|
const url = '/static/' + fileName
|
|
83
|
-
const
|
|
84
|
-
|
|
97
|
+
const localPath = path.join(staticOutDir, fileName)
|
|
98
|
+
|
|
99
|
+
// Cache-bust the import so consecutive builds in a watch loop don't
|
|
100
|
+
// pick up a stale module from Node's loader cache.
|
|
101
|
+
const importHref = pathToFileURL(localPath).href + `?ts=${Date.now()}`
|
|
102
|
+
let mod
|
|
103
|
+
try {
|
|
104
|
+
mod = await import(importHref)
|
|
105
|
+
} catch (err) {
|
|
106
|
+
this.error(
|
|
107
|
+
`[@ossy/app][build] Failed to import bundled ${entryInfo.kind} entry ` +
|
|
108
|
+
`${entryInfo.sourcePath}: ${err && err.message ? err.message : err}`,
|
|
109
|
+
)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const rawMeta = (mod && mod.metadata) || {}
|
|
113
|
+
const id = rawMeta.id || metadataIdFromFile(entryInfo.sourcePath, srcDir)
|
|
85
114
|
if (!id) {
|
|
86
115
|
this.error(`[@ossy/app][build] ${entryInfo.kind} entry missing metadata.id: ${entryInfo.sourcePath}`)
|
|
87
116
|
}
|
|
@@ -91,9 +120,10 @@ export function manifestPlugin ({ entriesByStub, configValue, manifestPath }) {
|
|
|
91
120
|
seenIds[entryInfo.kind].add(id)
|
|
92
121
|
|
|
93
122
|
if (entryInfo.kind === 'page') {
|
|
94
|
-
|
|
123
|
+
const pagePath = rawMeta.path !== undefined ? rawMeta.path : defaultPageRoute(id)
|
|
124
|
+
entries.push({ type: 'page', id, path: pagePath, title: rawMeta.title, entry: url })
|
|
95
125
|
} else if (entryInfo.kind === 'api') {
|
|
96
|
-
entries.push({ type: 'api', id, path:
|
|
126
|
+
entries.push({ type: 'api', id, path: rawMeta.path, entry: url })
|
|
97
127
|
} else {
|
|
98
128
|
entries.push({ type: 'task', id, entry: url })
|
|
99
129
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ossy/app",
|
|
3
|
-
"version": "1.16.
|
|
3
|
+
"version": "1.16.1",
|
|
4
4
|
"description": "",
|
|
5
5
|
"source": "./src/index.js",
|
|
6
6
|
"main": "./src/index.js",
|
|
@@ -33,15 +33,15 @@
|
|
|
33
33
|
"@babel/eslint-parser": "^7.15.8",
|
|
34
34
|
"@babel/preset-react": "^7.26.3",
|
|
35
35
|
"@babel/register": "^7.25.9",
|
|
36
|
-
"@ossy/connected-components": "^1.16.
|
|
37
|
-
"@ossy/design-system": "^1.16.
|
|
38
|
-
"@ossy/pages": "^1.16.
|
|
39
|
-
"@ossy/platform": "^1.15.
|
|
40
|
-
"@ossy/router": "^1.16.
|
|
41
|
-
"@ossy/router-react": "^1.16.
|
|
42
|
-
"@ossy/sdk": "^1.16.
|
|
43
|
-
"@ossy/sdk-react": "^1.16.
|
|
44
|
-
"@ossy/themes": "^1.16.
|
|
36
|
+
"@ossy/connected-components": "^1.16.1",
|
|
37
|
+
"@ossy/design-system": "^1.16.1",
|
|
38
|
+
"@ossy/pages": "^1.16.1",
|
|
39
|
+
"@ossy/platform": "^1.15.1",
|
|
40
|
+
"@ossy/router": "^1.16.1",
|
|
41
|
+
"@ossy/router-react": "^1.16.1",
|
|
42
|
+
"@ossy/sdk": "^1.16.1",
|
|
43
|
+
"@ossy/sdk-react": "^1.16.1",
|
|
44
|
+
"@ossy/themes": "^1.16.1",
|
|
45
45
|
"@rollup/plugin-alias": "^6.0.0",
|
|
46
46
|
"@rollup/plugin-babel": "6.1.0",
|
|
47
47
|
"@rollup/plugin-commonjs": "^29.0.0",
|
|
@@ -75,5 +75,5 @@
|
|
|
75
75
|
"README.md",
|
|
76
76
|
"tsconfig.json"
|
|
77
77
|
],
|
|
78
|
-
"gitHead": "
|
|
78
|
+
"gitHead": "031e2329e267a3e0cebc2ec0d484fa11b53082d9"
|
|
79
79
|
}
|