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

refactor(core): css-keyframes feature extraction #2215

Merged
merged 27 commits into from
Jan 2, 2022
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
7494698
empty `css-keframes` feature file to open the PR
idoros Dec 21, 2021
9490819
chore: prettify
idoros Dec 21, 2021
9f76573
refactor(core): move related code into feature
idoros Dec 21, 2021
fb4bacf
refactor: move code from `processor` to feature
idoros Dec 26, 2021
8cbc176
chore: removed commented code
idoros Dec 27, 2021
9ff4e2e
refactor: register imported keyframes to `st-symbol`
idoros Dec 27, 2021
c9cdab6
refactor: move code from `transformer` to feature
idoros Dec 27, 2021
8bc0c8c
chore: removed refactored out code from resolver
idoros Dec 27, 2021
f49df55
chore: forgotton leftovers
idoros Dec 27, 2021
1fe5530
test: move related keyframes tests to feature spec
idoros Dec 27, 2021
f54dedb
test: format and add tests
idoros Dec 27, 2021
8a5c95d
refactor: use the same `redeclare` diagnostics as other symbols
idoros Dec 27, 2021
e622de5
deprecate: keyframes meta fields
idoros Dec 27, 2021
20b05eb
refactor: keyframes symbol import registration to `css-keyframes` fea…
idoros Dec 27, 2021
ed9e594
fix: js-mixin keyframes to match current behavior
idoros Dec 28, 2021
6f9ef96
fix: keyframes symbols should not be inserted to depracated `mappedSy…
idoros Dec 28, 2021
aeb44d2
fix: report unresloved import keyframes diagnostic
idoros Dec 28, 2021
d4b6874
test: add back mistakenly removed test
idoros Dec 28, 2021
18fe4e6
chore: cleanup resolve import between namespaces
idoros Dec 29, 2021
bd8685d
fix: allow keyframes redeclare under seperate scopes
idoros Dec 29, 2021
ee3e6b5
fix: allow keyframes to be nested under root or `@media`
idoros Dec 29, 2021
3679bd7
fix: keyframe escaping
idoros Dec 29, 2021
336dcd3
chore: removed escape of keyframes
idoros Jan 2, 2022
4aed373
chore: remove un-required import
idoros Jan 2, 2022
043c9b4
feat: support `@keyframes` in `@supports`
idoros Jan 2, 2022
e77552b
fix: global keyframes with no name symbol
idoros Jan 2, 2022
6b81954
chore: small review changes
idoros Jan 2, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion packages/cli/src/base-generator.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { IFileSystem } from '@file-services/types';
import type { Stylable } from '@stylable/core';
import { CSSKeyframes } from '@stylable/core/dist/features';
import camelcase from 'lodash.camelcase';
import upperfirst from 'lodash.upperfirst';
import { basename, relative } from 'path';
Expand Down Expand Up @@ -111,7 +112,7 @@ export function reExportsAllSymbols(filePath: string, generator: Generator): ReE
acc[varName] = `--${rootExport}__${varName.slice(2)}`;
return acc;
}, {});
const keyframes = Object.keys(meta.mappedKeyframes).reduce<Record<string, string>>(
const keyframes = Object.keys(CSSKeyframes.getAll(meta)).reduce<Record<string, string>>(
idoros marked this conversation as resolved.
Show resolved Hide resolved
(acc, keyframe) => {
acc[keyframe] = `${rootExport}__${keyframe}`;
return acc;
Expand Down
303 changes: 303 additions & 0 deletions packages/core/src/features/css-keyframes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,303 @@
import { createFeature, FeatureContext } from './feature';
import * as STSymbol from './st-symbol';
import * as STImport from './st-import';
import type { Imported } from './st-import';
import type { StylableMeta } from '../stylable-meta';
import type { StylableResolver } from '../stylable-resolver';
import { plugableRecord } from '../helpers/plugable-record';
import { ignoreDeprecationWarn } from '../helpers/deprecation';
import { isInConditionalGroup } from '../helpers/rule';
import { namespace } from '../helpers/namespace';
import { paramMapping } from '../stylable-value-parsers';
import { globalValue } from '../utils';
import type * as postcss from 'postcss';
import postcssValueParser from 'postcss-value-parser';

export interface KeyframesSymbol {
_kind: 'keyframes';
alias: string;
name: string;
import?: Imported;
global?: boolean;
}

export interface KeyframesResolve {
meta: StylableMeta;
symbol: KeyframesSymbol;
}

export const reservedKeyFrames = [
'none',
'inherited',
'initial',
'unset',
/* single-timing-function */
'linear',
'ease',
'ease-in',
'ease-in-out',
'ease-out',
'step-start',
'step-end',
'start',
'end',
/* single-animation-iteration-count */
'infinite',
/* single-animation-direction */
'normal',
'reverse',
'alternate',
'alternate-reverse',
/* single-animation-fill-mode */
'forwards',
'backwards',
'both',
/* single-animation-play-state */
'running',
'paused',
];

export const diagnostics = {
ILLEGAL_KEYFRAMES_NESTING() {
return `illegal nested "@keyframes"`;
},
MISSING_KEYFRAMES_NAME() {
return '"@keyframes" missing parameter';
},
MISSING_KEYFRAMES_NAME_INSIDE_GLOBAL() {
return `"@keyframes" missing parameter inside "${paramMapping.global}()"`;
},
KEYFRAME_NAME_RESERVED(name: string) {
return `keyframes "${name}" is reserved`;
},
UNKNOWN_IMPORTED_KEYFRAMES(name: string, path: string) {
return `cannot resolve imported keyframes "${name}" from stylesheet "${path}"`;
},
};

const dataKey = plugableRecord.key<{
statements: postcss.AtRule[];
paths: Record<string, string[]>;
imports: string[];
}>('keyframes');

// HOOKS

STImport.ImportTypeHook.set(`keyframes`, (context, localName, importName, importDef) => {
addKeyframes({
context,
name: localName,
importName,
ast: importDef.rule,
importDef,
});
});

export const hooks = createFeature<{
RESOLVED: Record<string, KeyframesResolve>;
}>({
metaInit({ meta }) {
plugableRecord.set(meta.data, dataKey, { statements: [], paths: {}, imports: [] });
},
analyzeAtRule({ context, atRule }) {
let { params: name } = atRule;
// check nesting validity
if (!isInConditionalGroup(atRule, true)) {
context.diagnostics.error(atRule, diagnostics.ILLEGAL_KEYFRAMES_NESTING());
return;
}
// save keyframes declarations
const { statements: keyframesAsts } = plugableRecord.getUnsafe(context.meta.data, dataKey);
keyframesAsts.push(atRule);
// deprecated
ignoreDeprecationWarn(() => context.meta.keyframes.push(atRule));
// validate name
if (!name) {
context.diagnostics.warn(atRule, diagnostics.MISSING_KEYFRAMES_NAME());
return;
}
//
let global: boolean | undefined;
const globalName = globalValue(name);
if (globalName !== undefined) {
name = globalName;
global = true;
}
if (name === '') {
context.diagnostics.warn(atRule, diagnostics.MISSING_KEYFRAMES_NAME_INSIDE_GLOBAL());
idoros marked this conversation as resolved.
Show resolved Hide resolved
}
if (reservedKeyFrames.includes(name)) {
context.diagnostics.error(atRule, diagnostics.KEYFRAME_NAME_RESERVED(name), {
word: name,
});
}
addKeyframes({
context,
name,
importName: name,
ast: atRule,
global,
});
},
transformResolve({ context }) {
const symbols = STSymbol.getAllByType(context.meta, `keyframes`);
const resolved: Record<string, KeyframesResolve> = {};
for (const [name, symbol] of Object.entries(symbols)) {
const res = resolveKeyframes(context.meta, symbol, context.resolver);
if (res) {
idoros marked this conversation as resolved.
Show resolved Hide resolved
resolved[name] = res;
} else if (symbol.import) {
context.diagnostics.error(
symbol.import.rule,
diagnostics.UNKNOWN_IMPORTED_KEYFRAMES(symbol.name, symbol.import.request),
{
word: symbol.name,
}
);
}
}
return resolved;
},
transformAtRuleNode({ context, atRule, resolved }) {
const globalName = globalValue(atRule.params);
const name = globalName ?? atRule.params;
const resolve = resolved[name];
/* js keyframes mixins won't have resolved keyframes */
atRule.params = resolve
? getTransformedName(resolve)
: globalName ?? namespace(name, context.meta.namespace);
},
transformDeclaration({ decl, resolved }) {
const parsed = postcssValueParser(decl.value);
// ToDo: improve by correctly parse & identify `animation-name`
// ToDo: handle symbols from js mixin
parsed.nodes.forEach((node) => {
const resolve = resolved[node.value];
const scoped = resolve && getTransformedName(resolve);
if (scoped) {
node.value = scoped;
}
});
decl.value = parsed.toString();
},
transformJSExports({ exports, resolved }) {
for (const [name, resolve] of Object.entries(resolved)) {
exports.keyframes[name] = getTransformedName(resolve);
}
},
});

// API

export function getKeyframesStatements({ data }: StylableMeta): ReadonlyArray<postcss.AtRule> {
const { statements } = plugableRecord.getUnsafe(data, dataKey);
return statements;
}

export function get(meta: StylableMeta, name: string): KeyframesSymbol | undefined {
return STSymbol.get(meta, name, `keyframes`);
}

export function getAll(meta: StylableMeta): Record<string, KeyframesSymbol> {
return STSymbol.getAllByType(meta, `keyframes`);
}

function addKeyframes({
context,
name,
importName,
ast,
global,
importDef,
}: {
context: FeatureContext;
name: string;
importName: string;
ast: postcss.AtRule | postcss.Rule;
global?: boolean;
importDef?: Imported;
}) {
const isFirstInPath = addKeyframesDeclaration(context.meta, name, ast, !!importDef);
const safeRedeclare = isFirstInPath && !!STSymbol.get(context.meta, name, `keyframes`);
idoros marked this conversation as resolved.
Show resolved Hide resolved
// fields are confusing in this symbol:
// name: the import name if imported OR the local name
// alias: the local name
STSymbol.addSymbol({
context,
node: ast,
localName: name,
symbol: {
_kind: 'keyframes',
alias: name,
name: importName,
global,
import: importDef,
},
safeRedeclare,
});
// deprecated
ignoreDeprecationWarn(() => {
context.meta.mappedKeyframes[name] = STSymbol.get(context.meta, name, `keyframes`)!;
});
}

function addKeyframesDeclaration(
meta: StylableMeta,
name: string,
origin: postcss.AtRule | postcss.Rule,
isImported: boolean
) {
let path = ``;
let current = origin.parent;
while (current) {
if (current.type === `rule`) {
path += ` -> ` + (current as postcss.Rule).selector;
} else if (current.type === `atrule`) {
path +=
` -> ` +
(current as postcss.AtRule).name +
` ` +
(current as postcss.AtRule).params;
}
current = current.parent as any;
}
const { paths, imports } = plugableRecord.getUnsafe(meta.data, dataKey);
if (!paths[path]) {
paths[path] = [];
}
const isFirstInPath = !paths[path].includes(name);
const isImportedBefore = imports.includes(name);
paths[path].push(name);
if (isImported) {
imports.push(name);
}
return isFirstInPath && !isImportedBefore;
}

function resolveKeyframes(meta: StylableMeta, symbol: KeyframesSymbol, resolver: StylableResolver) {
let current = { meta, symbol };
while (current.symbol?.import) {
const res = resolver.resolveImported(
current.symbol.import,
current.symbol.name,
'mappedKeyframes' // ToDo: refactor out of resolver
);
if (res?._kind === 'css' && res.symbol?._kind === 'keyframes') {
const { meta, symbol } = res;
current = {
meta,
symbol,
};
} else {
return undefined;
}
}
if (current.symbol) {
return current;
}
return undefined;
}

function getTransformedName({ symbol, meta }: KeyframesResolve) {
return symbol.global ? symbol.alias : namespace(symbol.alias, meta.namespace);
}
33 changes: 29 additions & 4 deletions packages/core/src/features/feature.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { StylableMeta } from '../stylable-meta';
import type { ScopeContext } from '../stylable-transformer';
import type { ScopeContext, StylableExports } from '../stylable-transformer';
import type { StylableResolver } from '../stylable-resolver';
import type * as postcss from 'postcss';
import type { ImmutableSelectorNode } from '@tokey/css-selector-parser';
Expand All @@ -20,11 +20,12 @@ export interface FeatureTransformContext extends FeatureContext {
}

export interface NodeTypes {
SELECTOR: any;
IMMUTABLE_SELECTOR: any;
SELECTOR?: any;
IMMUTABLE_SELECTOR?: any;
RESOLVED?: any;
}

export interface FeatureHooks<T extends NodeTypes> {
export interface FeatureHooks<T extends NodeTypes = NodeTypes> {
metaInit: (context: FeatureContext) => void;
analyzeInit: (context: FeatureContext) => void;
analyzeAtRule: (options: { context: FeatureContext; atRule: postcss.AtRule }) => void;
Expand All @@ -35,11 +36,23 @@ export interface FeatureHooks<T extends NodeTypes> {
walkContext: SelectorNodeContext;
}) => void;
transformInit: (options: { context: FeatureTransformContext }) => void;
transformResolve: (options: { context: FeatureTransformContext }) => T['RESOLVED'];
transformAtRuleNode: (options: {
context: FeatureTransformContext;
atRule: postcss.AtRule;
resolved: T['RESOLVED'];
}) => void;
transformSelectorNode: (options: {
context: FeatureTransformContext;
node: T['SELECTOR'];
selectorContext: Required<ScopeContext>;
}) => void;
transformDeclaration: (options: {
context: FeatureTransformContext;
decl: postcss.Declaration;
resolved: T['RESOLVED'];
}) => void;
transformJSExports: (options: { exports: StylableExports; resolved: T['RESOLVED'] }) => void;
}
const defaultHooks: FeatureHooks<NodeTypes> = {
metaInit() {
Expand All @@ -57,9 +70,21 @@ const defaultHooks: FeatureHooks<NodeTypes> = {
transformInit() {
/**/
},
transformResolve() {
return {};
},
transformAtRuleNode() {
/**/
},
transformSelectorNode() {
/**/
},
transformDeclaration() {
/**/
},
transformJSExports() {
/**/
},
};
export function createFeature<T extends NodeTypes>(
hooks: Partial<FeatureHooks<T>>
Expand Down
Loading