Compare commits
10 Commits
b24f2a5e7b
...
afe98e6b1a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
afe98e6b1a | ||
|
|
f15aa0032a | ||
|
|
9e4d4518bd | ||
|
|
3138a88bb1 | ||
|
|
db5aca2b8d | ||
|
|
dd1b36c148 | ||
|
|
4579e49465 | ||
|
|
882d3b3f50 | ||
|
|
667d487a0f | ||
|
|
36b3542371 |
2
package-lock.json
generated
2
package-lock.json
generated
@@ -1857,7 +1857,7 @@
|
|||||||
},
|
},
|
||||||
"node_modules/@quartz-community/utils": {
|
"node_modules/@quartz-community/utils": {
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"resolved": "git+ssh://git@github.com/quartz-community/utils.git#7a43501ac1c728e8a7bfa75db35e8791bfa1f41c",
|
"resolved": "git+ssh://git@github.com/quartz-community/utils.git#e09d8b0bbccb46073b23c00affaf3e40a2c3b0c4",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@@ -70,6 +70,9 @@ plugins:
|
|||||||
- source: github:quartz-community/table-of-contents
|
- source: github:quartz-community/table-of-contents
|
||||||
enabled: true
|
enabled: true
|
||||||
order: 50
|
order: 50
|
||||||
|
layout:
|
||||||
|
position: right
|
||||||
|
priority: 30
|
||||||
- source: github:quartz-community/crawl-links
|
- source: github:quartz-community/crawl-links
|
||||||
enabled: true
|
enabled: true
|
||||||
options:
|
options:
|
||||||
@@ -142,7 +145,7 @@ plugins:
|
|||||||
enabled: true
|
enabled: true
|
||||||
layout:
|
layout:
|
||||||
position: right
|
position: right
|
||||||
priority: 30
|
priority: 50
|
||||||
- source: github:quartz-community/article-title
|
- source: github:quartz-community/article-title
|
||||||
enabled: true
|
enabled: true
|
||||||
layout:
|
layout:
|
||||||
|
|||||||
@@ -22,8 +22,8 @@
|
|||||||
"bases-page": {
|
"bases-page": {
|
||||||
"source": "github:quartz-community/bases-page",
|
"source": "github:quartz-community/bases-page",
|
||||||
"resolved": "https://github.com/quartz-community/bases-page.git",
|
"resolved": "https://github.com/quartz-community/bases-page.git",
|
||||||
"commit": "077ebacd657a86bc92adc4ec392a238cacbfc298",
|
"commit": "ec5711f91e40b1e8477a101f6e2604926e4f0222",
|
||||||
"installedAt": "2026-05-24T19:26:20.456Z"
|
"installedAt": "2026-05-26T16:15:16.668Z"
|
||||||
},
|
},
|
||||||
"breadcrumbs": {
|
"breadcrumbs": {
|
||||||
"source": "github:quartz-community/breadcrumbs",
|
"source": "github:quartz-community/breadcrumbs",
|
||||||
@@ -76,8 +76,8 @@
|
|||||||
"crawl-links": {
|
"crawl-links": {
|
||||||
"source": "github:quartz-community/crawl-links",
|
"source": "github:quartz-community/crawl-links",
|
||||||
"resolved": "https://github.com/quartz-community/crawl-links.git",
|
"resolved": "https://github.com/quartz-community/crawl-links.git",
|
||||||
"commit": "533a2213b673c3ea2a67f6e160f4e6a1262e8a6d",
|
"commit": "69497e22046d62ee601415ad832bcde89a70a544",
|
||||||
"installedAt": "2026-05-24T15:36:16.458Z"
|
"installedAt": "2026-05-26T15:56:37.042Z"
|
||||||
},
|
},
|
||||||
"created-modified-date": {
|
"created-modified-date": {
|
||||||
"source": "github:quartz-community/created-modified-date",
|
"source": "github:quartz-community/created-modified-date",
|
||||||
@@ -142,8 +142,8 @@
|
|||||||
"graph": {
|
"graph": {
|
||||||
"source": "github:quartz-community/graph",
|
"source": "github:quartz-community/graph",
|
||||||
"resolved": "https://github.com/quartz-community/graph.git",
|
"resolved": "https://github.com/quartz-community/graph.git",
|
||||||
"commit": "27eb4dd71ca10c8bd204cf1537da4448811c15f3",
|
"commit": "67ac4993907618a0ea5bd33726f792c572cddbad",
|
||||||
"installedAt": "2026-05-24T15:36:16.989Z"
|
"installedAt": "2026-05-26T17:45:56.210Z"
|
||||||
},
|
},
|
||||||
"hard-line-breaks": {
|
"hard-line-breaks": {
|
||||||
"source": "github:quartz-community/hard-line-breaks",
|
"source": "github:quartz-community/hard-line-breaks",
|
||||||
@@ -160,20 +160,20 @@
|
|||||||
"note-properties": {
|
"note-properties": {
|
||||||
"source": "github:quartz-community/note-properties",
|
"source": "github:quartz-community/note-properties",
|
||||||
"resolved": "https://github.com/quartz-community/note-properties.git",
|
"resolved": "https://github.com/quartz-community/note-properties.git",
|
||||||
"commit": "cccd275468e93ea59115e755d4bb8599192c45b9",
|
"commit": "874712a9910299b906606c885ea1fe9602114ca7",
|
||||||
"installedAt": "2026-05-24T15:36:16.987Z"
|
"installedAt": "2026-05-26T16:15:17.177Z"
|
||||||
},
|
},
|
||||||
"obsidian-flavored-markdown": {
|
"obsidian-flavored-markdown": {
|
||||||
"source": "github:quartz-community/obsidian-flavored-markdown",
|
"source": "github:quartz-community/obsidian-flavored-markdown",
|
||||||
"resolved": "https://github.com/quartz-community/obsidian-flavored-markdown.git",
|
"resolved": "https://github.com/quartz-community/obsidian-flavored-markdown.git",
|
||||||
"commit": "186d6bd990a1b88ab35c4f66ea4f257f1e7b4a51",
|
"commit": "7f61d4bcf7537d056528f6cecaf5c35b39f74fe8",
|
||||||
"installedAt": "2026-05-25T23:55:00.345Z"
|
"installedAt": "2026-05-27T18:30:09.459Z"
|
||||||
},
|
},
|
||||||
"obsidian-plugin-excalidraw": {
|
"obsidian-plugin-excalidraw": {
|
||||||
"source": "github:quartz-community/obsidian-plugin-excalidraw",
|
"source": "github:quartz-community/obsidian-plugin-excalidraw",
|
||||||
"resolved": "https://github.com/quartz-community/obsidian-plugin-excalidraw.git",
|
"resolved": "https://github.com/quartz-community/obsidian-plugin-excalidraw.git",
|
||||||
"commit": "9c5ff6bd6d2ee27814aeaf98ac4ddfd025a985fa",
|
"commit": "554ae8af7ac4d7fdff8d78e65bfa9b4dd53cbb87",
|
||||||
"installedAt": "2026-05-25T17:40:01.376Z"
|
"installedAt": "2026-05-27T18:30:09.649Z"
|
||||||
},
|
},
|
||||||
"obsidian-plugin-ttrpg-tools-maps": {
|
"obsidian-plugin-ttrpg-tools-maps": {
|
||||||
"source": "github:quartz-community/obsidian-plugin-ttrpg-tools-maps",
|
"source": "github:quartz-community/obsidian-plugin-ttrpg-tools-maps",
|
||||||
@@ -206,9 +206,9 @@
|
|||||||
"subdir": "plugin"
|
"subdir": "plugin"
|
||||||
},
|
},
|
||||||
"resolved": "https://github.com/saberzero1/quartz-themes.git",
|
"resolved": "https://github.com/saberzero1/quartz-themes.git",
|
||||||
"commit": "b82021d7866c11353a0cc55b75ac3198d988d7d7",
|
"commit": "0da7355c49aa68cb375726913f41c64ac1f11733",
|
||||||
"subdir": "plugin",
|
"subdir": "plugin",
|
||||||
"installedAt": "2026-05-25T15:05:52.311Z"
|
"installedAt": "2026-05-27T18:31:15.006Z"
|
||||||
},
|
},
|
||||||
"reader-mode": {
|
"reader-mode": {
|
||||||
"source": "github:quartz-community/reader-mode",
|
"source": "github:quartz-community/reader-mode",
|
||||||
@@ -219,8 +219,8 @@
|
|||||||
"recent-notes": {
|
"recent-notes": {
|
||||||
"source": "github:quartz-community/recent-notes",
|
"source": "github:quartz-community/recent-notes",
|
||||||
"resolved": "https://github.com/quartz-community/recent-notes.git",
|
"resolved": "https://github.com/quartz-community/recent-notes.git",
|
||||||
"commit": "74cb2588deae80fd7c13bf5fdeedebffe547d214",
|
"commit": "4505adba9c213967ddf94ea5fc05039245fd9fe9",
|
||||||
"installedAt": "2026-05-24T15:36:17.428Z"
|
"installedAt": "2026-05-26T20:20:42.557Z"
|
||||||
},
|
},
|
||||||
"remove-draft": {
|
"remove-draft": {
|
||||||
"source": "github:quartz-community/remove-draft",
|
"source": "github:quartz-community/remove-draft",
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import {
|
|||||||
gitPull,
|
gitPull,
|
||||||
popContentFolder,
|
popContentFolder,
|
||||||
stashContentFolder,
|
stashContentFolder,
|
||||||
|
symlinkOrCopy,
|
||||||
} from "./helpers.js"
|
} from "./helpers.js"
|
||||||
import {
|
import {
|
||||||
handlePluginRestore,
|
handlePluginRestore,
|
||||||
@@ -210,7 +211,7 @@ export async function handleCreate(argv) {
|
|||||||
preserveTimestamps: true,
|
preserveTimestamps: true,
|
||||||
})
|
})
|
||||||
} else if (setupStrategy === "symlink") {
|
} else if (setupStrategy === "symlink") {
|
||||||
await fs.promises.symlink(originalFolder, contentFolder, "dir")
|
await symlinkOrCopy(originalFolder, contentFolder)
|
||||||
}
|
}
|
||||||
} else if (setupStrategy === "new") {
|
} else if (setupStrategy === "new") {
|
||||||
await fs.promises.writeFile(
|
await fs.promises.writeFile(
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { styleText } from "util"
|
|||||||
import { contentCacheFolder } from "./constants.js"
|
import { contentCacheFolder } from "./constants.js"
|
||||||
import { spawnSync } from "child_process"
|
import { spawnSync } from "child_process"
|
||||||
import fs from "fs"
|
import fs from "fs"
|
||||||
|
import path from "path"
|
||||||
|
|
||||||
export function escapePath(fp) {
|
export function escapePath(fp) {
|
||||||
return fp
|
return fp
|
||||||
@@ -33,7 +34,7 @@ export async function stashContentFolder(contentFolder) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function gitPull(origin, branch) {
|
export function gitPull(origin, branch) {
|
||||||
const flags = ["--no-rebase", "--autostash", "--no-edit"]
|
const flags = ["--no-rebase", "--autostash", "--no-edit", "--allow-unrelated-histories"]
|
||||||
const out = spawnSync("git", ["pull", ...flags, origin, branch], { stdio: "inherit" })
|
const out = spawnSync("git", ["pull", ...flags, origin, branch], { stdio: "inherit" })
|
||||||
if (out.stderr) {
|
if (out.stderr) {
|
||||||
throw new Error(styleText("red", `Error while pulling updates: ${out.stderr}`))
|
throw new Error(styleText("red", `Error while pulling updates: ${out.stderr}`))
|
||||||
@@ -52,3 +53,57 @@ export async function popContentFolder(contentFolder) {
|
|||||||
})
|
})
|
||||||
await fs.promises.rm(contentCacheFolder, { force: true, recursive: true })
|
await fs.promises.rm(contentCacheFolder, { force: true, recursive: true })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a directory symlink with Windows fallback.
|
||||||
|
*
|
||||||
|
* On Windows, creating symlinks requires Developer Mode or admin privileges.
|
||||||
|
* When that fails (EPERM), we try a junction first (no elevation needed),
|
||||||
|
* then fall back to a recursive copy as a last resort.
|
||||||
|
*
|
||||||
|
* @param {string} target Symlink target (may be relative)
|
||||||
|
* @param {string} linkPath Path where the link is created
|
||||||
|
*/
|
||||||
|
export function symlinkOrCopySync(target, linkPath) {
|
||||||
|
try {
|
||||||
|
fs.symlinkSync(target, linkPath, "dir")
|
||||||
|
} catch (err) {
|
||||||
|
if (err.code === "EEXIST") return
|
||||||
|
if (err.code === "EPERM" && process.platform === "win32") {
|
||||||
|
try {
|
||||||
|
fs.symlinkSync(target, linkPath, "junction")
|
||||||
|
return
|
||||||
|
} catch {
|
||||||
|
const resolvedTarget = path.resolve(path.dirname(linkPath), target)
|
||||||
|
fs.cpSync(resolvedTarget, linkPath, { recursive: true })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Async version of {@link symlinkOrCopySync}.
|
||||||
|
*
|
||||||
|
* @param {string} target Symlink target (may be relative)
|
||||||
|
* @param {string} linkPath Path where the link is created
|
||||||
|
*/
|
||||||
|
export async function symlinkOrCopy(target, linkPath) {
|
||||||
|
try {
|
||||||
|
await fs.promises.symlink(target, linkPath, "dir")
|
||||||
|
} catch (err) {
|
||||||
|
if (err.code === "EEXIST") return
|
||||||
|
if (err.code === "EPERM" && process.platform === "win32") {
|
||||||
|
try {
|
||||||
|
await fs.promises.symlink(target, linkPath, "junction")
|
||||||
|
return
|
||||||
|
} catch {
|
||||||
|
const resolvedTarget = path.resolve(path.dirname(linkPath), target)
|
||||||
|
await fs.promises.cp(resolvedTarget, linkPath, { recursive: true })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
237
quartz/cli/helpers.test.js
Normal file
237
quartz/cli/helpers.test.js
Normal file
@@ -0,0 +1,237 @@
|
|||||||
|
import test, { describe, beforeEach, afterEach, mock } from "node:test"
|
||||||
|
import assert from "node:assert"
|
||||||
|
import fs from "fs"
|
||||||
|
import path from "path"
|
||||||
|
import os from "os"
|
||||||
|
import { symlinkOrCopySync, symlinkOrCopy } from "./helpers.js"
|
||||||
|
|
||||||
|
function makeTmpDir() {
|
||||||
|
return fs.mkdtempSync(path.join(os.tmpdir(), "quartz-symlink-test-"))
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeTarget(tmpDir) {
|
||||||
|
const target = path.join(tmpDir, "target")
|
||||||
|
fs.mkdirSync(target)
|
||||||
|
fs.writeFileSync(path.join(target, "marker.txt"), "hello")
|
||||||
|
return target
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("symlinkOrCopySync", () => {
|
||||||
|
let tmpDir
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
tmpDir = makeTmpDir()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
fs.rmSync(tmpDir, { recursive: true, force: true })
|
||||||
|
})
|
||||||
|
|
||||||
|
test("creates a symlink on success", () => {
|
||||||
|
const target = makeTarget(tmpDir)
|
||||||
|
const linkPath = path.join(tmpDir, "link")
|
||||||
|
|
||||||
|
symlinkOrCopySync(target, linkPath)
|
||||||
|
|
||||||
|
const stat = fs.lstatSync(linkPath)
|
||||||
|
assert.ok(stat.isSymbolicLink())
|
||||||
|
assert.ok(fs.existsSync(path.join(linkPath, "marker.txt")))
|
||||||
|
})
|
||||||
|
|
||||||
|
test("silently succeeds when link already exists (EEXIST)", () => {
|
||||||
|
const target = makeTarget(tmpDir)
|
||||||
|
const linkPath = path.join(tmpDir, "link")
|
||||||
|
|
||||||
|
symlinkOrCopySync(target, linkPath)
|
||||||
|
assert.doesNotThrow(() => symlinkOrCopySync(target, linkPath))
|
||||||
|
})
|
||||||
|
|
||||||
|
test("re-throws non-EPERM errors", () => {
|
||||||
|
const target = makeTarget(tmpDir)
|
||||||
|
const linkPath = path.join(tmpDir, "nonexistent-parent", "link")
|
||||||
|
|
||||||
|
assert.throws(
|
||||||
|
() => symlinkOrCopySync(target, linkPath),
|
||||||
|
(err) => {
|
||||||
|
return err.code === "ENOENT"
|
||||||
|
},
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("falls back to junction on Windows EPERM", () => {
|
||||||
|
const target = makeTarget(tmpDir)
|
||||||
|
const linkPath = path.join(tmpDir, "link")
|
||||||
|
|
||||||
|
const originalPlatform = Object.getOwnPropertyDescriptor(process, "platform")
|
||||||
|
Object.defineProperty(process, "platform", { value: "win32", configurable: true })
|
||||||
|
|
||||||
|
let callCount = 0
|
||||||
|
const originalSymlinkSync = fs.symlinkSync
|
||||||
|
mock.method(fs, "symlinkSync", (t, lp, type) => {
|
||||||
|
callCount++
|
||||||
|
if (callCount === 1 && type === "dir") {
|
||||||
|
const err = new Error("EPERM: operation not permitted, symlink")
|
||||||
|
err.code = "EPERM"
|
||||||
|
err.errno = -4048
|
||||||
|
err.syscall = "symlink"
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
return originalSymlinkSync(t, lp, type)
|
||||||
|
})
|
||||||
|
|
||||||
|
try {
|
||||||
|
symlinkOrCopySync(target, linkPath)
|
||||||
|
assert.ok(fs.existsSync(path.join(linkPath, "marker.txt")))
|
||||||
|
assert.strictEqual(callCount, 2)
|
||||||
|
} finally {
|
||||||
|
fs.symlinkSync.mock.restore()
|
||||||
|
if (originalPlatform) {
|
||||||
|
Object.defineProperty(process, "platform", originalPlatform)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
test("falls back to copy when both symlink and junction fail on Windows", () => {
|
||||||
|
const target = makeTarget(tmpDir)
|
||||||
|
const linkPath = path.join(tmpDir, "link")
|
||||||
|
|
||||||
|
const originalPlatform = Object.getOwnPropertyDescriptor(process, "platform")
|
||||||
|
Object.defineProperty(process, "platform", { value: "win32", configurable: true })
|
||||||
|
|
||||||
|
const originalSymlinkSync = fs.symlinkSync
|
||||||
|
mock.method(fs, "symlinkSync", (_t, _lp, _type) => {
|
||||||
|
const err = new Error("EPERM: operation not permitted, symlink")
|
||||||
|
err.code = "EPERM"
|
||||||
|
err.errno = -4048
|
||||||
|
err.syscall = "symlink"
|
||||||
|
throw err
|
||||||
|
})
|
||||||
|
|
||||||
|
try {
|
||||||
|
symlinkOrCopySync(target, linkPath)
|
||||||
|
|
||||||
|
const stat = fs.lstatSync(linkPath)
|
||||||
|
assert.ok(stat.isDirectory(), "fallback should produce a real directory, not a symlink")
|
||||||
|
assert.strictEqual(fs.readFileSync(path.join(linkPath, "marker.txt"), "utf-8"), "hello")
|
||||||
|
} finally {
|
||||||
|
fs.symlinkSync.mock.restore()
|
||||||
|
if (originalPlatform) {
|
||||||
|
Object.defineProperty(process, "platform", originalPlatform)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
test("does not fall back on EPERM when not on Windows", () => {
|
||||||
|
const target = makeTarget(tmpDir)
|
||||||
|
const linkPath = path.join(tmpDir, "link")
|
||||||
|
|
||||||
|
const originalPlatform = Object.getOwnPropertyDescriptor(process, "platform")
|
||||||
|
Object.defineProperty(process, "platform", { value: "linux", configurable: true })
|
||||||
|
|
||||||
|
const originalSymlinkSync = fs.symlinkSync
|
||||||
|
mock.method(fs, "symlinkSync", (_t, _lp, _type) => {
|
||||||
|
const err = new Error("EPERM: operation not permitted, symlink")
|
||||||
|
err.code = "EPERM"
|
||||||
|
throw err
|
||||||
|
})
|
||||||
|
|
||||||
|
try {
|
||||||
|
assert.throws(
|
||||||
|
() => symlinkOrCopySync(target, linkPath),
|
||||||
|
(err) => err.code === "EPERM",
|
||||||
|
)
|
||||||
|
} finally {
|
||||||
|
fs.symlinkSync.mock.restore()
|
||||||
|
if (originalPlatform) {
|
||||||
|
Object.defineProperty(process, "platform", originalPlatform)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("symlinkOrCopy", () => {
|
||||||
|
let tmpDir
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
tmpDir = makeTmpDir()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
fs.rmSync(tmpDir, { recursive: true, force: true })
|
||||||
|
})
|
||||||
|
|
||||||
|
test("creates a symlink on success", async () => {
|
||||||
|
const target = makeTarget(tmpDir)
|
||||||
|
const linkPath = path.join(tmpDir, "link")
|
||||||
|
|
||||||
|
await symlinkOrCopy(target, linkPath)
|
||||||
|
|
||||||
|
const stat = fs.lstatSync(linkPath)
|
||||||
|
assert.ok(stat.isSymbolicLink())
|
||||||
|
assert.ok(fs.existsSync(path.join(linkPath, "marker.txt")))
|
||||||
|
})
|
||||||
|
|
||||||
|
test("silently succeeds when link already exists (EEXIST)", async () => {
|
||||||
|
const target = makeTarget(tmpDir)
|
||||||
|
const linkPath = path.join(tmpDir, "link")
|
||||||
|
|
||||||
|
await symlinkOrCopy(target, linkPath)
|
||||||
|
await assert.doesNotReject(() => symlinkOrCopy(target, linkPath))
|
||||||
|
})
|
||||||
|
|
||||||
|
test("falls back to copy when both symlink and junction fail on Windows", async () => {
|
||||||
|
const target = makeTarget(tmpDir)
|
||||||
|
const linkPath = path.join(tmpDir, "link")
|
||||||
|
|
||||||
|
const originalPlatform = Object.getOwnPropertyDescriptor(process, "platform")
|
||||||
|
Object.defineProperty(process, "platform", { value: "win32", configurable: true })
|
||||||
|
|
||||||
|
const originalSymlink = fs.promises.symlink
|
||||||
|
mock.method(fs.promises, "symlink", async (_t, _lp, _type) => {
|
||||||
|
const err = new Error("EPERM: operation not permitted, symlink")
|
||||||
|
err.code = "EPERM"
|
||||||
|
err.errno = -4048
|
||||||
|
err.syscall = "symlink"
|
||||||
|
throw err
|
||||||
|
})
|
||||||
|
|
||||||
|
try {
|
||||||
|
await symlinkOrCopy(target, linkPath)
|
||||||
|
|
||||||
|
const stat = fs.lstatSync(linkPath)
|
||||||
|
assert.ok(stat.isDirectory(), "fallback should produce a real directory, not a symlink")
|
||||||
|
assert.strictEqual(fs.readFileSync(path.join(linkPath, "marker.txt"), "utf-8"), "hello")
|
||||||
|
} finally {
|
||||||
|
fs.promises.symlink.mock.restore()
|
||||||
|
if (originalPlatform) {
|
||||||
|
Object.defineProperty(process, "platform", originalPlatform)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
test("does not fall back on EPERM when not on Windows", async () => {
|
||||||
|
const target = makeTarget(tmpDir)
|
||||||
|
const linkPath = path.join(tmpDir, "link")
|
||||||
|
|
||||||
|
const originalPlatform = Object.getOwnPropertyDescriptor(process, "platform")
|
||||||
|
Object.defineProperty(process, "platform", { value: "linux", configurable: true })
|
||||||
|
|
||||||
|
mock.method(fs.promises, "symlink", async (_t, _lp, _type) => {
|
||||||
|
const err = new Error("EPERM: operation not permitted, symlink")
|
||||||
|
err.code = "EPERM"
|
||||||
|
throw err
|
||||||
|
})
|
||||||
|
|
||||||
|
try {
|
||||||
|
await assert.rejects(
|
||||||
|
() => symlinkOrCopy(target, linkPath),
|
||||||
|
(err) => err.code === "EPERM",
|
||||||
|
)
|
||||||
|
} finally {
|
||||||
|
fs.promises.symlink.mock.restore()
|
||||||
|
if (originalPlatform) {
|
||||||
|
Object.defineProperty(process, "platform", originalPlatform)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -20,6 +20,7 @@ import {
|
|||||||
resolveLockfileName,
|
resolveLockfileName,
|
||||||
getNameOverrides,
|
getNameOverrides,
|
||||||
} from "./plugin-data.js"
|
} from "./plugin-data.js"
|
||||||
|
import { symlinkOrCopySync } from "./helpers.js"
|
||||||
|
|
||||||
const INTERNAL_EXPORTS = new Set(["manifest", "default"])
|
const INTERNAL_EXPORTS = new Set(["manifest", "default"])
|
||||||
|
|
||||||
@@ -128,12 +129,7 @@ function needsBuild(pluginDir) {
|
|||||||
* share a single copy of packages like unified, vfile, rehype-raw, etc.
|
* share a single copy of packages like unified, vfile, rehype-raw, etc.
|
||||||
*/
|
*/
|
||||||
function trySymlink(target, linkPath) {
|
function trySymlink(target, linkPath) {
|
||||||
try {
|
symlinkOrCopySync(target, linkPath)
|
||||||
fs.symlinkSync(target, linkPath, "dir")
|
|
||||||
} catch (err) {
|
|
||||||
if (err.code === "EEXIST") return
|
|
||||||
throw err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function linkPeerPlugins(pluginDir) {
|
function linkPeerPlugins(pluginDir) {
|
||||||
@@ -621,7 +617,7 @@ export async function handlePluginInstallUnified({
|
|||||||
}
|
}
|
||||||
console.log(styleText("cyan", `→ Linking ${name} from ${resolvedPath}...`))
|
console.log(styleText("cyan", `→ Linking ${name} from ${resolvedPath}...`))
|
||||||
fs.mkdirSync(path.dirname(pluginDir), { recursive: true })
|
fs.mkdirSync(path.dirname(pluginDir), { recursive: true })
|
||||||
fs.symlinkSync(resolvedPath, pluginDir, "dir")
|
symlinkOrCopySync(resolvedPath, pluginDir)
|
||||||
lockfile.plugins[name] = {
|
lockfile.plugins[name] = {
|
||||||
source: entry.source,
|
source: entry.source,
|
||||||
resolved: resolvedPath,
|
resolved: resolvedPath,
|
||||||
@@ -810,7 +806,7 @@ export async function handlePluginInstallUnified({
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
fs.mkdirSync(path.dirname(pluginDir), { recursive: true })
|
fs.mkdirSync(path.dirname(pluginDir), { recursive: true })
|
||||||
fs.symlinkSync(entry.resolved, pluginDir, "dir")
|
symlinkOrCopySync(entry.resolved, pluginDir)
|
||||||
console.log(styleText("green", `✓ ${name} restored (local symlink)`))
|
console.log(styleText("green", `✓ ${name} restored (local symlink)`))
|
||||||
restoredPlugins.push({ name, pluginDir })
|
restoredPlugins.push({ name, pluginDir })
|
||||||
installed++
|
installed++
|
||||||
@@ -1045,7 +1041,7 @@ export async function handlePluginInstallUnified({
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
fs.mkdirSync(path.dirname(pluginDir), { recursive: true })
|
fs.mkdirSync(path.dirname(pluginDir), { recursive: true })
|
||||||
fs.symlinkSync(entry.resolved, pluginDir, "dir")
|
symlinkOrCopySync(entry.resolved, pluginDir)
|
||||||
console.log(styleText("green", ` ✓ ${name} (local) linked`))
|
console.log(styleText("green", ` ✓ ${name} (local) linked`))
|
||||||
pluginsToBuild.push({ name, pluginDir })
|
pluginsToBuild.push({ name, pluginDir })
|
||||||
installed++
|
installed++
|
||||||
@@ -1222,7 +1218,7 @@ export async function handlePluginAdd(
|
|||||||
}
|
}
|
||||||
console.log(styleText("cyan", `→ Adding ${name} from local path ${resolvedPath}...`))
|
console.log(styleText("cyan", `→ Adding ${name} from local path ${resolvedPath}...`))
|
||||||
fs.mkdirSync(path.dirname(pluginDir), { recursive: true })
|
fs.mkdirSync(path.dirname(pluginDir), { recursive: true })
|
||||||
fs.symlinkSync(resolvedPath, pluginDir, "dir")
|
symlinkOrCopySync(resolvedPath, pluginDir)
|
||||||
lockfile.plugins[name] = {
|
lockfile.plugins[name] = {
|
||||||
source,
|
source,
|
||||||
resolved: resolvedPath,
|
resolved: resolvedPath,
|
||||||
@@ -1315,13 +1311,13 @@ export async function handlePluginAdd(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (manifest?.components) {
|
if (manifest?.components) {
|
||||||
|
const layoutPositions = new Set(["left", "right", "beforeBody", "afterBody"])
|
||||||
const firstComponentKey = Object.keys(manifest.components)[0]
|
const firstComponentKey = Object.keys(manifest.components)[0]
|
||||||
const comp = manifest.components[firstComponentKey]
|
const comp = manifest.components[firstComponentKey]
|
||||||
if (comp?.defaultPosition) {
|
if (comp?.defaultPosition && layoutPositions.has(comp.defaultPosition)) {
|
||||||
newEntry.layout = {
|
newEntry.layout = {
|
||||||
position: comp.defaultPosition,
|
position: comp.defaultPosition,
|
||||||
priority: comp.defaultPriority ?? 50,
|
priority: comp.defaultPriority ?? 50,
|
||||||
display: "all",
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,6 +72,9 @@ plugins:
|
|||||||
- source: github:quartz-community/table-of-contents
|
- source: github:quartz-community/table-of-contents
|
||||||
enabled: true
|
enabled: true
|
||||||
order: 50
|
order: 50
|
||||||
|
layout:
|
||||||
|
position: right
|
||||||
|
priority: 30
|
||||||
- source: github:quartz-community/crawl-links
|
- source: github:quartz-community/crawl-links
|
||||||
enabled: true
|
enabled: true
|
||||||
options:
|
options:
|
||||||
@@ -156,7 +159,7 @@ plugins:
|
|||||||
enabled: true
|
enabled: true
|
||||||
layout:
|
layout:
|
||||||
position: right
|
position: right
|
||||||
priority: 30
|
priority: 50
|
||||||
- source: github:quartz-community/article-title
|
- source: github:quartz-community/article-title
|
||||||
enabled: true
|
enabled: true
|
||||||
layout:
|
layout:
|
||||||
|
|||||||
@@ -72,6 +72,9 @@ plugins:
|
|||||||
- source: github:quartz-community/table-of-contents
|
- source: github:quartz-community/table-of-contents
|
||||||
enabled: true
|
enabled: true
|
||||||
order: 50
|
order: 50
|
||||||
|
layout:
|
||||||
|
position: right
|
||||||
|
priority: 30
|
||||||
- source: github:quartz-community/crawl-links
|
- source: github:quartz-community/crawl-links
|
||||||
enabled: true
|
enabled: true
|
||||||
options:
|
options:
|
||||||
@@ -161,7 +164,7 @@ plugins:
|
|||||||
enabled: true
|
enabled: true
|
||||||
layout:
|
layout:
|
||||||
position: right
|
position: right
|
||||||
priority: 30
|
priority: 50
|
||||||
- source: github:quartz-community/article-title
|
- source: github:quartz-community/article-title
|
||||||
enabled: true
|
enabled: true
|
||||||
layout:
|
layout:
|
||||||
|
|||||||
@@ -82,6 +82,9 @@ plugins:
|
|||||||
- source: github:quartz-community/table-of-contents
|
- source: github:quartz-community/table-of-contents
|
||||||
enabled: true
|
enabled: true
|
||||||
order: 50
|
order: 50
|
||||||
|
layout:
|
||||||
|
position: right
|
||||||
|
priority: 30
|
||||||
- source: github:quartz-community/crawl-links
|
- source: github:quartz-community/crawl-links
|
||||||
enabled: true
|
enabled: true
|
||||||
options:
|
options:
|
||||||
@@ -172,7 +175,7 @@ plugins:
|
|||||||
enabled: true
|
enabled: true
|
||||||
layout:
|
layout:
|
||||||
position: right
|
position: right
|
||||||
priority: 30
|
priority: 50
|
||||||
- source: github:quartz-community/article-title
|
- source: github:quartz-community/article-title
|
||||||
enabled: true
|
enabled: true
|
||||||
layout:
|
layout:
|
||||||
|
|||||||
@@ -82,6 +82,9 @@ plugins:
|
|||||||
- source: github:quartz-community/table-of-contents
|
- source: github:quartz-community/table-of-contents
|
||||||
enabled: true
|
enabled: true
|
||||||
order: 50
|
order: 50
|
||||||
|
layout:
|
||||||
|
position: right
|
||||||
|
priority: 30
|
||||||
- source: github:quartz-community/crawl-links
|
- source: github:quartz-community/crawl-links
|
||||||
enabled: true
|
enabled: true
|
||||||
options:
|
options:
|
||||||
@@ -172,7 +175,7 @@ plugins:
|
|||||||
enabled: true
|
enabled: true
|
||||||
layout:
|
layout:
|
||||||
position: right
|
position: right
|
||||||
priority: 30
|
priority: 50
|
||||||
- source: github:quartz-community/article-title
|
- source: github:quartz-community/article-title
|
||||||
enabled: true
|
enabled: true
|
||||||
layout:
|
layout:
|
||||||
|
|||||||
Reference in New Issue
Block a user