akveo / react-native-ui-kitten

:boom: React Native UI Library based on Eva Design System :new_moon_with_face::sparkles:Dark Mode
https://akveo.github.io/react-native-ui-kitten/
MIT License
10.33k stars 956 forks source link

More transparent theme style JSON generation. #1019

Open dehypnosis opened 4 years ago

dehypnosis commented 4 years ago

🚀 Feature Proposal

Currently(v5), theme style JSON can be pre-generated with @ui-kitten/metro-config module. Which watches a given custom mapping file and creates a JSON file, then injects the created JSON file under node_modules/@eva-design/... directory. And the @eva-design/eva or material modules' entry source code is rewrited to exports exports.style = (style object from generated JSON). And finally <ApplicationProvider styles={evaModule.styles} (...)> be not going to create styles in runtime.

It works well and seems to make launching somewhat faster (I have not exactly measured yet).

While struggling with above problems, I thought rather another way of style pre-generation would be great. So now I generate styles modules by programmatically call the schemaProcessor.createStyles and store it into project files, then requires modules for each platforms and provide as styles prop ofApplicationProvider.

Yes it won't watch the custom mapping dynamically but it can support multiple styles and seems to easy to understand the theming flow.

I hope a tool to generate styles JSON (or js module) with command line might be good rather current veiled process. What do you think about such problems?

Example

Generate styles as js module to reduce JSON parsing time before build for native and web platforms each.

const crypto = require("crypto");
const fs = require("fs");
const path = require("path");
const _ = require("lodash");
const { SchemaProcessor } = require("@eva-design/processor");
const schemaProcessor = new SchemaProcessor();

const defaultMapping = require("@eva-design/eva/mapping.json");
const nativeMapping = _.merge({}, defaultMapping, require("./theme.mapping.json"));
const webMapping = _.merge({}, defaultMapping, require("./theme.mapping.web.json"));

createStylesFile("theme.styles.generated", nativeMapping);
createStylesFile("theme.styles.generated.web", webMapping);

function createStylesFile(filePath, mapping) {
  const json = JSON.stringify({
    checksum: crypto.createHash("sha1").update(JSON.stringify(mapping, null, 2)).digest("hex"),
    styles: schemaProcessor.process(mapping),
  }, null, 2);
  const file = `module.exports = ${json};`;
  fs.writeFileSync(
    path.resolve(__dirname, filePath),
    file,
  );
}

Provide styles in runtime

import { styles } from "./theme.styles.generated";
import { theme } from "./theme.palettes";

// ...
function App() {
  return (
    <EvaThemeProvider styles={styles} theme={theme}>
    {/* ... */}
    </EvaThemeProvider>
  );
}
artyorsh commented 4 years ago

But the whole process of styles injection is complicate and veiled, I think.

As for a final user, how does it affect your product? The idea is let users do nothing, making the plugin doing the required job. As a user in most cases I don't want to focus on the details. I'm glad to discuss if you have any ideas to simplify, but saving this concept is required.

Also my project is mono-repo which raises no such file path.. error with @ui-kitten/metro-config. (I had to make a tricky workaround to resolve it with @ui-kitten/metro-config)

Could you please explain in a bit more details or (what is even better) create a demo project?

I hope a tool to generate styles JSON (or js module) with command line might be good

Do you have ideas of what it should be like to cover the problems you faced? If it is a command-line function, what it should be like?

dehypnosis commented 4 years ago

A1/A2. It does not affect to the final product surely but it intervened development process.

Firstly, I couldn't make variant styles for each platforms. So I had to dig into and figured out whole generation process and de-hoisted each @eva-design/{theme} modules for each platforms. It took me a lot of time.

At this point, I thought rather transparent even complicated API could be better for my case. Because setting up UI framework is very important and only required for the phase of initial project setup, so I could gladly elaborate it with much times.

Secondly, Automated metro-config module didn't find right directory of @eva-design/{theme} for my project which looks like below.

node_modules
  @eva-design/
pkg/
  common/
    src/
      app.tsx
  mobile/
     ios/
     android/
   web/
...

Sorry for not providing demo project but simply RELATIVE_PATHS and resolution logic cannot fit for every structure.

const RELATIVE_PATHS = {
  evaPackage: (evaPackage: string): string => {
    return `node_modules/${evaPackage}`;
  },
  evaMapping: (evaPackage: string): string => {
    return `node_modules/${evaPackage}/mapping.json`;
  },
  evaIndex: (evaPackage: string): string => {
    return `node_modules/${evaPackage}/index.js`;
  },
  cache: (evaPackage: string): string => {
    return `node_modules/${evaPackage}/${CACHE_FILE_NAME}`;
  },
};

