mirror of
https://github.com/withastro/astro.git
synced 2025-02-17 22:44:24 -05:00
normalize client:params
directives in astrojsx
This commit is contained in:
parent
9e90a25ebd
commit
ea914d38d1
4 changed files with 106 additions and 40 deletions
51
packages/astro/src/core/client-directive/utils.ts
Normal file
51
packages/astro/src/core/client-directive/utils.ts
Normal file
|
@ -0,0 +1,51 @@
|
|||
/**
|
||||
* @param value value of the directive (e.g. `{directive: "media", value:'(min-width: 640px)'}`)
|
||||
* @returns a normalized directive object (e.g. `{ directive: 'media', value: '(min-width: 640px)' }`). Returns `null` if the value is nullish.
|
||||
* @throws if the value of the `client:params` directive is invalid
|
||||
*/
|
||||
export function normalizeClientParamsDirective(value: any) {
|
||||
const maybeDirectiveOptions = value;
|
||||
|
||||
// skip the transform if the value is nullish
|
||||
if (isNullish(maybeDirectiveOptions)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!isObject(maybeDirectiveOptions)) {
|
||||
throw new Error(
|
||||
`Error: invalid \`params\` directive value ${JSON.stringify(
|
||||
maybeDirectiveOptions
|
||||
)}. Expected an object of the form \`{ directive: string, value?: any }\`, but got ${typeof maybeDirectiveOptions}.`
|
||||
);
|
||||
}
|
||||
|
||||
// validate the object shape
|
||||
// it should only have two keys: `directive` and `value` (which is optional)
|
||||
for (let _key of Object.keys(maybeDirectiveOptions)) {
|
||||
if (_key !== 'directive' && _key !== 'value') {
|
||||
throw new Error(
|
||||
`Error: invalid \`params\` directive value. Expected an object of the form \`{ directive: string, value?: any }\`, but got ${JSON.stringify(
|
||||
maybeDirectiveOptions
|
||||
)}.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof maybeDirectiveOptions.directive !== 'string') {
|
||||
throw new Error(
|
||||
`Error: expected \`directive\` to be a string, but got ${typeof maybeDirectiveOptions.directive}.`
|
||||
);
|
||||
}
|
||||
return {
|
||||
directive: `client:${maybeDirectiveOptions.directive}`,
|
||||
value: maybeDirectiveOptions.value,
|
||||
};
|
||||
}
|
||||
|
||||
function isObject(value: unknown): value is Record<string, unknown> {
|
||||
return value !== null && typeof value === 'object';
|
||||
}
|
||||
|
||||
function isNullish(value: unknown): value is null | undefined {
|
||||
return typeof value === 'undefined' || value === null;
|
||||
}
|
|
@ -4,6 +4,7 @@ import { AstroError } from '../core/errors/errors.js';
|
|||
import { AstroErrorData } from '../core/errors/index.js';
|
||||
import { resolvePath } from '../core/util.js';
|
||||
import type { PluginMetadata } from '../vite-plugin-astro/types.js';
|
||||
import { normalizeClientParamsDirective } from '../core/client-directive/utils.js';
|
||||
|
||||
const ClientOnlyPlaceholder = 'astro-client-only';
|
||||
|
||||
|
@ -194,6 +195,10 @@ export default function astroJSX(): PluginObj {
|
|||
}
|
||||
const parent = path.findParent((n) => t.isJSXElement(n.node))!;
|
||||
const parentNode = parent.node as t.JSXElement;
|
||||
|
||||
// normalize `client:params` directives
|
||||
normalizeParamsDirectives(parentNode);
|
||||
|
||||
const tagName = getTagName(parentNode);
|
||||
if (!isComponent(tagName)) return;
|
||||
if (!hasClientDirective(parentNode)) return;
|
||||
|
@ -253,6 +258,10 @@ export default function astroJSX(): PluginObj {
|
|||
if (isAttr) return;
|
||||
const parent = path.findParent((n) => t.isJSXElement(n.node))!;
|
||||
const parentNode = parent.node as t.JSXElement;
|
||||
|
||||
// normalize `client:params` directives
|
||||
normalizeParamsDirectives(parentNode);
|
||||
|
||||
const tagName = getTagName(parentNode);
|
||||
if (!isComponent(tagName)) return;
|
||||
if (!hasClientDirective(parentNode)) return;
|
||||
|
@ -323,3 +332,44 @@ export default function astroJSX(): PluginObj {
|
|||
},
|
||||
};
|
||||
}
|
||||
|
||||
// transforms `client:params={{directive: "directive-name", value: "directive-value" }}`
|
||||
// to `client:directive-name="directive-value"`
|
||||
// TODO: test this
|
||||
function normalizeParamsDirectives(node: t.JSXElement) {
|
||||
for (const [i, attr] of node.openingElement.attributes.entries()) {
|
||||
if (isAttributeClientParams(attr)) {
|
||||
// if the attribute is client:params
|
||||
// we normalize it to client:directive-name="directive-value"
|
||||
|
||||
// @ts-expect-error
|
||||
const attributeValue = attr.value.expression || attr.value.value;
|
||||
const maybeNormalizedDirective = normalizeClientParamsDirective(attributeValue);
|
||||
if (!maybeNormalizedDirective) {
|
||||
continue;
|
||||
}
|
||||
const { directive, value: directiveValue } = maybeNormalizedDirective;
|
||||
const newAttr = t.jsxAttribute(
|
||||
t.jsxNamespacedName(t.jsxIdentifier('client'), t.jsxIdentifier(directive)),
|
||||
typeof directiveValue === 'string'
|
||||
? t.stringLiteral(directiveValue)
|
||||
: t.jsxExpressionContainer(directiveValue as t.Expression)
|
||||
);
|
||||
node.openingElement.attributes[i] = newAttr;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function isAttributeClientParams(
|
||||
attribute: t.JSXAttribute | t.JSXSpreadAttribute
|
||||
): attribute is t.JSXAttribute {
|
||||
// TODO: support spread attributes
|
||||
if (attribute.type === 'JSXAttribute' && attribute.name.type === 'JSXNamespacedName') {
|
||||
return jsxAttributeToString(attribute) === 'client:params';
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function isNonNullish<T>(value: T | null | undefined): value is T {
|
||||
return value !== null && value !== undefined;
|
||||
}
|
||||
|
|
|
@ -4,10 +4,10 @@ import type {
|
|||
SSRLoadedRenderer,
|
||||
SSRResult,
|
||||
} from '../../@types/astro.js';
|
||||
import { normalizeClientParamsDirective } from '../../core/client-directive/utils.js';
|
||||
import { AstroError, AstroErrorData } from '../../core/errors/index.js';
|
||||
import { escapeHTML } from './escape.js';
|
||||
import { serializeProps } from './serialize.js';
|
||||
import { isNullish, isObject } from './util.js';
|
||||
|
||||
export interface HydrationMetadata {
|
||||
directive: string;
|
||||
|
@ -80,41 +80,14 @@ export function extractDirectives(
|
|||
// standard cliend directives.
|
||||
// (e.g `client:params={{directive: 'media', value: '(min-width: 640px)'}}`
|
||||
// => `client:media="(min-width: 640px)"`)
|
||||
const maybeDirectiveOptions = value;
|
||||
const maybeNormalizedDirective = normalizeClientParamsDirective(value);
|
||||
|
||||
// skip the transform if the value is nullish
|
||||
if (isNullish(maybeDirectiveOptions)) {
|
||||
if (!maybeNormalizedDirective) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (!isObject(maybeDirectiveOptions)) {
|
||||
throw new Error(
|
||||
`Error: invalid \`params\` directive value ${JSON.stringify(
|
||||
maybeDirectiveOptions
|
||||
)}. Expected an object of the form \`{ directive: string, value?: any }\`, but got ${typeof maybeDirectiveOptions}.`
|
||||
);
|
||||
}
|
||||
|
||||
// validate the object shape
|
||||
// it should only have two keys: `directive` and `value` (which is optional)
|
||||
for (let _key of Object.keys(maybeDirectiveOptions)) {
|
||||
if (_key !== 'directive' && _key !== 'value') {
|
||||
throw new Error(
|
||||
`Error: invalid \`params\` directive value. Expected an object of the form \`{ directive: string, value?: any }\`, but got ${JSON.stringify(
|
||||
maybeDirectiveOptions
|
||||
)}.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof maybeDirectiveOptions.directive !== 'string') {
|
||||
throw new Error(
|
||||
`Error: expected \`directive\` to be a string, but got ${typeof maybeDirectiveOptions.directive}.`
|
||||
);
|
||||
}
|
||||
|
||||
key = `client:${maybeDirectiveOptions.directive}`;
|
||||
value = maybeDirectiveOptions.value;
|
||||
key = `client:${maybeNormalizedDirective.directive}`;
|
||||
value = maybeNormalizedDirective.value;
|
||||
// intentionally fall-through to the next case
|
||||
}
|
||||
|
||||
|
|
|
@ -15,11 +15,3 @@ export async function* streamAsyncIterator(stream: ReadableStream) {
|
|||
reader.releaseLock();
|
||||
}
|
||||
}
|
||||
|
||||
export function isObject(value: unknown): value is Record<string, unknown> {
|
||||
return value !== null && typeof value === 'object';
|
||||
}
|
||||
|
||||
export function isNullish(value: unknown): value is null | undefined {
|
||||
return typeof value === 'undefined' || value === null;
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue