Skip to content

Commit

Permalink
Merge pull request #57 from bywhitebird/53-add-support-for-onmount-in…
Browse files Browse the repository at this point in the history
…struction

53 add support for onmount instruction
  • Loading branch information
arthur-fontaine authored Oct 27, 2023
2 parents 1aae0f5 + 01802bf commit 5f26ef7
Show file tree
Hide file tree
Showing 30 changed files with 257 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { EndInstructionToken, StartInstructionToken } from '..'
import { s } from '../../../lib/voltair'
import { ComputedInstructionSequence } from '../../computed-instruction'
import { ImportInstructionSequence } from '../../import-instruction'
import { LifecycleInstructionSequence } from '../../lifecycle-instruction'
import { PropInstructionSequence } from '../../prop-instruction'
import { StateInstructionSequence } from '../../state-instruction'
import { WatchInstructionSequence } from '../../watch-instruction'
Expand All @@ -14,6 +15,7 @@ export const InstructionsSequence = s(
StateInstructionSequence,
ComputedInstructionSequence,
WatchInstructionSequence,
LifecycleInstructionSequence,
]),
() => EndInstructionToken,
{ min: 0 },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { InstructionContext } from '..'
import { Token } from '../../../lib/voltair'
import { ComputedInstructionContext } from '../../computed-instruction'
import { ImportInstructionContext } from '../../import-instruction'
import { LifecycleInstructionContext } from '../../lifecycle-instruction'
import { PropInstructionContext } from '../../prop-instruction'
import { StateInstructionContext } from '../../state-instruction'
import { WatchInstructionContext } from '../../watch-instruction'
Expand All @@ -12,6 +13,7 @@ const instructionContexts = [
() => PropInstructionContext,
() => StateInstructionContext,
() => WatchInstructionContext,
() => LifecycleInstructionContext,
() => InstructionContext,
]

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Context } from '../../../lib/voltair'
import { ArrowFunctionBodyToken, ArrowToken, LeftParenthesisToken, RightParenthesisToken, WhitespaceToken } from '../../../shared'
import { EndInstructionToken } from '../../instruction'

