From 721ab06191a1fac780e94de6efb0f4ea79692128 Mon Sep 17 00:00:00 2001 From: Luiz Ferraz Date: Sat, 13 Apr 2024 16:45:18 -0300 Subject: [PATCH] [pkg/inline-mod]: Implements new `lazyValue` utility --- .changeset/wicked-fireants-switch.md | 6 ++ packages/aik-mod/src/index.ts | 8 +- .../inline-mod/src/closure/inspectCode.ts | 56 +++++++++++ packages/inline-mod/src/index.ts | 5 + packages/inline-mod/tests/functions.test.ts | 86 ---------------- .../inline-mod/tests/specialValues.test.ts | 99 +++++++++++++++++++ packages/inline-mod/tsconfig.json | 5 +- 7 files changed, 177 insertions(+), 88 deletions(-) create mode 100644 .changeset/wicked-fireants-switch.md create mode 100644 packages/inline-mod/tests/specialValues.test.ts diff --git a/.changeset/wicked-fireants-switch.md b/.changeset/wicked-fireants-switch.md new file mode 100644 index 00000000..f770a32f --- /dev/null +++ b/.changeset/wicked-fireants-switch.md @@ -0,0 +1,6 @@ +--- +"@inox-tools/inline-mod": minor +"@inox-tools/aik-mod": patch +--- + +Implements new `lazyValue` utility diff --git a/packages/aik-mod/src/index.ts b/packages/aik-mod/src/index.ts index 2f91eb22..439d5bc8 100644 --- a/packages/aik-mod/src/index.ts +++ b/packages/aik-mod/src/index.ts @@ -8,7 +8,13 @@ import { definePlugin } from 'astro-integration-kit'; import { AstroError } from 'astro/errors'; import type { PluginOption } from 'vite'; -export { asyncFactory, factory } from '@inox-tools/inline-mod'; +export { + asyncFactory, + factory, + lazyValue, + type LazyValue, + type ResolvedLazyValue, +} from '@inox-tools/inline-mod'; type HookParams = HookParameters<'astro:config:setup'>; diff --git a/packages/inline-mod/src/closure/inspectCode.ts b/packages/inline-mod/src/closure/inspectCode.ts index 2fd92c1e..535d3823 100644 --- a/packages/inline-mod/src/closure/inspectCode.ts +++ b/packages/inline-mod/src/closure/inspectCode.ts @@ -284,6 +284,14 @@ class Inspector { }; } + const lazyValue = tryExtractLazyValue(value); + if (lazyValue) { + console.log('Detected lazy value'); + const resolvedValue = await lazyValue; + console.log('Resolved lazy value', resolvedValue); + return this.inspect(resolvedValue); + } + if (this.doNotCapture(value)) { log('Value should skip capture'); return Entry.json(); @@ -1151,6 +1159,54 @@ function tryExtractMagicFactory(value: any): MagicPlaceholder[typeof factorySymb } } +const lazyValueSymbol = Symbol('inox-tool/lazy-value'); + +export interface LazyValue { + [lazyValueSymbol]: Promise; + resolved: boolean; + resolve(this: LazyValue, value: T): asserts this is ResolvedLazyValue; +} + +export type ResolvedLazyValue = { + [lazyValueSymbol]: Promise; + resolved: true; + resolve: never; +}; + +const lazyProxyHandler: ProxyHandler> = { + set: () => { + throw new Error('Cannot set properties on a lazy value'); + }, +}; + +export function makeLazyValue(): LazyValue { + let resolve: (value: T) => void; + const promise = new Promise((_resolve) => { + resolve = _resolve; + }); + + const target: LazyValue = { + [lazyValueSymbol]: promise, + resolved: false, + resolve(value) { + if (target.resolved) { + throw new Error('A lazy value can only be resolved once.'); + } + + resolve(value); + target.resolved = true; + }, + }; + + return new Proxy>(target, lazyProxyHandler); +} + +function tryExtractLazyValue(value: any): Promise | undefined { + if (lazyValueSymbol in value) { + return value[lazyValueSymbol]; + } +} + /** * Cache of global entries */ diff --git a/packages/inline-mod/src/index.ts b/packages/inline-mod/src/index.ts index 566079bc..212217bf 100644 --- a/packages/inline-mod/src/index.ts +++ b/packages/inline-mod/src/index.ts @@ -1,4 +1,9 @@ import { magicFactory } from './closure/inspectCode.js'; +export { + type LazyValue, + type ResolvedLazyValue, + makeLazyValue as lazyValue, +} from './closure/inspectCode.js'; export function factory(factoryFn: () => T): T { return magicFactory({ diff --git a/packages/inline-mod/tests/functions.test.ts b/packages/inline-mod/tests/functions.test.ts index 4c1a3ed2..2f65cf01 100644 --- a/packages/inline-mod/tests/functions.test.ts +++ b/packages/inline-mod/tests/functions.test.ts @@ -82,89 +82,3 @@ test('recurring function', async () => { export default __f; `); }); - -test('factory values', async () => { - let callCount = 0; - - const factoryValue = magicFactory({ - isAsync: false, - fn: () => { - callCount++; - - return { - value: 'foo', - }; - }, - }); - - const modInfo = await inspectInlineMod({ - defaultExport: factoryValue, - }); - - expect(modInfo.text).toEqualIgnoringWhitespace(` - function __f0() { - return (function() { - const callCount = 0; - return () => { - callCount++; - return { - value: "foo" - }; - }; - }).apply(undefined, undefined).apply(this, arguments); - } - - const __defaultExport = __f0(); - - export default __defaultExport; - `); - - expect(callCount).toBe(0); - - expect(factoryValue.value).toEqual('foo'); - - expect(callCount).toBe(1); - - factoryValue.value = 'bar'; - expect(factoryValue.value).toEqual('bar'); - - expect(callCount).toBe(1); -}); - -test('async factory values', async () => { - // eslint-disable-next-line @typescript-eslint/no-unused-vars -- Used to test serialization - let callCount = 0; - - const factoryValue = magicFactory({ - isAsync: true, - fn: () => { - callCount++; - - return Promise.resolve({ - value: 'foo', - }); - }, - }); - - const modInfo = await inspectInlineMod({ - defaultExport: factoryValue, - }); - - expect(modInfo.text).toEqualIgnoringWhitespace(` - function __f0() { - return (function() { - const callCount = 0; - return () => { - callCount++; - return Promise.resolve({ - value: "foo" - }); - }; - }).apply(undefined, undefined).apply(this, arguments); - } - - const __defaultExport = await __f0(); - - export default __defaultExport; - `); -}); diff --git a/packages/inline-mod/tests/specialValues.test.ts b/packages/inline-mod/tests/specialValues.test.ts new file mode 100644 index 00000000..59eda847 --- /dev/null +++ b/packages/inline-mod/tests/specialValues.test.ts @@ -0,0 +1,99 @@ +import { expect, test } from 'vitest'; +import { inspectInlineMod } from '../src/inlining.js'; +import { asyncFactory, factory, lazyValue } from '../src/index.js'; + +test('factory values', async () => { + let callCount = 0; + + const factoryValue = factory(() => { + callCount++; + + return { + value: 'foo', + }; + }); + + const modInfo = await inspectInlineMod({ + defaultExport: factoryValue, + }); + + expect(modInfo.text).toEqualIgnoringWhitespace(` + function __f0() { + return (function() { + const callCount = 0; + return () => { + callCount++; + return { + value: "foo" + }; + }; + }).apply(undefined, undefined).apply(this, arguments); + } + + const __defaultExport = __f0(); + + export default __defaultExport; + `); + + expect(callCount).toBe(0); + + expect(factoryValue.value).toEqual('foo'); + + expect(callCount).toBe(1); + + factoryValue.value = 'bar'; + expect(factoryValue.value).toEqual('bar'); + + expect(callCount).toBe(1); +}); + +test('async factory values', async () => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars -- Used to test serialization + let callCount = 0; + + const factoryValue = asyncFactory(() => { + callCount++; + + return Promise.resolve({ + value: 'foo', + }); + }); + + const modInfo = await inspectInlineMod({ + defaultExport: factoryValue, + }); + + expect(modInfo.text).toEqualIgnoringWhitespace(` + function __f0() { + return (function() { + const callCount = 0; + return () => { + callCount++; + return Promise.resolve({ + value: "foo" + }); + }; + }).apply(undefined, undefined).apply(this, arguments); + } + + const __defaultExport = await __f0(); + + export default __defaultExport; + `); +}); + +test('lazy values', async () => { + const value = lazyValue(); + + setTimeout(() => { + value.resolve('foo'); + }, 500); + + const modInfo = await inspectInlineMod({ + defaultExport: value, + }); + + expect(modInfo.text).toEqualIgnoringWhitespace(` + export default "foo"; + `); +}); diff --git a/packages/inline-mod/tsconfig.json b/packages/inline-mod/tsconfig.json index bfc6df51..b659d426 100644 --- a/packages/inline-mod/tsconfig.json +++ b/packages/inline-mod/tsconfig.json @@ -1,4 +1,7 @@ { "$schema": "https://json.schemastore.org/tsconfig", - "extends": "../../tsconfig.base.json" + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "lib": ["ESNext"] + } }