-
Notifications
You must be signed in to change notification settings - Fork 556
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge branch 'feature/alpinejs' of github.com:sbrow/mitosis into feat…
…ure/multiple-outputs
- Loading branch information
Showing
11 changed files
with
436 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
15 changes: 15 additions & 0 deletions
15
packages/core/src/__tests__/__snapshots__/alpine.test.ts.snap
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
// Jest Snapshot v1, https://goo.gl/fbAQLP | ||
|
||
exports[`Alpine.js Remove Internal mitosis package 1`] = ` | ||
"<div x-data=\\"myBasicComponent()\\"> | ||
Hello | ||
<span x-html=\\"name\\"></span> | ||
! I can run in React, Qwik, Vue, Solid, or Liquid! | ||
</div> | ||
<script> | ||
document.addEventListener(\\"alpine:init\\", () => { | ||
Alpine.data(\\"myBasicComponent\\", () => ({ name: \\"PatrickJS\\" })); | ||
}); | ||
</script> | ||
" | ||
`; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
import { componentToAlpine, ToAlpineOptions } from '../generators/alpine'; | ||
import { runTestsForTarget } from './shared'; | ||
|
||
describe('Alpine.js', () => { | ||
const possibleOptions: ToAlpineOptions[] = [ | ||
{}, | ||
// { inlineState: true }, | ||
// { useShorthandSyntax: true }, | ||
// { inlineState: true, useShorthandSyntax: true }, | ||
] | ||
possibleOptions.map(options => runTestsForTarget({ options, target: 'alpine', generator: componentToAlpine })); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,240 @@ | ||
import { format } from 'prettier/standalone'; | ||
import { collectCss } from '../../helpers/styles/collect-css'; | ||
import { fastClone } from '../../helpers/fast-clone'; | ||
import { stripStateAndPropsRefs } from '../../helpers/strip-state-and-props-refs'; | ||
import { selfClosingTags } from '../../parsers/jsx'; | ||
import { checkIsForNode, ForNode, MitosisNode } from '../../types/mitosis-node'; | ||
import { | ||
runPostCodePlugins, | ||
runPostJsonPlugins, | ||
runPreCodePlugins, | ||
runPreJsonPlugins, | ||
} from '../../modules/plugins'; | ||
import { stripMetaProperties } from '../../helpers/strip-meta-properties'; | ||
import { getStateObjectStringFromComponent } from '../../helpers/get-state-object-string'; | ||
import { BaseTranspilerOptions, TranspilerGenerator } from '../../types/transpiler'; | ||
import { dashCase } from '../../helpers/dash-case'; | ||
import { removeSurroundingBlock } from '../../helpers/remove-surrounding-block'; | ||
import { camelCase, curry, flow, flowRight as compose } from 'lodash'; | ||
import { getRefs } from '../../helpers/get-refs'; | ||
import { MitosisComponent } from '../../types/mitosis-component'; | ||
import { hasRootUpdateHook, renderUpdateHooks } from './render-update-hooks'; | ||
import { renderMountHook } from './render-mount-hook'; | ||
|
||
export interface ToAlpineOptions extends BaseTranspilerOptions { | ||
/** | ||
* use @on and : instead of `x-on` and `x-bind` | ||
*/ | ||
useShorthandSyntax?: boolean, | ||
/** | ||
* If true, the javascript won't be extracted into a separate script block. | ||
*/ | ||
inlineState?: boolean, | ||
} | ||
|
||
export const checkIsComponentNode = (node: MitosisNode): boolean => node.name === '@builder.io/mitosis/component'; | ||
|
||
/** | ||
* Test if the binding expression would be likely to generate | ||
* valid or invalid liquid. If we generate invalid liquid tags | ||
* Shopify will reject our PUT to update the template | ||
*/ | ||
export const isValidAlpineBinding = (str = '') => { | ||
return true; | ||
/* | ||
const strictMatches = Boolean( | ||
// Test for our `context.shopify.liquid.*(expression), which | ||
// we regex out later to transform back into valid liquid expressions | ||
str.match(/(context|ctx)\s*(\.shopify\s*)?\.liquid\s*\./), | ||
); | ||
return ( | ||
strictMatches || | ||
// Test is the expression is simple and would map to Shopify bindings // Test for our `context.shopify.liquid.*(expression), which | ||
// e.g. `state.product.price` -> `{{product.price}} // we regex out later to transform back into valid liquid expressions | ||
Boolean(str.match(/^[a-z0-9_\.\s]+$/i)) | ||
); | ||
*/ | ||
}; | ||
|
||
const removeOnFromEventName = (str: string) => str.replace(/^on/, '') | ||
const prefixEvent = (str: string) => str.replace(/(?<=[\s]|^)event/gm, '$event') | ||
const removeTrailingSemicolon = (str: string) => str.replace(/;$/, '') | ||
const trim = (str: string) => str.trim(); | ||
|
||
const replaceInputRefs = curry((json: MitosisComponent, str: string) => { | ||
getRefs(json).forEach(value => { | ||
str = str.replaceAll(value, `this.$refs.${value}`); | ||
}); | ||
|
||
return str; | ||
}); | ||
const replaceStateWithThis = (str: string) => str.replaceAll('state.', 'this.'); | ||
const getStateObjectString = (json: MitosisComponent) => flow( | ||
getStateObjectStringFromComponent, | ||
trim, | ||
replaceInputRefs(json), | ||
renderMountHook(json), | ||
renderUpdateHooks(json), | ||
replaceStateWithThis, | ||
)(json); | ||
|
||
const bindEventHandlerKey = compose( | ||
dashCase, | ||
removeOnFromEventName | ||
); | ||
const bindEventHandlerValue = compose( | ||
prefixEvent, | ||
removeTrailingSemicolon, | ||
trim, | ||
removeSurroundingBlock, | ||
stripStateAndPropsRefs | ||
); | ||
|
||
const bindEventHandler = ({ useShorthandSyntax }: ToAlpineOptions) => (eventName: string, code: string) => { | ||
const bind = useShorthandSyntax ? '@' : 'x-on:' | ||
return ` ${bind}${bindEventHandlerKey(eventName)}="${bindEventHandlerValue(code).trim()}"`; | ||
}; | ||
|
||
const mappers: { | ||
[key: string]: (json: MitosisNode, options: ToAlpineOptions) => string; | ||
} = { | ||
For: (json, options) => ( | ||
!(checkIsForNode(json) && isValidAlpineBinding(json.bindings.each?.code) && isValidAlpineBinding(json.scope.forName)) | ||
? '' | ||
: `<template x-for="${json.scope.forName} in ${stripStateAndPropsRefs(json.bindings.each?.code)}"> | ||
${(json.children ?? []).map((item) => blockToAlpine(item, options)).join('\n')} | ||
</template>` | ||
), | ||
Fragment: (json, options) => blockToAlpine({ ...json, name: "div" }, options), | ||
Show: (json, options) => ( | ||
!isValidAlpineBinding(json.bindings.when?.code) | ||
? '' | ||
: `<template x-if="${stripStateAndPropsRefs(json.bindings.when?.code)}"> | ||
${(json.children ?? []).map((item) => blockToAlpine(item, options)).join('\n')} | ||
</template>` | ||
) | ||
}; | ||
|
||
// TODO: spread support | ||
const blockToAlpine = (json: MitosisNode|ForNode, options: ToAlpineOptions = {}): string => { | ||
if (mappers[json.name]) { | ||
return mappers[json.name](json, options); | ||
} | ||
|
||
// TODO: Add support for `{props.children}` bindings | ||
|
||
if (json.properties._text) { | ||
return json.properties._text; | ||
} | ||
|
||
if (json.bindings._text?.code) { | ||
return isValidAlpineBinding(json.bindings._text.code) | ||
? `<span x-html="${stripStateAndPropsRefs(json.bindings._text.code as string)}"></span>` | ||
: ''; | ||
} | ||
|
||
let str = `<${json.name} `; | ||
|
||
/* | ||
// Copied from the liquid generator. Not sure what it does. | ||
if ( | ||
json.bindings._spread?.code === '_spread' && | ||
isValidAlpineBinding(json.bindings._spread.code) | ||
) { | ||
str += ` | ||
<template x-for="_attr in ${json.bindings._spread.code}"> | ||
{{ _attr[0] }}="{{ _attr[1] }}" | ||
</template> | ||
`; | ||
} | ||
*/ | ||
|
||
for (const key in json.properties) { | ||
const value = json.properties[key]; | ||
str += ` ${key}="${value}" `; | ||
} | ||
|
||
for (const key in json.bindings) { | ||
if (key === '_spread' || key === 'css') { | ||
continue; | ||
} | ||
const { code: value, type: bindingType } = json.bindings[key]!; | ||
// TODO: proper babel transform to replace. Util for this | ||
const useValue = stripStateAndPropsRefs(value); | ||
|
||
if (key.startsWith('on')) { | ||
str += bindEventHandler(options)(key, value); | ||
} else if (key === 'ref') { | ||
str += ` x-ref="${useValue}"`; | ||
} else if (isValidAlpineBinding(useValue)) { | ||
const bind = options.useShorthandSyntax && bindingType !== 'spread' ? ':' : 'x-bind:' | ||
str += ` ${bind}${bindingType === 'spread' ? '' : key}="${useValue}" `.replace(':=', '='); | ||
} | ||
} | ||
return selfClosingTags.has(json.name) | ||
? `${str} />` | ||
: `${str}>${(json.children ?? []).map((item) => blockToAlpine(item, options)).join('\n')}</${json.name}>`; | ||
}; | ||
|
||
|
||
export const componentToAlpine: TranspilerGenerator<ToAlpineOptions> = | ||
(options = {}) => | ||
({ component }) => { | ||
let json = fastClone(component); | ||
if (options.plugins) { | ||
json = runPreJsonPlugins(json, options.plugins); | ||
} | ||
const css = collectCss(json); | ||
stripMetaProperties(json); | ||
if (options.plugins) { | ||
json = runPostJsonPlugins(json, options.plugins); | ||
} | ||
|
||
const stateObjectString = getStateObjectString(json); | ||
// Set x-data on root element | ||
json.children[0].properties['x-data'] = options.inlineState | ||
? stateObjectString | ||
: `${camelCase(json.name)}()`; | ||
|
||
if (hasRootUpdateHook(json)) { | ||
json.children[0].properties['x-effect'] = 'onUpdate' | ||
} | ||
|
||
let str = css.trim().length | ||
? `<style>${css}</style>` | ||
: ''; | ||
str += json.children.map((item) => blockToAlpine(item, options)).join('\n'); | ||
|
||
if (!options.inlineState) { | ||
str += `<script> | ||
document.addEventListener('alpine:init', () => { | ||
Alpine.data('${camelCase(json.name)}', () => (${stateObjectString})) | ||
}) | ||
</script>` | ||
} | ||
|
||
if (options.plugins) { | ||
str = runPreCodePlugins(str, options.plugins); | ||
} | ||
if (options.prettier !== false) { | ||
try { | ||
str = format(str, { | ||
parser: 'html', | ||
htmlWhitespaceSensitivity: 'ignore', | ||
plugins: [ | ||
// To support running in browsers | ||
require('prettier/parser-html'), | ||
require('prettier/parser-postcss'), | ||
require('prettier/parser-babel'), | ||
], | ||
}); | ||
} catch (err) { | ||
console.warn('Could not prettify', { string: str }, err); | ||
} | ||
} | ||
if (options.plugins) { | ||
str = runPostCodePlugins(str, options.plugins); | ||
} | ||
return str; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from './generate' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
import { curry } from 'lodash'; | ||
import { MitosisComponent } from '../../types/mitosis-component'; | ||
import { hasWatchHooks, renderWatchHooks } from './render-update-hooks'; | ||
|
||
function shouldRenderMountHook(json: MitosisComponent): boolean { | ||
return json.hooks.onMount !== undefined | ||
|| hasWatchHooks(json) | ||
} | ||
|
||
export const renderMountHook = curry((json: MitosisComponent, objectString: string) => { | ||
return shouldRenderMountHook(json) | ||
? objectString.replace(/(?:,)?(\s*)(}\s*)$/, `, init() { | ||
${renderWatchHooks(json)} | ||
${json.hooks.onMount?.code ?? ''} | ||
}$1$2`) | ||
: objectString; | ||
}); |
48 changes: 48 additions & 0 deletions
48
packages/core/src/generators/alpine/render-update-hooks.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
import { curry } from "lodash"; | ||
import { extendedHook, MitosisComponent } from "../../types/mitosis-component"; | ||
|
||
const extractCode = (hook: extendedHook) => hook.code; | ||
function renderRootUpdateHook(hooks: extendedHook[], output: string) { | ||
if (hooks.length === 0) { | ||
return output | ||
} | ||
const str = `onUpdate() { | ||
${hooks.map(extractCode).join('\n')} | ||
}`; | ||
|
||
return output.replace(/,?(\s*})$/, `,\n${str}$1`); | ||
} | ||
|
||
function getRootUpdateHooks(json: MitosisComponent) { | ||
return (json.hooks.onUpdate ?? []).filter(hook => hook.deps == '') | ||
} | ||
|
||
export function hasRootUpdateHook(json: MitosisComponent): boolean { | ||
return getRootUpdateHooks(json).length > 0 | ||
} | ||
|
||
export const renderUpdateHooks = curry((json: MitosisComponent, output: string) => { | ||
return renderRootUpdateHook(getRootUpdateHooks(json), output); | ||
}); | ||
|
||
function getWatchHooks(json: MitosisComponent) { | ||
return (json.hooks.onUpdate ?? []).filter(hook => hook.deps?.match(/state|this/)) | ||
} | ||
|
||
export const hasWatchHooks = (json: MitosisComponent): boolean => { | ||
return getWatchHooks(json).length > 0 | ||
} | ||
|
||
function renderWatchHook(hook: extendedHook): string { | ||
const deps = (hook.deps ?? '')?.slice(1).slice(0, -1).split(', ') | ||
.filter(dep => dep.match(/state|this/)); | ||
|
||
|
||
return deps.map(dep => `this.$watch('${dep.replace(/(state|this)\./, '')}', (value, oldValue) => { ${hook.code} });`).join('\n') | ||
} | ||
|
||
export const renderWatchHooks = (json: MitosisComponent): string => { | ||
return hasWatchHooks(json) | ||
? getWatchHooks(json).map(renderWatchHook).join('\n') | ||
: '' | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.