https://github.com/akveo/react-native-ui-kitten/blob/master/src/metro-config/services/bootstrap.service.ts#L18

So I had to modify metro-config module code a little to correct the wrong path for my project.


A3. After struggling with above problems. I just removed metro-config module and manually compiled each styles for each platforms with @eva-design/processor module. Then simply put them to context provider component directly.

So here IMHO, what about to generate styles directly to project tree with metro-config rather creating styles into node_modules directory and modifying @eva-design/{theme} module main code? (forget about the CLI here)

For example... as like below.

metro-config.js

const MetroConfig = require('@ui-kitten/metro-config');
const evaConfig = [
  {
     evaPackagePath: path.resolve('@eva-design/eva'),
     customMappingPath: path.resolve('./custom-mapping.web.json'),
     outputStylesPath: path.resolve('./styles.web.json'),
  },
  {
     evaPackagePath: path.resolve('@eva-design/eva'),
     customMappingPath: path.resolve('./custom-mapping.native.json'),
     outputStylesPath: path.resolve('./styles.native.json'),
  },
];

module.exports = MetroConfig.create(evaConfig, {
  // Whatever was previously specified
});

Application code

import { checksum, styles } from "./styles";

// ...
function App() {
  return (
    <EvaThemeProvider styles={styles} theme={theme}>
    {/* ... */}
    </EvaThemeProvider>
  );
}

Thank you for reading.

artyorsh commented 4 years ago

@dehypnosis Well, I thinking a bit about the possible solutions for platform-dependent mappings, and here is the best option I realized: Instead of using several mapping files, you can just use a variant groups feature. For instance, in mapping:

// ...
"MyComponent": {
  // ...
  "mapping": {},
  "variantGroups": {
     "platform": {
       "ios": {
           "fontSize": 24
        },
       "android": {
           "fontSize": 24
        },
       "web": {
           "fontSize": 32
        },
     },
   },
}

Then, in js:

<MyComponent platform={Platform.OS} />

I guess it covers every use-case and allows you having only one mapping.json. Also, it wouldn't break watchers. What do you think?

but simply RELATIVE_PATHS and resolution logic cannot fit for every structure.

That's true. I guess it should be rewritten with using of node process arguments.

dehypnosis commented 4 years ago

Wow you are such a genious, that structure looks really good. I have no different idea.

Bytheway what about module.exports.styles = (generated module) logic? Going as original one with correct path resolution?

Seeing your new mapping structure, It seems that variant components (providers? right?) can have variant plaforms' mappings. So is there an idea for generating multiple compilations to inject into each components for each proper platforms automatically or rather manually?

2020년 4월 20일 (월) 오후 9:57, Artur Yorsh notifications@github.com님이 작성:

@dehypnosis https://github.com/dehypnosis Well, I thinking a bit about the possible solutions for platform-dependent mappings, and here is the best option I realized: Instead of using several mapping files, you can just use a variant groups feature. For instance, in mapping:

// ..."MyComponent": { // ... "mapping": {}, "variantGroups": { "platform": { "ios": { "fontSize": 24 }, "android": { "fontSize": 24 }, "web": { "fontSize": 32 }, }, }, }

Then, in js:

I guess it covers every use-case and allows you having only one mapping.json. Also, it wouldn't break watchers. What do you think?

but simply RELATIVE_PATHS and resolution logic cannot fit for every structure.

That's true. I guess it should be rewritten with using of node process arguments.

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/akveo/react-native-ui-kitten/issues/1019#issuecomment-616534954, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAY4ZLJWFQBAYYD6JVAKWHDRNRBCDANCNFSM4MHO747Q .

artyorsh commented 4 years ago

Seeing your new mapping structure

It's not new. This is something that already works. For instance, in terms of Eva status and size properties of button are variant groups. Also, you can combine multiple variant groups to achieve the result.

Mapping is extendable. For now, the only thing you may stuck when adding custom variant groups (like platform) to existing components is that you'll have TS issues if you use it. Anyways, it will not break your own components if you describe them.

dehypnosis commented 4 years ago

Okay I got it. what a flexible code base!

2020년 4월 21일 (화) 오전 1:37, Artur Yorsh notifications@github.com님이 작성:

Seeing your new mapping structure

It's not new. This is something that is already works. For instance, status and size properties of button are variant groups. Also, you can combine multiple variant groups to achieve the result.

Mapping is extendable. For now, the only thing you may stuck when adding custom variant groups (like platform) to existing components is that you'll have TS issues if you use it. Anyways, it will not break your own components if you describe them.

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/akveo/react-native-ui-kitten/issues/1019#issuecomment-616670468, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAY4ZLNRGNV3JLPEVRBGAP3RNR24TANCNFSM4MHO747Q .

