diff --git a/packages/vite-plugin-gea/src/closure-codegen/transform/transform-store.ts b/packages/vite-plugin-gea/src/closure-codegen/transform/transform-store.ts index 24abd01..9ee87b2 100644 --- a/packages/vite-plugin-gea/src/closure-codegen/transform/transform-store.ts +++ b/packages/vite-plugin-gea/src/closure-codegen/transform/transform-store.ts @@ -20,6 +20,50 @@ export interface StoreTransformResult { export type ResolveImportPath = (importer: string, source: string) => string | null +function hasUnsafeStorePattern(ast: File, storeLocalName: string): boolean { + let unsafe = false + + function walk(node: any) { + if (!node || typeof node !== 'object' || unsafe) return + + if (Array.isArray(node)) { + for (const child of node) walk(child) + return + } + + if (t.isIdentifier(node) && (node.name === 'flushSync' || node.name === 'silent')) { + unsafe = true + return + } + + if ( + (t.isMemberExpression(node) || t.isOptionalMemberExpression(node)) && + t.isIdentifier(node.object, { name: storeLocalName }) + ) { + unsafe = true + return + } + + if ( + t.isNewExpression(node) && + t.isIdentifier(node.callee, { name: storeLocalName }) + ) { + unsafe = true + return + } + + for (const key of Object.keys(node)) { + if (key === 'loc' || key === 'start' || key === 'end' || key === 'type' || key === 'comments' || key === 'leadingComments' || key === 'trailingComments') { + continue + } + walk(node[key]) + } + } + + walk(ast.program) + return unsafe +} + export function transformCompiledStoreModule( source: string, moduleId = '', @@ -59,7 +103,7 @@ export function transformCompiledStoreModule( ir: fallbackIrs[0], irs: fallbackIrs, } - if (/\b(flushSync|silent|Store\.|new\s+Store\s*\()/.test(source)) { + if (hasUnsafeStorePattern(ast, imported.localName)) { return fallback } // All-or-nothing across the module's store classes: a partial transform diff --git a/packages/vite-plugin-gea/tests/closure-codegen/transform-store.test.ts b/packages/vite-plugin-gea/tests/closure-codegen/transform-store.test.ts new file mode 100644 index 0000000..18abb8e --- /dev/null +++ b/packages/vite-plugin-gea/tests/closure-codegen/transform-store.test.ts @@ -0,0 +1,103 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' +import { transformCompiledStoreModule } from '../../src/closure-codegen/transform/transform-store.ts' + +describe('transformCompiledStoreModule', () => { + it('should transform to CompiledStore without false positives from JSDoc comments containing Store.', () => { + const input = ` + import { Store } from '@geajs/core' + /** + * JSDoc comment test: Store.property + * This is Store. + * .Store + */ + export class MyStore extends Store { + data = [] + selected = null + run() {} + runLots() {} + add() {} + update() {} + clear() {} + swapRows() {} + select() {} + remove() {} + } + export default new MyStore() + ` + + const result = transformCompiledStoreModule(input, 'MyStore.ts') + assert.ok(result) + assert.strictEqual(result.changed, true) + assert.match(result.code, /extends Compiled(?:Lean)?Store/) + }) + + it('should transform without false positives from single-line comments containing Store.', () => { + const input = ` + import { Store } from '@geajs/core' + // FIXME: Store.reset() needs fix + export class MyStore extends Store { + data = [] + selected = null + run() {} + runLots() {} + add() {} + update() {} + clear() {} + swapRows() {} + select() {} + remove() {} + } + export default new MyStore() + ` + + const result = transformCompiledStoreModule(input, 'MyStore.ts') + assert.ok(result) + assert.strictEqual(result.changed, true) + assert.match(result.code, /extends Compiled(?:Lean)?Store/) + }) + + it('should fall back when source code contains static Store. calls', () => { + const input = ` + import { Store } from '@geajs/core' + Store.someMethod() + export class MyStore extends Store {} + export default new MyStore() + ` + + const result = transformCompiledStoreModule(input, 'MyStore.ts') + assert.ok(result) + assert.strictEqual(result.changed, false) + assert.doesNotMatch(result.code, /extends Compiled(?:Lean)?Store/) + }) + + it('should fall back when optional Store member access (Store?.someMethod) is present', () => { + const input = ` + import { Store } from '@geajs/core' + Store?.someMethod() + export class MyStore extends Store {} + export default new MyStore() + ` + + const result = transformCompiledStoreModule(input, 'MyStore.ts') + assert.ok(result) + assert.strictEqual(result.changed, false) + assert.doesNotMatch(result.code, /extends Compiled(?:Lean)?Store/) + }) + + it('should fall back when Store. is called after // inside a string literal', () => { + const input = ` + import { Store } from '@geajs/core' + const url = "https://example.com"; Store.someMethod() + export class MyStore extends Store { + data = [] + run() {} + } + export default new MyStore() + ` + const result = transformCompiledStoreModule(input, 'MyStore.ts') + assert.ok(result) + assert.strictEqual(result.changed, false) + assert.doesNotMatch(result.code, /extends Compiled(?:Lean)?Store/) + }) +}) \ No newline at end of file