This commit is contained in:
2025-11-17 18:45:35 +01:00
parent 0f58e3bdff
commit 14d6f9aa73
7607 changed files with 1969407 additions and 0 deletions

View File

@@ -0,0 +1,2 @@
.out
node_modules

View File

@@ -0,0 +1,3 @@
# lowcoder-cli
CLI tool used to start build and publish lowcoder component library.

View File

@@ -0,0 +1,133 @@
import { execSync } from "child_process";
import fsExtra from "fs-extra";
import { build } from "vite";
import { writeFileSync, existsSync, readFileSync, readdirSync } from "fs";
import { resolve } from "path";
import { pathToFileURL } from "url";
import paths from "../config/paths.js";
import "../util/log.js";
import chalk from "chalk";
const { copySync } = fsExtra;
const packageJSON = JSON.parse(readFileSync(paths.appPackageJson).toString());
function validPackageJSON() {
if (!packageJSON.name) {
return "- package name is required";
}
if (!packageJSON.version) {
return "- package version is required";
}
if (!packageJSON.lowcoder) {
return "- lowcoder field is required in package.json";
}
const lowcoder = packageJSON.lowcoder;
if (!lowcoder.comps || Object.keys(lowcoder.comps).length === 0) {
return "- not found any comps to build";
}
const compErrors = [];
Object.keys(lowcoder.comps).forEach((name) => {
const compManifest = packageJSON.lowcoder.comps[name];
if (!compManifest.icon) {
// compErrors.push(`- comp ${name} must specify an icon`);
return;
}
if (
!compManifest.icon.startsWith("data:") &&
!existsSync(paths.resolveApp(compManifest.icon))
) {
compErrors.push(`- comp ${name}'s icon file ${chalk.cyan(compManifest.icon)} not found`);
return;
}
});
if (compErrors.length > 0) {
return compErrors.join("\n");
}
}
function findReadmeFileName(directory) {
const files = readdirSync(directory);
const readmeFile = files.find(file => file.toLowerCase() === 'readme.md');
return readmeFile ? `${directory}/${readmeFile}` : null;
}
/**
* 1. webpack production build
* 2. generate package.json
* 3. copy locales
* 3. pack tar ball
*/
export default async function buildAction(options) {
const beginTime = performance.now();
process.env.NODE_ENV = "production";
const { outDir } = options;
const err = validPackageJSON();
if (err) {
console.red("Invalid package.json:\n");
console.red(err);
console.log("");
return;
}
const compNames = Object.keys(packageJSON.lowcoder.comps);
console.cyan(`Name : ${packageJSON.name}`);
console.cyan(`Version : ${packageJSON.version}`);
console.cyan(`Comps : ${compNames.length}\n`);
compNames.forEach((i) => {
console.log(` ${i}`);
});
console.log("");
console.cyan("Building...");
const viteConfigURL = pathToFileURL(paths.appViteConfigJs);
const viteConfig = await import(viteConfigURL).default;
console.log(paths.appViteConfigJs);
await build(viteConfig);
// write package.json
packageJSON.lowcoder.entry = "index.js";
writeFileSync(paths.appOutPackageJson, JSON.stringify(packageJSON, null, 2));
// copy locales
if (existsSync(paths.appLocales)) {
copySync(paths.appLocales, resolve(paths.appOutPath, "locales"));
}
// copy icon files
compNames.forEach((name) => {
const compManifest = packageJSON.lowcoder.comps[name];
if (compManifest.icon) {
copySync(paths.resolveApp(compManifest.icon), resolve(paths.appOutPath, compManifest.icon));
}
});
// copy readme file
const readmePath = findReadmeFileName(paths.appPath + '/src');
if (readmePath) {
const destinationPath = resolve(paths.appOutPath, 'readme.md');
copySync(readmePath, destinationPath);
console.log(`Copied README file to: ${destinationPath}`);
} else {
console.warn('README.md file not found.');
}
if (options.publish) {
// publish
execSync("npm publish", {
stdio: "inherit",
cwd: paths.appOutPath,
});
} else {
// pack
const tarOutPath = paths.resolveApp(outDir);
execSync(`npm pack --pack-destination ${tarOutPath}`, {
stdio: "ignore",
cwd: paths.appOutPath,
});
console.green(`Package generated in: ${tarOutPath}`);
}
console.green(`Done in ${Math.round(performance.now() - beginTime)}ms!`);
}

