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鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: string-based codegen utils #2

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 4 additions & 1 deletion .eslintrc
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
{
"extends": [
"@nuxtjs/eslint-config-typescript"
]
],
"rules": {
"no-redeclare": "off"
}
}
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ jobs:
run: yarn lint

- name: Test
run: yarn dev
run: yarn test

# - name: Coverage
# uses: codecov/codecov-action@v1
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
node_modules
dist
*.log

.nyc_output
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ import { parse, compile } from 'paneer'
import * as p from 'paneer'

// CommonJS
const { parse, compile } = require('panner')
const p = require('panner')
const { parse, compile } = require('paneer')
const p = require('paneer')
```

**Example:** Modify a file:
Expand Down
9 changes: 7 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,19 +19,24 @@
],
"scripts": {
"build": "siroc build",
"dev": "jiti test/test.ts",
"lint": "eslint --ext .js,.ts .",
"prepack": "yarn build",
"release": "yarn test && standard-version && git push --follow-tags && npm publish",
"test": "yarn lint"
"test": "nyc mocha -r jiti/register ./test/**/*.test.ts"
},
"dependencies": {
"pathe": "^0.2.0",
"recast": "^0.20.4"
},
"devDependencies": {
"@nuxtjs/eslint-config-typescript": "latest",
"@types/chai": "^4.2.22",
"@types/mocha": "^9.0.0",
"chai": "^4.3.4",
"eslint": "latest",
"jiti": "latest",
"mocha": "^9.1.3",
"nyc": "^15.1.0",
"siroc": "latest",
"standard-version": "latest"
}
Expand Down
165 changes: 165 additions & 0 deletions src/gen.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
import { isAbsolute, normalize } from 'pathe'

export interface CodegenOptions {
/** whether to use single quotes - @default false */
singleQuotes?: boolean
/** whether to add semicolon after a full statement - @default true */
semi?: boolean
/** set to false to disable indentation, or the current indent level */
indent?: false | string
/** set to false to disable multi-line arrays/objects */
lineBreaks?: boolean
}

// genImport and genExport
export type Name<T = string, As = string> = T | { name: T, as?: As }
export interface ImportOrExportOptions extends CodegenOptions {
/** if set to true, normalize any absolute paths passed as specifiers - @default false */
normalizePaths?: boolean
}

export function genImport (specifier: string, defaultImport?: Name, opts?: ImportOrExportOptions): string
export function genImport (specifier: string, imports?: Name[], opts?: ImportOrExportOptions): string
export function genImport (specifier: string, names?: Name | Name[], opts: ImportOrExportOptions = {}) {
if (opts.normalizePaths && isAbsolute(specifier)) {
specifier = normalize(specifier)
}
const specifierStr = genString(specifier, opts) + (opts.semi === false ? '' : ';')
if (!names) {
// import with side effects
return `import ${specifierStr}`
}

const normalizedNames = normalizeNames(names)
const shouldDestructure = Array.isArray(names)
if (normalizedNames.some(i => i.as === 'default' || (!i.as && i.name === 'default'))) {
throw new Error('Cannot import a module as `default`')
}
const namesStr = genNameString(normalizedNames, shouldDestructure)
return `import ${namesStr} from ${specifierStr}`
}

export function genExport (specifier: string, namespaced?: Name<'*'>, opts?: ImportOrExportOptions): string
export function genExport (specifier: string, exports?: Name[], opts?: ImportOrExportOptions): string
export function genExport (specifier: string, names?: Name | Name[], opts: ImportOrExportOptions = {}) {
if (opts.normalizePaths && isAbsolute(specifier)) {
specifier = normalize(specifier)
}
const specifierStr = genString(specifier, opts) + (opts.semi === false ? '' : ';')

const normalizedNames = normalizeNames(names)
const shouldDestructure = Array.isArray(names)
if (!shouldDestructure && !normalizedNames[0].as && normalizedNames[0].name !== '*') {
throw new Error('Cannot export a module without providing a name')
}
const namesStr = genNameString(normalizedNames, shouldDestructure)
return `export ${namesStr} from ${specifierStr}`
}

// genDynamicImport
export interface DynamicImportOptions extends CodegenOptions {
comment?: string
wrapper?: boolean
interopDefault?: boolean
}

export function genDynamicImport (specifier: string, opts: DynamicImportOptions = {}) {
const commentStr = opts.comment ? ` /* ${opts.comment} */` : ''
const wrapperStr = (opts.wrapper === false) ? '' : '() => '
const ineropStr = opts.interopDefault ? '.then(m => m.default || m)' : ''
return `${wrapperStr}import(${genString(specifier, opts)}${commentStr})${ineropStr}`
}

// raw generation utils
export function genObjectFromRaw (obj: Record<string, any>, opts: CodegenOptions = {}): string {
return genObjectFromRawEntries(Object.entries(obj), opts)
}

export function genArrayFromRaw (array: any[], opts: CodegenOptions = {}) {
const indent = opts.indent ?? ''
const newIdent = indent !== false && (indent + ' ')
return wrapInDelimiters(array.map(i => genRawValue(i, { ...opts, indent: newIdent })), { ...opts, indent, delimiters: '[]' })
}

export function genObjectFromRawEntries (array: [key: string, value: any][], opts: CodegenOptions = {}) {
const indent = opts.indent ?? ''
const newIdent = indent !== false && (indent + ' ')
return wrapInDelimiters(array.map(([key, value]) => `${genObjectKey(key, opts)}: ${genRawValue(value, { ...opts, indent: newIdent })}`), { ...opts, indent, delimiters: '{}' })
}

function normalizeNames (names: Name[] | Name) {
return (Array.isArray(names) ? names : [names]).map((i: Name) => {
if (typeof i === 'string') { return { name: i } }
if (i.name === i.as) { i = { name: i.name } }

return i
})
}

function genNameString (names: Exclude<Name, string>[], wrap: boolean) {
const namesStr = names.map(i => i.as ? `${i.name} as ${i.as}` : i.name).join(', ')
if (wrap) {
return wrapInDelimiters([namesStr])
}
return namesStr
}

interface WrapDelimiterOptions extends CodegenOptions {
delimiters?: string
}

export function wrapInDelimiters (lines: string[], { lineBreaks = true, delimiters = '{}', indent = false }: WrapDelimiterOptions = {}) {
if (!lines.length) {
return delimiters
}
const [start, end] = delimiters
const lineBreak = (indent === false || !lineBreaks) ? ' ' : '\n'
if (indent !== false && lineBreak) {
lines = lines.map(l => `${indent} ${l}`)
}
return `${start}${lineBreak}` + lines.join(`,${lineBreak}`) + `${lineBreak}${indent || ''}${end}`
}

export function genString (input: string, opts: CodegenOptions = {}) {
if (!opts.singleQuotes) {
// Use JSON.stringify strategy rather than escaping it ourselves
return JSON.stringify(input)
}
return `'${escapeString(input)}'`
}

export function genRawValue (value: unknown, opts: CodegenOptions = {}): string {
if (typeof value === 'undefined') {
return 'undefined'
}
if (value === null) {
return 'null'
}
if (Array.isArray(value)) {
return genArrayFromRaw(value, opts)
}
if (value && typeof value === 'object') {
return genObjectFromRaw(value, opts)
}
return value.toString()
}

// Internal

const VALID_IDENTIFIER_RE = /^[$_]?[\w\d]*$/

function genObjectKey (key: string, opts: CodegenOptions) {
return key.match(VALID_IDENTIFIER_RE) ? key : genString(key, opts)
}

// https://github.com/rollup/rollup/blob/master/src/utils/escapeId.ts
const NEEDS_ESCAPE_RE = /[\\'\r\n\u2028\u2029]/
const QUOTE_NEWLINE_RE = /(['\r\n\u2028\u2029])/g
const BACKSLASH_RE = /\\/g

function escapeString (id: string): string {
if (!id.match(NEEDS_ESCAPE_RE)) {
return id
}
return id.replace(BACKSLASH_RE, '\\\\').replace(QUOTE_NEWLINE_RE, '\\$1')
}
14 changes: 14 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,17 @@
export * from './ast'
export * from './fs'
export {
CodegenOptions,
DynamicImportOptions,
ImportOrExportOptions,
Name,
genArrayFromRaw,
genDynamicImport,
genExport,
genImport,
genObjectFromRaw,
genObjectFromRawEntries,
genRawValue,
genString
} from './gen'
export * from './proxy'
26 changes: 26 additions & 0 deletions test/ast.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { expect } from 'chai'
import { parse, proxifyAST, generate } from '../src'

describe('proxyAST', () => {
it('should produce correct code', () => {
const ast = proxifyAST(
parse(`
export const a = {}
export default {
// This is foo
foo: ['a']
}`)
)

ast.exports.default.props.foo.push('b')

expect(generate(ast).code).to.equal(
`
export const a = {}
export default {
// This is foo
foo: ['a', "b"]
}`
)
})
})