Skip to content

Commit 6f95021

Browse files
committed
[function scope extraction] include lexical scope as code
1 parent b78dd4e commit 6f95021

File tree

8 files changed

+129
-63
lines changed

8 files changed

+129
-63
lines changed

bun.lockb

32 Bytes
Binary file not shown.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
},
4848
"dependencies": {
4949
"@babel/core": "^7.21.0",
50+
"@babel/generator": "^7.21.3",
5051
"@babel/parser": "^7.19.3",
5152
"@babel/traverse": "^7.21.2",
5253
"autoprefixer": "^10.4.12",

src/compiler/client-script/extract-function.ts

Lines changed: 32 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import fs from 'fs'
22

33
import Parser from '../parser'
4-
import {traverseFast, FunctionExpression, ArrowFunctionExpression} from '@babel/types'
4+
import {traverseFast, FunctionExpression, ArrowFunctionExpression, Statement} from '@babel/types'
55
import {HashType} from '../../jsx/VirtualElement'
66
import {Scope} from './scope'
77
import {getCurrentComponentHash} from '../template/template-builder'
@@ -16,7 +16,12 @@ const transpiler = new Bun.Transpiler({
1616
})
1717

1818
const scopes = {}
19-
const lexicalScopes: { [id: HashType]: Array<Node | { importName: string, localName: string, filePath: string }> } = {}
19+
export type ImportDataType = { importName: string | typeof Scope.importAll | typeof Scope.defaultImport, localName: string, filePath: string }
20+
const lexicalScopes: { [id: HashType]: Array<Statement | ImportDataType>[] } = {}
21+
22+
export function getLexicalScope(id: HashType, index: number) {
23+
return lexicalScopes[id][index]
24+
}
2025

2126
export async function extractFunction(
2227
{functionName, filePath, line, column}: { functionName: string, filePath: string, line: number, column: number }
@@ -27,21 +32,21 @@ export async function extractFunction(
2732
if (!parser.fileNames.includes(filePath)) {
2833
const fileContents = fs.readFileSync(filePath, {encoding: 'utf8'})
2934
const transformedFileContents = transpiler.transformSync(fileContents, 'tsx')
30-
console.log(transformedFileContents.split("\n")[line - 1])
31-
console.log(Array(column - 1).fill(' ').join('') + '^')
3235
parser.parseFile(filePath, transformedFileContents)
3336
}
3437

35-
const scope = scopes[filePath] ?? new Scope()
38+
const scope: Scope = scopes[filePath] ?? new Scope()
3639
const thingsInUse = []
3740
parser.traverseFileFast(filePath, (node) => {
3841
if (!(filePath in scope)) {
3942
// declarations
4043
if (node.type === 'FunctionDeclaration') {
4144
scope.add(node.id, node)
42-
} else if (node.type === 'VariableDeclarator') {
43-
if (node.id.type === 'Identifier')
44-
scope.add(node.id, node)
45+
} else if (node.type === 'VariableDeclaration') {
46+
node.declarations.forEach(declaration => {
47+
if (declaration.id.type === 'Identifier')
48+
scope.add(declaration.id, node)
49+
})
4550
} else if (node.type === 'ImportDeclaration') {
4651
node.specifiers.forEach(importSpecifier => {
4752
let importName: string | typeof Scope.importAll | typeof Scope.defaultImport
@@ -89,18 +94,24 @@ export async function extractFunction(
8994
}
9095
})
9196
})
92-
lexicalScopes[getCurrentComponentHash()] = thingsInUse
93-
.map(thing => {
94-
if (scope.has(scope.getId(thing))) {
95-
return [
96-
scope.getOrder(scope.getId(thing)),
97-
scope.get(scope.getId(thing))
98-
]
99-
} else if (scope.hasImport(thing)) {
100-
return [0, scope.getImport(thing)]
101-
}
102-
})
103-
.sort((a, b) => a[0] - b[0])
104-
.map(a => a[1])
97+
const id = getCurrentComponentHash()
98+
if (!lexicalScopes[id])
99+
lexicalScopes[id] = []
100+
lexicalScopes[id].push(
101+
thingsInUse
102+
.map((thing): [number, Statement[] | ImportDataType] => {
103+
if (scope.has(scope.getId(thing))) {
104+
return [
105+
scope.getOrder(scope.getId(thing)),
106+
scope.get(scope.getId(thing))
107+
]
108+
} else if (scope.hasImport(thing)) {
109+
return [0, scope.getImport(thing)]
110+
}
111+
})
112+
.sort((a, b) => a[0] - b[0])
113+
.map(<V>(a: [number, V]): V => a[1])
114+
.flat()
115+
)
105116
scopes[filePath] = scope
106117
}

src/compiler/client-script/generate-data-files.ts

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,27 +5,28 @@ import {Volume} from 'memfs/lib/volume'
55

66
import projectRoot, {resolve as resolveProjectRoot} from '../../utils/project-root'
77
import {resolve as resolveModuleRoot} from '../../utils/module-root'
8-
import {mapObjectToArray} from '../../utils/iterate-object'
9-
import {randomBytes} from 'crypto'
10-
import {replaceAsync} from '../../utils/replace-async'
11-
import {addMarker, addRange} from '../profiler'
12-
import {getRefs, getStateFromPlaceholderId, stateIdPlaceholderPrefix} from '../states-collector'
8+
import {iterateObject} from '../../utils/iterate-object'
9+
import {addMarker} from '../profiler'
10+
import {getRefs} from '../states-collector'
1311
import {getAssetsFilePaths} from '../assets'
1412
import {clientTemplatesToJs, refsToJs} from '../generate-code'
1513
import {getStateUsagesAsCode} from '../template/state-usage'
1614
import {getStateListenersAsCode} from '../../state/do-something'
1715

1816
const entryPoint = process.env.CHERRY_COLA_ENTRY
19-
const entryDir = path.dirname(entryPoint)
17+
export const entryDir = path.dirname(entryPoint)
2018
const mountFromSrc = ['runtime', 'messages', 'utils']
2119
export const outputPath = '/dist'
2220
export const virtualFilesPath = '/_virtual-files'
23-
export const stateListenersFilePath = path.join(virtualFilesPath, 'state-listeners.js')
21+
export const stateListenersFileName = 'state-listeners.js'
22+
export const stateListenersFilePath = path.join(virtualFilesPath, stateListenersFileName)
2423
export const refsAndTemplatesFilePath = path.join(virtualFilesPath, 'refs-and-templates.js')
2524

25+
export const hfsEntryDir = entryDir.replace(projectRoot, '')
26+
2627
const hfs: Volume = createHybridFs([
2728
[resolveProjectRoot('node_modules'), '/node_modules'],
28-
[entryDir, entryDir.replace(projectRoot, '')],
29+
[entryDir, hfsEntryDir],
2930
...mountFromSrc.map(dir => [resolveModuleRoot('src', dir), '/' + dir]),
3031
])
3132

@@ -42,7 +43,14 @@ export async function generateClientScriptFile() {
4243
let inputFile = ''
4344
inputFile += getAssetsFilePaths().map(path => `import '${path}'`).join(newLine)
4445
inputFile += newLine
45-
inputFile += getStateListenersAsCode()
46+
iterateObject(getStateListenersAsCode(), ([fileName, fileContents]) => {
47+
if (fileName === stateListenersFileName) {
48+
inputFile += fileContents
49+
} else {
50+
const filePath = path.join(virtualFilesPath, fileName)
51+
hfs.writeFileSync(filePath, fileContents)
52+
}
53+
})
4654
// inputFile += clientScripts.map(virtualPath => `import '${virtualPath}'`).join(newLine)
4755
hfs.writeFileSync(stateListenersFilePath, inputFile)
4856
addMarker('bundler', 'client-script-file')

src/compiler/client-script/scope.ts

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
1-
import {Identifier} from '@babel/types'
1+
import {Identifier, Statement} from '@babel/types'
2+
import {ImportDataType} from './extract-function'
23

34
export class Scope {
4-
data: Map<Identifier, Array<Node>> = new Map()
5-
imports: { [filePath: string]: { [localName: string]: string } } = {}
5+
data: Map<Identifier, Array<Statement>> = new Map()
6+
imports: { [filePath: string]: { [localName: string]: string | typeof Scope.importAll | typeof Scope.defaultImport } } = {}
67
order: [Identifier, number][] = []
78

89
static idMatch(idA: Identifier, idB: Identifier) {
9-
return idA.name === idB.name
10+
return idA?.name === idB?.name
1011
}
1112

1213
static defaultImport = Symbol.for('defaultImport')
@@ -33,7 +34,7 @@ export class Scope {
3334
return false
3435
}
3536

36-
add(id: Identifier, value) {
37+
add(id: Identifier, value: Statement) {
3738
this.ensureKey(id)
3839
this.order.push([id, this.data.get(id).length])
3940
this.data.get(id).push(value)
@@ -42,7 +43,7 @@ export class Scope {
4243
addImport(localName: string, importName: string | typeof Scope.importAll | typeof Scope.defaultImport, fileName) {
4344
if (!this.imports[fileName])
4445
this.imports[fileName] = {}
45-
this.imports[fileName][localName] = localName
46+
this.imports[fileName][localName] = importName
4647
}
4748

4849
get(id: Identifier) {
@@ -61,8 +62,8 @@ export class Scope {
6162
return index * 1e3 + this.order[index][1]
6263
}
6364

64-
getImport(localName: string) {
65-
for (const filePath in Object.values(this.imports)) {
65+
getImport(localName: string): ImportDataType {
66+
for (const filePath of Object.keys(this.imports)) {
6667
if (localName in this.imports[filePath])
6768
return {
6869
importName: this.imports[filePath][localName],
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import {HashType} from '../../jsx/VirtualElement'
2+
import {getLexicalScope, ImportDataType} from './extract-function'
3+
import generate from '@babel/generator'
4+
import {program, Statement} from '@babel/types'
5+
import {Scope} from './scope'
6+
7+
export function getStringifiedLexicalScope(id: HashType, index: number, transformImportPath: (path: string) => string) {
8+
const lexicalScope = getLexicalScope(id, index)
9+
const lexicalScopeStatements = lexicalScope.filter((element): element is Statement => 'kind' in element)
10+
const lexicalScopeImports = lexicalScope.filter((element): element is ImportDataType => !('kind' in element))
11+
const imports = lexicalScopeImports
12+
.map(importData => {
13+
let importNames
14+
if (importData.importName === importData.localName) {
15+
importNames = `{${importData.importName}}`
16+
} else if (importData.importName === Scope.defaultImport) {
17+
importNames = importData.localName
18+
} else if (importData.importName === Scope.importAll) {
19+
importNames = `* as ${importData.localName}`
20+
} else {
21+
importNames = `{${importData.importName as string} as ${importData.localName}}`
22+
}
23+
return `import ${importNames} from '${transformImportPath(importData.filePath)}'`
24+
})
25+
.join("\n")
26+
const statements = generate(program(lexicalScopeStatements)).code
27+
return imports + "\n" + statements
28+
}

src/runtime/state-listener.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ function prepStatesAndRefs(statesAndRefs: (State | Ref<any>)[]): ([any, (value:
1515
}
1616

1717
export function registerStateListeners() {
18-
// todo only execute currently used state listeners
18+
// todo only execute state listeners of currently used components
1919
stateListeners.forEach((listeners, id) => {
2020
const parametersArray = stateListenersParameters.get(id)
2121
listeners.forEach((listener, index) => {

src/state/do-something.ts

Lines changed: 41 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ import {getCurrentComponentHash} from '../compiler/template/template-builder'
77
import {findNode} from '../runtime'
88
import {getCallerPosition} from '../utils/get-caller-position'
99
import {extractFunction} from '../compiler/client-script/extract-function'
10+
import {getStringifiedLexicalScope} from '../compiler/client-script/stringify-scope'
11+
import {entryDir, hfsEntryDir, stateListenersFileName} from '../compiler/client-script/generate-data-files'
12+
import path from 'path'
1013

1114
export type StateOrRefType = State | Ref
1215
type StateType = State
@@ -41,7 +44,6 @@ const stateListeners: Map<HashType, StateListenerType<any>[]> = new Map()
4144
const stateListenersParameters: Map<HashType, StateOrRefType[][]> = new Map()
4245

4346
function includeStateListener(callback: StateListenerType<any>, statesAndRefs: StateOrRefType[]) {
44-
// todo: extract lexical scope of function
4547
const id = getCurrentComponentHash()
4648
if (!stateListeners.has(id))
4749
stateListeners.set(id, [])
@@ -51,44 +53,59 @@ function includeStateListener(callback: StateListenerType<any>, statesAndRefs: S
5153
stateListenersParameters.get(id).push(statesAndRefs)
5254
}
5355

54-
export function getStateListenersAsCode() {
56+
export function getStateListenersAsCode(): { [fileName: string]: string } {
5557
const stateListenersName = 'stateListeners'
5658
const stateListenersParametersName = 'stateListenersParameters'
57-
let code = ''
59+
let mainFile = ''
5860
const newLine = "\n"
59-
code += `import {${getClientState.name}} from '/runtime/client-state';` + newLine
60-
code += `import {${findNode.name}} from '/runtime/dom';` + newLine
61-
code += `const ${stateListenersName} = new Map();` + newLine
62-
code += `const ${stateListenersParametersName} = new Map();` + newLine
61+
mainFile += `import {${getClientState.name}} from '/runtime/client-state';` + newLine
62+
mainFile += `import {${findNode.name}} from '/runtime/dom';` + newLine
63+
mainFile += `const ${stateListenersName} = new Map();` + newLine
64+
mainFile += `const ${stateListenersParametersName} = new Map();` + newLine
65+
const files: { [fileName: string]: string } = {}
6366
stateListeners.forEach((callbacks, id) => {
67+
const componentFileName = `${id}.js`
68+
let componentFile = ''
6469
const statesAndRefArrays = stateListenersParameters.get(id)
65-
code += '{' + newLine
66-
// todo: put lexical stuff here
6770
let stringifiedCallbacks = '['
68-
for (const callback of callbacks) {
69-
stringifiedCallbacks += callback.toString()
71+
for (const index in callbacks) {
72+
// todo: put lexical stuff here
73+
componentFile += getStringifiedLexicalScope(
74+
id, Number(index),
75+
importPath => path.join(entryDir, importPath)
76+
) + newLine
77+
stringifiedCallbacks += callbacks[index].toString()
7078
stringifiedCallbacks += ','
7179
}
7280
stringifiedCallbacks += ']'
73-
code += `${stateListenersName}.set('${id}', ${stringifiedCallbacks});` + newLine
74-
let stateArrays = '['
81+
componentFile += 'export const callbacks = ' + stringifiedCallbacks + newLine
82+
mainFile += `import {callbacks as callbacks_${id}} from './${componentFileName}'` + newLine
83+
mainFile += `${stateListenersName}.set('${id}', callbacks_${id});` + newLine
84+
componentFile += 'export const states = ['
7585
for (const statesAndRefs of statesAndRefArrays) {
76-
stateArrays += '['
86+
componentFile += '['
7787
for (const stateOrRef of statesAndRefs) {
7888
if (isRef(stateOrRef)) {
79-
stateArrays += `${findNode.name}('${stateOrRef.id}')`
89+
componentFile += `${findNode.name}('${stateOrRef.id}')`
8090
} else {
81-
stateArrays += `${getClientState.name}('${stateOrRef.id}', ${JSON.stringify(stateOrRef.valueOf())})` // todo: use value from ast
91+
componentFile += `${getClientState.name}('${stateOrRef.id}', ${JSON.stringify(stateOrRef.valueOf())})` // todo: use value from ast
8292
}
83-
stateArrays += ', '
93+
componentFile += ', '
8494
}
85-
stateArrays += '],'
95+
componentFile += '],'
8696
}
87-
stateArrays += ']'
88-
code += `${stateListenersParametersName}.set('${id}', ${stateArrays});` + newLine
89-
code += '}' + newLine
97+
componentFile += '];'
98+
mainFile += `import {states as states_${id}} from './${componentFileName}'` + newLine
99+
mainFile += `${stateListenersParametersName}.set('${id}', states_${id});` + newLine
100+
files[componentFileName] = componentFile
101+
// console.log()
102+
// console.log(componentFile)
90103
})
91-
code += `export {${stateListenersName}};` + newLine
92-
code += `export {${stateListenersParametersName}};`
93-
return code
104+
mainFile += `export {${stateListenersName}};` + newLine
105+
mainFile += `export {${stateListenersParametersName}};`
106+
// return code
107+
files[stateListenersFileName] = mainFile
108+
// console.log()
109+
// console.log(mainFile)
110+
return files
94111
}

0 commit comments

Comments
 (0)