From 1a3d11f94ef8e5c72adb4127b6f77cfb7c30d47a Mon Sep 17 00:00:00 2001 From: Jian Lu Date: Thu, 3 Apr 2025 20:05:49 +0800 Subject: [PATCH] feat: support commerical plugin server inject --- packages/core/build/package.json | 7 +- packages/core/build/src/buildPlugin.ts | 87 ++++++++++++++++++- .../plugins/pluginEsbuildCommercialInject.ts | 45 ++++++++++ .../core/build/src/utils/obfuscationResult.ts | 48 ++++++++++ 4 files changed, 183 insertions(+), 4 deletions(-) create mode 100644 packages/core/build/src/plugins/pluginEsbuildCommercialInject.ts create mode 100644 packages/core/build/src/utils/obfuscationResult.ts diff --git a/packages/core/build/package.json b/packages/core/build/package.json index 57d4af24b6..931b998ad1 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", @@ -40,7 +42,7 @@ "react-imported-component": "^6.5.4", "style-loader": "^3.3.3", "tar": "^6.2.0", - "tsup": "8.2.4", + "tsup": "^8.4.0", "typescript": "5.1.3", "update-notifier": "3.0.0", "vite-plugin-css-injected-by-js": "^3.2.1", @@ -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 f851f37ed8..905c59b7ad 100644 --- a/packages/core/build/src/buildPlugin.ts +++ b/packages/core/build/src/buildPlugin.ts @@ -14,7 +14,7 @@ import fg from 'fast-glob'; import fs from 'fs-extra'; import path from 'path'; import { build as tsupBuild } from 'tsup'; - +import * as bundleRequire from 'bundle-require'; import { EsbuildSupportExts, globExcludeFiles } from './constant'; import { PkgLog, UserConfig, getPackageJson } from './utils'; import { @@ -27,6 +27,8 @@ import { } 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]; @@ -307,6 +309,83 @@ export async function buildPluginServer(cwd: string, userConfig: UserConfig, sou await buildServerDeps(cwd, serverFiles, log); } +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/index.ts'); + // bundle all files、inject commercial code and obfuscate + await tsupBuild( + userConfig.modifyTsupConfig({ + entry: [entryFile], + splitting: false, + clean: false, + bundle: true, + silent: true, + treeshake: false, + target: 'node16', + sourcemap, + outDir: path.join(cwd, target_dir), + format: 'cjs', + skipNodeModulesBundle: true, + tsconfig: tsconfig.path, + loader: { + ...otherExts.reduce((prev, cur) => ({ ...prev, [cur]: 'copy' }), {}), + '.json': 'copy', + }, + esbuildPlugins: [pluginEsbuildCommercialInject], + onSuccess: async () => { + const serverFiles = [path.join(cwd, target_dir, 'index.js')]; + serverFiles.forEach((file) => { + obfuscate(file); + }); + }, + }), + ); + fs.removeSync(tsconfig.path); + + await buildServerDeps(cwd, serverFiles, log); +} + export async function buildPluginClient(cwd: string, userConfig: UserConfig, sourcemap: boolean, log: PkgLog) { log('build plugin client'); const packageJson = getPackageJson(cwd); @@ -567,6 +646,10 @@ __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') && !cwd.includes('plugin-commercial')) { + await buildProPluginServer(cwd, userConfig, sourcemap, log); + } else { + await buildPluginServer(cwd, userConfig, sourcemap, log); + } writeExternalPackageVersion(cwd, log); } diff --git a/packages/core/build/src/plugins/pluginEsbuildCommercialInject.ts b/packages/core/build/src/plugins/pluginEsbuildCommercialInject.ts new file mode 100644 index 0000000000..c81a8c3b0f --- /dev/null +++ b/packages/core/build/src/plugins/pluginEsbuildCommercialInject.ts @@ -0,0 +1,45 @@ +/** + * 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 { transform } from 'esbuild'; +import * as path from 'path'; +import fs from 'node:fs' + +const pluginEsbuildCommercialInject = { + name: 'plugin-esbuild-commercial-inject', + setup(build) { + + build.onLoad({ filter: /src\/index\.ts$/ }, async (args) => { + let text = fs.readFileSync(args.path, 'utf8'); + const regex = /export\s*\{\s*default\s*\}\s*from\s*(?:'([^']*)'|"([^"]*)");?/; // match: export { default } from './plugin'; + const match = text.match(regex); + if (match) { + text = text.replace(regex, ``); + const moduleName = match[1] || match[2]; + text = +` +import { withCommercial } from '@nocobase/plugin-for-commercial/server'; +import _plugin from '${moduleName}'; +export default withCommercial(_plugin); +${text} +`; + console.log(`${args.path} insert commercial server code`); + } else { + console.error(`${args.path} can't insert commercial code`); + } + + return { + contents: text, + loader: 'ts', + } + }) + }, +}; + +export default pluginEsbuildCommercialInject \ 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 0000000000..28e52d2c32 --- /dev/null +++ b/packages/core/build/src/utils/obfuscationResult.ts @@ -0,0 +1,48 @@ +/** + * 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: true, + controlFlowFlatteningThreshold: 0.75, + deadCodeInjection: true, + deadCodeInjectionThreshold: 0.4, + debugProtection: false, + debugProtectionInterval: 0, + disableConsoleOutput: true, + identifierNamesGenerator: 'hexadecimal', + log: false, + numbersToExpressions: true, + renameGlobals: false, + selfDefending: true, + simplify: true, + splitStrings: true, + splitStringsChunkLength: 10, + stringArray: true, + stringArrayCallsTransform: true, + stringArrayCallsTransformThreshold: 0.75, + stringArrayEncoding: ['base64'], + stringArrayIndexShift: true, + stringArrayRotate: true, + stringArrayShuffle: true, + stringArrayWrappersCount: 2, + stringArrayWrappersChainedCalls: true, + stringArrayWrappersParametersMaxCount: 4, + stringArrayWrappersType: 'function', + stringArrayThreshold: 0.75, + transformObjectKeys: true, + unicodeEscapeSequence: false + }); + fs.writeFileSync(filePath, obfuscationResult.getObfuscatedCode(), 'utf8'); +}; \ No newline at end of file