View File

@@ -0,0 +1,109 @@
import path from "path";
import fs from "fs-extra";
import { spawn } from "cross-spawn";
import paths from "../config/paths.js";
import { createRequire } from "node:module";
const require = createRequire(import.meta.url);
const isUsingYarn = (process.env.npm_config_user_agent || "").indexOf("yarn") === 0;
function install(dependencies, registry) {
return new Promise((resolve, reject) => {
let cmd = "npm";
let args = ["install", "--no-audit", "--save", "--save-exact", "--loglevel", "error"];
if (isUsingYarn) {
cmd = "yarn";
args = ["add"];
}
if (registry) {
args.push("--registry", registry);
}
args.push(...dependencies);
const child = spawn(cmd, args, { stdio: "inherit" });
child.on("close", (code) => {
if (code !== 0) {
reject({
command: `${cmd} ${args.join(" ")}`,
});
return;
}
resolve();
});
});
}
async function uninstall(dependencies) {
return new Promise((resolve, reject) => {
let cmd = "npm";
let args = ["uninstall"];
if (isUsingYarn) {
cmd = "yarn";
args = ["remove"];
}
args.push(...dependencies);
const child = spawn(cmd, args, { stdio: "inherit" });
child.on("close", (code) => {
if (code !== 0) {
reject({
command: `${cmd} ${args.join(" ")}`,
});
return;
}
resolve();
});
});
}
/**
* init dir with specified template name
* 1. install template package
* 2. update package.json
* 3. copy template files
* 4. install other dependencies
* 5. uninstall template package
*/
export default async function initAction(options) {
const { template, registry } = options;
const templatePackageName = `lowcoder-cli-template-${template}`;
await install([templatePackageName], registry);
console.log("template package installed");
const templatePackageJsonFile = require.resolve(`${templatePackageName}/package.json`);
const templateDir = path.dirname(templatePackageJsonFile);
const templatePackageJson = fs.readJsonSync(templatePackageJsonFile);
const appPackageJson = fs.readJsonSync(paths.appPackageJson);
appPackageJson.lowcoder = templatePackageJson.lowcoder || {};
appPackageJson.scripts = {
start: "vite",
build: "lowcoder-cli build",
build_publish: "lowcoder-cli build --publish",
};
fs.writeFileSync(paths.appPackageJson, JSON.stringify(appPackageJson, null, 2));
console.log("package.json updated");
const notCopiedFiles = ["package.json", "README.md", "README-template.md", "node_modules"];
fs.copySync(templateDir, "./", {
filter: (src) => notCopiedFiles.every((i) => !src.startsWith(path.join(templateDir, i))),
});
fs.copyFile(path.join(templateDir, "README-template.md"), "./README.md");
console.log("template files copied");
const dependencies = [];
if (template === "typescript") {
dependencies.push("typescript");
}
if (dependencies.length > 0) {
await install(dependencies, registry);
console.log("dependencies installed");
}
await uninstall([templatePackageName]);
console.log("template package uninstalled");
console.log();
console.log("Done! Now, you can run below command to start:");
console.log(` ${isUsingYarn ? "yarn" : "npm"} start`);
console.log();
}

View File

@@ -0,0 +1,41 @@
/// <reference path="./global.d.ts" />
/// <reference types="vite/client" />
declare module "*.svg" {
import * as React from "react";
export const ReactComponent: React.FunctionComponent<
React.SVGProps<SVGSVGElement> & { title?: string }
>;
// const src: string;
// export default src;
}
declare module "*.md" {
const value: string;
export default value;
}
declare module "eslint4b-prebuilt-2";
declare module "mq-polyfill";
declare module "@rjsf/antd";
declare module "really-relaxed-json";
declare module "tui-image-editor";
declare var numbro: any;
declare var uuid: any;
declare var PUBLIC_URL: string;
declare var REACT_APP_EDITION: string;
declare var REACT_APP_LANGUAGES: string;
declare var REACT_APP_COMMIT_ID: string;
declare var REACT_APP_API_SERVICE_URL: string;
declare var REACT_APP_NODE_SERVICE_URL: string;
declare var REACT_APP_ENV: string;
declare var REACT_APP_BUILD_ID: string;
declare var REACT_APP_LOG_LEVEL: string;
declare var REACT_APP_IMPORT_MAP: string;
declare var REACT_APP_SERVER_IPS: string;
declare var REACT_APP_BUNDLE_TYPE: "sdk" | "app";
declare var REACT_APP_DISABLE_JS_SANDBOX: string;
declare var REACT_APP_BUNDLE_BUILTIN_PLUGIN: string;

View File

@@ -0,0 +1,119 @@
import fs from "fs";
import path from "path";
import paths from "./paths.js";
import resolve from "resolve";
function getAdditionalEntries() {}
/**
* Get additional module paths based on the baseUrl of a compilerOptions object.
*
* @param {Object} options
*/
function getAdditionalModulePaths(options = {}) {
const baseUrl = options.baseUrl;
if (!baseUrl) {
return "";
}
const baseUrlResolved = path.resolve(paths.appPath, baseUrl);
// We don't need to do anything if `baseUrl` is set to `node_modules`. This is
// the default behavior.
if (path.relative(paths.appNodeModules, baseUrlResolved) === "") {
return null;
}
// Allow the user set the `baseUrl` to `appSrc`.
if (path.relative(paths.appSrc, baseUrlResolved) === "") {
return [paths.appSrc];
}
// If the path is equal to the root directory we ignore it here.
// We don't want to allow importing from the root directly as source files are
// not transpiled outside of `src`. We do allow importing them with the
// absolute path (e.g. `src/Components/Button.js`) but we set that up with
// an alias.
if (path.relative(paths.appPath, baseUrlResolved) === "") {
return null;
}
// Otherwise, throw an error.
throw new Error("Your project's `baseUrl` can only be set to `src` or `node_modules`.");
}
/**
* Get webpack aliases based on the baseUrl of a compilerOptions object.
*
* @param {*} options
*/
function getWebpackAliases(options = {}) {
const baseUrl = options.baseUrl;
if (!baseUrl) {
return {};
}
const baseUrlResolved = path.resolve(paths.appPath, baseUrl);
if (path.relative(paths.appPath, baseUrlResolved) === "") {
return {
src: paths.appSrc,
};
}
}
/**
* Get jest aliases based on the baseUrl of a compilerOptions object.
*
* @param {*} options
*/
function getJestAliases(options = {}) {
const baseUrl = options.baseUrl;
if (!baseUrl) {
return {};
}
const baseUrlResolved = path.resolve(paths.appPath, baseUrl);
if (path.relative(paths.appPath, baseUrlResolved) === "") {
return {
"^src/(.*)$": "<rootDir>/src/$1",
};
}
}
function getModules() {
// Check if TypeScript is setup
const hasTsConfig = fs.existsSync(paths.appTsConfig);
let config;
// If there's a tsconfig.json we assume it's a
// TypeScript project and set up the config
// based on tsconfig.json
if (hasTsConfig) {
const ts = require(resolve.sync("typescript", {
basedir: paths.appNodeModules,
}));
config = ts.readConfigFile(paths.appTsConfig, ts.sys.readFile).config;
// Otherwise we'll check if there is jsconfig.json
// for non TS projects.
}
config = config || {};
const options = config.compilerOptions || {};
const additionalModulePaths = getAdditionalModulePaths(options);
return {
additionalModulePaths: additionalModulePaths,
webpackAliases: getWebpackAliases(options),
jestAliases: getJestAliases(options),
hasTsConfig,
};
}
export default getModules();

