@acms-subir/core 0.3.4 → 0.3.5
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
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# @acms-subir/core
|
|
2
2
|
|
|
3
|
+
## 0.3.5
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Require an old-site address or content ZIP for Ghost exports with relative asset references. Failed copies retain only absolute source URLs; unavailable sources leave empty references and named failures in the import report instead of broken URLs on the new site. Correct the import warnings to describe these outcomes.
|
|
8
|
+
|
|
3
9
|
## 0.3.4
|
|
4
10
|
|
|
5
11
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -98,7 +98,7 @@ function ImportTab({ onFinished, openHistory }: { onFinished: () => void; openHi
|
|
|
98
98
|
const key = planKeyFor(options);
|
|
99
99
|
const stale = !!plan && planKey !== key;
|
|
100
100
|
const assetFiles = bundle ? bundle.assets.filter((a) => a.kind !== 'media').length : 0;
|
|
101
|
-
const needsFiles = isGhost &&
|
|
101
|
+
const needsFiles = isGhost && !!bundle?.assets.some((a) => !/^https?:\/\//i.test(a.url)) && !siteUrl.trim() && !contentZip && !source?.zip;
|
|
102
102
|
const resumable = source ? readJournal(source.hash) : null;
|
|
103
103
|
const toImport = plan ? plan.counts.posts.create + plan.counts.posts.update + plan.counts.pages.create + plan.counts.pages.update : 0;
|
|
104
104
|
|
|
@@ -198,7 +198,7 @@ function ImportTab({ onFinished, openHistory }: { onFinished: () => void; openHi
|
|
|
198
198
|
<span className="btn sm">{contentZip ? 'Change ZIP' : 'Add content folder ZIP'}</span>
|
|
199
199
|
<span className="hint">{contentZip ? `${contentZip.name} · ${plural(contentZip.zip.names.length, 'file')}` : 'Optional. Used first; the address is the fallback.'}</span>
|
|
200
200
|
</label>
|
|
201
|
-
{needsFiles && <p className="imp-mark warn">
|
|
201
|
+
{needsFiles && <p className="imp-mark warn">Enter the old site’s address or add its content folder ZIP before importing. Missing files without a source URL are left empty and listed in the report.</p>}
|
|
202
202
|
</div>
|
|
203
203
|
)}
|
|
204
204
|
|
|
@@ -252,7 +252,7 @@ function ImportTab({ onFinished, openHistory }: { onFinished: () => void; openHi
|
|
|
252
252
|
<div className="imp-actions">
|
|
253
253
|
<button type="button" className="btn" disabled={!!busy} onClick={check}>{busy === 'plan' ? 'Checking…' : plan ? 'Check again' : 'Check import'}</button>
|
|
254
254
|
{plan && (
|
|
255
|
-
<button type="button" className="btn primary" disabled={!!busy || stale || toImport === 0} onClick={run} title={stale ? 'Options changed: check again first' : ''}>
|
|
255
|
+
<button type="button" className="btn primary" disabled={!!busy || stale || needsFiles || toImport === 0} onClick={run} title={stale ? 'Options changed: check again first' : ''}>
|
|
256
256
|
{busy === 'run' ? 'Importing…' : resumable?.runId ? `Resume import` : `Import ${plural(toImport, 'item')}`}
|
|
257
257
|
</button>
|
|
258
258
|
)}
|
|
@@ -341,7 +341,7 @@ function Done({ report, reset, openHistory }: { report: Report; reset: () => voi
|
|
|
341
341
|
)}
|
|
342
342
|
{!!report.failedAssets?.length && (
|
|
343
343
|
<>
|
|
344
|
-
<p className="imp-mark warn">{plural(report.failedAssets.length, 'file')} couldn’t be copied
|
|
344
|
+
<p className="imp-mark warn">{plural(report.failedAssets.length, 'file')} couldn’t be copied. Source URLs are kept when available; otherwise the references are left empty.</p>
|
|
345
345
|
<ul className="imp-list">{report.failedAssets.map((f, i) => <li key={i}>{f.url}<small>{f.error}</small></li>)}</ul>
|
|
346
346
|
</>
|
|
347
347
|
)}
|
|
@@ -101,8 +101,14 @@ export function findInZip(zip: ZipIndex, path: string): string | null {
|
|
|
101
101
|
}
|
|
102
102
|
|
|
103
103
|
export function sourceAssetUrl(bundle: ImportBundle, asset: ImportAsset): string {
|
|
104
|
-
|
|
105
|
-
|
|
104
|
+
// A ZIP path is not a URL on the destination site. Only an absolute
|
|
105
|
+
// source URL is safe to retain when copying fails.
|
|
106
|
+
try {
|
|
107
|
+
const url = /^[a-z][a-z\d+.-]*:/i.test(asset.url)
|
|
108
|
+
? new URL(asset.url)
|
|
109
|
+
: new URL(asset.url.replace(/^\/+/, ''), `${bundle.source.siteUrl.replace(/\/+$/, '')}/`);
|
|
110
|
+
return ['http:', 'https:'].includes(url.protocol) ? url.href : '';
|
|
111
|
+
} catch { return ''; }
|
|
106
112
|
}
|
|
107
113
|
|
|
108
114
|
async function fetchAsset(bundle: ImportBundle, asset: ImportAsset, contentZip: ZipIndex | null): Promise<File> {
|
|
@@ -170,6 +176,10 @@ export async function runImport(
|
|
|
170
176
|
const journal: Journal = readJournal(hash) ?? { runId: '', assets: {}, failed: {}, done: [], authorIds: {} };
|
|
171
177
|
const save = () => writeJournal(hash, journal);
|
|
172
178
|
|
|
179
|
+
if (source.kind === 'ghost' && !files && bundle.assets.some((a) => !journal.assets[a.ref] && !sourceAssetUrl(bundle, a))) {
|
|
180
|
+
throw new Error('This Ghost export references images or files. Enter the old site’s address or add its content folder ZIP before importing.');
|
|
181
|
+
}
|
|
182
|
+
|
|
173
183
|
// 1. files → media library
|
|
174
184
|
const assets = bundle.assets.filter((a) => a.kind !== 'media');
|
|
175
185
|
const total = assets.length + bundle.media.length;
|
|
@@ -214,6 +224,13 @@ export async function runImport(
|
|
|
214
224
|
|
|
215
225
|
const resolved = new Map(Object.entries(journal.assets).map(([ref, r]) => [ref, r]));
|
|
216
226
|
const fallback = new Map(bundle.assets.map((a) => [a.ref, sourceAssetUrl(bundle, a)]));
|
|
227
|
+
// Video/audio are not copied yet, but missing source URLs still need to
|
|
228
|
+
// be reported instead of quietly turning into destination-relative URLs.
|
|
229
|
+
for (const a of bundle.assets.filter((a) => a.kind === 'media')) {
|
|
230
|
+
if (!resolved.has(a.ref) && !fallback.get(a.ref)) {
|
|
231
|
+
journal.failed[a.ref] = 'Video/audio cannot be copied yet. Enter the old site’s address to keep its source URL.';
|
|
232
|
+
} else delete journal.failed[a.ref];
|
|
233
|
+
}
|
|
217
234
|
const fix = (v: string) => resolveAssets(v, resolved, fallback);
|
|
218
235
|
|
|
219
236
|
// 2. run + authors + tags; only people with a byline become authors
|
|
@@ -261,7 +278,7 @@ export async function runImport(
|
|
|
261
278
|
const identity = Object.fromEntries(Object.entries(bundle.identity).map(([k, v]) => [k, typeof v === 'string' ? fix(v) : v]));
|
|
262
279
|
const failedAssets = Object.entries(journal.failed).map(([ref, error]) => {
|
|
263
280
|
const a = bundle.assets.find((x) => x.ref === ref);
|
|
264
|
-
return { url: a ? sourceAssetUrl(bundle, a) : ref.replace(/^m-/, 'media '), error };
|
|
281
|
+
return { url: a ? sourceAssetUrl(bundle, a) || a.url : ref.replace(/^m-/, 'media '), error };
|
|
265
282
|
});
|
|
266
283
|
const { report } = await api<{ report: Record<string, unknown> }>({
|
|
267
284
|
op: 'finish', runId: journal.runId, options, identity,
|
package/src/lib/import/bundle.ts
CHANGED
|
@@ -146,7 +146,8 @@ export const assetPlaceholder = (ref: string) => `${ASSET_SCHEME}${ref}`;
|
|
|
146
146
|
// Swap every acms-asset:<ref> for its re-hosted URL. <img> tags built by the
|
|
147
147
|
// importers carry only src/alt/width/height, so images that gained responsive
|
|
148
148
|
// variants get srcset/sizes added here. Unresolved refs (a failed download)
|
|
149
|
-
// fall back to
|
|
149
|
+
// fall back to an absolute source URL, or an empty string if none is known.
|
|
150
|
+
// Copy failures are reported separately by the runner.
|
|
150
151
|
export interface ResolvedAsset { url: string; srcset?: string; sizes?: string }
|
|
151
152
|
const PLACEHOLDER_RE = /acms-asset:([a-z0-9-]{1,40})/g;
|
|
152
153
|
export function resolveAssets(value: string, resolved: Map<string, ResolvedAsset>, fallback: Map<string, string>): string {
|