Skip to content
This repository has been archived by the owner on Oct 23, 2023. It is now read-only.

Commit

Permalink
feat: extend slot default options
Browse files Browse the repository at this point in the history
* fix: update default slot content properly

* fix: ensure correct children index

* fix: allow multiple jsx elements at root level

* fix: make sure single child slots work properly

* feat: allow slots in default values

* fix: restore essentials library

* fix: remove stray debug text

* fix: refactor slot content analyzing

* feat: open slots by default

* test: write unit tests for slot default adjustments
  • Loading branch information
tilmx committed Apr 9, 2019
1 parent 1b4741c commit 6ffb90d
Show file tree
Hide file tree
Showing 17 changed files with 384 additions and 154 deletions.
38 changes: 38 additions & 0 deletions packages/analyzer/src/react-utils/get-react-slot-type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// tslint:disable:no-bitwise
import * as TypeScript from 'typescript';
import { isTypeReference } from '../typescript-utils/is-type-reference';

const REACT_SLOT_TYPES_SINGLE = ['Element', 'ReactElement', 'ReactChild'];

export function getReactSlotType(
type: TypeScript.Type,
ctx: { program: TypeScript.Program }
): string | undefined {
const typechecker = ctx.program.getTypeChecker();
const symbol = type.aliasSymbol || type.symbol || type.getSymbol();

if (!symbol) {
return;
}

const resolvedSymbol =
(symbol.flags & TypeScript.SymbolFlags.AliasExcludes) === TypeScript.SymbolFlags.AliasExcludes
? typechecker.getAliasedSymbol(symbol)
: symbol;

if (resolvedSymbol.name === 'Array' && isTypeReference(type) && type.typeArguments) {
const arg = type.typeArguments[0]!;

if (!arg) {
return;
}

return getReactSlotType(arg, ctx);
}

if (REACT_SLOT_TYPES_SINGLE.includes(resolvedSymbol.name)) {
return 'single';
}

return 'multiple';
}
1 change: 1 addition & 0 deletions packages/analyzer/src/react-utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from './find-react-component-type';
export * from './is-react-event-handler-type';
export * from './is-react-slot-type';
export * from './get-react-slot-type';
export * from './get-event-type';
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as Fs from 'fs';
import { getDefaultCode, getCommentValue, analyzeSlots } from './slot-analzyer';
import { getDefaultCode, getCommentValue, analyzeSlots } from './slot-analyzer';
import * as TypescriptUtils from '../typescript-utils';
import * as TestUtils from '../test-utils';
import { Project } from 'ts-simple-ast';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,8 @@ export function analyzeSlots(
id,
propertyName,
required,
quantity:
ReactUtils.getReactSlotType(memberType, { program: ctx.program }) || 'multiple',
type: propertyName === 'children' && !isExplicitSlot ? 'children' : 'property'
};
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,67 @@ test('picks up number props', () => {
);
});

test('picks up jsx props', () => {
const project = new tsa.Project({ useVirtualFileSystem: true, addFilesFromTsConfig: false });

project.createSourceFile(
'/a.ts',
`import * as React from 'react'; export const A: React.SFC = () => null;`
);

project.createSourceFile(
'/c.ts',
`import * as React from 'react'; export const C: React.SFC = () => null;`
);

const result = analyzeSlotDefault(
`
import * as React from 'react';
import { A } from '/a';
import { C } from '/c';
export default () => <A jsx={<C />} />
`,
{ project, id: 'jsx-props', path: '/b.ts' }
);

expect(result).toEqual(
expect.objectContaining({
props: [
{
propName: 'jsx',
value: expect.objectContaining({
patternContextId: 'c.ts:C'
})
}
]
})
);
});

test('supports jsx fragment', () => {
const project = new tsa.Project({ useVirtualFileSystem: true, addFilesFromTsConfig: false });

project.createSourceFile(
'/a.ts',
`import * as React from 'react'; export const A: React.SFC = () => null;`
);

const result = analyzeSlotDefault(
`
import * as React from 'react';
import { A } from '/a';
export default () => <><A /></>
`,
{ project, id: 'jsx-fragment', path: '/b.ts' }
);

expect(result).toEqual(
expect.objectContaining({
jsxFragment: true
})
);
});