artyorsh commented 4 years ago

@dehypnosis I would suggest looking through existing implementation to realize how it works. There is no meaningful documentation on working with mapping to be honest.

mapping.json

dehypnosis commented 4 years ago

So <ApplicationProvider platform={..} ... /> is avilable with strict mapping with variant groups now?

2020년 4월 21일 (화) 오전 1:44, Artur Yorsh notifications@github.com님이 작성:

@dehypnosis https://github.com/dehypnosis I would suggest looking through existing implementation to realize how it works. There is no meaningful documentation on working with mapping to be honest.

mapping.json https://github.com/eva-design/eva/blob/master/packages/eva/mapping.json

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/akveo/react-native-ui-kitten/issues/1019#issuecomment-616674966, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAY4ZLK5OIYHISSSO4K4QYDRNR3X3ANCNFSM4MHO747Q .

artyorsh commented 4 years ago

@dehypnosis ApplicationProvider has no variant groups, this is about the components.

dehypnosis commented 4 years ago

Okay then for examlple how can i set whole font family for web plaform only? Sorry for bothering you with too much questions. If cannot, It comes back to the start of the issue for me..

2020년 4월 21일 (화) 오전 1:59, Artur Yorsh notifications@github.com님이 작성:

@dehypnosis https://github.com/dehypnosis ApplicationProvider has no variant groups, this is about the components.

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/akveo/react-native-ui-kitten/issues/1019#issuecomment-616683797, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAY4ZLLZ4XF2DBAOXSJIBT3RNR5OJANCNFSM4MHO747Q .

artyorsh commented 4 years ago

Well, let's dive deeper into the mapping engine together. 🚀

So what the docs say: you may use text-font-family to set the font globally for all components. Now lets go deeper and remember that the text in React Native should also be wrapped in a Text component, meaning we're able to customize it. Knowing all of that, let's see how the Text mapping is structured. And realize we can create a platform variant group to make it platform dependent like it was described above. The final optimization I see here would be creating own component which renders UI Kitten Text component, but a little optimized with defaultProps to include platform: Platform.OS which will finally make the text configured for the platform it runs with no extra props.

Easy 😄 🤷‍♂️

dehypnosis commented 4 years ago

I got the idea, so you mean it?

<ApplicationProvider {...eva}>
  <MyTextWrapperWithPlatformProps platform={Platform.OS}>
    // ...
  </MyTextWrapper>
</ApplicationProvider>

with "MyTextWrapperWithPlatformProps" mapping for custom text-font-family.

Then will the mapping be inherited by the descendants? If then, all my needs will satisfy. Anyway thanks a lot for your awesome work and about the answers. I will try your solution later when the path resolution issue fixed.

artyorsh commented 4 years ago

@dehypnosis what // ... in text children supposed to be?

Then will the mapping be inherited by the descendants?

Not by children. That's why I suggest creating defaultProps

asherccohen commented 3 years ago

Having the same issue with a NX monorepo setup, curious to know how this evolves....

sajadghawami commented 3 years ago

@dehypnosis

you wrote:

  • Also my project is mono-repo which raises no such file path.. error with @ui-kitten/metro-config. (I had to make a tricky workaround to resolve it with @ui-kitten/metro-config)

Also my project is mono-repo which raises no such file path.. error with @ui-kitten/metro-config. (I had to make a tricky workaround to resolve it with @ui-kitten/metro-config)

What was that workaround? I am having the exact same issue, and i cant find anything on how to resolve this...

thanks in advance!

sschottler commented 3 years ago

I ran into similar issues. This was my workaround for the hard-coded RELATIVE_PATHS inside BootstrapService (detect root and call process.chdir before calling BootstrapService.run):

// cli.js
const path = require('path');
const findWorkspaceRoot = require('find-yarn-workspace-root');
const BootstrapService = require('@ui-kitten/metro-config/services/bootstrap.service').default;

function findRootFolder() {
  const packageFolder = path.dirname(__dirname);
  const index = packageFolder.toLowerCase().indexOf('node_modules');
  const isRunningInsideNodeModules = index > -1;

  if (isRunningInsideNodeModules) {
    return packageFolder.substring(0, index);
  }
  // running inside local monorepo:
  return findWorkspaceRoot(packageFolder);
}

const rootFolder = findRootFolder();
process.chdir(rootFolder);

BootstrapService.run({ evaPackage: '@eva-design/eva', customMappingFile: '....' });
node cli