init: the first public commit for AFFiNE

This commit is contained in:
DarkSky
2022-07-22 15:49:21 +08:00
commit e3e3741393
1451 changed files with 108124 additions and 0 deletions

View File

@@ -0,0 +1,45 @@
const path = require('path');
const process = require('process');
const downloadIcons = require('./figma');
const patchStyles = require('./patch-styles');
/**
* @param {*} options
* @param {array} options.assets
* @param {string} options.assets.fileId
* @param {string} options.assets.nodeId
* @param {string} options.assets.folder
* @param {*} context
* @returns
*/
exports['default'] = async function downloadFigmaRes(options, context) {
const libRoot = context.workspace.projects[context.projectName].root;
const token = process.env.FIGMA_TOKEN;
if (!token) {
throw new Error(
'FIGMA_TOKEN is not defined. Please set it in your .env.local file.'
);
}
await Promise.allSettled(
(options.assets || []).map(async (asset, index) => {
const fileId = asset.fileId;
const nodeId = asset.nodeId;
const folder =
asset.folder || `./src/icon${index > 0 ? index : ''}`;
if (!token || !fileId || !nodeId) {
const message = `Please check if token/fileId/nodeId exists (No.${index}).`;
console.error(message);
throw new Error(message);
}
await downloadIcons({
token,
fileId,
nodeId,
folder: path.resolve(libRoot, folder),
patchStyles,
});
})
);
return { success: true };
};

View File

@@ -0,0 +1,9 @@
{
"executors": {
"figmaRes": {
"implementation": "./download.js",
"schema": "./schema.json",
"description": "Run `figmaRes` (to download figma icons and pictures)."
}
}
}

View File

@@ -0,0 +1,56 @@
const PREFIX_URL = 'https://api.figma.com/v1/';
async function initializeApi(token) {
const got = await import('got');
const api = got.got.extend({
prefixUrl: PREFIX_URL,
headers: {
'X-Figma-Token': token,
},
});
return {
async getChildren(fileId, nodeId) {
const decodedNodeId = decodeURIComponent(nodeId);
console.log(
`Fetching: ${`${PREFIX_URL}files/${fileId}/nodes?ids=${decodedNodeId}`}`
);
let data;
try {
data = await api({
url: `files/${fileId}/nodes?ids=${decodedNodeId}`,
}).json();
} catch (error) {
console.log(`Error: ${error}`);
}
return data.nodes[decodedNodeId].document.children;
},
async getIconsUrl(fileId, iconId) {
console.log(
`Fetching: ${`${PREFIX_URL}images/${fileId}/?ids=${iconId}&format=svg`}`
);
let body;
try {
body = await api({
url: `images/${fileId}/?ids=${iconId}&format=svg`,
}).json();
} catch (error) {
console.log(`Error: ${error}`);
}
return body.images;
},
async downloadIcon(iconUrl) {
const { body } = await got.got({
url: iconUrl,
});
return body;
},
};
}
module.exports = initializeApi;

View File

@@ -0,0 +1,126 @@
const svgr = require('@svgr/core');
const util = require('./util');
function getColors(colorCount) {
return colorCount > 0
? Array(colorCount)
.fill(0)
.reduce((acc, _, index) => {
acc.push({
type: 'string',
value: `--color-${index}`,
propName: `color${index}`,
});
if (index === 0) {
acc.push({
type: 'string',
value: `--color-${index}`,
propName: 'primaryColor',
});
}
if (index === 1) {
acc.push({
type: 'string',
value: `--color-${index}`,
propName: 'secondaryColor',
});
}
return acc;
}, [])
: [
{
type: 'string',
value: 'color',
propName: 'color',
},
];
}
function getColorsInterfaceProps(colors) {
return colors
.map(color => {
return `${color.propName}?: ${color.type}`;
})
.join('\n ');
}
function getRestColors(colors) {
return colors.map(color => color.propName).join(', ');
}
function getPropNameToColorValue(colors) {
const maps = colors.reduce((acc, color) => {
if (acc[color.value]) {
acc[color.value] = `${acc[color.value]} || ${color.propName}`;
} else {
acc[color.value] = color.propName;
}
return acc;
}, {});
const kvString = Object.entries(maps)
.map(kv => {
return `"${kv[0]}": ${kv[1]}`;
})
.join(', ');
return `{${kvString}}`;
}
/**
* get icon component template
*
* @param {string} name
* @param {string} svgCode svg original code
* @param {Object} customStyles custom style properties
*/
module.exports = async function generateReactIcon(name, svgCode, customStyles) {
let svgrContent = '';
try {
svgrContent = await svgr.transform(
svgCode,
{
icon: true,
typescript: true,
},
{ componentName: `${name}Icon1` }
);
} catch (err) {
console.error(err);
}
let svgContent = svgrContent.match(/<svg [^\>]+>([\s\S]*?)<\/svg>/)[1];
let colorIdx = 0;
if (util.isDuotone(name)) {
svgContent = svgContent.replace(
/fill="#[A-Za-z0-9]+"/g,
() => `style={{fill: 'var(--color-${colorIdx++})'}}`
);
}
const colors = getColors(colorIdx);
return `
import { FC } from 'react';
// eslint-disable-next-line no-restricted-imports
import { SvgIcon } from '@mui/material';
// eslint-disable-next-line no-restricted-imports
import type { SvgIconProps } from '@mui/material';
export interface ${name}IconProps extends Omit<SvgIconProps, 'color'> {
${getColorsInterfaceProps(colors)}
}
export const ${name}Icon: FC<${name}IconProps> = ({ ${getRestColors(
colors
)}, style, ...props}) => {
const propsStyles = ${getPropNameToColorValue(colors)};
const customStyles = ${JSON.stringify(customStyles || {})};
const styles = {...propsStyles, ...customStyles, ...style}
return (
<SvgIcon style={styles} {...props}>
${svgContent}
</SvgIcon>
)
};
`;
};

