Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

More transparent theme style JSON generation. #1019

Open
dehypnosis opened this issue Apr 14, 2020 · 16 comments
Open

More transparent theme style JSON generation. #1019

dehypnosis opened this issue Apr 14, 2020 · 16 comments
Labels
💡 Proposal 🤷‍♂️ Needs info There is no enough info to resolve the issue

Comments

@dehypnosis
Copy link

dehypnosis commented Apr 14, 2020

🚀 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).

  • But the whole process of styles injection is complicate and veiled, I think.
  • And also I have used two type of custom mappings for each native and web platforms, but current method cannot support such configuration.
  • 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)

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
Copy link
Collaborator

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?

@artyorsh artyorsh added the 🤷‍♂️ Needs info There is no enough info to resolve the issue label Apr 18, 2020
@dehypnosis
Copy link
Author

dehypnosis commented Apr 20, 2020

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
Copy link
Collaborator

@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
Copy link
Author

dehypnosis commented Apr 20, 2020 via email

@artyorsh
Copy link
Collaborator

artyorsh commented Apr 20, 2020

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
Copy link
Author

dehypnosis commented Apr 20, 2020 via email

@artyorsh
Copy link
Collaborator

@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
Copy link
Author

dehypnosis commented Apr 20, 2020 via email

@artyorsh
Copy link
Collaborator

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

@dehypnosis
Copy link
Author

dehypnosis commented Apr 20, 2020 via email

@artyorsh
Copy link
Collaborator

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
Copy link
Author

dehypnosis commented Apr 21, 2020

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
Copy link
Collaborator

@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
Copy link

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

@sajadghawami
Copy link

@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
Copy link

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
💡 Proposal 🤷‍♂️ Needs info There is no enough info to resolve the issue
Projects
None yet
Development

No branches or pull requests

5 participants