Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: sync version of method #11

Merged
merged 2 commits into from
Jun 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 6 additions & 28 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ $ npm i parse-imports
## Usage

```js
import parseImports from 'parse-imports'
import { parseImports } from 'parse-imports'

const code = `
import a from 'b'
Expand Down Expand Up @@ -169,35 +169,13 @@ console.log(imports[8])

## API

### `parseImports(code[, options]) -> Promise<Iterable<Import>>`
Use `parseImports` when you're able to await a `Promise` result and
`parseImportsSync` otherwise.

Returns a `Promise` resolving to a lazy
[iterable](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols#The_iterable_protocol)/[iterator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols#The_iterator_protocol)
that iterates over the imports in `code`.
> [!IMPORTANT] You can only call `parseImportsSync` once the WASM has loaded.
> You can be sure this has happened by awaiting the exported `wasmLoadPromise`.

### Parameters

#### `code`

Type: `string`

The JavaScript code to parse for imports.

#### `options`

Type: `object` (optional)

##### Properties

###### `resolveFrom`

Type: `string` (optional)\
Default: `undefined`

If set to a file path, then `moduleSpecifier.resolved` of the returned `Import`
instances will be set to the result of calling
`require.resolve(moduleSpecifier.value)` from the given file path. Otherwise,
will be `undefined`.
See the [type definitions](./src/index.d.ts) for details.

### Types

Expand Down
114 changes: 112 additions & 2 deletions src/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,27 @@
* limitations under the License.
*/

/** Options for parsing imports. */
export type Options = {
/**
* If set to a file path, then {@link Import.moduleSpecifier.resolved} of
* returned instances will be set to the result of calling
* `require.resolve(moduleSpecifier.value)` from the given file path.
* Otherwise, will be undefined.
*/
readonly resolveFrom?: string
}

/**
* A type representing what kind of module a specifier refers to.
*
* - 'unknown' if the module specifier is not a simple constant string literal
* - 'invalid' if the module specifier is the empty string
* - 'absolute' if the module specifier is an absolute file path
* - 'relative' if the module specifier is a relative file path
* - 'builtin' if the module specifier is the name of a builtin Node.js package
* - 'package' otherwise
*/
export type ModuleSpecifierType =
| 'invalid'
| 'absolute'
Expand All @@ -26,29 +43,122 @@ export type ModuleSpecifierType =
| 'package'
| 'unknown'

/**
* A type representing an import in JavaScript code.
*
* `code.substring(startIndex, endIndex)` returns the full import statement or
* expression.
*/
export type Import = {
/** The start index of the import in the JavaScript (inclusive). */
startIndex: number

/** The end index of the import in the JavaScript (exclusive). */
endIndex: number

/** Whether the import is a dynamic import (e.g. `import('module')`). */
isDynamicImport: boolean

/**
* A type representing the code specifiying the module being imported.
*
* `code.substring(moduleSpecifier.startIndex, moduleSpecifier.endIndex)`
* returns the module specifier including quotes.
*/
moduleSpecifier: {
/**
* What kind of module the specifier refers to.
*
* 'unknown' when `moduleSpecifier.isConstant` is false.
*/
type: ModuleSpecifierType

/** The start index of the specifier in the JavaScript (inclusive). */
startIndex: number

/** The end index of the specifier in the JavaScript (exclusive). */
endIndex: number

/**
* True when the import is not a dynamic import (`isDynamicImport` is
* false), or when the import is a dynamic import where the specifier is a
* simple string literal (e.g. import('fs'), import("fs"), import(`fs`)).
*/
isConstant: boolean

/**
* The module specifier as it was written in the code. For non-constant
* dynamic imports it could be a complex expression.
*/
code: string

/**
* `code` without string literal quotes and unescaped if `isConstant` is
* true. Otherwise, it is undefined.
*/
value?: string

/** Set if the `resolveFrom` option is set and `value` is not undefined. */
resolved?: string
}

/**
* A type representing what is being imported from the module.
*
* Undefined if `isDynamicImport` is true.
*/
importClause?: {
/**
* The default import identifier or undefined if the import statement does
* not have a default import.
*/
default?: string

/**
* An array of objects representing the named imports of the import
* statement. It is empty if the import statement does not have any named
* imports. Each object in the array has a specifier field set to the
* imported identifier and a binding field set to the identifier for
* accessing the imported value.
* For example, `import { a, x as y } from 'something'` would have the
* following array:
* ```
* [{ specifier: 'a', binding: 'a' }, { specifier: 'x', binding: 'y' }]
* ```
*/
named: { specifier: string; binding: string }[]

/**
* The namespace import identifier or undefined if the import statement does
* not have a namespace import.
*/
namespace?: string
}
}

declare const parseImports: (
/**
* A promise that resolves once WASM has finished loading.
*
* Await this promise to be certain calling `parseImportsSync` is safe.
*/
export const wasmLoadPromise: Promise<void>

/**
* Returns a promise resolving to a lazy iterable/iterator that iterates over
* the imports in `code`.
*/
export const parseImports: (
code: string,
options?: Options,
) => Promise<Iterable<Import>>

export default parseImports
/**
* Returns a lazy iterable/iterator that iterates over the imports in `code`.
*
* @throws if called before WASM has finished loading. Await `wasmLoadPromise`
* to be sure it has finished.
*/
export const parseImportsSync: (
code: string,
options?: Options,
) => Iterable<Import>
24 changes: 16 additions & 8 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,25 @@
* limitations under the License.
*/

import { parse } from 'es-module-lexer'
import { init, parse } from 'es-module-lexer'
import parseImportClause from './parse-import-clause/index.js'
import parseModuleSpecifier from './parse-module-specifier/index.js'

const parseImports = async (code, { resolveFrom } = {}) => {
const [imports] = await parse(
code,
resolveFrom == null ? undefined : resolveFrom,
)
export const wasmLoadPromise = init

export const parseImports = async (code, options) => {
await wasmLoadPromise
return parseImportsSync(code, options)
}

export const parseImportsSync = (code, { resolveFrom } = {}) => {
const result = parse(code, resolveFrom == null ? undefined : resolveFrom)
if (!Array.isArray(result)) {
throw new TypeError(
`Expected WASM to be loaded before calling parseImportsSync`,
)
}
const [imports] = result

return {
*[Symbol.iterator]() {
Expand Down Expand Up @@ -89,5 +99,3 @@ const parseImports = async (code, { resolveFrom } = {}) => {
},
}
}

export default parseImports
25 changes: 24 additions & 1 deletion test/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,21 @@ import fs from 'node:fs/promises'
import { dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url'
import { expectTypeOf } from 'tomer'
import parseImports from '../src/index.js'
import {
parseImports,
parseImportsSync,
wasmLoadPromise,
} from '../src/index.js'
import type { Import } from '../src/index.js'

const currentDirectoryPath = dirname(fileURLToPath(import.meta.url))

test(`parseImportsSync throws before WASM load`, () => {
expect(() => parseImportsSync(``)).toThrow(
new Error(`Expected WASM to be loaded before calling parseImportsSync`),
)
})

test.each([
{
path: `no-resolve.js`,
Expand Down Expand Up @@ -595,7 +605,9 @@ test.each([
join(currentDirectoryPath, `fixtures`, path),
`utf8`,
)
await wasmLoadPromise

const parsedImportsSync = [...parseImportsSync(code, { resolveFrom })]
const parsedImports = [...(await parseImports(code, { resolveFrom }))]

console.log(
Expand All @@ -610,6 +622,7 @@ test.each([
code.slice(startIndex, endIndex),
),
)
expect(parsedImportsSync).toStrictEqual(expectedImports)
expect(parsedImports).toStrictEqual(expectedImports)
},
)
Expand All @@ -624,4 +637,14 @@ test(`types`, () => {
expectTypeOf(parseImports(`some code`)).toMatchTypeOf<
Promise<Iterable<Import>>
>(parseImports(`some code`, { resolveFrom: `./wow` }))

expectTypeOf(parseImportsSync(`some code`)).toMatchTypeOf<Iterable<Import>>(
parseImportsSync(`some code`),
)
expectTypeOf(parseImportsSync(`some code`)).toMatchTypeOf<Iterable<Import>>(
parseImportsSync(`some code`, {}),
)
expectTypeOf(parseImportsSync(`some code`)).toMatchTypeOf<Iterable<Import>>(
parseImportsSync(`some code`, { resolveFrom: `./wow` }),
)
})