@nocobase/utils 2.1.0-beta.8 → 2.2.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.d.ts CHANGED
@@ -44,4 +44,6 @@ export * from './variable-usage';
44
44
  export * from './wrap-middleware';
45
45
  export * from './run-sql';
46
46
  export * from './liquidjs';
47
+ export * from './server-request';
48
+ export * from './storage-path';
47
49
  export { lodash };
package/lib/index.js CHANGED
@@ -80,6 +80,8 @@ __reExport(src_exports, require("./variable-usage"), module.exports);
80
80
  __reExport(src_exports, require("./wrap-middleware"), module.exports);
81
81
  __reExport(src_exports, require("./run-sql"), module.exports);
82
82
  __reExport(src_exports, require("./liquidjs"), module.exports);
83
+ __reExport(src_exports, require("./server-request"), module.exports);
84
+ __reExport(src_exports, require("./storage-path"), module.exports);
83
85
  // Annotate the CommonJS export names for ESM import in node:
84
86
  0 && (module.exports = {
85
87
  Schema,
@@ -119,5 +121,7 @@ __reExport(src_exports, require("./liquidjs"), module.exports);
119
121
  ...require("./variable-usage"),
120
122
  ...require("./wrap-middleware"),
121
123
  ...require("./run-sql"),
122
- ...require("./liquidjs")
124
+ ...require("./liquidjs"),
125
+ ...require("./server-request"),
126
+ ...require("./storage-path")
123
127
  });
@@ -128,6 +128,10 @@ function isDate(input) {
128
128
  return input instanceof Date || Object.prototype.toString.call(input) === "[object Date]";
129
129
  }
130
130
  __name(isDate, "isDate");
