Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions packages/vite/hmr/helpers/ast-extract.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,27 @@ export default obj;
expect(result.diagnostics.some((d) => d.includes('[ast][warn]'))).toBe(false);
expect(result.body.replace(/\s+/g, ' ').trim()).toContain('const obj = { default: 1, other: 2 }');
});

it('keeps object properties whose value is undefined or null', () => {
const source = `
import { ref } from 'vue';
const themeOptions = [{ label: 'System', value: undefined }, { label: 'Light', value: 'light' }];
const flags = { a: 1, b: null, c: void 0, nullable: 2 };
`;
const body = astExtractImportsAndStripTypes(source).body.replace(/\s+/g, ' ');
expect(body).toContain("{ label: 'System', value: undefined }");
expect(body).toContain('{ a: 1, b: null, c: void 0, nullable: 2 }');
});

it('strips type annotations left on function parameters', () => {
const source = `
function f(a: string, b?: undefined, ...rest: any[]) { return [a, b, rest]; }
const g = (x: number = 1) => x;
`;
const result = astExtractImportsAndStripTypes(source);
const body = result.body.replace(/\s+/g, ' ');
expect(body).toContain('function f(a, b, ...rest)');
expect(body).toContain('const g = (x = 1) => x');
expect(result.diagnostics).toEqual(expect.arrayContaining(['[ast-fallback] stripped residual param annotations']));
});
});
37 changes: 32 additions & 5 deletions packages/vite/hmr/helpers/ast-extract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,34 @@ export interface AstProcessResult {
diagnostics: string[];
}

function stripParamAnnotations(statements: Statement[]): boolean {
let stripped = false;
const strip = (param: any) => {
for (const node of [param, param?.left, param?.argument, param?.parameter]) {
if (node?.typeAnnotation) {
node.typeAnnotation = null;
stripped = true;
}
if (node?.optional) {
node.optional = false;
stripped = true;
}
}
};
const visit = (node: any) => {
if (!node || typeof node.type !== 'string') return;
if (Array.isArray(node.params)) node.params.forEach(strip);
for (const key of Object.keys(node)) {
if (key === 'loc' || key === 'leadingComments' || key === 'trailingComments' || key === 'innerComments') continue;
const value = node[key];
if (Array.isArray(value)) value.forEach(visit);
else if (value && typeof value === 'object') visit(value);
}
};
statements.forEach(visit);
return stripped;
}

export function astExtractImportsAndStripTypes(source: string): AstProcessResult {
const diagnostics: string[] = [];
let ast: File | null = null;
Expand Down Expand Up @@ -39,6 +67,10 @@ export function astExtractImportsAndStripTypes(source: string): AstProcessResult
}
}

if (stripParamAnnotations(other)) {
diagnostics.push('[ast-fallback] stripped residual param annotations');
}

const fileNoImports: File = {
...ast,
program: { ...ast.program, body: other },
Expand All @@ -52,11 +84,6 @@ export function astExtractImportsAndStripTypes(source: string): AstProcessResult
bodyCode = source;
}

if (/(\(|,|=)\s*[A-Za-z_$][\w$]*\s*:\s*(?:boolean|string|number|any|unknown|void|null|undefined)/.test(bodyCode)) {
bodyCode = bodyCode.replace(/(\(|,|=)\s*([A-Za-z_$][\w$]*)\s*:\s*(boolean|string|number|any|unknown|void|null|undefined)/g, '$1 $2');
diagnostics.push('[ast-fallback] stripped residual param annotations');
}

const beforeVarStrip = bodyCode;
bodyCode = bodyCode.replace(/\b(const|let|var)\s+([A-Za-z_$][\w$]*)\s*:\s*([^=;]+?)(=)/g, (_m, decl, name, _type, eq) => `${decl} ${name} ${eq}`);
if (bodyCode !== beforeVarStrip) {
Expand Down
Loading