uniweb 0.33.1 → 0.34.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/package.json +7 -7
- package/partials/agents.md +73 -38
- package/src/backend/client.js +122 -35
- package/src/backend/site-sync.js +42 -43
- package/src/commands/build.js +17 -17
- package/src/commands/clone.js +7 -7
- package/src/commands/content.js +2 -3
- package/src/commands/doctor.js +34 -10
- package/src/commands/i18n.js +53 -39
- package/src/commands/publish.js +39 -15
- package/src/commands/pull.js +26 -25
- package/src/commands/push.js +24 -14
- package/src/commands/validate.js +1 -1
- package/src/framework-index.json +16 -9
- package/src/index.js +39 -4
- package/src/utils/flag-guard.js +2 -2
- package/src/utils/git.js +8 -2
- package/src/utils/records-guard.js +80 -0
- package/src/utils/schemaless-report.js +4 -4
- package/src/utils/site-data-upload.js +2 -2
- package/src/utils/site-identity.js +65 -2
- package/templates/site/site.yml.hbs +24 -6
package/src/commands/build.js
CHANGED
|
@@ -592,7 +592,7 @@ function resolveFoundationDir(projectDir, siteConfig) {
|
|
|
592
592
|
*
|
|
593
593
|
* The `buildLocalizedContent` step is the same call bundle mode makes
|
|
594
594
|
* post-vite, so multi-locale sites get identical per-locale outputs in
|
|
595
|
-
* either mode. Collection translation (`
|
|
595
|
+
* either mode. Collection translation (`buildLocalizedRecords`)
|
|
596
596
|
* also runs here so deploy ships translated collection JSONs.
|
|
597
597
|
*
|
|
598
598
|
* Bug surfaced + fixed by routing deploy through this path: the bundle
|
|
@@ -652,28 +652,28 @@ async function buildSiteLink(projectDir, options = {}) {
|
|
|
652
652
|
// Collection translations — optional; don't fail the build if
|
|
653
653
|
// missing. Bundle mode does the same.
|
|
654
654
|
try {
|
|
655
|
-
const {
|
|
656
|
-
const
|
|
655
|
+
const { buildLocalizedRecords } = await import('@uniweb/build/i18n')
|
|
656
|
+
const recordOutputs = await buildLocalizedRecords(projectDir, {
|
|
657
657
|
locales: i18nConfig.locales,
|
|
658
658
|
outputDir: distDir,
|
|
659
|
-
|
|
659
|
+
recordLocalesDir: join(
|
|
660
660
|
projectDir,
|
|
661
661
|
i18nConfig.localesDir,
|
|
662
|
-
'
|
|
662
|
+
'records'
|
|
663
663
|
)
|
|
664
664
|
})
|
|
665
|
-
const
|
|
665
|
+
const recordCount = Object.values(recordOutputs).reduce(
|
|
666
666
|
(sum, localeOutputs) => sum + Object.keys(localeOutputs).length,
|
|
667
667
|
0
|
|
668
668
|
)
|
|
669
|
-
if (
|
|
669
|
+
if (recordCount > 0) {
|
|
670
670
|
success(
|
|
671
|
-
`Translated
|
|
671
|
+
`Translated records for ${Object.keys(recordOutputs).length} locale(s)`
|
|
672
672
|
)
|
|
673
673
|
}
|
|
674
674
|
} catch (err) {
|
|
675
675
|
if (process.env.UNIWEB_DEBUG)
|
|
676
|
-
console.error('
|
|
676
|
+
console.error('Record translation:', err.message)
|
|
677
677
|
}
|
|
678
678
|
} catch (err) {
|
|
679
679
|
error(`i18n build failed: ${err.message}`)
|
|
@@ -772,33 +772,33 @@ async function buildSite(projectDir, options = {}) {
|
|
|
772
772
|
|
|
773
773
|
// Translate collections if they exist
|
|
774
774
|
try {
|
|
775
|
-
const {
|
|
775
|
+
const { buildLocalizedRecords } = await import('@uniweb/build/i18n')
|
|
776
776
|
|
|
777
|
-
const
|
|
777
|
+
const recordOutputs = await buildLocalizedRecords(projectDir, {
|
|
778
778
|
locales: i18nConfig.locales,
|
|
779
779
|
outputDir: join(projectDir, 'dist'),
|
|
780
|
-
|
|
780
|
+
recordLocalesDir: join(
|
|
781
781
|
projectDir,
|
|
782
782
|
i18nConfig.localesDir,
|
|
783
|
-
'
|
|
783
|
+
'records'
|
|
784
784
|
)
|
|
785
785
|
})
|
|
786
786
|
|
|
787
787
|
// Count collections translated
|
|
788
|
-
const
|
|
788
|
+
const recordCount = Object.values(recordOutputs).reduce(
|
|
789
789
|
(sum, localeOutputs) => sum + Object.keys(localeOutputs).length,
|
|
790
790
|
0
|
|
791
791
|
)
|
|
792
792
|
|
|
793
|
-
if (
|
|
793
|
+
if (recordCount > 0) {
|
|
794
794
|
success(
|
|
795
|
-
`Translated
|
|
795
|
+
`Translated records for ${Object.keys(recordOutputs).length} locale(s)`
|
|
796
796
|
)
|
|
797
797
|
}
|
|
798
798
|
} catch (err) {
|
|
799
799
|
// Collection translation is optional, don't fail build
|
|
800
800
|
if (process.env.UNIWEB_DEBUG) {
|
|
801
|
-
console.error('
|
|
801
|
+
console.error('Record translation:', err.message)
|
|
802
802
|
}
|
|
803
803
|
}
|
|
804
804
|
} catch (err) {
|
package/src/commands/clone.js
CHANGED
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
* seed;
|
|
25
25
|
* 4. install, then delegate the projection to the project-local `uniweb pull` (which
|
|
26
26
|
* resolves the now-installed project-local `@uniweb/build`; clone forwards
|
|
27
|
-
* `--no-
|
|
27
|
+
* `--no-records` to it when set).
|
|
28
28
|
*
|
|
29
29
|
* Sites are private — authenticate with `uniweb login` first; the session carries
|
|
30
30
|
* identity + the backend origin. There is no `--foundation` flag: the site carries
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
* the current workspace when run inside one)
|
|
38
38
|
* uniweb clone <uuid> --path sites Place under sites/ (segregated layout)
|
|
39
39
|
* uniweb clone <uuid> --project docs Co-located docs/site
|
|
40
|
-
* uniweb clone <uuid> --no-
|
|
40
|
+
* uniweb clone <uuid> --no-records Pull pages only; skip records
|
|
41
41
|
*
|
|
42
42
|
* Backend: via BackendClient (the site-content pull lane). Origin from
|
|
43
43
|
* --registry > UNIWEB_REGISTER_URL > the local default (internal dev overrides;
|
|
@@ -176,7 +176,7 @@ export async function clone(args = [], deps = {}) {
|
|
|
176
176
|
if (!siteUuid) {
|
|
177
177
|
error('Missing site uuid.')
|
|
178
178
|
log(
|
|
179
|
-
`\nUsage: ${getCliPrefix()} clone <site-uuid> [name|.] [--path <dir>] [--project <name>] [--no-
|
|
179
|
+
`\nUsage: ${getCliPrefix()} clone <site-uuid> [name|.] [--path <dir>] [--project <name>] [--no-records]`
|
|
180
180
|
)
|
|
181
181
|
log(
|
|
182
182
|
`${colors.dim}Sites are private — run \`uniweb login\` first.${colors.reset}`
|
|
@@ -184,8 +184,8 @@ export async function clone(args = [], deps = {}) {
|
|
|
184
184
|
return { exitCode: 2 }
|
|
185
185
|
}
|
|
186
186
|
|
|
187
|
-
const
|
|
188
|
-
args.includes('--no-
|
|
187
|
+
const noRecords =
|
|
188
|
+
args.includes('--no-records') || args.includes('--content-only')
|
|
189
189
|
const pathFlag = flagValue(args, '--path')
|
|
190
190
|
const projectFlag = flagValue(args, '--project')
|
|
191
191
|
const tokenFlag = flagValue(args, '--token')
|
|
@@ -386,7 +386,7 @@ export async function clone(args = [], deps = {}) {
|
|
|
386
386
|
// bytes on disk, not of what the scaffolder first wrote.
|
|
387
387
|
recordWritten(
|
|
388
388
|
siteDir,
|
|
389
|
-
['site.yml', 'theme.yml', 'head.html', '
|
|
389
|
+
['site.yml', 'theme.yml', 'head.html', 'queries.yml', 'records.yml']
|
|
390
390
|
.map((f) => join(siteDir, f))
|
|
391
391
|
.filter((p) => existsSync(p))
|
|
392
392
|
)
|
|
@@ -394,7 +394,7 @@ export async function clone(args = [], deps = {}) {
|
|
|
394
394
|
const pullExtra = []
|
|
395
395
|
if (explicitBackend) pullExtra.push('--backend', explicitBackend)
|
|
396
396
|
if (tokenFlag) pullExtra.push('--token', tokenFlag)
|
|
397
|
-
if (
|
|
397
|
+
if (noRecords) pullExtra.push('--no-records')
|
|
398
398
|
|
|
399
399
|
if (deps.skipPull) {
|
|
400
400
|
note('Skipping pull (test mode).')
|
package/src/commands/content.js
CHANGED
|
@@ -162,8 +162,7 @@ async function contentExport(args) {
|
|
|
162
162
|
if (entity.layout_sections?.length)
|
|
163
163
|
counts.layout_sections = entity.layout_sections.length
|
|
164
164
|
if (entity.extensions?.length) counts.extensions = entity.extensions.length
|
|
165
|
-
if (entity.
|
|
166
|
-
counts.collections = entity.collections.length
|
|
165
|
+
if (entity.queries?.length) counts.queries = entity.queries.length
|
|
167
166
|
}
|
|
168
167
|
|
|
169
168
|
console.log('')
|
|
@@ -198,6 +197,6 @@ async function contentExport(args) {
|
|
|
198
197
|
)
|
|
199
198
|
}
|
|
200
199
|
console.log('')
|
|
201
|
-
say.warn('v0 scope: media bytes,
|
|
200
|
+
say.warn('v0 scope: media bytes, records, and @-nested section')
|
|
202
201
|
say.dim('hierarchy are not yet carried (documented).')
|
|
203
202
|
}
|
package/src/commands/doctor.js
CHANGED
|
@@ -153,8 +153,8 @@ function loadSiteYml(dir) {
|
|
|
153
153
|
/**
|
|
154
154
|
* Diagnose the compiled-collection output directory.
|
|
155
155
|
*
|
|
156
|
-
* `public/<DATA_DIR>/` holds what the build compiles from `
|
|
157
|
-
* nothing else — `
|
|
156
|
+
* `public/<DATA_DIR>/` holds what the build compiles from `entities/`, and
|
|
157
|
+
* nothing else — `entities/` + `records.yml` is the only supported way to provide
|
|
158
158
|
* structured data. Two consequences, both checked here:
|
|
159
159
|
*
|
|
160
160
|
* 1. **The mapping is a bijection.** Every entry should be backed by a
|
|
@@ -548,9 +548,33 @@ export async function checkFormSubmitTarget({ sitePath, siteName, siteYml, issue
|
|
|
548
548
|
)
|
|
549
549
|
}
|
|
550
550
|
|
|
551
|
+
/** The bare map in `queries.yml`, or `{}` when there is none. */
|
|
552
|
+
function readQueriesYml(sitePath) {
|
|
553
|
+
try {
|
|
554
|
+
const doc = yaml.load(readFileSync(join(sitePath, 'queries.yml'), 'utf8'))
|
|
555
|
+
return doc && typeof doc === 'object' && !Array.isArray(doc) ? doc : {}
|
|
556
|
+
} catch {
|
|
557
|
+
return {}
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
551
561
|
function checkGeneratedDataDir({ sitePath, siteName, siteYml, issues, shouldFix, fixed }) {
|
|
552
562
|
const dataDir = join(sitePath, 'public', DATA_DIR)
|
|
553
|
-
|
|
563
|
+
// ⚠️ THE QUERIES, not the pool. `public/<DATA_DIR>/x.json` is a query's
|
|
564
|
+
// MATERIALIZATION — one file per named query — so the bijection is with the
|
|
565
|
+
// declared queries, never with the schema folders under `entities/`.
|
|
566
|
+
//
|
|
567
|
+
// ⛔ BOTH HOMES, or this reports every compiled file as an orphan. A site that
|
|
568
|
+
// keeps its queries in `queries.yml` has none in `site.yml`, and reading one
|
|
569
|
+
// file would turn the whole check into a false positive — the loudest possible
|
|
570
|
+
// failure for a check whose job is to find stale output.
|
|
571
|
+
//
|
|
572
|
+
// Read directly rather than through `resolveQueriesConfig`: this pass is
|
|
573
|
+
// synchronous, and it needs the NAMES rather than resolved declarations.
|
|
574
|
+
const declared = new Set([
|
|
575
|
+
...Object.keys(siteYml.queries || {}),
|
|
576
|
+
...Object.keys(readQueriesYml(sitePath))
|
|
577
|
+
])
|
|
554
578
|
|
|
555
579
|
if (existsSync(dataDir)) {
|
|
556
580
|
// A collection `x` owns `x.json` (the cascade) and `x/` (per-record files
|
|
@@ -565,16 +589,16 @@ function checkGeneratedDataDir({ sitePath, siteName, siteYml, issues, shouldFix,
|
|
|
565
589
|
.map((entry) => (entry.isDirectory() ? `${entry.name}/` : entry.name))
|
|
566
590
|
|
|
567
591
|
if (orphans.length > 0) {
|
|
568
|
-
const id = 'orphaned-
|
|
592
|
+
const id = 'orphaned-query-output'
|
|
569
593
|
issues.push({
|
|
570
594
|
id,
|
|
571
595
|
type: 'warning',
|
|
572
596
|
site: siteName,
|
|
573
|
-
message: `${orphans.length} entr${orphans.length === 1 ? 'y' : 'ies'} in public/${DATA_DIR}/ with no declared
|
|
597
|
+
message: `${orphans.length} entr${orphans.length === 1 ? 'y' : 'ies'} in public/${DATA_DIR}/ with no declared query`
|
|
574
598
|
})
|
|
575
599
|
warn(`[${id}] Stale output in public/${DATA_DIR}/: ${orphans.join(', ')}`)
|
|
576
600
|
log(
|
|
577
|
-
` No
|
|
601
|
+
` No query produces ${orphans.length === 1 ? 'it' : 'these'}. ` +
|
|
578
602
|
`${orphans.length === 1 ? 'It is' : 'They are'} still served and deployed.`
|
|
579
603
|
)
|
|
580
604
|
if (shouldFix(id)) {
|
|
@@ -614,7 +638,7 @@ function checkGeneratedDataDir({ sitePath, siteName, siteYml, issues, shouldFix,
|
|
|
614
638
|
const body = existing === null ? '' : existing.replace(/\n*$/, '\n')
|
|
615
639
|
writeFileSync(
|
|
616
640
|
gitignorePath,
|
|
617
|
-
`${body}\n# Compiled
|
|
641
|
+
`${body}\n# Compiled query results — generated from entities/ + queries.yml\n${rule}\n`
|
|
618
642
|
)
|
|
619
643
|
fixed(`added ${rule} to ${gitignorePath}`)
|
|
620
644
|
if (existsSync(dataDir)) {
|
|
@@ -891,9 +915,9 @@ export async function doctor(args = []) {
|
|
|
891
915
|
}
|
|
892
916
|
|
|
893
917
|
// `public/<DATA_DIR>/` is the build's output directory and nothing else —
|
|
894
|
-
// `
|
|
895
|
-
// makes the mapping a bijection: every entry there should be backed
|
|
896
|
-
// declared
|
|
918
|
+
// `entities/` + `records.yml` is the only supported way to provide structured
|
|
919
|
+
// data. That makes the mapping a bijection: every entry there should be backed
|
|
920
|
+
// by a declared QUERY, so anything else is stale, and identifiable.
|
|
897
921
|
//
|
|
898
922
|
// It matters because the directory is written into the source tree rather
|
|
899
923
|
// than dist/, so what lands there persists and gets deployed. A collection
|
package/src/commands/i18n.js
CHANGED
|
@@ -242,13 +242,13 @@ async function loadSiteConfig(siteRoot) {
|
|
|
242
242
|
async function runExtract(siteRoot, config, args) {
|
|
243
243
|
const verbose = args.includes('--verbose') || args.includes('-v')
|
|
244
244
|
const dryRun = args.includes('--dry-run')
|
|
245
|
-
const
|
|
246
|
-
args.includes('--
|
|
247
|
-
const
|
|
245
|
+
const recordsOnly =
|
|
246
|
+
args.includes('--records-only') || args.includes('--records')
|
|
247
|
+
const noRecords = args.includes('--no-records')
|
|
248
248
|
// --with-collections is now a no-op (collections are included by default)
|
|
249
249
|
|
|
250
|
-
// Extract page content (unless --
|
|
251
|
-
if (!
|
|
250
|
+
// Extract page content (unless --records-only)
|
|
251
|
+
if (!recordsOnly) {
|
|
252
252
|
log(
|
|
253
253
|
`\n${colors.cyan}Extracting translatable content${dryRun ? ' (dry run)' : ''}...${colors.reset}\n`
|
|
254
254
|
)
|
|
@@ -309,43 +309,43 @@ async function runExtract(siteRoot, config, args) {
|
|
|
309
309
|
}
|
|
310
310
|
}
|
|
311
311
|
|
|
312
|
-
// Extract collection content (by default, skip with --no-
|
|
313
|
-
if (!
|
|
312
|
+
// Extract collection content (by default, skip with --no-records)
|
|
313
|
+
if (!noRecords) {
|
|
314
314
|
log(
|
|
315
|
-
`\n${colors.cyan}Extracting
|
|
315
|
+
`\n${colors.cyan}Extracting record content${dryRun ? ' (dry run)' : ''}...${colors.reset}\n`
|
|
316
316
|
)
|
|
317
317
|
|
|
318
318
|
// Check if collections exist
|
|
319
319
|
const dataDir = join(siteRoot, 'public', DATA_DIR)
|
|
320
320
|
if (!existsSync(dataDir)) {
|
|
321
|
-
if (
|
|
322
|
-
error('No
|
|
321
|
+
if (recordsOnly) {
|
|
322
|
+
error('No records found. Add entities under entities/ and list them in records.yml.')
|
|
323
323
|
process.exit(1)
|
|
324
324
|
}
|
|
325
|
-
log(`${colors.dim}No
|
|
325
|
+
log(`${colors.dim}No records found in public/data/.${colors.reset}`)
|
|
326
326
|
return
|
|
327
327
|
}
|
|
328
328
|
|
|
329
329
|
try {
|
|
330
|
-
const {
|
|
330
|
+
const { extractRecordManifest, formatSyncReport } =
|
|
331
331
|
await import('@uniweb/build/i18n')
|
|
332
332
|
|
|
333
|
-
const
|
|
333
|
+
const recordManifestPath = join(
|
|
334
334
|
siteRoot,
|
|
335
335
|
config.localesDir,
|
|
336
|
-
'
|
|
336
|
+
'records',
|
|
337
337
|
'manifest.json'
|
|
338
338
|
)
|
|
339
|
-
const isUpdate = existsSync(
|
|
339
|
+
const isUpdate = existsSync(recordManifestPath)
|
|
340
340
|
|
|
341
|
-
const { manifest, report } = await
|
|
341
|
+
const { manifest, report } = await extractRecordManifest(siteRoot, {
|
|
342
342
|
localesDir: config.localesDir,
|
|
343
343
|
dryRun
|
|
344
344
|
})
|
|
345
345
|
|
|
346
346
|
const unitCount = Object.keys(manifest.units).length
|
|
347
347
|
if (unitCount > 0) {
|
|
348
|
-
success(`Extracted ${unitCount} translatable strings from
|
|
348
|
+
success(`Extracted ${unitCount} translatable strings from records`)
|
|
349
349
|
|
|
350
350
|
if (report && isUpdate) {
|
|
351
351
|
log('')
|
|
@@ -356,18 +356,18 @@ async function runExtract(siteRoot, config, args) {
|
|
|
356
356
|
log(`\n${colors.dim}Dry run — no files were modified.${colors.reset}`)
|
|
357
357
|
} else {
|
|
358
358
|
log(
|
|
359
|
-
`\nManifest written to: ${colors.dim}${config.localesDir}/
|
|
359
|
+
`\nManifest written to: ${colors.dim}${config.localesDir}/records/manifest.json${colors.reset}`
|
|
360
360
|
)
|
|
361
361
|
}
|
|
362
362
|
} else {
|
|
363
363
|
log(
|
|
364
|
-
`${colors.dim}No translatable content found in
|
|
364
|
+
`${colors.dim}No translatable content found in records.${colors.reset}`
|
|
365
365
|
)
|
|
366
366
|
}
|
|
367
367
|
} catch (err) {
|
|
368
|
-
error(`
|
|
368
|
+
error(`Record extraction failed: ${err.message}`)
|
|
369
369
|
if (verbose) console.error(err)
|
|
370
|
-
if (
|
|
370
|
+
if (recordsOnly) process.exit(1)
|
|
371
371
|
}
|
|
372
372
|
}
|
|
373
373
|
}
|
|
@@ -652,7 +652,7 @@ async function runStatusFreeform(siteRoot, config, locale, options = {}) {
|
|
|
652
652
|
const allPaths = [
|
|
653
653
|
...discovered.pages,
|
|
654
654
|
...discovered.pageIds,
|
|
655
|
-
...discovered.
|
|
655
|
+
...discovered.records
|
|
656
656
|
]
|
|
657
657
|
|
|
658
658
|
// Check staleness
|
|
@@ -988,11 +988,11 @@ async function runAudit(siteRoot, config, args) {
|
|
|
988
988
|
* Usage:
|
|
989
989
|
* uniweb i18n init-freeform es pages/about hero
|
|
990
990
|
* uniweb i18n init-freeform es page-ids/installation intro
|
|
991
|
-
* uniweb i18n init-freeform es
|
|
991
|
+
* uniweb i18n init-freeform es entities/article getting-started
|
|
992
992
|
*/
|
|
993
993
|
async function runInitFreeform(siteRoot, config, args) {
|
|
994
994
|
const locale = args[0]
|
|
995
|
-
const pathType = args[1] // pages/about, page-ids/installation,
|
|
995
|
+
const pathType = args[1] // pages/about, page-ids/installation, entities/article
|
|
996
996
|
const sectionId = args[2] // hero, intro, getting-started
|
|
997
997
|
|
|
998
998
|
if (!locale || !pathType || !sectionId) {
|
|
@@ -1001,7 +1001,7 @@ async function runInitFreeform(siteRoot, config, args) {
|
|
|
1001
1001
|
log(' uniweb i18n init-freeform es pages/about hero')
|
|
1002
1002
|
log(' uniweb i18n init-freeform es page-ids/installation intro')
|
|
1003
1003
|
log(
|
|
1004
|
-
` uniweb i18n init-freeform es
|
|
1004
|
+
` uniweb i18n init-freeform es entities/article getting-started${colors.reset}`
|
|
1005
1005
|
)
|
|
1006
1006
|
process.exit(1)
|
|
1007
1007
|
}
|
|
@@ -1057,15 +1057,29 @@ async function runInitFreeform(siteRoot, config, args) {
|
|
|
1057
1057
|
if (sourceContent) break
|
|
1058
1058
|
}
|
|
1059
1059
|
}
|
|
1060
|
-
} else if (pathType.startsWith('
|
|
1061
|
-
//
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
)
|
|
1060
|
+
} else if (pathType.startsWith('entities/')) {
|
|
1061
|
+
// ⛔ ADDRESSED BY THE RECORD, matching where the loader reads. The freeform
|
|
1062
|
+
// tree is `entities/<schema dirs>/<slug>.md`; this used to take
|
|
1063
|
+
// `collections/<query>` and write a path nothing read — the loader moved
|
|
1064
|
+
// and this did not.
|
|
1065
|
+
//
|
|
1066
|
+
// The record's CONTENT still lives in a query's materialization, so the
|
|
1067
|
+
// query that covers this schema is resolved rather than named.
|
|
1068
|
+
const poolDirs = pathType.replace('entities/', '')
|
|
1069
|
+
const { resolveQueriesConfig, poolDirsForSchema } = await import('@uniweb/build/uwx')
|
|
1070
|
+
let queryName = null
|
|
1071
|
+
try {
|
|
1072
|
+
const { declarations } = await resolveQueriesConfig(siteRoot)
|
|
1073
|
+
for (const [name, decl] of Object.entries(declarations || {})) {
|
|
1074
|
+
const dirs = decl.schema ? poolDirsForSchema(decl.schema) : null
|
|
1075
|
+
if (dirs && dirs.join('/') === poolDirs) { queryName = name; break }
|
|
1076
|
+
}
|
|
1077
|
+
} catch {
|
|
1078
|
+
/* no resolvable config — fall through to the not-found message below */
|
|
1079
|
+
}
|
|
1080
|
+
const dataPath = queryName
|
|
1081
|
+
? join(siteRoot, 'public', 'data', `${queryName}.json`)
|
|
1082
|
+
: join(siteRoot, 'public', 'data', '__none__.json')
|
|
1069
1083
|
|
|
1070
1084
|
if (existsSync(dataPath)) {
|
|
1071
1085
|
const dataRaw = await readFile(dataPath, 'utf-8')
|
|
@@ -1607,8 +1621,8 @@ ${colors.bright}Options:${colors.reset}
|
|
|
1607
1621
|
--freeform (status/prune) Include free-form translation status
|
|
1608
1622
|
--json (status) Output as JSON for translation tools
|
|
1609
1623
|
--by-page (status --missing) Group missing strings by page
|
|
1610
|
-
--
|
|
1611
|
-
--no-
|
|
1624
|
+
--records-only (extract/status/audit) Process only records
|
|
1625
|
+
--no-records (extract/status/audit) Skip records (pages only)
|
|
1612
1626
|
--all-stale (update-hash) Update all stale translations at once
|
|
1613
1627
|
|
|
1614
1628
|
${colors.bright}Configuration:${colors.reset}
|
|
@@ -1638,14 +1652,14 @@ ${colors.bright}File Structure:${colors.reset}
|
|
|
1638
1652
|
.manifest.json Staleness tracking
|
|
1639
1653
|
pages/about/hero.md Translated content for /about page, hero section
|
|
1640
1654
|
page-ids/install/intro.md Translated content by page ID
|
|
1641
|
-
|
|
1655
|
+
entities/article/getting-started.md
|
|
1642
1656
|
|
|
1643
1657
|
${colors.bright}Examples:${colors.reset}
|
|
1644
1658
|
${colors.dim}# Hash-based workflow${colors.reset}
|
|
1645
1659
|
uniweb i18n extract # Extract all translatable strings
|
|
1646
1660
|
uniweb i18n extract --dry-run # Preview without writing
|
|
1647
1661
|
uniweb i18n extract --verbose # Show extracted strings
|
|
1648
|
-
uniweb i18n extract --no-
|
|
1662
|
+
uniweb i18n extract --no-records # Pages only (skip records)
|
|
1649
1663
|
uniweb i18n generate es fr # Create starter files for Spanish and French
|
|
1650
1664
|
uniweb i18n generate --empty # Create files with empty values (for translators)
|
|
1651
1665
|
uniweb i18n generate --force # Overwrite existing locale files
|
|
@@ -1658,7 +1672,7 @@ ${colors.bright}Examples:${colors.reset}
|
|
|
1658
1672
|
${colors.dim}# Free-form workflow (complete section replacement)${colors.reset}
|
|
1659
1673
|
uniweb i18n init-freeform es pages/about hero
|
|
1660
1674
|
uniweb i18n init-freeform es page-ids/installation intro
|
|
1661
|
-
uniweb i18n init-freeform es
|
|
1675
|
+
uniweb i18n init-freeform es entities/article getting-started
|
|
1662
1676
|
uniweb i18n status --freeform # Show free-form translation status
|
|
1663
1677
|
uniweb i18n update-hash es --all-stale # Update hashes after review
|
|
1664
1678
|
uniweb i18n move pages/docs/setup pages/getting-started
|
package/src/commands/publish.js
CHANGED
|
@@ -70,6 +70,7 @@ import {
|
|
|
70
70
|
readSiteIdentity
|
|
71
71
|
} from '../utils/site-identity.js'
|
|
72
72
|
import { isNonInteractive, confirm } from '../utils/interactive.js'
|
|
73
|
+
import { guardEmptyRecords } from '../utils/records-guard.js'
|
|
73
74
|
import { headProvenance } from '../utils/git.js'
|
|
74
75
|
import {
|
|
75
76
|
makeModelResolver,
|
|
@@ -93,7 +94,7 @@ import {
|
|
|
93
94
|
readPaymentRefusal,
|
|
94
95
|
reportPaymentRefusal
|
|
95
96
|
} from '../backend/payment-handoff.js'
|
|
96
|
-
import {
|
|
97
|
+
import { reportSchemalessQueries } from '../utils/schemaless-report.js'
|
|
97
98
|
import { uploadSiteData } from '../utils/site-data-upload.js'
|
|
98
99
|
|
|
99
100
|
const c = {
|
|
@@ -113,7 +114,20 @@ const say = {
|
|
|
113
114
|
dim: (m) => console.log(` ${c.dim}${m}${c.reset}`)
|
|
114
115
|
}
|
|
115
116
|
|
|
116
|
-
// Origin-relative serve path → clickable absolute URL
|
|
117
|
+
// Origin-relative serve path → clickable absolute URL.
|
|
118
|
+
//
|
|
119
|
+
// ⭐ THE TWO SHAPES ARE A CONTRACT, NOT AN INCONSISTENCY — ratified 2026-08-29 and
|
|
120
|
+
// documented in the backend's `wire-layer.md` rather than merely observed. A publish
|
|
121
|
+
// returns an ABSOLUTE url when Cloudflare hosts the site (another origin entirely)
|
|
122
|
+
// and an ORIGIN-RELATIVE path when the backend serves it itself, where its own
|
|
123
|
+
// external origin is not reliably self-reportable from behind an ALB.
|
|
124
|
+
//
|
|
125
|
+
// ⇒ So this branch is implementing the contract, not defending against drift. I
|
|
126
|
+
// reported the two shapes as a violation of "finished values only" in collab
|
|
127
|
+
// framework-backend-812b; the backend checked, found the adjacent ruling that
|
|
128
|
+
// explains the relative arm, and ratified both. Do not "fix" it by demanding one
|
|
129
|
+
// shape — the caller's own origin is the missing half on the relative arm, and we
|
|
130
|
+
// are the caller.
|
|
117
131
|
function absolutizeServeUrl(origin, url) {
|
|
118
132
|
if (!url || typeof url !== 'string') return null
|
|
119
133
|
if (/^https?:\/\//.test(url)) return url
|
|
@@ -234,15 +248,17 @@ export async function publish(args = []) {
|
|
|
234
248
|
}
|
|
235
249
|
const asOrg = org.asOrg
|
|
236
250
|
|
|
237
|
-
//
|
|
238
|
-
//
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
251
|
+
// ⛔ There is no capability gate here any more, deliberately.
|
|
252
|
+
//
|
|
253
|
+
// This used to read `delivery.publish` from the discovery document and refuse when it
|
|
254
|
+
// was false. It could never fire: the backend sent a literal true for every deployment,
|
|
255
|
+
// so the gate compared a constant against false. The key is now gone on both sides
|
|
256
|
+
// (2026-08-30). Restoring a reader for it would re-create a check that cannot fail
|
|
257
|
+
// while implying a capability that was never negotiable.
|
|
258
|
+
//
|
|
259
|
+
// Discovery is not consulted on this path at all. The one leaf the CLI still reads
|
|
260
|
+
// (`delivery.siteSubscriptionRequired`) is read after the site create, where a
|
|
261
|
+
// credential is already in hand.
|
|
246
262
|
|
|
247
263
|
// ⛔ NOTHING about a runtime is sent from here. `site.yml::runtime` was a
|
|
248
264
|
// vestigial prop and is no longer read [Diego, 2026-08-22]; `?runtime=` is no
|
|
@@ -436,6 +452,14 @@ export async function publish(args = []) {
|
|
|
436
452
|
// Non-local @std/registry Model schemas resolve through the backend (same as push).
|
|
437
453
|
const resolveModel = makeModelResolver({ client, offline: false })
|
|
438
454
|
|
|
455
|
+
// ⛔ AN EMPTY `records.yml` REMOVES. It is the one path where an ordinary act is
|
|
456
|
+
// destructive — a placeholder file, created meaning to fill it in — so the count
|
|
457
|
+
// is reported and confirmed before anything is sent.
|
|
458
|
+
{
|
|
459
|
+
const guard = await guardEmptyRecords({ siteDir, args, warn: say.warn, note: say.dim })
|
|
460
|
+
if (!guard.ok) return { exitCode: 1 }
|
|
461
|
+
}
|
|
462
|
+
|
|
439
463
|
// 3. Partition collections by schema presence (a first emit reads `schemaless`
|
|
440
464
|
// — collections with no data schema, delivered statically via the ball).
|
|
441
465
|
let probe
|
|
@@ -456,7 +480,7 @@ export async function publish(args = []) {
|
|
|
456
480
|
// A product decision the author is usually making unknowingly — say it at warn
|
|
457
481
|
// level, not dim among everything else. See the helper for what the old
|
|
458
482
|
// message got wrong.
|
|
459
|
-
|
|
483
|
+
reportSchemalessQueries(probe.schemaless, say)
|
|
460
484
|
const localAssets = probe.localAssets || []
|
|
461
485
|
|
|
462
486
|
// 3a. A clone with no `$uuid` is bound to no backend site, so every cached map
|
|
@@ -579,7 +603,7 @@ export async function publish(args = []) {
|
|
|
579
603
|
// `client.discover()` is the mechanism if that changes — `DISCOVERY_DEFAULTS`
|
|
580
604
|
// makes an absent key non-breaking by construction.
|
|
581
605
|
if (ball) {
|
|
582
|
-
say.info('Uploading
|
|
606
|
+
say.info('Uploading schema-less record data…')
|
|
583
607
|
try {
|
|
584
608
|
const r = await uploadSiteData({
|
|
585
609
|
apiBase: client.origin,
|
|
@@ -595,9 +619,9 @@ export async function publish(args = []) {
|
|
|
595
619
|
for (const f of r.failed) say.dim(` ${f.path} (HTTP ${f.status})`)
|
|
596
620
|
return { exitCode: 1 }
|
|
597
621
|
}
|
|
598
|
-
say.dim(`
|
|
622
|
+
say.dim(`Record data : ${r.uploaded.length} file(s) [${r.mode}]`)
|
|
599
623
|
} catch (err) {
|
|
600
|
-
say.err(`
|
|
624
|
+
say.err(`Record data upload failed: ${err.message}`)
|
|
601
625
|
return { exitCode: 1 }
|
|
602
626
|
}
|
|
603
627
|
}
|