View File

@@ -0,0 +1,57 @@
import path from "node:path";
import fs from "node:fs";
import { currentDirName } from "../dev-utils/util.js";
const currentDir = currentDirName(import.meta.url);
const appDirectory = fs.realpathSync(process.cwd());
const resolveApp = (relativePath) => path.resolve(appDirectory, relativePath);
const moduleFileExtensions = [
"web.mjs",
"mjs",
"web.js",
"js",
"web.ts",
"ts",
"web.tsx",
"tsx",
"json",
"web.jsx",
"jsx",
];
const resolveModule = (resolveFn, filePath) => {
const extension = moduleFileExtensions.find((extension) =>
fs.existsSync(resolveFn(`${filePath}.${extension}`))
);
if (extension) {
return resolveFn(`${filePath}.${extension}`);
}
return resolveFn(`${filePath}.js`);
};
const resolveOwn = (relativePath) => path.resolve(currentDir, "..", relativePath);
const paths = {
resolveApp,
appOutPath: resolveOwn(".out"),
appOutPackageJson: resolveOwn(".out/package.json"),
appPath: resolveApp("."),
appHtml: resolveOwn("ide/index.html"),
appRoot: resolveOwn("ide"),
appBaseTsConfig: resolveOwn("ide/tsconfig.json"),
appPackageJson: resolveApp("package.json"),
appSrc: resolveApp("src"),
appLocales: resolveApp("locales"),
compsIndexJs: resolveModule(resolveApp, "src/index"),
appViteConfigJs: resolveModule(resolveApp, "vite.config"),
appTsConfig: resolveApp("tsconfig.json"),
yarnLockFile: resolveApp("yarn.lock"),
appNodeModules: resolveApp("node_modules"),
appWebpackCache: resolveApp("node_modules/.cache"),
appTsBuildInfoFile: resolveApp("node_modules/.cache/tsconfig.tsbuildinfo"),
};
export default paths;

View File

@@ -0,0 +1,67 @@
import react from "@vitejs/plugin-react";
import svgrPlugin from "vite-plugin-svgr";
import global from "rollup-plugin-external-globals";
import { buildVars } from "../dev-utils/buildVars.js";
import injectCss from "vite-plugin-css-injected-by-js";
import { getLibNames, getAllLibGlobalVarNames } from "../dev-utils/external.js";
import paths from "./paths.js";
import { defineConfig } from "vite";
import { readJson } from "../dev-utils/util.js";
const isProduction = process.env.NODE_ENV === "production";
const packageJson = readJson(paths.appPackageJson);
const define = {};
buildVars.forEach(({ name, defaultValue }) => {
define[name] = JSON.stringify(process.env[name] || defaultValue);
});
export default defineConfig({
define: {
...define,
"process.env.NODE_ENV": JSON.stringify(process.env.NODE_ENV || "development"),
__LOWCODER_ORG__: JSON.stringify({}),
},
assetsInclude: ["**/*.md"],
resolve: {
extensions: [".mjs", ".js", ".ts", ".jsx", ".tsx", ".json"],
},
build: {
target: "es2020",
cssTarget: "chrome87",
outDir: paths.appOutPath,
emptyOutDir: true,
lib: {
formats: ["es"],
entry: paths.compsIndexJs,
fileName: "index",
},
rollupOptions: {
external: getLibNames(),
output: {
chunkFileNames: "[hash].js",
},
},
},
plugins: [
react({
babel: {
compact: false,
parserOpts: {
plugins: ["decorators-legacy"],
},
},
}),
svgrPlugin({
svgrOptions: {
exportType: "named",
prettier: false,
svgo: false,
titleProp: true,
ref: true,
},
}),
isProduction && global(getAllLibGlobalVarNames(), { exclude: [/\.css$/] }),
isProduction && injectCss({ styleId: `${packageJson.name}-${packageJson.version}` }),
].filter(Boolean),
});

