mirror of
https://github.com/logseq/logseq.git
synced 2026-05-17 09:22:21 +00:00
feat(build): replace gulp static pipeline with vite
This commit is contained in:
2
.github/workflows/build-desktop-release.yml
vendored
2
.github/workflows/build-desktop-release.yml
vendored
@@ -135,7 +135,7 @@ jobs:
|
||||
echo "ENABLE_FILE_SYNC_PRODUCTION=${{ github.event_name == 'schedule' || github.event.inputs.enable-file-sync-production == 'true' }}" >> $GITHUB_ENV
|
||||
|
||||
- name: Compile CLJS
|
||||
run: pnpm install --frozen-lockfile && gulp build && pnpm cljs:release-electron && pnpm vite:workers-build
|
||||
run: pnpm install --frozen-lockfile && pnpm static:build && pnpm cljs:release-electron && pnpm vite:workers-build
|
||||
env:
|
||||
LOGSEQ_SENTRY_DSN: ${{ secrets.LOGSEQ_SENTRY_DSN }}
|
||||
LOGSEQ_POSTHOG_TOKEN: ${{ secrets.LOGSEQ_POSTHOG_TOKEN }}
|
||||
|
||||
2
.github/workflows/clj-e2e.yml
vendored
2
.github/workflows/clj-e2e.yml
vendored
@@ -90,7 +90,7 @@ jobs:
|
||||
# NOTE: require the app to be build with DEV-RELEASE flag
|
||||
- name: Prepare E2E test build
|
||||
run: |
|
||||
pnpm gulp:build && clojure -M:cljs release app db-worker --config-merge "{:closure-defines {frontend.config/DEV-RELEASE true}}" --debug && pnpm vite:app-build && pnpm vite:workers-build
|
||||
pnpm static:build && clojure -M:cljs release app db-worker --config-merge "{:closure-defines {frontend.config/DEV-RELEASE true}}" --debug && pnpm vite:app-build && pnpm vite:workers-build
|
||||
|
||||
- name: Run e2e tests
|
||||
run: cd clj-e2e && timeout 30m bb dev
|
||||
|
||||
2
.github/workflows/clj-rtc-e2e.yml
vendored
2
.github/workflows/clj-rtc-e2e.yml
vendored
@@ -91,7 +91,7 @@ jobs:
|
||||
# NOTE: require the app to be build with DEV-RELEASE flag
|
||||
- name: Prepare E2E test build
|
||||
run: |
|
||||
pnpm gulp:build && clojure -M:cljs release app db-worker --config-merge "{:closure-defines {frontend.config/DEV-RELEASE true}}" --debug && pnpm vite:app-build && pnpm vite:workers-build
|
||||
pnpm static:build && clojure -M:cljs release app db-worker --config-merge "{:closure-defines {frontend.config/DEV-RELEASE true}}" --debug && pnpm vite:app-build && pnpm vite:workers-build
|
||||
|
||||
- name: Upload build artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
|
||||
2
.github/workflows/deploy-and-branch.yml
vendored
2
.github/workflows/deploy-and-branch.yml
vendored
@@ -55,7 +55,7 @@ jobs:
|
||||
|
||||
- name: Build web + workers
|
||||
run: |
|
||||
pnpm gulp:build
|
||||
pnpm static:build
|
||||
clojure -M:cljs release app db-worker \
|
||||
--config-merge '{:compiler-options {:source-map true :source-map-include-sources-content true :source-map-detail-level :symbols}}'
|
||||
pnpm vite:app-build
|
||||
|
||||
2
.github/workflows/deploy-db-test-pages.yml
vendored
2
.github/workflows/deploy-db-test-pages.yml
vendored
@@ -61,7 +61,7 @@ jobs:
|
||||
|
||||
- name: Build Released-Web
|
||||
run: |
|
||||
pnpm gulp:build && clojure -M:cljs release app db-worker --config-merge '{:compiler-options {:source-map true :source-map-include-sources-content true :source-map-detail-level :symbols}}' && pnpm vite:app-build && pnpm vite:workers-build
|
||||
pnpm static:build && clojure -M:cljs release app db-worker --config-merge '{:compiler-options {:source-map true :source-map-include-sources-content true :source-map-detail-level :symbols}}' && pnpm vite:app-build && pnpm vite:workers-build
|
||||
rsync -avz --exclude node_modules --exclude android --exclude ios --exclude mobile ./static/ ./public/
|
||||
ls -lR ./public && mkdir r2 && mv ./public/js/main.js.map ./r2/db-test.main.js.map
|
||||
sed -i 's/=main.js.map/=https:\/\/assets.logseq.io\/db-test.main.js.map/g' ./public/js/main.js
|
||||
|
||||
2
.github/workflows/deploy-sync-test.yml
vendored
2
.github/workflows/deploy-sync-test.yml
vendored
@@ -55,7 +55,7 @@ jobs:
|
||||
|
||||
- name: Build web + workers
|
||||
run: |
|
||||
pnpm gulp:build
|
||||
pnpm static:build
|
||||
clojure -M:cljs release app db-worker \
|
||||
--config-merge '{:compiler-options {:source-map true :source-map-include-sources-content true :source-map-detail-level :symbols}}'
|
||||
pnpm vite:app-build
|
||||
|
||||
317
gulpfile.js
317
gulpfile.js
@@ -1,317 +0,0 @@
|
||||
const fs = require('fs')
|
||||
const utils = require('util')
|
||||
const cp = require('child_process')
|
||||
const exec = utils.promisify(cp.exec)
|
||||
const path = require('path')
|
||||
const gulp = require('gulp')
|
||||
const replace = require('gulp-replace')
|
||||
|
||||
const outputPath = path.join(__dirname, 'static')
|
||||
const outputJsPath = path.join(outputPath, 'js')
|
||||
const resourcesPath = path.join(__dirname, 'resources')
|
||||
const publicRootPath = path.join(__dirname, 'public')
|
||||
const mobilePath = path.join(outputPath, 'mobile')
|
||||
const mobileJsPath = path.join(mobilePath, 'js')
|
||||
const sourcePath = path.join(__dirname, 'src/main/frontend')
|
||||
const resourceFilePath = path.join(resourcesPath, '**')
|
||||
const outputFilePath = path.join(outputPath, '**')
|
||||
const rawCopySrc = (globs, options = {}) =>
|
||||
gulp.src(globs, { encoding: false, ...options })
|
||||
const staticCleanKeep = new Set([
|
||||
'entitlements.plist',
|
||||
'node_modules',
|
||||
'package.json',
|
||||
'pnpm-lock.yaml',
|
||||
])
|
||||
|
||||
const css = {
|
||||
watchCSS () {
|
||||
return cp.spawn(`pnpm css:watch`, {
|
||||
shell: true,
|
||||
stdio: 'inherit',
|
||||
})
|
||||
},
|
||||
|
||||
watchMobileCSS () {
|
||||
return cp.spawn(`pnpm css:mobile-watch`, {
|
||||
shell: true,
|
||||
stdio: 'inherit',
|
||||
})
|
||||
},
|
||||
|
||||
buildCSS (...params) {
|
||||
return gulp.series(
|
||||
() => exec(`pnpm css:build`, {}),
|
||||
css._optimizeCSSForRelease,
|
||||
)(...params)
|
||||
},
|
||||
|
||||
buildMobileCSS (...params) {
|
||||
return gulp.series(
|
||||
() => exec(`pnpm css:mobile-build`, {}),
|
||||
)(...params)
|
||||
},
|
||||
|
||||
_optimizeCSSForRelease () {
|
||||
return gulp.src(path.join(outputPath, 'css', 'style.css')).
|
||||
pipe(gulp.dest(path.join(outputPath, 'css')))
|
||||
},
|
||||
}
|
||||
|
||||
const common = {
|
||||
clean () {
|
||||
if (!fs.existsSync(outputPath)) {
|
||||
fs.mkdirSync(outputPath, { recursive: true })
|
||||
}
|
||||
|
||||
for (const entry of fs.readdirSync(outputPath)) {
|
||||
if (staticCleanKeep.has(entry)) continue
|
||||
fs.rmSync(path.join(outputPath, entry), {
|
||||
recursive: true,
|
||||
force: true,
|
||||
maxRetries: 10,
|
||||
retryDelay: 100,
|
||||
})
|
||||
}
|
||||
return Promise.resolve()
|
||||
},
|
||||
|
||||
syncResourceFile () {
|
||||
return rawCopySrc(resourceFilePath).pipe(gulp.dest(outputPath))
|
||||
},
|
||||
|
||||
// NOTE: All assets from node_modules are copied to the output directory
|
||||
syncAssetFiles (...params) {
|
||||
return gulp.series(
|
||||
() => rawCopySrc([
|
||||
'node_modules/katex/dist/katex.min.js',
|
||||
'node_modules/katex/dist/contrib/mhchem.min.js',
|
||||
'node_modules/html2canvas/dist/html2canvas.min.js',
|
||||
'node_modules/interactjs/dist/interact.min.js',
|
||||
'node_modules/photoswipe/dist/umd/*.js',
|
||||
'node_modules/marked/lib/marked.umd.js',
|
||||
'node_modules/@highlightjs/cdn-assets/highlight.min.js',
|
||||
'node_modules/@isomorphic-git/lightning-fs/dist/lightning-fs.min.js',
|
||||
'packages/ui/dist/ui.js',
|
||||
'node_modules/@sqlite.org/sqlite-wasm/dist/sqlite3.wasm',
|
||||
'node_modules/react/umd/react.production.min.js',
|
||||
'node_modules/react/umd/react.development.js',
|
||||
'node_modules/react-dom/umd/react-dom.production.min.js',
|
||||
'node_modules/react-dom/umd/react-dom.development.js',
|
||||
'node_modules/prop-types/prop-types.min.js',
|
||||
'node_modules/dompurify/dist/purify.js',
|
||||
]).pipe(gulp.dest(path.join(outputPath, 'js'))),
|
||||
() => gulp.src([
|
||||
'node_modules/@tabler/icons-react/dist/umd/tabler-icons-react.min.js',
|
||||
]).
|
||||
pipe(replace('"@tabler/icons-react"]={},a.react,',
|
||||
'"tablerIcons"]={},a.React,')).
|
||||
pipe(gulp.dest(path.join(outputPath, 'js'))),
|
||||
() => rawCopySrc([
|
||||
'node_modules/@glidejs/glide/dist/glide.min.js',
|
||||
'node_modules/@glidejs/glide/dist/css/glide.core.min.css',
|
||||
'node_modules/@glidejs/glide/dist/css/glide.theme.min.css',
|
||||
]).pipe(gulp.dest(path.join(outputPath, 'js', 'glide'))),
|
||||
() => rawCopySrc([
|
||||
'node_modules/pdfjs-dist/legacy/build/pdf.mjs',
|
||||
'node_modules/pdfjs-dist/legacy/build/pdf.worker.mjs',
|
||||
'node_modules/pdfjs-dist/legacy/web/pdf_viewer.mjs',
|
||||
]).pipe(gulp.dest(path.join(outputPath, 'js', 'pdfjs'))),
|
||||
() => rawCopySrc([
|
||||
'node_modules/pdfjs-dist/cmaps/*.*',
|
||||
]).pipe(gulp.dest(path.join(outputPath, 'js', 'pdfjs', 'cmaps'))),
|
||||
() => rawCopySrc([
|
||||
'node_modules/inter-ui/inter.css',
|
||||
]).pipe(gulp.dest(path.join(outputPath, 'css'))),
|
||||
() => rawCopySrc('node_modules/inter-ui/web/*.*').
|
||||
pipe(gulp.dest(path.join(outputPath, 'css', 'web'))),
|
||||
() => rawCopySrc([
|
||||
'node_modules/katex/dist/fonts/*.woff2',
|
||||
]).pipe(gulp.dest(path.join(outputPath, 'css', 'fonts'))),
|
||||
() => rawCopySrc([
|
||||
'node_modules/katex/dist/katex.min.js',
|
||||
'node_modules/katex/dist/contrib/mhchem.min.js',
|
||||
'node_modules/marked/lib/marked.umd.js',
|
||||
'node_modules/@highlightjs/cdn-assets/highlight.min.js',
|
||||
'node_modules/@isomorphic-git/lightning-fs/dist/lightning-fs.min.js',
|
||||
'node_modules/react/umd/react.production.min.js',
|
||||
'node_modules/react/umd/react.development.js',
|
||||
'node_modules/react-dom/umd/react-dom.production.min.js',
|
||||
'node_modules/react-dom/umd/react-dom.development.js',
|
||||
'node_modules/prop-types/prop-types.min.js',
|
||||
'node_modules/interactjs/dist/interact.min.js',
|
||||
'node_modules/photoswipe/dist/umd/*.js',
|
||||
'packages/ui/dist/ui.js',
|
||||
'node_modules/@sqlite.org/sqlite-wasm/dist/sqlite3.wasm',
|
||||
]).pipe(gulp.dest(path.join(outputPath, 'mobile', 'js'))),
|
||||
() => rawCopySrc([
|
||||
'node_modules/inter-ui/inter.css',
|
||||
]).pipe(gulp.dest(path.join(outputPath, 'mobile', 'css'))),
|
||||
() => rawCopySrc('node_modules/inter-ui/web/*.*').
|
||||
pipe(gulp.dest(path.join(outputPath, 'mobile', 'css', 'web'))),
|
||||
() => rawCopySrc([
|
||||
'node_modules/katex/dist/fonts/*.woff2',
|
||||
]).pipe(gulp.dest(path.join(outputPath, 'mobile', 'css', 'fonts'))),
|
||||
)(...params)
|
||||
},
|
||||
|
||||
keepSyncResourceFile () {
|
||||
return gulp.watch(resourceFilePath, { ignoreInitial: true },
|
||||
common.syncResourceFile)
|
||||
},
|
||||
|
||||
syncAllStatic () {
|
||||
return rawCopySrc([
|
||||
outputFilePath,
|
||||
'!' + path.join(outputPath, 'node_modules/**'),
|
||||
'!' + path.join(outputPath, 'mobile/**'),
|
||||
'!' + path.join(outputPath, 'android/**'),
|
||||
'!' + path.join(outputPath, 'ios/**'),
|
||||
]).pipe(gulp.dest(publicRootPath))
|
||||
},
|
||||
|
||||
syncJS_CSSinRt () {
|
||||
return gulp.src([
|
||||
path.join(outputPath, 'js/**'),
|
||||
path.join(outputPath, 'css/**'),
|
||||
], { base: outputPath }).pipe(gulp.dest(publicRootPath))
|
||||
},
|
||||
|
||||
keepSyncStaticInRt () {
|
||||
return gulp.watch([
|
||||
path.join(outputPath, 'js/**'),
|
||||
path.join(outputPath, 'css/**'),
|
||||
], { ignoreInitial: true }, common.syncJS_CSSinRt)
|
||||
},
|
||||
|
||||
syncWorkersToMobile () {
|
||||
return gulp.src([
|
||||
path.join(outputPath, 'js/db-worker.js'),
|
||||
], { base: outputJsPath }).pipe(gulp.dest(mobileJsPath))
|
||||
},
|
||||
|
||||
keepSyncWorkersToMobile () {
|
||||
return gulp.watch([
|
||||
path.join(outputPath, 'js/db-worker.js'),
|
||||
], { ignoreInitial: false }, common.syncWorkersToMobile)
|
||||
},
|
||||
|
||||
async runCapWithLocalDevServerEntry (cb) {
|
||||
const mode = process.env.PLATFORM || 'ios'
|
||||
|
||||
const LOGSEQ_APP_SERVER_URL = `http://localhost:3002`
|
||||
|
||||
if (typeof global.fetch === 'function') {
|
||||
try {
|
||||
await fetch(LOGSEQ_APP_SERVER_URL)
|
||||
} catch (e) {
|
||||
return cb(new Error(
|
||||
`/* ❌ Please check if the service is ON. (${LOGSEQ_APP_SERVER_URL}) ❌ */`))
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`------ Cap ${mode.toUpperCase()} -----`)
|
||||
console.log(`Dev serve at: ${LOGSEQ_APP_SERVER_URL}`)
|
||||
console.log(`--------------------------------------`)
|
||||
|
||||
cp.execSync(`pnpm exec cap sync ${mode}`, {
|
||||
stdio: 'inherit',
|
||||
env: Object.assign(process.env, {
|
||||
LOGSEQ_APP_SERVER_URL,
|
||||
}),
|
||||
})
|
||||
|
||||
cp.execSync(`rm -rf ios/App/App/public/out`, {
|
||||
stdio: 'inherit',
|
||||
})
|
||||
|
||||
cp.execSync(`pnpm exec cap run ${mode}`, {
|
||||
stdio: 'inherit',
|
||||
env: Object.assign(process.env, {
|
||||
LOGSEQ_APP_SERVER_URL,
|
||||
}),
|
||||
})
|
||||
|
||||
cb()
|
||||
},
|
||||
|
||||
switchReactDevelopmentMode (cb) {
|
||||
try {
|
||||
const reactFrom = path.join(outputPath, 'js', 'react.development.js')
|
||||
const reactTo = path.join(outputPath, 'js', 'react.production.min.js')
|
||||
fs.renameSync(reactFrom, reactTo)
|
||||
|
||||
const reactDomFrom = path.join(outputPath, 'js',
|
||||
'react-dom.development.js')
|
||||
const reactDomTo = path.join(outputPath, 'js',
|
||||
'react-dom.production.min.js')
|
||||
fs.renameSync(reactDomFrom, reactDomTo)
|
||||
|
||||
cb()
|
||||
} catch (err) {
|
||||
console.error('Error during switchReactDevelopmentMode:', err)
|
||||
cb(err)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
exports.electron = () => {
|
||||
if (!fs.existsSync(path.join(outputPath, 'node_modules'))) {
|
||||
cp.execSync('pnpm install --frozen-lockfile', {
|
||||
cwd: outputPath,
|
||||
stdio: 'inherit',
|
||||
})
|
||||
}
|
||||
|
||||
cp.execSync('pnpm electron:dev', {
|
||||
cwd: outputPath,
|
||||
stdio: 'inherit',
|
||||
})
|
||||
}
|
||||
|
||||
exports.electronMaker = async () => {
|
||||
cp.execSync('pnpm cljs:release-electron', {
|
||||
stdio: 'inherit',
|
||||
})
|
||||
|
||||
const pkgPath = path.join(outputPath, 'package.json')
|
||||
const pkg = require(pkgPath)
|
||||
const version = fs.readFileSync(
|
||||
path.join(__dirname, 'src/main/frontend/version.cljs')).
|
||||
toString().
|
||||
match(/[0-9.]{3,}/)[0]
|
||||
|
||||
if (!version) {
|
||||
throw new Error('release version error in src/**/*/version.cljs')
|
||||
}
|
||||
|
||||
pkg.version = version
|
||||
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2))
|
||||
|
||||
if (!fs.existsSync(path.join(outputPath, 'node_modules'))) {
|
||||
cp.execSync('pnpm install --frozen-lockfile', {
|
||||
cwd: outputPath,
|
||||
stdio: 'inherit',
|
||||
})
|
||||
}
|
||||
|
||||
cp.execSync('pnpm electron:make', {
|
||||
cwd: outputPath,
|
||||
stdio: 'inherit',
|
||||
})
|
||||
}
|
||||
|
||||
exports.cap = common.runCapWithLocalDevServerEntry
|
||||
exports.clean = common.clean
|
||||
exports.watch = gulp.series(
|
||||
common.syncResourceFile,
|
||||
common.syncAssetFiles, common.switchReactDevelopmentMode,
|
||||
gulp.parallel(common.keepSyncResourceFile, css.watchCSS))
|
||||
exports.watchMobile = gulp.series(
|
||||
common.syncResourceFile, common.syncAssetFiles,
|
||||
gulp.parallel(common.keepSyncResourceFile, common.keepSyncWorkersToMobile, css.watchMobileCSS))
|
||||
exports.build = gulp.series(common.clean, common.syncResourceFile,
|
||||
common.syncAssetFiles, css.buildCSS)
|
||||
exports.buildMobile = gulp.series(common.clean, common.syncResourceFile,
|
||||
common.syncAssetFiles, css.buildMobileCSS)
|
||||
exports.syncWorkersToMobile = common.syncWorkersToMobile
|
||||
61
package.json
61
package.json
@@ -19,13 +19,9 @@
|
||||
"better-sqlite3": "^12.8.0",
|
||||
"cross-env": "^10.1.0",
|
||||
"cssnano": "^7.1.3",
|
||||
"gulp": "^5.0.1",
|
||||
"gulp-postcss": "^10.0.0",
|
||||
"gulp-replace": "^1.1.4",
|
||||
"npm-run-all2": "^8.0.4",
|
||||
"playwright": "=1.58.2",
|
||||
"postcss": "^8.5.8",
|
||||
"postcss-cli": "11.0.1",
|
||||
"postcss-import": "16.1.1",
|
||||
"postcss-import-ext-glob": "2.1.1",
|
||||
"postcss-nested": "7.0.2",
|
||||
@@ -38,22 +34,30 @@
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"typescript": "^5.9.3",
|
||||
"vite": "^8.0.9",
|
||||
"vite-plugin-static-copy": "4.1.0",
|
||||
"ws": "^8.19.0"
|
||||
},
|
||||
"scripts": {
|
||||
"watch": "run-p gulp:watch cljs:watch vite:app-watch vite:workers-watch",
|
||||
"electron-watch": "run-p gulp:watch cljs:electron-watch vite:app-watch vite:workers-watch",
|
||||
"app-watch": "run-p gulp:watch cljs:app-watch vite:app-watch vite:workers-watch",
|
||||
"mobile-watch": "run-p gulp:mobile-watch cljs:mobile-watch vite:mobile-watch vite:workers-mobile-watch",
|
||||
"watch": "run-p static:watch cljs:watch vite:app-watch vite:workers-watch",
|
||||
"electron-watch": "run-p static:watch cljs:electron-watch vite:app-watch vite:workers-watch",
|
||||
"app-watch": "run-p static:watch cljs:app-watch vite:app-watch vite:workers-watch",
|
||||
"mobile-watch": "run-p static:mobile-watch cljs:mobile-watch vite:mobile-watch vite:workers-mobile-watch",
|
||||
"publishing-watch": "run-p cljs:publishing-watch vite:publishing-watch",
|
||||
"dev": "run-p gulp:watch gulp:mobile-watch cljs:dev-watch vite:app-watch vite:mobile-watch vite:workers-watch vite:workers-mobile-watch",
|
||||
"release": "run-s gulp:build cljs:release vite:workers-build",
|
||||
"release-app": "run-s gulp:build cljs:release-app vite:workers-build",
|
||||
"release-mobile": "run-s gulp:buildMobile cljs:release-mobile gulp:syncWorkersToMobile vite:workers-mobile-build",
|
||||
"dev-release-app": "run-s gulp:build cljs:dev-release-app vite:workers-build",
|
||||
"dev-electron-app": "gulp electron",
|
||||
"release-electron": "run-s gulp:build && pnpm vite:workers-build && gulp electronMaker",
|
||||
"dev": "run-p vite:assets-watch:dev vite:css-watch vite:css-mobile-watch cljs:dev-watch vite:app-watch vite:mobile-watch vite:workers-watch vite:workers-mobile-watch",
|
||||
"release": "run-s static:build cljs:release vite:workers-build",
|
||||
"release-app": "run-s static:build cljs:release-app vite:workers-build",
|
||||
"release-mobile": "run-s static:mobile-build cljs:release-mobile static:sync-workers-to-mobile vite:workers-mobile-build",
|
||||
"dev-release-app": "run-s static:build cljs:dev-release-app vite:workers-build",
|
||||
"dev-electron-app": "node scripts/static-pipeline.mjs electron",
|
||||
"release-electron": "run-s static:build electron:make",
|
||||
"debug-electron": "cd static/ && pnpm electron:debug",
|
||||
"vite:assets-watch": "pnpm exec vite build --watch --config vite.config.assets.ts",
|
||||
"vite:assets-watch:dev": "cross-env LOGSEQ_REACT_DEV_ASSETS=true pnpm exec vite build --watch --config vite.config.assets.ts",
|
||||
"vite:assets-build": "cross-env NODE_ENV=production pnpm exec vite build --config vite.config.assets.ts",
|
||||
"vite:css-watch": "cross-env TAILWIND_MODE=watch LOGSEQ_CSS_TARGET=desktop pnpm exec vite build --watch --config vite.config.css.ts",
|
||||
"vite:css-build": "cross-env NODE_ENV=production LOGSEQ_CSS_TARGET=desktop pnpm exec vite build --config vite.config.css.ts",
|
||||
"vite:css-mobile-watch": "cross-env TAILWIND_MODE=watch LOGSEQ_CSS_TARGET=mobile pnpm exec vite build --watch --config vite.config.css.ts",
|
||||
"vite:css-mobile-build": "cross-env NODE_ENV=production LOGSEQ_CSS_TARGET=mobile pnpm exec vite build --config vite.config.css.ts",
|
||||
"vite:workers-watch": "run-s vite:prepare vite:workers-watch:bridge",
|
||||
"vite:workers-watch:bridge": "pnpm exec vite build --watch --config vite.config.workers.ts",
|
||||
"vite:workers-build": "run-s vite:prepare vite:workers-build:bridge",
|
||||
@@ -77,21 +81,22 @@
|
||||
"vite:publishing-build:bridge": "cross-env NODE_ENV=production pnpm exec vite build --config vite.config.publishing.ts",
|
||||
"sync-android-release": "pnpm clean && pnpm release-mobile && rm -rf ./static/mobile/**/*.map && pnpm exec cap sync android",
|
||||
"sync-ios-release": "pnpm clean && pnpm release-mobile && rm -rf ./static/mobile/**/*.map && pnpm exec cap sync ios",
|
||||
"clean": "gulp clean",
|
||||
"clean": "node scripts/static-pipeline.mjs clean",
|
||||
"test": "run-s cljs:test cljs:run-test",
|
||||
"test:node-adapter": "pnpm --dir deps/db-sync run test:node-adapter",
|
||||
"report": "run-s cljs:report",
|
||||
"style:lint": "stylelint \"src/**/*.css\"",
|
||||
"gulp:watch": "gulp watch",
|
||||
"gulp:build": "cross-env NODE_ENV=production gulp build",
|
||||
"gulp:buildMobile": "cross-env NODE_ENV=production gulp buildMobile",
|
||||
"gulp:syncWorkersToMobile": "gulp syncWorkersToMobile",
|
||||
"css:build": "postcss tailwind.all.css -o static/css/style.css --verbose --env production",
|
||||
"css:watch": "cross-env TAILWIND_MODE=watch postcss tailwind.all.css -o static/css/style.css --verbose --watch",
|
||||
"static:watch": "run-p vite:assets-watch:dev vite:css-watch",
|
||||
"static:mobile-watch": "run-p vite:assets-watch vite:css-mobile-watch",
|
||||
"static:build": "run-s clean vite:assets-build vite:css-build",
|
||||
"static:mobile-build": "run-s clean vite:assets-build vite:css-mobile-build",
|
||||
"static:sync-workers-to-mobile": "node scripts/static-pipeline.mjs sync-workers-to-mobile",
|
||||
"electron:make": "node scripts/static-pipeline.mjs electron-maker",
|
||||
"css:build": "pnpm vite:css-build",
|
||||
"css:watch": "pnpm vite:css-watch",
|
||||
"cljs:watch": "clojure -M:cljs watch app db-worker electron",
|
||||
"gulp:mobile-watch": "gulp watchMobile",
|
||||
"css:mobile-build": "postcss tailwind.mobile.css -o static/mobile/css/style.css --verbose --env production",
|
||||
"css:mobile-watch": "cross-env TAILWIND_MODE=watch postcss tailwind.mobile.css -o static/mobile/css/style.css --verbose --watch",
|
||||
"css:mobile-build": "pnpm vite:css-mobile-build",
|
||||
"css:mobile-watch": "pnpm vite:css-mobile-watch",
|
||||
"cljs:mobile-watch": "clojure -M:cljs watch mobile db-worker --config-merge \"{:asset-path \\\"/static/mobile/js\\\" :release {:asset-path \\\"http://localhost\\\"}}\"",
|
||||
"cljs:release-mobile": "run-s cljs:release-mobile:shadow vite:mobile-build",
|
||||
"cljs:release-mobile:shadow": "clojure -M:cljs release mobile db-worker --config-merge \"{:asset-path \\\"/static/mobile/js\\\" :release {:asset-path \\\"http://localhost\\\"}}\"",
|
||||
@@ -120,8 +125,8 @@
|
||||
"cljs:report": "clojure -M:cljs run shadow.cljs.build-report app db-worker report.html",
|
||||
"cljs:build-electron": "clojure -A:cljs compile app db-worker electron",
|
||||
"cljs:lint": "clojure -M:clj-kondo --parallel --lint src --cache false",
|
||||
"ios:dev": "cross-env PLATFORM=ios gulp cap",
|
||||
"android:dev": "cross-env PLATFORM=android gulp cap",
|
||||
"ios:dev": "cross-env PLATFORM=ios node scripts/static-pipeline.mjs cap",
|
||||
"android:dev": "cross-env PLATFORM=android node scripts/static-pipeline.mjs cap",
|
||||
"ui:build": "pnpm --dir packages/ui install",
|
||||
"postinstall": "pnpm ui:build"
|
||||
},
|
||||
@@ -194,9 +199,9 @@
|
||||
"react-transition-group": "4.4.5",
|
||||
"react-virtuoso": "4.18.3",
|
||||
"remove-accents": "0.5.0",
|
||||
"tiny-pinyin": "1.3.2",
|
||||
"sanitize-filename": "1.6.4",
|
||||
"send-intent": "^7.0.0",
|
||||
"tiny-pinyin": "1.3.2",
|
||||
"url": "^0.11.4",
|
||||
"util": "^0.12.5"
|
||||
},
|
||||
|
||||
879
pnpm-lock.yaml
generated
879
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,10 +0,0 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
'autoprefixer': {},
|
||||
'postcss-import-ext-glob': {},
|
||||
'postcss-import': {},
|
||||
'tailwindcss/nesting': 'postcss-nested',
|
||||
tailwindcss: {},
|
||||
...(process.env.NODE_ENV === 'production' ? { cssnano: {} } : {})
|
||||
}
|
||||
}
|
||||
158
scripts/static-pipeline.mjs
Normal file
158
scripts/static-pipeline.mjs
Normal file
@@ -0,0 +1,158 @@
|
||||
import { execFileSync } from 'node:child_process'
|
||||
import {
|
||||
copyFileSync,
|
||||
existsSync,
|
||||
mkdirSync,
|
||||
readFileSync,
|
||||
readdirSync,
|
||||
renameSync,
|
||||
rmSync,
|
||||
writeFileSync,
|
||||
} from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
|
||||
const staticDir = path.join(rootDir, 'static')
|
||||
|
||||
const staticCleanKeep = new Set([
|
||||
'entitlements.plist',
|
||||
'node_modules',
|
||||
'package.json',
|
||||
'pnpm-lock.yaml',
|
||||
])
|
||||
|
||||
function run(command, args, options = {}) {
|
||||
execFileSync(command, args, {
|
||||
cwd: options.cwd ?? rootDir,
|
||||
env: { ...process.env, ...(options.env ?? {}) },
|
||||
shell: process.platform === 'win32',
|
||||
stdio: 'inherit',
|
||||
})
|
||||
}
|
||||
|
||||
function clean() {
|
||||
mkdirSync(staticDir, { recursive: true })
|
||||
|
||||
for (const entry of readdirSync(staticDir)) {
|
||||
if (staticCleanKeep.has(entry)) {
|
||||
continue
|
||||
}
|
||||
|
||||
rmSync(path.join(staticDir, entry), {
|
||||
recursive: true,
|
||||
force: true,
|
||||
maxRetries: 10,
|
||||
retryDelay: 100,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function ensureStaticNodeModules() {
|
||||
if (!existsSync(path.join(staticDir, 'node_modules'))) {
|
||||
run('pnpm', ['install', '--frozen-lockfile'], { cwd: staticDir })
|
||||
}
|
||||
}
|
||||
|
||||
function electron() {
|
||||
ensureStaticNodeModules()
|
||||
run('pnpm', ['electron:dev'], { cwd: staticDir })
|
||||
}
|
||||
|
||||
function versionFromSource() {
|
||||
const versionSource = readFileSync(path.join(rootDir, 'src/main/frontend/version.cljs'), 'utf8')
|
||||
const match = versionSource.match(/[0-9.]{3,}/)
|
||||
|
||||
if (!match) {
|
||||
throw new Error('release version error in src/**/*/version.cljs')
|
||||
}
|
||||
|
||||
return match[0]
|
||||
}
|
||||
|
||||
function electronMaker() {
|
||||
run('pnpm', ['cljs:release-electron'])
|
||||
run('pnpm', ['vite:workers-build'])
|
||||
|
||||
const packagePath = path.join(staticDir, 'package.json')
|
||||
const packageJson = JSON.parse(readFileSync(packagePath, 'utf8'))
|
||||
packageJson.version = versionFromSource()
|
||||
writeFileSync(packagePath, `${JSON.stringify(packageJson, null, 2)}\n`)
|
||||
|
||||
ensureStaticNodeModules()
|
||||
run('pnpm', ['electron:make'], { cwd: staticDir })
|
||||
}
|
||||
|
||||
function syncWorkersToMobile() {
|
||||
const source = path.join(staticDir, 'js', 'db-worker.js')
|
||||
const dest = path.join(staticDir, 'mobile', 'js', 'db-worker.js')
|
||||
|
||||
if (!existsSync(source)) {
|
||||
throw new Error(`Missing worker output: ${source}`)
|
||||
}
|
||||
|
||||
mkdirSync(path.dirname(dest), { recursive: true })
|
||||
copyFileSync(source, dest)
|
||||
}
|
||||
|
||||
function switchReactDevelopmentMode() {
|
||||
const pairs = [
|
||||
['react.development.js', 'react.production.min.js'],
|
||||
['react-dom.development.js', 'react-dom.production.min.js'],
|
||||
]
|
||||
|
||||
for (const [fromName, toName] of pairs) {
|
||||
const from = path.join(staticDir, 'js', fromName)
|
||||
const to = path.join(staticDir, 'js', toName)
|
||||
|
||||
if (!existsSync(from)) {
|
||||
continue
|
||||
}
|
||||
|
||||
rmSync(to, { force: true })
|
||||
renameSync(from, to)
|
||||
}
|
||||
}
|
||||
|
||||
async function cap() {
|
||||
const mode = process.env.PLATFORM || 'ios'
|
||||
const logseqAppServerUrl = 'http://localhost:3002'
|
||||
|
||||
if (typeof global.fetch === 'function') {
|
||||
try {
|
||||
await fetch(logseqAppServerUrl)
|
||||
} catch {
|
||||
throw new Error(`Please check if the service is ON. (${logseqAppServerUrl})`)
|
||||
}
|
||||
}
|
||||
|
||||
process.stdout.write(`------ Cap ${mode.toUpperCase()} -----\n`)
|
||||
process.stdout.write(`Dev serve at: ${logseqAppServerUrl}\n`)
|
||||
process.stdout.write('--------------------------------------\n')
|
||||
|
||||
run('pnpm', ['exec', 'cap', 'sync', mode], {
|
||||
env: { LOGSEQ_APP_SERVER_URL: logseqAppServerUrl },
|
||||
})
|
||||
rmSync(path.join(rootDir, 'ios/App/App/public/out'), { recursive: true, force: true })
|
||||
run('pnpm', ['exec', 'cap', 'run', mode], {
|
||||
env: { LOGSEQ_APP_SERVER_URL: logseqAppServerUrl },
|
||||
})
|
||||
}
|
||||
|
||||
const tasks = {
|
||||
cap,
|
||||
clean,
|
||||
electron,
|
||||
'electron-maker': electronMaker,
|
||||
'switch-react-development-mode': switchReactDevelopmentMode,
|
||||
'sync-workers-to-mobile': syncWorkersToMobile,
|
||||
}
|
||||
|
||||
const taskName = process.argv[2]
|
||||
const task = tasks[taskName]
|
||||
|
||||
if (!task) {
|
||||
throw new Error(`Unknown static pipeline task: ${taskName}`)
|
||||
}
|
||||
|
||||
await task()
|
||||
164
vite.config.assets.ts
Normal file
164
vite.config.assets.ts
Normal file
@@ -0,0 +1,164 @@
|
||||
import { normalizePath, type Plugin, defineConfig } from 'vite'
|
||||
import { viteStaticCopy, type Target } from 'vite-plugin-static-copy'
|
||||
|
||||
const useReactDevelopmentAssets = process.env.LOGSEQ_REACT_DEV_ASSETS === 'true'
|
||||
|
||||
const virtualEntryId = 'virtual:logseq-assets-entry'
|
||||
const resolvedVirtualEntryId = `\0${virtualEntryId}`
|
||||
|
||||
function virtualAssetsEntryPlugin(): Plugin {
|
||||
return {
|
||||
name: 'logseq-assets-entry',
|
||||
resolveId(id) {
|
||||
if (id === virtualEntryId) {
|
||||
return resolvedVirtualEntryId
|
||||
}
|
||||
|
||||
return null
|
||||
},
|
||||
load(id) {
|
||||
if (id === resolvedVirtualEntryId) {
|
||||
return 'export {}\n'
|
||||
}
|
||||
|
||||
return null
|
||||
},
|
||||
generateBundle(_, bundle) {
|
||||
for (const fileName of Object.keys(bundle)) {
|
||||
delete bundle[fileName]
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function flatCopy(src: string | string[], dest: string): Target {
|
||||
return {
|
||||
src,
|
||||
dest,
|
||||
rename: { stripBase: true },
|
||||
}
|
||||
}
|
||||
|
||||
function reactRuntimeTargets(dest: string): Target[] {
|
||||
if (useReactDevelopmentAssets && dest === 'js') {
|
||||
return [
|
||||
{
|
||||
src: 'node_modules/react/umd/react.development.js',
|
||||
dest,
|
||||
rename: { stripBase: true, name: 'react.production.min.js' },
|
||||
},
|
||||
{
|
||||
src: 'node_modules/react-dom/umd/react-dom.development.js',
|
||||
dest,
|
||||
rename: { stripBase: true, name: 'react-dom.production.min.js' },
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
return [
|
||||
flatCopy([
|
||||
'node_modules/react/umd/react.production.min.js',
|
||||
'node_modules/react/umd/react.development.js',
|
||||
'node_modules/react-dom/umd/react-dom.production.min.js',
|
||||
'node_modules/react-dom/umd/react-dom.development.js',
|
||||
], dest),
|
||||
]
|
||||
}
|
||||
|
||||
const desktopJsTargets: Target[] = [
|
||||
flatCopy([
|
||||
'node_modules/katex/dist/katex.min.js',
|
||||
'node_modules/katex/dist/contrib/mhchem.min.js',
|
||||
'node_modules/html2canvas/dist/html2canvas.min.js',
|
||||
'node_modules/interactjs/dist/interact.min.js',
|
||||
'node_modules/photoswipe/dist/umd/*.js',
|
||||
'node_modules/marked/lib/marked.umd.js',
|
||||
'node_modules/@highlightjs/cdn-assets/highlight.min.js',
|
||||
'node_modules/@isomorphic-git/lightning-fs/dist/lightning-fs.min.js',
|
||||
'packages/ui/dist/ui.js',
|
||||
'node_modules/@sqlite.org/sqlite-wasm/dist/sqlite3.wasm',
|
||||
'node_modules/prop-types/prop-types.min.js',
|
||||
'node_modules/dompurify/dist/purify.js',
|
||||
], 'js'),
|
||||
...reactRuntimeTargets('js'),
|
||||
{
|
||||
src: 'node_modules/@tabler/icons-react/dist/umd/tabler-icons-react.min.js',
|
||||
dest: 'js',
|
||||
rename: { stripBase: true },
|
||||
transform: {
|
||||
encoding: 'utf8',
|
||||
handler(content) {
|
||||
return content.replace(
|
||||
'"@tabler/icons-react"]={},a.react,',
|
||||
'"tablerIcons"]={},a.React,',
|
||||
)
|
||||
},
|
||||
},
|
||||
},
|
||||
flatCopy([
|
||||
'node_modules/@glidejs/glide/dist/glide.min.js',
|
||||
'node_modules/@glidejs/glide/dist/css/glide.core.min.css',
|
||||
'node_modules/@glidejs/glide/dist/css/glide.theme.min.css',
|
||||
], 'js/glide'),
|
||||
flatCopy([
|
||||
'node_modules/pdfjs-dist/legacy/build/pdf.mjs',
|
||||
'node_modules/pdfjs-dist/legacy/build/pdf.worker.mjs',
|
||||
'node_modules/pdfjs-dist/legacy/web/pdf_viewer.mjs',
|
||||
], 'js/pdfjs'),
|
||||
flatCopy('node_modules/pdfjs-dist/cmaps/*.*', 'js/pdfjs/cmaps'),
|
||||
]
|
||||
|
||||
const mobileJsTargets: Target[] = [
|
||||
flatCopy([
|
||||
'node_modules/katex/dist/katex.min.js',
|
||||
'node_modules/katex/dist/contrib/mhchem.min.js',
|
||||
'node_modules/marked/lib/marked.umd.js',
|
||||
'node_modules/@highlightjs/cdn-assets/highlight.min.js',
|
||||
'node_modules/@isomorphic-git/lightning-fs/dist/lightning-fs.min.js',
|
||||
'node_modules/interactjs/dist/interact.min.js',
|
||||
'node_modules/photoswipe/dist/umd/*.js',
|
||||
'packages/ui/dist/ui.js',
|
||||
'node_modules/@sqlite.org/sqlite-wasm/dist/sqlite3.wasm',
|
||||
'node_modules/prop-types/prop-types.min.js',
|
||||
], 'mobile/js'),
|
||||
...reactRuntimeTargets('mobile/js'),
|
||||
]
|
||||
|
||||
const cssAssetTargets: Target[] = [
|
||||
flatCopy('node_modules/inter-ui/inter.css', 'css'),
|
||||
flatCopy('node_modules/inter-ui/web/*.*', 'css/web'),
|
||||
flatCopy('node_modules/katex/dist/fonts/*.woff2', 'css/fonts'),
|
||||
flatCopy('node_modules/inter-ui/inter.css', 'mobile/css'),
|
||||
flatCopy('node_modules/inter-ui/web/*.*', 'mobile/css/web'),
|
||||
flatCopy('node_modules/katex/dist/fonts/*.woff2', 'mobile/css/fonts'),
|
||||
]
|
||||
|
||||
export default defineConfig({
|
||||
publicDir: false,
|
||||
build: {
|
||||
emptyOutDir: false,
|
||||
outDir: 'static',
|
||||
rollupOptions: {
|
||||
input: virtualEntryId,
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
virtualAssetsEntryPlugin(),
|
||||
viteStaticCopy({
|
||||
targets: [
|
||||
{
|
||||
src: normalizePath('resources/**/*'),
|
||||
dest: '.',
|
||||
// Drop only the resources/ prefix while preserving its nested paths.
|
||||
rename: { stripBase: 1 },
|
||||
},
|
||||
...desktopJsTargets,
|
||||
...mobileJsTargets,
|
||||
...cssAssetTargets,
|
||||
],
|
||||
watch: {
|
||||
reloadPageOnChange: false,
|
||||
},
|
||||
}),
|
||||
],
|
||||
})
|
||||
137
vite.config.css.ts
Normal file
137
vite.config.css.ts
Normal file
@@ -0,0 +1,137 @@
|
||||
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import autoprefixer from 'autoprefixer'
|
||||
import cssnano from 'cssnano'
|
||||
import postcss, { type AcceptedPlugin } from 'postcss'
|
||||
import postcssImport from 'postcss-import'
|
||||
import postcssImportExtGlob from 'postcss-import-ext-glob'
|
||||
import tailwindcss from 'tailwindcss'
|
||||
import tailwindNesting from 'tailwindcss/nesting'
|
||||
import { defineConfig, type Plugin } from 'vite'
|
||||
|
||||
type CssTarget = {
|
||||
name: string
|
||||
input: string
|
||||
output: string
|
||||
}
|
||||
|
||||
const virtualEntryId = 'virtual:logseq-css-entry'
|
||||
const resolvedVirtualEntryId = `\0${virtualEntryId}`
|
||||
|
||||
const allCssTargets: CssTarget[] = [
|
||||
{
|
||||
name: 'desktop',
|
||||
input: 'tailwind.all.css',
|
||||
output: 'static/css/style.css',
|
||||
},
|
||||
{
|
||||
name: 'mobile',
|
||||
input: 'tailwind.mobile.css',
|
||||
output: 'static/mobile/css/style.css',
|
||||
},
|
||||
]
|
||||
|
||||
function requestedCssTargets(): CssTarget[] {
|
||||
const targetName = process.env.LOGSEQ_CSS_TARGET
|
||||
if (!targetName) {
|
||||
return allCssTargets
|
||||
}
|
||||
|
||||
const target = allCssTargets.find(({ name }) => name === targetName)
|
||||
if (!target) {
|
||||
throw new Error(`Unknown LOGSEQ_CSS_TARGET: ${targetName}`)
|
||||
}
|
||||
|
||||
return [target]
|
||||
}
|
||||
|
||||
function filesToWatch(): string[] {
|
||||
return [
|
||||
'tailwind.config.js',
|
||||
...allCssTargets.map(({ input }) => input),
|
||||
'src',
|
||||
'resources',
|
||||
path.join('deps', 'shui', 'src'),
|
||||
path.join('packages', 'ui', 'src'),
|
||||
path.join('packages', 'ui', '@'),
|
||||
].filter(existsSync)
|
||||
}
|
||||
|
||||
async function buildCss(targets: CssTarget[]) {
|
||||
const postcssPlugin = (plugin: unknown): AcceptedPlugin => plugin as AcceptedPlugin
|
||||
const postcssPlugins: AcceptedPlugin[] = [
|
||||
postcssPlugin(postcssImportExtGlob()),
|
||||
postcssPlugin(postcssImport()),
|
||||
postcssPlugin(tailwindNesting()),
|
||||
postcssPlugin(tailwindcss()),
|
||||
postcssPlugin(autoprefixer()),
|
||||
]
|
||||
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
postcssPlugins.push(postcssPlugin(cssnano({ preset: 'default' })))
|
||||
}
|
||||
|
||||
const processor = postcss(postcssPlugins)
|
||||
|
||||
for (const target of targets) {
|
||||
const input = path.resolve(target.input)
|
||||
const output = path.resolve(target.output)
|
||||
const result = await processor.process(readFileSync(input, 'utf8'), {
|
||||
from: input,
|
||||
to: output,
|
||||
map: false,
|
||||
})
|
||||
|
||||
mkdirSync(path.dirname(output), { recursive: true })
|
||||
writeFileSync(output, result.css)
|
||||
}
|
||||
}
|
||||
|
||||
function cssPipelinePlugin(targets: CssTarget[]): Plugin {
|
||||
return {
|
||||
name: 'logseq-css-pipeline',
|
||||
resolveId(id) {
|
||||
if (id === virtualEntryId) {
|
||||
return resolvedVirtualEntryId
|
||||
}
|
||||
|
||||
return null
|
||||
},
|
||||
load(id) {
|
||||
if (id === resolvedVirtualEntryId) {
|
||||
return 'export {}\n'
|
||||
}
|
||||
|
||||
return null
|
||||
},
|
||||
buildStart() {
|
||||
for (const file of filesToWatch()) {
|
||||
this.addWatchFile(path.resolve(file))
|
||||
}
|
||||
},
|
||||
async closeBundle() {
|
||||
await buildCss(targets)
|
||||
},
|
||||
generateBundle(_, bundle) {
|
||||
for (const fileName of Object.keys(bundle)) {
|
||||
delete bundle[fileName]
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const cssTargets = requestedCssTargets()
|
||||
|
||||
export default defineConfig({
|
||||
publicDir: false,
|
||||
build: {
|
||||
emptyOutDir: false,
|
||||
outDir: 'static',
|
||||
rollupOptions: {
|
||||
input: virtualEntryId,
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
cssPipelinePlugin(cssTargets),
|
||||
],
|
||||
})
|
||||
Reference in New Issue
Block a user