0
Fork 0
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:
Moustapha HappyDev 2023-12-21 14:03:18 +00:00
parent 9e90a25ebd
commit ea914d38d1
4 changed files with 106 additions and 40 deletions

View 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;
}

View file

@ -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;
}

View file

@ -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
}

View file

@ -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;
}