@adobe/aem-cli 16.17.1 → 16.18.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/CHANGELOG.md +7 -0
- package/README.md +1 -1
- package/package.json +6 -1
- package/src/cli.js +3 -0
- package/src/content/add.cmd.js +133 -0
- package/src/content/add.js +47 -0
- package/src/content/clone.cmd.js +214 -0
- package/src/content/clone.js +74 -0
- package/src/content/commit.cmd.js +57 -0
- package/src/content/commit.js +40 -0
- package/src/content/content-git.js +181 -0
- package/src/content/content-metadata-html.js +321 -0
- package/src/content/content-shared.js +44 -0
- package/src/content/content.js +38 -0
- package/src/content/da-api.js +198 -0
- package/src/content/da-auth.js +209 -0
- package/src/content/diff.cmd.js +123 -0
- package/src/content/diff.js +45 -0
- package/src/content/merge.cmd.js +150 -0
- package/src/content/merge.js +45 -0
- package/src/content/push.cmd.js +291 -0
- package/src/content/push.js +58 -0
- package/src/content/status.cmd.js +94 -0
- package/src/content/status.js +33 -0
- package/src/server/BaseServer.js +9 -1
- package/src/server/HelixProject.js +19 -0
- package/src/server/HelixServer.js +82 -0
- package/src/server/MetadataSheetSupport.js +221 -0
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2026 Adobe. All rights reserved.
|
|
3
|
+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
* you may not use this file except in compliance with the License. You may obtain a copy
|
|
5
|
+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
|
|
6
|
+
*
|
|
7
|
+
* Unless required by applicable law or agreed to in writing, software distributed under
|
|
8
|
+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
|
|
9
|
+
* OF ANY KIND, either express or implied. See the License for the specific language
|
|
10
|
+
* governing permissions and limitations under the License.
|
|
11
|
+
*/
|
|
12
|
+
import path from 'path';
|
|
13
|
+
import fse from 'fs-extra';
|
|
14
|
+
import git from 'isomorphic-git';
|
|
15
|
+
|
|
16
|
+
/** Ref pointing at the last commit whose tree was fully synced to da.live. */
|
|
17
|
+
export const DA_SYNCED_REF = 'refs/da/synced';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* OID of the last da.live sync. Uses refs/da/synced when present; otherwise infers
|
|
21
|
+
* from commit history (newest `push:` then `clone:`) so repos created before the ref
|
|
22
|
+
* existed still compare against the correct baseline.
|
|
23
|
+
* @param {import('isomorphic-git').FsClient} fs
|
|
24
|
+
* @param {string} dir
|
|
25
|
+
* @returns {Promise<string>}
|
|
26
|
+
*/
|
|
27
|
+
export async function resolveSyncedOid(fs, dir) {
|
|
28
|
+
try {
|
|
29
|
+
return await git.resolveRef({ fs, dir, ref: DA_SYNCED_REF });
|
|
30
|
+
} catch (err) {
|
|
31
|
+
if (err && (err.code === 'NotFoundError' || err.name === 'NotFoundError')) {
|
|
32
|
+
const commits = await git.log({ fs, dir, depth: 500 });
|
|
33
|
+
for (const c of commits) {
|
|
34
|
+
if (String(c.commit.message).startsWith('push:')) {
|
|
35
|
+
return c.oid;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
for (const c of commits) {
|
|
39
|
+
if (String(c.commit.message).startsWith('clone:')) {
|
|
40
|
+
return c.oid;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return git.resolveRef({ fs, dir, ref: 'HEAD' });
|
|
44
|
+
}
|
|
45
|
+
throw err;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* @param {import('isomorphic-git').FsClient} fs
|
|
51
|
+
* @param {string} dir
|
|
52
|
+
* @param {string} oid
|
|
53
|
+
* @returns {Promise<void>}
|
|
54
|
+
*/
|
|
55
|
+
export async function writeSyncedRef(fs, dir, oid) {
|
|
56
|
+
await git.writeRef({
|
|
57
|
+
fs,
|
|
58
|
+
dir,
|
|
59
|
+
ref: DA_SYNCED_REF,
|
|
60
|
+
value: oid,
|
|
61
|
+
force: true,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* True when the index or working tree differs from HEAD (uncommitted work).
|
|
67
|
+
* @param {Array<[string, number, number, number]>} matrix
|
|
68
|
+
* @returns {boolean}
|
|
69
|
+
*/
|
|
70
|
+
export function statusMatrixHasUncommitted(matrix) {
|
|
71
|
+
return matrix.some(([, h, w, s]) => !(h === 1 && w === 1 && s === 1));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Diff trees at two commits: paths as da.live paths (`/file`).
|
|
76
|
+
* @param {import('isomorphic-git').FsClient} fs
|
|
77
|
+
* @param {string} dir
|
|
78
|
+
* @param {string} baseOid
|
|
79
|
+
* @param {string} headOid
|
|
80
|
+
* @returns {Promise<{ added: string[], modified: string[], deleted: string[] }>}
|
|
81
|
+
*/
|
|
82
|
+
export async function diffCommitTrees(fs, dir, baseOid, headOid) {
|
|
83
|
+
if (baseOid === headOid) {
|
|
84
|
+
return { added: [], modified: [], deleted: [] };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const baseFiles = new Set(await git.listFiles({ fs, dir, ref: baseOid }));
|
|
88
|
+
const headFiles = new Set(await git.listFiles({ fs, dir, ref: headOid }));
|
|
89
|
+
|
|
90
|
+
const added = [];
|
|
91
|
+
const modified = [];
|
|
92
|
+
const deleted = [];
|
|
93
|
+
|
|
94
|
+
for (const f of headFiles) {
|
|
95
|
+
if (!baseFiles.has(f)) {
|
|
96
|
+
added.push(`/${f}`);
|
|
97
|
+
} else {
|
|
98
|
+
// eslint-disable-next-line no-await-in-loop
|
|
99
|
+
const b1 = await git.readBlob({
|
|
100
|
+
fs,
|
|
101
|
+
dir,
|
|
102
|
+
oid: baseOid,
|
|
103
|
+
filepath: f,
|
|
104
|
+
});
|
|
105
|
+
// eslint-disable-next-line no-await-in-loop
|
|
106
|
+
const b2 = await git.readBlob({
|
|
107
|
+
fs,
|
|
108
|
+
dir,
|
|
109
|
+
oid: headOid,
|
|
110
|
+
filepath: f,
|
|
111
|
+
});
|
|
112
|
+
if (b1.oid !== b2.oid) {
|
|
113
|
+
modified.push(`/${f}`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
for (const f of baseFiles) {
|
|
119
|
+
if (!headFiles.has(f)) {
|
|
120
|
+
deleted.push(`/${f}`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return { added, modified, deleted };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Number of commits reachable from `tipOid` before hitting `ancestorOid` (exclusive).
|
|
129
|
+
* @param {import('isomorphic-git').FsClient} fs
|
|
130
|
+
* @param {string} dir
|
|
131
|
+
* @param {string} tipOid
|
|
132
|
+
* @param {string} ancestorOid
|
|
133
|
+
* @returns {Promise<number>}
|
|
134
|
+
*/
|
|
135
|
+
export async function countCommitsAhead(fs, dir, tipOid, ancestorOid) {
|
|
136
|
+
if (tipOid === ancestorOid) {
|
|
137
|
+
return 0;
|
|
138
|
+
}
|
|
139
|
+
const commits = await git.log({
|
|
140
|
+
fs,
|
|
141
|
+
dir,
|
|
142
|
+
ref: tipOid,
|
|
143
|
+
depth: 5000,
|
|
144
|
+
});
|
|
145
|
+
let n = 0;
|
|
146
|
+
for (const c of commits) {
|
|
147
|
+
if (c.oid === ancestorOid) {
|
|
148
|
+
return n;
|
|
149
|
+
}
|
|
150
|
+
n += 1;
|
|
151
|
+
}
|
|
152
|
+
return n;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Committer time in ms for conflict checks (da.live lastModified).
|
|
157
|
+
* @param {import('isomorphic-git').FsClient} fs
|
|
158
|
+
* @param {string} dir
|
|
159
|
+
* @param {string} commitOid
|
|
160
|
+
* @returns {Promise<number>}
|
|
161
|
+
*/
|
|
162
|
+
export async function getCommitCommitterTimeMs(fs, dir, commitOid) {
|
|
163
|
+
const { commit } = await git.readCommit({ fs, dir, oid: commitOid });
|
|
164
|
+
return commit.committer.timestamp * 1000;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Ensures an entry is present in the project .gitignore, creating the file if needed.
|
|
169
|
+
* @param {string} projectDir
|
|
170
|
+
* @param {string} entry
|
|
171
|
+
*/
|
|
172
|
+
export async function ensureGitIgnored(projectDir, entry) {
|
|
173
|
+
const gitIgnorePath = path.resolve(projectDir, '.gitignore');
|
|
174
|
+
let content = '';
|
|
175
|
+
if (await fse.pathExists(gitIgnorePath)) {
|
|
176
|
+
content = await fse.readFile(gitIgnorePath, 'utf-8');
|
|
177
|
+
}
|
|
178
|
+
if (!content.split('\n').map((l) => l.trim()).includes(entry)) {
|
|
179
|
+
await fse.appendFile(gitIgnorePath, `\n${entry}\n`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2026 Adobe. All rights reserved.
|
|
3
|
+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
* you may not use this file except in compliance with the License. You may obtain a copy
|
|
5
|
+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
|
|
6
|
+
*
|
|
7
|
+
* Unless required by applicable law or agreed to in writing, software distributed under
|
|
8
|
+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
|
|
9
|
+
* OF ANY KIND, either express or implied. See the License for the specific language
|
|
10
|
+
* governing permissions and limitations under the License.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Transforms da.live-style `<div class="metadata">` blocks in content HTML into
|
|
15
|
+
* `<meta>` tags for local dev (`aem up`), strips the block from the body, and
|
|
16
|
+
* adds description / Open Graph / Twitter tags where data is available.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { unified } from 'unified';
|
|
20
|
+
import rehypeParse from 'rehype-parse';
|
|
21
|
+
import { select } from 'hast-util-select';
|
|
22
|
+
import { toHtml } from 'hast-util-to-html';
|
|
23
|
+
|
|
24
|
+
/** @typedef {import('hast').Root} HastRoot */
|
|
25
|
+
/** @typedef {import('hast').Element} HastElement */
|
|
26
|
+
|
|
27
|
+
const REHYPE_PARSE = { fragment: true };
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @param {string} s
|
|
31
|
+
* @returns {string}
|
|
32
|
+
*/
|
|
33
|
+
export function escapeHtmlAttr(s) {
|
|
34
|
+
return String(s)
|
|
35
|
+
.replace(/&/g, '&')
|
|
36
|
+
.replace(/"/g, '"')
|
|
37
|
+
.replace(/</g, '<');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* @param {string} label
|
|
42
|
+
* @returns {string}
|
|
43
|
+
*/
|
|
44
|
+
export function slugifyMetadataLabel(label) {
|
|
45
|
+
return label
|
|
46
|
+
.normalize('NFKD')
|
|
47
|
+
.replace(/[\u0300-\u036f]/g, '')
|
|
48
|
+
.trim()
|
|
49
|
+
.toLowerCase()
|
|
50
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
51
|
+
.replace(/^-|-$/g, '');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* @param {import('hast').Node | null | undefined} node
|
|
56
|
+
* @returns {string}
|
|
57
|
+
*/
|
|
58
|
+
function textContent(node) {
|
|
59
|
+
if (!node) {
|
|
60
|
+
return '';
|
|
61
|
+
}
|
|
62
|
+
if (node.type === 'text') {
|
|
63
|
+
return node.value;
|
|
64
|
+
}
|
|
65
|
+
if (Array.isArray(node.children)) {
|
|
66
|
+
return node.children.map((c) => textContent(c)).join('');
|
|
67
|
+
}
|
|
68
|
+
return '';
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* @param {HastElement} metadataRoot
|
|
73
|
+
* @returns {Array<[string, string]>}
|
|
74
|
+
*/
|
|
75
|
+
export function extractMetadataPairs(metadataRoot) {
|
|
76
|
+
/** @type {Array<[string, string]>} */
|
|
77
|
+
const pairs = [];
|
|
78
|
+
if (!metadataRoot.children) {
|
|
79
|
+
return pairs;
|
|
80
|
+
}
|
|
81
|
+
for (const row of metadataRoot.children) {
|
|
82
|
+
if (row.type !== 'element' || row.tagName !== 'div') {
|
|
83
|
+
// eslint-disable-next-line no-continue
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
const cells = (row.children || []).filter(
|
|
87
|
+
(c) => c.type === 'element' && c.tagName === 'div',
|
|
88
|
+
);
|
|
89
|
+
if (cells.length < 2) {
|
|
90
|
+
// eslint-disable-next-line no-continue
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
const label = textContent(cells[0]).trim();
|
|
94
|
+
const value = textContent(cells[1]).trim();
|
|
95
|
+
if (label) {
|
|
96
|
+
pairs.push([label, value]);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return pairs;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* @param {HastElement} node
|
|
104
|
+
* @param {string} className
|
|
105
|
+
* @returns {boolean}
|
|
106
|
+
*/
|
|
107
|
+
function hasClass(node, className) {
|
|
108
|
+
const cn = node.properties?.className;
|
|
109
|
+
if (Array.isArray(cn)) {
|
|
110
|
+
return cn.includes(className);
|
|
111
|
+
}
|
|
112
|
+
if (typeof cn === 'string') {
|
|
113
|
+
return cn.split(/\s+/).includes(className);
|
|
114
|
+
}
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* @param {import('hast').Node} tree
|
|
120
|
+
* @param {import('hast').Element} target
|
|
121
|
+
* @returns {boolean}
|
|
122
|
+
*/
|
|
123
|
+
function removeNode(tree, target) {
|
|
124
|
+
if (tree === target) {
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
if ('children' in tree && Array.isArray(tree.children)) {
|
|
128
|
+
const { children } = tree;
|
|
129
|
+
for (let i = 0; i < children.length; i += 1) {
|
|
130
|
+
const c = children[i];
|
|
131
|
+
if (c === target) {
|
|
132
|
+
children.splice(i, 1);
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
if (removeNode(c, target)) {
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* @param {HastRoot} tree
|
|
145
|
+
* @returns {string}
|
|
146
|
+
*/
|
|
147
|
+
function firstImgSrc(tree) {
|
|
148
|
+
const img = select('img[src]', tree);
|
|
149
|
+
if (!img || img.type !== 'element') {
|
|
150
|
+
return '';
|
|
151
|
+
}
|
|
152
|
+
const s = img.properties?.src;
|
|
153
|
+
return typeof s === 'string' ? s : '';
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* @param {HastRoot} tree
|
|
158
|
+
* @returns {string}
|
|
159
|
+
*/
|
|
160
|
+
function firstParagraphText(tree) {
|
|
161
|
+
const p = select('p', tree);
|
|
162
|
+
return p && p.type === 'element' ? textContent(p).trim() : '';
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Truncates a string to `max` characters, trimming and appending an ellipsis when needed.
|
|
167
|
+
* @param {string} s
|
|
168
|
+
* @param {number} max
|
|
169
|
+
* @returns {string}
|
|
170
|
+
*/
|
|
171
|
+
function truncateWithEllipsis(s, max) {
|
|
172
|
+
if (s.length <= max) {
|
|
173
|
+
return s;
|
|
174
|
+
}
|
|
175
|
+
return `${s.slice(0, max - 1).trim()}…`;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const SEO_LABEL_SKIP = new Set(['title', 'description', 'image']);
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* @param {object | null | undefined} sheetRow row from /metadata.json matched for this URL
|
|
182
|
+
* @param {Set<string> | null | undefined} excludeMetaNames lowercase meta `name` values to skip
|
|
183
|
+
* (local page wins)
|
|
184
|
+
* @returns {string[]}
|
|
185
|
+
*/
|
|
186
|
+
export function buildSheetMetaLines(sheetRow, excludeMetaNames) {
|
|
187
|
+
if (!sheetRow || typeof sheetRow !== 'object') {
|
|
188
|
+
return [];
|
|
189
|
+
}
|
|
190
|
+
/** @type {string[]} */
|
|
191
|
+
const lines = [];
|
|
192
|
+
for (const [k, v] of Object.entries(sheetRow)) {
|
|
193
|
+
if (k === 'URL' || k.startsWith(':')) {
|
|
194
|
+
// eslint-disable-next-line no-continue
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
if (excludeMetaNames && excludeMetaNames.has(k.toLowerCase())) {
|
|
198
|
+
// eslint-disable-next-line no-continue
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
if (v === undefined || v === null) {
|
|
202
|
+
// eslint-disable-next-line no-continue
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
const s = String(v).trim();
|
|
206
|
+
if (!s) {
|
|
207
|
+
// eslint-disable-next-line no-continue
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
lines.push(`<meta name="${escapeHtmlAttr(k)}" content="${escapeHtmlAttr(s)}">`);
|
|
211
|
+
}
|
|
212
|
+
return lines;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* @param {string[]} lines
|
|
217
|
+
* @returns {string}
|
|
218
|
+
*/
|
|
219
|
+
function joinLines(lines) {
|
|
220
|
+
return lines.length > 0 ? `\n${lines.join('\n')}\n` : '';
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* @param {string} htmlFragment body-only or partial HTML from content/
|
|
225
|
+
* @param {{ absolutePageUrl?: string, sheetRow?: object | null }} [options]
|
|
226
|
+
* @returns {{ htmlFragment: string, metaTagsHtml: string }}
|
|
227
|
+
*/
|
|
228
|
+
export function transformContentMetadataHtml(htmlFragment, options = {}) {
|
|
229
|
+
const { absolutePageUrl = '', sheetRow = null } = options;
|
|
230
|
+
|
|
231
|
+
let tree;
|
|
232
|
+
try {
|
|
233
|
+
tree = unified().use(rehypeParse, REHYPE_PARSE).parse(htmlFragment);
|
|
234
|
+
} catch {
|
|
235
|
+
return { htmlFragment, metaTagsHtml: joinLines(buildSheetMetaLines(sheetRow)) };
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const metadataEl = select('div.metadata', tree);
|
|
239
|
+
if (!metadataEl || metadataEl.type !== 'element' || !hasClass(metadataEl, 'metadata')) {
|
|
240
|
+
return { htmlFragment, metaTagsHtml: joinLines(buildSheetMetaLines(sheetRow)) };
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const pairs = extractMetadataPairs(metadataEl);
|
|
244
|
+
|
|
245
|
+
/** Names from page metadata; sheet entries with the same meta name are skipped. */
|
|
246
|
+
const localPairMetaNames = new Set();
|
|
247
|
+
for (const [label, value] of pairs) {
|
|
248
|
+
const slug = slugifyMetadataLabel(label);
|
|
249
|
+
if (!slug || value === undefined) {
|
|
250
|
+
// eslint-disable-next-line no-continue
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
localPairMetaNames.add(slug);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const sheetLines = buildSheetMetaLines(sheetRow, localPairMetaNames);
|
|
257
|
+
|
|
258
|
+
const lowerMap = new Map(pairs.map(([k, v]) => [k.toLowerCase().trim(), v]));
|
|
259
|
+
|
|
260
|
+
removeNode(tree, metadataEl);
|
|
261
|
+
|
|
262
|
+
const title = (lowerMap.get('title') || textContent(select('h1', tree)).trim() || '').trim();
|
|
263
|
+
let description = (lowerMap.get('description') || firstParagraphText(tree) || '').trim();
|
|
264
|
+
description = truncateWithEllipsis(description, 200);
|
|
265
|
+
const image = (lowerMap.get('image') || lowerMap.get('og image') || firstImgSrc(tree) || '').trim();
|
|
266
|
+
|
|
267
|
+
/** @type {string[]} */
|
|
268
|
+
const seoLines = [];
|
|
269
|
+
|
|
270
|
+
if (description) {
|
|
271
|
+
const e = escapeHtmlAttr(description);
|
|
272
|
+
seoLines.push(`<meta name="description" content="${e}">`);
|
|
273
|
+
seoLines.push(`<meta property="og:description" content="${e}">`);
|
|
274
|
+
seoLines.push(`<meta name="twitter:description" content="${e}">`);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (title) {
|
|
278
|
+
const e = escapeHtmlAttr(title);
|
|
279
|
+
seoLines.push(`<meta property="og:title" content="${e}">`);
|
|
280
|
+
seoLines.push(`<meta name="twitter:title" content="${e}">`);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (absolutePageUrl) {
|
|
284
|
+
seoLines.push(`<meta property="og:url" content="${escapeHtmlAttr(absolutePageUrl)}">`);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (image) {
|
|
288
|
+
const e = escapeHtmlAttr(image);
|
|
289
|
+
const alt = escapeHtmlAttr(title || 'image');
|
|
290
|
+
seoLines.push(`<meta property="og:image" content="${e}">`);
|
|
291
|
+
seoLines.push(`<meta property="og:image:secure_url" content="${e}">`);
|
|
292
|
+
seoLines.push(`<meta property="og:image:alt" content="${alt}">`);
|
|
293
|
+
seoLines.push('<meta name="twitter:card" content="summary_large_image">');
|
|
294
|
+
seoLines.push(`<meta name="twitter:image" content="${e}">`);
|
|
295
|
+
} else {
|
|
296
|
+
seoLines.push('<meta name="twitter:card" content="summary">');
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/** @type {string[]} */
|
|
300
|
+
const pairLines = [];
|
|
301
|
+
for (const [label, value] of pairs) {
|
|
302
|
+
const key = label.toLowerCase().trim();
|
|
303
|
+
if (SEO_LABEL_SKIP.has(key)) {
|
|
304
|
+
// eslint-disable-next-line no-continue
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
const slug = slugifyMetadataLabel(label);
|
|
308
|
+
if (!slug || value === undefined) {
|
|
309
|
+
// eslint-disable-next-line no-continue
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
pairLines.push(
|
|
313
|
+
`<meta name="${escapeHtmlAttr(slug)}" content="${escapeHtmlAttr(value)}">`,
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// SEO first, then sheet (fields not overridden by local pairs), then local page metadata
|
|
318
|
+
const allLines = [...seoLines, ...sheetLines, ...pairLines];
|
|
319
|
+
const htmlOut = toHtml(tree);
|
|
320
|
+
return { htmlFragment: htmlOut, metaTagsHtml: joinLines(allLines) };
|
|
321
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2026 Adobe. All rights reserved.
|
|
3
|
+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
* you may not use this file except in compliance with the License. You may obtain a copy
|
|
5
|
+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
|
|
6
|
+
*
|
|
7
|
+
* Unless required by applicable law or agreed to in writing, software distributed under
|
|
8
|
+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
|
|
9
|
+
* OF ANY KIND, either express or implied. See the License for the specific language
|
|
10
|
+
* governing permissions and limitations under the License.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export const CONTENT_DIR = 'content';
|
|
14
|
+
export const CONFIG_FILE = '.da-config.json';
|
|
15
|
+
export const GIT_AUTHOR = { name: 'aem-cli', email: 'aem-cli@adobe.com' };
|
|
16
|
+
|
|
17
|
+
/** Above this count, clone warns and requires confirmation (or --yes). */
|
|
18
|
+
export const LARGE_CLONE_FILE_THRESHOLD = 10000;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Parallelism for da.live I/O: recursive list fan-out, clone downloads, push uploads/deletes.
|
|
22
|
+
*/
|
|
23
|
+
export const CONTENT_IO_CONCURRENCY = 10;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Normalizes a da.live path: leading slash, no trailing slash except root.
|
|
27
|
+
* @param {string} input
|
|
28
|
+
* @returns {string}
|
|
29
|
+
* @throws {Error} if input is null, undefined, or empty
|
|
30
|
+
*/
|
|
31
|
+
export function normalizeDaPath(input) {
|
|
32
|
+
if (input === undefined || input === null) {
|
|
33
|
+
throw new Error('Content path is required.');
|
|
34
|
+
}
|
|
35
|
+
let s = String(input).trim();
|
|
36
|
+
if (s === '') {
|
|
37
|
+
throw new Error('Content path cannot be empty.');
|
|
38
|
+
}
|
|
39
|
+
if (!s.startsWith('/')) {
|
|
40
|
+
s = `/${s}`;
|
|
41
|
+
}
|
|
42
|
+
s = s.replace(/\/+$/, '') || '/';
|
|
43
|
+
return s;
|
|
44
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2026 Adobe. All rights reserved.
|
|
3
|
+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
* you may not use this file except in compliance with the License. You may obtain a copy
|
|
5
|
+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
|
|
6
|
+
*
|
|
7
|
+
* Unless required by applicable law or agreed to in writing, software distributed under
|
|
8
|
+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
|
|
9
|
+
* OF ANY KIND, either express or implied. See the License for the specific language
|
|
10
|
+
* governing permissions and limitations under the License.
|
|
11
|
+
*/
|
|
12
|
+
import clone from './clone.js';
|
|
13
|
+
import add from './add.js';
|
|
14
|
+
import commit from './commit.js';
|
|
15
|
+
import push from './push.js';
|
|
16
|
+
import status from './status.js';
|
|
17
|
+
import diff from './diff.js';
|
|
18
|
+
import mergeCmd from './merge.js';
|
|
19
|
+
|
|
20
|
+
export default function content() {
|
|
21
|
+
return {
|
|
22
|
+
command: 'content',
|
|
23
|
+
description: 'Manage AEM content from da.live',
|
|
24
|
+
builder: (yargs) => {
|
|
25
|
+
yargs
|
|
26
|
+
.command(clone())
|
|
27
|
+
.command(add())
|
|
28
|
+
.command(commit())
|
|
29
|
+
.command(push())
|
|
30
|
+
.command(status())
|
|
31
|
+
.command(diff())
|
|
32
|
+
.command(mergeCmd())
|
|
33
|
+
.demandCommand(1, 'You need at least one content subcommand.')
|
|
34
|
+
.help();
|
|
35
|
+
},
|
|
36
|
+
handler: () => {},
|
|
37
|
+
};
|
|
38
|
+
}
|