View File

@@ -0,0 +1,119 @@
const path = require('path');
const fs = require('fs-extra');
const { pascalCase, paramCase } = require('change-case');
const initializeApi = require('./api');
const svgo = require('./svgo');
const util = require('./util');
const generateReactIcon = require('./generateReactIcon');
function getRemoveAttrs(name) {
if (util.isBrands(name)) {
return {
name: 'removeAttrs',
params: {
attrs: '',
},
};
}
return {
name: 'removeAttrs',
params: {
attrs: util.isDuotone(name) ? 'stroke' : '(stroke|fill)',
},
};
}
async function generateImportEntry(iconNodes, folder) {
const fileWithImportsPath = path.resolve(folder, 'index.ts');
const importsContent = iconNodes
.map(iconNode => {
const iconName = paramCase(iconNode.name);
if (!iconName) {
return `// Error: ${iconNode.name}`;
}
return `export * from './${iconName}/${iconName}';`;
})
.join('\n');
await fs.writeFile(
fileWithImportsPath,
`export const timestamp = ${Date.now()};\n${importsContent}`,
{ encoding: 'utf8' }
);
}
function filterIcons(icons, iconsUrl) {
const icon_name_set = new Set();
const icons_filtered = icons.filter(i => {
if (icon_name_set.has(paramCase(i.name))) {
console.warn(
`\nWarn: There is an icon with the same name: ${i.name}`
);
return false;
}
icon_name_set.add(paramCase(i.name));
return iconsUrl[i.id];
});
return icons_filtered;
}
async function downloadFigmaIcons(props) {
const { token, fileId, nodeId, folder, patchStyles } = props;
await fs.ensureDir(folder);
await fs.emptyDir(folder);
const api = await initializeApi(token);
let icons = await api.getChildren(fileId, nodeId);
const iconsUrl = await api.getIconsUrl(
fileId,
icons.map(i => i.id).join(',')
);
icons = filterIcons(icons, iconsUrl);
const generateIcons = icons.map(async icon => {
const iconUrl = iconsUrl[icon.id];
const iconName = paramCase(icon.name);
let originSvg;
try {
originSvg = await api.downloadIcon(iconUrl);
} catch (err) {
console.error(err);
}
let optimizedSvg;
try {
const data = await svgo.optimize(
originSvg,
getRemoveAttrs(iconName)
);
optimizedSvg = data.data;
} catch (err) {
console.error(err);
console.log(iconName);
}
const iconFolder = path.resolve(folder, iconName);
const JSXContent = await generateReactIcon(
pascalCase(icon.name),
optimizedSvg,
patchStyles?.[iconName]
);
await Promise.all([
fs.outputFile(
path.resolve(iconFolder, `${iconName || icon.name}.svg`),
optimizedSvg,
{ encoding: 'utf8' }
),
fs.outputFile(
path.resolve(iconFolder, `${iconName || icon.name}.tsx`),
JSXContent,
{ encoding: 'utf8', flag: '' }
),
]);
});
await Promise.allSettled([
...generateIcons,
generateImportEntry(icons, folder),
]);
}
module.exports = downloadFigmaIcons;