export const LifecycleInstructionContext: Context<'LifecycleInstructionContext'> = new Context({
$name: 'LifecycleInstructionContext',
breakingPatterns: [/\s+/, /^\($/, /^\)$/, /^=>$/, /^,$/],
availableTokens: [
() => LeftParenthesisToken,
() => RightParenthesisToken,
() => ArrowToken,
() => ArrowFunctionBodyToken,
() => EndInstructionToken,
() => WhitespaceToken,
],
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const LifecycleInstructionFixtures = [
{},
]
5 changes: 5 additions & 0 deletions packages/kaz-ast/src/features/lifecycle-instruction/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export * from './contexts/LifecycleInstructionContext'

export * from './tokens/LifecycleInstructionToken'

export * from './sequences/LifecycleInstructionSequence'
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { LifecycleInstructionContext, LifecycleInstructionToken } from '..'
import { g, gp, s } from '../../../lib/voltair'
import { ArrowFunctionBodyToken, ArrowToken, LeftParenthesisToken, RightParenthesisToken } from '../../../shared'

export const LifecycleInstructionSequence = gp(
'LifecycleEventInstruction',
s(
g('event', () => LifecycleInstructionToken),
LeftParenthesisToken,
RightParenthesisToken,
ArrowToken,
() => g('callbackExpression', ArrowFunctionBodyToken.extends({
endContexts: [LifecycleInstructionContext],
})),
),
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { LifecycleInstructionContext } from '..'
import { Token } from '../../../lib/voltair'
import { InstructionContext } from '../../instruction'

export const LifecycleInstructionToken = new Token({
$name: 'LifecycleInstructionToken',
validator: /^on((?:Mount))$/,
startContexts: [() => LifecycleInstructionContext],
inContexts: [() => InstructionContext],
getValue(rawValue) {
return rawValue.slice(2).toLowerCase()
},
semantic: {
type: 'keyword',
},
})
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { TemplateExpressionContext } from '../../../features/template'
import { Token } from '../../../lib/voltair'

const getExpression = (rawValue: string) => {
const parsed = parse(`const _ = ${rawValue}`, { warnOnUnsupportedTypeScriptVersion: false })
const parsed = parse(`const _ = ${rawValue}`, { warnOnUnsupportedTypeScriptVersion: false, range: true })
const variable = parsed.body[0]

if (variable?.type !== 'VariableDeclaration' || variable.declarations[0] === undefined)
Expand Down
7 changes: 7 additions & 0 deletions packages/kaz-ast/src/types/KazAst.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,12 @@ export const kazImportInstructionSchema = zod.object({
).optional(),
})

export const kazLifecycleEventInstructionSchema = zod.object({
$type: zod.literal('LifecycleEventInstruction'),
event: valueSchema(zod.literal('mount')),
callbackExpression: valueSchema(zod.string()),
})

export const kazWatchedVariableSchema = zod.object({
name: valueSchema(zod.string()),
type: valueSchema(zod.string()).optional(),
Expand Down Expand Up @@ -196,6 +202,7 @@ export const kazAstSchema = zod.object({
kazComputedInstructionSchema,
kazPropInstructionSchema,
kazStateInstructionSchema,
kazLifecycleEventInstructionSchema,
]),
),
template: zod.array(kazTemplateSchema),
Expand Down
6 changes: 6 additions & 0 deletions packages/kaz-ast/src/voltair.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { ForLeftParenthesisToken, ForLogicalToken, ForParametersToken, ForRightP
import { AliasKeywordToken, FromKeywordToken, ImportInstructionToken, WildcardCharacterToken } from './features/import-instruction'
import { EndInstructionToken, StartInstructionToken } from './features/instruction'
import { KazSequence } from './features/kaz'
import { LifecycleInstructionToken } from './features/lifecycle-instruction'
import { PropInstructionToken } from './features/prop-instruction'
import { StateInstructionToken } from './features/state-instruction'
import { TagAttributeEqualToken, TagAttributeLeftCurlyBracketToken, TagAttributeNameToken, TagAttributeRightCurlyBracketToken, TagAttributeSeparatorToken, TagEventAttributeNameToken, TagLeftParenthesisToken, TagNameOrTextToken, TagRightParenthesisToken } from './features/tag'
Expand Down Expand Up @@ -47,6 +48,11 @@ export default {
WatchInstructionToken,
],

[
// Lifecycle instruction
LifecycleInstructionToken,
],

EndInstructionToken,
],

Expand Down
1 change: 1 addition & 0 deletions packages/kaz-ast/tests/fixtures/parse/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export * from './prop-instructions'
export * from './state-instructions'
export * from './computed-instructions'
export * from './watch-instructions'
export * from './lifecycle-instructions'
export * from './for-logical'
export * from './condition-logical'
export * from './tag'
48 changes: 48 additions & 0 deletions packages/kaz-ast/tests/fixtures/parse/lifecycle-instructions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import type { JsonValue } from '../../../src/lib/voltair/types/JsonValue'

export const lifecycleInstructionsFixtures: (
{
name: string
input: string
} & (
| { expectedTree: JsonValue }
| { expectError: boolean }
)
)[] = [
{
name: 'When I use a onMount instruction with a function, onMount is validated',
input: `
- onMount () => {
console.log('Hello')
console.log('Mounted')
}
`,
expectedTree: {
$type: 'Kaz',
instructions: [
{
$type: 'LifecycleEventInstruction',
event: 'mount',
callbackExpression: 'console.log(\'Hello\')\n console.log(\'Mounted\')',
},
],
},
},
{
name: 'When I use a onMount instruction without an arrow, onMount is not validated',
input: `
- onMount () {
console.log('Hello')
console.log('Mounted')
}
`,
expectError: true,
},
{
name: 'When I use only the onMount keyword, onMount is not validated',
input: `
- onMount
`,
expectError: true,
},
]
1 change: 1 addition & 0 deletions packages/kaz-ast/tests/fixtures/tokenize/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export * from './prop-instructions'
export * from './state-instructions'
export * from './computed-instructions'
export * from './watch-instructions'
export * from './lifecycle-instructions'
export * from './for-logical'
export * from './condition-logical'
export * from './tag'
33 changes: 33 additions & 0 deletions packages/kaz-ast/tests/fixtures/tokenize/lifecycle-instructions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { EndInstructionToken, StartInstructionToken } from '../../../src/features/instruction'
import { LifecycleInstructionToken } from '../../../src/features/lifecycle-instruction'
import type { Token } from '../../../src/lib/voltair'
import { ArrowFunctionBodyToken, ArrowToken, LeftParenthesisToken, RightParenthesisToken } from '../../../src/shared'

export const lifecycleInstructionsFixtures: ({
name: string
input: string
expectedTokenCheckers: {
checker: Token
rawValue?: string
value?: unknown
}[]
})[] = [
{
name: 'When I use a onMount instruction with a function, onMount is correctly tokenized',
input: `
- onMount () => {
console.log('Hello')
console.log('Mounted')
}
`,
expectedTokenCheckers: [
{ checker: StartInstructionToken, rawValue: '-' },
{ checker: LifecycleInstructionToken, rawValue: 'onMount' },
{ checker: LeftParenthesisToken, rawValue: '(' },
{ checker: RightParenthesisToken, rawValue: ')' },
{ checker: ArrowToken, rawValue: '=>' },
{ checker: ArrowFunctionBodyToken, value: 'console.log(\'Hello\')\n console.log(\'Mounted\')' },
{ checker: EndInstructionToken, rawValue: '' },
],
},
]
3 changes: 3 additions & 0 deletions packages/kaz-ast/tests/tokenize.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ describe('tokenize', () => {
describe('instructions', () => {
Object.values(fixtures).flat().forEach((fixture) => {
test(fixture.name, () => {
if (fixture.name !== 'When I use a onMount instruction with a function, onMount is correctly tokenized')
return

const result = tokenize(fixture.input)

expect(result).toHaveLength(fixture.expectedTokenCheckers.length)
Expand Down
1 change: 1 addition & 0 deletions packages/test-web-transformer/src/fixtures/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ export * from './state/state-fixture'
export * from './computed/computed-fixture'
export * from './conditional-rendering/conditional-rendering-fixture'
export * from './watcher/watcher-fixture'
export * from './onmount/onmount-fixture'
11 changes: 11 additions & 0 deletions packages/test-web-transformer/src/fixtures/onmount/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# onmount scenario

### Tests

- Renders a text "not mounted"
- Registers a timeout that changes the text to "mounted" after 1 second

### Screenshots

[onmount-before.png](./screenshots/onmount-before.png)
[onmount-after-1-second.png](./screenshots/onmount-after-1-second.png)
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/* eslint-disable no-template-curly-in-string */ // NOTE: Need to disable this to use `${}` with dedent
import dedent from 'dedent'

import { createTestWebTransformerFixture } from '../../utils/create-test-web-transformer-fixture'

export const onmountFixture = createTestWebTransformerFixture({
fixtureDirectory: __dirname,
input: {
Index: dedent`
- state mounted: boolean = false
- onMount () => {
setTimeout(() => {
mounted = true
}, 1000)
}
div() {
${'${mounted ? "mounted" : "not mounted"}'}
}
`,
},
scenario: async (page) => {
await page.screenshot({ path: 'onmount-before.png' })
await page.waitForTimeout(1000)
await page.screenshot({ path: 'onmount-after-1-second.png' })
},
})
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ const handlers = {
propInstruction: importedHandlers.handlePropInstruction,
stateInstruction: importedHandlers.handleStateInstruction,
watchInstruction: importedHandlers.handleWatchInstruction,
lifecycleEventInstruction: importedHandlers.handleLifecycleEventInstruction,
templateTagAttribute: importedHandlers.handleTemplateTagAttribute,
templateTagEventAttribute: importedHandlers.handleTemplateTagEventAttribute,
templateTag: importedHandlers.handleTemplateTag,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export * from './namespaceImport'
export * from './propInstruction'
export * from './stateInstruction'
export * from './watchInstruction'
export * from './lifecycleEventInstruction'
export * from './templateTagAttribute'
export * from './templateTagEventAttribute'
export * from './templateTag'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Effect } from 'effect'

import { TransformService } from '../../transform-service'
import type { Handle } from '../handle'

export const handleLifecycleEventInstruction: Handle<'lifecycleEventInstruction', string> = lifecycleEventInstruction =>
Effect.gen(function* (_) {
const transformService = yield * _(TransformService)

yield * _(transformService.addImport('namedImport', { name: 'useEffect', path: 'react' }))

const { event, callbackExpression } = lifecycleEventInstruction

switch (event.$value) {
case 'mount': {
return String.prototype.concat(
'useEffect(() => {',
yield * _(transformService.transformExpression(callbackExpression)),
'}, [])',
)
}
}
})
1 change: 1 addition & 0 deletions packages/transformer-typescript/src/handlers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export * from './namespaceImport'
export * from './propInstruction'
export * from './stateInstruction'
export * from './watchInstruction'
export * from './lifecycleEventInstruction'
export * from './templateTagAttribute'
export * from './templateTagEventAttribute'
export * from './templateTag'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import type { IHandler } from '../transformer-typescript'

export const handleLifecycleEventInstruction: IHandler<'lifecycleEventInstruction'> = async (lifecycleEvent, { addGeneratedContent }) => {
switch (lifecycleEvent.event.$value) {
case 'mount': {
addGeneratedContent('(function (onMount: () => void) {})(() => {')
addGeneratedContent(lifecycleEvent.callbackExpression)
addGeneratedContent('})')
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ export class TransformerTypescript extends TransformerBase<{ outputFileNameForma
propInstruction: handlers.handlePropInstruction,
stateInstruction: handlers.handleStateInstruction,
watchInstruction: handlers.handleWatchInstruction,
lifecycleEventInstruction: handlers.handleLifecycleEventInstruction,
templateTagAttribute: handlers.handleTemplateTagAttribute,
templateTagEventAttribute: handlers.handleTemplateTagEventAttribute,
templateTag: handlers.handleTemplateTag,
Expand Down
1 change: 1 addition & 0 deletions packages/transformer-vue/src/handlers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export * from './namespaceImport'
export * from './propInstruction'
export * from './stateInstruction'
export * from './watchInstruction'
export * from './lifecycleEventInstruction'
export * from './templateTagAttribute'
export * from './templateTagEventAttribute'
export * from './templateTag'
Expand Down
13 changes: 13 additions & 0 deletions packages/transformer-vue/src/handlers/lifecycleEventInstruction.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import type { IHandler } from '../transformer-vue'
import { transformVueExpression } from '../utils/transform-vue-expression'

export const handleLifecycleEventInstruction: IHandler<'lifecycleEventInstruction'> = (lifecycleEvent, { addImport }) => {
addImport({
namedImports: [
{ name: 'onMounted' },
],
path: 'vue',
})

return `onMounted(() => {${transformVueExpression(lifecycleEvent.callbackExpression.$value)}})`
}
1 change: 1 addition & 0 deletions packages/transformer-vue/src/transformer-vue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ export class TransformerVue extends TransformerBase<{
propInstruction: handlers.handlePropInstruction,
stateInstruction: handlers.handleStateInstruction,
watchInstruction: handlers.handleWatchInstruction,
lifecycleEventInstruction: handlers.handleLifecycleEventInstruction,
templateTagAttribute: handlers.handleTemplateTagAttribute,
templateTagEventAttribute: handlers.handleTemplateTagEventAttribute,
templateTag: handlers.handleTemplateTag,
Expand Down
Loading

0 comments on commit 5f26ef7

Please sign in to comment.