From c7a6583e33adc2748b1a98e39a1d0c9e0d10bc65 Mon Sep 17 00:00:00 2001 From: Luiz Ferraz Date: Sun, 21 Jan 2024 14:49:28 -0300 Subject: [PATCH] Implement inline mod (#4) --- .changeset/ninety-numbers-search.md | 5 + .eslintignore | 1 + .github/labeler.yml | 8 +- .github/workflows/changesets.yml | 53 + .github/workflows/label.yml | 7 +- .prettierignore | 1 + packages/inline-mod/.eslintrc.cjs | 6 + packages/inline-mod/README.md | 1 + packages/inline-mod/gitignore | 3 + packages/inline-mod/npmignore | 3 + packages/inline-mod/package.json | 46 + packages/inline-mod/src/closure/entry.test.ts | 56 ++ packages/inline-mod/src/closure/entry.ts | 205 ++++ .../inline-mod/src/closure/inspectCode.ts | 910 +++++++++++++++++ packages/inline-mod/src/closure/lazy.ts | 24 + packages/inline-mod/src/closure/package.ts | 278 ++++++ .../inline-mod/src/closure/parseFunction.ts | 943 ++++++++++++++++++ .../inline-mod/src/closure/rewriteSuper.ts | 128 +++ .../inline-mod/src/closure/serialization.ts | 536 ++++++++++ packages/inline-mod/src/closure/types.ts | 80 ++ packages/inline-mod/src/closure/utils.ts | 52 + packages/inline-mod/src/closure/v8.ts | 316 ++++++ packages/inline-mod/src/closure/v8Hooks.ts | 63 ++ packages/inline-mod/src/inlining.ts | 59 ++ packages/inline-mod/src/state.ts | 4 + packages/inline-mod/src/vite.ts | 33 + packages/inline-mod/tsconfig.json | 4 + packages/inline-mod/tsup.config.ts | 13 + packages/velox-luna/index.ts | 1 + packages/velox-luna/package.json | 4 +- pnpm-lock.yaml | 716 ++++++++++++- turbo/generators/app/index.ts | 4 +- turbo/generators/app/templates/README.md | 22 + turbo/generators/app/templates/gitignore | 3 + turbo/generators/app/templates/index.html | 12 + turbo/generators/app/templates/npmignore | 3 + turbo/generators/app/templates/src/index.ts | 30 + turbo/generators/app/templates/vite.config.ts | 7 + 38 files changed, 4616 insertions(+), 24 deletions(-) create mode 100644 .changeset/ninety-numbers-search.md create mode 100644 .github/workflows/changesets.yml create mode 100644 packages/inline-mod/.eslintrc.cjs create mode 100644 packages/inline-mod/README.md create mode 100644 packages/inline-mod/gitignore create mode 100644 packages/inline-mod/npmignore create mode 100644 packages/inline-mod/package.json create mode 100644 packages/inline-mod/src/closure/entry.test.ts create mode 100644 packages/inline-mod/src/closure/entry.ts create mode 100644 packages/inline-mod/src/closure/inspectCode.ts create mode 100644 packages/inline-mod/src/closure/lazy.ts create mode 100644 packages/inline-mod/src/closure/package.ts create mode 100644 packages/inline-mod/src/closure/parseFunction.ts create mode 100644 packages/inline-mod/src/closure/rewriteSuper.ts create mode 100644 packages/inline-mod/src/closure/serialization.ts create mode 100644 packages/inline-mod/src/closure/types.ts create mode 100644 packages/inline-mod/src/closure/utils.ts create mode 100644 packages/inline-mod/src/closure/v8.ts create mode 100644 packages/inline-mod/src/closure/v8Hooks.ts create mode 100644 packages/inline-mod/src/inlining.ts create mode 100644 packages/inline-mod/src/state.ts create mode 100644 packages/inline-mod/src/vite.ts create mode 100644 packages/inline-mod/tsconfig.json create mode 100644 packages/inline-mod/tsup.config.ts create mode 100644 turbo/generators/app/templates/README.md create mode 100644 turbo/generators/app/templates/gitignore create mode 100644 turbo/generators/app/templates/index.html create mode 100644 turbo/generators/app/templates/npmignore create mode 100644 turbo/generators/app/templates/src/index.ts create mode 100644 turbo/generators/app/templates/vite.config.ts diff --git a/.changeset/ninety-numbers-search.md b/.changeset/ninety-numbers-search.md new file mode 100644 index 00000000..1efa061d --- /dev/null +++ b/.changeset/ninety-numbers-search.md @@ -0,0 +1,5 @@ +--- +"@inox-tools/inline-mod": patch +--- + +Initial test release diff --git a/.eslintignore b/.eslintignore index 3faaf82c..950c089f 100644 --- a/.eslintignore +++ b/.eslintignore @@ -2,5 +2,6 @@ packages/**/*.min.js packages/**/dist/**/* examples/**/* +/turbo/ .github .changeset diff --git a/.github/labeler.yml b/.github/labeler.yml index c5bdcf09..b8ebcacb 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -5,5 +5,11 @@ automation: workspace: # Any config file at the root -- '*.{js,json}' +- '*.{js,json}' - '.*' + +pkg/inline-mod: +- 'packages/inline-mod/**' + +pkg/velox-luna: +- 'packages/velox-luna/**' diff --git a/.github/workflows/changesets.yml b/.github/workflows/changesets.yml new file mode 100644 index 00000000..dbde33aa --- /dev/null +++ b/.github/workflows/changesets.yml @@ -0,0 +1,53 @@ +name: Surface PR Changesets + +on: pull_request + +permissions: + pull-requests: write + checks: write + statuses: write + +jobs: + check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - name: Get changed files in the .changeset folder + id: changed-files + uses: tj-actions/changed-files@v35 + with: + files: | + .changeset/**/*.md + + - name: Check if any changesets contain minor or major changes + id: check + run: | + echo "Checking for changesets marked as minor or major" + echo "found=false" >> $GITHUB_OUTPUT + + regex="[\"']astro[\"']: (minor|major)" + for file in ${{ steps.changed-files.outputs.all_changed_files }}; do + if [[ $(cat $file) =~ $regex ]]; then + version="${BASH_REMATCH[1]}" + echo "version=$version" >> $GITHUB_OUTPUT + echo "found=true" >> $GITHUB_OUTPUT + echo "$file has a $version release tag" + fi + done + + - name: Add label + uses: actions/github-script@v6 + if: steps.check.outputs.found == 'true' + env: + issue_number: ${{ github.event.number }} + with: + script: | + github.rest.issues.addLabels({ + issue_number: process.env.issue_number, + owner: context.repo.owner, + repo: context.repo.repo, + labels: ['semver: ${{ steps.check.outputs.version }}'] + }); diff --git a/.github/workflows/label.yml b/.github/workflows/label.yml index 94bef5d2..4a5fcf35 100644 --- a/.github/workflows/label.yml +++ b/.github/workflows/label.yml @@ -3,11 +3,16 @@ name: Label PRs on: - - pull_request_target + - pull_request jobs: triage: runs-on: ubuntu-latest + permissions: + contents: read + statuses: write + issues: write + pull-requests: write steps: - uses: actions/labeler@v4 with: diff --git a/.prettierignore b/.prettierignore index 7678bf74..ad74362e 100644 --- a/.prettierignore +++ b/.prettierignore @@ -8,6 +8,7 @@ **/fixtures **/vendor **/.vercel +/turbo/ # Directories .github diff --git a/packages/inline-mod/.eslintrc.cjs b/packages/inline-mod/.eslintrc.cjs new file mode 100644 index 00000000..c529ce8a --- /dev/null +++ b/packages/inline-mod/.eslintrc.cjs @@ -0,0 +1,6 @@ +module.exports = { + rules: { + '@typescript-eslint/ban-types': 'off', + '@typescript-eslint/no-namespace': 'off', + }, +}; diff --git a/packages/inline-mod/README.md b/packages/inline-mod/README.md new file mode 100644 index 00000000..cbe92810 --- /dev/null +++ b/packages/inline-mod/README.md @@ -0,0 +1 @@ +# Inox-tools Inline Module diff --git a/packages/inline-mod/gitignore b/packages/inline-mod/gitignore new file mode 100644 index 00000000..45f72223 --- /dev/null +++ b/packages/inline-mod/gitignore @@ -0,0 +1,3 @@ +node_modules +*.log +lib diff --git a/packages/inline-mod/npmignore b/packages/inline-mod/npmignore new file mode 100644 index 00000000..cace0d6d --- /dev/null +++ b/packages/inline-mod/npmignore @@ -0,0 +1,3 @@ +node_modules +*.log +src diff --git a/packages/inline-mod/package.json b/packages/inline-mod/package.json new file mode 100644 index 00000000..ebcd45aa --- /dev/null +++ b/packages/inline-mod/package.json @@ -0,0 +1,46 @@ +{ + "name": "@inox-tools/inline-mod", + "version": "0.0.0", + "description": "Define a virtual module inline with any reference to buildtime values", + "keywords": [ + "vite-plugin" + ], + "repository": "https://github.com/Fryuni/inox-tools.git", + "license": "MIT", + "author": "Luiz Ferraz ", + "type": "module", + "exports": { + "./vite": { + "types": "./dist/vite.d.ts", + "default": "./dist/index.js" + } + }, + "files": [ + "README.md", + "dist" + ], + "scripts": { + "build": "tsup", + "clean": "rimraf ./dist", + "test": "vitest", + "dev": "vite --host", + "prepublish": "npm run clean && npm run build" + }, + "dependencies": { + "typescript": "^5" + }, + "devDependencies": { + "@vitest/ui": "^1.1.3", + "tsup": "^8.0.1", + "upath": "^2.0.1", + "vitest": "^1.1.3" + }, + "peerDependencies": { + "vite": "^3.2.0" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + } + } +} diff --git a/packages/inline-mod/src/closure/entry.test.ts b/packages/inline-mod/src/closure/entry.test.ts new file mode 100644 index 00000000..d84fb22b --- /dev/null +++ b/packages/inline-mod/src/closure/entry.test.ts @@ -0,0 +1,56 @@ +import { describe, expect, it } from 'vitest'; +import type { Entry } from './entry.js'; +import { EntryRegistry } from './entry.js'; + +describe('A smart entry registry', () => { + const entry: Entry = { + type: 'expr', + value: 'fooExpr', + }; + + it('should return the entry added for a key', () => { + const registry = new EntryRegistry(); + + registry.add('foo', entry); + + expect(registry.lookup('foo')).toBe(entry); + expect(registry.lookup('bar')).toBeUndefined(); + }); + + it('should support sealing', () => { + const registry: EntryRegistry = new EntryRegistry(); + + registry.seal(); + + expect(() => registry.add('foo', entry)).toThrow(); + }); + + it('should fork into independent copies', () => { + const baseRegistry = new EntryRegistry(); + + const entryOne: Entry = { type: 'expr', value: 'one' }; + const entryTwo: Entry = { type: 'expr', value: 'two' }; + + baseRegistry.add('foo', entryOne); + + const forkOne = baseRegistry.fork(); + const forkTwo = forkOne.fork(); + + expect(forkOne.lookup('foo')).toBe(entryOne); + expect(forkTwo.lookup('foo')).toBe(entryOne); + + forkOne.remove('foo'); + forkOne.add('foo', entryTwo); + + expect(forkTwo.remove('foo')).toBe(entryOne); + + expect(baseRegistry.lookup('foo')).toBe(entryOne); + expect(forkOne.lookup('foo')).toBe(entryTwo); + expect(forkTwo.lookup('foo')).toBeUndefined(); + + baseRegistry.add('bar', entryTwo); + + expect(forkOne.lookup('bar')).toBeUndefined(); + expect(forkTwo.lookup('bar')).toBeUndefined(); + }); +}); diff --git a/packages/inline-mod/src/closure/entry.ts b/packages/inline-mod/src/closure/entry.ts new file mode 100644 index 00000000..339cad6b --- /dev/null +++ b/packages/inline-mod/src/closure/entry.ts @@ -0,0 +1,205 @@ +import type { InspectedFunction, InspectedObject } from './types.js'; + +type EntryMap = { + // A value which can be safely json serialized. + json: any; + + // An RegExp. Will be serialized as 'new RegExp(re.source, re.flags)' + regexp: { source: string; flags: string }; + + // A closure we are dependent on. + function: InspectedFunction; + + // An object which may contain nested closures. + // Can include an optional proto if the user is not using the default Object.prototype. + object: InspectedObject; + + // An array which may contain nested closures. + array: Entry[]; + + // A reference to a requirable module name. + module: { + type: 'default' | 'star'; + reference: string; + }; + + // A promise value. this will be serialized as the underlyign value the promise + // points to. And deserialized as Promise.resolve() + promise: Entry; + + // A simple expression to use to represent this instance. For example "global.Number"; + expr: string; + + // A placeholder for a pending entry + pending: never; +}; + +export type Entry = { + [K in keyof EntryMap]: { + type: K; + value: EntryMap[K]; + }; +}[T]; + +export namespace Entry { + export function expr(value: string): Entry<'expr'> { + return { type: 'expr', value: value }; + } + + export function json(value?: any): Entry<'json'> { + return { type: 'json', value: value }; + } + + export function regexp(value: RegExp): Entry<'regexp'> { + return { type: 'regexp', value: value }; + } + + export function array(value: Entry[]): Entry<'array'> { + return { type: 'array', value: value }; + } +} + +export type SealedRegistry = Omit, 'add'>; + +export class EntryRegistry { + protected readonly inner = new Map(); + + #sealed = false; + + public lookup(key: K): Entry | undefined { + return this.inner.get(key); + } + + public preparedLookup(key: K): Entry { + if (key === undefined || key === null) { + // Undefined and null keys can never be set. + return { type: 'pending' } as Entry<'pending'>; + } + + const existingEntry = this.lookup(key); + if (existingEntry === undefined) { + this.prepare(key); + + // The key is set on prepare. + return this.lookup(key)!; + } + + return existingEntry; + } + + public add(key: K, entry: Entry) { + if (key === undefined || key === null) { + return; + } + + const existingEntry = this.lookup(key); + + if (Object.is(existingEntry, entry)) { + // Entry already stored. Do nothing. + return; + } + + if (existingEntry !== undefined) { + if (existingEntry.type === 'pending') { + Object.assign(existingEntry, entry); + return; + } + + throw new Error('An entry for the given key was already registered.'); + } + + if (this.#sealed) { + throw new Error('Cannot add to a sealed registry'); + } + + this.inner.set(key, entry); + } + + public prepare(key: K) { + // Pending entry is intentionally not constructable without a cast + this.add(key, { type: 'pending' } as Entry<'pending'>); + } + + public remove(key: K): Entry | undefined { + const removedEntry = this.inner.get(key); + return this.inner.delete(key) ? removedEntry : undefined; + } + + public fork(): EntryRegistry { + if (this.#sealed) { + // We are immutable, so there is no risk of interference + return new LayeredRegistry(this); + } + + // We are mutatble, so we move our current self down and + // become a layered registry as well. + // This pins the data up to this point and allow two independent + // registries to share the base map without re-allocating. + + const newBase: EntryRegistry = Object.assign( + Object.create(Object.getPrototypeOf(this)), + this + ); + + const forked = new LayeredRegistry(newBase); + + // Become a fork ourselves + const newThis = new LayeredRegistry(newBase); + Object.setPrototypeOf(this, Object.getPrototypeOf(newThis)); + Object.assign(this, newThis); + + return forked; + } + + public get sealed(): boolean { + return this.#sealed; + } + + public seal(): asserts this is SealedRegistry { + this.#sealed = true; + } +} + +/** + * Forked sides of a registry + * + * @internal + */ +class LayeredRegistry extends EntryRegistry { + private readonly deletedKeys = new Set(); + + public constructor(private readonly parent: EntryRegistry) { + super(); + } + + public lookup(key: K): Entry | undefined { + return this.inner.get(key) ?? this.parentLookup(key); + } + + private parentLookup(key: K): Entry | undefined { + if (this.deletedKeys.has(key)) { + return; + } + + return this.parent.lookup(key); + } + + public remove(key: K): Entry | undefined { + const removedEntry = this.lookup(key); + + this.inner.delete(key); + this.deletedKeys.add(key); + + return removedEntry; + } + + public fork(): EntryRegistry { + if (this.deletedKeys.size === 0 && this.inner.size === 0) { + // Optimization. If we have not layered any change on top of our + // parent, our fork can be our sibling. + return new LayeredRegistry(this.parent); + } + + return super.fork(); + } +} diff --git a/packages/inline-mod/src/closure/inspectCode.ts b/packages/inline-mod/src/closure/inspectCode.ts new file mode 100644 index 00000000..a4779c94 --- /dev/null +++ b/packages/inline-mod/src/closure/inspectCode.ts @@ -0,0 +1,910 @@ +import * as modules from 'node:module'; +import upath from 'upath'; +import { Entry, EntryRegistry } from './entry.js'; +import { Lazy } from './lazy.js'; +import { getModuleFromPath } from './package.js'; +import { + parseFunction, + type CapturedPropertyChain, + type CapturedVariables +} from './parseFunction.js'; +import { rewriteSuperReferences } from './rewriteSuper.js'; +import { InspectedFunction, type PropertyInfo, type PropertyMap } from './types.js'; +import * as utils from './utils.js'; +import * as v8 from './v8.js'; + +interface ContextFrame { + // TODO: Add reporting for function location + // functionLocation?: FunctionLocation; + capturedFunctionName?: string; + capturedVariableName?: string; + isArrowFunction?: boolean; + captureModule?: { name: string; value: any }; +} + +const serializationInspectors = new WeakMap(); + +const alwaysSerialize = (_: unknown) => true; + +/** @internal */ +export function getInspector(serializeFn: (val: unknown) => boolean = alwaysSerialize): Inspector { + const cached = serializationInspectors.get(serializeFn); + if (cached) { + return cached; + } + + const inspector = new Inspector(serializeFn); + serializationInspectors.set(serializeFn, inspector); + return inspector; +} + +// Prevent capture from recursing into the inspection logic. +// This function and all the tooling it refer to cannot be serialized. +(getInspector as any).doNotCapture = true; + +class InspectionError extends Error { + public constructor(message: string, frames?: ContextFrame[]) { + super(message); + if (frames) { + let stack = message + '\nWhile inspecting:'; + for (let i = frames.length - 1; i >= 0; i--) { + const frame = frames[i]; + + stack += '\n '; + + if (i !== frames.length - 1) { + stack += 'in '; + } + + if (frame.capturedFunctionName) { + stack += frame.capturedFunctionName; + } else if (frame.capturedVariableName) { + stack += frame.capturedVariableName; + } else if (frame.captureModule) { + stack += frame.captureModule.name; + } else if (frame.isArrowFunction) { + stack += 'anonymous function'; + } else { + stack += 'unknown'; + } + } + + this.stack = stack; + } + } +} + +/** + * @internal + */ +class Inspector { + public static doNotCapture = true; + + // The cache stores a map of objects to the entries we've created for them. It's used so that + // we only ever create a single environemnt entry for a single object. i.e. if we hit the same + // object multiple times while walking the memory graph, we only emit it once. + public readonly cache = GlobalCache.fork(); + + // The 'frames' we push/pop as we're walking the object graph serializing things. + // These frames allow us to present a useful error message to the user in the context + // of their code as opposed the async callstack we have while serializing. + private readonly frames: ContextFrame[] = []; + + // A mapping from a class method/constructor to the environment entry corresponding to the + // __super value. When we emit the code for any class member we will end up adding + // + // with ( { __super: <...> }) + // + // We will also rewrite usages of "super" in the methods to refer to __super. This way we can + // accurately serialize out the class members, while preserving functionality. + private readonly classInstanceMemberToSuperEntry = new EntryRegistry(); + private readonly classStaticMemberToSuperEntry = new EntryRegistry(); + + // A list of 'simple' functions. Simple functions do not capture anything, do not have any + // special properties on them, and do not have a custom prototype. If we run into multiple + // functions that are simple, and share the same code, then we can just emit the function once + // for them. + // A good example of this is polyfill functions injected by transpilation steps. Normally, + // those functions are repeated for every emitted file that uses them. Instead of generating + // 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 async inspect( + value: unknown, + capturedProperties?: CapturedPropertyChain[] + ): Promise { + try { + return await this.unsafeInspect(value, capturedProperties); + } catch (error) { + if (error instanceof InspectionError) { + throw error; + } + + throw new InspectionError(error instanceof Error ? error.message : `${error}`, this.frames); + } + } + + public async unsafeInspect( + value: unknown, + capturedProperties?: CapturedPropertyChain[] + ): Promise { + // Try simple values + if (value == null) { + return Entry.json(value); + } + + switch (typeof value) { + case 'number': + return this.inspectNumber(value); + case 'boolean': + case 'string': + return Entry.json(value); + case 'bigint': + return Entry.expr(`${value}n`); + case 'symbol': + return Entry.expr(`Symbol.for(${JSON.stringify(value.description)})`); + } + + if (value instanceof RegExp) { + return Entry.regexp(value); + } + + // Check for a cache hit + { + const entry = this.cache.lookup(value); + if (entry) { + if (entry.type === 'object') { + // Even though we've already serialized out this object, it might be the case + // that we serialized out a different set of properties than the current set + // we're being asked to serialize. So we have to make sure that all these props + // are actually serialized. + await this.inspectObject(value, capturedProperties); + } + return entry; + } + } + + this.cache.prepare(value); + + const entry: Entry = await this.inspectComplex(value, capturedProperties); + + this.cache.add(value, entry); + + return entry; + } + + private inspectNumber(val: number): Entry { + // Check if this is a special number that we cannot json serialize. Instead, we'll just inject + // the code necessary to represent the number on the other side. Note: we have to do this + // before we do *anything* else. This is because these special numbers don't even work in maps + // properly. So, if we lookup the value in a map, we may get the cached value for something + // else *entirely*. For example, 0 and -0 will map to the same entry. + if (Object.is(val, -0)) { + return Entry.expr('-0'); + } + if (Object.is(val, NaN)) { + return Entry.expr('NaN'); + } + if (Object.is(val, Infinity)) { + return Entry.expr('Infinity'); + } + if (Object.is(val, -Infinity)) { + return Entry.expr('-Infinity'); + } + + // Not special, just use normal json serialization. + return Entry.json(val); + } + + private async inspectComplex( + value: object, + capturedProperties?: CapturedPropertyChain[] + ): Promise { + if (this.doNotCapture(value)) { + return Entry.json(); + } + + const normalizedModuleName = await findNormalizedModuleName(value); + if (normalizedModuleName) { + return this.captureModule(normalizedModuleName); + } + + if (value instanceof Function) { + return this.inspectFunction(value); + } + + if (value instanceof Promise) { + const val = await value; + return { + type: 'promise', + value: await this.inspect(val), + }; + } + + if (Array.isArray(value)) { + const array: Entry[] = []; + for (const descriptor of getOwnPropertyDescriptors(value)) { + // Property descriptors are note properly typed in TS + array[descriptor.name as unknown as number] = await this.inspect( + getOwnProperty(value, descriptor) + ); + } + + return Entry.array(array); + } + + if (Object.prototype.toString.call(value) === '[object Arguments]') { + // From: https://stackoverflow.com/questions/7656280/how-do-i-check-whether-an-object-is-an-arguments-object-in-javascript + const array: Entry[] = []; + for (const elem of value as unknown[]) { + array.push(await this.inspect(elem)); + } + + return Entry.array(array); + } + + return this.inspectObject(value, capturedProperties); + } + + private async inspectObject( + obj: NonNullable, + capturedProperties: CapturedPropertyChain[] = [] + ): Promise> { + const [entry, serializeAll] = await this.serializeObjectWorker(obj, capturedProperties); + if (capturedProperties.length !== 0 && serializeAll) { + // Object was not fully serialized, but that is needed. Serialize again with all properties. + const [fullEntry] = await this.serializeObjectWorker(obj, []); + return fullEntry; + } + + return entry; + } + + private async serializeObjectWorker( + obj: NonNullable, + _capturedProperties: CapturedPropertyChain[] + ): Promise<[Entry<'object'>, boolean]> { + // TODO: Add optimization for capturing the minimal referenced subset of an object. + + // if (capturedProperties.length === 0) { + const entry = await this.serializeAllObjectProperties(obj); + + return [entry, false]; + // } + + // const [newInspection, serializeAll] = await this.serializeObjectProperties( + // obj, + // capturedProperties + // ); + // Object.assign(entry.value, newInspection); + + // return [entry, serializeAll]; + } + + private async serializeAllObjectProperties(obj: NonNullable): Promise> { + const entry = this.loadObjectEntry(obj); + + // we wanted to capture everything (including the prototype chain) + const descriptors = getOwnPropertyDescriptors(obj); + + for (const descriptor of descriptors) { + const keyEntry = await this.inspect(getNameOrSymbol(descriptor)); + + // We're about to recurse inside this object. In order to prevent infinite loops, put a + // dummy entry in the environment map. That way, if we hit this object again while + // recursing we won't try to generate this property. + // + // Note: we only stop recursing if we hit exactly our sentinel key (i.e. we're self + // recursive). We *do* want to recurse through the object again if we see it through + // non-recursive paths. That's because we might be hitting this object through one + // prop-name-path, but we created it the first time through another prop-name path. + // + // By processing the object again, we will add the different members we need. + if (entry.value.env.has(keyEntry) && entry.value.env.get(keyEntry) === undefined) { + continue; + } + entry.value.env.set(keyEntry, undefined as any); + + const propertyInfo = await this.createPropertyInfo(descriptor); + const prop = getOwnProperty(obj, descriptor); + const valEntry = await this.inspect(prop); + + // Now, replace the dummy entry with the actual one we want. + entry.value.env.set(keyEntry, { info: propertyInfo, entry: valEntry }); + } + + // If the object's __proto__ is not Object.prototype, then we have to capture what it + // actually is. On the other end, we'll use Object.create(deserializedProto) to set + // things up properly. + // + // We don't need to capture the prototype if the user is not capturing 'this' either. + if (!entry.value.proto) { + const proto = Object.getPrototypeOf(obj); + if (proto !== Object.prototype) { + entry.value.proto = await this.inspect(proto); + } + } + + return entry; + } + + private loadObjectEntry(obj: NonNullable): Entry<'object'> { + const existingEntry = this.cache.lookup(obj); + + if (existingEntry === undefined) { + const newEntry: Entry<'object'> = { + type: 'object', + value: { + env: new Map(), + }, + }; + this.cache.add(obj, newEntry); + return newEntry; + } + + switch (existingEntry.type) { + case 'object': + return existingEntry; + case 'pending': { + this.cache.add(obj, { + type: 'object', + value: { + env: new Map(), + }, + }); + + // Cache will turn the existing entry into the new entry. + return existingEntry as unknown as Entry<'object'>; + } + default: + throw new Error('Mismatching entry in cache'); + } + } + + private async inspectFunction(func: Function): Promise> { + if (utils.hasTrueBooleanMember(func, 'doNotCapture')) { + // If we get a function we're not supposed to capture, emit a function that will throw + // at runtime so the user can understand the problem better. + const funcName = func.name || 'anonymous'; + const message = `Function ${funcName} cannot be safely serialized.`; + + func = () => { + throw new Error(message); + }; + } + + // TODO: Add location information back in + // const location = v8.getFunctionLocationAsync + const frame: ContextFrame = { isArrowFunction: false }; + + this.frames.push(frame); + const entry: Entry<'function'> = { + type: 'function', + value: await this.serializeWorker(func), + }; + this.frames.pop(); + + if (InspectedFunction.isSimple(entry.value)) { + const existingSimpleFunction = this.findSimpleFunction(entry.value); + + if (existingSimpleFunction) { + return existingSimpleFunction; + } else { + this.simpleFunctions.push(entry); + } + } + + return entry; + } + + private async serializeWorker(func: Function): Promise { + const funcEntry = this.cache.preparedLookup(func); + const frame = this.frames.at(-1)!; + const functionString = func.toString(); + + const [error, parsedFunction] = parseFunction(functionString); + if (error) { + this.throwSerializableError(error); + } + + frame.isArrowFunction = parsedFunction.isArrowFunction; + const { funcExprWithName, functionDeclarationName } = parsedFunction; + + const capturedValues: PropertyMap = await this.processCapturedVariables( + func, + parsedFunction.capturedVariables + ); + + const functionInfo: InspectedFunction = { + code: parsedFunction.funcExprWithoutName, + capturedValues: capturedValues, + env: new Map(), + usesNonLexicalThis: parsedFunction.usesNonLexicalThis, + name: functionDeclarationName, + paramCount: func.length, + }; + + const proto = Object.getPrototypeOf(func); + // https://github.com/pulumi/pulumi/blob/d4969f3338eb55f8072518ca89ed17a9b72bde93/sdk/nodejs/runtime/closure/createClosure.ts#L625-L628 + const isAsyncFunction = func.constructor && func.constructor.name === 'AsyncFunction'; + + // Ensure the Function's prototype is also serialized. + // This is only needed for functions with custom prototype, be it classes + // or functions with explicitly set prototype. + if ( + !Object.is(proto, Function.prototype) && + !isAsyncFunction && + isDerivedNoCaptureConstructor(func) + ) { + const protoEntry = await this.inspect(proto); + functionInfo.proto = protoEntry; + + if (functionString.startsWith('class ')) { + // This was a class (which is effectively synonymous with a constructor-function). + // We also know that it's a derived class because of the `proto !== + // Function.prototype` check above. (The prototype of a non-derived class points at + // Function.prototype). + // + // they're a bit trickier to serialize than just a straight function. Specifically, + // we have to keep track of the inheritance relationship between classes. That way + // if any of the class members references 'super' we'll be able to rewrite it + // accordingly (since we emit classes as Functions) + await this.processDerivedClassConstructor(func, protoEntry); + + // Because this was was class constructor function, rewrite any 'super' references + // in it do its derived type if it has one. + functionInfo.code = rewriteSuperReferences(funcExprWithName!, /*isStatic*/ false); + } + } + + // Capture any property on the function itself. + for (const descriptor of getOwnPropertyDescriptors(func)) { + if (descriptor.name === 'length' || descriptor.name === 'name') { + // Do not capture `length` and `name` properties since those cannot + // be changed anyway. + continue; + } + + const funcProp = getOwnProperty(func, descriptor); + + if (descriptor.name === 'prototype' && isDefaultFunctionPrototype(func, funcProp)) { + // Only emit the function's prototype if it actually changed. + continue; + } + + const keyEntry = await this.inspect(getNameOrSymbol(descriptor)); + const valEntry = await this.inspect(funcProp); + const propertyInfo = await this.createPropertyInfo(descriptor); + + functionInfo.env.set(keyEntry, { + info: propertyInfo, + entry: valEntry, + }); + } + + const superEntry = + this.classInstanceMemberToSuperEntry.lookup(func) ?? + this.classStaticMemberToSuperEntry.lookup(func); + if (superEntry) { + // This was a class constructor or method. We need to put a special `__super` + // entry into scope and then rewrite any calls to `super()` to refer to it. + capturedValues.set(await this.inspect('__super'), { + entry: superEntry, + }); + + functionInfo.code = rewriteSuperReferences( + funcExprWithName!, + this.classStaticMemberToSuperEntry.lookup(func) !== undefined + ); + } + + // If this was a named function (literally, only a named function-expr or function-decl), then + // place an entry in the environment that maps from this function name to the serialized + // function we're creating. This ensures that recursive functions will call the right method. + // i.e if we have "function f() { f(); }" this will get rewritten to: + // + // function __f() { + // with ({ f: __f }) { + // return function () { f(); } + // + // i.e. the inner call to "f();" will actually call the *outer* __f function, and not + // itself. + if (functionDeclarationName !== undefined) { + capturedValues.set(await this.inspect(functionDeclarationName), { + entry: funcEntry, + }); + } + + return functionInfo; + } + + private async processDerivedClassConstructor(func: Function, protoEntry: Entry): Promise { + // Map from derived class' constructor and members, to the entry for the base class (i.e. + // the base class' constructor function). We'll use this when serializing out those members + // to rewrite any usages of 'super' appropriately. + + // We're processing the derived class constructor itself. Just map it directly to the base + // class function. + this.classInstanceMemberToSuperEntry.add(func, protoEntry); + + const addIfFunction = (prop: any, isStatic: boolean) => { + if (prop instanceof Function) { + const set = isStatic + ? this.classStaticMemberToSuperEntry + : this.classInstanceMemberToSuperEntry; + set.add(prop, protoEntry); + } + }; + + // Also, make sure our methods can also find this entry so they too can refer to + // 'super'. + for (const descriptor of getOwnPropertyDescriptors(func)) { + if ( + descriptor.name !== 'length' && + descriptor.name !== 'name' && + descriptor.name !== 'prototype' + ) { + // static method. + const classProp = getOwnProperty(func, descriptor); + addIfFunction(classProp, /*isStatic*/ true); + } + } + + for (const descriptor of getOwnPropertyDescriptors(func.prototype)) { + // instance method. + const classProp = getOwnProperty(func.prototype, descriptor); + addIfFunction(classProp, /*isStatic*/ false); + } + } + + private async captureModule(normalizedModuleName: string): Promise> { + // Splitting on "/" is safe to do as this module name is already in a normalized form. + const moduleParts = normalizedModuleName.split('/'); + + const nodeModulesSegment = 'node_modules'; + const nodeModulesSegmentIndex = moduleParts.findIndex((v) => v === nodeModulesSegment); + const isInNodeModules = nodeModulesSegmentIndex >= 0; + + // If the path goes into node_modules, strip off the node_modules part. This will help + // ensure that lookup of those modules will work on the cloud-side even if the module + // isn't in a relative node_modules directory. For example, this happens with aws-sdk. + // It ends up actually being in /var/runtime/node_modules inside aws lambda. + // + // This also helps ensure that modules that are 'yarn link'ed are found properly. The + // module path we have may be on some non-local path due to the linking, however this + // will ensure that the module-name we load is a simple path that can be found off the + // node_modules that we actually upload with our serialized functions. + return { + type: 'module', + value: { + type: 'star', + reference: isInNodeModules + ? getModuleFromPath(upath.join(...moduleParts.slice(nodeModulesSegmentIndex + 1))) + : normalizedModuleName, + }, + }; + } + + private async processCapturedVariables( + func: Function, + capturedVariables: CapturedVariables + ): Promise { + const capturedValues: PropertyMap = new Map(); + + for (const scope of ['required', 'optional'] as const) { + for (const [name, properties] of capturedVariables[scope].entries()) { + const value = await v8.lookupCapturedVariableValue(func, name, scope === 'required'); + + const moduleName = await findNormalizedModuleName(value); + const frameLength = this.frames.length; + + if (moduleName) { + this.frames.push({ + captureModule: { + name: moduleName, + value: value, + }, + }); + } else if (value instanceof Function) { + // Only bother pushing on context frame if the name of the variable + // we captured is different from the name of the function. If the + // names are the same, this is a direct reference, and we don't have + // to list both the name of the capture and of the function. if they + // are different, it's an indirect reference, and the name should be + // included for clarity. + if (name !== value.name) { + this.frames.push({ capturedFunctionName: name }); + } + } else { + this.frames.push({ capturedVariableName: name }); + } + + const serializedName = await this.inspect(name); + const serializedValue = await this.inspect(value, properties); + + capturedValues.set(serializedName, { entry: serializedValue }); + + while (this.frames.length > frameLength) { + // Pop the frames until we are back where we begun. + this.frames.pop(); + } + } + } + + return capturedValues; + } + + private async createPropertyInfo(descriptor: ClosurePropertyDescriptor): Promise { + const propertyInfo: PropertyInfo = { hasValue: descriptor.value !== undefined }; + propertyInfo.configurable = descriptor.configurable; + propertyInfo.enumerable = descriptor.enumerable; + propertyInfo.writable = descriptor.writable; + + if (descriptor.get) { + propertyInfo.get = await this.inspect(descriptor.get); + } + + if (descriptor.set) { + propertyInfo.set = await this.inspect(descriptor.set); + } + + return propertyInfo; + } + + private findSimpleFunction(info: InspectedFunction): Entry<'function'> | undefined { + for (const simpleEntry of this.simpleFunctions) { + const simpleFunction = simpleEntry.value; + if ( + simpleFunction.code === info.code && + simpleFunction.usesNonLexicalThis === info.usesNonLexicalThis + ) { + return simpleEntry; + } + } + } + + private doNotCapture(value: object): boolean { + if (!this.serialize(value)) { + return true; + } + + if (utils.hasTrueBooleanMember(value, 'doNotCapture')) { + return true; + } + + if (value instanceof Function && isDerivedNoCaptureConstructor(value)) { + // constructor derived from something that should not be captured + return true; + } + + return false; + } + + private throwSerializableError(info: string): never { + throw new InspectionError(info, this.frames); + } +} + +interface ClosurePropertyDescriptor { + /** name of the property for a normal property. either 'name' or 'symbol' will be present. but not both. */ + name?: string; + /** symbol-name of the property. either 'name' or 'symbol' will be present. but not both. */ + symbol?: symbol; + + configurable?: boolean; + enumerable?: boolean; + value?: any; + writable?: boolean; + get?: () => any; + set?: (v: any) => void; +} + +function createClosurePropertyDescriptor( + nameOrSymbol: string | symbol, + descriptor: PropertyDescriptor +): ClosurePropertyDescriptor { + if (nameOrSymbol === undefined) { + throw new Error('Was not given a name or symbol'); + } + + const copy: ClosurePropertyDescriptor = { ...descriptor }; + if (typeof nameOrSymbol === 'string') { + copy.name = nameOrSymbol; + } else { + copy.symbol = nameOrSymbol; + } + + return copy; +} + +function getOwnPropertyDescriptors(obj: any): ClosurePropertyDescriptor[] { + const result: ClosurePropertyDescriptor[] = []; + + for (const name of Object.getOwnPropertyNames(obj)) { + if (name === '__proto__') { + // don't return prototypes here. If someone wants one, they should call + // Object.getPrototypeOf. Note: this is the standard behavior of + // Object.getOwnPropertyNames. However, the Inspector API returns these, and we want to + // filter them out. + continue; + } + + const descriptor = Object.getOwnPropertyDescriptor(obj, name); + if (!descriptor) { + throw new Error(`Could not get descriptor for ${name} on: ${JSON.stringify(obj)}`); + } + + result.push(createClosurePropertyDescriptor(name, descriptor)); + } + + for (const symbol of Object.getOwnPropertySymbols(obj)) { + const descriptor = Object.getOwnPropertyDescriptor(obj, symbol); + if (!descriptor) { + throw new Error( + `Could not get descriptor for symbol ${symbol.toString()} on: ${JSON.stringify(obj)}` + ); + } + + result.push(createClosurePropertyDescriptor(symbol, descriptor)); + } + + return result; +} + +function getNameOrSymbol(descriptor: ClosurePropertyDescriptor): symbol | string { + if (descriptor.symbol === undefined && descriptor.name === undefined) { + throw new Error("Descriptor didn't have symbol or name: " + JSON.stringify(descriptor)); + } + + return descriptor.symbol || descriptor.name!; +} + +function getOwnProperty(obj: any, descriptor: ClosurePropertyDescriptor): any { + return descriptor.get || descriptor.set ? undefined : obj[getNameOrSymbol(descriptor)]; +} + +function isDefaultFunctionPrototype(func: Function, prototypeProp: any): boolean { + // The initial value of prototype on any newly-created Function instance is a new instance of + // Object, but with the own-property 'constructor' set to point back to the new function. + if (prototypeProp && prototypeProp.constructor === func) { + const descriptors = getOwnPropertyDescriptors(prototypeProp); + return descriptors.length === 1 && descriptors[0].name === 'constructor'; + } + + return false; +} + +const builtInModules = Lazy.of(async () => { + return new Map( + await Promise.all( + modules.builtinModules.map(async (name) => [await import(`node:${name}`), name] as const) + ) + ); +}); + +type ModuleCache = { + id: string; + exports: any; + loaded: boolean; +}; + +// findNormalizedModuleName attempts to find a global name bound to the object, which can be used as +// a stable reference across serialization. For built-in modules (i.e. "os", "fs", etc.) this will +// return that exact name of the module. Otherwise, this will return the relative path to the +// module from the current working directory of the process. This will normally be something of the +// form ./node_modules/... +// +// This function will also always return modules in a normalized form (i.e. all path components will +// be '/'). +async function findNormalizedModuleName(obj: any): Promise { + // First, check the built-in modules + const builtInMods = await builtInModules.get(); + const key = builtInMods.get(obj); + if (key) { + return key; + } + + // Next, check the Node module cache, which will store cached values + // of all non-built-in Node modules loaded by the program so far. _Note_: We + // don't pre-compute this because the require cache will get populated + // dynamically during execution. + for (const mod of Object.values((modules as any)._cache)) { + if (Object.is(mod.exports, obj)) { + // Rewrite the path to be a local module reference relative to the current working + // directory. + const modPath = upath.relative(process.cwd(), mod.id); + return './' + modPath; + } + } + + // Else, return that no global name is available for this object. + return undefined; +} + +// Is this a constructor derived from a noCapture constructor. if so, we don't want to +// emit it. We would be unable to actually hook up the "super()" call as one of the base +// constructors was set to not be captured. +function isDerivedNoCaptureConstructor(func: Function): boolean { + for (let current: any = func; current; current = Object.getPrototypeOf(current)) { + if (utils.hasTrueBooleanMember(current, 'doNotCapture')) { + return true; + } + } + + return false; +} + +/** + * Cache of global entries + */ +class GlobalCache { + private static singleton = Lazy.of(() => new this()); + + public static fork(): EntryRegistry { + return this.singleton.get().cache.fork(); + } + + // The cache stores a map of objects to the entries we've created for them. It's used so that + // we only ever create a single environemnt entry for a single object. i.e. if we hit the same + // object multiple times while walking the memory graph, we only emit it once. + private readonly cache = new EntryRegistry(); + + private constructor() { + this.addWellKnownGlobalEntries(); + } + + private addWellKnownGlobalEntries() { + this.addGlobalInfo('Object'); + this.addGlobalInfo('Function'); + this.addGlobalInfo('Array'); + this.addGlobalInfo('Number'); + this.addGlobalInfo('String'); + + // Global prototype chain + for (let current = global; current; current = Object.getPrototypeOf(current)) { + for (const key of Object.getOwnPropertyNames(current)) { + // "GLOBAL" and "root" are deprecated and give warnings if you try to access them. + if (key !== 'GLOBAL' && key !== 'root') { + this.addGlobalInfo(key); + } + } + } + + // Prototype of syntax desugaring can't change across inspector invocations + // 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 {}; + + this.cache.add(Object.getPrototypeOf(emptyGenerator), { + type: 'expr', + value: 'Object.getPrototypeOf(function*(){})', + }); + this.cache.add(Object.getPrototypeOf(emptyGenerator.prototype), { + type: 'expr', + value: 'Object.getPrototypeOf((function*(){}).prototype)', + }); + this.cache.add(Symbol.iterator, { type: 'expr', value: 'Symbol.iterator' }); + + this.cache.add(process.env, Entry.expr('process.env')); + } + + private addGlobalInfo(key: string) { + const globalObj = (global as any)[key]; + const text = utils.isLegalMemberName(key) ? `global.${key}` : `global[${JSON.stringify(key)}]`; + + if (globalObj !== undefined && globalObj !== null) { + this.cache.add(globalObj, { type: 'expr', value: text }); + this.cache.add(Object.getPrototypeOf(globalObj), { + type: 'expr', + value: `Object.getPrototypeOf(${text})`, + }); + this.cache.add(globalObj.prototype, { + type: 'expr', + value: `${text}.prototype`, + }); + } + } +} diff --git a/packages/inline-mod/src/closure/lazy.ts b/packages/inline-mod/src/closure/lazy.ts new file mode 100644 index 00000000..5a213f55 --- /dev/null +++ b/packages/inline-mod/src/closure/lazy.ts @@ -0,0 +1,24 @@ +/** + * A lazily computed memoized value. + * + * The given factory is only constructed on first use of the value. + * Any subsequent use retrieves the same instance of the value. + */ +export class Lazy { + private initialized = false; + private value?: T; + private constructor(private factory: () => T) {} + + public static of(factory: () => T): Lazy { + return new this(factory); + } + + public get(): T { + if (!this.initialized) { + this.value = this.factory(); + this.initialized = true; + } + + return this.value!; + } +} diff --git a/packages/inline-mod/src/closure/package.ts b/packages/inline-mod/src/closure/package.ts new file mode 100644 index 00000000..ec184dc2 --- /dev/null +++ b/packages/inline-mod/src/closure/package.ts @@ -0,0 +1,278 @@ +// Copyright 2016-2022, Pulumi Corporation. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import * as upath from 'upath'; + +type Exports = string | { [key: string]: SubExports }; +type SubExports = string | { [key: string]: SubExports } | null; + +type PackageDefinition = { + name: string; + exports?: Exports; +}; + +// TODO[issue] handle https://nodejs.org/api/packages.html#package-entry-points +// +// Warning: Introducing the "exports" field prevents consumers of a package from using +// any entry points that are not defined, including the package.json +// (e.g. require("your-package/package.json"). This will likely be a breaking change. + +function getPackageDefinition(path: string): PackageDefinition | undefined { + try { + const directories = path.split(upath.sep); + let last: string | undefined = undefined; + let lastFullPath: string | undefined = undefined; + while (directories.length > 0) { + const curPath = directories.join(upath.sep); + try { + lastFullPath = require.resolve(curPath); + last = curPath; + } catch (e) { + // current path is not a module + } + directories.pop(); + } + if (last === undefined || lastFullPath === undefined) { + throw new Error(`no package.json found for ${path}`); + } + const packageDefinitionAbsPath = + lastFullPath.slice(0, lastFullPath.indexOf(last)) + last + '/package.json'; + return require(packageDefinitionAbsPath); + } catch (err) { + return undefined; + } +} + +// a module's implementations are leaves of the document tree. +function getAllLeafStrings(objectOrPath: SubExports, opts?: RequireOpts): string[] { + if (objectOrPath === null) { + // module blacklisted return no implementations + return []; + } + if (typeof objectOrPath === 'string') { + return [objectOrPath]; + } + const strings: string[] = []; + for (const [key, value] of Object.entries(objectOrPath)) { + if (opts && !opts.isRequire && key === 'require') { + continue; + } + if (opts && !opts.isImport && key === 'import') { + continue; + } + const leaves = getAllLeafStrings(value); + if (leaves.length === 0) { + // if there's an environment where this export does not work, + // don't suggest requires from this match as a more preferable path may + // match this file. + return []; + } + strings.push(...leaves); + } + return strings; +} + +// from https://github.com/nodejs/node/blob/b191e66ddf/lib/internal/modules/esm/resolve.js#L686 +function patternKeyCompare(a: string, b: string) { + const aPatternIndex = a.indexOf('*'); + const bPatternIndex = b.indexOf('*'); + const baseLenA = aPatternIndex === -1 ? a.length : aPatternIndex + 1; + const baseLenB = bPatternIndex === -1 ? b.length : bPatternIndex + 1; + if (baseLenA > baseLenB) { + return -1; + } + if (baseLenB > baseLenA) { + return 1; + } + if (aPatternIndex === -1) { + return 1; + } + if (bPatternIndex === -1) { + return -1; + } + if (a.length > b.length) { + return -1; + } + if (b.length > a.length) { + return 1; + } + return 0; +} + +type SrcPrefix = string; +type Rule = [ + SrcPrefix, + { + modPrefix: string; + modSuffix: string; + srcSuffix: string; + }, +]; + +function makeRule(srcPattern: string, modPattern: string): Rule { + const srcSplit = srcPattern.split('*'); // NodeJS doesn't error out when provided multiple '*'. + const modSplit = modPattern.split('*'); + if (srcSplit.length > 2 || modSplit.length > 2) { + // there is undefined behavior on more than 1 "*" + // see https://github.com/nodejs/node/blob/b191e66ddf/lib/internal/modules/esm/resolve.js#L664 + throw new Error('multiple wildcards in single export target specification'); + } + const [srcPrefix, srcSuffix] = srcSplit; + const [modPrefix, modSuffix] = modSplit; + return [ + srcPrefix, + { + modPrefix, + modSuffix: modSuffix || '', + srcSuffix: srcSuffix || '', + }, + ]; +} + +class WildcardMap { + private map: { [srcPrefix: string]: string }; + private rules: Rule[]; + constructor(matches: [string, string[]][]) { + this.map = {}; + const rules: Rule[] = []; + for (const [match, srcPaths] of matches) { + for (const srcPath of srcPaths) { + if (srcPath.includes('*')) { + // wildcard match + rules.push(makeRule(srcPath, match)); + continue; + } + this.map[srcPath] = match; + } + } + this.rules = rules.sort((a, b) => patternKeyCompare(a[0], b[0])); + } + get(srcName: string): string | undefined { + if (this.map[srcName]) { + return this.map[srcName]; + } + for (const [srcPrefix, srcRule] of this.rules) { + if (!srcName.startsWith(srcPrefix) || !srcName.endsWith(srcRule.srcSuffix)) { + continue; + } + + const srcSubpath = srcName.slice(srcPrefix.length, srcName.length - srcRule.srcSuffix.length); + const result = srcRule.modPrefix + srcSubpath + srcRule.modSuffix; + return result; + } + return undefined; + } +} + +function isConditionalSugar(exports: Exports, name: string) { + // exports sugar does not handle mixing ["./path/to/module"] path keys + // and ["default"|"require"|"import"] conditional keys + // details https://github.com/nodejs/node/blob/b191e66ddf/lib/internal/modules/esm/resolve.js#L593 + let isSugar = false; + for (const key of Object.keys(exports)) { + if (isSugar && key.startsWith('.')) { + throw new Error( + `${name}:package.json "exports" cannot contain some keys starting with "." and some not.` + + ' The exports object must either be an object of package subpath keys' + + ' or an object of main entry condition name keys only.' + ); + } + if (!key.startsWith('.')) { + isSugar = true; + continue; + } + } + return isSugar; +} + +class ModuleMap { + readonly name: string; + private wildcardMap: WildcardMap; + constructor(name: string, exports: Exports, opts?: RequireOpts) { + this.name = name; + + if (isConditionalSugar(exports, name)) { + // the exports keys are not paths meaning it is an exports sugar we need to simplify + exports = { '.': exports }; + } + + const rules: [string, string[]][] = []; + for (const [modPath, objectOrPath] of Object.entries(exports)) { + const modName: string = name + modPath.slice(1); + const leaves = getAllLeafStrings(objectOrPath, opts); + rules.push([modName, leaves.map((leaf) => name + leaf.slice(1))]); + } + this.wildcardMap = new WildcardMap(rules); + } + get(srcName: string) { + const modPath = this.wildcardMap.get(srcName); + if (modPath === undefined) { + throw new Error(`package.json export path for "${srcName}" not found`); + } + return modPath; + } +} + +type RequireOpts = { + isRequire?: boolean; + isImport?: boolean; +}; + +/* + We need to resolve from a source file path to a valid module export. + + Exports to source file is a many-to-one relationship. Reversing this is a one-to-many relationship. + Any of the initial exports are aliases to the same module and + we assume to be semantically equivalent. This makes it a one-to-any relationship. + for example, + <./package.json> + "exports": { + "./foo.js": "./lib/index.js", + "./bar.js": "./lib/index.js", + } + we will resolve ./lib/index.js into either ./foo.js or ./bar.js + a module can resolve into many files conditionally, but aliases are treated as equivalent. + + Due to null specifiers for modules and this one-to-many relationship, we assume that anything with a + null specifier may be unreachable on a different platform and opt for a different alias to cover it if + it exists. + + Exports ending in "/" will be deprecated by node. + + + For more details https://nodejs.org/api/esm.html#resolution-algorithm +*/ + +export function getModuleFromPath( + path: string, + packageDefinition?: PackageDefinition, + opts: RequireOpts = { isRequire: true } +) { + packageDefinition = packageDefinition || getPackageDefinition(path); + if (packageDefinition === undefined) { + return path; + } + if (packageDefinition.exports === undefined) { + return path; + } + if (typeof packageDefinition.exports === 'string') { + return packageDefinition.name; + } + if (typeof packageDefinition.exports === 'object') { + const modMap = new ModuleMap(packageDefinition.name, packageDefinition.exports, opts); + const modulePath = modMap.get(path); + return modulePath; + } + return path; +} diff --git a/packages/inline-mod/src/closure/parseFunction.ts b/packages/inline-mod/src/closure/parseFunction.ts new file mode 100644 index 00000000..6bd3f606 --- /dev/null +++ b/packages/inline-mod/src/closure/parseFunction.ts @@ -0,0 +1,943 @@ +// Copyright 2016-2018, Pulumi Corporation. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import ts from 'typescript'; +import * as utils from './utils.js'; + +/** @internal */ +export interface ParsedFunctionCode { + // The serialized code for the function, usable as an expression. Valid for all functions forms + // (functions, lambdas, methods, etc.). + funcExprWithoutName: string; + + // The serialized code for the function, usable as an function-declaration. Valid only for + // non-lambda function forms. + funcExprWithName?: string; + + // the name of the function if it was a function-declaration. This is needed so + // that we can include an entry in the environment mapping this function name to + // the actual function we generate for it. This is needed so that nested recursive calls + // to the function see the function we're generating. + functionDeclarationName?: string; + + // Whether or not this was an arrow function. + isArrowFunction: boolean; +} + +/** @internal */ +export interface ParsedFunction extends ParsedFunctionCode { + // The set of variables the function attempts to capture. + capturedVariables: CapturedVariables; + + // Whether or not the real 'this' (i.e. not a lexically captured this) is used in the function. + usesNonLexicalThis: boolean; +} + +// Information about a captured property. Both the name and whether or not the property was +// invoked. +/** @internal */ +export interface CapturedPropertyInfo { + name: string; + invoked: boolean; +} + +// Information about a chain of captured properties. i.e. if you have "foo.bar.baz.quux()", we'll +// say that 'foo' was captured, but that "[bar, baz, quux]" was accessed off of it. We'll also note +// that 'quux' was invoked. +/** @internal */ +export interface CapturedPropertyChain { + infos: CapturedPropertyInfo[]; +} + +// A mapping from the names of variables we captured, to information about how those variables were +// used. For example, if we see "a.b.c()" (and 'a' is not declared in the function), we'll record a +// mapping of { "a": ['b', 'c' (invoked)] }. i.e. we captured 'a', accessed the properties 'b.c' +// off of it, and we invoked that property access. With this information we can decide the totality +// of what we need to capture for 'a'. +// +// Note: if we want to capture everything, we just use an empty array for 'CapturedPropertyChain[]'. +// Otherwise, we'll use the chains to determine what portions of the object to serialize. +/** @internal */ +export type CapturedVariableMap = Map; + +// The set of variables the function attempts to capture. There is a required set an an optional +// set. The optional set will not block closure-serialization if we cannot find them, while the +// required set will. For each variable that is captured we also specify the list of properties of +// that variable we need to serialize. An empty-list means 'serialize all properties'. +/** @internal */ +export interface CapturedVariables { + required: CapturedVariableMap; + optional: CapturedVariableMap; +} + +// These are the special globals we've marked as ones we do not want to capture by value. +// These values have a dual meaning. They mean one thing at deployment time and one thing +// at cloud-execution time. By **not** capturing-by-value we take the view that the user +// wants the cloud-execution time view of things. +const nodeModuleGlobals: { [key: string]: boolean } = { + __dirname: true, + __filename: true, + // We definitely should not try to capture/serialize 'require'. Not only will it bottom + // out as a native function, but it is definitely something the user intends to run + // against the right module environment at cloud-execution time and not deployment time. + require: true, +}; + +// Gets the text of the provided function (using .toString()) and massages it so that it is a legal +// function declaration. Note: this ties us heavily to V8 and its representation for functions. In +// particular, it has expectations around how functions/lambdas/methods/generators/constructors etc. +// are represented. If these change, this will likely break us. +/** @internal */ +export function parseFunction(funcString: string): [string, ParsedFunction] { + const [error, functionCode] = parseFunctionCode(funcString); + if (error) { + return [error, undefined as any]; + } + + // In practice it's not guaranteed that a function's toString is parsable by TypeScript. + // V8 intrinsics are prefixed with a '%' and TypeScript does not consider that to be a valid + // identifier. + const [parseError, file] = createSourceFile(functionCode); + if (parseError) { + return [parseError, undefined as any]; + } + + const capturedVariables = computeCapturedVariableNames(file!); + + // if we're looking at an arrow function, the it is always using lexical 'this's + // so we don't have to bother even examining it. + const usesNonLexicalThis = !functionCode.isArrowFunction && computeUsesNonLexicalThis(file!); + + const result = functionCode as ParsedFunction; + result.capturedVariables = capturedVariables; + result.usesNonLexicalThis = usesNonLexicalThis; + + if (result.capturedVariables.required.has('this')) { + return [ + "arrow function captured 'this'. Assign 'this' to another name outside function and capture that.", + result, + ]; + } + + return ['', result]; +} + +function parseFunctionCode(funcString: string): [string, ParsedFunctionCode] { + if (funcString.startsWith('[Function:')) { + return [`the function form was not understood.`, undefined as any]; + } + + // Split this constant out so that if this function *itself* is closure serialized, + // it will not be thought to be native code itself. + const nativeCodeString = '[native ' + 'code]'; + if (funcString.indexOf(nativeCodeString) !== -1) { + return [`it was a native code function.`, undefined as any]; + } + + // There are three general forms of node toString'ed Functions we're trying to find out here. + // + // 1. `[mods] (...) => ... + // + // i.e. an arrow function. We need to ensure that arrow-functions stay arrow-functions, + // and non-arrow-functions end up looking like normal `function` functions. This will make + // it so that we can correctly handle 'this' properly depending on if that should be + // treated as the lexical capture of 'this' or the non-lexical 'this'. + // + // 2. `class Foo { ... }` + // + // i.e. node uses the entire string of a class when toString'ing the constructor function + // for it. + // + // 3. `[mods] function ... + // + // i.e. a normal function (maybe async, maybe a get/set function, but def not an arrow + // function) + + if (tryParseAsArrowFunction(funcString)) { + return ['', { funcExprWithoutName: funcString, isArrowFunction: true }]; + } + + // First check to see if this startsWith 'class'. If so, this is definitely a class. This + // works as Node does not place preceding comments on a class/function, allowing us to just + // directly see if we've started with the right text. + if (funcString.startsWith('class ')) { + // class constructor function. We want to get the actual constructor + // in the class definition (synthesizing an empty one if one does not) + // exist. + const [file, firstDiagnostic] = tryCreateSourceFile(funcString); + if (firstDiagnostic) { + return [`the class could not be parsed: ${firstDiagnostic}`, undefined as any]; + } + + const classDecl = file!.statements.find((x) => ts.isClassDeclaration(x)) as ts.ClassDeclaration; + if (!classDecl) { + return [`the class form was not understood:\n${funcString}`, undefined as any]; + } + + const constructor = classDecl.members.find((m) => + ts.isConstructorDeclaration(m) + ) as ts.ConstructorDeclaration; + if (!constructor) { + // class without explicit constructor. + const isSubClass = classDecl.heritageClauses?.some( + (c) => c.token === ts.SyntaxKind.ExtendsKeyword + ); + return isSubClass + ? makeFunctionDeclaration( + 'constructor() { super(); }', + /*isAsync:*/ false, + /*isFunctionDeclaration:*/ false + ) + : makeFunctionDeclaration( + 'constructor() { }', + /*isAsync:*/ false, + /*isFunctionDeclaration:*/ false + ); + } + + const constructorCode = funcString + .substring(constructor.getStart(file, /*includeJsDocComment*/ false), constructor.end) + .trim(); + return makeFunctionDeclaration( + constructorCode, + /*isAsync:*/ false, + /*isFunctionDeclaration: */ false + ); + } + + let isAsync = false; + if (funcString.startsWith('async ')) { + isAsync = true; + funcString = funcString.slice('async'.length).trimLeft(); + } + + if (funcString.startsWith('function get ') || funcString.startsWith('function set ')) { + const trimmed = funcString.slice('function get'.length); + return makeFunctionDeclaration(trimmed, isAsync, /*isFunctionDeclaration: */ false); + } + + if (funcString.startsWith('get ') || funcString.startsWith('set ')) { + const trimmed = funcString.slice('get '.length); + return makeFunctionDeclaration(trimmed, isAsync, /*isFunctionDeclaration: */ false); + } + + if (funcString.startsWith('function')) { + const trimmed = funcString.slice('function'.length); + return makeFunctionDeclaration(trimmed, isAsync, /*isFunctionDeclaration: */ true); + } + + // Add "function" (this will make methods parseable). i.e. "foo() { }" becomes + // "function foo() { }" + // this also does the right thing for functions with computed names. + return makeFunctionDeclaration(funcString, isAsync, /*isFunctionDeclaration: */ false); +} + +function tryParseAsArrowFunction(toParse: string): boolean { + const [file] = tryCreateSourceFile(toParse); + if (!file || file.statements.length !== 1) { + return false; + } + + const firstStatement = file.statements[0]; + return ts.isExpressionStatement(firstStatement) && ts.isArrowFunction(firstStatement.expression); +} + +function makeFunctionDeclaration( + v: string, + isAsync: boolean, + isFunctionDeclaration: boolean +): [string, ParsedFunctionCode] { + let prefix = isAsync ? 'async ' : ''; + prefix += 'function '; + + v = v.trimLeft(); + + if (v.startsWith('*')) { + v = v.slice(1).trimLeft(); + prefix = 'function* '; + } + + const openParenIndex = v.indexOf('('); + if (openParenIndex < 0) { + return [`the function form was not understood.`, undefined as any]; + } + + if (isComputed(v, openParenIndex)) { + v = v.slice(openParenIndex); + return [ + '', + { + funcExprWithoutName: prefix + v, + funcExprWithName: prefix + '__computed' + v, + functionDeclarationName: undefined, + isArrowFunction: false, + }, + ]; + } + + const nameChunk = v.slice(0, openParenIndex); + const funcName = utils.isLegalMemberName(nameChunk) + ? utils.isLegalFunctionName(nameChunk) + ? nameChunk + : '/*' + nameChunk + '*/' + : ''; + const commentedName = utils.isLegalMemberName(nameChunk) ? '/*' + nameChunk + '*/' : ''; + v = v.slice(openParenIndex).trimLeft(); + + return [ + '', + { + funcExprWithoutName: prefix + commentedName + v, + funcExprWithName: prefix + funcName + v, + functionDeclarationName: isFunctionDeclaration ? nameChunk : undefined, + isArrowFunction: false, + }, + ]; +} + +function isComputed(v: string, openParenIndex: number) { + if (openParenIndex === 0) { + // node 8 and lower use no name at all for computed members. + return true; + } + + if (v.length > 0 && v.charAt(0) === '[') { + // node 10 actually has the name as: [expr] + return true; + } + + return false; +} + +function createSourceFile(serializedFunction: ParsedFunctionCode): [string, ts.SourceFile | null] { + const funcstr = serializedFunction.funcExprWithName || serializedFunction.funcExprWithoutName; + + // Wrap with parens to make into something parseable. This is necessary as many + // types of functions are valid function expressions, but not valid function + // declarations. i.e. "function () { }". This is not a valid function declaration + // (it's missing a name). But it's totally legal as "(function () { })". + const toParse = '(' + funcstr + ')'; + + const [file, firstDiagnostic] = tryCreateSourceFile(toParse); + if (firstDiagnostic) { + return [`the function could not be parsed: ${firstDiagnostic}`, null]; + } + + return ['', file!]; +} + +function tryCreateSourceFile(toParse: string): [ts.SourceFile | undefined, string | undefined] { + const file = ts.createSourceFile( + '', + toParse, + ts.ScriptTarget.Latest, + /*setParentNodes:*/ true, + ts.ScriptKind.TS + ); + + const diagnostics: ts.Diagnostic[] = (file as any).parseDiagnostics; + if (diagnostics.length) { + return [undefined, `${diagnostics[0].messageText}`]; + } + + return [file, undefined]; +} + +function computeUsesNonLexicalThis(file: ts.SourceFile): boolean { + let inTopmostFunction = false; + let usesNonLexicalThis = false; + + ts.forEachChild(file, walk); + + return usesNonLexicalThis; + + function walk(node: ts.Node | undefined) { + if (!node) { + return; + } + + switch (node.kind) { + case ts.SyntaxKind.SuperKeyword: + case ts.SyntaxKind.ThisKeyword: + usesNonLexicalThis = true; + break; + + case ts.SyntaxKind.CallExpression: + return visitCallExpression(node as ts.CallExpression); + + case ts.SyntaxKind.MethodDeclaration: + case ts.SyntaxKind.FunctionDeclaration: + case ts.SyntaxKind.FunctionExpression: + return visitBaseFunction(node as ts.FunctionLikeDeclarationBase); + + // Note: it is intentional that we ignore ArrowFunction. If we use 'this' inside of it, + // then that should be considered a use of the non-lexical-this from an outer function. + // i.e. + // function f() { var v = () => console.log(this) } + // + // case ts.SyntaxKind.ArrowFunction: + default: + break; + } + + ts.forEachChild(node, walk); + } + + function visitBaseFunction(node: ts.FunctionLikeDeclarationBase): void { + if (inTopmostFunction) { + // we're already in the topmost function. No need to descend into any + // further functions. + return; + } + + // Entering the topmost function. + inTopmostFunction = true; + + // Now, visit its body to see if we use 'this/super'. + walk(node.body); + + inTopmostFunction = false; + } + + function visitCallExpression(node: ts.CallExpression) { + // Most call expressions are normal. But we must special case one kind of function: + // TypeScript's __awaiter functions. They are of the form `__awaiter(this, void 0, void 0, + // function* (){})`, + + // The first 'this' argument is passed along in case the expression awaited uses 'this'. + // However, doing that can be very bad for us as in many cases the 'this' just refers to the + // surrounding module, and the awaited expression won't be using that 'this' at all. + walk(node.expression); + + if (isAwaiterCall(node)) { + const lastFunction = node.arguments[3] as ts.FunctionExpression; + walk(lastFunction.body); + return; + } + + // For normal calls, just walk all arguments normally. + for (const arg of node.arguments) { + walk(arg); + } + } +} + +/** + * computeCapturedVariableNames computes the set of free variables in a given function string. Note that this string is + * expected to be the usual V8-serialized function expression text. + */ +function computeCapturedVariableNames(file: ts.SourceFile): CapturedVariables { + // Now that we've parsed the file, compute the free variables, and return them. + + let required: CapturedVariableMap = new Map(); + let optional: CapturedVariableMap = new Map(); + const scopes: Set[] = []; + let functionVars = new Set(); + + // Recurse through the tree. We use typescript's AST here and generally walk the entire + // tree. One subtlety to be aware of is that we generally assume that when we hit an + // identifier that it either introduces a new variable, or it lexically references a + // variable. This clearly doesn't make sense for *all* identifiers. For example, if you + // have "console.log" then "console" tries to lexically reference a variable, but "log" does + // not. So, to avoid that being an issue, we carefully decide when to recurse. For + // example, for member access expressions (i.e. A.B) we do not recurse down the right side. + + ts.forEachChild(file, walk); + + // Now just return all variables whose value is true. Filter out any that are part of the built-in + // Node.js global object, however, since those are implicitly availble on the other side of serialization. + const result: CapturedVariables = { required: new Map(), optional: new Map() }; + + for (const key of required.keys()) { + if (!isBuiltIn(key)) { + result.required.set( + key, + required.get(key)!.concat(optional.has(key) ? optional.get(key)! : []) + ); + } + } + + for (const key of optional.keys()) { + if (!isBuiltIn(key) && !required.has(key)) { + result.optional.set(key, optional.get(key)!); + } + } + + return result; + + function isBuiltIn(ident: string): boolean { + // __awaiter and __rest are never considered built-in. We do this as async/await code will generate + // an __awaiter (so we will need it), but some libraries (like tslib) will add this to the 'global' + // object. The same is true for __rest when destructuring. + // If we think these are built-in, we won't serialize them, and the functions may not + // actually be available if the import that caused it to get attached isn't included in the + // final serialized code. + if (ident === '__awaiter' || ident === '__rest') { + return false; + } + + // Anything in the global dictionary is a built-in. So is anything that's a global Node.js object; + // note that these only exist in the scope of modules, and so are not truly global in the usual sense. + // See https://nodejs.org/api/globals.html for more details. + return global.hasOwnProperty(ident) || nodeModuleGlobals[ident]; + } + + function currentScope(): Set { + return scopes[scopes.length - 1]; + } + + function visitIdentifier(node: ts.Identifier): void { + // Remember undeclared identifiers during the walk, as they are possibly free. + const name = node.text; + for (let i = scopes.length - 1; i >= 0; i--) { + if (scopes[i].has(name)) { + // This is currently known in the scope chain, so do not add it as free. + return; + } + } + + // We reached the top of the scope chain and this wasn't found; it's captured. + const capturedPropertyChain = determineCapturedPropertyChain(node); + if (node.parent.kind === ts.SyntaxKind.TypeOfExpression) { + // "typeof undeclared_id" is legal in JS (and is actually used in libraries). So keep + // track that we would like to capture this variable, but mark that capture as optional + // so we will not throw if we aren't able to find it in scope. + optional.set(name, combineProperties(optional.get(name), capturedPropertyChain)); + } else { + required.set(name, combineProperties(required.get(name), capturedPropertyChain)); + } + } + + function walk(node: ts.Node | undefined) { + if (!node) { + return; + } + + switch (node.kind) { + case ts.SyntaxKind.Identifier: + return visitIdentifier(node as ts.Identifier); + case ts.SyntaxKind.ThisKeyword: + return visitThisExpression(node as ts.ThisExpression); + case ts.SyntaxKind.Block: + return visitBlockStatement(node as ts.Block); + case ts.SyntaxKind.CallExpression: + return visitCallExpression(node as ts.CallExpression); + case ts.SyntaxKind.CatchClause: + return visitCatchClause(node as ts.CatchClause); + case ts.SyntaxKind.MethodDeclaration: + return visitMethodDeclaration(node as ts.MethodDeclaration); + case ts.SyntaxKind.MetaProperty: + // don't walk down an es6 metaproperty (i.e. "new.target"). It doesn't + // capture anything. + return; + case ts.SyntaxKind.PropertyAssignment: + return visitPropertyAssignment(node as ts.PropertyAssignment); + case ts.SyntaxKind.PropertyAccessExpression: + return visitPropertyAccessExpression(node as ts.PropertyAccessExpression); + case ts.SyntaxKind.FunctionDeclaration: + case ts.SyntaxKind.FunctionExpression: + return visitFunctionDeclarationOrExpression(node as ts.FunctionDeclaration); + case ts.SyntaxKind.ArrowFunction: + return visitBaseFunction( + node as ts.ArrowFunction, + /*isArrowFunction:*/ true, + /*name:*/ undefined + ); + case ts.SyntaxKind.VariableDeclaration: + return visitVariableDeclaration(node as ts.VariableDeclaration); + default: + break; + } + + ts.forEachChild(node, walk); + } + + function visitThisExpression(node: ts.ThisExpression): void { + required.set( + 'this', + combineProperties(required.get('this'), determineCapturedPropertyChain(node)) + ); + } + + function combineProperties( + existing: CapturedPropertyChain[] | undefined, + current: CapturedPropertyChain | undefined + ) { + if (existing && existing.length === 0) { + // We already want to capture everything. Keep things that way. + return existing; + } + + if (current === undefined) { + // We want to capture everything. So ignore any properties we've filtered down + // to and just capture them all. + return []; + } + + // We want to capture a specific set of properties. Add this set of properties + // into the existing set. + const combined = existing || []; + combined.push(current); + + return combined; + } + + // Finds nodes of the form `(...expr...).PropName` or `(...expr...)["PropName"]` + // For element access expressions, the argument must be a string literal. + function isPropertyOrElementAccessExpression( + node: ts.Node + ): node is ts.PropertyAccessExpression | ts.ElementAccessExpression { + if (ts.isPropertyAccessExpression(node)) { + return true; + } + + if (ts.isElementAccessExpression(node) && ts.isStringLiteral(node.argumentExpression)) { + return true; + } + + return false; + } + + function determineCapturedPropertyChain(node: ts.Node): CapturedPropertyChain | undefined { + let infos: CapturedPropertyInfo[] | undefined; + + // Walk up a sequence of property-access'es, recording the names we hit, until we hit + // something that isn't a property-access. + while ( + node?.parent && + isPropertyOrElementAccessExpression(node.parent) && + node.parent.expression === node + ) { + if (!infos) { + infos = []; + } + + const propOrElementAccess = node.parent; + + const name = ts.isPropertyAccessExpression(propOrElementAccess) + ? propOrElementAccess.name.text + : (propOrElementAccess.argumentExpression as ts.StringLiteral).text; + + const invoked = + propOrElementAccess.parent !== undefined && + ts.isCallExpression(propOrElementAccess.parent) && + propOrElementAccess.parent.expression === propOrElementAccess; + + // Keep track if this name was invoked. If so, we'll have to analyze it later + // to see if it captured 'this' + infos.push({ name, invoked }); + node = propOrElementAccess; + } + + if (infos) { + // Invariant checking. + if (infos.length === 0) { + throw new Error('How did we end up with an empty list?'); + } + + for (let i = 0; i < infos.length - 1; i++) { + if (infos[i].invoked) { + throw new Error('Only the last item in the dotted chain is allowed to be invoked.'); + } + } + + return { infos }; + } + + // For all other cases, capture everything. + return undefined; + } + + function visitBlockStatement(node: ts.Block): void { + // Push new scope, visit all block statements, and then restore the scope. + scopes.push(new Set()); + ts.forEachChild(node, walk); + scopes.pop(); + } + + function visitFunctionDeclarationOrExpression( + node: ts.FunctionDeclaration | ts.FunctionExpression + ): void { + // A function declaration is special in one way: its identifier is added to the current function's + // var-style variables, so that its name is in scope no matter the order of surrounding references to it. + + if (node.name) { + functionVars.add(node.name.text); + } + + visitBaseFunction(node, /*isArrowFunction:*/ false, node.name); + } + + function visitBaseFunction( + node: ts.FunctionLikeDeclarationBase, + isArrowFunction: boolean, + functionName: ts.Identifier | undefined + ): void { + // First, push new free vars list, scope, and function vars + const savedRequired = required; + const savedOptional = optional; + const savedFunctionVars = functionVars; + + required = new Map(); + optional = new Map(); + functionVars = new Set(); + scopes.push(new Set()); + + // If this is a named function, it's name is in scope at the top level of itself. + if (functionName) { + functionVars.add(functionName.text); + } + + // this/arguments are in scope inside any non-arrow function. + if (!isArrowFunction) { + functionVars.add('this'); + functionVars.add('arguments'); + } + + // The parameters of any function are in scope at the top level of the function. + for (const param of node.parameters) { + nameWalk(param.name, /*isVar:*/ true); + + // Parse default argument expressions + if (param.initializer) { + walk(param.initializer); + } + } + + // Next, visit the body underneath this new context. + walk(node.body); + + // Remove any function-scoped variables that we encountered during the walk. + for (const v of functionVars) { + required.delete(v); + optional.delete(v); + } + + // Restore the prior context and merge our free list with the previous one. + scopes.pop(); + + mergeMaps(savedRequired, required); + mergeMaps(savedOptional, optional); + + functionVars = savedFunctionVars; + required = savedRequired; + optional = savedOptional; + } + + function mergeMaps(target: CapturedVariableMap, source: CapturedVariableMap) { + for (const key of source.keys()) { + const sourcePropInfos = source.get(key)!; + let targetPropInfos = target.get(key)!; + + if (sourcePropInfos.length === 0) { + // we want to capture everything. Make sure that's reflected in the target. + targetPropInfos = []; + } else { + // we want to capture a subet of properties. merge that subset into whatever + // subset we've recorded so far. + for (const sourceInfo of sourcePropInfos) { + targetPropInfos = combineProperties(targetPropInfos, sourceInfo); + } + } + + target.set(key, targetPropInfos); + } + } + + function visitCatchClause(node: ts.CatchClause): void { + scopes.push(new Set()); + + // Add the catch pattern to the scope as a variable. Note that it is scoped to our current + // fresh scope (so it can't be seen by the rest of the function). + if (node.variableDeclaration) { + nameWalk(node.variableDeclaration.name, /*isVar:*/ false); + } + + // And then visit the block without adding them as free variables. + walk(node.block); + + // Relinquish the scope so the error patterns aren't available beyond the catch. + scopes.pop(); + } + + function visitCallExpression(node: ts.CallExpression): void { + // Most call expressions are normal. But we must special case one kind of function: + // TypeScript's __awaiter functions. They are of the form `__awaiter(this, void 0, void 0, function* (){})`, + + // The first 'this' argument is passed along in case the expression awaited uses 'this'. + // However, doing that can be very bad for us as in many cases the 'this' just refers to the + // surrounding module, and the awaited expression won't be using that 'this' at all. + // + // However, there are cases where 'this' may be legitimately lexically used in the awaited + // expression and should be captured properly. We'll figure this out by actually descending + // explicitly into the "function*(){}" argument, asking it to be treated as if it was + // actually a lambda and not a JS function (with the standard js 'this' semantics). By + // doing this, if 'this' is used inside the function* we'll act as if it's a real lexical + // capture so that we pass 'this' along. + walk(node.expression); + + if (isAwaiterCall(node)) { + return visitBaseFunction( + node.arguments[3] as ts.FunctionExpression as ts.FunctionLikeDeclarationBase, + /*isArrowFunction*/ true, + /*name*/ undefined + ); + } + + // For normal calls, just walk all arguments normally. + for (const arg of node.arguments) { + walk(arg); + } + } + + function visitMethodDeclaration(node: ts.MethodDeclaration): void { + if (ts.isComputedPropertyName(node.name)) { + // Don't walk down the 'name' part of the property assignment if it is an identifier. It + // does not capture any variables. However, if it is a computed property name, walk it + // as it may capture variables. + walk(node.name); + } + + // Always walk the method. Pass 'undefined' for the name as a method's name is not in scope + // inside itself. + visitBaseFunction(node, /*isArrowFunction:*/ false, /*name:*/ undefined); + } + + function visitPropertyAssignment(node: ts.PropertyAssignment): void { + if (ts.isComputedPropertyName(node.name)) { + // Don't walk down the 'name' part of the property assignment if it is an identifier. It + // is not capturing any variables. However, if it is a computed property name, walk it + // as it may capture variables. + walk(node.name); + } + + // Always walk the property initializer. + walk(node.initializer); + } + + function visitPropertyAccessExpression(node: ts.PropertyAccessExpression): void { + // Don't walk down the 'name' part of the property access. It could not capture a free variable. + // i.e. if you have "A.B", we should analyze the "A" part and not the "B" part. + walk(node.expression); + } + + function nameWalk(n: ts.BindingName | undefined, isVar: boolean): void { + if (!n) { + return; + } + + switch (n.kind) { + case ts.SyntaxKind.Identifier: + return visitVariableDeclarationIdentifier(n, isVar); + case ts.SyntaxKind.ObjectBindingPattern: + case ts.SyntaxKind.ArrayBindingPattern: + const bindingPattern = n; + for (const element of bindingPattern.elements) { + if (ts.isBindingElement(element)) { + visitBindingElement(element, isVar); + } + } + + return; + default: + return; + } + } + + function visitVariableDeclaration(node: ts.VariableDeclaration): void { + const isLet = + node.parent !== undefined && + ts.isVariableDeclarationList(node.parent) && + (node.parent.flags & ts.NodeFlags.Let) !== 0; + const isConst = + node.parent !== undefined && + ts.isVariableDeclarationList(node.parent) && + (node.parent.flags & ts.NodeFlags.Const) !== 0; + const isVar = !isLet && !isConst; + + // Walk the declaration's `name` property (which may be an Identifier or Pattern) placing + // any variables we encounter into the right scope. + nameWalk(node.name, isVar); + + // Also walk into the variable initializer with the original walker to make sure we see any + // captures on the right hand side. + walk(node.initializer); + } + + function visitVariableDeclarationIdentifier(node: ts.Identifier, isVar: boolean): void { + // If the declaration is an identifier, it isn't a free variable, for whatever scope it + // pertains to (function-wide for var and scope-wide for let/const). Track it so we can + // remove any subseqeunt references to that variable, so we know it isn't free. + if (isVar) { + functionVars.add(node.text); + } else { + currentScope().add(node.text); + } + } + + function visitBindingElement(node: ts.BindingElement, isVar: boolean): void { + // array and object patterns can be quite complex. You can have: + // + // var {t} = val; // lookup a property in 'val' called 't' and place into a variable 't'. + // var {t: m} = val; // lookup a property in 'val' called 't' and place into a variable 'm'. + // var {t: } = val; // lookup a property in 'val' called 't' and decompose further into the pattern. + // + // And, for all of the above, you can have: + // + // var {t = def} = val; + // var {t: m = def} = val; + // var {t: = def} = val; + // + // These are the same as the above, except that if there is no property 't' in 'val', + // then the default value will be used. + // + // You can also have at the end of the literal: { ...rest} + + // Walk the name portion, looking for names to add. for + // + // var {t} // this will be 't'. + // + // for + // + // var {t: m} // this will be 'm' + // + // and for + // + // var {t: } // this will recurse into the pattern. + // + // and for + // + // ...rest // this will be 'rest' + nameWalk(node.name, isVar); + + // if there is a default value, walk it as well, looking for captures. + walk(node.initializer); + + // importantly, we do not walk into node.propertyName + // This Name defines what property will be retrieved from the value being pattern + // matched against. Importantly, it does not define a new name put into scope, + // nor does it reference a variable in scope. + } +} + +function isAwaiterCall(node: ts.CallExpression) { + const result = + ts.isIdentifier(node.expression) && + node.expression.text === '__awaiter' && + node.arguments.length === 4 && + node.arguments[0].kind === ts.SyntaxKind.ThisKeyword && + ts.isFunctionLike(node.arguments[3]); + + return result; +} diff --git a/packages/inline-mod/src/closure/rewriteSuper.ts b/packages/inline-mod/src/closure/rewriteSuper.ts new file mode 100644 index 00000000..4af3c787 --- /dev/null +++ b/packages/inline-mod/src/closure/rewriteSuper.ts @@ -0,0 +1,128 @@ +// Copyright 2016-2022, Pulumi Corporation. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import ts from 'typescript'; +import * as utils from './utils.js'; + +/** @internal */ +export function rewriteSuperReferences(code: string, isStatic: boolean): string { + const sourceFile = ts.createSourceFile('', code, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS); + + // Transform any usages of "super(...)" into "__super.call(this, ...)", any + // instance usages of "super.xxx" into "__super.prototype.xxx" and any static + // usages of "super.xxx" into "__super.xxx" + const transformed = ts.transform(sourceFile, [rewriteSuperCallsWorker]); + const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed }); + + return printer.printNode(ts.EmitHint.Unspecified, transformed.transformed[0], sourceFile).trim(); + + function rewriteSuperCallsWorker(transformationContext: ts.TransformationContext) { + const { factory } = transformationContext; + const newNodes = new Set(); + let firstFunctionDeclaration = true; + + function visitor(node: ts.Node): ts.Node { + // Convert the top level function so it doesn't have a name. We want to convert the user + // function to an anonymous function so that interior references to the same function + // bind properly. i.e. if we start with "function f() { f(); }" then this gets converted to + // + // function __f() { + // with ({ f: __f }) { + // return /*f*/() { f(); } + // + // This means the inner call properly binds to the *outer* function we create. + if (firstFunctionDeclaration && ts.isFunctionDeclaration(node)) { + firstFunctionDeclaration = false; + const funcDecl = ts.visitEachChild(node, visitor, transformationContext); + + const text = utils.isLegalMemberName(funcDecl.name!.text) + ? '/*' + funcDecl.name!.text + '*/' + : ''; + return factory.updateFunctionDeclaration( + funcDecl, + funcDecl.modifiers, + funcDecl.asteriskToken, + factory.createIdentifier(text), + funcDecl.typeParameters, + funcDecl.parameters, + funcDecl.type, + funcDecl.body + ); + } + + if (node.kind === ts.SyntaxKind.SuperKeyword) { + const newNode = factory.createIdentifier('__super'); + newNodes.add(newNode); + return newNode; + } else if ( + ts.isPropertyAccessExpression(node) && + node.expression.kind === ts.SyntaxKind.SuperKeyword + ) { + const expr = isStatic + ? factory.createIdentifier('__super') + : factory.createPropertyAccessExpression( + factory.createIdentifier('__super'), + 'prototype' + ); + const newNode = factory.updatePropertyAccessExpression(node, expr, node.name); + newNodes.add(newNode); + return newNode; + } else if ( + ts.isElementAccessExpression(node) && + node.argumentExpression && + node.expression.kind === ts.SyntaxKind.SuperKeyword + ) { + const expr = isStatic + ? factory.createIdentifier('__super') + : factory.createPropertyAccessExpression( + factory.createIdentifier('__super'), + 'prototype' + ); + + const newNode = factory.updateElementAccessExpression(node, expr, node.argumentExpression); + newNodes.add(newNode); + return newNode; + } + + // for all other nodes, recurse first (so we update any usages of 'super') + // below them + const rewritten = ts.visitEachChild(node, visitor, transformationContext); + + if (ts.isCallExpression(rewritten) && newNodes.has(rewritten.expression)) { + // this was a call to super() or super.x() or super["x"](); + // the super will already have been transformed to __super or + // __super.prototype.x or __super.prototype["x"]. + // + // to that, we have to add the .call(this, ...) call. + + const argumentsCopy = rewritten.arguments.slice(); + argumentsCopy.unshift(transformationContext.factory.createThis()); + + return transformationContext.factory.updateCallExpression( + rewritten, + transformationContext.factory.createPropertyAccessExpression( + rewritten.expression, + 'call' + ), + rewritten.typeArguments, + argumentsCopy + ); + } + + return rewritten; + } + + return (node: ts.Node) => ts.visitNode(node, visitor); + } +} diff --git a/packages/inline-mod/src/closure/serialization.ts b/packages/inline-mod/src/closure/serialization.ts new file mode 100644 index 00000000..1cca4e2b --- /dev/null +++ b/packages/inline-mod/src/closure/serialization.ts @@ -0,0 +1,536 @@ +// Copyright 2016-2018, Pulumi Corporation. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import type { Entry } from './entry.js'; +import type { InspectedFunction } from './types.js'; +import { + type InspectedObject, + type PropertyInfo, + type PropertyInfoAndValue, + type PropertyMap +} from './types.js'; +import * as utils from './utils.js'; + +/** @internal */ +export interface ModEntry { + constExports?: Record; + defaultExport?: Entry; + assignExport?: Entry; +} + +/** @internal */ +export interface SerializedModule { + text: string; +} + +/** + * serializeModule serializes an ECMAScript module into a text form that can be loaded in another execution context, + * for example as part of a virtual module in a bundler. The module serialization captures any + * variables captured by the functions it refers to and serializes those values into the generated text along with the + * module. This process is recursive, so that functions referenced by the body of a serialized function will themselves + * be serialized as well. This process also deeply serializes captured object values, including prototype chains and + * property descriptors, such that the semantics of the function when deserialized should match the original function. + * + * There are several known limitations: + * - If a native function is captured either directly or indirectly, closure serialization will return an error. + * - Captured values will be serialized based on their values at the time that `serializeFunction` is called. Mutations + * to these values after that (but before the deserialized function is used) will not be observed by the deserialized + * function. + */ +export async function serializeModule(modEntry: ModEntry): Promise { + return ModuleSerializer.serialize(modEntry); +} + +class ModuleSerializer { + /** + * Mapping of entries to the name they are bound to. + */ + private readonly envEntryToEnvVar = new Map(); + + /** + * Set of all variable names added to the environment. + * In case of a collision, new entries will receive a different name. + */ + private readonly envVarNames = new Set(); + private readonly functionInfoToEnvVar = new Map(); + + /** + * Set of targets whose code have already beeing emitted. + */ + private readonly emittedTargets = new Set(); + + private importCode = ''; + + private environmentCode = ''; + + public static serialize(entry: ModEntry): SerializedModule { + return new this().serialize(entry); + } + + private serialize(entry: ModEntry): SerializedModule { + let exportCode = ''; + + if (entry.assignExport) { + const ref = this.envEntryToString(entry.assignExport, 'modExport'); + exportCode += `export = ${ref};\n`; + } + + if (entry.defaultExport) { + const ref = this.envEntryToString(entry.defaultExport, 'defaultExport'); + exportCode += `export default ${ref};`; + } + + for (const [key, exportEntry] of Object.entries(entry.constExports ?? {})) { + if (!utils.isLegalFunctionName(key)) { + throw new Error(`Exported const cannot have name "${key}", use assign export for that.`); + } + + const ref = this.envEntryToString(exportEntry, key); + exportCode += `export const ${key} = ${ref}`; + } + + return { + text: [this.importCode, this.environmentCode, exportCode].join('\n'), + }; + } + + private envEntryToString(envEntry: Entry, varName: string): string { + const envVar = this.envEntryToEnvVar.get(envEntry); + if (envVar !== undefined) { + return envVar; + } + + // Complex objects may also be referenced from multiple functions. As such, we have to + // create variables for them in the environment so that all references to them unify to the + // same reference to the env variable. Effectively, we need to do this for any object that + // could be compared for reference-identity. Basic types (strings, numbers, etc.) have + // value semantics and this can be emitted directly into the code where they are used as + // there is no way to observe that you are getting a different copy. + if (isObjOrArrayOrRegExp(envEntry)) { + return this.complexEnvEntryToString(envEntry, varName); + } else { + // Other values (like strings, bools, etc.) can just be emitted inline. + return this.simpleEnvEntryToString(envEntry, varName); + } + } + + private simpleEnvEntryToString(envEntry: Entry, varName: string): string { + switch (envEntry.type) { + case 'json': + return JSON.stringify(envEntry.value); + case 'function': + return this.emitFunctionAndGetName(envEntry); + case 'module': + return this.emitModule(envEntry, varName); + case 'promise': + return `Promise.resolve(${this.envEntryToString(envEntry.value, varName)})`; + case 'expr': + return envEntry.value; + default: + throw new Error('Not a simple entry.'); + } + } + + private complexEnvEntryToString(envEntry: Entry, varName: string): string { + // Call all environment variables __e to make them unique. But suffix + // them with the original name of the property to help provide context when + // looking at the source. + const envVar = this.createEnvVarName(varName, /*addIndexAtEnd:*/ false); + this.envEntryToEnvVar.set(envEntry, envVar); + + switch (envEntry.type) { + case 'object': + this.emitObject(envVar, envEntry, varName); + break; + case 'array': + this.emitArray(envVar, envEntry, varName); + break; + case 'regexp': { + const { source, flags } = envEntry.value; + const regexVal = `new RegExp(${JSON.stringify(source)}, ${JSON.stringify(flags)})`; + const entryString = `var ${envVar} = ${regexVal};\n`; + + this.emitCode(envEntry, entryString); + } + } + + return envVar; + } + + private emitModule(entry: Entry<'module'>, varName: string): string { + const modName = this.createEnvVarName(varName, /* addIndexAtEnd */ true); + + this.envEntryToEnvVar.set(entry, modName); + + const importPrefix = entry.value.type === 'default' ? 'import' : 'import * as'; + + this.importCode += `${importPrefix} ${modName} from '${entry.value.reference}';`; + + return modName; + } + + private emitFunctionAndGetName(entry: Entry<'function'>): string { + // If this is the first time seeing this function, then actually emit the function code for + // it. Otherwise, just return the name of the emitted function for anyone that wants to + // reference it from their own code. + let functionName = this.functionInfoToEnvVar.get(entry.value); + if (!functionName) { + functionName = entry.value.name + ? this.createEnvVarName(entry.value.name, /*addIndexAtEnd:*/ false) + : this.createEnvVarName('f', /*addIndexAtEnd:*/ true); + this.functionInfoToEnvVar.set(entry.value, functionName); + + this.emitFunctionWorker(entry, functionName); + } + + return functionName; + } + + private emitFunctionWorker(entry: Entry<'function'>, varName: string) { + const inspectedFunction = entry.value; + const capturedValues = this.envFromEnvObj(inspectedFunction.capturedValues); + + const thisCapture = capturedValues.this; + const argumentsCapture = capturedValues.arguments; + + capturedValues.this = undefined as unknown as string; + capturedValues.arguments = undefined as unknown as string; + + const parameters = [...Array(inspectedFunction.paramCount)] + .map((_, index) => `__${index}`) + .join(', '); + + // for (const [keyEntry, { entry: valEntry }] of functionInfo.capturedValues) { + // if (keyEntry.type !== 'json' || typeof keyEntry.value !== 'string') { + // throw new Error('Invalid key entry for a captured value.'); + // } + // + // if (valEntry.type === 'module') { + // delete capturedValues[keyEntry.json]; + // } + // } + + let functionText = + 'function ' + + varName + + '(' + + parameters + + ') {\n' + + ' return (function() {\n' + + envObjToString(capturedValues) + + 'return ' + + inspectedFunction.code + + ';\n\n' + + ' }).apply(' + + thisCapture + + ', ' + + argumentsCapture + + ').apply(this, arguments);\n' + + '}\n'; + + // If this function is complex (i.e. non-default __proto__, or has properties, etc.) + // then emit those as well. + this.emitComplexObjectProperties(varName, varName, inspectedFunction); + + if (inspectedFunction.proto !== undefined) { + const protoVar = this.envEntryToString(inspectedFunction.proto, `${varName}_proto`); + functionText += `Object.setPrototypeOf(${varName}, ${protoVar});\n`; + } + + this.emitCode(entry, functionText); + } + + private emitObject(envVar: string, entry: Entry<'object'>, varName: string): void { + const obj = entry.value; + const complex = isComplex(obj); + + if (complex) { + // we have a complex child. Because of the possibility of recursion in + // the object graph, we have to spit out this variable uninitialized first. + // Then we can walk our children, creating a single assignment per child. + // This way, if the child ends up referencing us, we'll have already emitted + // the **initialized** variable for them to reference. + if (obj.proto) { + const protoVar = this.envEntryToString(obj.proto, `${varName}_proto`); + this.emitCode(entry, `var ${envVar} = Object.create(${protoVar});\n`); + } else { + this.emitCode(entry, `var ${envVar} = {};\n`); + } + + this.emitComplexObjectProperties(envVar, varName, obj); + } else { + // All values inside this obj are simple. We can just emit the object + // directly as an object literal with all children embedded in the literal. + const props: string[] = []; + + for (const [keyEntry, { entry: valEntry }] of obj.env) { + const keyName = + keyEntry.type === 'json' && typeof keyEntry.value === 'string' ? keyEntry.value : 'sym'; + const propVal = this.simpleEnvEntryToString(valEntry, keyName); + + if ( + keyEntry.type === 'json' && + typeof keyEntry.value === 'string' && + utils.isLegalMemberName(keyEntry.value) + ) { + props.push(`${keyEntry.value}: ${propVal}`); + } else { + const propName = this.envEntryToString(keyEntry, keyName); + props.push(`[${propName}]: ${propVal}`); + } + } + + const allProps = props.join(', '); + const entryString = `var ${envVar} = {${allProps}};\n`; + this.emitCode(entry, entryString); + } + } + + private emitComplexObjectProperties( + envVar: string, + varName: string, + inspectedObj: InspectedObject + ): void { + let entriesCode = ''; + + for (const [keyEntry, { info, entry: valEntry }] of inspectedObj.env) { + const subName = + keyEntry.type === 'json' && typeof keyEntry.value === 'string' ? keyEntry.value : 'sym'; + + const valString = this.envEntryToString(valEntry, varName + '_' + subName); + + if (isSimplePropertyInfo(info)) { + // normal property. Just emit simply as a direct assignment. + if ( + keyEntry.type === 'json' && + typeof keyEntry.value === 'string' && + utils.isLegalMemberName(keyEntry.value) + ) { + entriesCode += `${envVar}.${keyEntry.value} = ${valString};\n`; + } else { + const keyString = this.envEntryToString(keyEntry, varName + '_' + subName); + entriesCode += `${envVar}[${keyString}] = ${valString};\n`; + } + } else { + const keyString = this.envEntryToString(keyEntry, varName + '_' + subName); + // Complex property, emit as Object.defineProperty + entriesCode += this.generateDefineProperty({ + parentName: envVar, + varName: varName, + desc: info!, + entryValue: valString, + propName: keyString, + }); + } + } + + this.emitCode(inspectedObj, entriesCode); + } + + private generateDefineProperty(options: { + parentName: string; + varName: string; + desc: PropertyInfo; + entryValue: string; + propName: string; + }): string { + const { parentName, varName, desc, entryValue, propName } = options; + const copy: any = {}; + if (desc.configurable) { + copy.configurable = desc.configurable; + } + if (desc.enumerable) { + copy.enumerable = desc.enumerable; + } + if (desc.writable) { + copy.writable = desc.writable; + } + if (desc.get) { + copy.get = this.envEntryToString(desc.get, `${varName}_get`); + } + if (desc.set) { + copy.set = this.envEntryToString(desc.set, `${varName}_set`); + } + if (desc.hasValue) { + copy.value = entryValue; + } + + return `Object.defineProperty(${parentName}, ${propName}, ${envObjToString(copy)});\n`; + } + + private emitArray(envVar: string, entry: Entry<'array'>, varName: string): void { + const arr = entry.value; + if (arr.some(deepContainsObjOrArrayOrRegExp) || isSparse(arr) || hasNonNumericIndices(arr)) { + // We have a complex child. Because of the possibility of recursion in the object + // graph, we have to spit out this variable initialized (but empty) first. Then we can + // walk our children, knowing we'll be able to find this variable if they reference it. + let emitCode = `var ${envVar} = [];\n`; + + // Walk the names of the array properties directly. This ensures we work efficiently + // with sparse arrays. i.e. if the array has length 1k, but only has one value in it + // set, we can just set that value, instead of setting 999 undefineds. + for (const key of Object.getOwnPropertyNames(arr)) { + if (key !== 'length') { + const entryString = this.envEntryToString(arr[key as any], `${varName}_${key}`); + emitCode += `${envVar}${isNumeric(key) ? `[${key}]` : `.${key}`} = ${entryString};\n`; + } + } + + this.emitCode(entry, emitCode); + } else { + // All values inside this array are simple. We can just emit the array elements in + // place. i.e. we can emit as ``var arr = [1, 2, 3]`` as that's far more preferred than + // having four individual statements to do the same. + const strings: string[] = []; + for (let i = 0, n = arr.length; i < n; i++) { + strings.push(this.simpleEnvEntryToString(arr[i], `${varName}_${i}`)); + } + + this.emitCode(entry, `var ${envVar} = [${strings.join(', ')}];\n`); + } + } + + private createEnvVarName(baseName: string, addIndexAtEnd: boolean): string { + const trimLeadingUnderscoreRegex = /^_*/g; + const legalName = makeLegalJSName(baseName).replace(trimLeadingUnderscoreRegex, ''); + let index = 0; + + let currentName = addIndexAtEnd ? '__' + legalName + index : '__' + legalName; + while (this.envVarNames.has(currentName)) { + currentName = addIndexAtEnd ? '__' + legalName + index : '__' + index + '_' + legalName; + index++; + } + + this.envVarNames.add(currentName); + return currentName; + } + + private envFromEnvObj(env: PropertyMap): Record { + const envObj: Record = {}; + + for (const [keyEntry, { entry: valEntry }] of env) { + if (keyEntry.type !== 'json' && typeof keyEntry.value !== 'string') { + throw new Error('PropertyMap key was not a string.'); + } + + envObj[keyEntry.value] = this.envEntryToString(valEntry, keyEntry.value); + } + + return envObj; + } + + private emitCode(entry: Entry | InspectedObject, code: string): void { + if (this.emittedTargets.has(entry)) { + // Sanity check + throw new Error('Code emitted twice for the same entry'); + } + + this.emittedTargets.add(entry); + + if (code) { + this.environmentCode += code; + } + } +} + +const makeLegalRegex = /[^0-9a-zA-Z_]/g; +function makeLegalJSName(n: string) { + return n.replace(makeLegalRegex, (_) => ''); +} + +function isSparse(arr: Array) { + // getOwnPropertyNames for an array returns all the indices as well as 'length'. + // so we subtract one to get all the real indices. If that's not the same as + // the array length, then we must have missing properties and are thus sparse. + return arr.length !== Object.getOwnPropertyNames(arr).length - 1; +} + +function hasNonNumericIndices(arr: Array) { + return Object.keys(arr).some((k) => k !== 'length' && !isNumeric(k)); +} + +function isNumeric(n: string) { + return !isNaN(parseFloat(n)) && isFinite(+n); +} + +function isObjOrArrayOrRegExp(env: Entry): boolean { + switch (env.type) { + case 'object': + case 'array': + case 'regexp': + return true; + default: + return false; + } +} + +function isComplex(obj: InspectedObject) { + if (obj.proto !== undefined) { + return true; + } + + for (const v of obj.env.values()) { + if (entryIsComplex(v)) { + return true; + } + } + + return false; +} + +function entryIsComplex(v: PropertyInfoAndValue) { + return !isSimplePropertyInfo(v.info) || deepContainsObjOrArrayOrRegExp(v.entry); +} + +function isSimplePropertyInfo(info?: PropertyInfo): boolean { + if (!info) { + return true; + } + + return ( + info.enumerable === true && + info.writable === true && + info.configurable === true && + !info.get && + !info.set + ); +} + +function deepContainsObjOrArrayOrRegExp(env: Entry): boolean { + return ( + isObjOrArrayOrRegExp(env) || + (env.type === 'promise' && deepContainsObjOrArrayOrRegExp(env.value)) + ); +} + +/** + * Converts an environment object into a string which can be embedded into a serialized function + * body. Note that this is not JSON serialization, as we may have property values which are + * variable references to other global functions. In other words, there can be free variables in the + * resulting object literal. + * + * @param envObj The environment object to convert to a string. + */ +function envObjToString(envObj: Record): string { + const entries = Object.entries(envObj) + .filter(([_, v]) => !!v) + .map(([k, v]) => `const ${k} = ${v};`) + .join('\n'); + + if (entries) { + return entries + '\n'; + } + + return ''; +} diff --git a/packages/inline-mod/src/closure/types.ts b/packages/inline-mod/src/closure/types.ts new file mode 100644 index 00000000..e959d096 --- /dev/null +++ b/packages/inline-mod/src/closure/types.ts @@ -0,0 +1,80 @@ +/* + * Based on: + * https://github.com/pulumi/pulumi/blob/035a502d86403d815059615a9c047ccccc2cbdd5/sdk/nodejs/runtime/closure/createClosure.ts#L29-L94 + */ + +import type { Entry } from './entry.js'; + +/** @internal */ +export interface InspectedObject { + // information about the prototype of this object/function. If this is an object, we only store + // this if the object's prototype is not Object.prototype. If this is a function, we only store + // this if the function's prototype is not Function.prototype. + proto?: Entry; + + // information about the properties of the object. We store all properties of the object, + // regardless of whether they have string or symbol names. + env: PropertyMap; +} + +// Information about a javascript function. Note that this derives from ObjectInfo as all functions +// are objects in JS, and thus can have their own proto and properties. +/** @internal */ +export interface InspectedFunction extends InspectedObject { + // a serialization of the function's source code as text. + code: string; + + // the captured lexical environment of names to values, if any. + capturedValues: PropertyMap; + + // Whether or not the real 'this' (i.e. not a lexically captured this) is used in the function. + usesNonLexicalThis: boolean; + + // name that the function was declared with. used only for trying to emit a better + // name into the serialized code for it. + name: string | undefined; + + // Number of parameters this function is declared to take. Used to generate a serialized + // function with the same number of parameters. This is valuable as some 3rd party libraries + // (like senchalabs: https://github.com/senchalabs/connect/blob/fa8916e6350e01262e86ccee82f490c65e04c728/index.js#L232-L241) + // will introspect function param count to decide what to do. + paramCount: number; +} + +// Similar to PropertyDescriptor. Helps describe an Entry in the case where it is not +// simple. +/** @internal */ +export interface PropertyInfo { + // If the property has a value we should directly provide when calling .defineProperty + hasValue: boolean; + + // same as PropertyDescriptor + configurable?: boolean; + enumerable?: boolean; + writable?: boolean; + + // The entries we've made for custom getters/setters if the property is defined that + // way. + get?: Entry; + set?: Entry; +} + +// Information about a property. Specifically the actual entry containing the data about it and +// then an optional PropertyInfo in the case that this isn't just a common property. +/** @internal */ +export interface PropertyInfoAndValue { + info?: PropertyInfo; + entry: Entry; +} + +// A mapping between the name of a property (symbolic or string) to information about the +// value for that property. +/** @internal */ +export type PropertyMap = Map; + +/** @internal */ +export namespace InspectedFunction { + export function isSimple(info: InspectedFunction) { + return info.capturedValues.size === 0 && info.env.size === 0 && !info.proto; + } +} diff --git a/packages/inline-mod/src/closure/utils.ts b/packages/inline-mod/src/closure/utils.ts new file mode 100644 index 00000000..7fb4c3f7 --- /dev/null +++ b/packages/inline-mod/src/closure/utils.ts @@ -0,0 +1,52 @@ +// Copyright 2016-2022, Pulumi Corporation. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import ts from 'typescript'; + +const legalNameRegex = /^[a-zA-Z_][0-9a-zA-Z_]*$/; + +/** @internal */ +export function isLegalMemberName(n: string) { + return legalNameRegex.test(n); +} + +/** @internal */ +export function isLegalFunctionName(n: string) { + if (!isLegalMemberName(n)) { + return false; + } + const scanner = ts.createScanner( + ts.ScriptTarget.Latest, + /*skipTrivia:*/ false, + ts.LanguageVariant.Standard, + n + ); + const tokenKind = scanner.scan(); + if (tokenKind !== ts.SyntaxKind.Identifier && tokenKind !== ts.SyntaxKind.ConstructorKeyword) { + return false; + } + + return true; +} + +/** @internal */ +export function hasTrueBooleanMember(obj: any, memberName: string | number | symbol): boolean { + if (obj === undefined || obj === null) { + return false; + } + + const val: unknown = obj[memberName]; + + return typeof val === 'boolean' && val === true; +} diff --git a/packages/inline-mod/src/closure/v8.ts b/packages/inline-mod/src/closure/v8.ts new file mode 100644 index 00000000..185f3d0a --- /dev/null +++ b/packages/inline-mod/src/closure/v8.ts @@ -0,0 +1,316 @@ +// Copyright 2016-2018, Pulumi Corporation. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// This file provides a low-level interface to a few V8 runtime objects. We will use this low-level +// interface when serializing closures to walk the scope chain and find the value of free variables +// captured by closures, as well as getting source-level debug information so that we can present +// high-quality error messages. +// +// As a side-effect of importing this file, we must enable the --allow-natives-syntax V8 flag. This +// is because we are using V8 intrinsics in order to implement this module. +import * as v8 from 'v8'; +v8.setFlagsFromString('--allow-natives-syntax'); + +import type * as inspector from 'node:inspector'; +import * as util from 'node:util'; +import * as vm from 'node:vm'; +import * as v8Hooks from './v8Hooks.js'; + +/** + * Given a function, returns the file, line and column number in the file where this function was + * defined. Returns { "", 0, 0 } if the location cannot be found or if the given function has no Script. + * @internal + */ +export async function getFunctionLocation(func: Function) { + // First, find the runtime's internal id for this function. + const functionId = await getRuntimeIdForFunction(func); + + // Now, query for the internal properties the runtime sets up for it. + const { internalProperties } = await runtimeGetProperties(functionId, /*ownProperties:*/ false); + + // There should normally be an internal property called [[FunctionLocation]]: + // https://chromium.googlesource.com/v8/v8.git/+/3f99afc93c9ba1ba5df19f123b93cc3079893c9b/src/inspector/v8-debugger.cc#793 + const functionLocation = internalProperties.find((p) => p.name === '[[FunctionLocation]]'); + if (!functionLocation || !functionLocation.value || !functionLocation.value.value) { + return { file: '', line: 0, column: 0 }; + } + + const value = functionLocation.value.value; + + // Map from the scriptId the value has to a file-url. + const file = v8Hooks.getScriptUrl(value.scriptId) || ''; + const line = value.lineNumber || 0; + const column = value.columnNumber || 0; + + return { file, line, column }; +} + +/** + * Given a function and a free variable name, lookupCapturedVariableValue looks up the value of that free variable + * in the scope chain of the provided function. If the free variable is not found, `throwOnFailure` indicates + * whether or not this function should throw or return `undefined. + * + * @param func The function whose scope chain is to be analyzed + * @param freeVariable The name of the free variable to inspect + * @param throwOnFailure If true, throws if the free variable can't be found. + * @returns The value of the free variable. If `throwOnFailure` is false, returns `undefined` if not found. + * @internal + */ +export async function lookupCapturedVariableValue( + func: Function, + freeVariable: string, + throwOnFailure: boolean +): Promise { + // First, find the runtime's internal id for this function. + const functionId = await getRuntimeIdForFunction(func); + + // Now, query for the internal properties the runtime sets up for it. + const { internalProperties } = await runtimeGetProperties(functionId, /*ownProperties:*/ false); + + // There should normally be an internal property called [[Scopes]]: + // https://chromium.googlesource.com/v8/v8.git/+/3f99afc93c9ba1ba5df19f123b93cc3079893c9b/src/inspector/v8-debugger.cc#820 + const scopes = internalProperties.find((p) => p.name === '[[Scopes]]'); + if (!scopes) { + throw new Error('Could not find [[Scopes]] property'); + } + + if (!scopes.value) { + throw new Error('[[Scopes]] property did not have [value]'); + } + + if (!scopes.value.objectId) { + throw new Error('[[Scopes]].value have objectId'); + } + + // This is sneaky, but we can actually map back from the [[Scopes]] object to a real in-memory + // v8 array-like value. Note: this isn't actually a real array. For example, it cannot be + // iterated. Nor can any actual methods be called on it. However, we can directly index into + // it, and we can. Similarly, the 'object' type it optionally points at is not a true JS + // object. So we can't call things like .hasOwnProperty on it. However, the values pointed to + // by 'object' are the real in-memory JS objects we are looking for. So we can find and return + // those successfully to our caller. + const scopesArray: { object?: Record }[] = await getValueForObjectId( + scopes.value.objectId + ); + + // scopesArray is ordered from innermost to outermost. + for (let i = 0, n = scopesArray.length; i < n; i++) { + const scope = scopesArray[i]; + if (scope.object) { + if (freeVariable in scope.object) { + const val = scope.object[freeVariable]; + return val; + } + } + } + + if (throwOnFailure) { + throw new Error('Unexpected missing variable in closure environment: ' + freeVariable); + } + + return undefined; +} + +// We want to call util.promisify on inspector.Session.post. However, due to all the overloads of +// that method, promisify gets confused. To prevent this, we cast our session object down to an +// interface containing only the single overload we care about. +type PostSession = { + post( + method: TMethod, + params?: TParams, + callback?: (err: Error | null, params: TReturn) => void + ): void; +}; + +type EvaluationSession = PostSession< + 'Runtime.evaluate', + inspector.Runtime.EvaluateParameterType, + inspector.Runtime.EvaluateReturnType +>; +type GetPropertiesSession = PostSession< + 'Runtime.getProperties', + inspector.Runtime.GetPropertiesParameterType, + inspector.Runtime.GetPropertiesReturnType +>; +type CallFunctionSession = PostSession< + 'Runtime.callFunctionOn', + inspector.Runtime.CallFunctionOnParameterType, + inspector.Runtime.CallFunctionOnReturnType +>; +type ContextSession = { + post(method: 'Runtime.disable' | 'Runtime.enable', callback?: (err: Error | null) => void): void; + once( + event: 'Runtime.executionContextCreated', + listener: ( + message: inspector.InspectorNotification + ) => void + ): void; +}; + +type InflightContext = { + contextId: number; + functions: Record; + currentFunctionId: number; + calls: Record; + currentCallId: number; +}; +// Isolated singleton context accessible from the inspector. +// Used instead of `global` object to support executions with multiple V8 vm contexts as, e.g., done by Jest. +let inflightContextCache: Promise | undefined; +function inflightContext() { + if (inflightContextCache) { + return inflightContextCache; + } + inflightContextCache = createContext(); + return inflightContextCache; +} +async function createContext(): Promise { + const context: InflightContext = { + contextId: 0, + functions: {}, + currentFunctionId: 0, + calls: {}, + currentCallId: 0, + }; + const session = v8Hooks.getSession() as ContextSession; + const post = util.promisify(session.post); + + // Create own context with known context id and functionsContext as `global` + await post.call(session, 'Runtime.enable'); + const contextIdAsync = new Promise((resolve) => { + session.once('Runtime.executionContextCreated', (event) => { + resolve(event.params.context.id); + }); + }); + vm.createContext(context); + context.contextId = await contextIdAsync; + await post.call(session, 'Runtime.disable'); + + return context; +} + +async function getRuntimeIdForFunction(func: Function): Promise { + // In order to get information about an object, we need to put it in a well known location so + // that we can call Runtime.evaluate and find it. To do this, we use a special map on the + // 'global' object of a vm context only used for this purpose, and map from a unique-id to that + // object. We then call Runtime.evaluate with an expression that then points to that unique-id + // in that global object. The runtime will then find the object and give us back an internal id + // for it. We can then query for information about the object through that internal id. + // + // Note: the reason for the mapping object and the unique-id we create is so that we don't run + // into any issues when being called asynchronously. We don't want to place the object in a + // location that might be overwritten by another call while we're asynchronously waiting for our + // original call to complete. + + const session = v8Hooks.getSession() as EvaluationSession; + const post = util.promisify(session.post); + + // Place the function in a unique location + const context = await inflightContext(); + const currentFunctionName = 'id' + context.currentFunctionId++; + context.functions[currentFunctionName] = func; + const contextId = context.contextId; + const expression = `functions.${currentFunctionName}`; + + try { + const retType = await post.call(session, 'Runtime.evaluate', { contextId, expression }); + + if (retType.exceptionDetails) { + throw new Error( + `Error calling "Runtime.evaluate(${expression})" on context ${contextId}: ` + + retType.exceptionDetails.text + ); + } + + const remoteObject = retType.result; + if (remoteObject.type !== 'function') { + throw new Error("Remote object was not 'function': " + JSON.stringify(remoteObject)); + } + + if (!remoteObject.objectId) { + throw new Error("Remote function does not have 'objectId': " + JSON.stringify(remoteObject)); + } + + return remoteObject.objectId; + } finally { + delete context.functions[currentFunctionName]; + } +} + +async function runtimeGetProperties( + objectId: inspector.Runtime.RemoteObjectId, + ownProperties: boolean | undefined +) { + const session = v8Hooks.getSession() as GetPropertiesSession; + const post = util.promisify(session.post); + + const retType = await post.call(session, 'Runtime.getProperties', { objectId, ownProperties }); + + if (retType.exceptionDetails) { + throw new Error( + `Error calling "Runtime.getProperties(${objectId}, ${ownProperties})": ` + + retType.exceptionDetails.text + ); + } + + return { internalProperties: retType.internalProperties || [], properties: retType.result }; +} + +async function getValueForObjectId(objectId: inspector.Runtime.RemoteObjectId): Promise { + // In order to get the raw JS value for the *remote wrapper* of the [[Scopes]] array, we use + // Runtime.callFunctionOn on it passing in a fresh function-declaration. The Node runtime will + // then compile that function, invoking it with the 'real' underlying scopes-array value in + // memory as the bound 'this' value. Inside that function declaration, we can then access + // 'this' and assign it to a unique-id in a well known mapping table we have set up. As above, + // the unique-id is to prevent any issues with multiple in-flight asynchronous calls. + + const session = v8Hooks.getSession() as CallFunctionSession; + const post = util.promisify(session.post); + const context = await inflightContext(); + + // Get an id for an unused location in the global table. + const tableId = 'id' + context.currentCallId++; + + // Now, ask the runtime to call a fictitious method on the scopes-array object. When it + // does, it will get the actual underlying value for the scopes array and bind it to the + // 'this' value inside the function. Inside the function we then just grab 'this' and + // stash it in our global table. After this completes, we'll then have access to it. + + // This cast will become unnecessary when we move to TS 3.1.6 or above. In that version they + // support typesafe '.call' calls. + const retType = await post.call(session, 'Runtime.callFunctionOn', { + objectId, + functionDeclaration: `function () { + calls["${tableId}"] = this; + }`, + }); + + if (retType.exceptionDetails) { + throw new Error( + `Error calling "Runtime.callFunction(${objectId})": ` + retType.exceptionDetails.text + ); + } + + if (!context.calls.hasOwnProperty(tableId)) { + throw new Error( + `Value was not stored into table after calling "Runtime.callFunctionOn(${objectId})"` + ); + } + + // Extract value and clear our table entry. + const val = context.calls[tableId]; + delete context.calls[tableId]; + + return val; +} diff --git a/packages/inline-mod/src/closure/v8Hooks.ts b/packages/inline-mod/src/closure/v8Hooks.ts new file mode 100644 index 00000000..108dbc83 --- /dev/null +++ b/packages/inline-mod/src/closure/v8Hooks.ts @@ -0,0 +1,63 @@ +// Copyright 2016-2018, Pulumi Corporation. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Module that hooks into v8 and provides information about it to interested parties. Because this +// hooks into v8 events it is critical that this module is loaded early when the process starts. +// Otherwise, information may not be known when needed. This module is only intended for use on +// Node v11 and higher. + +import * as inspector from 'inspector'; +import * as v8 from 'node:v8'; +v8.setFlagsFromString('--allow-natives-syntax'); + +const scriptIdToUrlMap = new Map(); + +async function createInspectorSession(): Promise { + const inspectorSession = new inspector.Session(); + inspectorSession.connect(); + + // Enable debugging support so we can hear about the Debugger.scriptParsed event. We need that + // event to know how to map from scriptId's to file-urls. + await new Promise((resolve, reject) => { + inspectorSession.post('Debugger.enable', (err, res) => (err ? reject(err) : resolve(res))); + }); + + inspectorSession.addListener('Debugger.scriptParsed', (event) => { + const { scriptId, url } = event.params; + + // TODO: Test how this behaves on the dev server + // it might need to be cleared on reloads. + scriptIdToUrlMap.set(scriptId, url); + }); + + return inspectorSession; +} + +const session = await createInspectorSession(); + +/** + * Returns the inspector session that can be used to query the state of this running Node instance. + * @internal + */ +export function getSession() { + return session; +} + +/** + * Maps from a script-id to the local file url it corresponds to. + * @internal + */ +export function getScriptUrl(id: inspector.Runtime.ScriptId) { + return scriptIdToUrlMap.get(id); +} diff --git a/packages/inline-mod/src/inlining.ts b/packages/inline-mod/src/inlining.ts new file mode 100644 index 00000000..db31c80d --- /dev/null +++ b/packages/inline-mod/src/inlining.ts @@ -0,0 +1,59 @@ +import { getRandomValues } from 'node:crypto'; +import type { Entry } from './closure/entry.js'; +import { getInspector } from './closure/inspectCode.js'; +import { serializeModule, type ModEntry, type SerializedModule } from './closure/serialization.js'; +import { modRegistry } from './state.js'; + +type ModuleExports = + | { + constExports?: Record; + defaultExport?: unknown; + assignExport?: never; + } + | { + constExports?: never; + defaultExport?: never; + assignExport: unknown; + }; + +type ModuleOptions = ModuleExports & { + serializeFn?: (val: unknown) => boolean; + modName?: string; +}; + +const idBuffer = Buffer.alloc(24); + +export function inlineMod(options: ModuleOptions): string { + const moduleId = + options.modName ?? `inox:inline-mod:${getRandomValues(idBuffer).toString('hex')}`; + + modRegistry.set(moduleId, inspectInlineMod(options)); + + return moduleId; +} + +async function inspectInlineMod(options: ModuleOptions): Promise { + const inspector = getInspector(options.serializeFn); + + const maybeInspect = (val: unknown): Promise | undefined => { + if (val === undefined) { + return; + } + + return inspector.inspect(val); + }; + + const modEntry: ModEntry = { + constExports: Object.fromEntries( + await Promise.all( + Object.entries(options.constExports ?? {}).map( + async ([key, value]) => [key, await inspector.inspect(value)] as const + ) + ) + ), + defaultExport: await maybeInspect(options.defaultExport), + assignExport: await maybeInspect(options.assignExport), + }; + + return serializeModule(modEntry); +} diff --git a/packages/inline-mod/src/state.ts b/packages/inline-mod/src/state.ts new file mode 100644 index 00000000..4fcbebc4 --- /dev/null +++ b/packages/inline-mod/src/state.ts @@ -0,0 +1,4 @@ +import type { SerializedModule } from './closure/serialization.js'; + +/** @internal */ +export const modRegistry = new Map>(); diff --git a/packages/inline-mod/src/vite.ts b/packages/inline-mod/src/vite.ts new file mode 100644 index 00000000..e18e0099 --- /dev/null +++ b/packages/inline-mod/src/vite.ts @@ -0,0 +1,33 @@ +import type { Plugin } from 'vite'; +import { modRegistry } from './state.js'; + +export { inlineMod } from './inlining.js'; + +export type Options = Record; + +export default function inlineModPlugin(_options: Options = {}): Plugin { + return { + name: '@inox-tools/inline-mod', + resolveId(id) { + if (modRegistry.has(id)) { + return '\0' + id; + } + return null; + }, + async load(id) { + if (!id.startsWith('\0')) { + return null; + } + + const ref = id.slice(1); + + if (!modRegistry.has(ref)) { + return null; + } + + const serializedModule = await modRegistry.get(ref)!; + + return serializedModule.text; + }, + }; +} diff --git a/packages/inline-mod/tsconfig.json b/packages/inline-mod/tsconfig.json new file mode 100644 index 00000000..bfc6df51 --- /dev/null +++ b/packages/inline-mod/tsconfig.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "extends": "../../tsconfig.base.json" +} diff --git a/packages/inline-mod/tsup.config.ts b/packages/inline-mod/tsup.config.ts new file mode 100644 index 00000000..0c40292b --- /dev/null +++ b/packages/inline-mod/tsup.config.ts @@ -0,0 +1,13 @@ +import { defineConfig } from 'tsup'; + +export default defineConfig({ + entry: ['src/vite.ts'], + format: ['esm'], + dts: true, + sourcemap: true, + clean: true, + splitting: true, + minify: true, + external: ['vite', 'typescript'], + tsconfig: 'tsconfig.json', +}); diff --git a/packages/velox-luna/index.ts b/packages/velox-luna/index.ts index 67a6109d..3c7639a3 100644 --- a/packages/velox-luna/index.ts +++ b/packages/velox-luna/index.ts @@ -1 +1,2 @@ +// eslint-disable-next-line no-console console.log('Velox Luna executed'); diff --git a/packages/velox-luna/package.json b/packages/velox-luna/package.json index a16ae7aa..b4a75712 100644 --- a/packages/velox-luna/package.json +++ b/packages/velox-luna/package.json @@ -17,8 +17,8 @@ "index.js.map" ], "scripts": { - "build": "esbuild --bundle index.ts --outfile=index.js --sourcemap", - "test": "pnpm run build && node index.js" + "build": "esbuild --bundle index.ts --outdir=dist --sourcemap", + "test": "pnpm run build && node dist/index.js" }, "dependencies": { "@lunariajs/core": "^0.0.25" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index af61ac00..73daa546 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -54,6 +54,28 @@ importers: specifier: ^5.3.3 version: 5.3.3 + packages/inline-mod: + dependencies: + typescript: + specifier: ^5 + version: 5.3.3 + vite: + specifier: ^3.2.0 + version: 3.2.7(@types/node@20.10.6) + devDependencies: + '@vitest/ui': + specifier: ^1.1.3 + version: 1.1.3(vitest@1.1.3) + tsup: + specifier: ^8.0.1 + version: 8.0.1(typescript@5.3.3) + upath: + specifier: ^2.0.1 + version: 2.0.1 + vitest: + specifier: ^1.1.3 + version: 1.1.3(@types/node@20.10.6)(@vitest/ui@1.1.3) + packages/velox-luna: dependencies: '@lunariajs/core': @@ -68,13 +90,13 @@ importers: version: 5.3.3 vitest: specifier: ^1.1.3 - version: 1.1.3(@types/node@20.10.6) + version: 1.1.3(@types/node@20.10.6)(@vitest/ui@1.1.3) turbo: devDependencies: '@turbo/gen': specifier: ^1.11.3 - version: 1.11.3(@types/node@20.10.6)(typescript@5.3.3) + version: 1.11.3(@types/node@20.11.5)(typescript@5.3.3) '@types/lodash': specifier: ^4.14.202 version: 4.14.202 @@ -360,6 +382,15 @@ packages: dev: true optional: true + /@esbuild/android-arm@0.15.18: + resolution: {integrity: sha512-5GT+kcs2WVGjVs7+boataCkO5Fg0y4kCjzkB5bAip7H4jfnOS3dA6KPiww9W1OEKTKeAcUVhdZGvgI65OXmUnw==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + requiresBuild: true + dev: false + optional: true + /@esbuild/android-arm@0.19.11: resolution: {integrity: sha512-5OVapq0ClabvKvQ58Bws8+wkLCV+Rxg7tUVbo9xu034Nm536QTII4YzhaFriQ7rMrorfnFKUsArD2lqKbFY4vw==} engines: {node: '>=12'} @@ -441,6 +472,15 @@ packages: dev: true optional: true + /@esbuild/linux-loong64@0.15.18: + resolution: {integrity: sha512-L4jVKS82XVhw2nvzLg/19ClLWg0y27ulRwuP7lcyL6AbUWB5aPglXY3M21mauDQMDfRLs8cQmeT03r/+X3cZYQ==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + requiresBuild: true + dev: false + optional: true + /@esbuild/linux-loong64@0.19.11: resolution: {integrity: sha512-ppZSSLVpPrwHccvC6nQVZaSHlFsvCQyjnvirnVjbKSHuE5N24Yl8F3UwYUUR1UEPaFObGD2tSvVKbvR+uT1Nrg==} engines: {node: '>=12'} @@ -606,6 +646,18 @@ packages: resolution: {integrity: sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==} dev: true + /@isaacs/cliui@8.0.2: + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + dependencies: + string-width: 5.1.2 + string-width-cjs: /string-width@4.2.3 + strip-ansi: 7.1.0 + strip-ansi-cjs: /strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: /wrap-ansi@7.0.0 + dev: true + /@jest/schemas@29.6.3: resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -613,11 +665,25 @@ packages: '@sinclair/typebox': 0.27.8 dev: true + /@jridgewell/gen-mapping@0.3.3: + resolution: {integrity: sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==} + engines: {node: '>=6.0.0'} + dependencies: + '@jridgewell/set-array': 1.1.2 + '@jridgewell/sourcemap-codec': 1.4.15 + '@jridgewell/trace-mapping': 0.3.9 + dev: true + /@jridgewell/resolve-uri@3.1.1: resolution: {integrity: sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==} engines: {node: '>=6.0.0'} dev: true + /@jridgewell/set-array@1.1.2: + resolution: {integrity: sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==} + engines: {node: '>=6.0.0'} + dev: true + /@jridgewell/sourcemap-codec@1.4.15: resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} dev: true @@ -744,11 +810,22 @@ packages: parse5: 7.1.2 dev: false + /@pkgjs/parseargs@0.11.0: + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + requiresBuild: true + dev: true + optional: true + /@pkgr/core@0.1.0: resolution: {integrity: sha512-Zwq5OCzuwJC2jwqmpEQt7Ds1DTi6BWSwoGkbb1n9pO3hzb35BoJELx7c0T23iDkBGkh2e7tvOtjF3tr3OaQHDQ==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} dev: true + /@polka/url@1.0.0-next.24: + resolution: {integrity: sha512-2LuNTFBIO0m7kKIQvvPHN6UE63VjpmL9rnEEaOOaiSPbZK+zUOYIzBAWcED+3XYzhYsd/0mD57VdxAEqqV52CQ==} + dev: true + /@rollup/rollup-android-arm-eabi@4.9.4: resolution: {integrity: sha512-ub/SN3yWqIv5CWiAZPHVS1DloyZsJbtXmX4HxUTIpS0BHm9pW5iYBo2mIZi+hE3AeiTzHz33blwSnhdUo+9NpA==} cpu: [arm] @@ -886,7 +963,7 @@ packages: resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} dev: true - /@turbo/gen@1.11.3(@types/node@20.10.6)(typescript@5.3.3): + /@turbo/gen@1.11.3(@types/node@20.11.5)(typescript@5.3.3): resolution: {integrity: sha512-cHGRj7Jn7Hw1cA7NuwWYfYdhEliQX4LuSfEB9L1m8ifGkHalU3bbYXcehzLThmckpGpUQGnXYx0UtVudbQ42HA==} hasBin: true dependencies: @@ -898,7 +975,7 @@ packages: minimatch: 9.0.3 node-plop: 0.26.3 proxy-agent: 6.3.1 - ts-node: 10.9.2(@types/node@20.10.6)(typescript@5.3.3) + ts-node: 10.9.2(@types/node@20.11.5)(typescript@5.3.3) update-check: 1.5.4 validate-npm-package-name: 5.0.0 transitivePeerDependencies: @@ -985,6 +1062,11 @@ packages: resolution: {integrity: sha512-Vac8H+NlRNNlAmDfGUP7b5h/KA+AtWIzuXy0E6OyP8f1tCLYAtPvKRRDJjAPqhpCb0t6U2j7/xqAuLEebW2kiw==} dependencies: undici-types: 5.26.5 + + /@types/node@20.11.5: + resolution: {integrity: sha512-g557vgQjUUfN76MZAN/dt1z3dzcUsimuysco0KeluHgrPdJXkP/XdAURgyO2W9fZWHRtRBiVKzKn8vyOAwlG+w==} + dependencies: + undici-types: 5.26.5 dev: true /@types/normalize-package-data@2.4.4: @@ -1186,6 +1268,21 @@ packages: tinyspy: 2.2.0 dev: true + /@vitest/ui@1.1.3(vitest@1.1.3): + resolution: {integrity: sha512-JKGgftXZgTtK7kfQNicE9Q2FuiUlYvCGyUENkA2/S1VBThtfQyGUwaJmiDFVAKBOrW305cNgjP67vsxMm9/SDQ==} + peerDependencies: + vitest: ^1.0.0 + dependencies: + '@vitest/utils': 1.1.3 + fast-glob: 3.3.2 + fflate: 0.8.1 + flatted: 3.2.9 + pathe: 1.1.1 + picocolors: 1.0.0 + sirv: 2.0.4 + vitest: 1.1.3(@types/node@20.10.6)(@vitest/ui@1.1.3) + dev: true + /@vitest/utils@1.1.3: resolution: {integrity: sha512-Dyt3UMcdElTll2H75vhxfpZu03uFpXRCHxWnzcrFjZxT1kTbq8ALUYIeBgGolo1gldVdI0YSlQRacsqxTwNqwg==} dependencies: @@ -1257,6 +1354,11 @@ packages: engines: {node: '>=8'} dev: true + /ansi-regex@6.0.1: + resolution: {integrity: sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==} + engines: {node: '>=12'} + dev: true + /ansi-styles@3.2.1: resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} engines: {node: '>=4'} @@ -1276,6 +1378,23 @@ packages: engines: {node: '>=10'} dev: true + /ansi-styles@6.2.1: + resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} + engines: {node: '>=12'} + dev: true + + /any-promise@1.3.0: + resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} + dev: true + + /anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + dev: true + /arg@4.1.3: resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} dev: true @@ -1370,6 +1489,11 @@ packages: is-windows: 1.0.2 dev: true + /binary-extensions@2.2.0: + resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==} + engines: {node: '>=8'} + dev: true + /bl@4.1.0: resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} dependencies: @@ -1416,6 +1540,16 @@ packages: semver: 7.5.4 dev: true + /bundle-require@4.0.2(esbuild@0.19.11): + resolution: {integrity: sha512-jwzPOChofl67PSTW2SGubV9HBQAhhR2i6nskiOThauo9dzwDUgOWQScFVaJkjEfYX+UXiD+LEx8EblQMc2wIag==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + peerDependencies: + esbuild: '>=0.17' + dependencies: + esbuild: 0.19.11 + load-tsconfig: 0.2.5 + dev: true + /cac@6.7.14: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'} @@ -1538,6 +1672,21 @@ packages: get-func-name: 2.0.2 dev: true + /chokidar@3.5.3: + resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==} + engines: {node: '>= 8.10.0'} + dependencies: + anymatch: 3.1.3 + braces: 3.0.2 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 + dev: true + /ci-info@3.9.0: resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} engines: {node: '>=8'} @@ -1625,6 +1774,11 @@ packages: resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} dev: true + /commander@4.1.1: + resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} + engines: {node: '>= 6'} + dev: true + /concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} dev: true @@ -1836,6 +1990,10 @@ packages: engines: {node: '>=10'} dev: true + /eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + dev: true + /editorconfig@0.15.3: resolution: {integrity: sha512-M9wIMFx96vq0R4F+gRpY3o2exzb8hEj/n9S8unZtHSvYjibBp/iMufSzvmOcV/laG0ZtuTVGtiJggPOSW2r93g==} hasBin: true @@ -1850,6 +2008,10 @@ packages: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} dev: true + /emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + dev: true + /enhanced-resolve@5.15.0: resolution: {integrity: sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg==} engines: {node: '>=10.13.0'} @@ -1946,6 +2108,216 @@ packages: is-symbol: 1.0.4 dev: true + /esbuild-android-64@0.15.18: + resolution: {integrity: sha512-wnpt3OXRhcjfIDSZu9bnzT4/TNTDsOUvip0foZOUBG7QbSt//w3QV4FInVJxNhKc/ErhUxc5z4QjHtMi7/TbgA==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + requiresBuild: true + dev: false + optional: true + + /esbuild-android-arm64@0.15.18: + resolution: {integrity: sha512-G4xu89B8FCzav9XU8EjsXacCKSG2FT7wW9J6hOc18soEHJdtWu03L3TQDGf0geNxfLTtxENKBzMSq9LlbjS8OQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + requiresBuild: true + dev: false + optional: true + + /esbuild-darwin-64@0.15.18: + resolution: {integrity: sha512-2WAvs95uPnVJPuYKP0Eqx+Dl/jaYseZEUUT1sjg97TJa4oBtbAKnPnl3b5M9l51/nbx7+QAEtuummJZW0sBEmg==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: false + optional: true + + /esbuild-darwin-arm64@0.15.18: + resolution: {integrity: sha512-tKPSxcTJ5OmNb1btVikATJ8NftlyNlc8BVNtyT/UAr62JFOhwHlnoPrhYWz09akBLHI9nElFVfWSTSRsrZiDUA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: false + optional: true + + /esbuild-freebsd-64@0.15.18: + resolution: {integrity: sha512-TT3uBUxkteAjR1QbsmvSsjpKjOX6UkCstr8nMr+q7zi3NuZ1oIpa8U41Y8I8dJH2fJgdC3Dj3CXO5biLQpfdZA==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + requiresBuild: true + dev: false + optional: true + + /esbuild-freebsd-arm64@0.15.18: + resolution: {integrity: sha512-R/oVr+X3Tkh+S0+tL41wRMbdWtpWB8hEAMsOXDumSSa6qJR89U0S/PpLXrGF7Wk/JykfpWNokERUpCeHDl47wA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + requiresBuild: true + dev: false + optional: true + + /esbuild-linux-32@0.15.18: + resolution: {integrity: sha512-lphF3HiCSYtaa9p1DtXndiQEeQDKPl9eN/XNoBf2amEghugNuqXNZA/ZovthNE2aa4EN43WroO0B85xVSjYkbg==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /esbuild-linux-64@0.15.18: + resolution: {integrity: sha512-hNSeP97IviD7oxLKFuii5sDPJ+QHeiFTFLoLm7NZQligur8poNOWGIgpQ7Qf8Balb69hptMZzyOBIPtY09GZYw==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /esbuild-linux-arm64@0.15.18: + resolution: {integrity: sha512-54qr8kg/6ilcxd+0V3h9rjT4qmjc0CccMVWrjOEM/pEcUzt8X62HfBSeZfT2ECpM7104mk4yfQXkosY8Quptug==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /esbuild-linux-arm@0.15.18: + resolution: {integrity: sha512-UH779gstRblS4aoS2qpMl3wjg7U0j+ygu3GjIeTonCcN79ZvpPee12Qun3vcdxX+37O5LFxz39XeW2I9bybMVA==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /esbuild-linux-mips64le@0.15.18: + resolution: {integrity: sha512-Mk6Ppwzzz3YbMl/ZZL2P0q1tnYqh/trYZ1VfNP47C31yT0K8t9s7Z077QrDA/guU60tGNp2GOwCQnp+DYv7bxQ==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /esbuild-linux-ppc64le@0.15.18: + resolution: {integrity: sha512-b0XkN4pL9WUulPTa/VKHx2wLCgvIAbgwABGnKMY19WhKZPT+8BxhZdqz6EgkqCLld7X5qiCY2F/bfpUUlnFZ9w==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /esbuild-linux-riscv64@0.15.18: + resolution: {integrity: sha512-ba2COaoF5wL6VLZWn04k+ACZjZ6NYniMSQStodFKH/Pu6RxzQqzsmjR1t9QC89VYJxBeyVPTaHuBMCejl3O/xg==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /esbuild-linux-s390x@0.15.18: + resolution: {integrity: sha512-VbpGuXEl5FCs1wDVp93O8UIzl3ZrglgnSQ+Hu79g7hZu6te6/YHgVJxCM2SqfIila0J3k0csfnf8VD2W7u2kzQ==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /esbuild-netbsd-64@0.15.18: + resolution: {integrity: sha512-98ukeCdvdX7wr1vUYQzKo4kQ0N2p27H7I11maINv73fVEXt2kyh4K4m9f35U1K43Xc2QGXlzAw0K9yoU7JUjOg==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + requiresBuild: true + dev: false + optional: true + + /esbuild-openbsd-64@0.15.18: + resolution: {integrity: sha512-yK5NCcH31Uae076AyQAXeJzt/vxIo9+omZRKj1pauhk3ITuADzuOx5N2fdHrAKPxN+zH3w96uFKlY7yIn490xQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + requiresBuild: true + dev: false + optional: true + + /esbuild-sunos-64@0.15.18: + resolution: {integrity: sha512-On22LLFlBeLNj/YF3FT+cXcyKPEI263nflYlAhz5crxtp3yRG1Ugfr7ITyxmCmjm4vbN/dGrb/B7w7U8yJR9yw==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + requiresBuild: true + dev: false + optional: true + + /esbuild-windows-32@0.15.18: + resolution: {integrity: sha512-o+eyLu2MjVny/nt+E0uPnBxYuJHBvho8vWsC2lV61A7wwTWC3jkN2w36jtA+yv1UgYkHRihPuQsL23hsCYGcOQ==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + requiresBuild: true + dev: false + optional: true + + /esbuild-windows-64@0.15.18: + resolution: {integrity: sha512-qinug1iTTaIIrCorAUjR0fcBk24fjzEedFYhhispP8Oc7SFvs+XeW3YpAKiKp8dRpizl4YYAhxMjlftAMJiaUw==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: false + optional: true + + /esbuild-windows-arm64@0.15.18: + resolution: {integrity: sha512-q9bsYzegpZcLziq0zgUi5KqGVtfhjxGbnksaBFYmWLxeV/S1fK4OLdq2DFYnXcLMjlZw2L0jLsk1eGoB522WXQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + requiresBuild: true + dev: false + optional: true + + /esbuild@0.15.18: + resolution: {integrity: sha512-x/R72SmW3sSFRm5zrrIjAhCeQSAWoni3CmHEqfQrZIQTM3lVCdehdwuIqaOtfC2slvpdlLa62GYoN8SxT23m6Q==} + engines: {node: '>=12'} + hasBin: true + requiresBuild: true + optionalDependencies: + '@esbuild/android-arm': 0.15.18 + '@esbuild/linux-loong64': 0.15.18 + esbuild-android-64: 0.15.18 + esbuild-android-arm64: 0.15.18 + esbuild-darwin-64: 0.15.18 + esbuild-darwin-arm64: 0.15.18 + esbuild-freebsd-64: 0.15.18 + esbuild-freebsd-arm64: 0.15.18 + esbuild-linux-32: 0.15.18 + esbuild-linux-64: 0.15.18 + esbuild-linux-arm: 0.15.18 + esbuild-linux-arm64: 0.15.18 + esbuild-linux-mips64le: 0.15.18 + esbuild-linux-ppc64le: 0.15.18 + esbuild-linux-riscv64: 0.15.18 + esbuild-linux-s390x: 0.15.18 + esbuild-netbsd-64: 0.15.18 + esbuild-openbsd-64: 0.15.18 + esbuild-sunos-64: 0.15.18 + esbuild-windows-32: 0.15.18 + esbuild-windows-64: 0.15.18 + esbuild-windows-arm64: 0.15.18 + dev: false + /esbuild@0.19.11: resolution: {integrity: sha512-HJ96Hev2hX/6i5cDVwcqiJBBtuo9+FeIJOtZ9W1kA5M6AMJRHUZlpYZ1/SbEwtO0ioNAW8rUooVpC/WehY2SfA==} engines: {node: '>=12'} @@ -2230,6 +2602,10 @@ packages: web-streams-polyfill: 3.3.2 dev: false + /fflate@0.8.1: + resolution: {integrity: sha512-/exOvEuc+/iaUm105QIiOt4LpBdMTWsXxqR0HDF35vx3fmaKzw7354gTilCh5rkzEt8WYyG//ku3h3nRmd7CHQ==} + dev: true + /figures@3.2.0: resolution: {integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==} engines: {node: '>=8'} @@ -2292,6 +2668,14 @@ packages: is-callable: 1.2.7 dev: true + /foreground-child@3.1.1: + resolution: {integrity: sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==} + engines: {node: '>=14'} + dependencies: + cross-spawn: 7.0.3 + signal-exit: 4.1.0 + dev: true + /formdata-polyfill@4.0.10: resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} engines: {node: '>=12.20.0'} @@ -2335,12 +2719,10 @@ packages: engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] requiresBuild: true - dev: true optional: true /function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} - dev: true /function.prototype.name@1.1.6: resolution: {integrity: sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==} @@ -2417,6 +2799,18 @@ packages: is-glob: 4.0.3 dev: true + /glob@10.3.10: + resolution: {integrity: sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==} + engines: {node: '>=16 || 14 >=14.17'} + hasBin: true + dependencies: + foreground-child: 3.1.1 + jackspeak: 2.3.6 + minimatch: 9.0.3 + minipass: 7.0.4 + path-scurry: 1.10.1 + dev: true + /glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} dependencies: @@ -2553,7 +2947,6 @@ packages: engines: {node: '>= 0.4'} dependencies: function-bind: 1.1.2 - dev: true /hast-util-embedded@3.0.0: resolution: {integrity: sha512-naH8sld4Pe2ep03qqULEtvYr7EjrLK2QHY8KJR6RJkTUjPGObe1vnx585uzem2hGra+s1q08DZZpfgDVYRbaXA==} @@ -2865,6 +3258,13 @@ packages: has-bigints: 1.0.2 dev: true + /is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + dependencies: + binary-extensions: 2.2.0 + dev: true + /is-boolean-object@1.1.2: resolution: {integrity: sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==} engines: {node: '>= 0.4'} @@ -2882,7 +3282,6 @@ packages: resolution: {integrity: sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==} dependencies: hasown: 2.0.0 - dev: true /is-date-object@1.0.5: resolution: {integrity: sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==} @@ -3040,11 +3439,25 @@ packages: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} dev: true + /jackspeak@2.3.6: + resolution: {integrity: sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==} + engines: {node: '>=14'} + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + dev: true + /jiti@1.21.0: resolution: {integrity: sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==} hasBin: true dev: false + /joycon@3.1.1: + resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} + engines: {node: '>=10'} + dev: true + /js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} dev: true @@ -3122,6 +3535,11 @@ packages: type-check: 0.4.0 dev: true + /lilconfig@3.0.0: + resolution: {integrity: sha512-K2U4W2Ff5ibV7j7ydLr+zLAkIg5JJ4lPn1Ltsdt+Tz/IjQ8buJ55pZAxoP34lqIiwtF9iAvtLv3JGv7CAyAg+g==} + engines: {node: '>=14'} + dev: true + /lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} dev: true @@ -3148,6 +3566,11 @@ packages: lit-html: 3.1.0 dev: false + /load-tsconfig@0.2.5: + resolution: {integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dev: true + /load-yaml-file@0.2.0: resolution: {integrity: sha512-OfCBkGEw4nN6JLtgRidPX6QxjBQGQf72q3si2uvqyFEMbycSFFHwAZeXx6cJgFM9wmLrf9zBwCP3Ivqa+LLZPw==} engines: {node: '>=6'} @@ -3188,6 +3611,10 @@ packages: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} dev: true + /lodash.sortby@4.7.0: + resolution: {integrity: sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==} + dev: true + /lodash.startcase@4.4.0: resolution: {integrity: sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==} dev: true @@ -3227,6 +3654,11 @@ packages: resolution: {integrity: sha512-2Fgx1Ycm599x+WGpIYwJOvsjmXFzTSc34IwDWALRA/8AopUKAVPwfJ+h5+f85BCp0PWmmJcWzEpxOpoXycMpdA==} dev: true + /lru-cache@10.1.0: + resolution: {integrity: sha512-/1clY/ui8CzjKFyjdvwPWJUYKiFVXG2I2cY0ssG7h4+hwk+XOIX7ZSG9Q7TW8TW3Kp3BUSqgFWBLgL4PJ+Blag==} + engines: {node: 14 || >=16.14} + dev: true + /lru-cache@4.1.5: resolution: {integrity: sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==} dependencies: @@ -3386,6 +3818,11 @@ packages: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} dev: true + /minipass@7.0.4: + resolution: {integrity: sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==} + engines: {node: '>=16 || 14 >=14.17'} + dev: true + /mixme@0.5.10: resolution: {integrity: sha512-5H76ANWinB1H3twpJ6JY8uvAtpmFvHNArpilJAjXRKXSDDLPIMoZArw5SH0q9z+lLs8IrMw7Q2VWpWimFKFT1Q==} engines: {node: '>= 8.0.0'} @@ -3413,6 +3850,11 @@ packages: ufo: 1.3.2 dev: true + /mrmime@2.0.0: + resolution: {integrity: sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==} + engines: {node: '>=10'} + dev: true + /ms@2.1.2: resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} @@ -3420,11 +3862,18 @@ packages: resolution: {integrity: sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==} dev: true + /mz@2.7.0: + resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} + dependencies: + any-promise: 1.3.0 + object-assign: 4.1.1 + thenify-all: 1.6.0 + dev: true + /nanoid@3.3.7: resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true - dev: true /natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} @@ -3497,6 +3946,11 @@ packages: validate-npm-package-license: 3.0.4 dev: true + /normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + dev: true + /npm-run-path@4.0.1: resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} engines: {node: '>=8'} @@ -3511,6 +3965,11 @@ packages: path-key: 4.0.0 dev: true + /object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + dev: true + /object-inspect@1.13.1: resolution: {integrity: sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==} dev: true @@ -3762,6 +4221,13 @@ packages: /path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + /path-scurry@1.10.1: + resolution: {integrity: sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==} + engines: {node: '>=16 || 14 >=14.17'} + dependencies: + lru-cache: 10.1.0 + minipass: 7.0.4 dev: true /path-to-regexp@6.2.1: @@ -3793,6 +4259,11 @@ packages: engines: {node: '>=6'} dev: true + /pirates@4.0.6: + resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==} + engines: {node: '>= 6'} + dev: true + /pkg-dir@4.2.0: resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} engines: {node: '>=8'} @@ -3808,6 +4279,22 @@ packages: pathe: 1.1.1 dev: true + /postcss-load-config@4.0.2: + resolution: {integrity: sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==} + engines: {node: '>= 14'} + peerDependencies: + postcss: '>=8.0.9' + ts-node: '>=9.0.0' + peerDependenciesMeta: + postcss: + optional: true + ts-node: + optional: true + dependencies: + lilconfig: 3.0.0 + yaml: 2.3.4 + dev: true + /postcss@8.4.33: resolution: {integrity: sha512-Kkpbhhdjw2qQs2O2DGX+8m5OVqEcbB9HRBvuYM9pgrjEFUg30A9LmXNlTAUj4S9kgtGyrMbTzVjH7E+s5Re2yg==} engines: {node: ^10 || ^12 || >=14} @@ -3815,7 +4302,6 @@ packages: nanoid: 3.3.7 picocolors: 1.0.0 source-map-js: 1.0.2 - dev: true /preferred-pm@3.1.2: resolution: {integrity: sha512-nk7dKrcW8hfCZ4H6klWcdRknBOXWzNQByJ0oJyX97BOupsYD+FzLS4hflgEu/uPUEHZCuRfMxzCBsuWd7OzT8Q==} @@ -3962,6 +4448,13 @@ packages: util-deprecate: 1.0.2 dev: true + /readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + dependencies: + picomatch: 2.3.1 + dev: true + /redent@3.0.0: resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} engines: {node: '>=8'} @@ -4079,7 +4572,6 @@ packages: is-core-module: 2.13.1 path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 - dev: true /restore-cursor@3.1.0: resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==} @@ -4100,6 +4592,14 @@ packages: glob: 7.2.3 dev: true + /rollup@2.79.1: + resolution: {integrity: sha512-uKxbd0IhMZOhjAiD5oAFp7BqvkA4Dv47qpOCtaNvng4HBwdbWtdOh8f5nZNuk2rp51PMGk3bzfWu5oayNEuYnw==} + engines: {node: '>=10.0.0'} + hasBin: true + optionalDependencies: + fsevents: 2.3.3 + dev: false + /rollup@4.9.4: resolution: {integrity: sha512-2ztU7pY/lrQyXSCnnoU4ICjT/tCG9cdH3/G25ERqE3Lst6vl2BCM5hL2Nw+sslAvAf+ccKsAq1SkKQALyqhR7g==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} @@ -4284,6 +4784,15 @@ packages: - supports-color dev: false + /sirv@2.0.4: + resolution: {integrity: sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==} + engines: {node: '>= 10'} + dependencies: + '@polka/url': 1.0.0-next.24 + mrmime: 2.0.0 + totalist: 3.0.1 + dev: true + /slash@3.0.0: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} engines: {node: '>=8'} @@ -4335,13 +4844,19 @@ packages: /source-map-js@1.0.2: resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==} engines: {node: '>=0.10.0'} - dev: true /source-map@0.6.1: resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} engines: {node: '>=0.10.0'} dev: true + /source-map@0.8.0-beta.0: + resolution: {integrity: sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==} + engines: {node: '>= 8'} + dependencies: + whatwg-url: 7.1.0 + dev: true + /space-separated-tokens@2.0.2: resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} dev: false @@ -4402,6 +4917,15 @@ packages: strip-ansi: 6.0.1 dev: true + /string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.1.0 + dev: true + /string.prototype.trim@1.2.8: resolution: {integrity: sha512-lfjY4HcixfQXOfaqCvcBuOIapyaroTXhbkfJN3gcB1OtyupngWK4sEET9Knd0cXd28kTUqu/kHoV4HKSJdnjiQ==} engines: {node: '>= 0.4'} @@ -4447,6 +4971,13 @@ packages: ansi-regex: 5.0.1 dev: true + /strip-ansi@7.1.0: + resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} + engines: {node: '>=12'} + dependencies: + ansi-regex: 6.0.1 + dev: true + /strip-bom@3.0.0: resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} engines: {node: '>=4'} @@ -4492,6 +5023,20 @@ packages: acorn: 8.11.3 dev: true + /sucrase@3.35.0: + resolution: {integrity: sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==} + engines: {node: '>=16 || 14 >=14.17'} + hasBin: true + dependencies: + '@jridgewell/gen-mapping': 0.3.3 + commander: 4.1.1 + glob: 10.3.10 + lines-and-columns: 1.2.4 + mz: 2.7.0 + pirates: 4.0.6 + ts-interface-checker: 0.1.13 + dev: true + /suf-log@2.5.3: resolution: {integrity: sha512-KvC8OPjzdNOe+xQ4XWJV2whQA0aM1kGVczMQ8+dStAO6KfEB140JEVQ9dE76ONZ0/Ylf67ni4tILPJB41U0eow==} dependencies: @@ -4515,7 +5060,6 @@ packages: /supports-preserve-symlinks-flag@1.0.0: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} - dev: true /swap-case@1.1.2: resolution: {integrity: sha512-BAmWG6/bx8syfc6qXPprof3Mn5vQgf5dwdUNJhsNqU9WdPt5P+ES/wQ5bxfijy8zwZgZZHslC3iAsxsuQMCzJQ==} @@ -4546,6 +5090,19 @@ packages: resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} dev: true + /thenify-all@1.6.0: + resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} + engines: {node: '>=0.8'} + dependencies: + thenify: 3.3.1 + dev: true + + /thenify@3.3.1: + resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + dependencies: + any-promise: 1.3.0 + dev: true + /through@2.3.8: resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} dev: true @@ -4595,10 +5152,26 @@ packages: dependencies: is-number: 7.0.0 + /totalist@3.0.1: + resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} + engines: {node: '>=6'} + dev: true + /tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} dev: true + /tr46@1.0.1: + resolution: {integrity: sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==} + dependencies: + punycode: 2.3.1 + dev: true + + /tree-kill@1.2.2: + resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} + hasBin: true + dev: true + /trim-lines@3.0.1: resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} dev: false @@ -4621,6 +5194,10 @@ packages: typescript: 5.3.3 dev: true + /ts-interface-checker@0.1.13: + resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + dev: true + /ts-morph@15.1.0: resolution: {integrity: sha512-RBsGE2sDzUXFTnv8Ba22QfeuKbgvAGJFuTN7HfmIRUkgT/NaVLfDM/8OFm2NlFkGlWEXdpW5OaFIp1jvqdDuOg==} dependencies: @@ -4628,7 +5205,7 @@ packages: code-block-writer: 11.0.3 dev: true - /ts-node@10.9.2(@types/node@20.10.6)(typescript@5.3.3): + /ts-node@10.9.2(@types/node@20.11.5)(typescript@5.3.3): resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==} hasBin: true peerDependencies: @@ -4647,7 +5224,7 @@ packages: '@tsconfig/node12': 1.0.11 '@tsconfig/node14': 1.0.3 '@tsconfig/node16': 1.0.4 - '@types/node': 20.10.6 + '@types/node': 20.11.5 acorn: 8.11.3 acorn-walk: 8.3.1 arg: 4.1.3 @@ -4676,6 +5253,45 @@ packages: resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==} dev: true + /tsup@8.0.1(typescript@5.3.3): + resolution: {integrity: sha512-hvW7gUSG96j53ZTSlT4j/KL0q1Q2l6TqGBFc6/mu/L46IoNWqLLUzLRLP1R8Q7xrJTmkDxxDoojV5uCVs1sVOg==} + engines: {node: '>=18'} + hasBin: true + peerDependencies: + '@microsoft/api-extractor': ^7.36.0 + '@swc/core': ^1 + postcss: ^8.4.12 + typescript: '>=4.5.0' + peerDependenciesMeta: + '@microsoft/api-extractor': + optional: true + '@swc/core': + optional: true + postcss: + optional: true + typescript: + optional: true + dependencies: + bundle-require: 4.0.2(esbuild@0.19.11) + cac: 6.7.14 + chokidar: 3.5.3 + debug: 4.3.4 + esbuild: 0.19.11 + execa: 5.1.1 + globby: 11.1.0 + joycon: 3.1.1 + postcss-load-config: 4.0.2 + resolve-from: 5.0.0 + rollup: 4.9.4 + source-map: 0.8.0-beta.0 + sucrase: 3.35.0 + tree-kill: 1.2.2 + typescript: 5.3.3 + transitivePeerDependencies: + - supports-color + - ts-node + dev: true + /tty-table@4.2.3: resolution: {integrity: sha512-Fs15mu0vGzCrj8fmJNP7Ynxt5J7praPXqFN0leZeZBXJwkMxv9cb2D454k1ltrtUSJbZ4yH4e0CynsHLxmUfFA==} engines: {node: '>=8.0.0'} @@ -4829,7 +5445,6 @@ packages: resolution: {integrity: sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==} engines: {node: '>=14.17'} hasBin: true - dev: true /ufo@1.3.2: resolution: {integrity: sha512-o+ORpgGwaYQXgqGDwd+hkS4PuZ3QnmqMMxRuajK/a38L6fTpcE5GPIfrf+L/KemFzfUpeUQc1rRS1iDBozvnFA==} @@ -4857,7 +5472,6 @@ packages: /undici-types@5.26.5: resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} - dev: true /unified@11.0.4: resolution: {integrity: sha512-apMPnyLjAX+ty4OrNap7yumyVAMlKx5IWU2wlzzUdYJO9A8f1p9m/gywF/GM2ZDFcjQPrx59Mc90KwmxsoklxQ==} @@ -4914,6 +5528,11 @@ packages: engines: {node: '>= 10.0.0'} dev: true + /upath@2.0.1: + resolution: {integrity: sha512-1uEe95xksV1O0CYKXo8vQvN1JEbtJp7lb7C5U9HMsIp6IVwntkH/oNUzyVNQSd4S1sYk2FpSSW44FqMc8qee5w==} + engines: {node: '>=4'} + dev: true + /update-check@1.5.4: resolution: {integrity: sha512-5YHsflzHP4t1G+8WGPlvKbJEbAJGCgw+Em+dGR1KmBUbr1J36SJBqlHLjR7oob7sco5hWHGQVcr9B2poIVDDTQ==} dependencies: @@ -5002,6 +5621,40 @@ packages: - terser dev: true + /vite@3.2.7(@types/node@20.10.6): + resolution: {integrity: sha512-29pdXjk49xAP0QBr0xXqu2s5jiQIXNvE/xwd0vUizYT2Hzqe4BksNNoWllFVXJf4eLZ+UlVQmXfB4lWrc+t18g==} + engines: {node: ^14.18.0 || >=16.0.0} + hasBin: true + peerDependencies: + '@types/node': '>= 14' + less: '*' + sass: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + sass: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + dependencies: + '@types/node': 20.10.6 + esbuild: 0.15.18 + postcss: 8.4.33 + resolve: 1.22.8 + rollup: 2.79.1 + optionalDependencies: + fsevents: 2.3.3 + dev: false + /vite@5.0.11(@types/node@20.10.6): resolution: {integrity: sha512-XBMnDjZcNAw/G1gEiskiM1v6yzM4GE5aMGvhWTlHAYYhxb7S3/V1s3m2LDHa8Vh6yIWYYB0iJwsEaS523c4oYA==} engines: {node: ^18.0.0 || >=20.0.0} @@ -5038,7 +5691,7 @@ packages: fsevents: 2.3.3 dev: true - /vitest@1.1.3(@types/node@20.10.6): + /vitest@1.1.3(@types/node@20.10.6)(@vitest/ui@1.1.3): resolution: {integrity: sha512-2l8om1NOkiA90/Y207PsEvJLYygddsOyr81wLQ20Ra8IlLKbyQncWsGZjnbkyG2KwwuTXLQjEPOJuxGMG8qJBQ==} engines: {node: ^18.0.0 || >=20.0.0} hasBin: true @@ -5068,6 +5721,7 @@ packages: '@vitest/runner': 1.1.3 '@vitest/snapshot': 1.1.3 '@vitest/spy': 1.1.3 + '@vitest/ui': 1.1.3(vitest@1.1.3) '@vitest/utils': 1.1.3 acorn-walk: 8.3.1 cac: 6.7.14 @@ -5114,6 +5768,10 @@ packages: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} dev: true + /webidl-conversions@4.0.2: + resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==} + dev: true + /whatwg-url@5.0.0: resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} dependencies: @@ -5121,6 +5779,14 @@ packages: webidl-conversions: 3.0.1 dev: true + /whatwg-url@7.1.0: + resolution: {integrity: sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==} + dependencies: + lodash.sortby: 4.7.0 + tr46: 1.0.1 + webidl-conversions: 4.0.2 + dev: true + /which-boxed-primitive@1.0.2: resolution: {integrity: sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==} dependencies: @@ -5200,6 +5866,15 @@ packages: strip-ansi: 6.0.1 dev: true + /wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + dependencies: + ansi-styles: 6.2.1 + string-width: 5.1.2 + strip-ansi: 7.1.0 + dev: true + /wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} dev: true @@ -5221,6 +5896,11 @@ packages: resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} dev: true + /yaml@2.3.4: + resolution: {integrity: sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==} + engines: {node: '>= 14'} + dev: true + /yargs-parser@18.1.3: resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==} engines: {node: '>=6'} diff --git a/turbo/generators/app/index.ts b/turbo/generators/app/index.ts index f2ccf151..c9d5e0ce 100644 --- a/turbo/generators/app/index.ts +++ b/turbo/generators/app/index.ts @@ -1,7 +1,7 @@ -import { kebabCase } from 'lodash'; import type { PlopTypes } from '@turbo/gen'; -import * as path from 'node:path'; +import { kebabCase } from 'lodash'; import * as fs from 'node:fs/promises'; +import * as path from 'node:path'; export default (plop: PlopTypes.NodePlopAPI, { destBasePath }: PlopTypes.PlopCfg) => { plop.setGenerator('vite-plugin', { diff --git a/turbo/generators/app/templates/README.md b/turbo/generators/app/templates/README.md new file mode 100644 index 00000000..f270b59f --- /dev/null +++ b/turbo/generators/app/templates/README.md @@ -0,0 +1,22 @@ +# <%= githubRepoName %> + +<%= description %> + +## Install + +```js +npm i -D <%= githubRepoName %> +``` + +Add plugin to your vite.config.ts: + +```js +// vite.config.ts +import { defineConfig } from 'vite' +import <%= pluginName %> from './src/index'; + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [<%= pluginName %>({})] +}) +``` diff --git a/turbo/generators/app/templates/gitignore b/turbo/generators/app/templates/gitignore new file mode 100644 index 00000000..4855833c --- /dev/null +++ b/turbo/generators/app/templates/gitignore @@ -0,0 +1,3 @@ +node_modules +*.log +lib \ No newline at end of file diff --git a/turbo/generators/app/templates/index.html b/turbo/generators/app/templates/index.html new file mode 100644 index 00000000..b6afa180 --- /dev/null +++ b/turbo/generators/app/templates/index.html @@ -0,0 +1,12 @@ + + + + + + + Preview vite-plugin-<%= name %> + + +

Vite Development service started.

+ + diff --git a/turbo/generators/app/templates/npmignore b/turbo/generators/app/templates/npmignore new file mode 100644 index 00000000..d433a553 --- /dev/null +++ b/turbo/generators/app/templates/npmignore @@ -0,0 +1,3 @@ +node_modules +*.log +src \ No newline at end of file diff --git a/turbo/generators/app/templates/src/index.ts b/turbo/generators/app/templates/src/index.ts new file mode 100644 index 00000000..075c966a --- /dev/null +++ b/turbo/generators/app/templates/src/index.ts @@ -0,0 +1,30 @@ +/** + * Vite Plugin API + * https://cn.vitejs.dev/guide/api-plugin.html + */ + import type { Plugin, ResolvedConfig, UserConfig, ViteDevServer } from "vite"; + +export type Options = { + // TODO +} + +export default function <%= pluginName %>(options: Options): Plugin { + let config: ResolvedConfig; + + return { + name: 'vite-plugin-<%= name %>', + apply(config, { command }) { + // TODO + return command === 'serve'; + }, + config(config: UserConfig, { command }) { + // TODO + }, + configResolved(resolvedConfig: ResolvedConfig) { + // TODO + }, + configureServer(server: ViteDevServer) { + // TODO + } + } +} diff --git a/turbo/generators/app/templates/vite.config.ts b/turbo/generators/app/templates/vite.config.ts new file mode 100644 index 00000000..41d9ace6 --- /dev/null +++ b/turbo/generators/app/templates/vite.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vite'; +import <%= pluginName %> from './src/index'; + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [<%= pluginName %>({})] +}) \ No newline at end of file