Files
cal.diy/packages/app-store-cli/src/app-store.ts
T
cf423dda70 Add AppStore General Settings support (#4998)
* added weather in calendar wrapper

* added textfield to weather app, needs GeneralSettings abstraction

* add automation cateogry for n8n and zapier

* fix border radius for seperated apps

* create app specific settings in installed apps

* Fixed design issues caused by app settings

* add first version of template cards with logos

* Add link to zapier template

* Add missing translations

* fix mobile view

* better readable app description in mobile view

* code clean up

* fix issue that lisItem was always expanded

* code clean up

* code clean up

* fix import

* fix typo

* code clean up

* Add AppStore General Settings support

* add missing alt to img

* use FiShare2 icon instead of FiZap

* Remove duplicate entries

* Add border to AppSettings consistently

Co-authored-by: Peer Richelsen <peer@cal.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
2022-10-15 10:05:14 +00:00

263 lines
7.8 KiB
TypeScript

import chokidar from "chokidar";
import fs from "fs";
import { debounce } from "lodash";
import path from "path";
import prettier from "prettier";
import { AppMeta } from "@calcom/types/App";
import prettierConfig from "../../config/prettier-preset";
import execSync from "./execSync";
function isFileThere(path) {
try {
fs.statSync(path);
return true;
} catch (e) {
return false;
}
}
let isInWatchMode = false;
if (process.argv[2] === "--watch") {
isInWatchMode = true;
}
const formatOutput = (source: string) => prettier.format(source, prettierConfig);
const getVariableName = function (appName: string) {
return appName.replace("-", "_");
};
const getAppId = function (app: { name: string }) {
// Handle stripe separately as it's an old app with different dirName than slug/appId
return app.name === "stripepayment" ? "stripe" : app.name;
};
const APP_STORE_PATH = path.join(__dirname, "..", "..", "app-store");
type App = Partial<AppMeta> & {
name: string;
path: string;
};
function getAppName(candidatePath) {
function isValidAppName(candidatePath) {
if (
!candidatePath.startsWith("_") &&
candidatePath !== "ee" &&
!candidatePath.includes("/") &&
!candidatePath.includes("\\")
) {
return candidatePath;
}
}
if (isValidAppName(candidatePath)) {
// Already a dirname of an app
return candidatePath;
}
// Get dirname of app from full path
const dirName = path.relative(APP_STORE_PATH, candidatePath);
return isValidAppName(dirName) ? dirName : null;
}
function generateFiles() {
const browserOutput = [`import dynamic from "next/dynamic"`];
const metadataOutput = [];
const schemasOutput = [];
const serverOutput = [];
const appDirs: { name: string; path: string }[] = [];
fs.readdirSync(`${APP_STORE_PATH}`).forEach(function (dir) {
if (dir === "ee") {
fs.readdirSync(path.join(APP_STORE_PATH, dir)).forEach(function (eeDir) {
if (fs.statSync(path.join(APP_STORE_PATH, dir, eeDir)).isDirectory()) {
if (!getAppName(path.resolve(eeDir))) {
appDirs.push({
name: eeDir,
path: path.join(dir, eeDir),
});
}
}
});
} else {
if (fs.statSync(path.join(APP_STORE_PATH, dir)).isDirectory()) {
if (!getAppName(dir)) {
return;
}
appDirs.push({
name: dir,
path: dir,
});
}
}
});
function forEachAppDir(callback: (arg: App) => void) {
for (let i = 0; i < appDirs.length; i++) {
const configPath = path.join(APP_STORE_PATH, appDirs[i].path, "config.json");
let app;
if (fs.existsSync(configPath)) {
app = JSON.parse(fs.readFileSync(configPath).toString());
} else {
app = {};
}
callback({
...app,
name: appDirs[i].name,
path: appDirs[i].path,
});
}
}
forEachAppDir((app) => {
const templateDestinationDir = path.join(APP_STORE_PATH, app.path, "extensions");
const templateDestinationFilePath = path.join(templateDestinationDir, "EventTypeAppCard.tsx");
const zodDestinationFilePath = path.join(APP_STORE_PATH, app.path, "zod.ts");
if (app.extendsFeature === "EventType" && !isFileThere(templateDestinationFilePath)) {
execSync(`mkdir -p ${templateDestinationDir}`);
execSync(`cp ../app-store/_templates/extensions/EventTypeAppCard.tsx ${templateDestinationFilePath}`);
execSync(`cp ../app-store/_templates/zod.ts ${zodDestinationFilePath}`);
}
});
function getObjectExporter(
objectName,
{
fileToBeImported,
importBuilder,
entryBuilder,
}: {
fileToBeImported: string;
importBuilder?: (arg: App) => string;
entryBuilder: (arg: App) => string;
}
) {
const output = [];
forEachAppDir((app) => {
if (
fs.existsSync(path.join(APP_STORE_PATH, app.path, fileToBeImported)) &&
typeof importBuilder === "function"
) {
output.push(importBuilder(app));
}
});
output.push(`export const ${objectName} = {`);
forEachAppDir((app) => {
if (fs.existsSync(path.join(APP_STORE_PATH, app.path, fileToBeImported))) {
output.push(entryBuilder(app));
}
});
output.push(`};`);
return output;
}
serverOutput.push(
...getObjectExporter("apiHandlers", {
fileToBeImported: "api/index.ts",
// Import path must have / even for windows and not \
entryBuilder: (app) => ` "${app.name}": import("./${app.path.replace(/\\/g, "/")}/api"),`,
})
);
metadataOutput.push(
...getObjectExporter("appStoreMetadata", {
fileToBeImported: "_metadata.ts",
// Import path must have / even for windows and not \
importBuilder: (app) =>
`import { metadata as ${getVariableName(app.name)}_meta } from "./${app.path.replace(
/\\/g,
"/"
)}/_metadata";`,
entryBuilder: (app) => ` "${app.name}":${getVariableName(app.name)}_meta,`,
})
);
schemasOutput.push(
...getObjectExporter("appDataSchemas", {
fileToBeImported: "zod.ts",
// Import path must have / even for windows and not \
importBuilder: (app) =>
`import { appDataSchema as ${getVariableName(app.name)}_schema } from "./${app.path.replace(
/\\/g,
"/"
)}/zod";`,
// Key must be appId as this is used by eventType metadata and lookup is by appId
entryBuilder: (app) => ` "${getAppId(app)}":${getVariableName(app.name)}_schema ,`,
})
);
browserOutput.push(
...getObjectExporter("InstallAppButtonMap", {
fileToBeImported: "components/InstallAppButton.tsx",
entryBuilder: (app) =>
` ${app.name}: dynamic(() =>import("./${app.path}/components/InstallAppButton")),`,
})
);
// TODO: Make a component map creator that accepts ComponentName and does the rest.
// TODO: dailyvideo has a slug of daily-video, so that mapping needs to be taken care of. But it is an old app, so it doesn't need AppSettings
browserOutput.push(
...getObjectExporter("AppSettingsComponentsMap", {
fileToBeImported: "components/AppSettings.tsx",
entryBuilder: (app) => ` ${app.name}: dynamic(() =>import("./${app.path}/components/AppSettings")),`,
})
);
browserOutput.push(
...getObjectExporter("EventTypeAddonMap", {
fileToBeImported: path.join("extensions", "EventTypeAppCard.tsx"),
entryBuilder: (app) =>
` ${app.name}: dynamic(() =>import("./${app.path}/extensions/EventTypeAppCard")),`,
})
);
const banner = `/**
This file is autogenerated using the command \`yarn app-store:build --watch\`.
Don't modify this file manually.
**/
`;
const filesToGenerate: [string, string[]][] = [
["apps.metadata.generated.ts", metadataOutput],
["apps.server.generated.ts", serverOutput],
["apps.browser.generated.tsx", browserOutput],
["apps.schemas.generated.ts", schemasOutput],
];
filesToGenerate.forEach(([fileName, output]) => {
fs.writeFileSync(`${APP_STORE_PATH}/${fileName}`, formatOutput(`${banner}${output.join("\n")}`));
});
console.log(`Generated ${filesToGenerate.map(([fileName]) => fileName).join(", ")}`);
}
const debouncedGenerateFiles = debounce(generateFiles);
if (isInWatchMode) {
chokidar
.watch(APP_STORE_PATH)
.on("addDir", (dirPath) => {
const appName = getAppName(dirPath);
if (appName) {
console.log(`Added ${appName}`);
debouncedGenerateFiles();
}
})
.on("change", (filePath) => {
if (filePath.endsWith("config.json")) {
console.log("Config file changed");
debouncedGenerateFiles();
}
})
.on("unlinkDir", (dirPath) => {
const appName = getAppName(dirPath);
if (appName) {
console.log(`Removed ${appName}`);
debouncedGenerateFiles();
}
});
} else {
generateFiles();
}