View File

@@ -0,0 +1,44 @@
const svgo = require('svgo');
module.exports = {
optimize(input, removeAttrs) {
return svgo.optimize(input, {
plugins: [
'cleanupAttrs',
'removeDoctype',
'removeXMLProcInst',
'removeComments',
'removeMetadata',
'removeTitle',
'removeDesc',
'removeUselessDefs',
'removeEditorsNSData',
'removeEmptyAttrs',
'removeHiddenElems',
'removeEmptyText',
'removeEmptyContainers',
'removeViewBox',
'cleanupEnableBackground',
'convertStyleToAttrs',
'convertColors',
'convertPathData',
'convertTransform',
'removeUnknownsAndDefaults',
'removeNonInheritableGroupAttrs',
'removeUselessStrokeAndFill',
'removeUnusedNS',
'cleanupIDs',
'cleanupNumericValues',
'moveElemsAttrsToGroup',
'moveGroupAttrsToElems',
'collapseGroups',
'removeRasterImages',
'mergePaths',
'convertShapeToPath',
'sortAttrs',
'removeDimensions',
removeAttrs,
],
});
},
};

View File

@@ -0,0 +1,9 @@
// Determine whether it is a two-color icon
module.exports.isDuotone = function isDuotone(name) {
return /[Dd]{1}uotone$/.test(name);
};
// Determine whether it is a brand trademark
module.exports.isBrands = function isBrands(name) {
return ['figma', 'youtube'].includes(name.toLowerCase());
};

View File

@@ -0,0 +1,3 @@
{
"executors": "./executor.json"
}

View File

@@ -0,0 +1,41 @@
/**
* Some icons may be downloaded with incorrect styles, and some styles need to be added.
* key is the name of the icon's kebab-case.
*/
const patchStyles = {
// add: {
// fill: 'none',
// fillOpacity: 0,
// stroke: 'currentColor'
// },
// text: {
// stroke: 'currentColor'
// },
// rectangle: {
// fill: 'none',
// fillOpacity: 0,
// stroke: 'currentColor'
// },
// ellipse: {
// fill: 'none',
// fillOpacity: 0,
// stroke: 'currentColor'
// },
// triangle: {
// fill: 'none',
// fillOpacity: 0,
// stroke: 'currentColor'
// },
// polygon: {
// fill: 'none',
// fillOpacity: 0,
// stroke: 'currentColor'
// },
// shape: {
// fill: 'none',
// fillOpacity: 0,
// stroke: 'currentColor'
// }
};
module.exports = patchStyles;

View File

@@ -0,0 +1,11 @@
{
"$schema": "http://json-schema.org/schema",
"type": "object",
"cli": "nx",
"properties": {
"figmaRes": {
"type": "string",
"description": "Download figma icons and pictures."
}
}
}

View File

@@ -0,0 +1,9 @@
{
"executors": {
"svgOptimize": {
"implementation": "./svgo.js",
"schema": "./schema.json",
"description": "Run `svgo` (to optimize svg)."
}
}
}

View File

@@ -0,0 +1,3 @@
{
"executors": "./executor.json"
}

View File

@@ -0,0 +1,11 @@
{
"$schema": "http://json-schema.org/schema",
"type": "object",
"cli": "nx",
"properties": {
"svgOptimize": {
"type": "string",
"description": "optimize svg"
}
}
}

View File