View File

@@ -0,0 +1,58 @@
export const buildVars = [
{
name: "PUBLIC_URL",
defaultValue: "/",
},
{
name: "REACT_APP_EDITION",
defaultValue: "community",
},
{
name: "REACT_APP_LANGUAGES",
defaultValue: "",
},
{
name: "REACT_APP_COMMIT_ID",
defaultValue: "00000",
},
{
name: "REACT_APP_API_SERVICE_URL",
defaultValue: "",
},
{
name: "REACT_APP_NODE_SERVICE_URL",
defaultValue: "",
},
{
name: "REACT_APP_ENV",
defaultValue: "production",
},
{
name: "REACT_APP_BUILD_ID",
defaultValue: "",
},
{
name: "REACT_APP_LOG_LEVEL",
defaultValue: "error",
},
{
name: "REACT_APP_IMPORT_MAP",
defaultValue: "{}",
},
{
name: "REACT_APP_SERVER_IPS",
defaultValue: "",
},
{
name: "REACT_APP_BUNDLE_BUILTIN_PLUGIN",
defaultValue: "",
},
{
name: "REACT_APP_BUNDLE_TYPE",
defaultValue: "app",
},
{
name: "REACT_APP_DISABLE_JS_SANDBOX",
defaultValue: "",
},
];

View File

@@ -0,0 +1,102 @@
/**
* libs to import as global var
* name: module name
* mergeDefaultAndNameExports: whether to merge default and named exports
*/
export const libs = [
"axios",
"redux",
"react-router",
"react-router-dom",
"react-redux",
"react",
"react-dom",
"lodash",
"history",
"antd",
"@dnd-kit/core",
"@dnd-kit/modifiers",
"@dnd-kit/sortable",
"@dnd-kit/utilities",
{
name: "moment",
extractDefault: true,
},
{
name: "dayjs",
extractDefault: true,
},
{
name: "lowcoder-sdk",
from: "./src/index.sdk.ts",
},
{
name: "styled-components",
mergeDefaultAndNameExports: true,
},
];
/**
* get global var name from module name
* @param {string} name
* @returns
*/
export const getLibGlobalVarName = (name) => {
return "$" + name.replace(/@/g, "$").replace(/[\/\-]/g, "_");
};
export const getLibNames = () => {
return libs.map((i) => {
if (typeof i === "object") {
return i.name;
}
return i;
});
};
export const getAllLibGlobalVarNames = () => {
const ret = {};
libs.forEach((lib) => {
let name = lib;
if (typeof lib === "object") {
name = lib.name;
}
ret[name] = getLibGlobalVarName(name);
});
return ret;
};
export const libsImportCode = (exclude = []) => {
const importLines = [];
const assignLines = [];
libs.forEach((i) => {
let name = i;
let merge = false;
let from = name;
let extractDefault = false;
if (typeof i === "object") {
name = i.name;
merge = i.mergeDefaultAndNameExports ?? false;
from = i.from ?? name;
extractDefault = i.extractDefault ?? false;
}
if (exclude.includes(name)) {
return;
}
const varName = getLibGlobalVarName(name);
if (merge) {
importLines.push(`import * as ${varName}_named_exports from '${from}';`);
importLines.push(`import ${varName} from '${from}';`);
assignLines.push(`Object.assign(${varName}, ${varName}_named_exports);`);
} else if (extractDefault) {
importLines.push(`import ${varName} from '${from}';`);
} else {
importLines.push(`import * as ${varName} from '${from}';`);
}
assignLines.push(`window.${varName} = ${varName};`);
});
return importLines.concat(assignLines).join("\n");
};

