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

proposal: Explicit exports #2604

Open
idoros opened this issue Jul 11, 2022 · 6 comments
Open

proposal: Explicit exports #2604

idoros opened this issue Jul 11, 2022 · 6 comments
Assignees
Labels
core Processing and transforming logic discussion Ongoing conversation feature New syntax feature or behavior

Comments

@idoros
Copy link
Collaborator

idoros commented Jul 11, 2022

Stylable stylesheet has 2 different export targets:

  • stylesheet - using stylable @st-import for customization, interface defintion or re-export
  • javascript - using JS import for view binding and dynamic CSS customization.

There are several stylesheet entities that can be exported today (element-type, class, custom-property, keyframes, layer, st-var), and we can assume there will be more in the future (e.g. container-name).

Current state

Today there is no way to mark which symbols are exported, and implicitly all symbols are considered public and exported for other stylesheets, while javascript gets all symbols except the imported st-var types (to reduce the bloat of probably unneeded data at run time).

Goals

  • control private definitions from leaking out
  • control generated JS module size by exporting only required view definitions
  • allow export name to differ from local (manage api over time)

Proposal

Add a new @st-export atrule to allow explicit exports control (taken from #1489):

  • the existence of the definition in a stylesheet would cancel the automatic implicit public API
  • multiple @st-export are allowed - with additive behavior
  • similar syntax to @st-import for typed/namespaced definitions (e.g. keyframes and layer)
  • public class is considered a custom pseudo-element (private are not)
  • public/private custom pseudo-states are not part of this proposal
  • default export is also not part of this proposal - currently root is automatically exported as default export (not to JS). This behavior will continue with explicit exports. And a user might be able to opt-in to explicit default export in the future (see discussion in the comments below).

Examples

export specific symbols for CSS and JS:

@st-export /*implicit export to css & js*/ [
    classX,
    classY,
    stVarZ,
    keyframes(
        jump,
    ),
];

export specific symbols just for CSS or JS:

@st-export to(css) [
    ...
];
@st-export to(js) [
    ...
];

override specific symbol export target:

@st-export to(css, js) [
    classX,
    stVarZ to(css),
];

/* identical to: */

@st-export to(css) [
    classX to(css, js),
    stVarZ,
];

additive definition:

/* a is exported to both CSS and JS */
@st-export to(css) [
    a
];
@st-export to(js) [
    a
];

rename export:

@st-export [
    /* local a export as b to CSS & JS*/
    a as b,

    /* local c export as d to JS only*/
    c as d to(js),
];

Notice(1) that the syntax is reflecting @st-import with the following differences:

  • to() is allowed as a general target for the entire export block
  • to() is allowed for a specific import override
  • from is not allowed atm (leaves open the possibility for re-export)
  • default is not allowed atm since overriding root has some bigger repercussions (might be enabled in the future)

Notice(2) the symbols are referenced like @st-import without a prefix for classes.
This is something that we want to change in @st-import, but I don't want to bind it to this proposal.
So for now classes are used without the . (dot). Alternatively @st-export could require a full qualified identifier, making it different.

Related

@idoros idoros added feature New syntax feature or behavior discussion Ongoing conversation core Processing and transforming logic labels Jul 11, 2022
@idoros idoros self-assigned this Jul 11, 2022
@barak007
Copy link
Collaborator

* as exports are not allowed also

@tomrav
Copy link
Collaborator

tomrav commented Jul 18, 2022

I think that as a first step for this feature, we can skip the nested overrides part of the API (unless it ends up being very trivial to implement.

I think there's also a question of addressing the root named export, and what meaning that has, if any.

@idoros
Copy link
Collaborator Author

idoros commented Aug 25, 2022

We need to specify how root is handled with explicit exports.

With implicit exports, like today, all classes (including root) are exported as named to both Stylesheet and JavaScript imports and the root is also available as the default import through @st-import.

For example, a button.st.css stylesheet with 2 classes:

.label {}
.icon {}

Result import

/* css */
@st-import Btn, [root, label, icon] from './button.st.css';
/* 
    Btn ==  button__root
    root ==  button__root
    label ==  button__label
    icon ==  button__icon
*/

/* js */
import { classes } from './button.st.css';
classes.root === 'button__root';
classes.label === 'button__label';
classes.icon === 'button__icon';

Explicit root export

With explicit exports, the question is, what is the default export and how is it declared.

To make things a bit easier, I suggest we start by only allowing a class to be exported as default, maybe even just the root itself. We can ease this limitation in the future.

Exporting nothing will result in no root:

@st-export [label, icon];

Result import

/* css */
@st-import Btn, [root, label, icon] from './button.st.css';
/* 
    Btn ==  undefined
    root ==  undefined
    label ==  button__label
    icon ==  button__icon
*/

/* js */
import { classes } from './button.st.css';
classes.root === undefined;
classes.label === 'button__label';
classes.icon === 'button__icon';

Option 1: default as root

Export default and use that it as a named root as well

@st-export root, [label, icon];

Result import

/* css */
@st-import Btn, [root, label, icon] from './button.st.css';
/* 
    Btn ==  button__root
    root ==  button__root
    label ==  button__label
    icon ==  button__icon
*/

/* js */
import { classes } from './button.st.css';
classes.root === 'button__root';
classes.label === 'button__label';
classes.icon === 'button__icon';

Option 2: named root to default

The symbol that is exported as root is also exported as default.

@st-export [root, label, icon];

Result import

/* css */
@st-import Btn, [root, label, icon] from './button.st.css';
/* 
    Btn ==  button__root
    root ==  button__root
    label ==  button__label
    icon ==  button__icon
*/

/* js */
import { classes } from './button.st.css';
classes.root === 'button__root';
classes.label === 'button__label';
classes.icon === 'button__icon';

@idoros idoros changed the title Explicit exports proposal: Explicit exports Aug 30, 2022
@idoros idoros mentioned this issue Aug 30, 2022
7 tasks
@barak007
Copy link
Collaborator

barak007 commented Sep 4, 2022

I prefer the 2nd option with the addition of allowing to default export something else.
reasons:

  1. The default is "less important" when we talking about exporting multiple components from single stylesheet
  2. keep the syntax cleaner only using named exports
  3. easy to explain
  4. we don't export the default to js currently we use the root via classes

@idoros
Copy link
Collaborator Author

idoros commented Sep 4, 2022

So if I understand you correctly:

  • no export - there are no auto root or default export
    • @st-export []; root=undefined, default=undefined
  • named root export - take default from root
    • @st-export [root]; root=ns__root, default=ns__root
  • default export - doesn't override root
    • @st-export root, []; root=undefined, default=ns__root
  • root doesn't override explicit root
    • @st-export other, [root]; root=ns__root, default=ns_other

There is also another options I failed to mention:

Option 3: root as default

root is a special case and whatever is exported as root is considered as the default export.

@st-export [root, label, icon];

Result import

/* css */
@st-import Btn, [root, label, icon] from './button.st.css';
/* 
    Btn ==  button__root
    root ==  button__root
    label ==  button__label
    icon ==  button__icon
*/

/* js */
import { classes } from './button.st.css';
classes.root === 'button__root';
classes.label === 'button__label';
classes.icon === 'button__icon';

Or with a another class as root:

@st-export [another as root, label, icon];

Result import

/* css */
@st-import Btn, [root, label, icon] from './button.st.css';
/* 
    Btn ==  button__another
    root ==  button__another
    label ==  button__label
    icon ==  button__icon
*/

/* js */
import { classes } from './button.st.css';
classes.root === 'button__another';
classes.label === 'button__label';
classes.icon === 'button__icon';

@idoros
Copy link
Collaborator Author

idoros commented Sep 13, 2022

This proposal has encountered some resistance offline.

The main issue is that a lot of the classes in a stylesheets are actually inner "parts" that are meant to be public, and opting-in to "private-by-default" with the approach offered here means that all the classes (and other symbols) would need to me marked as public, adding to the source size (making developers read and write more code).

An alternative to keep everything public and only mark private definitions was offered, but we have some unresolved issues with it.

The original goals:

  1. control private definitions from leaking out
  2. control generated JS module size by exporting only required view definitions
  3. allow export name to differ from local (manage api over time)

The first is covered by the alternative suggestion, but the other 2 are less covered. With the more strict "private-by-default" there was no unintentional leakage of definitions to be used as API or to bloat the runtime. And in addition we can't seem to settle on a syntax (bikeshedding here):

control declaration

.class {
  -st-access: private;
}
:vars {
  myVar: green; /* issue: no body */
}
@keyframes myFrames {
  -st-access: private; /* issue: prefer not to add into native definitions */
}

control with annotation-like atrule

/* issue: there is nothing like this in CSS - prefer not to add an atrule that affects an adjacent definition */
@access private;
.class {}
:vars {
  @access private;
  myVar: green;
}
@access private;
@keyframes myFrames {}

control nested definitions

/* much more verbose in cases with styling and not just interface */
@access private {
  .class {}
  :vars {
    myVar: green;
  }
  @keyframes myFrames {}
}
.class {} /* should it be public when defined outside? */

control by convention

/* start with "_" to mark private */
._class {}
:_vars {
  _myVar: green;
}
@keyframes _myFrames {}
/* issue: no way to mark mapping */

control accessor-like-prefix

/* issue: wraps all syntax into atrule, makes processing, syntax highlighting and generally everything more complex */
@access(private) .class {}
:vars {
  @access(private) myVar: green; /* issue: no body */
}
@access(private) @keyframes myFrames {}

I didn't go into mapping, CSS/JavaScript separate control, or a new request to control access to specific packages/directories in the previous examples.


This boils down mostly to a DX vs UX preferences, and I feel that by not going with the "private-by-default" approach it's not worth adding the complexity. ATM an initial "_" character can be used to symbolize private symbols with no changes to stylable.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
core Processing and transforming logic discussion Ongoing conversation feature New syntax feature or behavior
Projects
Status: No status
Development

No branches or pull requests

3 participants