mirror of
https://github.com/yinshaohua/obsidian-download-image.git
synced 2026-07-22 06:51:07 +00:00
chore: use project-local dependency tooling
This commit is contained in:
parent
0b979946b8
commit
b0b2305113
13 changed files with 14 additions and 387 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -23,6 +23,7 @@ data.json
|
|||
|
||||
# ── GSD baseline (auto-generated) ──
|
||||
.gsd-id
|
||||
.gsd/
|
||||
.bg-shell/
|
||||
Thumbs.db
|
||||
*.swp
|
||||
|
|
@ -35,7 +36,6 @@ Thumbs.db
|
|||
.env.*
|
||||
!.env.example
|
||||
node_modules/
|
||||
.tsconfig.external-node-modules.json
|
||||
.next/
|
||||
dist/
|
||||
build/
|
||||
|
|
|
|||
4
.gsd/.gitignore
vendored
4
.gsd/.gitignore
vendored
|
|
@ -1,4 +0,0 @@
|
|||
# Exclude all GSD data files from version control
|
||||
*
|
||||
# But keep this file itself tracked
|
||||
!.gitignore
|
||||
18
.mcp.json
18
.mcp.json
|
|
@ -1,18 +0,0 @@
|
|||
{
|
||||
"mcpServers": {
|
||||
"gsd-workflow": {
|
||||
"command": "C:\\rd\\nodejs\\node.exe",
|
||||
"args": [
|
||||
"C:\\rd\\nodejs\\node_global\\node_modules\\gsd-pi\\packages\\mcp-server\\dist\\cli.js"
|
||||
],
|
||||
"cwd": "C:\\OneDrive\\src\\obsidian-download-image",
|
||||
"env": {
|
||||
"GSD_CLI_PATH": "C:\\rd\\nodejs\\node_global\\node_modules\\gsd-pi\\dist\\loader.js",
|
||||
"GSD_WORKFLOW_EXECUTORS_MODULE": "C:\\Users\\yinsh\\.gsd\\agent\\extensions\\gsd\\tools\\workflow-tool-executors.js",
|
||||
"GSD_WORKFLOW_WRITE_GATE_MODULE": "C:\\Users\\yinsh\\.gsd\\agent\\extensions\\gsd\\bootstrap\\write-gate.js",
|
||||
"GSD_PERSIST_WRITE_GATE_STATE": "1",
|
||||
"GSD_WORKFLOW_PROJECT_ROOT": "C:\\OneDrive\\src\\obsidian-download-image"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -4,7 +4,7 @@
|
|||
- Keep source code in `src/`; keep `src/main.ts` focused on plugin lifecycle and command registration.
|
||||
- Do not commit generated artifacts such as `node_modules/` or `main.js` unless explicitly requested for a release workflow.
|
||||
- Use the project npm scripts, especially `npm run deps:install` instead of bare `npm install`.
|
||||
- If `EXTERNAL_NODE_MODULES` is set, dependencies are managed through the external dependency wrapper described in `README.md` and `EXTERNAL-NODE-MODULES-GUIDE.md`.
|
||||
- Keep dependencies in the project-local `node_modules/` directory; do not redirect them through environment variables or external dependency wrappers.
|
||||
- Register Obsidian events, DOM events, and intervals through plugin cleanup helpers so unload remains safe.
|
||||
- Preserve privacy: avoid unnecessary network requests, telemetry, remote code execution, or access outside the vault.
|
||||
- Keep `.planning/` project artifacts under version control; keep transient GSD runtime state in `.gsd/` ignored.
|
||||
|
|
|
|||
|
|
@ -1,137 +0,0 @@
|
|||
# External node_modules guide
|
||||
|
||||
This guide documents the reusable external `node_modules` setup used by this project. It is intended for maintainers who want to keep dependency folders out of synced project directories such as OneDrive, while still allowing the project to work as a normal local `node_modules` project by default.
|
||||
|
||||
## Behavior
|
||||
|
||||
The setup has two modes:
|
||||
|
||||
| Mode | How it is selected | Where dependencies live |
|
||||
|---|---|---|
|
||||
| Local mode | `EXTERNAL_NODE_MODULES` is not set | `./node_modules` in the project directory |
|
||||
| External mode | `EXTERNAL_NODE_MODULES` points to a dependency directory | The directory named by `EXTERNAL_NODE_MODULES` |
|
||||
|
||||
Use the same install command in both modes:
|
||||
|
||||
```powershell
|
||||
npm run deps:install
|
||||
```
|
||||
|
||||
- In local mode, this behaves like `npm install`.
|
||||
- In external mode, this installs with `npm --prefix <external-root>` and resolves tools from the external dependency directory.
|
||||
|
||||
Do not create a symlink or junction from the project directory back to the external dependency directory. The scripts resolve tools explicitly.
|
||||
|
||||
## PowerShell setup
|
||||
|
||||
Add a reusable `setenv` function to your PowerShell Profile, then run it from the project root before npm commands:
|
||||
|
||||
```powershell
|
||||
setenv
|
||||
```
|
||||
|
||||
A minimal Profile function is:
|
||||
|
||||
```powershell
|
||||
function setenv {
|
||||
$ProjectRoot = Get-Location
|
||||
$ProjectName = Split-Path -Leaf $ProjectRoot
|
||||
$ExternalRoot = "C:/local_data/$ProjectName"
|
||||
$ExternalNodeModules = "$ExternalRoot/node_modules"
|
||||
$ExternalNodeBin = "$ExternalNodeModules/.bin"
|
||||
|
||||
New-Item -ItemType Directory -Force -Path $ExternalRoot | Out-Null
|
||||
|
||||
$ManifestFiles = @('package.json', 'package-lock.json', '.npmrc')
|
||||
foreach ($FileName in $ManifestFiles) {
|
||||
$Source = Join-Path $ProjectRoot $FileName
|
||||
if (Test-Path $Source) {
|
||||
Copy-Item -Path $Source -Destination (Join-Path $ExternalRoot $FileName) -Force
|
||||
}
|
||||
}
|
||||
|
||||
$env:EXTERNAL_NODE_MODULES = $ExternalNodeModules
|
||||
$env:NODE_PATH = $ExternalNodeModules
|
||||
|
||||
$ExistingPathEntries = $env:PATH -split ';' | Where-Object {
|
||||
$_ -and ($_.TrimEnd('\/') -ine $ExternalNodeBin.TrimEnd('\/'))
|
||||
}
|
||||
$env:PATH = (@($ExternalNodeBin) + $ExistingPathEntries) -join ';'
|
||||
}
|
||||
```
|
||||
|
||||
The function sets:
|
||||
|
||||
```powershell
|
||||
$env:EXTERNAL_NODE_MODULES = "C:/local_data/<project-folder>/node_modules"
|
||||
$env:NODE_PATH = $env:EXTERNAL_NODE_MODULES
|
||||
$env:PATH = "<external-node_modules>/.bin;..."
|
||||
```
|
||||
|
||||
For this repository, the default external dependency path is:
|
||||
|
||||
```text
|
||||
C:/local_data/obsidian-download-image/node_modules
|
||||
```
|
||||
|
||||
## Reusing in another project
|
||||
|
||||
Copy these files into another project:
|
||||
|
||||
```text
|
||||
scripts/with-external-node-modules.mjs
|
||||
scripts/external-npm.mjs
|
||||
scripts/run-tool.mjs
|
||||
scripts/eslint-loader.mjs
|
||||
```
|
||||
|
||||
Then add the `setenv` function to your PowerShell Profile once and reuse it across projects.
|
||||
|
||||
## package.json scripts
|
||||
|
||||
Use a wrapper for dependency installation and tool execution:
|
||||
|
||||
```json
|
||||
{
|
||||
"scripts": {
|
||||
"deps:install": "node scripts/external-npm.mjs install",
|
||||
"deps:clean": "node scripts/external-npm.mjs exec -- rimraf node_modules",
|
||||
"build": "node scripts/run-tool.mjs tsc -noEmit -skipLibCheck && node esbuild.config.mjs production",
|
||||
"lint": "node --import ./scripts/eslint-loader.mjs scripts/run-tool.mjs eslint .",
|
||||
"test": "node scripts/run-tool.mjs vitest run"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## esbuild
|
||||
|
||||
Load esbuild through the wrapper so the build works in both modes:
|
||||
|
||||
```js
|
||||
import {
|
||||
EXTERNAL_MODE,
|
||||
EXTERNAL_NODE_MODULES,
|
||||
createToolEnv,
|
||||
ensureToolNodeModules,
|
||||
requireTool,
|
||||
} from './scripts/with-external-node-modules.mjs';
|
||||
|
||||
ensureToolNodeModules();
|
||||
process.env = createToolEnv();
|
||||
|
||||
const esbuild = requireTool('esbuild');
|
||||
```
|
||||
|
||||
When bundling, pass external node paths only in external mode:
|
||||
|
||||
```js
|
||||
...(EXTERNAL_MODE ? { nodePaths: [EXTERNAL_NODE_MODULES] } : {}),
|
||||
```
|
||||
|
||||
## TypeScript, ESLint, and Vitest
|
||||
|
||||
- `scripts/run-tool.mjs` resolves `tsc`, `eslint`, and `vitest` from the configured dependency directory.
|
||||
- In external mode, it writes `.tsconfig.external-node-modules.json` so TypeScript and ESLint can resolve external packages and types.
|
||||
- `vitest.config.ts` adds `deps.moduleDirectories` in external mode so test dependencies resolve from the external directory.
|
||||
|
||||
`.tsconfig.external-node-modules.json` is generated and should stay ignored by Git.
|
||||
15
README.md
15
README.md
|
|
@ -76,23 +76,14 @@ This repository is also usable for local plugin development. Build the plugin, t
|
|||
|
||||
### Install dependencies
|
||||
|
||||
Use the project dependency wrapper instead of bare `npm install`:
|
||||
Install dependencies into this project's local `node_modules` directory:
|
||||
|
||||
```bash
|
||||
npm run deps:install
|
||||
```
|
||||
|
||||
By default, this behaves like `npm install` and creates `./node_modules` in the project directory.
|
||||
|
||||
If you want to keep dependencies outside this OneDrive-synced project directory, run the PowerShell Profile function `setenv` from the project root first so `EXTERNAL_NODE_MODULES` points at `C:/local_data/obsidian-download-image/node_modules`, then run:
|
||||
|
||||
```bash
|
||||
npm run deps:install
|
||||
```
|
||||
|
||||
Do not use bare `npm install` for the external dependency workflow. `EXTERNAL_NODE_MODULES` is a project-specific variable, not an npm setting, so npm will ignore it unless you go through `npm run deps:install`. A bare `npm install` will still create `./node_modules` in the current project directory.
|
||||
|
||||
See [External node_modules guide](EXTERNAL-NODE-MODULES-GUIDE.md) for details.
|
||||
This script runs `npm install` in the project root. Dependencies are always kept in
|
||||
`./node_modules`; no environment setup or external dependency directory is used.
|
||||
|
||||
### Run the development build
|
||||
|
||||
|
|
|
|||
|
|
@ -1,18 +1,6 @@
|
|||
import { builtinModules, createRequire } from "node:module";
|
||||
import { builtinModules } from "node:module";
|
||||
import esbuild from "esbuild";
|
||||
import process from "process";
|
||||
import {
|
||||
EXTERNAL_MODE,
|
||||
EXTERNAL_NODE_MODULES,
|
||||
createToolEnv,
|
||||
ensureToolNodeModules,
|
||||
requireTool,
|
||||
} from "./scripts/with-external-node-modules.mjs";
|
||||
|
||||
ensureToolNodeModules();
|
||||
process.env = createToolEnv();
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
const esbuild = requireTool("esbuild");
|
||||
|
||||
const banner =
|
||||
`/*
|
||||
|
|
@ -29,7 +17,6 @@ const context = await esbuild.context({
|
|||
},
|
||||
entryPoints: ["src/main.ts"],
|
||||
bundle: true,
|
||||
...(EXTERNAL_MODE ? { nodePaths: [EXTERNAL_NODE_MODULES] } : {}),
|
||||
external: [
|
||||
"obsidian",
|
||||
"electron",
|
||||
|
|
|
|||
|
|
@ -5,13 +5,12 @@
|
|||
"main": "main.js",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"deps:install": "node scripts/external-npm.mjs install",
|
||||
"deps:clean": "node scripts/external-npm.mjs exec -- rimraf node_modules",
|
||||
"deps:install": "npm install",
|
||||
"dev": "node esbuild.config.mjs",
|
||||
"build": "node scripts/run-tool.mjs tsc -noEmit -skipLibCheck && node esbuild.config.mjs production",
|
||||
"build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production",
|
||||
"version": "node version-bump.mjs && git add manifest.json versions.json",
|
||||
"lint": "node --import ./scripts/eslint-loader.mjs scripts/run-tool.mjs eslint .",
|
||||
"test": "node scripts/run-tool.mjs vitest run",
|
||||
"lint": "eslint .",
|
||||
"test": "vitest run",
|
||||
"verify-release": "node scripts/verify-release.mjs"
|
||||
},
|
||||
"keywords": [],
|
||||
|
|
|
|||
|
|
@ -1,8 +0,0 @@
|
|||
import { dirname, join } from 'node:path';
|
||||
import { register } from 'node:module';
|
||||
import { pathToFileURL } from 'node:url';
|
||||
import { resolveTool } from './with-external-node-modules.mjs';
|
||||
|
||||
const jitiRegister = join(dirname(resolveTool('jiti/package.json')), 'lib/jiti-register.mjs');
|
||||
|
||||
register(pathToFileURL(jitiRegister), pathToFileURL(`${process.cwd()}/`));
|
||||
|
|
@ -1,46 +0,0 @@
|
|||
import { copyFileSync, existsSync, mkdirSync } from 'node:fs';
|
||||
import { basename, join } from 'node:path';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { EXTERNAL_MODE, EXTERNAL_ROOT } from './with-external-node-modules.mjs';
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
const command = args[0] ?? 'install';
|
||||
const forwarded = args.slice(1);
|
||||
|
||||
function syncPackageManifests() {
|
||||
if (!EXTERNAL_MODE || !['ci', 'install', 'i'].includes(command)) {
|
||||
return;
|
||||
}
|
||||
|
||||
mkdirSync(EXTERNAL_ROOT, { recursive: true });
|
||||
|
||||
for (const manifestPath of ['package.json', 'package-lock.json']) {
|
||||
if (existsSync(manifestPath)) {
|
||||
copyFileSync(manifestPath, join(EXTERNAL_ROOT, basename(manifestPath)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
syncPackageManifests();
|
||||
|
||||
const npmArgs = EXTERNAL_MODE
|
||||
? [command, '--prefix', EXTERNAL_ROOT, ...forwarded]
|
||||
: [command, ...forwarded];
|
||||
|
||||
const npmCommand = process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
||||
const result = spawnSync(npmCommand, npmArgs, {
|
||||
stdio: 'inherit',
|
||||
cwd: process.cwd(),
|
||||
shell: process.platform === 'win32',
|
||||
env: EXTERNAL_MODE ? {
|
||||
...process.env,
|
||||
npm_config_prefix: EXTERNAL_ROOT,
|
||||
} : process.env,
|
||||
});
|
||||
|
||||
if (result.error) {
|
||||
console.error(result.error);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
process.exit(result.status ?? 1);
|
||||
|
|
@ -1,69 +0,0 @@
|
|||
import { readFileSync, writeFileSync } from 'node:fs';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import {
|
||||
EXTERNAL_MODE,
|
||||
EXTERNAL_NODE_MODULES,
|
||||
createToolEnv,
|
||||
resolveTool,
|
||||
} from './with-external-node-modules.mjs';
|
||||
|
||||
const command = process.argv[2];
|
||||
const rawArgs = process.argv.slice(3);
|
||||
|
||||
const tools = {
|
||||
tsc: () => resolveTool('typescript/bin/tsc'),
|
||||
eslint: () => join(dirname(resolveTool('eslint/package.json')), 'bin/eslint.js'),
|
||||
vitest: () => join(dirname(resolveTool('vitest/package.json')), 'vitest.mjs'),
|
||||
};
|
||||
|
||||
if (!command || !(command in tools)) {
|
||||
console.error(`Usage: node scripts/run-tool.mjs ${Object.keys(tools).join('|')} [args...]`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
function createExternalTsConfig() {
|
||||
const baseConfig = JSON.parse(readFileSync('tsconfig.json', 'utf8'));
|
||||
const compilerOptions = baseConfig.compilerOptions ?? {};
|
||||
const existingPaths = compilerOptions.paths?.['*'] ?? ['src/*', 'node_modules/*'];
|
||||
|
||||
baseConfig.compilerOptions = {
|
||||
...compilerOptions,
|
||||
paths: {
|
||||
...(compilerOptions.paths ?? {}),
|
||||
'*': [
|
||||
...existingPaths,
|
||||
`${EXTERNAL_NODE_MODULES.replaceAll('\\', '/')}/*`,
|
||||
],
|
||||
},
|
||||
typeRoots: [
|
||||
'node_modules/@types',
|
||||
`${EXTERNAL_NODE_MODULES.replaceAll('\\', '/')}/@types`,
|
||||
],
|
||||
};
|
||||
|
||||
const externalTsConfigPath = '.tsconfig.external-node-modules.json';
|
||||
writeFileSync(externalTsConfigPath, `${JSON.stringify(baseConfig, null, '\t')}\n`);
|
||||
return externalTsConfigPath;
|
||||
}
|
||||
|
||||
function argsForCommand() {
|
||||
if (EXTERNAL_MODE && (command === 'tsc' || command === 'eslint')) {
|
||||
const externalTsConfig = createExternalTsConfig();
|
||||
|
||||
if (command === 'tsc' && !rawArgs.includes('--project') && !rawArgs.includes('-p')) {
|
||||
return ['--project', externalTsConfig, ...rawArgs];
|
||||
}
|
||||
}
|
||||
|
||||
return rawArgs;
|
||||
}
|
||||
|
||||
const entry = tools[command]();
|
||||
const result = spawnSync(process.execPath, [entry, ...argsForCommand()], {
|
||||
stdio: 'inherit',
|
||||
cwd: process.cwd(),
|
||||
env: createToolEnv(),
|
||||
});
|
||||
|
||||
process.exit(result.status ?? 1);
|
||||
|
|
@ -1,64 +0,0 @@
|
|||
import { createRequire } from 'node:module';
|
||||
import { existsSync } from 'node:fs';
|
||||
import { delimiter, dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
export const EXTERNAL_NODE_MODULES_ENV = 'EXTERNAL_NODE_MODULES';
|
||||
export const EXTERNAL_NODE_MODULES = process.env[EXTERNAL_NODE_MODULES_ENV]?.trim() || '';
|
||||
export const EXTERNAL_MODE = EXTERNAL_NODE_MODULES.length > 0;
|
||||
export const EXTERNAL_ROOT = EXTERNAL_MODE ? dirname(EXTERNAL_NODE_MODULES) : process.cwd();
|
||||
export const EXTERNAL_BIN = EXTERNAL_MODE ? join(EXTERNAL_NODE_MODULES, '.bin') : join(process.cwd(), 'node_modules/.bin');
|
||||
|
||||
const localRequire = createRequire(import.meta.url);
|
||||
const externalRequire = EXTERNAL_MODE ? createRequire(join(EXTERNAL_ROOT, 'package.json')) : localRequire;
|
||||
|
||||
export function requireTool(specifier) {
|
||||
return externalRequire(specifier);
|
||||
}
|
||||
|
||||
export function resolveTool(specifier) {
|
||||
return externalRequire.resolve(specifier);
|
||||
}
|
||||
|
||||
export function resolveOptionalTool(specifier) {
|
||||
try {
|
||||
return resolveTool(specifier);
|
||||
} catch (error) {
|
||||
if (EXTERNAL_MODE) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return localRequire.resolve(specifier);
|
||||
}
|
||||
}
|
||||
|
||||
export function ensureToolNodeModules() {
|
||||
if (EXTERNAL_MODE && !existsSync(EXTERNAL_NODE_MODULES)) {
|
||||
throw new Error(
|
||||
`${EXTERNAL_NODE_MODULES_ENV} points to ${EXTERNAL_NODE_MODULES}, but that directory does not exist. Run "npm run deps:install" after running the PowerShell Profile function setenv, or unset ${EXTERNAL_NODE_MODULES_ENV} to use local node_modules.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function createToolEnv(extra = {}) {
|
||||
ensureToolNodeModules();
|
||||
|
||||
if (!EXTERNAL_MODE) {
|
||||
return {
|
||||
...process.env,
|
||||
...extra,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...process.env,
|
||||
...extra,
|
||||
npm_config_prefix: EXTERNAL_ROOT,
|
||||
NODE_PATH: [EXTERNAL_NODE_MODULES, process.env.NODE_PATH].filter(Boolean).join(delimiter),
|
||||
PATH: [EXTERNAL_BIN, process.env.PATH].filter(Boolean).join(delimiter),
|
||||
};
|
||||
}
|
||||
|
||||
export function localScriptPath(relativePath) {
|
||||
return fileURLToPath(new URL(relativePath, import.meta.url));
|
||||
}
|
||||
|
|
@ -1,8 +1,7 @@
|
|||
import path from 'path';
|
||||
import { EXTERNAL_MODE, EXTERNAL_NODE_MODULES, EXTERNAL_ROOT } from './scripts/with-external-node-modules.mjs';
|
||||
import { defineConfig } from 'vitest/config';
|
||||
|
||||
export default {
|
||||
...(EXTERNAL_MODE ? { cacheDir: `${EXTERNAL_ROOT}/.vite-cache` } : {}),
|
||||
export default defineConfig({
|
||||
resolve: {
|
||||
alias: {
|
||||
obsidian: path.resolve(__dirname, 'tests/__mocks__/obsidian.ts'),
|
||||
|
|
@ -13,8 +12,5 @@ export default {
|
|||
test: {
|
||||
environment: 'node',
|
||||
include: ['tests/**/*.test.ts'],
|
||||
deps: {
|
||||
...(EXTERNAL_MODE ? { moduleDirectories: [EXTERNAL_NODE_MODULES] } : {}),
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue