@acms-subir/core 0.3.1 → 0.3.2
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,18 @@
|
|
|
1
1
|
# @acms-subir/core
|
|
2
2
|
|
|
3
|
+
## 0.3.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 2c349c0: Fix blank admin editors in `astro dev` on sites that install core from npm. Browser-side dependencies (TipTap, prismjs, mammoth and others) are now pre-bundled by Vite, so islands no longer fail with "doesn't provide an export named …".
|
|
8
|
+
|
|
9
|
+
Also fixed:
|
|
10
|
+
|
|
11
|
+
- A first request storm after an upgrade could run a new migration twice and fail with `UNIQUE constraint failed: migrations.version`.
|
|
12
|
+
- File and bookmark cards reloaded as plain links when a post was reopened in the editor.
|
|
13
|
+
- YouTube embeds now keep their start time (`?t=` / `start=`) and accept `youtube.com/live/…` links.
|
|
14
|
+
- `@tiptap/core` is now a declared dependency instead of relying on hoisting.
|
|
15
|
+
|
|
3
16
|
## 0.3.1
|
|
4
17
|
|
|
5
18
|
### Patch Changes
|
package/integration.mjs
CHANGED
|
@@ -88,6 +88,19 @@ const ROUTES = [
|
|
|
88
88
|
['/api/restore', 'api/restore.ts'],
|
|
89
89
|
];
|
|
90
90
|
|
|
91
|
+
// Packages imported by browser-side code (admin islands and the libs they
|
|
92
|
+
// pull in). Keep in sync when a component gains a new dependency; the
|
|
93
|
+
// integration test checks every entry is a declared dependency.
|
|
94
|
+
export const CLIENT_DEPS = [
|
|
95
|
+
'react', 'react-dom', 'react-dom/client',
|
|
96
|
+
'@tiptap/core', '@tiptap/react', '@tiptap/react/menus', '@tiptap/pm/state', '@tiptap/pm/view', '@tiptap/pm/model',
|
|
97
|
+
'@tiptap/starter-kit', '@tiptap/extensions', '@tiptap/extension-table', '@tiptap/extension-highlight',
|
|
98
|
+
'@tiptap/extension-code-block-lowlight', '@tiptap/suggestion',
|
|
99
|
+
'@dnd-kit/core', '@dnd-kit/sortable', '@dnd-kit/utilities',
|
|
100
|
+
'prismjs', 'simple-icons', 'lowlight', 'hast-util-to-html', 'marked', 'turndown', 'zod',
|
|
101
|
+
'mammoth/mammoth.browser.js',
|
|
102
|
+
];
|
|
103
|
+
|
|
91
104
|
// First segments of the fixed routes above — a configured base landing on one
|
|
92
105
|
// of these would shadow a real page, so it's a build-time error.
|
|
93
106
|
const RESERVED_BASES = new Set([
|
|
@@ -515,6 +528,25 @@ export default function acms(options = {}) {
|
|
|
515
528
|
}
|
|
516
529
|
updateConfig({ vite: { plugins: [acmsVitePlugin(userConfig)] } });
|
|
517
530
|
|
|
531
|
+
// Installed from npm, core's admin islands are source files inside
|
|
532
|
+
// node_modules, so Vite's dev scanner never crawls them: their
|
|
533
|
+
// dependencies are found only at runtime and CommonJS ones
|
|
534
|
+
// (use-sync-external-store via @tiptap/react, prismjs, mammoth) are
|
|
535
|
+
// served raw, which breaks hydration ("doesn't provide an export
|
|
536
|
+
// named …"), and late discoveries force a mid-load reload. Pre-bundle
|
|
537
|
+
// the whole client graph up front. The `core > dep` form resolves
|
|
538
|
+
// each package from core's own location, so it also works when the
|
|
539
|
+
// package manager doesn't hoist them. Build output is unaffected.
|
|
540
|
+
if (command === 'dev') {
|
|
541
|
+
updateConfig({
|
|
542
|
+
vite: {
|
|
543
|
+
optimizeDeps: {
|
|
544
|
+
include: CLIENT_DEPS.map((dep) => `@acms-subir/core > ${dep}`),
|
|
545
|
+
},
|
|
546
|
+
},
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
|
|
518
550
|
// "Project-local routes win" is enforced here: Astro registers
|
|
519
551
|
// injected and file-based routes at equal priority (and the injected
|
|
520
552
|
// one is served on a collision), so a route is only injected when the
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@acms-subir/core",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
@@ -59,6 +59,7 @@
|
|
|
59
59
|
"@dnd-kit/sortable": "^10.0.0",
|
|
60
60
|
"@dnd-kit/utilities": "^3.2.2",
|
|
61
61
|
"@libsql/client": "^0.15.0",
|
|
62
|
+
"@tiptap/core": "^3.30.1",
|
|
62
63
|
"@tiptap/extension-code-block-lowlight": "^3.30.1",
|
|
63
64
|
"@tiptap/extension-highlight": "3.30.1",
|
|
64
65
|
"@tiptap/extension-table": "^3.30.1",
|
|
@@ -3,6 +3,9 @@ import { NodeViewWrapper, NodeViewContent, ReactNodeViewRenderer } from '@tiptap
|
|
|
3
3
|
import type { NodeViewProps } from '@tiptap/react';
|
|
4
4
|
import { useState } from 'react';
|
|
5
5
|
import { cleanEventName } from '../../../lib/analytics';
|
|
6
|
+
import { detectEmbed } from '../../../lib/embeds';
|
|
7
|
+
|
|
8
|
+
export { detectEmbed, type EmbedInfo } from '../../../lib/embeds';
|
|
6
9
|
|
|
7
10
|
// Card nodes shared by the post editor. renderHTML emits the public markup
|
|
8
11
|
// (`.pc-*` classes, styled in src/styles/cards.css); parseHTML round-trips it
|
|
@@ -129,44 +132,6 @@ function ImageCardView({ node, updateAttributes, deleteNode, selected }: NodeVie
|
|
|
129
132
|
|
|
130
133
|
// ---- embed ----------------------------------------------------------------
|
|
131
134
|
|
|
132
|
-
export interface EmbedInfo {
|
|
133
|
-
provider: string;
|
|
134
|
-
embedSrc: string;
|
|
135
|
-
aspect: boolean; // true → 16:9, false → fixed height
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
export function detectEmbed(raw: string): EmbedInfo | null {
|
|
139
|
-
let url: URL;
|
|
140
|
-
try {
|
|
141
|
-
url = new URL(raw.trim());
|
|
142
|
-
} catch {
|
|
143
|
-
return null;
|
|
144
|
-
}
|
|
145
|
-
const host = url.hostname.replace(/^www\./, '');
|
|
146
|
-
if (host === 'youtube.com' || host === 'm.youtube.com' || host === 'youtu.be' || host === 'youtube-nocookie.com') {
|
|
147
|
-
const id =
|
|
148
|
-
host === 'youtu.be'
|
|
149
|
-
? url.pathname.slice(1).split('/')[0]
|
|
150
|
-
: url.pathname.startsWith('/shorts/') || url.pathname.startsWith('/embed/')
|
|
151
|
-
? url.pathname.split('/')[2]
|
|
152
|
-
: url.searchParams.get('v');
|
|
153
|
-
if (id) return { provider: 'youtube', embedSrc: `https://www.youtube-nocookie.com/embed/${id}`, aspect: true };
|
|
154
|
-
}
|
|
155
|
-
if (host === 'vimeo.com' || host === 'player.vimeo.com') {
|
|
156
|
-
const id = url.pathname.split('/').filter(Boolean).pop();
|
|
157
|
-
if (id && /^\d+$/.test(id)) return { provider: 'vimeo', embedSrc: `https://player.vimeo.com/video/${id}`, aspect: true };
|
|
158
|
-
}
|
|
159
|
-
if (host === 'loom.com' && url.pathname.startsWith('/share/')) {
|
|
160
|
-
return { provider: 'loom', embedSrc: `https://www.loom.com/embed/${url.pathname.split('/')[2]}`, aspect: true };
|
|
161
|
-
}
|
|
162
|
-
if (host === 'codepen.io' && url.pathname.includes('/pen/')) {
|
|
163
|
-
return { provider: 'codepen', embedSrc: raw.replace('/pen/', '/embed/'), aspect: false };
|
|
164
|
-
}
|
|
165
|
-
if (url.protocol === 'https:') {
|
|
166
|
-
return { provider: 'iframe', embedSrc: raw, aspect: false };
|
|
167
|
-
}
|
|
168
|
-
return null;
|
|
169
|
-
}
|
|
170
135
|
|
|
171
136
|
export const EmbedCard = Node.create({
|
|
172
137
|
name: 'embedCard',
|
|
@@ -40,6 +40,8 @@ export const FileCard = Node.create({
|
|
|
40
40
|
return [
|
|
41
41
|
{
|
|
42
42
|
tag: 'a.pc-filecard',
|
|
43
|
+
// outrank the Link mark's a[href] rule, or the card reloads as a plain link
|
|
44
|
+
priority: 60,
|
|
43
45
|
getAttrs: (el) => {
|
|
44
46
|
const e = el as HTMLElement;
|
|
45
47
|
return {
|
|
@@ -288,6 +290,8 @@ export const BookmarkCard = Node.create({
|
|
|
288
290
|
return [
|
|
289
291
|
{
|
|
290
292
|
tag: 'a.pc-bookmark',
|
|
293
|
+
// outrank the Link mark's a[href] rule, or the card reloads as a plain link
|
|
294
|
+
priority: 60,
|
|
291
295
|
getAttrs: (el) => {
|
|
292
296
|
const e = el as HTMLElement;
|
|
293
297
|
return {
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// Embeddable-URL detection shared by the editor's embed card (paste →
|
|
2
|
+
// card) and the importers (a Ghost/WordPress embed → embedCard markup).
|
|
3
|
+
export interface EmbedInfo {
|
|
4
|
+
provider: string;
|
|
5
|
+
embedSrc: string;
|
|
6
|
+
aspect: boolean; // true → 16:9, false → fixed height
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function detectEmbed(raw: string): EmbedInfo | null {
|
|
10
|
+
let url: URL;
|
|
11
|
+
try {
|
|
12
|
+
url = new URL(raw.trim());
|
|
13
|
+
} catch {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
const host = url.hostname.replace(/^www\./, '');
|
|
17
|
+
if (host === 'youtube.com' || host === 'm.youtube.com' || host === 'youtu.be' || host === 'youtube-nocookie.com') {
|
|
18
|
+
const id =
|
|
19
|
+
host === 'youtu.be'
|
|
20
|
+
? url.pathname.slice(1).split('/')[0]
|
|
21
|
+
: url.pathname.startsWith('/shorts/') || url.pathname.startsWith('/embed/') || url.pathname.startsWith('/live/')
|
|
22
|
+
? url.pathname.split('/')[2]
|
|
23
|
+
: url.searchParams.get('v');
|
|
24
|
+
// keep a start offset (?start=90, ?t=1h2m3s) — sermon and talk links
|
|
25
|
+
// usually point into the middle of a stream
|
|
26
|
+
const start = youtubeStart(url.searchParams.get('start') ?? url.searchParams.get('t'));
|
|
27
|
+
if (id) return { provider: 'youtube', embedSrc: `https://www.youtube-nocookie.com/embed/${id}${start ? `?start=${start}` : ''}`, aspect: true };
|
|
28
|
+
}
|
|
29
|
+
if (host === 'vimeo.com' || host === 'player.vimeo.com') {
|
|
30
|
+
const id = url.pathname.split('/').filter(Boolean).pop();
|
|
31
|
+
if (id && /^\d+$/.test(id)) return { provider: 'vimeo', embedSrc: `https://player.vimeo.com/video/${id}`, aspect: true };
|
|
32
|
+
}
|
|
33
|
+
if (host === 'loom.com' && url.pathname.startsWith('/share/')) {
|
|
34
|
+
return { provider: 'loom', embedSrc: `https://www.loom.com/embed/${url.pathname.split('/')[2]}`, aspect: true };
|
|
35
|
+
}
|
|
36
|
+
if (host === 'codepen.io' && url.pathname.includes('/pen/')) {
|
|
37
|
+
return { provider: 'codepen', embedSrc: raw.replace('/pen/', '/embed/'), aspect: false };
|
|
38
|
+
}
|
|
39
|
+
if (url.protocol === 'https:') {
|
|
40
|
+
return { provider: 'iframe', embedSrc: raw, aspect: false };
|
|
41
|
+
}
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function youtubeStart(raw: string | null): number {
|
|
46
|
+
if (!raw) return 0;
|
|
47
|
+
if (/^\d+s?$/.test(raw)) return parseInt(raw, 10);
|
|
48
|
+
const m = raw.match(/^(?:(\d+)h)?(?:(\d+)m)?(?:(\d+)s)?$/);
|
|
49
|
+
return m ? Number(m[1] ?? 0) * 3600 + Number(m[2] ?? 0) * 60 + Number(m[3] ?? 0) : 0;
|
|
50
|
+
}
|
package/src/lib/repo/index.ts
CHANGED
|
@@ -1,23 +1,27 @@
|
|
|
1
1
|
import type { ContentRepository } from './types';
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
// The promise (not the instance) is cached: concurrent first requests would
|
|
4
|
+
// otherwise each create a repository and race to run pending migrations.
|
|
5
|
+
let repo: Promise<ContentRepository> | null = null;
|
|
4
6
|
|
|
5
7
|
// Adapter priority: Cloudflare D1 (when the Worker binding exists —
|
|
6
8
|
// production on Workers and local wrangler/astro dev via platformProxy)
|
|
7
9
|
// → libSQL file (plain Node self-hosting).
|
|
8
10
|
// Pass Astro.locals (or the endpoint's `locals`) so the D1 binding is
|
|
9
11
|
// reachable; the chosen adapter is cached for the isolate's lifetime.
|
|
10
|
-
export
|
|
11
|
-
|
|
12
|
+
export function getRepo(locals?: unknown): Promise<ContentRepository> {
|
|
13
|
+
repo ??= create(locals).catch((e) => { repo = null; throw e; });
|
|
14
|
+
return repo;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async function create(locals?: unknown): Promise<ContentRepository> {
|
|
12
18
|
const d1 = (locals as { runtime?: { env?: { DB?: unknown } } } | undefined)?.runtime?.env?.DB;
|
|
13
19
|
if (d1) {
|
|
14
20
|
const { D1Repository } = await import('./d1');
|
|
15
|
-
|
|
16
|
-
} else {
|
|
17
|
-
const { SqliteRepository } = await import('./sqlite');
|
|
18
|
-
repo = new SqliteRepository();
|
|
21
|
+
return new D1Repository(d1 as import('./d1').D1Database);
|
|
19
22
|
}
|
|
20
|
-
|
|
23
|
+
const { SqliteRepository } = await import('./sqlite');
|
|
24
|
+
return new SqliteRepository();
|
|
21
25
|
}
|
|
22
26
|
|
|
23
27
|
export type { ContentRepository };
|