@@ -0,0 +1,138 @@
const path = require('path');
const svgo = require('svgo');
const { readdir, readFile, writeFile, exists } = require('fs/promises');
const { pascalCase, paramCase } = require('change-case');
const svgr = require('@svgr/core');
function isDuotone(name) {
return name.endsWith('Duotone');
}
async function optimizeSvg(folder) {
try {
const icons = await readdir(folder);
const generateIcons = icons
.filter(n => n.endsWith('.svg'))
.map(async icon => {
let originSvg;
try {
originSvg = await readFile(path.resolve(folder, icon));
} catch (err) {
console.error(err);
}
let optimizedSvg;
try {
const data = optimize(originSvg);
optimizedSvg = data.data;
} catch (err) {
console.error(err);
}
const JSXContent = await getJSXContent(
pascalCase(icon),
optimizedSvg
);
const iconName = path.basename(icon, '.svg');
await writeFile(
path.resolve(folder, `${iconName}.tsx`),
JSXContent,
{ encoding: 'utf8', flag: '' }
);
console.log('Generated:', iconName);
});
await Promise.allSettled([
...generateIcons,
generateImportEntry(icons, folder),
]);
} catch (err) {
console.error(err);
}
}
function optimize(input) {
return svgo.optimize(input, {
plugins: [
'preset-default',
'prefixIds',
{
name: 'sortAttrs',
params: {
xmlnsOrder: 'alphabetical',
},
},
],
});
}
/**
* get icon component template
*
* @param {string} name
*/
async function getJSXContent(name, svgCode) {
let svgrContent = '';
try {
svgrContent = await svgr.transform(
svgCode,
{
icon: true,
typescript: true,
},
{ componentName: `${name}Icon1` }
);
} catch (err) {
console.error(err);
}
let matcher = svgrContent.match(/<svg ([^\>]+)>([\s\S]*?)<\/svg>/);
return `
import { FC } from 'react';
import { SvgIcon, SvgIconProps } from '@mui/material';
export const ${name}Icon: FC<SvgIconProps> = (props) => (
<SvgIcon ${matcher[1]}>
${matcher[2]}
</SvgIcon>
);
`;
}
async function generateImportEntry(iconNodes, folder) {
const fileWithImportsPath = path.resolve(folder, 'index.ts');
const importsContent = iconNodes
.map(iconNode => {
const iconName = paramCase(iconNode.name);
if (!iconName) {
return `// Error: ${iconNode.name}`;
}
return `export * from './${iconName}/${iconName}';`;
})
.join('\n');
await fs.writeFile(
fileWithImportsPath,
`export const timestamp = ${Date.now()};\n${importsContent}`,
{ encoding: 'utf8' }
);
}
/**
* @param {*} options
* @param {array} options.assets
* @param {string} options.assets.folder
* @param {*} context
* @returns
*/
exports['default'] = async function svgo(options, context) {
const libRoot = context.workspace.projects[context.projectName].root;
await Promise.allSettled(
(options.assets || []).map(async (asset, index) => {
await optimizeSvg(path.resolve(libRoot, asset.folder));
})
);
return { success: true };
};

View File

@@ -0,0 +1,9 @@
{
"executors": {
"tsCheck": {
"implementation": "./tsCheck.js",
"schema": "./schema.json",
"description": "Runs `tsCheck`."
}
}
}

View File

@@ -0,0 +1,3 @@
{
"executors": "./executor.json"
}

View File

@@ -0,0 +1,11 @@
{
"$schema": "http://json-schema.org/schema",
"type": "object",
"cli": "nx",
"properties": {
"tsCheck": {
"type": "string",
"description": "Typescript check"
}
}
}

View File

@@ -0,0 +1,18 @@
const {
detectPackageManager,
} = require('@nrwl/tao/src/shared/package-manager');
const { spawn } = require('child_process');
exports['default'] = async function tscExecutor(_, context) {
const libRoot = context.workspace.projects[context.projectName].root;
const executionCode = await new Promise(resolve => {
const child = spawn('pnpm', ['exec', 'tsc', '-b', libRoot], {
stdio: 'inherit',
});
child.on('data', args => console.log(args));
child.on('close', code => resolve(code));
});
return { success: executionCode === 0 };
};

View File

16
tools/notify.mjs Normal file
View File

@@ -0,0 +1,16 @@
import got from 'got';
const STAGE_HOST = 'https://nightly.affine.pro/';
if (process.env.CF_PAGES_BRANCH === 'master') {
const message = `Daily builds: New deployment of Ligo-Virgo version [${process.env.CF_PAGES_COMMIT_SHA}](${STAGE_HOST}), this [${STAGE_HOST}](${STAGE_HOST}) link always points to the latest version deployment, to access the old version, please go to Github to view the deployment record.`;
const url = `https://api.telegram.org/bot${process.env.BOT_TOKEN}/sendMessage`;
got.post(url, {
json: {
chat_id: process.env.CHAT_ID,
text: message,
parse_mode: 'Markdown',
disable_notification: true,
},
}).then(r => console.log(r.body));
}

12
tools/tsconfig.tools.json Normal file
View File

@@ -0,0 +1,12 @@
{
"extends": "../tsconfig.base.json",
"compilerOptions": {
"outDir": "../dist/out-tsc/tools",
"rootDir": ".",
"module": "commonjs",
"target": "es5",
"types": ["node"],
"importHelpers": false
},
"include": ["**/*.ts"]
}