@crombolo/cli 0.9.1 → 0.11.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/bin/crombolo.ts +161 -37
- package/package.json +7 -7
- package/src/operations/commands.ts +244 -333
- package/src/templates.ts +1 -1
- package/templates/basic/bundle/package.json +16 -0
- package/templates/demo/harbourstone/bundle/package.json +16 -0
- package/templates/demo/harbourstone/files/schema/forms/contact.toml +2 -2
- package/templates/starter/bundle/package.json +16 -0
- package/src/operations/sections.ts +0 -423
- /package/templates/basic/bundle/{bundle.json → content/bundle.json} +0 -0
- /package/templates/basic/bundle/{nodes → content/nodes}/36955f6e-d110-469e-b280-72096eccd148.json +0 -0
- /package/templates/demo/harbourstone/bundle/{blobs → content/blobs}/180cc8ef/distillery-from-the-cliff-path.jpg +0 -0
- /package/templates/demo/harbourstone/bundle/{blobs → content/blobs}/68c9cfdf/bottle-brine-and-kiln.jpg +0 -0
- /package/templates/demo/harbourstone/bundle/{blobs → content/blobs}/6be45535/bottle-oloroso-pier.jpg +0 -0
- /package/templates/demo/harbourstone/bundle/{blobs → content/blobs}/6d8d6398/bottle-the-cove.jpg +0 -0
- /package/templates/demo/harbourstone/bundle/{blobs → content/blobs}/7b4713f8/copper-pot-stills.jpg +0 -0
- /package/templates/demo/harbourstone/bundle/{blobs → content/blobs}/c114b7d8/bottle-neap-tide.jpg +0 -0
- /package/templates/demo/harbourstone/bundle/{blobs → content/blobs}/fd337567/casks-in-the-sea-facing-warehouse.jpg +0 -0
- /package/templates/demo/harbourstone/bundle/{bundle.json → content/bundle.json} +0 -0
- /package/templates/demo/harbourstone/bundle/{nodes → content/nodes}/069cc3c5-c9f5-4792-90c9-e2bfcc4855b6.json +0 -0
- /package/templates/demo/harbourstone/bundle/{nodes → content/nodes}/13788fe7-1a45-4b48-8ae4-f4227e154e1e.json +0 -0
- /package/templates/demo/harbourstone/bundle/{nodes → content/nodes}/180cc8ef-caf1-4316-b6a7-e3b5aee41ffd.json +0 -0
- /package/templates/demo/harbourstone/bundle/{nodes → content/nodes}/22144db2-e214-4006-a0bb-53b5bd121fd6.json +0 -0
- /package/templates/demo/harbourstone/bundle/{nodes → content/nodes}/26e87610-e2d6-4b47-bf92-3967f7186907.json +0 -0
- /package/templates/demo/harbourstone/bundle/{nodes → content/nodes}/2caa099b-3884-472d-9cea-797a0f0c376f.json +0 -0
- /package/templates/demo/harbourstone/bundle/{nodes → content/nodes}/2efbdb5b-7888-4a49-bca5-c0f85b76e472.json +0 -0
- /package/templates/demo/harbourstone/bundle/{nodes → content/nodes}/2fe03891-1294-4344-8e24-47e8b212e5b0.json +0 -0
- /package/templates/demo/harbourstone/bundle/{nodes → content/nodes}/38b009f7-03c0-4fda-b5c7-843ad6044e9c.json +0 -0
- /package/templates/demo/harbourstone/bundle/{nodes → content/nodes}/3d312cc8-2937-40e1-ba14-fd781c9a862d.json +0 -0
- /package/templates/demo/harbourstone/bundle/{nodes → content/nodes}/3dc30fc1-973a-4217-b26f-24902210be17.json +0 -0
- /package/templates/demo/harbourstone/bundle/{nodes → content/nodes}/40107d59-f3e9-4592-aee8-5b03d4586cbd.json +0 -0
- /package/templates/demo/harbourstone/bundle/{nodes → content/nodes}/483a0d3b-a10c-4c9d-8c81-d9607d37e968.json +0 -0
- /package/templates/demo/harbourstone/bundle/{nodes → content/nodes}/4b760c0b-b83a-4eee-aa29-c225cfabf19b.json +0 -0
- /package/templates/demo/harbourstone/bundle/{nodes → content/nodes}/4e5c7f3f-54cb-4328-ad61-bc4c2736b890.json +0 -0
- /package/templates/demo/harbourstone/bundle/{nodes → content/nodes}/62f479ff-0875-453e-99ae-3bf1bec5e3c8.json +0 -0
- /package/templates/demo/harbourstone/bundle/{nodes → content/nodes}/68c9cfdf-6b40-47da-a1d2-17256a6cc6db.json +0 -0
- /package/templates/demo/harbourstone/bundle/{nodes → content/nodes}/6ad8f0f3-9334-47f1-b805-80c4b4b4fa88.json +0 -0
- /package/templates/demo/harbourstone/bundle/{nodes → content/nodes}/6be45535-83e5-4469-acf0-4127016721ae.json +0 -0
- /package/templates/demo/harbourstone/bundle/{nodes → content/nodes}/6d8d6398-738f-4f38-ad3c-e5d6e9a971fe.json +0 -0
- /package/templates/demo/harbourstone/bundle/{nodes → content/nodes}/7b4713f8-d917-457d-a9d8-7dd01a3617b4.json +0 -0
- /package/templates/demo/harbourstone/bundle/{nodes → content/nodes}/88ec1185-53b5-439a-a2ce-3f56603be4ff.json +0 -0
- /package/templates/demo/harbourstone/bundle/{nodes → content/nodes}/97469df2-107c-4010-8c14-4de88f735529.json +0 -0
- /package/templates/demo/harbourstone/bundle/{nodes → content/nodes}/a2eb439f-71d9-4ea6-89d7-c5bc72618281.json +0 -0
- /package/templates/demo/harbourstone/bundle/{nodes → content/nodes}/a562f0a2-cb6e-4ead-a1ce-4d180632419f.json +0 -0
- /package/templates/demo/harbourstone/bundle/{nodes → content/nodes}/a5e36a18-a2ef-4eb5-85b4-7e12b21cca06.json +0 -0
- /package/templates/demo/harbourstone/bundle/{nodes → content/nodes}/b1876d3a-50a7-4358-84bf-a0899932ffa9.json +0 -0
- /package/templates/demo/harbourstone/bundle/{nodes → content/nodes}/b4e38bbd-c1a3-431a-b7a9-5868aa32993a.json +0 -0
- /package/templates/demo/harbourstone/bundle/{nodes → content/nodes}/c114b7d8-0138-404b-9d8f-a39d992577a2.json +0 -0
- /package/templates/demo/harbourstone/bundle/{nodes → content/nodes}/cb4c569e-c28a-43b6-a4d7-d33bb544e973.json +0 -0
- /package/templates/demo/harbourstone/bundle/{nodes → content/nodes}/cd681b4b-9c4e-4624-9c13-450aa6a9c135.json +0 -0
- /package/templates/demo/harbourstone/bundle/{nodes → content/nodes}/d60ef12b-0f0a-4db5-9763-21fa52b8d4da.json +0 -0
- /package/templates/demo/harbourstone/bundle/{nodes → content/nodes}/d7f13222-d060-4496-b86d-bef21da963c1.json +0 -0
- /package/templates/demo/harbourstone/bundle/{nodes → content/nodes}/f36dd4a9-927c-4ca3-a790-c9c859ac7302.json +0 -0
- /package/templates/demo/harbourstone/bundle/{nodes → content/nodes}/fd337567-df67-41ca-ae94-9d84d8d6042f.json +0 -0
- /package/templates/starter/bundle/{bundle.json → content/bundle.json} +0 -0
- /package/templates/starter/bundle/{nodes → content/nodes}/634dd948-cf83-4cad-9b7b-7c938b33da76.json +0 -0
- /package/templates/starter/bundle/{nodes → content/nodes}/64d95359-d91a-4050-b440-3e5124a74999.json +0 -0
- /package/templates/starter/bundle/{nodes → content/nodes}/d2997ebb-36ca-47cb-bbc8-31fefca787c0.json +0 -0
package/src/templates.ts
CHANGED
|
@@ -29,7 +29,7 @@ export const TEMPLATES_DIR = join(import.meta.dir, '..', 'templates')
|
|
|
29
29
|
* Written into the site's own files rather than hidden anywhere, because that
|
|
30
30
|
* import is the whole of what lets a bundle answer a request — so it has to be
|
|
31
31
|
* a line the developer owns from the first commit and can delete
|
|
32
|
-
* (`docs/
|
|
32
|
+
* (`docs/bundles.md`).
|
|
33
33
|
*/
|
|
34
34
|
export interface TemplateServerBundle {
|
|
35
35
|
/** The npm package, added to the site's dependencies. */
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "demo-harbourstone",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"files": [
|
|
6
|
+
"content"
|
|
7
|
+
],
|
|
8
|
+
"peerDependencies": {
|
|
9
|
+
"@crombolo/crombolo": "^0.11.0"
|
|
10
|
+
},
|
|
11
|
+
"crombolo": {
|
|
12
|
+
"id": "demo-harbourstone",
|
|
13
|
+
"name": "Harbourstone Distillery",
|
|
14
|
+
"content": "content"
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# A form is structure, so it is a file like the content types beside it and a
|
|
2
|
-
# change to it shows up in a diff (`docs/
|
|
2
|
+
# change to it shows up in a diff (`docs/forms.md`). The Contact page points
|
|
3
3
|
# at this one through its `enquiryForm` property, and `components/contactPage.tsx`
|
|
4
4
|
# renders it with `<Form>`.
|
|
5
5
|
#
|
|
@@ -103,7 +103,7 @@ minimum-submit-seconds = 3
|
|
|
103
103
|
mandatory-message = "We cannot store your message without this"
|
|
104
104
|
|
|
105
105
|
# Telling somebody about a submission takes an e-mail provider, which this
|
|
106
|
-
# template does not configure — see `docs/
|
|
106
|
+
# template does not configure — see `docs/configuration.md`. With
|
|
107
107
|
# CROMBOLO_EMAIL_* set, uncomment this and the message is mailed on submit; the
|
|
108
108
|
# queue retries, so a provider that is briefly down loses nothing.
|
|
109
109
|
#
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "starter",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"files": [
|
|
6
|
+
"content"
|
|
7
|
+
],
|
|
8
|
+
"peerDependencies": {
|
|
9
|
+
"@crombolo/crombolo": "^0.11.0"
|
|
10
|
+
},
|
|
11
|
+
"crombolo": {
|
|
12
|
+
"id": "starter",
|
|
13
|
+
"name": "Starter",
|
|
14
|
+
"content": "content"
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -1,423 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The sections a bundle carries beyond its content: schema, views, partials,
|
|
3
|
-
* styles, scripts and dictionary items. `docs/17-bundles.md`.
|
|
4
|
-
*
|
|
5
|
-
* A bundle names what a file *is*, never where it goes — the destination site
|
|
6
|
-
* decides that from its own configuration, so a bundle written against one
|
|
7
|
-
* layout installs into another. This module owns that mapping, and the order
|
|
8
|
-
* applying them has to happen in: structure before the content that needs it,
|
|
9
|
-
* views before the content that names a template.
|
|
10
|
-
*
|
|
11
|
-
* Checking is read-only and has to answer "what would the schema do" *before*
|
|
12
|
-
* anything is written, which `checkSchemaDirectory` cannot do against files that
|
|
13
|
-
* are not there yet. So the check runs against a copy of the site's `schema/`
|
|
14
|
-
* with the bundle's files laid over it — the site's own directory is never
|
|
15
|
-
* touched until an apply.
|
|
16
|
-
*/
|
|
17
|
-
import {
|
|
18
|
-
copyFileSync,
|
|
19
|
-
cpSync,
|
|
20
|
-
existsSync,
|
|
21
|
-
mkdirSync,
|
|
22
|
-
mkdtempSync,
|
|
23
|
-
readFileSync,
|
|
24
|
-
renameSync,
|
|
25
|
-
rmSync,
|
|
26
|
-
writeFileSync,
|
|
27
|
-
} from 'node:fs'
|
|
28
|
-
import { tmpdir } from 'node:os'
|
|
29
|
-
import { dirname, join } from 'node:path'
|
|
30
|
-
import type { Db } from '@crombolo/data'
|
|
31
|
-
import type { CromboloConfig } from '@crombolo/server'
|
|
32
|
-
import { checkSchemaDirectory, importDictionaryUdt, syncSchemaDirectory } from '@crombolo/server'
|
|
33
|
-
import type { BundleSection, LoadedBundle } from '@crombolo/transfer'
|
|
34
|
-
import { BUNDLE_SECTIONS } from '@crombolo/transfer'
|
|
35
|
-
|
|
36
|
-
/** What applying one carried file would do to the destination. */
|
|
37
|
-
export type FileAction = 'create' | 'overwrite' | 'unchanged'
|
|
38
|
-
|
|
39
|
-
export interface SectionFile {
|
|
40
|
-
/** The path inside the bundle, e.g. `views/homePage.tsx`. */
|
|
41
|
-
path: string
|
|
42
|
-
/** Where it would land on this site. */
|
|
43
|
-
target: string
|
|
44
|
-
action: FileAction
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export interface SectionPlan {
|
|
48
|
-
files: SectionFile[]
|
|
49
|
-
/** How many dictionary items the bundle carries, when it carries any. */
|
|
50
|
-
dictionaryItems: number
|
|
51
|
-
/**
|
|
52
|
-
* What the schema half would do, checked against an overlay rather than the
|
|
53
|
-
* site: `none` when the bundle carries no schema.
|
|
54
|
-
*/
|
|
55
|
-
schema: { classification: string; findings: string[] } | undefined
|
|
56
|
-
/** Sections present in the bundle, for a caller that only wants to say so. */
|
|
57
|
-
sections: BundleSection[]
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
export interface SectionOptions {
|
|
61
|
-
/** Skip the schema half, leaving the site's own structure alone. */
|
|
62
|
-
withoutSchema?: boolean
|
|
63
|
-
/** Skip views, partials, styles and scripts. */
|
|
64
|
-
withoutFiles?: boolean
|
|
65
|
-
/**
|
|
66
|
-
* Allow replacing files this site already has.
|
|
67
|
-
*
|
|
68
|
-
* Off by default, and an install is refused while any file would be
|
|
69
|
-
* replaced. These files are editable in the backoffice — a stylesheet, a
|
|
70
|
-
* template, a content type — so overwriting one destroys somebody's work,
|
|
71
|
-
* and this site may well have no git to recover it from.
|
|
72
|
-
*/
|
|
73
|
-
replaceFiles?: boolean
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Where an install keeps the files it replaced, under the site.
|
|
78
|
-
*
|
|
79
|
-
* A sibling of the components cache, not inside it: `.crombolo/components` is
|
|
80
|
-
* rebuilt from `components/` and cleared at boot, and these have to outlive a
|
|
81
|
-
* restart to be worth taking.
|
|
82
|
-
*/
|
|
83
|
-
export const RUN_BACKUP_DIR = join('.crombolo', 'transfer')
|
|
84
|
-
|
|
85
|
-
/** What one install did to the files, so a revert can undo exactly that. */
|
|
86
|
-
interface FileRecord {
|
|
87
|
-
path: string
|
|
88
|
-
action: FileAction
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
const RECORD_FILE = 'files.json'
|
|
92
|
-
const REPLACED_DIR = 'replaced'
|
|
93
|
-
|
|
94
|
-
/** Where each section lands, from the site's own configuration. */
|
|
95
|
-
function destinationOf(config: CromboloConfig, section: BundleSection): string | undefined {
|
|
96
|
-
switch (section) {
|
|
97
|
-
case 'schema':
|
|
98
|
-
return config.schemaDir
|
|
99
|
-
case 'components':
|
|
100
|
-
return config.componentsDir
|
|
101
|
-
case 'styles':
|
|
102
|
-
return config.stylesheetsDir
|
|
103
|
-
case 'scripts':
|
|
104
|
-
return config.scriptsDir
|
|
105
|
-
case 'dictionary':
|
|
106
|
-
return undefined
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
const FILE_SECTIONS: readonly BundleSection[] = ['components', 'styles', 'scripts']
|
|
111
|
-
|
|
112
|
-
/** The path inside a section, with the section's own directory stripped. */
|
|
113
|
-
const withinSection = (path: string, section: BundleSection): string =>
|
|
114
|
-
path.slice(`${BUNDLE_SECTIONS[section]}/`.length)
|
|
115
|
-
|
|
116
|
-
function actionFor(target: string, content: string): FileAction {
|
|
117
|
-
if (!existsSync(target)) return 'create'
|
|
118
|
-
try {
|
|
119
|
-
return readFileSync(target, 'utf8') === content ? 'unchanged' : 'overwrite'
|
|
120
|
-
} catch {
|
|
121
|
-
return 'overwrite'
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
export interface CarriedSections {
|
|
126
|
-
/** Section → the bundle paths it carries. */
|
|
127
|
-
carries: Partial<Record<BundleSection, string[]>>
|
|
128
|
-
/** Bundle path → the file on disk holding it. */
|
|
129
|
-
files: Map<string, string>
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
/** What a loaded bundle carries, or nothing when it is content-only. */
|
|
133
|
-
export function carriedSections(loaded: LoadedBundle): CarriedSections {
|
|
134
|
-
return { carries: loaded.set?.manifest.carries ?? {}, files: loaded.files }
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
const read = (carried: CarriedSections, path: string): string => {
|
|
138
|
-
const file = carried.files.get(path)
|
|
139
|
-
if (!file) throw new Error(`${path} is declared but was not read`)
|
|
140
|
-
return readFileSync(file, 'utf8')
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
/** Read-only: what applying the carried sections here would do. */
|
|
144
|
-
export async function planSections(
|
|
145
|
-
db: Db,
|
|
146
|
-
config: CromboloConfig,
|
|
147
|
-
carried: CarriedSections,
|
|
148
|
-
options: SectionOptions = {},
|
|
149
|
-
): Promise<SectionPlan> {
|
|
150
|
-
const sections = Object.keys(carried.carries) as BundleSection[]
|
|
151
|
-
const files: SectionFile[] = []
|
|
152
|
-
|
|
153
|
-
if (!options.withoutFiles) {
|
|
154
|
-
for (const section of FILE_SECTIONS) {
|
|
155
|
-
const destination = destinationOf(config, section)
|
|
156
|
-
if (!destination) continue
|
|
157
|
-
for (const path of carried.carries[section] ?? []) {
|
|
158
|
-
const target = join(destination, withinSection(path, section))
|
|
159
|
-
files.push({ path, target, action: actionFor(target, read(carried, path)) })
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
const schemaPaths = options.withoutSchema ? [] : (carried.carries.schema ?? [])
|
|
165
|
-
if (!options.withoutSchema) {
|
|
166
|
-
for (const path of schemaPaths) {
|
|
167
|
-
const target = join(config.schemaDir, withinSection(path, 'schema'))
|
|
168
|
-
files.push({ path, target, action: actionFor(target, read(carried, path)) })
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
return {
|
|
173
|
-
files,
|
|
174
|
-
dictionaryItems: (carried.carries.dictionary ?? []).length,
|
|
175
|
-
schema:
|
|
176
|
-
schemaPaths.length > 0
|
|
177
|
-
? await checkSchemaOverlay(db, config, carried, schemaPaths)
|
|
178
|
-
: undefined,
|
|
179
|
-
sections,
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
/**
|
|
184
|
-
* The schema check, run against a copy of the site's `schema/` with the
|
|
185
|
-
* bundle's files laid over it.
|
|
186
|
-
*
|
|
187
|
-
* The site's directory is not written to: a check has to be able to say "this
|
|
188
|
-
* would need data work" without having already changed the thing it is
|
|
189
|
-
* reporting on.
|
|
190
|
-
*/
|
|
191
|
-
async function checkSchemaOverlay(
|
|
192
|
-
db: Db,
|
|
193
|
-
config: CromboloConfig,
|
|
194
|
-
carried: CarriedSections,
|
|
195
|
-
paths: readonly string[],
|
|
196
|
-
): Promise<{ classification: string; findings: string[] }> {
|
|
197
|
-
const overlay = mkdtempSync(join(tmpdir(), 'crombolo-schema-overlay-'))
|
|
198
|
-
try {
|
|
199
|
-
if (existsSync(config.schemaDir)) cpSync(config.schemaDir, overlay, { recursive: true })
|
|
200
|
-
for (const path of paths) {
|
|
201
|
-
const target = join(overlay, withinSection(path, 'schema'))
|
|
202
|
-
mkdirSync(dirname(target), { recursive: true })
|
|
203
|
-
writeFileSync(target, read(carried, path))
|
|
204
|
-
}
|
|
205
|
-
const report = await checkSchemaDirectory(
|
|
206
|
-
db,
|
|
207
|
-
{ ...config, schemaDir: overlay },
|
|
208
|
-
{ nodeId: config.nodeId, revision: config.schemaRevision },
|
|
209
|
-
)
|
|
210
|
-
return {
|
|
211
|
-
classification: report.classification,
|
|
212
|
-
findings: report.outstanding.map((finding) => finding.message),
|
|
213
|
-
}
|
|
214
|
-
} finally {
|
|
215
|
-
rmSync(overlay, { recursive: true, force: true })
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
export interface SectionResult {
|
|
220
|
-
/** The files written, by the path they were written to. */
|
|
221
|
-
written: string[]
|
|
222
|
-
/** Those that replaced something, whose originals were kept. */
|
|
223
|
-
replaced: string[]
|
|
224
|
-
/** The schema sync's outcome, when the bundle carried schema. */
|
|
225
|
-
schema: { action: string } | undefined
|
|
226
|
-
dictionary: { imported: number; skipped: string[] } | undefined
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
/**
|
|
230
|
-
* Applies the carried sections, structure first.
|
|
231
|
-
*
|
|
232
|
-
* Schema goes to `schema/` and is then synced — the same path a commit takes,
|
|
233
|
-
* so an install cannot produce a database the files do not describe. Views land
|
|
234
|
-
* before content is imported, because a node naming a template whose view is
|
|
235
|
-
* absent arrives without one.
|
|
236
|
-
*/
|
|
237
|
-
export async function applySections(
|
|
238
|
-
db: Db,
|
|
239
|
-
config: CromboloConfig,
|
|
240
|
-
carried: CarriedSections,
|
|
241
|
-
options: SectionOptions & { backupDir?: string } = {},
|
|
242
|
-
): Promise<SectionResult> {
|
|
243
|
-
const written: string[] = []
|
|
244
|
-
const replaced: string[] = []
|
|
245
|
-
const records: FileRecord[] = []
|
|
246
|
-
|
|
247
|
-
/**
|
|
248
|
-
* Writes one file, keeping what was there.
|
|
249
|
-
*
|
|
250
|
-
* The original is copied under its *bundle* path rather than its path on this
|
|
251
|
-
* site, so a revert re-derives the destination through the same mapping — and
|
|
252
|
-
* restores to where the file would go now, not where it went then.
|
|
253
|
-
*/
|
|
254
|
-
const put = (target: string, content: string, bundlePath: string) => {
|
|
255
|
-
const action = actionFor(target, content)
|
|
256
|
-
if (action === 'overwrite' && options.backupDir) {
|
|
257
|
-
const keep = join(options.backupDir, REPLACED_DIR, bundlePath)
|
|
258
|
-
mkdirSync(dirname(keep), { recursive: true })
|
|
259
|
-
copyFileSync(target, keep)
|
|
260
|
-
replaced.push(target)
|
|
261
|
-
}
|
|
262
|
-
records.push({ path: bundlePath, action })
|
|
263
|
-
mkdirSync(dirname(target), { recursive: true })
|
|
264
|
-
writeFileSync(target, content)
|
|
265
|
-
written.push(target)
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
const schemaPaths = options.withoutSchema ? [] : (carried.carries.schema ?? [])
|
|
269
|
-
for (const path of schemaPaths)
|
|
270
|
-
put(join(config.schemaDir, withinSection(path, 'schema')), read(carried, path), path)
|
|
271
|
-
|
|
272
|
-
let schema: { action: string } | undefined
|
|
273
|
-
if (schemaPaths.length > 0) {
|
|
274
|
-
const report = await syncSchemaDirectory(db, config, {
|
|
275
|
-
nodeId: config.nodeId,
|
|
276
|
-
revision: config.schemaRevision,
|
|
277
|
-
})
|
|
278
|
-
schema = { action: report.action }
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
if (!options.withoutFiles) {
|
|
282
|
-
for (const section of FILE_SECTIONS) {
|
|
283
|
-
const destination = destinationOf(config, section)
|
|
284
|
-
if (!destination) continue
|
|
285
|
-
for (const path of carried.carries[section] ?? [])
|
|
286
|
-
put(join(destination, withinSection(path, section)), read(carried, path), path)
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
let dictionary: SectionResult['dictionary']
|
|
291
|
-
for (const path of carried.carries.dictionary ?? []) {
|
|
292
|
-
const outcome = await importDictionaryUdt(db, read(carried, path))
|
|
293
|
-
dictionary = {
|
|
294
|
-
imported: (dictionary?.imported ?? 0) + outcome.imported,
|
|
295
|
-
skipped: [...(dictionary?.skipped ?? []), ...outcome.skipped],
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
// Written last: a record that exists means the files beside it are complete.
|
|
300
|
-
if (options.backupDir && records.length > 0) {
|
|
301
|
-
mkdirSync(options.backupDir, { recursive: true })
|
|
302
|
-
writeFileSync(join(options.backupDir, RECORD_FILE), `${JSON.stringify(records, null, 2)}\n`)
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
return { written, replaced, schema, dictionary }
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
/** Whether a bundle carries anything beyond its content. */
|
|
309
|
-
export const carriesSections = (carried: CarriedSections): boolean =>
|
|
310
|
-
Object.keys(carried.carries).length > 0
|
|
311
|
-
|
|
312
|
-
/**
|
|
313
|
-
* The files an install would replace, and so may not touch without being told
|
|
314
|
-
* it may.
|
|
315
|
-
*
|
|
316
|
-
* Its own function rather than a condition inside the importer, because this is
|
|
317
|
-
* the rule that stops an install destroying an editor's work: a stylesheet, a
|
|
318
|
-
* template or a content type changed in the backoffice, on a site that may
|
|
319
|
-
* have no git behind it. `unchanged` is not a replacement, and a declined
|
|
320
|
-
* section never reaches the plan.
|
|
321
|
-
*/
|
|
322
|
-
export function replacementsNeedingPermission(
|
|
323
|
-
plan: SectionPlan,
|
|
324
|
-
options: SectionOptions = {},
|
|
325
|
-
): SectionFile[] {
|
|
326
|
-
if (options.replaceFiles) return []
|
|
327
|
-
return plan.files.filter((file) => file.action === 'overwrite')
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
/** A staging directory for an install's backups, before the run it belongs to exists. */
|
|
331
|
-
export function stageBackups(config: CromboloConfig): string {
|
|
332
|
-
const dir = join(config.siteDir, RUN_BACKUP_DIR, `pending-${Date.now().toString(36)}`)
|
|
333
|
-
mkdirSync(dir, { recursive: true })
|
|
334
|
-
return dir
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
/**
|
|
338
|
-
* Files an install replaced belong to the run that imported its content, and
|
|
339
|
-
* the run id only exists once that import has happened — so the backups are
|
|
340
|
-
* staged first and moved here afterwards.
|
|
341
|
-
*
|
|
342
|
-
* A staging directory that is never promoted is an install whose content was
|
|
343
|
-
* refused after its files had landed. It is left where it is rather than
|
|
344
|
-
* deleted: it holds the only copy of what was replaced.
|
|
345
|
-
*/
|
|
346
|
-
export function promoteBackups(
|
|
347
|
-
config: CromboloConfig,
|
|
348
|
-
staging: string,
|
|
349
|
-
runId: string,
|
|
350
|
-
): string | undefined {
|
|
351
|
-
if (!existsSync(join(staging, RECORD_FILE))) {
|
|
352
|
-
rmSync(staging, { recursive: true, force: true })
|
|
353
|
-
return undefined
|
|
354
|
-
}
|
|
355
|
-
const target = join(config.siteDir, RUN_BACKUP_DIR, runId)
|
|
356
|
-
rmSync(target, { recursive: true, force: true })
|
|
357
|
-
mkdirSync(dirname(target), { recursive: true })
|
|
358
|
-
renameSync(staging, target)
|
|
359
|
-
return target
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
export interface RestoredFiles {
|
|
363
|
-
/** Files put back to what they were before the install. */
|
|
364
|
-
restored: string[]
|
|
365
|
-
/** Files the install created, and which reverting therefore removes. */
|
|
366
|
-
removed: string[]
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
/**
|
|
370
|
-
* Puts back what an install replaced, for a run that is being reverted.
|
|
371
|
-
*
|
|
372
|
-
* Undoing an install is one action because of this: the content comes back
|
|
373
|
-
* through the ledger, and the files come back from here. Nothing depends on the
|
|
374
|
-
* site being a git checkout — these files are editable in the backoffice, so
|
|
375
|
-
* assuming a repository would have been assuming somebody else's workflow.
|
|
376
|
-
*/
|
|
377
|
-
export function restoreSectionFiles(config: CromboloConfig, runId: string): RestoredFiles {
|
|
378
|
-
const dir = join(config.siteDir, RUN_BACKUP_DIR, runId)
|
|
379
|
-
const record = join(dir, RECORD_FILE)
|
|
380
|
-
if (!existsSync(record)) return { restored: [], removed: [] }
|
|
381
|
-
|
|
382
|
-
let records: FileRecord[]
|
|
383
|
-
try {
|
|
384
|
-
records = JSON.parse(readFileSync(record, 'utf8')) as FileRecord[]
|
|
385
|
-
} catch {
|
|
386
|
-
return { restored: [], removed: [] }
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
const restored: string[] = []
|
|
390
|
-
const removed: string[] = []
|
|
391
|
-
for (const entry of records) {
|
|
392
|
-
const section = sectionFor(entry.path)
|
|
393
|
-
if (!section) continue
|
|
394
|
-
const destination = destinationOf(config, section)
|
|
395
|
-
if (!destination) continue
|
|
396
|
-
const target = join(destination, withinSection(entry.path, section))
|
|
397
|
-
|
|
398
|
-
if (entry.action === 'overwrite') {
|
|
399
|
-
const kept = join(dir, REPLACED_DIR, entry.path)
|
|
400
|
-
if (!existsSync(kept)) continue
|
|
401
|
-
mkdirSync(dirname(target), { recursive: true })
|
|
402
|
-
copyFileSync(kept, target)
|
|
403
|
-
restored.push(target)
|
|
404
|
-
continue
|
|
405
|
-
}
|
|
406
|
-
// A file the install created: reverting takes it away again. `unchanged`
|
|
407
|
-
// is left alone — the install did not put it there.
|
|
408
|
-
if (entry.action === 'create' && existsSync(target)) {
|
|
409
|
-
rmSync(target, { force: true })
|
|
410
|
-
removed.push(target)
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
return { restored, removed }
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
/** The section a bundle path belongs to, by its leading directory. */
|
|
417
|
-
function sectionFor(path: string): BundleSection | undefined {
|
|
418
|
-
if (path === `${BUNDLE_SECTIONS.dictionary}.udt`) return 'dictionary'
|
|
419
|
-
const top = path.split('/')[0]
|
|
420
|
-
for (const [section, dir] of Object.entries(BUNDLE_SECTIONS))
|
|
421
|
-
if (top === dir) return section as BundleSection
|
|
422
|
-
return undefined
|
|
423
|
-
}
|
|
File without changes
|
/package/templates/basic/bundle/{nodes → content/nodes}/36955f6e-d110-469e-b280-72096eccd148.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
/package/templates/demo/harbourstone/bundle/{blobs → content/blobs}/6be45535/bottle-oloroso-pier.jpg
RENAMED
|
File without changes
|
/package/templates/demo/harbourstone/bundle/{blobs → content/blobs}/6d8d6398/bottle-the-cove.jpg
RENAMED
|
File without changes
|
/package/templates/demo/harbourstone/bundle/{blobs → content/blobs}/7b4713f8/copper-pot-stills.jpg
RENAMED
|
File without changes
|
/package/templates/demo/harbourstone/bundle/{blobs → content/blobs}/c114b7d8/bottle-neap-tide.jpg
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
/package/templates/starter/bundle/{nodes → content/nodes}/634dd948-cf83-4cad-9b7b-7c938b33da76.json
RENAMED
|
File without changes
|
/package/templates/starter/bundle/{nodes → content/nodes}/64d95359-d91a-4050-b440-3e5124a74999.json
RENAMED
|
File without changes
|
/package/templates/starter/bundle/{nodes → content/nodes}/d2997ebb-36ca-47cb-bbc8-31fefca787c0.json
RENAMED
|
File without changes
|