diff --git a/core/flow/src/flow.ts b/core/flow/src/flow.ts index ba1b13598..d8f38dc74 100644 --- a/core/flow/src/flow.ts +++ b/core/flow/src/flow.ts @@ -44,7 +44,7 @@ export class FlowInstance { /** A hook to intercept and block a transition */ skipTransition: new SyncBailHook< - [NamedState | undefined, string], + [NamedState | undefined], boolean | undefined >(), @@ -124,6 +124,19 @@ export class FlowInstance { throw new Error("Cannot transition when there's no current state"); } + if (options?.force) { + this.log?.debug(`Forced transition. Skipping validation checks`); + } else { + const skipTransition = this.hooks.skipTransition.call(this.currentState); + + if (skipTransition) { + this.log?.debug( + `Skipping transition from ${this.currentState} b/c hook told us to` + ); + return; + } + } + const state = this.hooks.beforeTransition.call( this.currentState.value, transitionValue @@ -170,23 +183,6 @@ export class FlowInstance { const prevState = this.currentState; - if (options?.force) { - this.log?.debug(`Forced transition. Skipping validation checks`); - } else { - const skipTransition = this.hooks.skipTransition.call( - prevState, - stateName - ); - - if (skipTransition) { - this.log?.debug( - `Skipping transition to ${stateName} b/c hook told us to` - ); - - return; - } - } - nextState = this.hooks.resolveTransitionNode.call(nextState); const newCurrentState = { diff --git a/core/player/src/__tests__/view.test.ts b/core/player/src/__tests__/view.test.ts index c7a32015f..0b829188d 100644 --- a/core/player/src/__tests__/view.test.ts +++ b/core/player/src/__tests__/view.test.ts @@ -308,6 +308,56 @@ describe('state node expression tests', () => { }); }); + test('evaluates onEnd before transition', () => { + player.start({ + ...minimal, + data: { + ...minimal.data, + viewRef: 'initial-view', + }, + navigation: { + BEGIN: 'FLOW_1', + FLOW_1: { + startState: 'ACTION_1', + ACTION_1: { + state_type: 'ACTION', + exp: "{{viewRef}} = 'view-exp'", + onStart: "{{viewRef}} = 'view-onStart'", + onEnd: "{{viewRef}} = 'VIEW_1'", + transitions: { + Next: '{{viewRef}}', + }, + }, + VIEW_1: { + state_type: 'VIEW', + ref: 'view-1', + transitions: {}, + }, + }, + }, + }); + jest.runOnlyPendingTimers(); + flowController?.transition('Next'); + + /** + * Expected eval order: + * 1. onStart + * 2. exp + * 3. onEnd + */ + expect(getView()).toStrictEqual({ + id: 'view-1', + type: 'view', + label: { + asset: { + id: 'action-label', + type: 'text', + value: 'Clicked 0 times', + }, + }, + }); + }); + test('triggers onStart before resolving view IDs', () => { player.start({ id: 'resolve-view-flow', diff --git a/core/player/src/player.ts b/core/player/src/player.ts index 0e74fdc50..356712e72 100644 --- a/core/player/src/player.ts +++ b/core/player/src/player.ts @@ -130,6 +130,11 @@ export class Player { }); } + /** Returns currently registered plugins */ + public getPlugins(): PlayerPlugin[] { + return this.config.plugins ?? []; + } + /** Find instance of [Plugin] that has been registered to Player */ public findPlugin( symbol: symbol @@ -278,6 +283,17 @@ export class Player { flowController.hooks.flow.tap('player', (flow: FlowInstance) => { flow.hooks.beforeTransition.tap('player', (state, transitionVal) => { + if ( + state.onEnd && + (state.transitions[transitionVal] || state.transitions['*']) + ) { + if (typeof state.onEnd === 'object' && 'exp' in state.onEnd) { + expressionEvaluator?.evaluate(state.onEnd.exp); + } else { + expressionEvaluator?.evaluate(state.onEnd); + } + } + if (!('transitions' in state) || !state.transitions[transitionVal]) { return state; } diff --git a/core/player/src/plugins/flow-exp-plugin.ts b/core/player/src/plugins/flow-exp-plugin.ts index 1fa6ec7ee..5e39e710c 100644 --- a/core/player/src/plugins/flow-exp-plugin.ts +++ b/core/player/src/plugins/flow-exp-plugin.ts @@ -47,17 +47,6 @@ export class FlowExpPlugin implements PlayerPlugin { // Eval state nodes flow.hooks.resolveTransitionNode.intercept({ call: (nextState: NavigationFlowState) => { - /** Get the current state of Player */ - const currentState = () => player.getState() as InProgressState; - - /** Get the current flow state */ - const currentFlowState = - currentState().controllers.flow.current?.currentState; - - if (currentFlowState?.value.onEnd) { - handleEval(currentFlowState.value.onEnd); - } - if (nextState?.onStart) { handleEval(nextState.onStart); } diff --git a/core/player/src/validation/controller.ts b/core/player/src/validation/controller.ts index d5b96b898..e5521dbe8 100644 --- a/core/player/src/validation/controller.ts +++ b/core/player/src/validation/controller.ts @@ -553,7 +553,7 @@ export class ValidationController implements BindingTracker { ); } - private getValidator(type: string) { + public getValidator(type: string) { if (this.validatorRegistry) { return this.validatorRegistry.get(type); } diff --git a/core/schema/src/schema.ts b/core/schema/src/schema.ts index 054cfa200..b34355eb0 100644 --- a/core/schema/src/schema.ts +++ b/core/schema/src/schema.ts @@ -87,7 +87,7 @@ export class SchemaController implements ValidationProvider { new Map(); private types: Map> = new Map(); - private schema: Map = new Map(); + public readonly schema: Map = new Map(); private bindingSchemaNormalizedCache: Map = new Map(); diff --git a/core/view/src/parser/index.ts b/core/view/src/parser/index.ts index c1fdb6945..6ab1e09db 100644 --- a/core/view/src/parser/index.ts +++ b/core/view/src/parser/index.ts @@ -3,7 +3,7 @@ import { SyncWaterfallHook } from 'tapable-ts'; import type { Template, AssetSwitch } from '@player-ui/types'; import type { Node, AnyAssetType } from './types'; import { NodeType } from './types'; -import { hasSwitch } from './utils'; +import { hasSwitch, hasApplicability } from './utils'; export * from './types'; @@ -56,6 +56,36 @@ export class Parser { return viewNode as Node.View; } + private parseApplicability( + obj: object, + type: Node.ChildrenTypes, + options: ParseObjectOptions + ): Node.Node | null { + const parsedApplicability = this.parseObject( + omit(obj, 'applicability'), + type, + options + ); + if (parsedApplicability !== null) { + const applicabilityNode = this.createASTNode( + { + type: NodeType.Applicability, + expression: (obj as any).applicability, + value: parsedApplicability, + }, + obj + ); + + if (applicabilityNode?.type === NodeType.Applicability) { + applicabilityNode.value.parent = applicabilityNode; + } + + return applicabilityNode; + } + + return null; + } + private parseSwitch( obj: AssetSwitch, options: ParseObjectOptions @@ -116,29 +146,8 @@ export class Parser { type: Node.ChildrenTypes = NodeType.Value, options: ParseObjectOptions = { templateDepth: 0 } ): Node.Node | null { - if (Object.prototype.hasOwnProperty.call(obj, 'applicability')) { - const parsedApplicability = this.parseObject( - omit(obj, 'applicability'), - type, - options - ); - - if (parsedApplicability !== null) { - const applicabilityNode = this.createASTNode( - { - type: NodeType.Applicability, - expression: (obj as any).applicability, - value: parsedApplicability, - }, - obj - ); - - if (applicabilityNode?.type === NodeType.Applicability) { - applicabilityNode.value.parent = applicabilityNode; - } - - return applicabilityNode; - } + if (hasApplicability(obj)) { + return this.parseApplicability(obj, type, options); } if (hasSwitch(obj)) { @@ -249,7 +258,21 @@ export class Parser { } } } else if (localValue && typeof localValue === 'object') { - parseLocalObject(localValue, [...path, localKey]); + if (hasApplicability(localValue)) { + const applicabilityNode = this.parseApplicability( + localValue, + type, + options + ); + if (applicabilityNode) { + children.push({ + path: [...path, localKey], + value: applicabilityNode, + }); + } + } else { + parseLocalObject(localValue, [...path, localKey]); + } } else { value = setIn(value, [...path, localKey], localValue); } diff --git a/core/view/src/parser/utils.ts b/core/view/src/parser/utils.ts index 027edcd74..08511e311 100644 --- a/core/view/src/parser/utils.ts +++ b/core/view/src/parser/utils.ts @@ -7,3 +7,8 @@ export function hasSwitch(obj: object): obj is AssetSwitch { Object.prototype.hasOwnProperty.call(obj, 'staticSwitch') ); } + +/** Check to see if the object contains applicability */ +export function hasApplicability(obj: object): boolean { + return Object.prototype.hasOwnProperty.call(obj, 'applicability'); +} diff --git a/core/view/src/plugins/__tests__/applicability.test.ts b/core/view/src/plugins/__tests__/applicability.test.ts index f10b5115e..c5079f572 100644 --- a/core/view/src/plugins/__tests__/applicability.test.ts +++ b/core/view/src/plugins/__tests__/applicability.test.ts @@ -61,6 +61,33 @@ describe('applicability', () => { }); }); + it('removes asset wrappers', () => { + const root = parser.parseObject({ + asset: { + title: { + applicability: '{{foo}}', + asset: { + value: 'foo', + }, + }, + value: 'Hello World', + }, + }); + model.set([['foo', true]]); + const resolver = new Resolver(root!, resolverOptions); + + new ApplicabilityPlugin().applyResolver(resolver); + new StringResolverPlugin().applyResolver(resolver); + + expect(resolver.update()).toStrictEqual({ + asset: { title: { asset: { value: 'foo' } }, value: 'Hello World' }, + }); + model.set([['foo', false]]); + expect(resolver.update()).toStrictEqual({ + asset: { value: 'Hello World' }, + }); + }); + it('handles empty models', () => { const root = parser.parseObject({ asset: { diff --git a/core/view/src/view.ts b/core/view/src/view.ts index 594176d9d..92c2b3fbf 100644 --- a/core/view/src/view.ts +++ b/core/view/src/view.ts @@ -91,7 +91,7 @@ export class ViewInstance implements ValidationProvider { private resolver?: Resolver; public readonly initialView: ViewType; - private readonly resolverOptions: Resolve.ResolverOptions; + public readonly resolverOptions: Resolve.ResolverOptions; private rootNode?: Node.Node; private validationProvider?: CrossfieldProvider; diff --git a/language/dsl/BUILD b/language/dsl/BUILD index 5d23a0c3e..1865a8ed3 100644 --- a/language/dsl/BUILD +++ b/language/dsl/BUILD @@ -15,6 +15,7 @@ javascript_pipeline( "@npm//react-flatten-children", "@npm//react-json-reconciler", "@npm//react-merge-refs", + "@npm//source-map-js", "@npm//signale", "@npm//ts-node", "@npm//typescript", diff --git a/language/dsl/src/__tests__/asset-api.test.tsx b/language/dsl/src/__tests__/asset-api.test.tsx index efd8df44c..8a871580c 100644 --- a/language/dsl/src/__tests__/asset-api.test.tsx +++ b/language/dsl/src/__tests__/asset-api.test.tsx @@ -19,7 +19,7 @@ describe('components', () => { ); - expect(await render(element)).toStrictEqual({ + expect((await render(element)).jsonValue).toStrictEqual({ id: 'root', type: 'collection', label: { @@ -50,7 +50,7 @@ describe('components', () => { ); - expect(await render(element)).toStrictEqual({ + expect((await render(element)).jsonValue).toStrictEqual({ id: 'root', type: 'collection', label: { @@ -87,7 +87,7 @@ describe('components', () => { ); - expect(await render(element)).toStrictEqual({ + expect((await render(element)).jsonValue).toStrictEqual({ id: 'root', type: 'collection', label: { asset: { id: 'label', type: 'text', value: 'Label' } }, @@ -142,7 +142,7 @@ describe('components', () => { ); - expect(element).toMatchInlineSnapshot(` + expect(element.jsonValue).toMatchInlineSnapshot(` Object { "id": "root", "modifiers": Array [ @@ -163,7 +163,7 @@ describe('components', () => { it('converts just a binding node into a ref', async () => { const element = await render({b`foo.bar`.toString()}); - expect(element).toStrictEqual({ + expect(element.jsonValue).toStrictEqual({ id: 'root', type: 'text', value: '{{foo.bar}}', @@ -175,7 +175,7 @@ describe('components', () => { Label {b`foo.bar`.toString()} End ); - expect(element).toStrictEqual({ + expect(element.jsonValue).toStrictEqual({ id: 'root', type: 'text', value: 'Label {{foo.bar}} End', @@ -189,7 +189,7 @@ describe('components', () => { ); - expect(element).toStrictEqual({ + expect(element.jsonValue).toStrictEqual({ id: 'root', type: 'input', binding: 'foo.bar.baz', @@ -212,7 +212,7 @@ describe('components', () => { ); - expect(element).toStrictEqual({ + expect(element.jsonValue).toStrictEqual({ id: 'custom-id', type: 'input', applicability: '{{foo.bar.baz}}', @@ -233,7 +233,7 @@ describe('components', () => { ); - expect(element).toStrictEqual({ + expect(element.jsonValue).toStrictEqual({ id: 'custom-id', type: 'input', applicability: false, @@ -255,7 +255,7 @@ describe('components', () => { ); - expect(await render(element)).toStrictEqual({ + expect((await render(element)).jsonValue).toStrictEqual({ id: 'first-thing', type: 'collection', label: { @@ -281,7 +281,7 @@ describe('components', () => { /> ); - expect(await render(element)).toStrictEqual({ + expect((await render(element)).jsonValue).toStrictEqual({ id: 'root', metaData: { optionalUnion: { @@ -305,7 +305,7 @@ describe('allows other props to be added to a slot', () => { ); - expect(element).toStrictEqual({ + expect(element.jsonValue).toStrictEqual({ id: 'custom-id', type: 'input', label: { @@ -332,7 +332,7 @@ describe('allows other props to be added to a slot', () => { ); - expect(element).toStrictEqual({ + expect(element.jsonValue).toStrictEqual({ id: 'custom-id', type: 'input', label: { diff --git a/language/dsl/src/__tests__/edge-cases.test.tsx b/language/dsl/src/__tests__/edge-cases.test.tsx index 3876a02b9..5f35e1540 100644 --- a/language/dsl/src/__tests__/edge-cases.test.tsx +++ b/language/dsl/src/__tests__/edge-cases.test.tsx @@ -48,7 +48,7 @@ test('works with a Component that returns a Fragment of items', async () => { ); - expect(contentWithFragment).toStrictEqual(expected); + expect(contentWithFragment.jsonValue).toStrictEqual(expected); const contentWithoutFragment = await render( @@ -60,5 +60,5 @@ test('works with a Component that returns a Fragment of items', async () => { ); - expect(contentWithoutFragment).toStrictEqual(expected); + expect(contentWithoutFragment.jsonValue).toStrictEqual(expected); }); diff --git a/language/dsl/src/__tests__/jsx.test.tsx b/language/dsl/src/__tests__/jsx.test.tsx index fe2a59881..446b2df96 100644 --- a/language/dsl/src/__tests__/jsx.test.tsx +++ b/language/dsl/src/__tests__/jsx.test.tsx @@ -44,7 +44,9 @@ it('works with JSX', async () => { ); - expect(await render(element)).toStrictEqual(expectedBasicCollection); + expect((await render(element)).jsonValue).toStrictEqual( + expectedBasicCollection + ); }); it('works for any json props', async () => { @@ -54,7 +56,7 @@ it('works for any json props', async () => { other: '', }; expect( - await render({toJsonProperties(testObj)}) + (await render({toJsonProperties(testObj)})).jsonValue ).toStrictEqual(testObj); }); @@ -64,7 +66,7 @@ it('works for BindingTemplateInstances and ExpressionTemplateInstances', async ( page_experience: e`foo.bar.GetDataResult`, }; expect( - await render({toJsonProperties(testObj)}) + (await render({toJsonProperties(testObj)})).jsonValue ).toStrictEqual(expectedTemplateInstanceObjects); }); @@ -79,7 +81,7 @@ it('handles array props', async () => { const element = ; - expect(await render(element)).toStrictEqual(expected); + expect((await render(element)).jsonValue).toStrictEqual(expected); }); test('flattens fragments', async () => { @@ -99,5 +101,7 @@ test('flattens fragments', async () => { ); - expect(await render(element)).toStrictEqual(expectedBasicCollection); + expect((await render(element)).jsonValue).toStrictEqual( + expectedBasicCollection + ); }); diff --git a/language/dsl/src/__tests__/schema.test.tsx b/language/dsl/src/__tests__/schema.test.tsx index 38ba2ac90..c93ac2dd2 100644 --- a/language/dsl/src/__tests__/schema.test.tsx +++ b/language/dsl/src/__tests__/schema.test.tsx @@ -245,7 +245,7 @@ describe('Schema Bindings Generate Properly', () => { ); - expect(content).toMatchInlineSnapshot(` + expect(content.jsonValue).toMatchInlineSnapshot(` Object { "test": "{{main.sub.a}}", } diff --git a/language/dsl/src/__tests__/switch.test.tsx b/language/dsl/src/__tests__/switch.test.tsx index f94960616..4e3ab85e3 100644 --- a/language/dsl/src/__tests__/switch.test.tsx +++ b/language/dsl/src/__tests__/switch.test.tsx @@ -21,7 +21,7 @@ describe('staticSwitch', () => { ); - expect(await render(element)).toStrictEqual({ + expect((await render(element)).jsonValue).toStrictEqual({ id: 'root', type: 'collection', label: { @@ -71,7 +71,7 @@ describe('staticSwitch', () => { ); - expect(await render(element)).toMatchInlineSnapshot(` + expect((await render(element)).jsonValue).toMatchInlineSnapshot(` Object { "id": "root", "label": Object { @@ -126,7 +126,7 @@ describe('staticSwitch', () => { ); - expect(await render(element)).toStrictEqual({ + expect((await render(element)).jsonValue).toStrictEqual({ id: 'root', type: 'collection', label: { @@ -168,7 +168,7 @@ describe('staticSwitch', () => { ); - expect(await render(element)).toStrictEqual({ + expect((await render(element)).jsonValue).toStrictEqual({ id: 'root', type: 'collection', label: { @@ -214,7 +214,7 @@ describe('generates ids', () => { ); - expect(await render(content)).toMatchInlineSnapshot(` + expect((await render(content)).jsonValue).toMatchInlineSnapshot(` Object { "id": "root", "type": "collection", diff --git a/language/dsl/src/__tests__/template.test.tsx b/language/dsl/src/__tests__/template.test.tsx index a0a27a7d5..2fd642244 100644 --- a/language/dsl/src/__tests__/template.test.tsx +++ b/language/dsl/src/__tests__/template.test.tsx @@ -18,7 +18,7 @@ test('finds output property based on array context', async () => { ); - expect(await render(element)).toStrictEqual({ + expect((await render(element)).jsonValue).toStrictEqual({ foo: ['Foo'], template: [ { @@ -42,7 +42,7 @@ test('works if already in a template array', async () => { ); - expect(await render(element)).toStrictEqual({ + expect((await render(element)).jsonValue).toStrictEqual({ template: [{ output: 'output', data: 'foo.output', value: 'bar' }], }); }); @@ -82,7 +82,7 @@ test('template will delete empty arrays related to the template only', async () ); - expect(await render(element)).toStrictEqual({ + expect((await render(element)).jsonValue).toStrictEqual({ id: 'root', type: 'collection', template: [ @@ -174,7 +174,7 @@ describe('template auto id', () => { ); - const actual = await render(element); + const actual = (await render(element)).jsonValue; expect(actual).toStrictEqual({ id: 'root', @@ -222,7 +222,7 @@ describe('template auto id', () => { ); - const actual = await render(element); + const actual = (await render(element)).jsonValue; expect(actual).toStrictEqual({ id: 'root', diff --git a/language/dsl/src/compiler/compiler.ts b/language/dsl/src/compiler/compiler.ts index 808e15a2c..180070861 100644 --- a/language/dsl/src/compiler/compiler.ts +++ b/language/dsl/src/compiler/compiler.ts @@ -1,5 +1,6 @@ import React from 'react'; import type { JsonType } from 'react-json-reconciler'; +import { SourceMapGenerator, SourceMapConsumer } from 'source-map-js'; import { render } from 'react-json-reconciler'; import type { Flow, View, Navigation as PlayerNav } from '@player-ui/types'; import { SyncHook } from 'tapable-ts'; @@ -34,6 +35,62 @@ const parseNavigationExpressions = (nav: Navigation): PlayerNav => { return replaceExpWithStr(nav); }; +type SourceMapList = Array<{ + /** The mappings of the original */ + sourceMap: string; + /** + * The id of the view we're indexing off of + * This should be a unique global identifier within the generated code + * e.g. `"id": "view_0",` + */ + offsetIndexSearch: string; + /** The generated source that produced the map */ + source: string; +}>; + +/** Given a list of source maps for all generated views, merge them into 1 */ +const mergeSourceMaps = ( + sourceMaps: SourceMapList, + generated: string +): string => { + const generator = new SourceMapGenerator(); + sourceMaps.forEach(({ sourceMap, offsetIndexSearch, source }) => { + const generatedLineOffset = generated + .split('\n') + .findIndex((line) => line.includes(offsetIndexSearch)); + + const sourceLineOffset = source + .split('\n') + .findIndex((line) => line.includes(offsetIndexSearch)); + + const lineOffset = generatedLineOffset - sourceLineOffset; + + const generatedLine = generated.split('\n')[generatedLineOffset]; + const sourceLine = source.split('\n')[sourceLineOffset]; + + const generatedColumn = generatedLine.indexOf(offsetIndexSearch); + const sourceColumn = sourceLine.indexOf(offsetIndexSearch); + const columnOffset = generatedColumn - sourceColumn; + + const consumer = new SourceMapConsumer(JSON.parse(sourceMap)); + consumer.eachMapping((mapping) => { + generator.addMapping({ + generated: { + line: mapping.generatedLine + lineOffset, + column: mapping.generatedColumn + columnOffset, + }, + original: { + line: mapping.originalLine, + column: mapping.originalColumn, + }, + source: mapping.source, + }); + }); + }); + + return generator.toString(); +}; + /** A compiler for transforming DSL content into JSON */ export class DSLCompiler { public hooks = { @@ -47,26 +104,64 @@ export class DSLCompiler { /** the fingerprinted content type of the source */ contentType: SerializeType; + + /** The sourcemap of the content */ + sourceMap?: string; }> { if (typeof value !== 'object' || value === null) { throw new Error('Unable to serialize non-object'); } if (React.isValidElement(value)) { + const { jsonValue, sourceMap } = await render(value, { + collectSourceMap: true, + }); + return { - value: await render(value), + value: jsonValue, + sourceMap, contentType: 'view', }; } if ('navigation' in value) { + // Source maps from all the nested views + // Merge these together before returning + const allSourceMaps: SourceMapList = []; + // Assume this is a flow const copiedValue: Flow = { ...(value as any), }; copiedValue.views = (await Promise.all( - copiedValue?.views?.map((node) => render(node as any)) ?? [] + copiedValue?.views?.map(async (node: any) => { + const { jsonValue, sourceMap, stringValue } = await render(node, { + collectSourceMap: true, + }); + + if (sourceMap) { + // Find the line that is the id of the view + // Use that as the identifier for the sourcemap offset calc + const searchIdLine = stringValue + .split('\n') + .find((line) => + line.includes( + `"id": "${(jsonValue as Record).id}"` + ) + ); + + if (searchIdLine) { + allSourceMaps.push({ + sourceMap, + offsetIndexSearch: searchIdLine, + source: stringValue, + }); + } + } + + return jsonValue; + }) ?? [] )) as View[]; // Go through the flow and sub out any view refs that are react elements w/ the right id @@ -102,7 +197,14 @@ export class DSLCompiler { } if (value) { - return { value: copiedValue as JsonType, contentType: 'flow' }; + return { + value: copiedValue as JsonType, + contentType: 'flow', + sourceMap: mergeSourceMaps( + allSourceMaps, + JSON.stringify(copiedValue, null, 2) + ), + }; } } diff --git a/language/dsl/src/string-templates/__tests__/react.test.tsx b/language/dsl/src/string-templates/__tests__/react.test.tsx index 3e48cfd76..6d433d558 100644 --- a/language/dsl/src/string-templates/__tests__/react.test.tsx +++ b/language/dsl/src/string-templates/__tests__/react.test.tsx @@ -5,12 +5,14 @@ import { Switch } from '../../switch'; import { Collection } from '../../__tests__/helpers/asset-library'; test('can be used as a react child element', async () => { - const content = await render( - - {e`test()`} - {b`foo.bar`} - - ); + const content = ( + await render( + + {e`test()`} + {b`foo.bar`} + + ) + ).jsonValue; expect(content).toStrictEqual({ expression: '@[test()]@', @@ -19,11 +21,13 @@ test('can be used as a react child element', async () => { }); test('Works when used as a child asset', async () => { - const content = await render( - - {b`foo.bar`} - - ); + const content = ( + await render( + + {b`foo.bar`} + + ) + ).jsonValue; expect(content).toStrictEqual({ id: 'root', @@ -39,15 +43,17 @@ test('Works when used as a child asset', async () => { }); test('Works as a switch child', async () => { - const content = await render( - - - - Testing 123 {b`foo.bar`} - - - - ); + const content = ( + await render( + + + + Testing 123 {b`foo.bar`} + + + + ) + ).jsonValue; expect(content).toStrictEqual({ id: 'root', diff --git a/package.json b/package.json index c5522f5f9..154f64b16 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "@babel/eslint-parser": "^7.15.8", "@babel/plugin-transform-runtime": "^7.15.8", "@babel/preset-env": "^7.15.6", + "@babel/plugin-transform-react-jsx-source": "^7.17.12", "@babel/preset-react": "^7.14.5", "@babel/preset-typescript": "^7.15.0", "@babel/register": "^7.17.7", @@ -149,7 +150,7 @@ "react-error-boundary": "^3.1.3", "react-flatten-children": "^1.1.2", "react-icons": "^4.3.1", - "react-json-reconciler": "^1.2.0", + "react-json-reconciler": "^2.0.0", "react-merge-refs": "^1.1.0", "react-redux": "^7.2.6", "react-syntax-highlighter": "^15.4.5", @@ -172,6 +173,7 @@ "signale": "^1.4.0", "smooth-scroll-into-view-if-needed": "1.1.32", "sorted-array": "^2.0.4", + "source-map-js": "^1.0.2", "std-mocks": "^1.0.1", "storybook": "^6.4.15", "storybook-dark-mode": "^1.0.8", diff --git a/plugins/check-path/core/src/index.test.ts b/plugins/check-path/core/src/index.test.ts index cf6a69e6d..17f234b92 100644 --- a/plugins/check-path/core/src/index.test.ts +++ b/plugins/check-path/core/src/index.test.ts @@ -342,6 +342,15 @@ describe('works with applicability', () => { 'asset', ]); }); + + test('getAsset', async () => { + expect(checkPathPlugin.getAsset('asset-4')).toBeUndefined(); + + dataController.set([['foo.baz', true]]); + await waitFor(() => { + expect(checkPathPlugin.getAsset('asset-4')).toBeDefined(); + }); + }); }); describe('handles non-initialized player', () => { diff --git a/plugins/check-path/core/src/index.ts b/plugins/check-path/core/src/index.ts index 38dfb0468..26ebe65e0 100644 --- a/plugins/check-path/core/src/index.ts +++ b/plugins/check-path/core/src/index.ts @@ -95,10 +95,7 @@ export class CheckPathPlugin implements PlayerPlugin { this.viewInfo = viewInfo; resolver.hooks.afterResolve.tap(this.name, (value, node) => { - let sourceNode = resolver.getSourceNode(node); - if (sourceNode?.type === 'applicability') { - sourceNode = sourceNode.value; - } + const sourceNode = this.getSourceAssetNode(node); if (sourceNode) { viewInfo.resolvedMap.set(sourceNode, { @@ -225,6 +222,16 @@ export class CheckPathPlugin implements PlayerPlugin { return undefined; } + /** Given a node, return itself, or the nested asset if the node is an applicability node */ + private getSourceAssetNode(node: Node.Node) { + let sourceNode = this.viewInfo?.resolver.getSourceNode(node); + if (sourceNode?.type === 'applicability') { + sourceNode = sourceNode.value; + } + + return sourceNode; + } + /** * Given the starting node, check to verify that the supplied queries are relevant to the current asset's parents. * @@ -308,7 +315,7 @@ export class CheckPathPlugin implements PlayerPlugin { const assetNode = this.viewInfo?.assetIdMap.get(id); if (!assetNode) return; - const sourceNode = this.viewInfo?.resolver.getSourceNode(assetNode); + const sourceNode = this.getSourceAssetNode(assetNode); if (!sourceNode) return; return this.viewInfo?.resolvedMap.get(sourceNode)?.value; diff --git a/plugins/reference-assets/components/src/index.test.tsx b/plugins/reference-assets/components/src/index.test.tsx index debc797c7..36a903921 100644 --- a/plugins/reference-assets/components/src/index.test.tsx +++ b/plugins/reference-assets/components/src/index.test.tsx @@ -5,7 +5,7 @@ import { Text, Action, Info, Collection, Input } from '.'; describe('JSON serialization', () => { describe('text', () => { it('works for basic text', async () => { - expect(await render(Hello World)).toStrictEqual({ + expect((await render(Hello World)).jsonValue).toStrictEqual({ id: 'root', type: 'text', value: 'Hello World', @@ -13,7 +13,9 @@ describe('JSON serialization', () => { }); it('works for value prop', async () => { - expect(await render()).toStrictEqual({ + expect( + (await render()).jsonValue + ).toStrictEqual({ id: 'root', type: 'text', value: 'Hello World', @@ -24,11 +26,13 @@ describe('JSON serialization', () => { describe('collection', () => { it('adds a label', async () => { expect( - await render( - - Test - - ) + ( + await render( + + Test + + ) + ).jsonValue ).toStrictEqual({ id: 'test-id', type: 'collection', @@ -44,14 +48,16 @@ describe('JSON serialization', () => { it('adds values', async () => { expect( - await render( - - - First - Second - - - ) + ( + await render( + + + First + Second + + + ) + ).jsonValue ).toStrictEqual({ id: 'root', type: 'collection', @@ -78,22 +84,24 @@ describe('JSON serialization', () => { describe('info', () => { it('works for a large view', async () => { expect( - await render( - - Info Title - - - Input Label - - - - - - Continue - - - - ) + ( + await render( + + Info Title + + + Input Label + + + + + + Continue + + + + ) + ).jsonValue ).toStrictEqual({ id: 'info-view', type: 'info', diff --git a/tools/cli/BUILD b/tools/cli/BUILD index 4a8f371bf..4f8bd23e3 100644 --- a/tools/cli/BUILD +++ b/tools/cli/BUILD @@ -13,6 +13,7 @@ BUILD_DATA = [ "@npm//@babel/preset-env", "@npm//@babel/preset-react", "@npm//@babel/preset-typescript", + "@npm//@babel/plugin-transform-react-jsx-source", "@npm//@babel/register", "@npm//@types/babel__register", "@npm//@oclif/core", diff --git a/tools/cli/bin/run b/tools/cli/bin/run index b6996159a..58aa54cdb 100755 --- a/tools/cli/bin/run +++ b/tools/cli/bin/run @@ -1,6 +1,7 @@ #!/usr/bin/env node const oclif = require('@oclif/core') -process.env.NODE_ENV = 'production' +// Setting this to prod break source-map generation for react dsl stuff +process.env.NODE_ENV = 'dev' oclif.run().then(require('@oclif/core/flush')).catch(require('@oclif/core/handle')) \ No newline at end of file diff --git a/tools/cli/src/commands/dsl/compile.ts b/tools/cli/src/commands/dsl/compile.ts index 15e8654c8..5df909e65 100644 --- a/tools/cli/src/commands/dsl/compile.ts +++ b/tools/cli/src/commands/dsl/compile.ts @@ -86,7 +86,11 @@ export default class DSLCompile extends BaseCommand { return; } - const relativePath = path.relative(input, file); + let relativePath = path.relative(input, file); + if (!relativePath) { + relativePath = path.basename(file); + } + const outputFile = path.join( output, path.format({ @@ -102,13 +106,19 @@ export default class DSLCompile extends BaseCommand { normalizePath(outputFile) ); - const { value, contentType } = await compiler.serialize(defaultExport); + const { value, contentType, sourceMap } = await compiler.serialize( + defaultExport + ); const contentStr = JSON.stringify(value, null, 2); await mkdirp(path.dirname(outputFile)); await fs.writeFile(outputFile, contentStr); + if (sourceMap) { + await fs.writeFile(`${outputFile}.map`, sourceMap); + } + return { contentType, outputFile, diff --git a/tools/cli/src/utils/babel-register.ts b/tools/cli/src/utils/babel-register.ts index 3d709fb8e..97be7f5b6 100644 --- a/tools/cli/src/utils/babel-register.ts +++ b/tools/cli/src/utils/babel-register.ts @@ -10,5 +10,6 @@ export const registerForPaths = () => { '@babel/preset-typescript', '@babel/preset-react', ], + plugins: ['@babel/plugin-transform-react-jsx-source'], }); }; diff --git a/yarn.lock b/yarn.lock index 680664499..0caae06fa 100644 --- a/yarn.lock +++ b/yarn.lock @@ -369,6 +369,11 @@ resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.16.7.tgz#aa3a8ab4c3cceff8e65eb9e73d87dc4ff320b2f5" integrity sha512-Qg3Nk7ZxpgMrsox6HreY1ZNKdBq7K72tDSliA6dCl5f007jR4ne8iD5UzuNnCJH2xBf2BEEVGr+/OL6Gdp7RxA== +"@babel/helper-plugin-utils@^7.18.6": + version "7.18.9" + resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.18.9.tgz#4b8aea3b069d8cb8a72cdfe28ddf5ceca695ef2f" + integrity sha512-aBXPT3bmtLryXaoJLyYPXPlSD4p1ld9aYeR+sJNOZjJJGiOpb+fKfh3NkcCu7J54nUJwCERPBExCCpyCOHnu/w== + "@babel/helper-remap-async-to-generator@^7.16.7": version "7.16.7" resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.16.7.tgz#5ce2416990d55eb6e099128338848ae8ffa58a9a" @@ -991,6 +996,13 @@ dependencies: "@babel/plugin-transform-react-jsx" "^7.16.7" +"@babel/plugin-transform-react-jsx-source@^7.17.12": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.18.6.tgz#06e9ae8a14d2bc19ce6e3c447d842032a50598fc" + integrity sha512-utZmlASneDfdaMh0m/WausbjUjEdGrQJz0vFK93d7wD3xf5wBtX219+q6IlCNZeguIcxS2f/CvLZrlLSvSHQXw== + dependencies: + "@babel/helper-plugin-utils" "^7.18.6" + "@babel/plugin-transform-react-jsx@^7.12.12", "@babel/plugin-transform-react-jsx@^7.16.7": version "7.16.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.16.7.tgz#86a6a220552afd0e4e1f0388a68a372be7add0d4" @@ -12288,6 +12300,11 @@ json-schema-traverse@^0.4.1: resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== +json-source-map@^0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/json-source-map/-/json-source-map-0.6.1.tgz#e0b1f6f4ce13a9ad57e2ae165a24d06e62c79a0f" + integrity sha512-1QoztHPsMQqhDq0hlXY5ZqcEdUzxQEIxgFkKl4WUp2pgShObl+9ovi4kRh2TfvAfxAoHOJ9vIMEqk3k4iex7tg== + json-stable-stringify-without-jsonify@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" @@ -15884,14 +15901,16 @@ react-is@^16.13.1, react-is@^16.7.0: resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== -react-json-reconciler@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/react-json-reconciler/-/react-json-reconciler-1.2.0.tgz#ef7f91f38c56d46631f1f0c924fc5aba2c061d19" - integrity sha512-oi2WwtxROYF1mYxWwcD7EMX45YTtvqO3ptvyOrI8JZxinQ/eIE28o5Mjdjdq8Ckg+xAXsQ46Dd8ucJ1W1YbjBA== +react-json-reconciler@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/react-json-reconciler/-/react-json-reconciler-2.0.0.tgz#4097e5471773f834016bfe476462d33a6c7a4e37" + integrity sha512-3JYu/uQ3hwbFW18LePpEm0m5LaFpCxTt+3gf/N84wFUv7EBhbR2SBGhTmXiJ92u6tLIXy7H+tB8t7LqfdYhPNA== dependencies: "@types/react-reconciler" "^0.26.1" + json-source-map "^0.6.1" react-flatten-children "^1.1.2" react-reconciler "^0.26.2" + source-map-js "^1.0.2" react-merge-refs@^1.0.0, react-merge-refs@^1.1.0: version "1.1.0" @@ -17392,6 +17411,11 @@ source-map-js@^1.0.1: resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.1.tgz#a1741c131e3c77d048252adfa24e23b908670caf" integrity sha512-4+TN2b3tqOCd/kaGRJ/sTYA0tR0mdXx26ipdolxcwtJVqEnqNYvlCAt1q3ypy4QMlYus+Zh34RNtYLoq2oQ4IA== +source-map-js@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" + integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw== + source-map-resolve@^0.5.0: version "0.5.3" resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.5.3.tgz#190866bece7553e1f8f267a2ee82c606b5509a1a"