@uniweb/build 1.1.1 → 1.2.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": "@uniweb/build",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Build tooling for the Uniweb Component Web Platform",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -56,15 +56,15 @@
|
|
|
56
56
|
"js-yaml": "^4.1.0",
|
|
57
57
|
"sharp": "^0.35.3",
|
|
58
58
|
"yaml": "^2.5.0",
|
|
59
|
-
"@uniweb/
|
|
60
|
-
"@uniweb/
|
|
61
|
-
"@uniweb/
|
|
62
|
-
"@uniweb/
|
|
63
|
-
"@uniweb/content-reader": "~1.
|
|
64
|
-
"@uniweb/
|
|
59
|
+
"@uniweb/semantic-parser": "~1.6.0",
|
|
60
|
+
"@uniweb/theming": "~1.2.0",
|
|
61
|
+
"@uniweb/schemas": "~1.1.0",
|
|
62
|
+
"@uniweb/projections": "~1.1.1",
|
|
63
|
+
"@uniweb/content-reader": "~1.3.0",
|
|
64
|
+
"@uniweb/content-writer": "~1.1.0"
|
|
65
65
|
},
|
|
66
66
|
"optionalDependencies": {
|
|
67
|
-
"@uniweb/runtime": "~1.1.
|
|
67
|
+
"@uniweb/runtime": "~1.1.1"
|
|
68
68
|
},
|
|
69
69
|
"peerDependencies": {
|
|
70
70
|
"vite": "^5.0.0 || ^6.0.0 || ^7.0.0",
|
|
@@ -73,8 +73,8 @@
|
|
|
73
73
|
"@tailwindcss/vite": "^4.0.0",
|
|
74
74
|
"@vitejs/plugin-react": "^4.0.0 || ^5.0.0",
|
|
75
75
|
"vite-plugin-svgr": "^4.0.0",
|
|
76
|
-
"@uniweb/core": "~1.
|
|
77
|
-
"@uniweb/runtime": "~1.1.
|
|
76
|
+
"@uniweb/core": "~1.2.0",
|
|
77
|
+
"@uniweb/runtime": "~1.1.1"
|
|
78
78
|
},
|
|
79
79
|
"peerDependenciesMeta": {
|
|
80
80
|
"vite": {
|
package/src/i18n/freeform.js
CHANGED
|
@@ -26,6 +26,7 @@ import { poolDirsForSchema, schemaForPoolDirs, RECORDS_DIR } from '../site/entit
|
|
|
26
26
|
import { parseFrontmatter } from '../utils/frontmatter.js'
|
|
27
27
|
import { computeSourceHash } from './freeform-manifest.js'
|
|
28
28
|
import { layoutAreaRoute } from '../site/layout-folder.js'
|
|
29
|
+
import { warnOutsideVocabulary } from '../site/vocabulary-warnings.js'
|
|
29
30
|
|
|
30
31
|
// Try to import content-reader for markdown → ProseMirror conversion
|
|
31
32
|
let markdownToProseMirror
|
|
@@ -46,6 +47,11 @@ try {
|
|
|
46
47
|
}
|
|
47
48
|
|
|
48
49
|
|
|
50
|
+
/** Report what a translation's markdown could not honour, naming the file. */
|
|
51
|
+
function warnFor(filePath) {
|
|
52
|
+
return (message) => console.warn(`[i18n] ${relative(process.cwd(), filePath)}: ${message}`)
|
|
53
|
+
}
|
|
54
|
+
|
|
49
55
|
/**
|
|
50
56
|
* Normalize route for filesystem path
|
|
51
57
|
* Removes leading slash, replaces remaining slashes with path separators
|
|
@@ -83,8 +89,10 @@ export async function loadFreeformTranslation(section, page, locale, localesDir)
|
|
|
83
89
|
const content = await readFile(filePath, 'utf-8')
|
|
84
90
|
const { frontmatter, body } = parseFrontmatter(content, filePath)
|
|
85
91
|
|
|
86
|
-
// Convert markdown body to ProseMirror
|
|
87
|
-
|
|
92
|
+
// Convert markdown body to ProseMirror — HTML in it shows as written,
|
|
93
|
+
// except `<br>`, and the translator is told which file.
|
|
94
|
+
const proseMirrorContent = markdownToProseMirror(body, { onWarning: warnFor(filePath) })
|
|
95
|
+
warnOutsideVocabulary(proseMirrorContent, warnFor(filePath))
|
|
88
96
|
|
|
89
97
|
return {
|
|
90
98
|
content: proseMirrorContent,
|
|
@@ -138,7 +146,8 @@ export async function loadFreeformRecord(item, schema, locale, localesDir) {
|
|
|
138
146
|
const { frontmatter, body } = parseFrontmatter(content, filePath)
|
|
139
147
|
|
|
140
148
|
// Convert markdown body to ProseMirror (if body exists)
|
|
141
|
-
const proseMirrorContent = body.trim() ? markdownToProseMirror(body) : null
|
|
149
|
+
const proseMirrorContent = body.trim() ? markdownToProseMirror(body, { onWarning: warnFor(filePath) }) : null
|
|
150
|
+
if (proseMirrorContent) warnOutsideVocabulary(proseMirrorContent, warnFor(filePath))
|
|
142
151
|
|
|
143
152
|
return {
|
|
144
153
|
frontmatter: Object.keys(frontmatter).length > 0 ? frontmatter : null,
|
|
@@ -42,6 +42,7 @@ import { authoredRedirectTarget } from '@uniweb/core/resolve-route'
|
|
|
42
42
|
import { parseFrontmatter } from '../utils/frontmatter.js'
|
|
43
43
|
import { parseGrid } from '@uniweb/schemas/grid'
|
|
44
44
|
import { runtimeServicesConfig, refuseRetiredServiceKeys, refuseUnreadableServices } from '../uwx/services-request.js'
|
|
45
|
+
import { warnOutsideVocabulary } from './vocabulary-warnings.js'
|
|
45
46
|
|
|
46
47
|
// Try to import content-reader, fall back to simplified parser
|
|
47
48
|
let markdownToProseMirror
|
|
@@ -1068,7 +1069,12 @@ async function processMarkdownFile(filePath, id, siteRoot, defaultStableId = nul
|
|
|
1068
1069
|
// come through here (a free-form translation, a record body) kept raw references
|
|
1069
1070
|
// nothing resolved, and the same section's content differed between a build and a
|
|
1070
1071
|
// document stored as written.
|
|
1071
|
-
|
|
1072
|
+
//
|
|
1073
|
+
// HTML in the markdown is not content: it shows as written, except `<br>`
|
|
1074
|
+
// (content-reader, since 2026-10-14). Say so with the file in hand.
|
|
1075
|
+
const warnHere = (message) => console.warn(`[content-collector] ${relative(siteRoot, filePath)}: ${message}`)
|
|
1076
|
+
const proseMirrorContent = markdownToProseMirror(markdown, { onWarning: warnHere })
|
|
1077
|
+
warnOutsideVocabulary(proseMirrorContent, warnHere)
|
|
1072
1078
|
|
|
1073
1079
|
// `query: team` → `fetch: { query: team }`; a list, one config per name, each under
|
|
1074
1080
|
// its own `as` — the one helper every level uses. Unrelated to a section type's
|
|
@@ -72,6 +72,7 @@ import { toDeliveredRecord, toStoredRecord, storedPath, briefFieldMap, contentBo
|
|
|
72
72
|
import { collectNestedRefs } from '@uniweb/schemas/format'
|
|
73
73
|
import { labelIn } from './entry-label.js'
|
|
74
74
|
import { fileRecordValue, makeFilePreview } from './file-records.js'
|
|
75
|
+
import { warnOutsideVocabulary } from './vocabulary-warnings.js'
|
|
75
76
|
|
|
76
77
|
// Try to import content-reader for markdown parsing
|
|
77
78
|
let markdownToProseMirror
|
|
@@ -563,8 +564,11 @@ async function processContentItem(dir, filename, config, siteRoot, basePath, rec
|
|
|
563
564
|
// record was published.
|
|
564
565
|
await processDataItemAssets(frontmatter, filepath, siteRoot, recordsRoot, basePath)
|
|
565
566
|
|
|
566
|
-
// Parse markdown body to ProseMirror
|
|
567
|
-
|
|
567
|
+
// Parse markdown body to ProseMirror — HTML in it shows as written, except
|
|
568
|
+
// `<br>`, and the author is told which file.
|
|
569
|
+
const warnHere = (message) => console.warn(`[records] ${relative(siteRoot, filepath)}: ${message}`)
|
|
570
|
+
const content = markdownToProseMirror(body, { onWarning: warnHere })
|
|
571
|
+
warnOutsideVocabulary(content, warnHere)
|
|
568
572
|
|
|
569
573
|
// Process assets (resolve paths, copy co-located files)
|
|
570
574
|
// This modifies content in place, updating paths to site-root-relative
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Say, with the file in hand, what in a document a page leaves out.
|
|
3
|
+
*
|
|
4
|
+
* A document holds the rich-text vocabulary (`@uniweb/content-reader/vocabulary`):
|
|
5
|
+
* its types and attributes, and the values that reach a page's markup. What is
|
|
6
|
+
* outside it — a link to `javascript:`, an inert `{.name}`, MathML outside the
|
|
7
|
+
* allowlist — the renderer leaves out of the page, and a backend that cleans the
|
|
8
|
+
* content it stores removes, so the next pull would write the file without it.
|
|
9
|
+
* The author hears it here first, once per thing, naming the file.
|
|
10
|
+
*/
|
|
11
|
+
import { outsideVocabulary } from '@uniweb/content-reader/vocabulary'
|
|
12
|
+
|
|
13
|
+
const SAYS = {
|
|
14
|
+
url: (f) => `a link to \`${f.found}\` is never followed`,
|
|
15
|
+
target: (f) => `\`target=${f.found}\` is not a link target`,
|
|
16
|
+
'style-name': (f) => `\`{${f.found}}\` is not a style name`,
|
|
17
|
+
color: (f) => `\`${f.found}\` is not a colour`,
|
|
18
|
+
mathml: (f) => `a formula's MathML holds what a page cannot show (${f.found})`,
|
|
19
|
+
'ref-id': (f) => `\`${f.found}\` is not an inset reference`,
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Warn, for each thing in `doc` outside the vocabulary, with the file it came from.
|
|
24
|
+
*
|
|
25
|
+
* @param {Object} doc - a ProseMirror document
|
|
26
|
+
* @param {(message: string) => void} warn - prints one message, prefixed with the file
|
|
27
|
+
*/
|
|
28
|
+
export function warnOutsideVocabulary(doc, warn) {
|
|
29
|
+
for (const finding of outsideVocabulary(doc)) {
|
|
30
|
+
const says = SAYS[finding.rule]?.(finding) ?? `\`${finding.found}\` is not part of a document (${finding.rule})`
|
|
31
|
+
warn(`${says} — a page leaves it out, and a backend that cleans what it stores removes it`)
|
|
32
|
+
}
|
|
33
|
+
}
|
package/src/uwx/data-schema.js
CHANGED
|
@@ -27,7 +27,8 @@ const TEXT_KINDS = new Set(['string', 'text'])
|
|
|
27
27
|
// Rich-content `format` markers on a `text` field — the file-based body shape that
|
|
28
28
|
// replaced the retired `richtext` kind (2026-06-02 / uwx-format.md). A `text` field
|
|
29
29
|
// carrying one round-trips as the RAW source string (no ProseMirror conversion).
|
|
30
|
-
|
|
30
|
+
// ⛔ `html` was one until 2026-10-14; `@uniweb/schemas` refuses it by name since.
|
|
31
|
+
const CONTENT_TEXT_FORMATS = new Set(['markdown'])
|
|
31
32
|
|
|
32
33
|
// The field an open map's key lowers into. `name` matches the idiom already in use
|
|
33
34
|
// for this shape (the backend's site-content `collections` section), so an open map
|
|
@@ -113,7 +114,7 @@ export function isContentBodyField(field) {
|
|
|
113
114
|
* (item_ref) ref to its full `@org/model/<section>` path. Falls back to
|
|
114
115
|
* `resolveName` (model only) when not supplied.
|
|
115
116
|
* @returns {Object} the declaration (`{ name, label?, plural?, icon?, description?,
|
|
116
|
-
* source_locale?, linkable?, sections }`).
|
|
117
|
+
* source_locale?, linkable?, grantable?, sections }`).
|
|
117
118
|
*/
|
|
118
119
|
export function toDataSchemaDeclaration(normalized, { name, resolveName, resolveOptions } = {}) {
|
|
119
120
|
if (!name) throw new Error('toDataSchemaDeclaration: a registry name is required')
|
|
@@ -158,6 +159,10 @@ export function toDataSchemaDeclaration(normalized, { name, resolveName, resolve
|
|
|
158
159
|
// reference, which read as a promise that one is accepted. A backend refuses it.
|
|
159
160
|
if (normalized.linkable === false) decl.linkable = false
|
|
160
161
|
|
|
162
|
+
// `grantable` — AUTHORED ONLY, like `linkable`: a model's entries may be shared one at a
|
|
163
|
+
// time unless it says `grantable: false` (omitted ⇒ true) [Diego, 2026-10-14].
|
|
164
|
+
if (normalized.grantable === false) decl.grantable = false
|
|
165
|
+
|
|
161
166
|
decl.sections = sections
|
|
162
167
|
return decl
|
|
163
168
|
}
|
|
@@ -424,6 +429,9 @@ function lowerLeaf(field, resolve, optResolve, { multiple = false } = {}) {
|
|
|
424
429
|
if (leafType === 'ref') {
|
|
425
430
|
out.type = 'entity_ref'
|
|
426
431
|
if (field.ref) out.model = resolve(field.ref)
|
|
432
|
+
// The level on the entry it points to that opens this entry, for `read` — by link
|
|
433
|
+
// [Diego, 2026-10-14]. `@uniweb/schemas` admits it only here, as read, edit or admin.
|
|
434
|
+
if (field.readable_with) out.readable_with = field.readable_with
|
|
427
435
|
return out
|
|
428
436
|
}
|
|
429
437
|
|
package/src/uwx/site-project.js
CHANGED
|
@@ -47,7 +47,8 @@ import { declarationsToQueriesYml } from './records-project.js'
|
|
|
47
47
|
import { FILLED_SECTION_TYPE, buildRouteOf } from './site.js'
|
|
48
48
|
import { translationContext, SITE_META_CONTEXT } from '../i18n/extract.js'
|
|
49
49
|
import { authorableDeclaration, DECLARATION_KEYS } from '../site/fetch-shapes.js'
|
|
50
|
-
import { createTranslationCollector, writeLocaleTranslations, writeFreeformTranslations, unwrapLocalizedContent } from './locale-sync.js'
|
|
50
|
+
import { createTranslationCollector, writeLocaleTranslations, writeFreeformTranslations, unwrapLocalizedContent, isProseMirrorDoc } from './locale-sync.js'
|
|
51
|
+
import { unwritableNodes } from '@uniweb/content-writer'
|
|
51
52
|
import { buildFreeformPath, freeformPathsFor } from '../i18n/freeform.js'
|
|
52
53
|
import { unwrapLocalized, unwrapLocalizedList } from './backfill.js'
|
|
53
54
|
import { LOCALIZED_FIELD_ASSUMPTION } from './localize.js'
|
|
@@ -1201,6 +1202,59 @@ function projectLayout(layoutSections, layoutBaseDir, report, prune, ctx) {
|
|
|
1201
1202
|
if (prune && written.length > 0) pruneOrphanLayout(layoutBaseDir, new Set(written), report)
|
|
1202
1203
|
}
|
|
1203
1204
|
|
|
1205
|
+
/**
|
|
1206
|
+
* ⛔ A PULL THAT WOULD DROP CONTENT WRITES NOTHING.
|
|
1207
|
+
*
|
|
1208
|
+
* A node a markdown file cannot hold is left out of the file it is written to,
|
|
1209
|
+
* and the push after the pull then deletes it from the site — with nobody told
|
|
1210
|
+
* but a log line. So every section document the projection would write is asked
|
|
1211
|
+
* first (`unwritableNodes`, `@uniweb/content-writer`), every locale's included,
|
|
1212
|
+
* and the pull stops, naming each section and what it holds.
|
|
1213
|
+
* ⛔ Until 2026-10-14 the writer logged one line per type and the pull went on:
|
|
1214
|
+
* measured with a button an editor saved as a node, which a pull and a push then
|
|
1215
|
+
* deleted.
|
|
1216
|
+
*
|
|
1217
|
+
* @param {object} document - the `@uniweb/site-content` document
|
|
1218
|
+
* @param {string} sourceLocale
|
|
1219
|
+
*/
|
|
1220
|
+
function refuseContentFilesCannotHold(document, sourceLocale) {
|
|
1221
|
+
const found = []
|
|
1222
|
+
const name = (record, where) => {
|
|
1223
|
+
const own = record?.slug !== undefined ? pageDirName(record, sourceLocale) : record?.stable_id || record?.$uuid
|
|
1224
|
+
return own ? (where ? `${where}/${own}` : String(own)) : where
|
|
1225
|
+
}
|
|
1226
|
+
const visit = (value, where) => {
|
|
1227
|
+
if (Array.isArray(value)) {
|
|
1228
|
+
for (const item of value) visit(item, where)
|
|
1229
|
+
return
|
|
1230
|
+
}
|
|
1231
|
+
if (!value || typeof value !== 'object') return
|
|
1232
|
+
const here = name(value, where)
|
|
1233
|
+
for (const [key, field] of Object.entries(value)) {
|
|
1234
|
+
if (key === 'content') {
|
|
1235
|
+
const docs = isProseMirrorDoc(field)
|
|
1236
|
+
? [field]
|
|
1237
|
+
: isPlainRecord(field)
|
|
1238
|
+
? Object.values(field).filter(isProseMirrorDoc)
|
|
1239
|
+
: []
|
|
1240
|
+
const types = [...new Set(docs.flatMap((doc) => unwritableNodes(doc)))]
|
|
1241
|
+
if (types.length) found.push(`${here || 'a section'}: ${types.join(', ')}`)
|
|
1242
|
+
if (docs.length) continue
|
|
1243
|
+
}
|
|
1244
|
+
visit(field, here)
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
visit(document?.pages, '')
|
|
1248
|
+
visit(document?.layout_sections, 'layout')
|
|
1249
|
+
if (found.length === 0) return
|
|
1250
|
+
throw new Error(
|
|
1251
|
+
'This pull would leave content out of your files — a file cannot hold it, so your next push would ' +
|
|
1252
|
+
'delete it from the site. Nothing was written.\n' +
|
|
1253
|
+
found.map((entry) => ` ${entry}`).join('\n') +
|
|
1254
|
+
'\nChange or remove it where it was made, then pull again.'
|
|
1255
|
+
)
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1204
1258
|
/**
|
|
1205
1259
|
* Project a whole `@uniweb/site-content` document to a site's files: `info` →
|
|
1206
1260
|
* config (siteInfoToConfig), `collections[]` declarations →
|
|
@@ -1226,6 +1280,9 @@ function projectLayout(layoutSections, layoutBaseDir, report, prune, ctx) {
|
|
|
1226
1280
|
* @returns {{ config: object, collections: object, locales: object, pages: string[], sections: string[], layout: string[], deleted: string[], renamed: object[] }}
|
|
1227
1281
|
*/
|
|
1228
1282
|
export function siteContentDocumentToProject({ document, siteRoot, backend = null, sourceLocale = LOCALIZED_FIELD_ASSUMPTION.defaultSourceLocale, prune = false, keepAuthoredFoundation = false, scope, dataModel = null }) {
|
|
1283
|
+
// Before anything is written: a pull that would leave content out of the files stops here.
|
|
1284
|
+
refuseContentFilesCannotHold(document, sourceLocale)
|
|
1285
|
+
|
|
1229
1286
|
const report = { config: null, collections: null, locales: null, assets: null, pages: [], sections: [], layout: [], deleted: [], renamed: [] }
|
|
1230
1287
|
|
|
1231
1288
|
// Collects target-locale translations of localized scalars as they're projected;
|