View File

@@ -0,0 +1,18 @@
import { libsImportCode } from "./external.js";
export function globalDepPlugin(exclude = []) {
const virtualModuleId = "virtual:globals";
return {
name: "lowcoder-global-plugin",
resolveId(id) {
if (id === virtualModuleId) {
return id;
}
},
load(id) {
if (id === virtualModuleId) {
return libsImportCode(exclude);
}
},
};
}

View File

@@ -0,0 +1,28 @@
import fs from "node:fs";
import { dirname } from "node:path";
import { fileURLToPath } from "node:url";
export function stripLastSlash(str) {
if (str.endsWith("/")) {
return str.slice(0, str.length - 1);
}
return str;
}
export function ensureLastSlash(str) {
if (!str) {
return "/";
}
if (!str.endsWith("/")) {
return `${str}/`;
}
return str;
}
export function readJson(file) {
return JSON.parse(fs.readFileSync(file).toString());
}
export function currentDirName(importMetaUrl) {
return dirname(fileURLToPath(importMetaUrl));
}

View File

@@ -0,0 +1,8 @@
declare global {
interface Window {
printPerf: () => void;
__LOWCODER_ORG__?: {};
}
}
export {};

View File

@@ -0,0 +1,28 @@
#!/usr/bin/env node
import("./util/log.js");
import fs from "node:fs";
import { Command } from "commander";
import initAction from "./actions/init.js";
import buildAction from "./actions/build.js";
const program = new Command();
const pkg = JSON.parse(fs.readFileSync("./package.json").toString());
program.name(pkg.name).description(pkg.description);
program
.command("init")
.description("init project")
.option("-t, --template", "template name", "typescript")
.option("--registry [addr]", "npm registry")
.action(initAction);
program
.command("build")
.description("build component lib")
.option("--outDir", "where to place tar ball", "./")
.option("--publish", "publish to npm", false)
.action(buildAction);
program.parse();

View File

@@ -0,0 +1,49 @@
{
"name": "lowcoder-cli",
"description": "CLI tool used to start build publish lowcoder components",
"version": "0.0.30",
"license": "MIT",
"bin": "./index.js",
"type": "module",
"exports": {
".": {
"import": "./index.js",
"require": "./index.js"
},
"./config/vite.config": {
"import": "./config/vite.config.js",
"require": "./config/vite.config.js"
},
"./client": {
"types": "./client.d.ts"
},
"./actions/init.js": {
"import": "./actions/init.js",
"require": "./actions/init.js"
}
},
"dependencies": {
"@vitejs/plugin-react": "^2.2.0",
"axios": "^1.7.4",
"chalk": "4",
"commander": "^9.4.1",
"cross-spawn": "^7.0.3",
"fs-extra": "^10.1.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-json-view": "^1.21.3",
"rollup-plugin-external-globals": "^0.7.1",
"vite": "^4.5.5",
"vite-plugin-css-injected-by-js": "^2.1.1",
"vite-plugin-svgr": "^2.2.2"
},
"devDependencies": {
"typescript": "^4.8.4"
},
"peerDependencies": {
"lowcoder-sdk": "*"
},
"keywords": [
"lowcoder"
]
}

View File

@@ -0,0 +1,30 @@
import chalk from "chalk";
const colors = [
"black",
"red",
"green",
"yellow",
"blue",
"magenta",
"cyan",
"white",
"gray",
"redBright",
"greenBright",
"yellowBright",
"blueBright",
"magentaBright",
"cyanBright",
"whiteBright",
];
colors.forEach((color) => {
console[color] = (text) => {
console.log(chalk[color](text));
};
});
export const logBug = (text) => {
console.red(`${text}\n This maybe is a bug, you can issue a bug for us.`);
};