@plutocms/core 0.47.0 → 0.48.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 +18 -0
- package/FEATURES.md +15 -4
- package/README.md +5 -5
- package/app/components/Pluto/Admin/PlutoAdminBackendBadge.vue +84 -0
- package/app/components/Pluto/Admin/PlutoAdminNavbar.vue +7 -0
- package/app/components/Pluto/Github/PlutoGithubBackendDetails.vue +180 -0
- package/app/components/Pluto/Github/PlutoGithubBuildStatus.vue +17 -4
- package/app/components/Pluto/Github/PlutoGithubDeployPage.vue +472 -0
- package/app/components/Pluto/Github/PlutoGithubSetupPanel.vue +1 -1
- package/app/components/Pluto/Github/PlutoGithubUsersPage.vue +214 -0
- package/app/components/Pluto/PlutoHtml.vue +8 -2
- package/app/composables/internal/async-component.ts +9 -0
- package/app/composables/pluto-extension.ts +4 -0
- package/app/composables/pluto-navbar.ts +25 -0
- package/app/composables/pluto-registry.ts +1 -0
- package/i18n/locales/en.json +164 -2
- package/i18n/locales/pt-BR.json +164 -2
- package/modules/pluto-github/index.ts +127 -51
- package/modules/pluto-github/options.ts +18 -5
- package/modules/pluto-github/runtime/app/composables/pluto-github-local-git.ts +50 -0
- package/modules/pluto-github/runtime/app/composables/pluto-github-session.ts +8 -1
- package/modules/pluto-github/runtime/app/plugins/pluto-github.ts +84 -1
- package/modules/pluto-github/runtime/server/adapter.ts +26 -7
- package/modules/pluto-github/runtime/server/api/deploy/pages.post.ts +5 -0
- package/modules/pluto-github/runtime/server/api/deploy/status.get.ts +5 -0
- package/modules/pluto-github/runtime/server/api/local/push.post.ts +5 -0
- package/modules/pluto-github/runtime/server/api/local/status.get.ts +5 -0
- package/modules/pluto-github/runtime/server/api/users.get.ts +5 -0
- package/modules/pluto-github/runtime/server/store/github.ts +4 -1
- package/modules/pluto-github/runtime/server/store/local-git.ts +145 -0
- package/modules/pluto-github/runtime/server/store/memory.ts +4 -1
- package/modules/pluto-github/runtime/server/utils/build-status.ts +16 -7
- package/modules/pluto-github/runtime/server/utils/deploy.ts +204 -0
- package/modules/pluto-github/runtime/server/utils/github-setup.ts +1 -3
- package/modules/pluto-github/runtime/server/utils/github-users.ts +120 -0
- package/modules/pluto-github/runtime/server/utils/local-git-routes.ts +95 -0
- package/modules/pluto-github/runtime/shared/build-status.ts +26 -0
- package/modules/pluto-github/runtime/shared/client-rules.ts +16 -0
- package/modules/pluto-github/runtime/shared/deploy.ts +43 -0
- package/modules/pluto-github/runtime/shared/pages-workflow.ts +138 -0
- package/modules/pluto-github/runtime/shared/repo-users.ts +97 -0
- package/package.json +1 -1
- package/shared/types/github.d.ts +2 -0
- package/shared/types/registry.ts +22 -0
- package/shared/utils/html-base.ts +23 -0
|
@@ -55,6 +55,16 @@ export default defineNuxtModule<PlutoGithubOptions>({
|
|
|
55
55
|
return meta?.name === 'supabase' || meta?.plutoBackend === true
|
|
56
56
|
}) || modules.includes('@nuxtjs/supabase')
|
|
57
57
|
|
|
58
|
+
// A static build (`nuxt generate`, or a static Nitro preset such as `github_pages`): the pages
|
|
59
|
+
// are prerendered from the files of the checkout. The output has no server, so the module adds
|
|
60
|
+
// no sign-in, admin or write route.
|
|
61
|
+
// `_generate` is the internal flag of `nuxt generate`. It is not in the public types.
|
|
62
|
+
const generate = (nuxt.options as { _generate?: boolean })._generate
|
|
63
|
+
const isStatic =
|
|
64
|
+
!nuxt.options.dev &&
|
|
65
|
+
(Boolean(generate || nuxt.options.nitro?.static) ||
|
|
66
|
+
isStaticPreset(nuxt.options.nitro?.preset ?? process.env.NITRO_PRESET))
|
|
67
|
+
|
|
58
68
|
const { enabled, resolved, warnings, errors } = resolvePlutoGithubOptions(
|
|
59
69
|
options,
|
|
60
70
|
{
|
|
@@ -65,6 +75,7 @@ export default defineNuxtModule<PlutoGithubOptions>({
|
|
|
65
75
|
gitOrigin: nuxt.options.dev
|
|
66
76
|
? readGitOrigin(nuxt.options.rootDir)
|
|
67
77
|
: undefined,
|
|
78
|
+
static: isStatic,
|
|
68
79
|
}
|
|
69
80
|
)
|
|
70
81
|
|
|
@@ -98,7 +109,8 @@ export default defineNuxtModule<PlutoGithubOptions>({
|
|
|
98
109
|
Object.assign(secrets, {
|
|
99
110
|
apiUrl: resolved.github.apiUrl,
|
|
100
111
|
skipDeploy: resolved.commit.skipDeploy,
|
|
101
|
-
|
|
112
|
+
// A static build only reads.
|
|
113
|
+
localCommit: resolved.commit.local && !isStatic,
|
|
102
114
|
headTtl: resolved.cache.headTtl,
|
|
103
115
|
cacheStorage: resolved.cache.storage,
|
|
104
116
|
postType: resolved.postType,
|
|
@@ -118,6 +130,8 @@ export default defineNuxtModule<PlutoGithubOptions>({
|
|
|
118
130
|
dir: resolved.dir,
|
|
119
131
|
postType: resolved.postType,
|
|
120
132
|
skipDeploy: resolved.commit.skipDeploy,
|
|
133
|
+
// A static build has no server: the client asks for no session.
|
|
134
|
+
static: isStatic,
|
|
121
135
|
}
|
|
122
136
|
// The media library of core uses the GitHub driver (runtime/server/media.ts).
|
|
123
137
|
const media = nuxt.options.runtimeConfig.public.plutoMedia as
|
|
@@ -129,31 +143,55 @@ export default defineNuxtModule<PlutoGithubOptions>({
|
|
|
129
143
|
|
|
130
144
|
const resolver = createResolver(import.meta.url)
|
|
131
145
|
const server = (file: string) => resolver.resolve('./runtime/server', file)
|
|
146
|
+
// A route of the server build. A static build has no server, so it adds none.
|
|
147
|
+
const addRoute = (route: string, method: 'get' | 'post', file: string) => {
|
|
148
|
+
if (!isStatic) {
|
|
149
|
+
addServerHandler({ route, method, handler: server(file) })
|
|
150
|
+
}
|
|
151
|
+
}
|
|
132
152
|
|
|
133
|
-
//
|
|
134
|
-
//
|
|
135
|
-
addServerHandler({
|
|
136
|
-
route: '/api/_pluto/auth/github/login',
|
|
137
|
-
method: 'get',
|
|
138
|
-
handler: server('api/auth/github/login.get'),
|
|
139
|
-
})
|
|
140
|
-
addServerHandler({
|
|
141
|
-
route: '/api/_pluto/auth/github/callback',
|
|
142
|
-
method: 'get',
|
|
143
|
-
handler: server('api/auth/github/callback.get'),
|
|
144
|
-
})
|
|
145
|
-
addServerHandler({
|
|
146
|
-
route: '/api/_pluto/auth/logout',
|
|
147
|
-
method: 'post',
|
|
148
|
-
handler: server('api/auth/logout.post'),
|
|
149
|
-
})
|
|
150
|
-
addServerHandler({
|
|
151
|
-
route: '/api/_pluto/auth/session',
|
|
152
|
-
method: 'get',
|
|
153
|
-
handler: server('api/auth/session.get'),
|
|
154
|
-
})
|
|
153
|
+
// The Nitro plugin registers the store, the adapter and the post type. A static build needs
|
|
154
|
+
// it too: the public API reads the files while the pages prerender.
|
|
155
155
|
addServerPlugin(server('plugins/pluto-github'))
|
|
156
156
|
|
|
157
|
+
if (isStatic) {
|
|
158
|
+
// A project site of GitHub Pages lives under `/<repo>/`. The workflow gives the path in
|
|
159
|
+
// `PLUTO_BASE_URL`, and the module writes it in the build config and the app config.
|
|
160
|
+
// `NUXT_APP_BASE_URL` does not work here: the prerenderer reads it at run time too, the
|
|
161
|
+
// paths then do not match, and every page answers a redirect.
|
|
162
|
+
const basePath = process.env.PLUTO_BASE_URL?.trim()
|
|
163
|
+
if (basePath) {
|
|
164
|
+
const baseURL = normalizeBasePath(basePath)
|
|
165
|
+
nuxt.options.app.baseURL = baseURL
|
|
166
|
+
nuxt.options.runtimeConfig.app = {
|
|
167
|
+
...nuxt.options.runtimeConfig.app,
|
|
168
|
+
baseURL,
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
// The admin, the API routes of the admin and the preview pages stay out of the output.
|
|
172
|
+
nuxt.options.nitro.prerender ||= {}
|
|
173
|
+
nuxt.options.nitro.prerender.ignore = [
|
|
174
|
+
...(nuxt.options.nitro.prerender.ignore ?? []),
|
|
175
|
+
'/admin',
|
|
176
|
+
'/api/_pluto',
|
|
177
|
+
'/_pluto',
|
|
178
|
+
]
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Server: the sign-in routes.
|
|
182
|
+
addRoute(
|
|
183
|
+
'/api/_pluto/auth/github/login',
|
|
184
|
+
'get',
|
|
185
|
+
'api/auth/github/login.get'
|
|
186
|
+
)
|
|
187
|
+
addRoute(
|
|
188
|
+
'/api/_pluto/auth/github/callback',
|
|
189
|
+
'get',
|
|
190
|
+
'api/auth/github/callback.get'
|
|
191
|
+
)
|
|
192
|
+
addRoute('/api/_pluto/auth/logout', 'post', 'api/auth/logout.post')
|
|
193
|
+
addRoute('/api/_pluto/auth/session', 'get', 'api/auth/session.get')
|
|
194
|
+
|
|
157
195
|
// Client: the extension plugin and the session composable. Core owns the
|
|
158
196
|
// admin routes, the login page and the route guard.
|
|
159
197
|
addPlugin({ src: resolver.resolve('./runtime/app/plugins/pluto-github') })
|
|
@@ -183,10 +221,27 @@ export default defineNuxtModule<PlutoGithubOptions>({
|
|
|
183
221
|
const settingsFile = resolver.resolve(
|
|
184
222
|
'../../app/components/Pluto/Github/PlutoGithubSettingsPage.vue'
|
|
185
223
|
)
|
|
224
|
+
// The Deploy tab of Settings (`/admin/settings/deploy`): the hosts, and GitHub Pages.
|
|
225
|
+
const deployFile = resolver.resolve(
|
|
226
|
+
'../../app/components/Pluto/Github/PlutoGithubDeployPage.vue'
|
|
227
|
+
)
|
|
228
|
+
// The Users page (`/admin/users`): the people with write access to the repo, read only.
|
|
229
|
+
const usersFile = resolver.resolve(
|
|
230
|
+
'../../app/components/Pluto/Github/PlutoGithubUsersPage.vue'
|
|
231
|
+
)
|
|
186
232
|
extendPages((pages) => {
|
|
187
|
-
const
|
|
188
|
-
|
|
189
|
-
|
|
233
|
+
const admin = pages.find((page) => page.path === '/admin')
|
|
234
|
+
if (admin) {
|
|
235
|
+
admin.children ||= []
|
|
236
|
+
admin.children.push({
|
|
237
|
+
name: 'admin-users',
|
|
238
|
+
path: 'users',
|
|
239
|
+
file: usersFile,
|
|
240
|
+
})
|
|
241
|
+
} else {
|
|
242
|
+
logger.warn('Could not find the admin page. The Users page is off.')
|
|
243
|
+
}
|
|
244
|
+
const settings = admin?.children?.find((page) => page.path === 'settings')
|
|
190
245
|
if (settings) {
|
|
191
246
|
settings.children ||= []
|
|
192
247
|
settings.children.push({
|
|
@@ -194,6 +249,11 @@ export default defineNuxtModule<PlutoGithubOptions>({
|
|
|
194
249
|
path: 'github',
|
|
195
250
|
file: settingsFile,
|
|
196
251
|
})
|
|
252
|
+
settings.children.push({
|
|
253
|
+
name: 'admin-settings-deploy',
|
|
254
|
+
path: 'deploy',
|
|
255
|
+
file: deployFile,
|
|
256
|
+
})
|
|
197
257
|
} else {
|
|
198
258
|
logger.warn('Could not find the Settings page. The GitHub tab is off.')
|
|
199
259
|
}
|
|
@@ -201,37 +261,36 @@ export default defineNuxtModule<PlutoGithubOptions>({
|
|
|
201
261
|
|
|
202
262
|
// Setup routes: the checks and the App Manifest flow. See the git-cms skill.
|
|
203
263
|
// They are static, so they win over the `:type/:slug` route below.
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
method: 'get',
|
|
212
|
-
handler: server('api/setup/start.get'),
|
|
213
|
-
})
|
|
214
|
-
addServerHandler({
|
|
215
|
-
route: '/api/_pluto/github/setup/callback',
|
|
216
|
-
method: 'get',
|
|
217
|
-
handler: server('api/setup/callback.get'),
|
|
218
|
-
})
|
|
264
|
+
addRoute('/api/_pluto/github/setup/status', 'get', 'api/setup/status.get')
|
|
265
|
+
addRoute('/api/_pluto/github/setup/start', 'get', 'api/setup/start.get')
|
|
266
|
+
addRoute(
|
|
267
|
+
'/api/_pluto/github/setup/callback',
|
|
268
|
+
'get',
|
|
269
|
+
'api/setup/callback.get'
|
|
270
|
+
)
|
|
219
271
|
|
|
220
272
|
// The build status of the head commit. It needs a signed-in user.
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
273
|
+
addRoute('/api/_pluto/github/build-status', 'get', 'api/build-status.get')
|
|
274
|
+
|
|
275
|
+
// The Deploy tab: the facts of the hosts (signed-in user), and the GitHub Pages setup (local
|
|
276
|
+
// mode on this computer only; the handler checks it).
|
|
277
|
+
addRoute('/api/_pluto/github/deploy/status', 'get', 'api/deploy/status.get')
|
|
278
|
+
addRoute('/api/_pluto/github/deploy/pages', 'post', 'api/deploy/pages.post')
|
|
279
|
+
|
|
280
|
+
// The people with write access to the repo, for the Users page. It needs a signed-in user.
|
|
281
|
+
addRoute('/api/_pluto/github/users', 'get', 'api/users.get')
|
|
282
|
+
|
|
283
|
+
// Local mode: the Git state of the project and the Push button of the backend badge.
|
|
284
|
+
// A dev build with the `fs` store only. The handlers also check the request.
|
|
285
|
+
if (nuxt.options.dev && resolved.storage === 'fs') {
|
|
286
|
+
addRoute('/api/_pluto/github/local/status', 'get', 'api/local/status.get')
|
|
287
|
+
addRoute('/api/_pluto/github/local/push', 'post', 'api/local/push.post')
|
|
288
|
+
}
|
|
226
289
|
|
|
227
290
|
// The public reads of the posts go through the public API of core (`/api/pluto/v1/content/post`).
|
|
228
291
|
|
|
229
292
|
// The media files that are in the repo but not in a deploy yet. See the media skill.
|
|
230
|
-
|
|
231
|
-
route: '/media/**',
|
|
232
|
-
method: 'get',
|
|
233
|
-
handler: server('routes/media.get'),
|
|
234
|
-
})
|
|
293
|
+
addRoute('/media/**', 'get', 'routes/media.get')
|
|
235
294
|
|
|
236
295
|
logger.info(
|
|
237
296
|
`Standalone CMS on. Storage: ${resolved.storage}. Dir: ${resolved.dir}.`
|
|
@@ -245,3 +304,20 @@ function toCamelCase(name: string): string {
|
|
|
245
304
|
.toLowerCase()
|
|
246
305
|
.replace(/_([a-z])/g, (_match, char: string) => char.toUpperCase())
|
|
247
306
|
}
|
|
307
|
+
|
|
308
|
+
/** The Nitro presets that write only static files: `static`, `github_pages`, `*_static`. */
|
|
309
|
+
export function isStaticPreset(preset: unknown): boolean {
|
|
310
|
+
if (typeof preset !== 'string') {
|
|
311
|
+
return false
|
|
312
|
+
}
|
|
313
|
+
const name = preset.toLowerCase().replaceAll('-', '_')
|
|
314
|
+
return (
|
|
315
|
+
name === 'static' || name === 'github_pages' || name.endsWith('_static')
|
|
316
|
+
)
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/** `repo`, `/repo` and `/repo/` become `/repo/`. An empty path becomes `/`. */
|
|
320
|
+
export function normalizeBasePath(path: string): string {
|
|
321
|
+
const segments = path.split('/').filter(Boolean)
|
|
322
|
+
return segments.length > 0 ? `/${segments.join('/')}/` : '/'
|
|
323
|
+
}
|
|
@@ -7,12 +7,15 @@ export interface PlutoGithubOptions {
|
|
|
7
7
|
* `false` turns it off.
|
|
8
8
|
*/
|
|
9
9
|
enabled?: boolean | 'auto'
|
|
10
|
-
/**
|
|
10
|
+
/**
|
|
11
|
+
* Where the posts live. `fs` is valid in dev and in a static build only. Default `fs` in dev and
|
|
12
|
+
* in a static build (`nuxt generate`), `github` in a server build.
|
|
13
|
+
*/
|
|
11
14
|
storage?: 'github' | 'fs'
|
|
12
15
|
github?: {
|
|
13
16
|
/**
|
|
14
17
|
* The repo as `owner/name`. Required when storage is `github`. Default: the env var
|
|
15
|
-
* `NUXT_PLUTO_GITHUB_REPO` or `PLUTO_GITHUB_REPO`, then Vercel's repo, then (dev only)
|
|
18
|
+
* `NUXT_PLUTO_GITHUB_REPO` or `PLUTO_GITHUB_REPO`, then Vercel's repo, then Netlify's repo, then (dev only)
|
|
16
19
|
* the `origin` remote of the local git repo.
|
|
17
20
|
*/
|
|
18
21
|
repo?: string
|
|
@@ -72,6 +75,11 @@ export interface PlutoGithubResolveContext {
|
|
|
72
75
|
env?: Record<string, string | undefined>
|
|
73
76
|
/** The URL of the `origin` git remote. The module reads it in dev only. */
|
|
74
77
|
gitOrigin?: string
|
|
78
|
+
/**
|
|
79
|
+
* A static build (`nuxt generate`): the site reads the content from the files of the checkout
|
|
80
|
+
* while it prerenders. The `fs` store is the default and is valid. No sign-in, no admin.
|
|
81
|
+
*/
|
|
82
|
+
static?: boolean
|
|
75
83
|
}
|
|
76
84
|
|
|
77
85
|
export interface PlutoGithubResolveResult {
|
|
@@ -137,7 +145,7 @@ export function resolvePlutoGithubOptions(
|
|
|
137
145
|
options.storage ??
|
|
138
146
|
(fromEnv('PLUTO_GITHUB_STORAGE') as
|
|
139
147
|
ResolvedPlutoGithubOptions['storage'] | undefined) ??
|
|
140
|
-
(ctx.dev ? 'fs' : 'github'),
|
|
148
|
+
(ctx.dev || ctx.static ? 'fs' : 'github'),
|
|
141
149
|
github: {
|
|
142
150
|
repo: options.github?.repo || detectRepo(ctx, fromEnv),
|
|
143
151
|
branch: options.github?.branch ?? 'main',
|
|
@@ -185,9 +193,9 @@ export function resolvePlutoGithubOptions(
|
|
|
185
193
|
errors.push(
|
|
186
194
|
`plutoGithub.storage must be "github" or "fs". Got "${String(resolved.storage)}".`
|
|
187
195
|
)
|
|
188
|
-
} else if (resolved.storage === 'fs' && !ctx.dev) {
|
|
196
|
+
} else if (resolved.storage === 'fs' && !ctx.dev && !ctx.static) {
|
|
189
197
|
errors.push(
|
|
190
|
-
'plutoGithub.storage "fs" works in dev only. Use "github" for a build.'
|
|
198
|
+
'plutoGithub.storage "fs" works in dev and in a static build (nuxt generate) only. Use "github" for a server build.'
|
|
191
199
|
)
|
|
192
200
|
} else if (resolved.storage === 'github' && !resolved.github.repo) {
|
|
193
201
|
errors.push(
|
|
@@ -238,6 +246,11 @@ function detectRepo(
|
|
|
238
246
|
if (owner && slug) {
|
|
239
247
|
return `${owner}/${slug}`
|
|
240
248
|
}
|
|
249
|
+
// Netlify gives the URL of the repo to the build.
|
|
250
|
+
const netlify = parseGithubRepoUrl(fromEnv('REPOSITORY_URL') ?? '')
|
|
251
|
+
if (netlify) {
|
|
252
|
+
return netlify
|
|
253
|
+
}
|
|
241
254
|
return ctx.dev && ctx.gitOrigin
|
|
242
255
|
? (parseGithubRepoUrl(ctx.gitOrigin) ?? '')
|
|
243
256
|
: ''
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { LocalGitStatus } from '../../server/store/local-git'
|
|
2
|
+
import { computed } from 'vue'
|
|
3
|
+
import { useState } from '#imports'
|
|
4
|
+
|
|
5
|
+
export type { LocalGitStatus }
|
|
6
|
+
|
|
7
|
+
export const CMS_LOCAL_STATUS_URL = '/api/_pluto/github/local/status'
|
|
8
|
+
export const CMS_LOCAL_PUSH_URL = '/api/_pluto/github/local/push'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The local Git repo of the project in local mode (`fs` store, dev): the branch and the commits
|
|
12
|
+
* that are not pushed. The state is shared under one key. It is `null` until the first read, and
|
|
13
|
+
* after a failed read. The CMS plugin reads it again on each admin navigation, when the tab shows
|
|
14
|
+
* again, and every 15 seconds while an admin page shows.
|
|
15
|
+
*/
|
|
16
|
+
export function usePlutoGithubLocalGit() {
|
|
17
|
+
const status = useState<LocalGitStatus | null>(
|
|
18
|
+
'pluto-github-local-git',
|
|
19
|
+
() => null
|
|
20
|
+
)
|
|
21
|
+
const pushing = useState('pluto-github-local-git-pushing', () => false)
|
|
22
|
+
|
|
23
|
+
async function refresh(): Promise<void> {
|
|
24
|
+
try {
|
|
25
|
+
status.value = await $fetch<LocalGitStatus>(CMS_LOCAL_STATUS_URL)
|
|
26
|
+
} catch {
|
|
27
|
+
status.value = null
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Pushes the branch. Throws the fetch error, so the caller can show it. */
|
|
32
|
+
async function push(): Promise<void> {
|
|
33
|
+
pushing.value = true
|
|
34
|
+
try {
|
|
35
|
+
status.value = await $fetch<LocalGitStatus>(CMS_LOCAL_PUSH_URL, {
|
|
36
|
+
method: 'POST',
|
|
37
|
+
})
|
|
38
|
+
} finally {
|
|
39
|
+
pushing.value = false
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
status,
|
|
45
|
+
pushing,
|
|
46
|
+
ahead: computed(() => (status.value?.repo ? status.value.ahead : 0)),
|
|
47
|
+
refresh,
|
|
48
|
+
push,
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { computed } from 'vue'
|
|
2
|
-
import { useFetch, useRequestFetch } from '#imports'
|
|
2
|
+
import { useFetch, useRequestFetch, useRuntimeConfig } from '#imports'
|
|
3
3
|
|
|
4
4
|
export const CMS_SESSION_URL = '/api/_pluto/auth/session'
|
|
5
5
|
export const CMS_LOGOUT_URL = '/api/_pluto/auth/logout'
|
|
@@ -26,11 +26,18 @@ export interface PlutoGithubSessionResponse {
|
|
|
26
26
|
* The browser fetches it after hydration (`server: false`). The server never
|
|
27
27
|
* reads it, so a public page that a CDN caches never holds a user. The admin
|
|
28
28
|
* is client-only, so it loses nothing. The response has no token.
|
|
29
|
+
*
|
|
30
|
+
* A static build (`public.plutoGithub.static`) has no server, so it never asks: nobody is
|
|
31
|
+
* signed in there.
|
|
29
32
|
*/
|
|
30
33
|
export function usePlutoGithubSession() {
|
|
34
|
+
const isStatic =
|
|
35
|
+
(useRuntimeConfig().public.plutoGithub as { static?: boolean } | undefined)
|
|
36
|
+
?.static === true
|
|
31
37
|
const asyncData = useFetch<PlutoGithubSessionResponse>(CMS_SESSION_URL, {
|
|
32
38
|
key: 'pluto-github-session',
|
|
33
39
|
server: false,
|
|
40
|
+
immediate: !isStatic,
|
|
34
41
|
$fetch: useRequestFetch(),
|
|
35
42
|
default: () => ({ user: null }),
|
|
36
43
|
})
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { PlutoGithubSessionResponse } from '../composables/pluto-github-session'
|
|
2
2
|
import { defineAsyncComponent } from 'vue'
|
|
3
|
-
import { defineNuxtPlugin, useRuntimeConfig } from '#imports'
|
|
3
|
+
import { defineNuxtPlugin, useRouter, useRuntimeConfig } from '#imports'
|
|
4
4
|
import PlutoGithubSignIn from '../../../../../app/components/Pluto/Github/PlutoGithubSignIn.vue'
|
|
5
|
+
import { isAdminPath } from '../../../../../app/composables/internal/admin-shell'
|
|
5
6
|
import { definePlutoExtension } from '../../../../../app/composables/pluto-extension'
|
|
6
7
|
import { usePlutoRegistry } from '../../../../../app/composables/pluto-registry'
|
|
7
8
|
import {
|
|
@@ -10,11 +11,21 @@ import {
|
|
|
10
11
|
showPublicRepoWarning,
|
|
11
12
|
} from '../../shared/client-rules'
|
|
12
13
|
import { plutoGithubPostType } from '../../shared/post-type'
|
|
14
|
+
import { usePlutoGithubLocalGit } from '../composables/pluto-github-local-git'
|
|
13
15
|
import {
|
|
14
16
|
CMS_SESSION_URL,
|
|
15
17
|
usePlutoGithubSession,
|
|
16
18
|
} from '../composables/pluto-github-session'
|
|
17
19
|
|
|
20
|
+
// The popover of the backend badge. It loads when the popover opens.
|
|
21
|
+
const PlutoGithubBackendDetails = defineAsyncComponent(
|
|
22
|
+
() =>
|
|
23
|
+
import('../../../../../app/components/Pluto/Github/PlutoGithubBackendDetails.vue')
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
/** How often the badge reads the local repo again while an admin page shows. */
|
|
27
|
+
const LOCAL_GIT_INTERVAL = 15_000
|
|
28
|
+
|
|
18
29
|
// The dashboard loads each widget on demand, with a skeleton.
|
|
19
30
|
const PlutoGithubPublicRepoWarning = defineAsyncComponent(
|
|
20
31
|
() =>
|
|
@@ -51,6 +62,16 @@ export default defineNuxtPlugin(() => {
|
|
|
51
62
|
| undefined
|
|
52
63
|
)
|
|
53
64
|
|
|
65
|
+
// Local mode: the saves are commits in the local repo. The badge counts the commits that are
|
|
66
|
+
// not pushed. The status route exists in this mode only.
|
|
67
|
+
const local =
|
|
68
|
+
(useRuntimeConfig().public.plutoGithub as { storage?: string } | undefined)
|
|
69
|
+
?.storage === 'fs'
|
|
70
|
+
const localGit = usePlutoGithubLocalGit()
|
|
71
|
+
if (local && import.meta.client) {
|
|
72
|
+
watchLocalGit(localGit.refresh)
|
|
73
|
+
}
|
|
74
|
+
|
|
54
75
|
const { read, write, delete: remove } = plutoGithubPostType.capabilities ?? {}
|
|
55
76
|
const capabilities = (
|
|
56
77
|
postType
|
|
@@ -105,6 +126,35 @@ export default defineNuxtPlugin(() => {
|
|
|
105
126
|
signInTitle: 'pluto.github.login.title',
|
|
106
127
|
signInDescription: 'pluto.github.login.description',
|
|
107
128
|
},
|
|
129
|
+
// The Users item of the menu, between Media (900) and Settings (1000). The page
|
|
130
|
+
// (`/admin/users`, added by the module) lists the people with write access to the repo.
|
|
131
|
+
nav: [
|
|
132
|
+
{
|
|
133
|
+
id: 'users',
|
|
134
|
+
order: 950,
|
|
135
|
+
label: 'pluto.github.users.title',
|
|
136
|
+
icon: 'i-lucide-users',
|
|
137
|
+
to: '/admin/users',
|
|
138
|
+
},
|
|
139
|
+
],
|
|
140
|
+
|
|
141
|
+
// The backend badge of the navbar: local files with Git, or a GitHub repo.
|
|
142
|
+
backend: {
|
|
143
|
+
id: 'git',
|
|
144
|
+
kind: 'git',
|
|
145
|
+
label: local
|
|
146
|
+
? 'pluto.github.backend.local'
|
|
147
|
+
: 'pluto.github.backend.github',
|
|
148
|
+
icon: local ? 'i-lucide-hard-drive' : 'i-pluto-github',
|
|
149
|
+
count: () => (local ? localGit.ahead.value : 0),
|
|
150
|
+
countLabel: 'pluto.github.local.aheadShort',
|
|
151
|
+
details: PlutoGithubBackendDetails,
|
|
152
|
+
onOpen: () => {
|
|
153
|
+
if (local) {
|
|
154
|
+
localGit.refresh()
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
},
|
|
108
158
|
// The GitHub tab of Settings: the setup check and the Create button. The module
|
|
109
159
|
// adds its route (`/admin/settings/github`) with `extendPages`.
|
|
110
160
|
settingsPages: [
|
|
@@ -115,6 +165,14 @@ export default defineNuxtPlugin(() => {
|
|
|
115
165
|
icon: 'pluto:github',
|
|
116
166
|
to: '/admin/settings/github',
|
|
117
167
|
},
|
|
168
|
+
// The Deploy tab: put the site on a host. The module adds its route too.
|
|
169
|
+
{
|
|
170
|
+
id: 'deploy',
|
|
171
|
+
order: 160,
|
|
172
|
+
label: 'pluto.settings.tabs.deploy',
|
|
173
|
+
icon: 'i-lucide-rocket',
|
|
174
|
+
to: '/admin/settings/deploy',
|
|
175
|
+
},
|
|
118
176
|
],
|
|
119
177
|
// The content type gives the Posts nav item, the list and the form.
|
|
120
178
|
contentTypes: postType ? [{ id: 'post', type: plutoGithubPostType }] : [],
|
|
@@ -169,3 +227,28 @@ export default defineNuxtPlugin(() => {
|
|
|
169
227
|
},
|
|
170
228
|
})
|
|
171
229
|
})
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Reads the local repo on each admin navigation, when the tab shows again, and every
|
|
233
|
+
* `LOCAL_GIT_INTERVAL` while an admin page shows. A save commits on the server, so the count
|
|
234
|
+
* follows within that time, or at once on the next navigation.
|
|
235
|
+
*/
|
|
236
|
+
let watchingLocalGit = false
|
|
237
|
+
|
|
238
|
+
function watchLocalGit(refresh: () => Promise<void>): void {
|
|
239
|
+
if (watchingLocalGit) {
|
|
240
|
+
return
|
|
241
|
+
}
|
|
242
|
+
watchingLocalGit = true
|
|
243
|
+
const router = useRouter()
|
|
244
|
+
const onAdmin = () => isAdminPath(router.currentRoute.value.path)
|
|
245
|
+
const tick = () => {
|
|
246
|
+
if (onAdmin() && document.visibilityState === 'visible') {
|
|
247
|
+
refresh()
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
router.afterEach(tick)
|
|
251
|
+
document.addEventListener('visibilitychange', tick)
|
|
252
|
+
setInterval(tick, LOCAL_GIT_INTERVAL)
|
|
253
|
+
tick()
|
|
254
|
+
}
|
|
@@ -52,6 +52,10 @@ export interface MarkdownContentAdapterOptions {
|
|
|
52
52
|
/** Reads the caller of a request. `null` means anonymous. */
|
|
53
53
|
getAccess: (event: H3Event) => Promise<PlutoGithubAccess | null>
|
|
54
54
|
commit?: {
|
|
55
|
+
/**
|
|
56
|
+
* `true`: every commit gets `[skip ci]`. `false`: only a commit that changes no published
|
|
57
|
+
* content gets it (a draft that stays a draft), so a draft save starts no deploy.
|
|
58
|
+
*/
|
|
55
59
|
skipDeploy?: boolean
|
|
56
60
|
}
|
|
57
61
|
/** The cache that holds parsed files by blob id. A memory cache by default. */
|
|
@@ -183,7 +187,7 @@ export function createMarkdownContentAdapter(
|
|
|
183
187
|
const cache = options.cache ?? createGithubCache({ headTtl: 0 })
|
|
184
188
|
const retries = options.retries ?? 2
|
|
185
189
|
const now = options.now ?? (() => new Date())
|
|
186
|
-
const
|
|
190
|
+
const skipAll = options.commit?.skipDeploy === true
|
|
187
191
|
|
|
188
192
|
// --- Errors ---------------------------------------------------------------
|
|
189
193
|
|
|
@@ -469,7 +473,7 @@ export function createMarkdownContentAdapter(
|
|
|
469
473
|
function isPublished(
|
|
470
474
|
type: PlutoContentType,
|
|
471
475
|
layout: Layout,
|
|
472
|
-
entry: Entry
|
|
476
|
+
entry: Pick<Entry, 'frontmatter'>
|
|
473
477
|
): boolean {
|
|
474
478
|
if (!layout.statusColumn) {
|
|
475
479
|
return true
|
|
@@ -535,8 +539,17 @@ export function createMarkdownContentAdapter(
|
|
|
535
539
|
|
|
536
540
|
// --- Writes ---------------------------------------------------------------
|
|
537
541
|
|
|
538
|
-
|
|
539
|
-
|
|
542
|
+
/**
|
|
543
|
+
* The commit message. `publicChange` is false when the commit changes no published content:
|
|
544
|
+
* the item is a draft before and after. Such a commit gets `[skip ci]`, so it starts no deploy.
|
|
545
|
+
*/
|
|
546
|
+
function messageOf(
|
|
547
|
+
type: PlutoContentType,
|
|
548
|
+
verb: string,
|
|
549
|
+
publicChange: boolean
|
|
550
|
+
): string {
|
|
551
|
+
const skip = skipAll || !publicChange ? ' [skip ci]' : ''
|
|
552
|
+
return `content(${type.name}): ${verb}${skip}`
|
|
540
553
|
}
|
|
541
554
|
|
|
542
555
|
/** Runs a plan against a fresh head. Retries when only unrelated files changed. */
|
|
@@ -859,6 +872,8 @@ export function createMarkdownContentAdapter(
|
|
|
859
872
|
|
|
860
873
|
const frontmatter: Record<string, unknown> = {}
|
|
861
874
|
applyValues(type, layout, frontmatter, values, 'create')
|
|
875
|
+
// A published item (or a type with no status) is public: its commit starts a deploy.
|
|
876
|
+
const published = isPublished(type, layout, { frontmatter })
|
|
862
877
|
if (
|
|
863
878
|
layout.statusColumn &&
|
|
864
879
|
frontmatter[layout.statusColumn] === layout.publishedValue
|
|
@@ -874,7 +889,7 @@ export function createMarkdownContentAdapter(
|
|
|
874
889
|
throw slugTakenError(slug)
|
|
875
890
|
}
|
|
876
891
|
return {
|
|
877
|
-
message: messageOf(type, `create "${slug}"
|
|
892
|
+
message: messageOf(type, `create "${slug}"`, published),
|
|
878
893
|
additions: [{ path, contents: text }],
|
|
879
894
|
deletions: [],
|
|
880
895
|
targets: [{ path, oid: null }],
|
|
@@ -982,7 +997,7 @@ export function createMarkdownContentAdapter(
|
|
|
982
997
|
verb = `${nowPublished ? 'publish' : 'unpublish'} "${oldSlug}"`
|
|
983
998
|
}
|
|
984
999
|
return {
|
|
985
|
-
message: messageOf(type, verb),
|
|
1000
|
+
message: messageOf(type, verb, wasPublished || nowPublished),
|
|
986
1001
|
additions: [{ path: newPath, contents: text }],
|
|
987
1002
|
deletions: newPath === oldPath ? [] : [{ path: oldPath }],
|
|
988
1003
|
targets,
|
|
@@ -1018,7 +1033,11 @@ export function createMarkdownContentAdapter(
|
|
|
1018
1033
|
throw notFoundError()
|
|
1019
1034
|
}
|
|
1020
1035
|
return {
|
|
1021
|
-
message: messageOf(
|
|
1036
|
+
message: messageOf(
|
|
1037
|
+
type,
|
|
1038
|
+
`delete "${slug}"`,
|
|
1039
|
+
isPublished(type, layout, current)
|
|
1040
|
+
),
|
|
1022
1041
|
additions: [],
|
|
1023
1042
|
deletions: [{ path }],
|
|
1024
1043
|
targets: [{ path, oid: current.oid }],
|
|
@@ -323,7 +323,10 @@ export function createGithubStore(
|
|
|
323
323
|
)
|
|
324
324
|
return new Uint8Array(result.data)
|
|
325
325
|
} catch (error) {
|
|
326
|
-
if (
|
|
326
|
+
if (
|
|
327
|
+
error instanceof PlutoGithubStoreError &&
|
|
328
|
+
error.code === 'not-found'
|
|
329
|
+
) {
|
|
327
330
|
return null
|
|
328
331
|
}
|
|
329
332
|
throw error
|