test('supports multiple levels of JSX elements', () => {
const project = new tsa.Project({ useVirtualFileSystem: true, addFilesFromTsConfig: false });

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,18 @@ function candidateFromJSXElement(
element: tsa.JsxChild,
{ project, id }: { project: tsa.Project; id: string }
): ElementCandidate | undefined {
if (tsa.TypeGuards.isJsxFragment(element)) {
return {
parent: id,
id: [id, 'default'].join(':'),
libraryId: '',
patternContextId: '',
props: [],
jsxFragment: true,
children: getChildrenCandidates(element, { project, id })
};
}

const nameElement = getNameElement(element);

if (!nameElement) {
Expand Down Expand Up @@ -113,12 +125,13 @@ function candidateFromJSXElement(
id: [id, 'default'].join(':'),
libraryId: pkg.name,
patternContextId: [patternContextBase, exportSpecifier].join(':'),
jsxFragment: false,
props: nameElement
.getAttributes()
.filter(tsa.TypeGuards.isJsxAttribute)
.map(attribute => ({
propName: attribute.getName(),
value: getInitValue(attribute.getInitializer())
value: getInitValue(attribute.getInitializer(), { project, id })
})),
children
};
Expand All @@ -128,7 +141,7 @@ function getChildrenCandidates(
element: tsa.JsxChild,
{ project, id }: { project: tsa.Project; id: string }
): ElementCandidate[] {
if (tsa.TypeGuards.isJsxElement(element)) {
if (tsa.TypeGuards.isJsxElement(element) || tsa.TypeGuards.isJsxFragment(element)) {
return element
.getJsxChildren()
.map(child => candidateFromJSXElement(child, { project, id }))
Expand All @@ -139,7 +152,8 @@ function getChildrenCandidates(
}

function getInitValue(
init?: tsa.StringLiteral | tsa.JsxExpression | tsa.Expression | undefined
init: tsa.StringLiteral | tsa.JsxExpression | tsa.Expression | undefined,
{ project, id }: { project: tsa.Project; id: string }
): unknown {
if (typeof init === 'undefined') {
return;
Expand All @@ -149,17 +163,41 @@ function getInitValue(
return init.getLiteralValue();
}

if (isElement(init)) {
const element = getElement(init);

if (element) {
return candidateFromJSXElement(element, { id, project });
}
}

if (tsa.TypeGuards.isJsxExpression(init)) {
const exp = init.getExpression();
return getInitValue(exp);
return getInitValue(exp, { id, project });
}

// TODO: Propagate error/warning to user for non-literal attributes
return;
}

export function getElement(jsx: tsa.Node): tsa.JsxElement | tsa.JsxSelfClosingElement | undefined {
if (tsa.TypeGuards.isJsxElement(jsx) || tsa.TypeGuards.isJsxSelfClosingElement(jsx)) {
export function isElement(element: tsa.Node): boolean {
if (
tsa.TypeGuards.isJsxOpeningElement(element) ||
tsa.TypeGuards.isJsxSelfClosingElement(element)
) {
return true;
}
return false;
}

export function getElement(
jsx: tsa.Node
): tsa.JsxElement | tsa.JsxSelfClosingElement | tsa.JsxFragment | undefined {
if (
tsa.TypeGuards.isJsxElement(jsx) ||
tsa.TypeGuards.isJsxSelfClosingElement(jsx) ||
tsa.TypeGuards.isJsxFragment(jsx)
) {
return jsx;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import * as Fs from 'fs';
import * as Path from 'path';
import * as PropertyAnalyzer from './property-analyzer';
import * as ReactUtils from '../react-utils';
import * as SlotAnalyzer from './slot-analzyer';
import * as SlotAnalyzer from './slot-analyzer';
import * as Types from '@meetalva/types';
import * as TypeScriptUtils from '../typescript-utils';
import * as ts from 'typescript';
Expand Down Expand Up @@ -295,6 +295,7 @@ export function analyzePatternExport(
id: IdHasher.getGlobalSlotId(id, 'children'),
label: 'children',
propertyName: 'children',
quantity: 'multiple',
required: false,
type: 'children'
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import * as pkgDir from 'pkg-dir';
import * as M from '@meetalva/message';
import { MessageType } from '@meetalva/message';
import * as T from '@meetalva/types';
import * as Analzyer from '@meetalva/analyzer';
import * as Analyzer from '@meetalva/analyzer';
import { MatcherCreator } from './context';

export const connectNpmPatternLibrary: MatcherCreator<M.ConnectNpmPatternLibraryRequest> = ({
Expand Down Expand Up @@ -67,7 +67,7 @@ export const connectNpmPatternLibrary: MatcherCreator<M.ConnectNpmPatternLibrary
return abort();
}

const result = await Analzyer.getPackage(m.payload.npmId, {
const result = await Analyzer.getPackage(m.payload.npmId, {
cwd: await host.resolveFrom(T.HostBase.AppData, 'packages'),
dirname,
appPath: await host.resolveFrom(T.HostBase.AppPath, '.')
Expand All @@ -90,7 +90,7 @@ export const connectNpmPatternLibrary: MatcherCreator<M.ConnectNpmPatternLibrary
return abort();
}

const analysisResult = await Analzyer.analyze(result.path);
const analysisResult = await Analyzer.analyze(result.path);

if (analysisResult.type === T.LibraryAnalysisResultType.Error) {
host.log(analysisResult.error.message);
Expand Down
9 changes: 7 additions & 2 deletions packages/core/src/preview/preview-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -216,11 +216,11 @@ export class PreviewStore<V> {
public getSlots<T>(
element: Model.Element,
render: (element: Model.Element) => T
): { [propName: string]: T[] | null } {
): { [propName: string]: T | T[] | null } {
return element
.getContents()
.filter(content => content.getSlotType() !== Types.SlotType.Children)
.reduce<{ [key: string]: T[] | null }>((renderProperties, content) => {
.reduce<{ [key: string]: T | T[] | null }>((renderProperties, content) => {
const slot = content.getSlot();

if (!slot) {
Expand All @@ -231,6 +231,11 @@ export class PreviewStore<V> {
const children =
elements.length === 0 && !slot.getRequired() ? null : elements.map(render);

if (slot.getQuantity() === Types.SlotQuantity.Single) {
renderProperties[slot.getPropertyName()] = children && children[0];
return renderProperties;
}

renderProperties[slot.getPropertyName()] = children;
return renderProperties;
}, {});
Expand Down
30 changes: 20 additions & 10 deletions packages/core/src/store/view-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import * as Types from '@meetalva/types';
import { Sender } from '../sender';

import * as uuid from 'uuid';
import { ElementCandidate } from '@meetalva/types';

export interface ViewStoreInit {
app: Model.AlvaApp<Message>;
Expand Down Expand Up @@ -200,6 +201,12 @@ export class ViewStore {
});

const fromCandidate = (candidate: Types.ElementCandidate, content: Model.ElementContent) => {
if (candidate.jsxFragment) {
candidate.children.forEach(childCandidate => {
fromCandidate(childCandidate, content);
});
}

const library = project.getPatternLibraryByName(candidate.libraryId);

if (!library) {
Expand All @@ -217,23 +224,26 @@ export class ViewStore {
pattern
});

content.insert({
at: undefined,
element: child
});

this.addElement(child);

candidate.props.forEach(propCandidate => {
const prop = child.getPropertyByContextId(propCandidate.propName);
const slotContent = child.getContentBySlotContextId(propCandidate.propName);

if (!prop) {
return;
if (prop) {
prop.setValue(propCandidate.value);
}

prop.setValue(propCandidate.value);
});

content.insert({
at: 0,
element: child
if (slotContent) {
fromCandidate(propCandidate.value, slotContent);
}
});

this.addElement(child);

candidate.children.forEach(childCandidate => {
const childContent = child.getContentBySlotType(Types.SlotType.Children);

Expand Down
280 changes: 154 additions & 126 deletions packages/essentials/src/analysis.ts

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion packages/essentials/src/conditional.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export interface ConditionalProps {
* alignItems={E.AlignItems.center}
* height="100px"
* backgroundColor="#eee">
* <Text text="Shown if condition is true"/>
* <E.Text text="Shown if condition is true"/>
* </E.Box>
* );
* ~~~
Expand Down
Loading

1 comment on commit 6ffb90d

@marionebl
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.