@acms-subir/core 0.3.1 → 0.3.3-next.20260929113209

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
@@ -68,6 +68,7 @@ const ROUTES = [
68
68
  ['/api/email/run', 'api/email/run.ts'],
69
69
  ['/api/ops/status', 'api/ops/status.ts'],
70
70
  ['/admin/ai', 'admin/ai.astro'],
71
+ ['/admin/import', 'admin/import.astro'],
71
72
  ['/admin/tags', 'admin/tags.astro'],
72
73
  ['/admin/users', 'admin/users.astro'],
73
74
  // API
@@ -86,6 +87,23 @@ const ROUTES = [
86
87
  ['/api/mcp', 'api/mcp.ts'],
87
88
  ['/api/media/upload', 'api/media/upload.ts'],
88
89
  ['/api/restore', 'api/restore.ts'],
90
+ ['/api/export', 'api/export.ts'],
91
+ ['/api/import', 'api/import/index.ts'],
92
+ ['/api/import/asset', 'api/import/asset.ts'],
93
+ ['/api/import/media', 'api/import/media.ts'],
94
+ ];
95
+
96
+ // Packages imported by browser-side code (admin islands and the libs they
97
+ // pull in). Keep in sync when a component gains a new dependency; the
98
+ // integration test checks every entry is a declared dependency.
99
+ export const CLIENT_DEPS = [
100
+ 'react', 'react-dom', 'react-dom/client',
101
+ '@tiptap/core', '@tiptap/react', '@tiptap/react/menus', '@tiptap/pm/state', '@tiptap/pm/view', '@tiptap/pm/model',
102
+ '@tiptap/starter-kit', '@tiptap/extensions', '@tiptap/extension-table', '@tiptap/extension-highlight',
103
+ '@tiptap/extension-code-block-lowlight', '@tiptap/suggestion',
104
+ '@dnd-kit/core', '@dnd-kit/sortable', '@dnd-kit/utilities',
105
+ 'prismjs', 'simple-icons', 'lowlight', 'hast-util-to-html', 'marked', 'turndown', 'zod',
106
+ 'mammoth/mammoth.browser.js',
89
107
  ];
90
108
 
91
109
  // First segments of the fixed routes above — a configured base landing on one
@@ -515,6 +533,25 @@ export default function acms(options = {}) {
515
533
  }
516
534
  updateConfig({ vite: { plugins: [acmsVitePlugin(userConfig)] } });
517
535
 
536
+ // Installed from npm, core's admin islands are source files inside
537
+ // node_modules, so Vite's dev scanner never crawls them: their
538
+ // dependencies are found only at runtime and CommonJS ones
539
+ // (use-sync-external-store via @tiptap/react, prismjs, mammoth) are
540
+ // served raw, which breaks hydration ("doesn't provide an export
541
+ // named …"), and late discoveries force a mid-load reload. Pre-bundle
542
+ // the whole client graph up front. The `core > dep` form resolves
543
+ // each package from core's own location, so it also works when the
544
+ // package manager doesn't hoist them. Build output is unaffected.
545
+ if (command === 'dev') {
546
+ updateConfig({
547
+ vite: {
548
+ optimizeDeps: {
549
+ include: CLIENT_DEPS.map((dep) => `@acms-subir/core > ${dep}`),
550
+ },
551
+ },
552
+ });
553
+ }
554
+
518
555
  // "Project-local routes win" is enforced here: Astro registers
519
556
  // injected and file-based routes at equal priority (and the injected
520
557
  // 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.1",
3
+ "version": "0.3.3-next.20260929113209",
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 {