131
+ function isDateFieldWithoutTimezone(field) {
132
+ return (field == null ? void 0 : field.type) === "dateOnly" || (field == null ? void 0 : field.type) === "datetimeNoTz" || (field == null ? void 0 : field.constructor.name) === "DateOnlyField" || (field == null ? void 0 : field.constructor.name) === "DatetimeNoTzField";
133
+ }
134
+ __name(isDateFieldWithoutTimezone, "isDateFieldWithoutTimezone");
131
135
  const dateValueWrapper = /* @__PURE__ */ __name((value, timezone) => {
132
136
  if (!value) {
133
137
  return null;
@@ -195,7 +199,7 @@ const parseFilter = /* @__PURE__ */ __name(async (filter, opts = {}) => {
195
199
  }
196
200
  if (isDateOperator(operator)) {
197
201
  const field = getField == null ? void 0 : getField(path);
198
- if ((field == null ? void 0 : field.constructor.name) === "DateOnlyField" || (field == null ? void 0 : field.constructor.name) === "DatetimeNoTzField") {
202
+ if (isDateFieldWithoutTimezone(field)) {
199
203
  if (value.type) {
200
204
  return (0, import_dateRangeUtils.getDayRangeByParams)({ ...value, timezone: (field == null ? void 0 : field.timezone) || timezone });
201
205
  }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
9
+ import { AxiosRequestConfig, AxiosResponse } from 'axios';
10
+ /**
11
+ * Match a hostname against a domain pattern.
12
+ * `*.example.com` matches exactly one subdomain level (e.g. `foo.example.com`)
13
+ * but not `example.com` itself or deeper levels like `a.b.example.com`.
14
+ */
15
+ export declare function matchesDomainPattern(hostname: string, pattern: string): boolean;
16
+ /**
17
+ * Validate a URL against the SERVER_REQUEST_WHITELIST environment variable.
18
+ *
19
+ * Throws an error if:
20
+ * - The URL scheme is not http or https.
21
+ * - SERVER_REQUEST_WHITELIST is set and the host does not match any entry.
22
+ *
23
+ * Silently returns for relative URLs (no scheme) so that internal API calls
24
+ * that use a relative path are not affected.
25
+ *
26
+ * Prefer using {@link serverRequest} over calling this directly.
27
+ */
28
+ export declare function checkUrlAgainstWhitelist(url?: string): void;
29
+ /**
30
+ * Drop-in replacement for `axios.request()` with built-in SSRF protection.
31
+ *
32
+ * Validates `config.url` against {@link checkUrlAgainstWhitelist} before
33
+ * forwarding to axios. Use this instead of calling axios directly for all
34
+ * server-initiated outbound HTTP requests.
35
+ */
36
+ export declare function serverRequest<T = any>(config: AxiosRequestConfig): Promise<AxiosResponse<T>>;
@@ -0,0 +1,128 @@
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
9
+
10
+ var __create = Object.create;
11
+ var __defProp = Object.defineProperty;
12
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
13
+ var __getOwnPropNames = Object.getOwnPropertyNames;
14
+ var __getProtoOf = Object.getPrototypeOf;
15
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
16
+ var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
17
+ var __export = (target, all) => {
18
+ for (var name in all)
19
+ __defProp(target, name, { get: all[name], enumerable: true });
20
+ };
21
+ var __copyProps = (to, from, except, desc) => {
22
+ if (from && typeof from === "object" || typeof from === "function") {
23
+ for (let key of __getOwnPropNames(from))
24
+ if (!__hasOwnProp.call(to, key) && key !== except)
25
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
26
+ }
27
+ return to;
28
+ };
29
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
30
+ // If the importer is in node compatibility mode or this is not an ESM
31
+ // file that has been converted to a CommonJS file using a Babel-
32
+ // compatible transform (i.e. "__esModule" has not been set), then set
33
+ // "default" to the CommonJS "module.exports" for node compatibility.
34
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
35
+ mod
36
+ ));
37
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
38
+ var server_request_exports = {};
39
+ __export(server_request_exports, {
40
+ checkUrlAgainstWhitelist: () => checkUrlAgainstWhitelist,
41
+ matchesDomainPattern: () => matchesDomainPattern,
42
+ serverRequest: () => serverRequest
43
+ });
44
+ module.exports = __toCommonJS(server_request_exports);
45
+ var import_ipaddr = __toESM(require("ipaddr.js"));
46
+ var import_axios = __toESM(require("axios"));
47
+ const ALLOWED_PROTOCOLS = /* @__PURE__ */ new Set(["http:", "https:"]);
48
+ function matchesIpEntry(hostname, entry) {
49
+ try {
50
+ let addr = import_ipaddr.default.parse(hostname);
51
+ if (addr.kind() === "ipv6" && addr.isIPv4MappedAddress()) {
52
+ addr = addr.toIPv4Address();
53
+ }
54
+ if (entry.includes("/")) {
55
+ const cidr = import_ipaddr.default.parseCIDR(entry);
56
+ if (addr.kind() !== cidr[0].kind()) return false;
57
+ if (addr.kind() === "ipv4") {
58
+ return addr.match(cidr);
59
+ }
60
+ return addr.match(cidr);
61
+ }
62
+ let entryAddr = import_ipaddr.default.parse(entry);
63
+ if (entryAddr.kind() === "ipv6" && entryAddr.isIPv4MappedAddress()) {
64
+ entryAddr = entryAddr.toIPv4Address();
65
+ }
66
+ return addr.toString() === entryAddr.toString();
67
+ } catch {
68
+ return false;
69
+ }
70
+ }
71
+ __name(matchesIpEntry, "matchesIpEntry");
72
+ function matchesDomainPattern(hostname, pattern) {
73
+ const h = hostname.toLowerCase();
74
+ const p = pattern.toLowerCase();
75
+ if (p.startsWith("*.")) {
76
+ const suffix = p.slice(1);
77
+ if (!h.endsWith(suffix) || h.length <= suffix.length) return false;
78
+ const prefix = h.slice(0, h.length - suffix.length);
79
+ return !prefix.includes(".");
80
+ }
81
+ return h === p;
82
+ }
83
+ __name(matchesDomainPattern, "matchesDomainPattern");
84
+ function matchesEntry(hostname, entry) {
85
+ const e = entry.trim();
86
+ if (!e) return false;
87
+ return import_ipaddr.default.isValid(hostname) ? matchesIpEntry(hostname, e) : matchesDomainPattern(hostname, e);
88
+ }
89
+ __name(matchesEntry, "matchesEntry");
90
+ function checkUrlAgainstWhitelist(url) {
91
+ if (!url) return;
92
+ if (!url.includes("://")) return;
93
+ let parsed;
94
+ try {
95
+ parsed = new URL(url);
96
+ } catch {
97
+ return;
98
+ }
99
+ if (!ALLOWED_PROTOCOLS.has(parsed.protocol)) {
100
+ throw new Error(
101
+ `URL scheme "${parsed.protocol.replace(":", "")}" is not allowed. Only http and https are permitted.`
102
+ );
103
+ }
104
+ const whitelist = process.env.SERVER_REQUEST_WHITELIST;
105
+ if (!whitelist || !whitelist.trim()) return;
106
+ const entries = whitelist.split(",").map((e) => e.trim()).filter(Boolean);
107
+ if (entries.length === 0) return;
108
+ const { hostname } = parsed;
109
+ const host = hostname.startsWith("[") && hostname.endsWith("]") ? hostname.slice(1, -1) : hostname;
110
+ for (const entry of entries) {
111
+ if (matchesEntry(host, entry)) return;
112
+ }
113
+ throw new Error(
114
+ `Outbound request to "${host}" is blocked. Add it to SERVER_REQUEST_WHITELIST to allow this request.`
115
+ );
116
+ }
117
+ __name(checkUrlAgainstWhitelist, "checkUrlAgainstWhitelist");
118
+ async function serverRequest(config) {
119
+ checkUrlAgainstWhitelist(config.url);
120
+ return import_axios.default.request(config);
121
+ }
122
+ __name(serverRequest, "serverRequest");
123
+ // Annotate the CommonJS export names for ESM import in node:
124
+ 0 && (module.exports = {
125
+ checkUrlAgainstWhitelist,
126
+ matchesDomainPattern,
127
+ serverRequest
128
+ });
@@ -0,0 +1,25 @@
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
9
+ /**
10
+ * Absolute path to the application storage root (same rules as CLI `resolveStorageRoot` in `cli-v1/src/util.js`).
11
+ */
12
+ export declare function resolveStorageRoot(): string;
13
+ /**
14
+ * Join path segments under the application storage root.
15
+ * Resolution matches CLI `resolveStorageRoot()` / `initEnv`: use `STORAGE_PATH` when set
16
+ * (absolute or relative to cwd), otherwise `<cwd>/storage`.
17
+ *
18
+ * @example storagePathJoin('tmp')
19
+ * @example storagePathJoin('cache', 'apps', appName)
20
+ */
21
+ export declare function storagePathJoin(...segments: string[]): string;
22
+ /**
23
+ * Resolve plugin storage path: `PLUGIN_STORAGE_PATH` first, else `<STORAGE_PATH>/plugins`.
24
+ */
25
+ export declare function resolvePluginStoragePath(): string;
@@ -0,0 +1,71 @@
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
9
+
10
+ var __create = Object.create;
11
+ var __defProp = Object.defineProperty;
12
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
13
+ var __getOwnPropNames = Object.getOwnPropertyNames;
14
+ var __getProtoOf = Object.getPrototypeOf;
15
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
16
+ var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
17
+ var __export = (target, all) => {
18
+ for (var name in all)
19
+ __defProp(target, name, { get: all[name], enumerable: true });
20
+ };
21
+ var __copyProps = (to, from, except, desc) => {
22
+ if (from && typeof from === "object" || typeof from === "function") {
23
+ for (let key of __getOwnPropNames(from))
24
+ if (!__hasOwnProp.call(to, key) && key !== except)
25
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
26
+ }
27
+ return to;
28
+ };
29
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
30
+ // If the importer is in node compatibility mode or this is not an ESM
31
+ // file that has been converted to a CommonJS file using a Babel-
32
+ // compatible transform (i.e. "__esModule" has not been set), then set
33
+ // "default" to the CommonJS "module.exports" for node compatibility.
34
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
35
+ mod
36
+ ));
37
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
38
+ var storage_path_exports = {};
39
+ __export(storage_path_exports, {
40
+ resolvePluginStoragePath: () => resolvePluginStoragePath,
41
+ resolveStorageRoot: () => resolveStorageRoot,
42
+ storagePathJoin: () => storagePathJoin
43
+ });
44
+ module.exports = __toCommonJS(storage_path_exports);
45
+ var import_path = __toESM(require("path"));
46
+ function resolveStorageRoot() {
47
+ const raw = process.env.STORAGE_PATH;
48
+ if (raw) {
49
+ return import_path.default.isAbsolute(raw) ? raw : import_path.default.resolve(process.cwd(), raw);
50
+ }
51
+ return import_path.default.resolve(process.cwd(), "storage");
52
+ }
53
+ __name(resolveStorageRoot, "resolveStorageRoot");
54
+ function storagePathJoin(...segments) {
55
+ return import_path.default.join(resolveStorageRoot(), ...segments);
56
+ }
57
+ __name(storagePathJoin, "storagePathJoin");
58
+ function resolvePluginStoragePath() {
59
+ if (process.env.PLUGIN_STORAGE_PATH) {
60
+ const pluginStoragePath = process.env.PLUGIN_STORAGE_PATH;
61
+ return import_path.default.isAbsolute(pluginStoragePath) ? pluginStoragePath : import_path.default.resolve(process.cwd(), pluginStoragePath);
62
+ }
63
+ return storagePathJoin("plugins");
64
+ }
65
+ __name(resolvePluginStoragePath, "resolvePluginStoragePath");
66
+ // Annotate the CommonJS export names for ESM import in node:
67
+ 0 && (module.exports = {
68
+ resolvePluginStoragePath,
69
+ resolveStorageRoot,
70
+ storagePathJoin
71
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nocobase/utils",
3
- "version": "2.1.0-beta.8",
3
+ "version": "2.2.0-alpha.1",
4
4
  "main": "lib/index.js",
5
5
  "types": "./lib/index.d.ts",
6
6
  "license": "Apache-2.0",
@@ -8,16 +8,18 @@
8
8
  "@budibase/handlebars-helpers": "0.14.0",
9
9
  "@hapi/topo": "^6.0.0",
10
10
  "@rc-component/mini-decimal": "^1.1.0",
11
+ "axios": "^1.7.0",
11
12
  "dayjs": "^1.11.9",
12
13
  "deepmerge": "^4.2.2",
13
14
  "flat-to-nested": "^1.1.1",
14
15
  "fs-extra": "^11.1.1",
15
16
  "graphlib": "^2.1.8",
16
17
  "handlebars": "^4.7.8",
18
+ "ipaddr.js": "^1.9.1",
17
19
  "liquidjs": "^10.23.0",
18
20
  "multer": "^1.4.5-lts.2",
19
21
  "object-path": "^0.11.8",
20
22
  "ses": "^1.14.0"
21
23
  },
22
- "gitHead": "5099d561c5467292414c1e77ad6bad3730d97344"
24
+ "gitHead": "303663aba6c6eefa27e6a6435b4c0352074ec40f"
23
25
  }
@@ -0,0 +1,45 @@
1
+ export interface PluginPackageInfo {
2
+ name: string;
3
+ packageName: string;
4
+ origins: string[];
5
+ resolvedPath: string;
6
+ }
7
+
8
+ export interface ParsePluginNameOptions {
9
+ nodeModulesPath?: string;
10
+ pluginPackagePrefixes?: string[];
11
+ }
12
+
13
+ export interface PresetPackageJsonLike {
14
+ dependencies?: Record<string, string>;
15
+ builtIn?: string[];
16
+ deprecated?: string[];
17
+ }
18
+
19
+ export declare const DEFAULT_PLUGIN_PACKAGE_PREFIXES: string[];
20
+ export declare function splitPluginNames(value?: string): string[];
21
+ export declare function getPluginPackagePrefixes(): string[];
22
+ export declare function getPluginNameFromPackageName(packageName: string, prefixes?: string[]): string;
23
+ export declare function isValidPackageName(packageName: string): boolean;
24
+ export declare function looksLikePluginPackage(packageName: string, prefixes?: string[]): boolean;
25
+ export declare function parsePluginName(
26
+ nameOrPkg: string,
27
+ options?: ParsePluginNameOptions,
28
+ ): Promise<{ name: string; packageName: string }>;
29
+ export declare function getPresetNocoBasePackageJson(options?: {
30
+ nodeModulesPath?: string;
31
+ cwd?: string;
32
+ }): Promise<PresetPackageJsonLike | null>;
33
+ export declare function resolvePluginPackagePath(
34
+ packageName: string,
35
+ options?: {
36
+ nodeModulesPath?: string;
37
+ storagePluginsPath?: string;
38
+ },
39
+ ): Promise<string>;
40
+ export declare function discoverPluginPackages(options?: {
41
+ nodeModulesPath?: string;
42
+ storagePluginsPath?: string;
43
+ cwd?: string;
44
+ pluginPackagePrefixes?: string[];
45
+ }): Promise<PluginPackageInfo[]>;
@@ -0,0 +1,268 @@
1
+ const fs = require('fs-extra');
2
+ const path = require('path');
3
+ const {
4
+ getPluginSourceRoots,
5
+ getStoragePluginNames,
6
+ resolvePluginSourcePath,
7
+ resolvePluginStoragePath,
8
+ } = require('./plugin-symlink');
9
+
10
+ const DEFAULT_PLUGIN_PACKAGE_PREFIXES = ['@nocobase/plugin-', '@nocobase/preset-'];
11
+ const PRESET_PACKAGE_NAME = '@nocobase/preset-nocobase';
12
+
13
+ function uniqStrings(values) {
14
+ return [...new Set(values.filter(Boolean))];
15
+ }
16
+
17
+ function splitPluginNames(value) {
18
+ return uniqStrings(
19
+ String(value || '')
20
+ .split(',')
21
+ .map((item) => item.trim())
22
+ .filter(Boolean),
23
+ );
24
+ }
25
+
26
+ function getPluginPackagePrefixes() {
27
+ const prefixes = splitPluginNames(process.env.PLUGIN_PACKAGE_PREFIX || DEFAULT_PLUGIN_PACKAGE_PREFIXES.join(','));
28
+ return prefixes.length > 0 ? prefixes : DEFAULT_PLUGIN_PACKAGE_PREFIXES;
29
+ }
30
+
31
+ function getPluginNameFromPackageName(packageName, prefixes = getPluginPackagePrefixes()) {
32
+ const prefix = prefixes.find((item) => packageName.startsWith(item));
33
+ return prefix ? packageName.slice(prefix.length) : packageName;
34
+ }
35
+
36
+ function isValidPackageName(packageName) {
37
+ if (!packageName || typeof packageName !== 'string') {
38
+ return false;
39
+ }
40
+
41
+ if (packageName.includes('\0')) {
42
+ return false;
43
+ }
44
+
45
+ if (path.isAbsolute(packageName)) {
46
+ return false;
47
+ }
48
+
49
+ if (packageName.includes('..') || packageName.includes('\\')) {
50
+ return false;
51
+ }
52
+
53
+ return /^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/i.test(packageName);
54
+ }
55
+
56
+ function looksLikePluginPackage(packageName) {
57
+ return isValidPackageName(packageName);
58
+ }
59
+
60
+ async function parsePluginName(nameOrPkg, options = {}) {
61
+ const input = String(nameOrPkg || '').trim();
62
+ const prefixes = options.pluginPackagePrefixes || getPluginPackagePrefixes();
63
+
64
+ if (!input) {
65
+ return { name: '', packageName: '' };
66
+ }
67
+
68
+ const matchedPrefix = prefixes.find((prefix) => input.startsWith(prefix));
69
+ if (matchedPrefix) {
70
+ return {
71
+ packageName: input,
72
+ name: input.slice(matchedPrefix.length),
73
+ };
74
+ }
75
+
76
+ const nodeModulesPath = String(options.nodeModulesPath || process.env.NODE_MODULES_PATH || '').trim();
77
+ if (nodeModulesPath) {
78
+ for (const prefix of prefixes) {
79
+ const candidate = `${prefix}${input}`;
80
+ if (await fs.pathExists(path.resolve(nodeModulesPath, candidate, 'package.json'))) {
81
+ return { name: input, packageName: candidate };
82
+ }
83
+ }
84
+ }
85
+
86
+ return { name: input, packageName: input };
87
+ }
88
+
89
+ function getPresetPackageJsonCandidates(options = {}) {
90
+ const candidates = [];
91
+ const nodeModulesPath = String(options.nodeModulesPath || process.env.NODE_MODULES_PATH || '').trim();
92
+
93
+ if (nodeModulesPath) {
94
+ candidates.push(path.resolve(nodeModulesPath, PRESET_PACKAGE_NAME, 'package.json'));
95
+ }
96
+
97
+ candidates.push(path.resolve(options.cwd || process.cwd(), 'packages/presets/nocobase/package.json'));
98
+ return uniqStrings(candidates);
99
+ }
100
+
101
+ async function getPresetNocoBasePackageJson(options = {}) {
102
+ for (const packageJsonPath of getPresetPackageJsonCandidates(options)) {
103
+ if (await fs.pathExists(packageJsonPath)) {
104
+ return fs.readJson(packageJsonPath);
105
+ }
106
+ }
107
+ return null;
108
+ }
109
+
110
+ async function getNodeModulesEntryType(linkPath) {
111
+ try {
112
+ const statResult = await fs.lstat(linkPath);
113
+ if (statResult.isSymbolicLink()) {
114
+ return 'symlink';
115
+ }
116
+ if (statResult.isDirectory()) {
117
+ return 'dir';
118
+ }
119
+ return 'other';
120
+ } catch (error) {
121
+ if (error.code === 'ENOENT') {
122
+ return 'missing';
123
+ }
124
+ return 'other';
125
+ }
126
+ }
127
+
128
+ async function resolvePluginPackagePath(packageName, options = {}) {
129
+ const normalizedPackageName = String(packageName || '').trim();
130
+ if (!normalizedPackageName) {
131
+ return '';
132
+ }
133
+
134
+ const nodeModulesPath = String(options.nodeModulesPath || process.env.NODE_MODULES_PATH || '').trim();
135
+ const storagePluginsPath = options.storagePluginsPath || resolvePluginStoragePath();
136
+ const nodeModulesPackagePath = nodeModulesPath ? path.resolve(nodeModulesPath, normalizedPackageName) : '';
137
+
138
+ if (nodeModulesPackagePath) {
139
+ const entryType = await getNodeModulesEntryType(nodeModulesPackagePath);
140
+ if (entryType === 'dir') {
141
+ return nodeModulesPackagePath;
142
+ }
143
+ }
144
+
145
+ const sourcePath = await resolvePluginSourcePath(normalizedPackageName, storagePluginsPath);
146
+ if (sourcePath) {
147
+ return sourcePath;
148
+ }
149
+
150
+ if (nodeModulesPackagePath && (await fs.pathExists(path.resolve(nodeModulesPackagePath, 'package.json')))) {
151
+ return nodeModulesPackagePath;
152
+ }
153
+
154
+ return '';
155
+ }
156
+
157
+ async function readPluginPackagesFromRoot(rootPath) {
158
+ if (!rootPath || !(await fs.pathExists(rootPath))) {
159
+ return [];
160
+ }
161
+
162
+ const relativePluginDirs = await getStoragePluginNames(rootPath);
163
+ const items = [];
164
+
165
+ for (const relativePluginDir of relativePluginDirs) {
166
+ const packageJsonPath = path.resolve(rootPath, relativePluginDir, 'package.json');
167
+ if (!(await fs.pathExists(packageJsonPath))) {
168
+ continue;
169
+ }
170
+ const packageJson = await fs.readJson(packageJsonPath);
171
+ const packageName = String(packageJson?.name || '').trim();
172
+
173
+ if (!looksLikePluginPackage(packageName)) {
174
+ continue;
175
+ }
176
+
177
+ items.push({
178
+ packageName,
179
+ packagePath: path.resolve(rootPath, relativePluginDir),
180
+ sourceRoot: rootPath,
181
+ });
182
+ }
183
+
184
+ return items;
185
+ }
186
+
187
+ function pushOrigin(targetMap, packageName, origin) {
188
+ if (!packageName) {
189
+ return;
190
+ }
191
+ if (!targetMap.has(packageName)) {
192
+ targetMap.set(packageName, new Set());
193
+ }
194
+ targetMap.get(packageName).add(origin);
195
+ }
196
+
197
+ async function discoverPluginPackages(options = {}) {
198
+ const nodeModulesPath = String(options.nodeModulesPath || process.env.NODE_MODULES_PATH || '').trim();
199
+ const storagePluginsPath = options.storagePluginsPath || resolvePluginStoragePath();
200
+ const prefixes = options.pluginPackagePrefixes || getPluginPackagePrefixes();
201
+ const originsByPackageName = new Map();
202
+
203
+ const presetPackageJson = await getPresetNocoBasePackageJson({ nodeModulesPath, cwd: options.cwd });
204
+ const presetDependencies = Object.keys(presetPackageJson?.dependencies || {}).filter((packageName) =>
205
+ packageName.startsWith('@nocobase/plugin-'),
206
+ );
207
+
208
+ for (const packageName of presetDependencies) {
209
+ pushOrigin(originsByPackageName, packageName, 'preset-dependency');
210
+ }
211
+
212
+ const sourceRoots = getPluginSourceRoots(storagePluginsPath);
213
+ for (const sourceRoot of sourceRoots) {
214
+ const packages = await readPluginPackagesFromRoot(sourceRoot);
215
+ for (const item of packages) {
216
+ pushOrigin(originsByPackageName, item.packageName, sourceRoot);
217
+ }
218
+ }
219
+
220
+ for (const packageNameOrName of splitPluginNames(process.env.APPEND_PRESET_BUILT_IN_PLUGINS)) {
221
+ const { packageName } = await parsePluginName(packageNameOrName, {
222
+ nodeModulesPath,
223
+ pluginPackagePrefixes: prefixes,
224
+ });
225
+ pushOrigin(originsByPackageName, packageName, 'append-built-in');
226
+ }
227
+
228
+ for (const packageNameOrName of splitPluginNames(process.env.APPEND_PRESET_LOCAL_PLUGINS)) {
229
+ const { packageName } = await parsePluginName(packageNameOrName, {
230
+ nodeModulesPath,
231
+ pluginPackagePrefixes: prefixes,
232
+ });
233
+ pushOrigin(originsByPackageName, packageName, 'append-local');
234
+ }
235
+
236
+ const items = [];
237
+ for (const [packageName, origins] of originsByPackageName.entries()) {
238
+ const resolvedPath = await resolvePluginPackagePath(packageName, {
239
+ nodeModulesPath,
240
+ storagePluginsPath,
241
+ });
242
+
243
+ if (!resolvedPath) {
244
+ continue;
245
+ }
246
+
247
+ const { name } = await parsePluginName(packageName, { nodeModulesPath, pluginPackagePrefixes: prefixes });
248
+ items.push({
249
+ name,
250
+ packageName,
251
+ origins: [...origins],
252
+ resolvedPath,
253
+ });
254
+ }
255
+
256
+ return items.sort((a, b) => a.packageName.localeCompare(b.packageName));
257
+ }
258
+
259
+ exports.DEFAULT_PLUGIN_PACKAGE_PREFIXES = DEFAULT_PLUGIN_PACKAGE_PREFIXES;
260
+ exports.getPluginPackagePrefixes = getPluginPackagePrefixes;
261
+ exports.getPluginNameFromPackageName = getPluginNameFromPackageName;
262
+ exports.getPresetNocoBasePackageJson = getPresetNocoBasePackageJson;
263
+ exports.isValidPackageName = isValidPackageName;
264
+ exports.looksLikePluginPackage = looksLikePluginPackage;
265
+ exports.parsePluginName = parsePluginName;
266
+ exports.resolvePluginPackagePath = resolvePluginPackagePath;
267
+ exports.discoverPluginPackages = discoverPluginPackages;
268
+ exports.splitPluginNames = splitPluginNames;
@@ -1,6 +1,9 @@
1
+ export declare function resolvePluginStoragePath(): string;
1
2
  export declare function getStoragePluginNames(target: any): Promise<any[]>;
2
- export declare function fsExists(path: any): Promise<boolean>;
3
+ export declare function getPluginSourceRoots(storagePluginsPath: string): string[];
4
+ export declare function resolvePluginSourcePath(pluginName: string, storagePluginsPath: string): Promise<string>;
3
5
  export declare function createStoragePluginSymLink(pluginName: any): Promise<void>;
4
6
  export declare function createStoragePluginsSymlink(): Promise<void>;
5
7
  export declare function createDevPluginSymLink(pluginName: any): Promise<void>;
6
8
  export declare function createDevPluginsSymlink(): Promise<void>;
9
+ export declare function syncPluginSymlinks(): Promise<void>;
package/plugin-symlink.js CHANGED
@@ -1,6 +1,15 @@
1
- const { resolve } = require('path');
1
+ const path = require('path');
2
+ const { resolve } = path;
2
3
  const fs = require('fs-extra');
3
4
 
5
+ function resolvePluginStoragePath() {
6
+ if (process.env.PLUGIN_STORAGE_PATH) {
7
+ const p = process.env.PLUGIN_STORAGE_PATH;
8
+ return path.isAbsolute(p) ? p : path.resolve(process.cwd(), p);
9
+ }
10
+ return path.join(process.env.STORAGE_PATH || path.resolve(process.cwd(), 'storage'), 'plugins');
11
+ }
12
+
4
13
  /**
5
14
  * Recursively get plugin names from a directory
6
15
  * @param {string} target - Target directory to scan
@@ -28,6 +37,36 @@ async function getStoragePluginNames(target) {
28
37
  return plugins;
29
38
  }
30
39
 
40
+ async function getPluginNamesFromSourceRoot(rootPath) {
41
+ if (!(await fs.pathExists(rootPath))) {
42
+ return [];
43
+ }
44
+ return await getStoragePluginNames(rootPath);
45
+ }
46
+
47
+ async function isValidPluginSourcePath(candidate) {
48
+ return await fs.pathExists(resolve(candidate, 'package.json'));
49
+ }
50
+
51
+ function getPluginSourceRoots(storagePluginsPath) {
52
+ return [
53
+ resolve(process.cwd(), 'packages/plugins'),
54
+ resolve(process.cwd(), 'packages/pro-plugins'),
55
+ storagePluginsPath,
56
+ ];
57
+ }
58
+
59
+ async function resolvePluginSourcePath(pluginName, storagePluginsPath) {
60
+ const sourceRoots = getPluginSourceRoots(storagePluginsPath);
61
+ for (const rootPath of sourceRoots) {
62
+ const candidate = resolve(rootPath, pluginName);
63
+ if (await isValidPluginSourcePath(candidate)) {
64
+ return candidate;
65
+ }
66
+ }
67
+ return '';
68
+ }
69
+
31
70
  /**
32
71
  * Ensure the organization directory exists for scoped packages
33
72
  * @param {string} nodeModulesPath - Path to node_modules
@@ -43,22 +82,35 @@ async function ensureOrgDirectory(nodeModulesPath, pluginName) {
43
82
  }
44
83
 
45
84
  /**
46
- * Check if a symlink already points to the correct target
47
- * @param {string} linkPath - Path to the symlink
48
- * @param {string} targetPath - Expected target path
49
- * @returns {Promise<boolean>} True if symlink exists and points to target
85
+ * Check whether node_modules entry is a real directory, symlink, or missing
86
+ * @param {string} linkPath - Path inside node_modules
87
+ * @returns {Promise<'missing'|'dir'|'symlink'|'other'>}
50
88
  */
51
- async function isSymlinkValid(linkPath, targetPath) {
89
+ async function getNodeModulesEntryType(linkPath) {
52
90
  try {
53
- if (await fs.pathExists(linkPath)) {
54
- const realPath = await fs.realpath(linkPath);
55
- return realPath === targetPath;
91
+ const statResult = await fs.lstat(linkPath);
92
+ if (statResult.isSymbolicLink()) {
93
+ return 'symlink';
94
+ }
95
+ if (statResult.isDirectory()) {
96
+ return 'dir';
56
97
  }
98
+ return 'other';
57
99
  } catch (error) {
58
- // If realpath fails, the symlink is invalid
100
+ if (error.code === 'ENOENT') {
101
+ return 'missing';
102
+ }
103
+ return 'other';
104
+ }
105
+ }
106
+
107
+ async function isSymlinkValid(linkPath, targetPath) {
108
+ try {
109
+ const realPath = await fs.realpath(linkPath);
110
+ return realPath === targetPath;
111
+ } catch {
59
112
  return false;
60
113
  }
61
- return false;
62
114
  }
63
115
 
64
116
  /**
@@ -110,13 +162,18 @@ async function createPluginSymLink(pluginName, sourcePath, nodeModulesPath, plug
110
162
  return;
111
163
  }
112
164
 
165
+ const linkPath = resolve(nodeModulesPath, pluginName);
166
+ const entryType = await getNodeModulesEntryType(linkPath);
167
+
168
+ if (entryType === 'dir') {
169
+ return;
170
+ }
171
+
113
172
  // Ensure organization directory exists for scoped packages
114
173
  await ensureOrgDirectory(nodeModulesPath, pluginName);
115
174
 
116
- const linkPath = resolve(nodeModulesPath, pluginName);
117
-
118
175
  // Check if symlink already points to the correct target
119
- if (await isSymlinkValid(linkPath, targetPath)) {
176
+ if (entryType === 'symlink' && (await isSymlinkValid(linkPath, targetPath))) {
120
177
  return; // Symlink is already correct, no need to recreate
121
178
  }
122
179
 
@@ -133,10 +190,9 @@ async function createPluginSymLink(pluginName, sourcePath, nodeModulesPath, plug
133
190
  /**
134
191
  * Create a symlink for a storage plugin
135
192
  * @param {string} pluginName - Name of the plugin
136
- * @returns {Promise<void>}
137
193
  */
138
194
  async function createStoragePluginSymLink(pluginName) {
139
- const storagePluginsPath = resolve(process.cwd(), 'storage/plugins');
195
+ const storagePluginsPath = resolvePluginStoragePath();
140
196
  const nodeModulesPath = process.env.NODE_MODULES_PATH;
141
197
  await createPluginSymLink(pluginName, storagePluginsPath, nodeModulesPath, 'storage');
142
198
  }
@@ -146,7 +202,7 @@ async function createStoragePluginSymLink(pluginName) {
146
202
  * @returns {Promise<void>}
147
203
  */
148
204
  async function createStoragePluginsSymlink() {
149
- const storagePluginsPath = resolve(process.cwd(), 'storage/plugins');
205
+ const storagePluginsPath = resolvePluginStoragePath();
150
206
  if (!(await fs.pathExists(storagePluginsPath))) {
151
207
  return;
152
208
  }
@@ -178,7 +234,59 @@ async function createDevPluginsSymlink() {
178
234
  await Promise.all(pluginNames.map((pluginName) => createDevPluginSymLink(pluginName)));
179
235
  }
180
236
 
237
+ /**
238
+ * Create symlinks for all plugins from all sources
239
+ * @returns {Promise<void>}
240
+ */
241
+ async function syncPluginSymlinks() {
242
+ const nodeModulesPath = process.env.NODE_MODULES_PATH;
243
+ if (!nodeModulesPath) {
244
+ return;
245
+ }
246
+
247
+ const storagePluginsPath = resolvePluginStoragePath();
248
+ const sourceRoots = getPluginSourceRoots(storagePluginsPath);
249
+ const pluginNames = new Set();
250
+
251
+ for (const rootPath of sourceRoots) {
252
+ const names = await getPluginNamesFromSourceRoot(rootPath);
253
+ names.forEach((name) => pluginNames.add(name));
254
+ }
255
+
256
+ await Promise.all(
257
+ [...pluginNames].map(async (pluginName) => {
258
+ const linkPath = resolve(nodeModulesPath, pluginName);
259
+ const entryType = await getNodeModulesEntryType(linkPath);
260
+
261
+ if (entryType === 'dir') {
262
+ return;
263
+ }
264
+
265
+ const sourcePath = await resolvePluginSourcePath(pluginName, storagePluginsPath);
266
+ if (!sourcePath) {
267
+ if (entryType === 'symlink' || entryType === 'other') {
268
+ await removeExistingLink(linkPath, pluginName, 'plugin');
269
+ }
270
+ return;
271
+ }
272
+
273
+ if (entryType === 'symlink' && (await isSymlinkValid(linkPath, sourcePath))) {
274
+ return;
275
+ }
276
+
277
+ await ensureOrgDirectory(nodeModulesPath, pluginName);
278
+ await removeExistingLink(linkPath, pluginName, 'plugin');
279
+ await fs.symlink(sourcePath, linkPath, 'dir');
280
+ }),
281
+ );
282
+ }
283
+
284
+ exports.resolvePluginStoragePath = resolvePluginStoragePath;
285
+ exports.getStoragePluginNames = getStoragePluginNames;
286
+ exports.getPluginSourceRoots = getPluginSourceRoots;
287
+ exports.resolvePluginSourcePath = resolvePluginSourcePath;
181
288
  exports.createStoragePluginSymLink = createStoragePluginSymLink;
182
289
  exports.createStoragePluginsSymlink = createStoragePluginsSymlink;
183
290
  exports.createDevPluginSymLink = createDevPluginSymLink;
184
291
  exports.createDevPluginsSymlink = createDevPluginsSymlink;
292
+ exports.syncPluginSymlinks = syncPluginSymlinks;