diff --git a/packages/core/build/package.json b/packages/core/build/package.json index f58e2613f7e..c74881aa7f6 100644 --- a/packages/core/build/package.json +++ b/packages/core/build/package.json @@ -26,12 +26,14 @@ "@vercel/ncc": "0.36.1", "babel-loader": "^9.2.1", "babel-plugin-syntax-dynamic-import": "^6.18.0", + "bundle-require": "^5.1.0", "chalk": "2.4.2", "css-loader": "^6.8.1", "esbuild-register": "^3.4.2", "fast-glob": "^3.3.1", "gulp": "4.0.2", "gulp-typescript": "6.0.0-alpha.1", + "javascript-obfuscator": "^4.1.1", "less": "^4.2.0", "less-loader": "^12.2.0", "postcss": "^8.4.29", @@ -49,7 +51,8 @@ }, "license": "AGPL-3.0", "scripts": { - "build": "tsup" + "build": "tsup", + "build:watch": "tsup --watch" }, "gitHead": "d0b4efe4be55f8c79a98a331d99d9f8cf99021a1" } diff --git a/packages/core/build/src/buildPlugin.ts b/packages/core/build/src/buildPlugin.ts index ed8a766f95e..cfc02f0ef24 100644 --- a/packages/core/build/src/buildPlugin.ts +++ b/packages/core/build/src/buildPlugin.ts @@ -14,9 +14,8 @@ import fg from 'fast-glob'; import fs from 'fs-extra'; import path from 'path'; import { build as tsupBuild } from 'tsup'; - -import { RsdoctorRspackPlugin } from '@rsdoctor/rspack-plugin'; -import { EsbuildSupportExts, globExcludeFiles } from './constant'; +import * as bundleRequire from 'bundle-require'; +import { EsbuildSupportExts, globExcludeFiles, PLUGIN_COMMERCIAL } from './constant'; import { PkgLog, UserConfig, getPackageJson } from './utils'; import { buildCheck, @@ -27,6 +26,9 @@ import { getSourcePackages, } from './utils/buildPluginUtils'; import { getDepPkgPath, getDepsConfig } from './utils/getDepsConfig'; +import { RsdoctorRspackPlugin } from '@rsdoctor/rspack-plugin'; +import { obfuscate } from './utils/obfuscationResult'; +import pluginEsbuildCommercialInject from './plugins/pluginEsbuildCommercialInject'; const validExts = ['.ts', '.tsx', '.js', '.jsx', '.mjs']; const serverGlobalFiles: string[] = ['src/**', '!src/client/**', ...globExcludeFiles]; @@ -55,6 +57,7 @@ const external = [ '@nocobase/server', '@nocobase/test', '@nocobase/utils', + '@nocobase/license-kit', // @nocobase/auth 'jsonwebtoken', @@ -311,10 +314,112 @@ export async function buildPluginServer(cwd: string, userConfig: UserConfig, sou await buildServerDeps(cwd, serverFiles, log); } -export async function buildPluginClient(cwd: string, userConfig: UserConfig, sourcemap: boolean, log: PkgLog) { +export async function buildProPluginServer(cwd: string, userConfig: UserConfig, sourcemap: boolean, log: PkgLog) { + log('build pro plugin server source'); + const packageJson = getPackageJson(cwd); + const serverFiles = fg.globSync(serverGlobalFiles, { cwd, absolute: true }); + buildCheck({ cwd, packageJson, entry: 'server', files: serverFiles, log }); + const otherExts = Array.from( + new Set(serverFiles.map((item) => path.extname(item)).filter((item) => !EsbuildSupportExts.includes(item))), + ); + if (otherExts.length) { + log('%s will not be processed, only be copied to the dist directory.', chalk.yellow(otherExts.join(','))); + } + + deleteServerFiles(cwd, log); + + // remove compilerOptions.paths in tsconfig.json + let tsconfig = bundleRequire.loadTsConfig(path.join(cwd, 'tsconfig.json')); + fs.writeFileSync(path.join(cwd, 'tsconfig.json'), JSON.stringify({ + ...tsconfig.data, + compilerOptions: { ...tsconfig.data.compilerOptions, paths: [] } + }, null, 2)); + tsconfig = bundleRequire.loadTsConfig(path.join(cwd, 'tsconfig.json')); + + // convert all ts to js, some files may not be referenced by the entry file + await tsupBuild( + userConfig.modifyTsupConfig({ + entry: serverFiles, + splitting: false, + clean: false, + bundle: false, + silent: true, + treeshake: false, + target: 'node16', + sourcemap, + outDir: path.join(cwd, target_dir), + format: 'cjs', + skipNodeModulesBundle: true, + loader: { + ...otherExts.reduce((prev, cur) => ({ ...prev, [cur]: 'copy' }), {}), + '.json': 'copy', + }, + }), + ); + + const entryFile = path.join(cwd, 'src/server/index.ts'); + if (!fs.existsSync(entryFile)) { + log('server entry file not found', entryFile); + return; + } + + // plugin-commercial build to a bundle + const externalOptions = { + external: [], + noExternal: [], + onSuccess: async () => {}, + esbuildPlugins: [], + }; + // other plugins build to a bundle just include plugin-commercial + if (!cwd.includes(PLUGIN_COMMERCIAL)) { + externalOptions.external = [/^[./]/]; + externalOptions.noExternal = [entryFile, /@nocobase\/plugin-commercial\/server/, /dist\/server\/index\.js/]; + externalOptions.onSuccess = async () => { + const serverFiles = [path.join(cwd, target_dir, 'server', 'index.js')]; + serverFiles.forEach((file) => { + obfuscate(file); + }); + }; + externalOptions.esbuildPlugins = [pluginEsbuildCommercialInject]; + } + + // bundle all files、inject commercial code and obfuscate + await tsupBuild( + userConfig.modifyTsupConfig({ + entry: [entryFile], + // minify: true, + splitting: false, + clean: false, + bundle: true, + silent: true, + treeshake: false, + target: 'node16', + sourcemap, + outDir: path.join(cwd, target_dir, 'server'), + format: 'cjs', + skipNodeModulesBundle: true, + tsconfig: tsconfig.path, + loader: { + ...otherExts.reduce((prev, cur) => ({ ...prev, [cur]: 'copy' }), {}), + '.json': 'copy', + }, + + ...externalOptions, + }), + ); + fs.removeSync(tsconfig.path); + + await buildServerDeps(cwd, serverFiles, log); +} + +export async function buildPluginClient(cwd: string, userConfig: any, sourcemap: boolean, log: PkgLog, isCommercial = false) { log('build plugin client'); const packageJson = getPackageJson(cwd); const clientFiles = fg.globSync(clientGlobalFiles, { cwd, absolute: true }); + if (isCommercial) { + const commercialFiles = fg.globSync(clientGlobalFiles, { cwd: path.join(process.cwd(), 'packages/pro-plugins', PLUGIN_COMMERCIAL), absolute: true }); + clientFiles.push(...commercialFiles); + } const clientFileSource = clientFiles.map((item) => fs.readFileSync(item, 'utf-8')); const sourcePackages = getPackagesFromFiles(clientFileSource); const excludePackages = getExcludePackages(sourcePackages, external, pluginPrefix); @@ -440,31 +545,51 @@ export async function buildPluginClient(cwd: string, userConfig: UserConfig, sou { test: /\.tsx$/, exclude: /[\\/]node_modules[\\/]/, - loader: 'builtin:swc-loader', - options: { - sourceMap: true, - jsc: { - parser: { - syntax: 'typescript', - tsx: true, + use: [ + { + loader: 'builtin:swc-loader', + options: { + sourceMap: true, + jsc: { + parser: { + syntax: 'typescript', + tsx: true, + }, + target: 'es5', + }, }, - target: 'es5', }, - }, + { + loader: require.resolve('./plugins/pluginRspackCommercialLoader'), + options: { + isCommercial + } + } + ] }, { test: /\.ts$/, exclude: /[\\/]node_modules[\\/]/, - loader: 'builtin:swc-loader', - options: { - sourceMap: true, - jsc: { - parser: { - syntax: 'typescript', + use: [ + { + loader: 'builtin:swc-loader', + options: { + sourceMap: true, + jsc: { + parser: { + syntax: 'typescript', + }, + target: 'es5', + }, }, - target: 'es5', }, - }, + { + loader: require.resolve('./plugins/pluginRspackCommercialLoader'), + options: { + isCommercial + } + } + ] }, ], }, @@ -571,7 +696,12 @@ __webpack_require__.p = (function() { } export async function buildPlugin(cwd: string, userConfig: UserConfig, sourcemap: boolean, log: PkgLog) { - await buildPluginClient(cwd, userConfig, sourcemap, log); - await buildPluginServer(cwd, userConfig, sourcemap, log); + if (cwd.includes('/pro-plugins/') && fs.existsSync(path.join(process.cwd(), 'packages/pro-plugins/', PLUGIN_COMMERCIAL))) { + await buildPluginClient(cwd, userConfig, sourcemap, log, true); + await buildProPluginServer(cwd, userConfig, sourcemap, log); + } else { + await buildPluginClient(cwd, userConfig, sourcemap, log); + await buildPluginServer(cwd, userConfig, sourcemap, log); + } writeExternalPackageVersion(cwd, log); } diff --git a/packages/core/build/src/constant.ts b/packages/core/build/src/constant.ts index 43dd612c741..dde0397e6df 100644 --- a/packages/core/build/src/constant.ts +++ b/packages/core/build/src/constant.ts @@ -43,8 +43,11 @@ export const PLUGINS_DIR = ['plugins', 'samples', 'pro-plugins'] .filter(Boolean) .map((name) => path.join(PACKAGES_PATH, name)); export const PRESETS_DIR = path.join(PACKAGES_PATH, 'presets'); +export const PLUGIN_COMMERCIAL = '@nocobase/plugin-commercial'; export const getPluginPackages = (packages: Package[]) => - packages.filter((item) => PLUGINS_DIR.some((pluginDir) => item.location.startsWith(pluginDir))); + packages.filter((item) => PLUGINS_DIR.some((pluginDir) => item.location.startsWith(pluginDir))).sort((a, b) => { + return a.name === PLUGIN_COMMERCIAL ? -1 : 1; + }); export const getPresetsPackages = (packages: Package[]) => packages.filter((item) => item.location.startsWith(PRESETS_DIR)); export const CORE_APP = path.join(PACKAGES_PATH, 'core/app'); diff --git a/packages/core/build/src/plugins/pluginEsbuildCommercialInject.ts b/packages/core/build/src/plugins/pluginEsbuildCommercialInject.ts new file mode 100644 index 00000000000..bfdb45f3c33 --- /dev/null +++ b/packages/core/build/src/plugins/pluginEsbuildCommercialInject.ts @@ -0,0 +1,54 @@ +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. + * For more information, please refer to: https://www.nocobase.com/agreement. + */ + +import fs from 'node:fs' + +const pluginEsbuildCommercialInject = { + name: 'plugin-esbuild-commercial-inject', + setup(build) { + build.onLoad({ filter: /src\/server\/index\.ts$/ }, async (args) => { + let source = fs.readFileSync(args.path, 'utf8'); + const regex = /export\s*\{\s*default\s*\}\s*from\s*(?:'([^']*)'|"([^"]*)");?/; // match: export { default } from './plugin'; + const regex2 = /export\s+default\s+([a-zA-Z_0-9]+)\s*;?/; // match: export default xxx; + const match = source.match(regex); + const match2 = source.match(regex2); + if (match) { + source = source.replace(regex, ``); + const moduleName = match[1] || match[2]; + source = + ` +import { withCommercial } from '@nocobase/plugin-commercial/server'; +import _plugin from '${moduleName}'; +export default withCommercial(_plugin); +${source} +`; + console.log(`Insert commercial server code success`); + } else if (match2) { + source = source.replace(regex2, ``); + const moduleName = match2[1] || match2[2]; + source = + ` +import { withCommercial } from '@nocobase/plugin-commercial/server'; +${source} +export default withCommercial(${moduleName}); +`; + console.log(`Insert commercial server code success`); + } else { + console.error(`Insert commercial server code fail`); + } + + return { + contents: source, + loader: 'ts', + } + }) + }, +}; + +export default pluginEsbuildCommercialInject \ No newline at end of file diff --git a/packages/core/build/src/plugins/pluginRspackCommercialLoader.ts b/packages/core/build/src/plugins/pluginRspackCommercialLoader.ts new file mode 100644 index 00000000000..ff6b1f60413 --- /dev/null +++ b/packages/core/build/src/plugins/pluginRspackCommercialLoader.ts @@ -0,0 +1,41 @@ +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. + * For more information, please refer to: https://www.nocobase.com/agreement. + */ + +import type { LoaderContext } from '@rspack/core'; + +export default function myLoader( + this: LoaderContext>, + source: string, +) { + const options = this.getOptions(); + if (!options?.isCommercial) { + return source; + } + const isEntry = this.resourcePath.match(/client\/index\.(ts|tsx)/) && !this.resourcePath.includes('plugin-commercial'); + + if (isEntry) { + const regex = /export\s+default\s+([a-zA-Z_0-9]+)\s*;?/; // match: export default xxx; + const match = source.match(regex); + if (match) { + source = source.replace(regex, ``); + const moduleName = match[1]; + source = + ` + import { withCommercial } from '@nocobase/plugin-commercial/client'; + ${source} + export default withCommercial(${moduleName}); + `; + console.log(`Insert commercial client code success`); + } else { + console.error(`Insert commercial client code fail`); + } + return source; + } + return source; +} \ No newline at end of file diff --git a/packages/core/build/src/utils/obfuscationResult.ts b/packages/core/build/src/utils/obfuscationResult.ts new file mode 100644 index 00000000000..622c2d3c366 --- /dev/null +++ b/packages/core/build/src/utils/obfuscationResult.ts @@ -0,0 +1,43 @@ +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. + * For more information, please refer to: https://www.nocobase.com/agreement. + */ + +import fs from 'fs-extra'; +import * as JavaScriptObfuscator from 'javascript-obfuscator'; + +export const obfuscate = (filePath: string) => { + const fileContent = fs.readFileSync(filePath, 'utf8'); + const obfuscationResult = JavaScriptObfuscator.obfuscate(fileContent, { + compact: true, + controlFlowFlattening: false, + deadCodeInjection: false, + debugProtection: false, + debugProtectionInterval: 0, + disableConsoleOutput: true, + identifierNamesGenerator: 'hexadecimal', + log: false, + numbersToExpressions: false, + renameGlobals: false, + selfDefending: true, + simplify: true, + splitStrings: false, + stringArray: true, + stringArrayCallsTransform: false, + stringArrayEncoding: [], + stringArrayIndexShift: true, + stringArrayRotate: true, + stringArrayShuffle: true, + stringArrayWrappersCount: 1, + stringArrayWrappersChainedCalls: true, + stringArrayWrappersParametersMaxCount: 2, + stringArrayWrappersType: 'variable', + stringArrayThreshold: 0.75, + unicodeEscapeSequence: false + }); + fs.writeFileSync(filePath, obfuscationResult.getObfuscatedCode(), 'utf8'); +}; \ No newline at end of file diff --git a/packages/core/cli/package.json b/packages/core/cli/package.json index 6126e76cac4..30de67fb723 100644 --- a/packages/core/cli/package.json +++ b/packages/core/cli/package.json @@ -9,6 +9,7 @@ }, "dependencies": { "@nocobase/app": "1.7.0-alpha.13", + "@nocobase/license-kit": "^0.2.3", "@types/fs-extra": "^11.0.1", "@umijs/utils": "3.5.20", "chalk": "^4.1.1", diff --git a/packages/core/cli/src/commands/index.js b/packages/core/cli/src/commands/index.js index 14dd20e2ef9..0aac68232b6 100644 --- a/packages/core/cli/src/commands/index.js +++ b/packages/core/cli/src/commands/index.js @@ -35,6 +35,8 @@ module.exports = (cli) => { require('./upgrade')(cli); require('./postinstall')(cli); require('./pkg')(cli); + require('./instance-id')(cli); + require('./view-license-key')(cli); if (isPackageValid('@umijs/utils')) { require('./create-plugin')(cli); } diff --git a/packages/core/cli/src/commands/instance-id.js b/packages/core/cli/src/commands/instance-id.js new file mode 100644 index 00000000000..d072cebbda8 --- /dev/null +++ b/packages/core/cli/src/commands/instance-id.js @@ -0,0 +1,46 @@ +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. + * For more information, please refer to: https://www.nocobase.com/agreement. + */ + +const chalk = require('chalk'); +const { Command } = require('commander'); +const { run, isDev } = require('../util'); +const { getInstanceIdAsync } = require('@nocobase/license-kit'); +const path = require('path'); +const fs = require('fs'); + +/** + * + * @param {Command} cli + */ +module.exports = (cli) => { + cli + .command('generate-instance-id') + .description('Generate InstanceID') + .option('--force', 'Force generate InstanceID') + .action(async (options) => { + console.log('Generating InstanceID...'); + const dir = path.resolve(process.cwd(), 'storage/.license'); + const filePath = path.resolve(dir, 'instance-id'); + if (fs.existsSync(filePath) && !options.force) { + console.log('InstanceID already exists at ' + filePath); + return; + } else { + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); + } + try { + const instanceId = await getInstanceIdAsync(); + fs.writeFileSync(filePath, instanceId + '\n'); + console.log(chalk.greenBright(`InstanceID saved to ${filePath}`)); + } catch (e) { + console.log(e); + } + } + }); +}; diff --git a/packages/core/cli/src/commands/pkg.js b/packages/core/cli/src/commands/pkg.js index a0a4e883dd3..a6806b35c54 100644 --- a/packages/core/cli/src/commands/pkg.js +++ b/packages/core/cli/src/commands/pkg.js @@ -15,6 +15,7 @@ const tar = require('tar'); const path = require('path'); const { createStoragePluginsSymlink } = require('@nocobase/utils/plugin-symlink'); const chalk = require('chalk'); +const { getAccessKeyPair } = require('../util'); class Package { data; @@ -248,10 +249,13 @@ module.exports = (cli) => { NOCOBASE_PKG_USERNAME, NOCOBASE_PKG_PASSWORD, } = process.env; - if (!(NOCOBASE_PKG_USERNAME && NOCOBASE_PKG_PASSWORD)) { + const { accessKeyId, accessKeySecret } = getAccessKeyPair(); + if (!(NOCOBASE_PKG_USERNAME && NOCOBASE_PKG_PASSWORD) && !(accessKeyId && accessKeySecret)) { return; } - const credentials = { username: NOCOBASE_PKG_USERNAME, password: NOCOBASE_PKG_PASSWORD }; + const credentials = accessKeyId + ? { username: accessKeyId, password: accessKeySecret } + : { username: NOCOBASE_PKG_USERNAME, password: NOCOBASE_PKG_PASSWORD }; const pm = new PackageManager({ baseURL: NOCOBASE_PKG_URL }); await pm.login(credentials); const file = path.resolve(__dirname, '../../package.json'); diff --git a/packages/core/cli/src/commands/view-license-key.js b/packages/core/cli/src/commands/view-license-key.js new file mode 100644 index 00000000000..774ffd1b26d --- /dev/null +++ b/packages/core/cli/src/commands/view-license-key.js @@ -0,0 +1,44 @@ +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. + * For more information, please refer to: https://www.nocobase.com/agreement. + */ + +const chalk = require('chalk'); +const { Command } = require('commander'); +const { keyDecrypt } = require('@nocobase/license-kit'); +const path = require('path'); +const fs = require('fs'); + +/** + * + * @param {Command} cli + */ +module.exports = (cli) => { + cli + .command('view-license-key') + .description('View License Key') + .action(async (options) => { + const dir = path.resolve(process.cwd(), 'storage/.license'); + const filePath = path.resolve(dir, 'license-key'); + if (!fs.existsSync(filePath)) { + console.log('License key not found at ' + filePath); + return; + } + const key = fs.readFileSync(filePath, 'utf-8'); + let keyDataStr; + try { + keyDataStr = keyDecrypt(key); + } catch (e) { + console.log('License key decrypt failed', e); + return; + } + const keyData = JSON.parse(keyDataStr); + const { accessKeyId, accessKeySecret } = keyData; + console.log(chalk.greenBright(`Access Key ID: ${accessKeyId}`)); + console.log(chalk.greenBright(`Access Key Secret: ${accessKeySecret}`)); + }); +}; diff --git a/packages/core/cli/src/util.js b/packages/core/cli/src/util.js index ef8f2272fc3..15e2177d52e 100644 --- a/packages/core/cli/src/util.js +++ b/packages/core/cli/src/util.js @@ -18,6 +18,7 @@ const dotenv = require('dotenv'); const fs = require('fs-extra'); const os = require('os'); const moment = require('moment-timezone'); +const { keyDecrypt } = require('@nocobase/license-kit'); exports.isPackageValid = (pkg) => { try { @@ -165,10 +166,11 @@ exports.promptForTs = () => { }; exports.downloadPro = async () => { - const { NOCOBASE_PKG_USERNAME, NOCOBASE_PKG_PASSWORD } = process.env; - if (!(NOCOBASE_PKG_USERNAME && NOCOBASE_PKG_PASSWORD)) { - return; - } + // 此处不再判定,由pkgg命令处理 + // const { NOCOBASE_PKG_USERNAME, NOCOBASE_PKG_PASSWORD } = process.env; + // if (!(NOCOBASE_PKG_USERNAME && NOCOBASE_PKG_PASSWORD)) { + // return; + // } await exports.run('yarn', ['nocobase', 'pkg', 'download-pro']); }; @@ -487,3 +489,21 @@ exports.generatePlugins = function () { return; } }; + +exports.getAccessKeyPair = function () { + const keyFile = resolve(process.cwd(), 'storage/.license/license-key'); + if (!fs.existsSync(keyFile)) { + console.log(chalk.yellow('license-key file not found', keyFile)); + return {}; + } + try { + const str = fs.readFileSync(keyFile, 'utf-8'); + const keyDataStr = keyDecrypt(str); + const keyData = JSON.parse(keyDataStr); + const { accessKeyId, accessKeySecret } = keyData; + return { accessKeyId, accessKeySecret }; + } catch (error) { + console.log(chalk.yellow('Key parse failed, please check your key')); + return {}; + } +}; diff --git a/packages/core/client/src/locale/zh-CN.json b/packages/core/client/src/locale/zh-CN.json index e3ebb0c9de4..764cdd0d18a 100644 --- a/packages/core/client/src/locale/zh-CN.json +++ b/packages/core/client/src/locale/zh-CN.json @@ -1102,7 +1102,6 @@ "Italic": "斜体", "Response record":"响应结果记录", "Colon":"冒号", - "After successful submission, the selected data blocks will be automatically refreshed.": "提交成功后,会自动刷新这里选中的数据区块。", "No pages yet, please configure first": "暂无页面,请先配置", "Click the \"UI Editor\" icon in the upper right corner to enter the UI Editor mode": "点击右上角的“界面配置”图标,进入界面配置模式", "Specifies a Permissions Policy for the