From 721ab06191a1fac780e94de6efb0f4ea79692128 Mon Sep 17 00:00:00 2001 From: Luiz Ferraz Date: Sat, 13 Apr 2024 16:45:18 -0300 Subject: [PATCH 01/12] [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"] + } } From c8cb545d2701193fdcfd06ebb40d4a8c0b17dadd Mon Sep 17 00:00:00 2001 From: Luiz Ferraz Date: Sat, 13 Apr 2024 17:49:28 -0300 Subject: [PATCH 02/12] Fix type reference --- packages/inline-mod/src/closure/inspectCode.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/inline-mod/src/closure/inspectCode.ts b/packages/inline-mod/src/closure/inspectCode.ts index 535d3823..30abb37c 100644 --- a/packages/inline-mod/src/closure/inspectCode.ts +++ b/packages/inline-mod/src/closure/inspectCode.ts @@ -1153,7 +1153,9 @@ export function magicFactory>({ ) as unknown as T; } -function tryExtractMagicFactory(value: any): MagicPlaceholder[typeof factorySymbol] | undefined { +function tryExtractMagicFactory( + value: any +): MagicPlaceholder[typeof factorySymbol] | undefined { if (factorySymbol in value) { return value[factorySymbol]; } From 1223a041552f296017c67c3271435d7d914e8ecc Mon Sep 17 00:00:00 2001 From: Luiz Ferraz Date: Sat, 13 Apr 2024 18:07:32 -0300 Subject: [PATCH 03/12] WIP Docs --- docs/src/content/docs/inline-mod/aik-plugin.md | 2 +- docs/src/content/docs/inline-mod/lazy.md | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 docs/src/content/docs/inline-mod/lazy.md diff --git a/docs/src/content/docs/inline-mod/aik-plugin.md b/docs/src/content/docs/inline-mod/aik-plugin.md index 95f9e17d..cb6e80e3 100644 --- a/docs/src/content/docs/inline-mod/aik-plugin.md +++ b/docs/src/content/docs/inline-mod/aik-plugin.md @@ -35,7 +35,7 @@ export default defineIntegration({ ## API -The plugin exposes multiple entrypoints, all of them accept normal values and [factory wrappers](/inline-mod/factory-wrappers) as values to be included in the virtual modules. +The plugin exposes multiple entrypoints, all of them accept normal values, [factory wrappers](/inline-mod/factory-wrappers) and [lazy values](/inline-mod/lazy/) as values to be included in the virtual modules. ### `inlineModule` diff --git a/docs/src/content/docs/inline-mod/lazy.md b/docs/src/content/docs/inline-mod/lazy.md new file mode 100644 index 00000000..bc36953f --- /dev/null +++ b/docs/src/content/docs/inline-mod/lazy.md @@ -0,0 +1,18 @@ +--- +title: Lazy values +sidebar: + order: 2 +--- + +:::caution[VERY ADVANCED] +Lazy values are an advanced utility, misuse of this feature may cause dealocks, infinite recursions or even serialization of unsafe values. + +Do not use them unless you know _FOR SURE_ that the value is _guaranteed to resolve_: + +- Independently of the virtual source code of a module using the value +- Before the module is needed + ::: + +Sometimes you can only get a valye after the module where it should be serialized to is created. When using the [AIK Plugin](/inline-mod/aik-plugin/), for example, you can only define an inline module during the `astro:config:setup` hook, but you might want to serialize a value from other hooks. + +# WIP From 567834918cc9454d16ea1335925969735e685f6d Mon Sep 17 00:00:00 2001 From: Luiz Ferraz Date: Sun, 14 Apr 2024 21:01:23 -0300 Subject: [PATCH 04/12] Document lazy value and danger of use --- docs/src/content/docs/inline-mod/lazy.md | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/docs/src/content/docs/inline-mod/lazy.md b/docs/src/content/docs/inline-mod/lazy.md index bc36953f..fe142755 100644 --- a/docs/src/content/docs/inline-mod/lazy.md +++ b/docs/src/content/docs/inline-mod/lazy.md @@ -4,15 +4,29 @@ sidebar: order: 2 --- -:::caution[VERY ADVANCED] +Sometimes you can only get a valye after the module where it should be serialized to is created. When using the [AIK Plugin](/inline-mod/aik-plugin/), for example, you can only define an inline module during the `astro:config:setup` hook, but you might want to serialize a value from other hooks. + +```ts +import { lazyValue } from '@inox-tools/inline-mod'; +import { defineModule } from '@inox-tools/inline-mod/vite'; + +const remoteValue = lazyValue(); + +defineModule('virtual:your-plugin/config', { + constExports: { value }, +}); + +remoteValue.set('hello'); +``` + +:::danger[VERY ADVANCED] Lazy values are an advanced utility, misuse of this feature may cause dealocks, infinite recursions or even serialization of unsafe values. +In most cases, [`asyncFactory`](/inline-mod/factory-wrappers#asyncfactory) is a better choice. + Do not use them unless you know _FOR SURE_ that the value is _guaranteed to resolve_: - Independently of the virtual source code of a module using the value - Before the module is needed - ::: - -Sometimes you can only get a valye after the module where it should be serialized to is created. When using the [AIK Plugin](/inline-mod/aik-plugin/), for example, you can only define an inline module during the `astro:config:setup` hook, but you might want to serialize a value from other hooks. -# WIP +::: From 9a2abf0bb7336cae9101c8bec62e5570ae18fa80 Mon Sep 17 00:00:00 2001 From: Luiz Ferraz Date: Sun, 14 Apr 2024 21:02:37 -0300 Subject: [PATCH 05/12] Fix links --- docs/src/content/docs/inline-mod/aik-plugin.md | 2 +- docs/src/content/docs/inline-mod/lazy.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/src/content/docs/inline-mod/aik-plugin.md b/docs/src/content/docs/inline-mod/aik-plugin.md index cb6e80e3..164462ea 100644 --- a/docs/src/content/docs/inline-mod/aik-plugin.md +++ b/docs/src/content/docs/inline-mod/aik-plugin.md @@ -35,7 +35,7 @@ export default defineIntegration({ ## API -The plugin exposes multiple entrypoints, all of them accept normal values, [factory wrappers](/inline-mod/factory-wrappers) and [lazy values](/inline-mod/lazy/) as values to be included in the virtual modules. +The plugin exposes multiple entrypoints, all of them accept normal values, [factory wrappers](/inline-mod/factory-wrappers) and [lazy values](/inline-mod/lazy) as values to be included in the virtual modules. ### `inlineModule` diff --git a/docs/src/content/docs/inline-mod/lazy.md b/docs/src/content/docs/inline-mod/lazy.md index fe142755..67706183 100644 --- a/docs/src/content/docs/inline-mod/lazy.md +++ b/docs/src/content/docs/inline-mod/lazy.md @@ -4,7 +4,7 @@ sidebar: order: 2 --- -Sometimes you can only get a valye after the module where it should be serialized to is created. When using the [AIK Plugin](/inline-mod/aik-plugin/), for example, you can only define an inline module during the `astro:config:setup` hook, but you might want to serialize a value from other hooks. +Sometimes you can only get a valye after the module where it should be serialized to is created. When using the [AIK Plugin](/inline-mod/aik-plugin), for example, you can only define an inline module during the `astro:config:setup` hook, but you might want to serialize a value from other hooks. ```ts import { lazyValue } from '@inox-tools/inline-mod'; From a37d621129857b799e0ecc3d1fe84d1a363aec0a Mon Sep 17 00:00:00 2001 From: Luiz Ferraz Date: Sun, 14 Apr 2024 21:04:38 -0300 Subject: [PATCH 06/12] Fix example --- docs/src/content/docs/inline-mod/lazy.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/src/content/docs/inline-mod/lazy.md b/docs/src/content/docs/inline-mod/lazy.md index 67706183..00996b0d 100644 --- a/docs/src/content/docs/inline-mod/lazy.md +++ b/docs/src/content/docs/inline-mod/lazy.md @@ -10,13 +10,13 @@ Sometimes you can only get a valye after the module where it should be serialize import { lazyValue } from '@inox-tools/inline-mod'; import { defineModule } from '@inox-tools/inline-mod/vite'; -const remoteValue = lazyValue(); +const value = lazyValue(); defineModule('virtual:your-plugin/config', { constExports: { value }, }); -remoteValue.set('hello'); +value.set('hello'); ``` :::danger[VERY ADVANCED] From d2d2ca8b24a6322a75b2d35e2e19817f849fa056 Mon Sep 17 00:00:00 2001 From: Luiz Ferraz Date: Sun, 14 Apr 2024 21:24:37 -0300 Subject: [PATCH 07/12] Improve docs --- docs/src/content/docs/inline-mod/lazy.md | 29 ++++++++++++++++++------ 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/docs/src/content/docs/inline-mod/lazy.md b/docs/src/content/docs/inline-mod/lazy.md index 00996b0d..0b2486f8 100644 --- a/docs/src/content/docs/inline-mod/lazy.md +++ b/docs/src/content/docs/inline-mod/lazy.md @@ -2,10 +2,22 @@ title: Lazy values sidebar: order: 2 + badge: + text: ADVANCED + variant: danger --- +:::danger +Lazy values are an advanced utility, misuse of this feature may cause dealocks, infinite recursions or even serialization of unsafe values. +In most cases, [`asyncFactory`](/inline-mod/factory-wrappers#asyncfactory) is a better choice. + +Make sure your use case comply with all the [requirements](#requirements) laid out on this page. +::: + Sometimes you can only get a valye after the module where it should be serialized to is created. When using the [AIK Plugin](/inline-mod/aik-plugin), for example, you can only define an inline module during the `astro:config:setup` hook, but you might want to serialize a value from other hooks. +For such use cases, you can use the `lazyValue` utility to create a placeholder value that you can set later: + ```ts import { lazyValue } from '@inox-tools/inline-mod'; import { defineModule } from '@inox-tools/inline-mod/vite'; @@ -19,14 +31,17 @@ defineModule('virtual:your-plugin/config', { value.set('hello'); ``` -:::danger[VERY ADVANCED] -Lazy values are an advanced utility, misuse of this feature may cause dealocks, infinite recursions or even serialization of unsafe values. +## Under the hood -In most cases, [`asyncFactory`](/inline-mod/factory-wrappers#asyncfactory) is a better choice. +Internally this utility will park the module inspection and serialization in Node's event loop such that it can be resumed once, and only once, the value of the lazy value is set. +This allows for as much of the inspection/serialization work to be done preemptively, and not wait for the value to be set. -Do not use them unless you know _FOR SURE_ that the value is _guaranteed to resolve_: +During the module resolution, anything awaiting on the `load` event of the virtual module will also be parked until the value is set. -- Independently of the virtual source code of a module using the value -- Before the module is needed +## Requirements -::: +When using `lazyValue` you must provide the following guarantees: + +- The value is set on all possible code paths +- The value is set before Vite's bundling is expected to complete (and here we say expected because if you don't set the value, Vite will never complete the bundling) +- The value being set does not depend on resolving the serialization of the value (neither directly or indirectly) From 8384240648c2bfea55ead73afe52907fc9e246e0 Mon Sep 17 00:00:00 2001 From: Luiz Ferraz Date: Sun, 14 Apr 2024 21:35:21 -0300 Subject: [PATCH 08/12] Remove debug logs --- packages/inline-mod/src/closure/inspectCode.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/inline-mod/src/closure/inspectCode.ts b/packages/inline-mod/src/closure/inspectCode.ts index 30abb37c..bafba073 100644 --- a/packages/inline-mod/src/closure/inspectCode.ts +++ b/packages/inline-mod/src/closure/inspectCode.ts @@ -118,7 +118,7 @@ class Inspector { // a serialized function for each of those, we can emit them a single time. private readonly simpleFunctions: Entry<'function'>[] = []; - public constructor(private readonly serialize: (o: unknown) => boolean) {} + public constructor(private readonly serialize: (o: unknown) => boolean) { } public async inspect( value: unknown, @@ -286,9 +286,7 @@ 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); } @@ -1249,7 +1247,7 @@ class GlobalCache { // these values can be cached once and reused across avery run. // Add entries to allow proper serialization over generators and iterators. - const emptyGenerator = function* (): any {}; + const emptyGenerator = function*(): any { }; this.cache.addUnchecked(Object.getPrototypeOf(emptyGenerator), { type: 'expr', From 956b137bc586ff245497adc70f1a672868e526af Mon Sep 17 00:00:00 2001 From: Luiz Ferraz Date: Thu, 18 Apr 2024 19:03:33 -0300 Subject: [PATCH 09/12] Apply suggestions from code review Co-authored-by: Bryce Russell Co-authored-by: Florian Lefebvre Signed-off-by: Luiz Ferraz --- docs/src/content/docs/inline-mod/lazy.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/src/content/docs/inline-mod/lazy.md b/docs/src/content/docs/inline-mod/lazy.md index 0b2486f8..17ad3094 100644 --- a/docs/src/content/docs/inline-mod/lazy.md +++ b/docs/src/content/docs/inline-mod/lazy.md @@ -8,13 +8,13 @@ sidebar: --- :::danger -Lazy values are an advanced utility, misuse of this feature may cause dealocks, infinite recursions or even serialization of unsafe values. +Lazy values are an advanced utility, misuse of this feature may cause deadlocks, infinite recursions or even serialization of unsafe values. In most cases, [`asyncFactory`](/inline-mod/factory-wrappers#asyncfactory) is a better choice. Make sure your use case comply with all the [requirements](#requirements) laid out on this page. ::: -Sometimes you can only get a valye after the module where it should be serialized to is created. When using the [AIK Plugin](/inline-mod/aik-plugin), for example, you can only define an inline module during the `astro:config:setup` hook, but you might want to serialize a value from other hooks. +Sometimes you can only get a value after the module where it should be serialized to is created. When using the [AIK Plugin](/inline-mod/aik-plugin), for example, you can only define an inline module during the `astro:config:setup` hook, but you might want to serialize a value from other hooks. For such use cases, you can use the `lazyValue` utility to create a placeholder value that you can set later: From 69e213953a87e05772f6f0fca465ebd5621b9987 Mon Sep 17 00:00:00 2001 From: Luiz Ferraz Date: Sun, 14 Apr 2024 21:35:59 -0300 Subject: [PATCH 10/12] Format files --- packages/inline-mod/src/closure/inspectCode.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/inline-mod/src/closure/inspectCode.ts b/packages/inline-mod/src/closure/inspectCode.ts index bafba073..211ecf7f 100644 --- a/packages/inline-mod/src/closure/inspectCode.ts +++ b/packages/inline-mod/src/closure/inspectCode.ts @@ -118,7 +118,7 @@ class Inspector { // a serialized function for each of those, we can emit them a single time. private readonly simpleFunctions: Entry<'function'>[] = []; - public constructor(private readonly serialize: (o: unknown) => boolean) { } + public constructor(private readonly serialize: (o: unknown) => boolean) {} public async inspect( value: unknown, @@ -1247,7 +1247,7 @@ class GlobalCache { // these values can be cached once and reused across avery run. // Add entries to allow proper serialization over generators and iterators. - const emptyGenerator = function*(): any { }; + const emptyGenerator = function* (): any {}; this.cache.addUnchecked(Object.getPrototypeOf(emptyGenerator), { type: 'expr', From cf200eb05e64b59e0a27e00ecb70eec7e209d49a Mon Sep 17 00:00:00 2001 From: Luiz Ferraz Date: Thu, 18 Apr 2024 19:41:49 -0300 Subject: [PATCH 11/12] Improve docs --- docs/src/content/docs/inline-mod/lazy.md | 162 ++++++++++++++++++++++- 1 file changed, 158 insertions(+), 4 deletions(-) diff --git a/docs/src/content/docs/inline-mod/lazy.md b/docs/src/content/docs/inline-mod/lazy.md index 17ad3094..545d2ddb 100644 --- a/docs/src/content/docs/inline-mod/lazy.md +++ b/docs/src/content/docs/inline-mod/lazy.md @@ -40,8 +40,162 @@ During the module resolution, anything awaiting on the `load` event of the virtu ## Requirements -When using `lazyValue` you must provide the following guarantees: +When using `lazyValue` you must guarantee some invariants about the code using it. Failing to do so will cause serious problems in your project, ranging from deadlocks to unsafe code execution on your server. -- The value is set on all possible code paths -- The value is set before Vite's bundling is expected to complete (and here we say expected because if you don't set the value, Vite will never complete the bundling) -- The value being set does not depend on resolving the serialization of the value (neither directly or indirectly) +### Set value on all possible code paths + +There must not be any possible code path where a lazy value is not set. + +#### Example + +If the API call to the remote config fails or if the response is not valid JSON, the value will not be set. + +```ts +import { lazyValue } from '@inox-tools/inline-mod'; + +const remoteConfig = lazyValue(); + +fetch('https://your.api.com/config') + .then((res) => res.json()) + .then((config) => remoteConfig.set(config)); +``` + +Do this instead: + +```ts ins={9-13} +import { lazyValue } from '@inox-tools/inline-mod'; + +const remoteConfig = lazyValue(); + +fetch('https://your.api.com/config') + .then((res) => res.json()) + .then( + (config) => remoteConfig.set(config), + (error) => { + // Set a default value if the API call fails that is available as the error is propagated + remoteConfig.set(null); + throw error; + } + ); +``` + +### Set value before bundling completes + +The value must be set before Vite's bundling is expected to complete (and here we say expected because if you don't set the value, Vite will never complete the bundling). + +#### Example + +In the following code, `pagesData` is set on an Astro hook that runs before Vite's bundling, allowing the pages data to be serialized. + +On the other hand, `pagesDataLate` is being set on an Astro hook that runs _after_ Vite's bundling, this is not allowed. Using the `pagesDataLate` value will lead to problems. + +```ts ins={"This runs before Vite's bundling, correct": 16-17} del={"This runs after Vite's bundling, incorrect": 20-21} +import { defineIntegration, withPlugins } from 'astro-integration-kit'; +import { lazyValue } from '@inox-tools/inline-mod'; +import aikMod from '@inox-tools/aik-mod'; + +export default defineIntegration({ + name: 'my-integration', + setup: ({ name }) => { + const pagesData = lazyValue(); + const pagesDataLate = lazyValue(); + + return withPlugins({ + name, + plugins: [aikMod], + hooks: { + 'astro:build:setup': ({ pages }) => { +. + pagesData.set(pages); + }, + 'astro:build:done': ({ pages }) => { +. + pagesDataLate.set(pages); + }, + }, + }); + }, +}); +``` + +### No self dependency + +The value must not depend on it's own serialization, neither directly on it's value, or indirectly due to control flow. + +#### Example 1: Value is set to it's own serialization + +```ts del={"The value is set to the result of serializing itself": 24-25} +import inlineMod from '@inox-tools/inline-mod/vite'; +import { lazyValue } from '@inox-tools/inline-mod'; + +const value = lazyValue(); + +const moduleId = inlineMod({ + constExports: { + value, + }, +}); + +export default () => { + return [ + inlineMod(), + { + name: 'your-plugin', + resolveId(id) { + if (id === moduleId) { + return '\x00' + moduleId; + } + }, + async load(id) { + if (id !== '\x00' + moduleId) return; + + value.set(await this.load({ id })); + + return result; + }, + }, + ]; +}; +``` + +#### Example 2: Value is not set unless serialized + +In this case the value doesn't seem to depend on itself, but it does. It is only set after it is serialized, but it can't be serialized until it's set, so even though the value being set is independent of the serialization the action of setting the value is not. + +```ts {"Serializes the module, which includes the value": 25-26} del={"But value is only set after the serialization completes, which can't happen": 28-29} +import inlineMod from '@inox-tools/inline-mod/vite'; +import { lazyValue } from '@inox-tools/inline-mod'; + +const value = lazyValue(); + +const moduleId = inlineMod({ + constExports: { + value, + }, +}); + +export default () => { + return [ + inlineMod(), + { + name: 'your-plugin', + resolveId(id) { + if (id === moduleId) { + return '\x00' + moduleId; + } + }, + async load(id) { + if (id !== '\x00' + moduleId) return; + +. + const result = await this.load({ id }); + +. + value.set('foo'); + + return result; + }, + }, + ]; +}; +``` From b23ebb986cb5f5094f969510b42b15855a6881a3 Mon Sep 17 00:00:00 2001 From: Luiz Ferraz Date: Fri, 19 Apr 2024 00:20:53 -0300 Subject: [PATCH 12/12] Update docs/src/content/docs/inline-mod/lazy.md Co-authored-by: Bryce Russell Signed-off-by: Luiz Ferraz --- docs/src/content/docs/inline-mod/lazy.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/content/docs/inline-mod/lazy.md b/docs/src/content/docs/inline-mod/lazy.md index 545d2ddb..bf65d1bf 100644 --- a/docs/src/content/docs/inline-mod/lazy.md +++ b/docs/src/content/docs/inline-mod/lazy.md @@ -11,7 +11,7 @@ sidebar: Lazy values are an advanced utility, misuse of this feature may cause deadlocks, infinite recursions or even serialization of unsafe values. In most cases, [`asyncFactory`](/inline-mod/factory-wrappers#asyncfactory) is a better choice. -Make sure your use case comply with all the [requirements](#requirements) laid out on this page. +Make sure your use case complies with all the [requirements](#requirements) laid out on this page. ::: Sometimes you can only get a value after the module where it should be serialized to is created. When using the [AIK Plugin](/inline-mod/aik-plugin), for example, you can only define an inline module during the `astro:config:setup` hook, but you might want to serialize a value from other hooks.