mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2026-07-15 13:51:35 +00:00
Feature: use nuxt bridge to bundle client
This commit is contained in:
parent
98c117f0b8
commit
228b5203a8
23 changed files with 8484 additions and 19783 deletions
|
|
@ -14,6 +14,7 @@ npm-debug.log
|
|||
dev.js
|
||||
test/
|
||||
/client/.nuxt/
|
||||
/client/.output/
|
||||
/client/dist/
|
||||
/dist/
|
||||
/build/
|
||||
|
|
|
|||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -9,6 +9,7 @@
|
|||
/metadata/
|
||||
test/
|
||||
/client/.nuxt/
|
||||
/client/.output/
|
||||
/client/dist/
|
||||
/dist/
|
||||
/deploy/
|
||||
|
|
|
|||
|
|
@ -23,16 +23,12 @@ RUN apk update && \
|
|||
|
||||
COPY --from=tone /usr/local/bin/tone /usr/local/bin/
|
||||
COPY package* ./
|
||||
COPY --from=build /client/package*.json client/
|
||||
|
||||
RUN npm ci --omit=dev
|
||||
RUN cd client && npm ci --omit=dev && cd ..
|
||||
|
||||
COPY index.js package* ./
|
||||
COPY server server
|
||||
COPY --from=build /client/nuxt.config.js* client/
|
||||
COPY --from=build /client/.nuxt client/.nuxt
|
||||
COPY --from=build /client/modules client/modules
|
||||
COPY --from=build /client/.output ./client/.output
|
||||
|
||||
EXPOSE 80
|
||||
HEALTHCHECK \
|
||||
|
|
|
|||
|
|
@ -10,12 +10,12 @@
|
|||
<div v-show="routeName === route.iod" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
||||
</nuxt-link>
|
||||
|
||||
<modals-changelog-view-modal v-model="showChangelogModal" :changelog="currentVersionChangelog" :currentVersion="$config.version" />
|
||||
</div>
|
||||
<modals-changelog-view-modal v-model="showChangelogModal" :changelog="currentVersionChangelog" :currentVersion="$config.public.version" />
|
||||
</div>
|
||||
|
||||
<div class="w-44 h-12 px-4 border-t bg-bg border-black border-opacity-20 fixed left-0 flex flex-col justify-center" :class="wrapperClass" :style="{ bottom: streamLibraryItem ? '160px' : '0px' }">
|
||||
<div class="flex justify-between">
|
||||
<p class="underline font-mono text-sm" @click="clickChangelog">v{{ $config.version }}</p>
|
||||
<div class="w-44 h-12 px-4 border-t bg-bg border-black border-opacity-20 fixed left-0 flex flex-col justify-center" :class="wrapperClass" :style="{ bottom: streamLibraryItem ? '160px' : '0px' }">
|
||||
<div class="flex justify-between">
|
||||
<p class="underline font-mono text-sm" @click="clickChangelog">v{{ $config.public.version }}</p>
|
||||
|
||||
<p class="font-mono text-xs text-gray-300 italic">{{ Source }}</p>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -114,12 +114,12 @@
|
|||
</nuxt-link>
|
||||
|
||||
<div class="w-full h-12 px-1 py-2 border-t border-black border-opacity-20 absolute left-0" :style="{ bottom: streamLibraryItem ? '240px' : '65px' }">
|
||||
<p class="underline font-mono text-xs text-center text-gray-300 leading-3 mb-1" @click="clickChangelog">v{{ $config.version }}</p>
|
||||
<a v-if="hasUpdate" :href="githubTagUrl" target="_blank" class="text-warning text-xxs text-center block leading-3">Update</a>
|
||||
<p v-else class="text-xxs text-gray-400 leading-3 text-center italic">{{ Source }}</p>
|
||||
</div>
|
||||
<p class="underline font-mono text-xs text-center text-gray-300 leading-3 mb-1" @click="clickChangelog">v{{ $config.public.version }}</p>
|
||||
<a v-if="hasUpdate" :href="githubTagUrl" target="_blank" class="text-warning text-xxs text-center block leading-3">Update</a>
|
||||
<p v-else class="text-xxs text-gray-400 leading-3 text-center italic">{{ Source }}</p>
|
||||
</div>
|
||||
|
||||
<modals-changelog-view-modal v-model="showChangelogModal" :changelog="currentVersionChangelog" :currentVersion="$config.version" />
|
||||
<modals-changelog-view-modal v-model="showChangelogModal" :changelog="currentVersionChangelog" :currentVersion="$config.public.version" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ export default {
|
|||
if (!this.imagePath) return null
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
// Testing
|
||||
return `http://localhost:3333${this.$config.routerBasePath}/api/authors/${this.authorId}/image?token=${this.userToken}&ts=${this.updatedAt}`
|
||||
return `http://localhost:3333${this.$config.public.routerBasePath}/api/authors/${this.authorId}/image?token=${this.userToken}&ts=${this.updatedAt}`
|
||||
}
|
||||
return `/api/authors/${this.authorId}/image?token=${this.userToken}&ts=${this.updatedAt}`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ export default {
|
|||
return this.item.coverPath
|
||||
},
|
||||
coverUrl() {
|
||||
if (!this.coverPath) return `${this.$config.routerBasePath}/book_placeholder.jpg`
|
||||
if (!this.coverPath) return `${this.$config.public.routerBasePath}/book_placeholder.jpg`
|
||||
return this.$store.getters['globals/getLibraryItemCoverSrcById'](this.libraryItemId)
|
||||
},
|
||||
bookCoverAspectRatio() {
|
||||
|
|
|
|||
|
|
@ -137,7 +137,7 @@ export default {
|
|||
slug: this.newFeedSlug,
|
||||
metadataDetails: this.metadataDetails
|
||||
}
|
||||
if (this.$isDev) payload.serverAddress = `http://localhost:3333${this.$config.routerBasePath}`
|
||||
if (this.$isDev) payload.serverAddress = `http://localhost:3333${this.$config.public.routerBasePath}`
|
||||
|
||||
console.log('Payload', payload)
|
||||
this.$axios
|
||||
|
|
|
|||
|
|
@ -1,26 +1,26 @@
|
|||
<template>
|
||||
<div class="h-full w-full">
|
||||
<div class="h-full flex items-center justify-center">
|
||||
<div style="width: 100px; max-width: 100px" class="h-full hidden sm:flex items-center overflow-x-hidden justify-center">
|
||||
<span v-if="hasPrev" class="material-icons text-white text-opacity-50 hover:text-opacity-80 cursor-pointer text-6xl" @mousedown.prevent @click="prev">chevron_left</span>
|
||||
<div class="w-full h-full">
|
||||
<div class="flex items-center justify-center h-full">
|
||||
<div style="width: 100px; max-width: 100px" class="items-center justify-center hidden h-full overflow-x-hidden sm:flex">
|
||||
<span v-if="hasPrev" class="text-6xl text-white text-opacity-50 cursor-pointer material-icons hover:text-opacity-80" @mousedown.prevent @click="prev">chevron_left</span>
|
||||
</div>
|
||||
<div id="frame" class="w-full" style="height: 80%">
|
||||
<div id="viewer"></div>
|
||||
</div>
|
||||
<div style="width: 100px; max-width: 100px" class="h-full hidden sm:flex items-center justify-center overflow-x-hidden">
|
||||
<span v-if="hasNext" class="material-icons text-white text-opacity-50 hover:text-opacity-80 cursor-pointer text-6xl" @mousedown.prevent @click="next">chevron_right</span>
|
||||
<div style="width: 100px; max-width: 100px" class="items-center justify-center hidden h-full overflow-x-hidden sm:flex">
|
||||
<span v-if="hasNext" class="text-6xl text-white text-opacity-50 cursor-pointer material-icons hover:text-opacity-80" @mousedown.prevent @click="next">chevron_right</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ePub from 'epubjs'
|
||||
import { Epub, Rendition, NavItem } from 'epubjs'
|
||||
|
||||
/**
|
||||
* @typedef {object} EpubReader
|
||||
* @property {ePub.Book} book
|
||||
* @property {ePub.Rendition} rendition
|
||||
* @property {Epub} book
|
||||
* @property {Rendition} rendition
|
||||
*/
|
||||
export default {
|
||||
props: {
|
||||
|
|
@ -33,9 +33,9 @@ export default {
|
|||
data() {
|
||||
return {
|
||||
windowWidth: 0,
|
||||
/** @type {ePub.Book} */
|
||||
/** @type {Epub} */
|
||||
book: null,
|
||||
/** @type {ePub.Rendition} */
|
||||
/** @type {Rendition} */
|
||||
rendition: null
|
||||
}
|
||||
},
|
||||
|
|
@ -50,7 +50,7 @@ export default {
|
|||
hasNext() {
|
||||
return !this.rendition?.location?.atEnd
|
||||
},
|
||||
/** @returns {Array<ePub.NavItem>} */
|
||||
/** @returns {Array<NavItem>} */
|
||||
chapters() {
|
||||
return this.book ? this.book.navigation.toc : []
|
||||
},
|
||||
|
|
@ -200,13 +200,13 @@ export default {
|
|||
/** @type {EpubReader} */
|
||||
const reader = this
|
||||
|
||||
/** @type {ePub.Book} */
|
||||
/** @type {Epub} */
|
||||
reader.book = new ePub(reader.url, {
|
||||
width: this.readerWidth,
|
||||
height: window.innerHeight - 50
|
||||
})
|
||||
|
||||
/** @type {ePub.Rendition} */
|
||||
/** @type {Rendition} */
|
||||
reader.rendition = reader.book.renderTo('viewer', {
|
||||
width: this.readerWidth,
|
||||
height: window.innerHeight * 0.8
|
||||
|
|
|
|||
|
|
@ -368,7 +368,7 @@ export default {
|
|||
initializeSocket() {
|
||||
this.socket = this.$nuxtSocket({
|
||||
name: process.env.NODE_ENV === 'development' ? 'dev' : 'prod',
|
||||
channel: this.$config.routerBasePath,
|
||||
channel: this.$config.public.routerBasePath,
|
||||
persist: 'main',
|
||||
teardown: false,
|
||||
transports: ['websocket'],
|
||||
|
|
|
|||
17
client/nitro.entry.js
Normal file
17
client/nitro.entry.js
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import '#internal/nitro/virtual/polyfill'
|
||||
|
||||
const nitroApp = useNitroApp()
|
||||
const listener = toNodeListener(nitroApp.h3App)
|
||||
const handler = listener
|
||||
{
|
||||
process.on(
|
||||
"unhandledRejection",
|
||||
(err) => console.error("[nitro] [dev] [unhandledRejection] " + err)
|
||||
);
|
||||
process.on(
|
||||
"uncaughtException",
|
||||
(err) => console.error("[nitro] [dev] [uncaughtException] " + err)
|
||||
);
|
||||
}
|
||||
|
||||
export { useRuntimeConfig, getRouteRules, handler, listener, useNitroApp };
|
||||
7
client/nitro.preset.js
Normal file
7
client/nitro.preset.js
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import { defineNitroPreset } from 'nitropack'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
export default defineNitroPreset({
|
||||
extends: 'node-server',
|
||||
entry: fileURLToPath(new URL('./nitro.entry.js', import.meta.url))
|
||||
})
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
const { defineNuxtConfig } = require('@nuxt/bridge')
|
||||
import { fromNodeMiddleware } from 'h3';
|
||||
const pkg = require('./package.json')
|
||||
|
||||
module.exports = ({ command }) => ({
|
||||
module.exports = defineNuxtConfig({
|
||||
// Disable server-side rendering: https://go.nuxtjs.dev/ssr-mode
|
||||
ssr: false,
|
||||
target: 'server',
|
||||
|
|
@ -11,9 +13,11 @@ module.exports = ({ command }) => ({
|
|||
},
|
||||
telemetry: false,
|
||||
|
||||
publicRuntimeConfig: {
|
||||
version: pkg.version,
|
||||
routerBasePath: process.env.ROUTER_BASE_PATH || ''
|
||||
runtimeConfig: {
|
||||
public: {
|
||||
version: pkg.version,
|
||||
routerBasePath: process.env.ROUTER_BASE_PATH || ''
|
||||
}
|
||||
},
|
||||
|
||||
// Global page headers: https://go.nuxtjs.dev/config-head
|
||||
|
|
@ -31,7 +35,7 @@ module.exports = ({ command }) => ({
|
|||
|
||||
router: {
|
||||
// We must specify `./` during build to support dynamic router base paths (https://github.com/nuxt/nuxt/issues/10088)
|
||||
base: command == 'build' ? './' : process.env.ROUTER_BASE_PATH || ''
|
||||
base: process.env.ROUTER_BASE_PATH || ''
|
||||
},
|
||||
|
||||
// Global CSS: https://go.nuxtjs.dev/config-css
|
||||
|
|
@ -59,31 +63,16 @@ module.exports = ({ command }) => ({
|
|||
buildModules: [
|
||||
// https://go.nuxtjs.dev/tailwindcss
|
||||
'@nuxtjs/tailwindcss',
|
||||
'@nuxtjs/pwa',
|
||||
'@nuxt/postcss8'
|
||||
'pwa-nuxt-bridge',
|
||||
],
|
||||
|
||||
// Modules: https://go.nuxtjs.dev/config-modules
|
||||
modules: [
|
||||
'nuxt-socket-io',
|
||||
'@nuxtjs/axios',
|
||||
'@nuxtjs/proxy',
|
||||
'@/modules/rewritePwaManifest.js'
|
||||
// '@/modules/rewritePwaManifest.js'
|
||||
],
|
||||
|
||||
proxy: {
|
||||
[`${process.env.ROUTER_BASE_PATH || ''}/dev/`]: {
|
||||
target: `http://localhost:3333${process.env.ROUTER_BASE_PATH || ''}`,
|
||||
pathRewrite: { [`^${process.env.ROUTER_BASE_PATH || ''}/dev/`]: process.env.ROUTER_BASE_PATH || '' }
|
||||
},
|
||||
[`${process.env.ROUTER_BASE_PATH || ''}/ebook/`]: {
|
||||
target: (process.env.NODE_ENV !== 'production' ? 'http://localhost:3333' : '') + `${process.env.ROUTER_BASE_PATH || ''}/`
|
||||
},
|
||||
[`${process.env.ROUTER_BASE_PATH || ''}/s/`]: {
|
||||
target: (process.env.NODE_ENV !== 'production' ? 'http://localhost:3333' : '') + `${process.env.ROUTER_BASE_PATH || ''}/`
|
||||
}
|
||||
},
|
||||
|
||||
io: {
|
||||
sockets: [{
|
||||
name: 'dev',
|
||||
|
|
@ -138,9 +127,11 @@ module.exports = ({ command }) => ({
|
|||
// Build Configuration: https://go.nuxtjs.dev/config-build
|
||||
build: {
|
||||
postcss: {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
postcssOptions: {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
|
|
@ -155,6 +146,34 @@ module.exports = ({ command }) => ({
|
|||
host: '0.0.0.0'
|
||||
},
|
||||
|
||||
bridge: {
|
||||
capi: false,
|
||||
scriptSetup: false
|
||||
},
|
||||
|
||||
nitro: {
|
||||
preset: './nitro.preset.js',
|
||||
devProxy: {
|
||||
[`${process.env.ROUTER_BASE_PATH || ''}/dev/`]: {
|
||||
target: `http://localhost:3333${process.env.ROUTER_BASE_PATH || ''}`,
|
||||
pathRewrite: { [`^${process.env.ROUTER_BASE_PATH || ''}/dev/`]: process.env.ROUTER_BASE_PATH || '' }
|
||||
},
|
||||
[`${process.env.ROUTER_BASE_PATH || ''}/ebook/`]: {
|
||||
target: (process.env.NODE_ENV !== 'production' ? 'http://localhost:3333' : '') + `${process.env.ROUTER_BASE_PATH || ''}/`
|
||||
},
|
||||
[`${process.env.ROUTER_BASE_PATH || ''}/s/`]: {
|
||||
target: (process.env.NODE_ENV !== 'production' ? 'http://localhost:3333' : '') + `${process.env.ROUTER_BASE_PATH || ''}/`
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
hooks: {
|
||||
// 'nitro:config': (config) => {
|
||||
// console.log('nitro:config', config)
|
||||
// process.exit(0)
|
||||
// },
|
||||
},
|
||||
|
||||
/**
|
||||
* Temporary workaround for @nuxt-community/tailwindcss-module.
|
||||
*
|
||||
|
|
|
|||
28061
client/package-lock.json
generated
28061
client/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -4,26 +4,26 @@
|
|||
"description": "Self-hosted audiobook and podcast client",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"dev": "nuxt",
|
||||
"dev2": "nuxt --hostname localhost --port 1337",
|
||||
"build": "nuxt build",
|
||||
"start": "nuxt start",
|
||||
"generate": "nuxt generate"
|
||||
"dev": "nuxi dev",
|
||||
"dev2": "nuxi dev --hostname localhost --port 1337",
|
||||
"build": "nuxi build",
|
||||
"start": "nuxi preview",
|
||||
"generate": "nuxi generate"
|
||||
},
|
||||
"author": "advplyr",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@nuxt/bridge": "npm:@nuxt/bridge-edge@^3.0.0-28051151.e88ad64",
|
||||
"@nuxtjs/axios": "^5.13.6",
|
||||
"@nuxtjs/proxy": "^2.1.0",
|
||||
"core-js": "^3.16.0",
|
||||
"cron-parser": "^4.7.1",
|
||||
"date-fns": "^2.25.0",
|
||||
"epubjs": "^0.3.88",
|
||||
"glob": "^10.2.2",
|
||||
"epubjs": "^0.5.0-alpha.0",
|
||||
"hls.js": "^1.0.7",
|
||||
"libarchive.js": "^1.3.0",
|
||||
"nuxt": "^2.15.8",
|
||||
"nuxt": "^2.16.3",
|
||||
"nuxt-socket-io": "^1.1.18",
|
||||
"pwa-nuxt-bridge": "^5.0.1",
|
||||
"trix": "^1.3.1",
|
||||
"v-click-outside": "^3.1.2",
|
||||
"vue-pdf": "^4.2.0",
|
||||
|
|
@ -31,10 +31,9 @@
|
|||
"vuedraggable": "^2.24.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nuxt/postcss8": "^1.1.3",
|
||||
"@nuxtjs/pwa": "^3.3.5",
|
||||
"@nuxtjs/tailwindcss": "^4.2.1",
|
||||
"@nuxtjs/tailwindcss": "^6.6.7",
|
||||
"autoprefixer": "^10.4.7",
|
||||
"minimatch": "^8.0.4",
|
||||
"postcss": "^8.3.6",
|
||||
"tailwindcss": "^3.1.4"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -415,7 +415,7 @@ export default {
|
|||
const audioEl = this.audioEl || document.createElement('audio')
|
||||
var src = audioTrack.contentUrl + `?token=${this.userToken}`
|
||||
if (this.$isDev) {
|
||||
src = `http://localhost:3333${this.$config.routerBasePath}${src}`
|
||||
src = `http://localhost:3333${this.$config.public.routerBasePath}${src}`
|
||||
}
|
||||
|
||||
audioEl.src = src
|
||||
|
|
|
|||
|
|
@ -18,8 +18,8 @@ export default function ({ $axios, store, $config }) {
|
|||
}
|
||||
|
||||
console.log($config)
|
||||
if ($config.routerBasePath) {
|
||||
config.url = `${$config.routerBasePath}${config.url}`
|
||||
if ($config.public.routerBasePath) {
|
||||
config.url = `${$config.public.routerBasePath}${config.url}`
|
||||
}
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ export default function ({ $config }) {
|
|||
const link = document.createElement('link')
|
||||
link.rel = 'icon'
|
||||
link.type = 'image/x-icon'
|
||||
link.href = `${$config.routerBasePath || ''}${faviconPath}`
|
||||
link.href = `${$config.public.routerBasePath || ''}${faviconPath}`
|
||||
|
||||
document.head.appendChild(link)
|
||||
}
|
||||
|
|
@ -182,5 +182,5 @@ export default ({ app, store }, inject) => {
|
|||
inject('eventBus', new Vue())
|
||||
inject('isDev', process.env.NODE_ENV !== 'production')
|
||||
|
||||
store.commit('setRouterBasePath', app.$config.routerBasePath)
|
||||
store.commit('setRouterBasePath', app.$config.public.routerBasePath)
|
||||
}
|
||||
|
|
|
|||
1
package-lock.json
generated
1
package-lock.json
generated
|
|
@ -13,6 +13,7 @@
|
|||
"express": "^4.17.1",
|
||||
"graceful-fs": "^4.2.10",
|
||||
"htmlparser2": "^8.0.1",
|
||||
"mime-types": "^2.1.35",
|
||||
"node-tone": "^1.0.1",
|
||||
"socket.io": "^4.5.4",
|
||||
"xml2js": "^0.5.0"
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@
|
|||
"express": "^4.17.1",
|
||||
"graceful-fs": "^4.2.10",
|
||||
"htmlparser2": "^8.0.1",
|
||||
"mime-types": "^2.1.35",
|
||||
"node-tone": "^1.0.1",
|
||||
"socket.io": "^4.5.4",
|
||||
"xml2js": "^0.5.0"
|
||||
|
|
@ -46,4 +47,4 @@
|
|||
"devDependencies": {
|
||||
"nodemon": "^2.0.20"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -139,7 +139,7 @@ class Server {
|
|||
Logger.info('=== Starting Server ===')
|
||||
await this.init()
|
||||
if (process.env.NODE_ENV !== 'development') {
|
||||
this.clientRouter.start()
|
||||
await this.clientRouter.start()
|
||||
}
|
||||
|
||||
const app = express()
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
const express = require('express')
|
||||
const mime = require('mime-types')
|
||||
const Path = require('path')
|
||||
const Logger = require('../Logger')
|
||||
|
||||
|
|
@ -14,18 +15,15 @@ class ClientRouter {
|
|||
|
||||
async init () {
|
||||
const clientDir = Path.join(this.appRoot, '/client')
|
||||
const { loadNuxt } = require(Path.resolve(clientDir, 'node_modules/nuxt'))
|
||||
this.client = await loadNuxt({ rootDir: clientDir, for: 'start' })
|
||||
const { handler } = await import(Path.join(clientDir, '.output/server/index.mjs'))
|
||||
this.client = handler
|
||||
}
|
||||
|
||||
async start () {
|
||||
Logger.info('[Client] Starting')
|
||||
await this.init()
|
||||
|
||||
this.router.use(this.client.render)
|
||||
|
||||
this.client.hook('error', (err) => Logger.error('[Client]', err))
|
||||
this.client.ready().then(() => Logger.info('[Client] Ready'))
|
||||
this.router.use('/', this.client)
|
||||
}
|
||||
|
||||
async stop () {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue