-
-
Notifications
You must be signed in to change notification settings - Fork 4.2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Start implementing the runtime compiler
- Loading branch information
Showing
64 changed files
with
17,569 additions
and
9,837 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
<!DOCTYPE html> | ||
<html> | ||
<head> | ||
<script type="importmap"> | ||
{ | ||
"imports": { | ||
"@ember/template-compiler": "./node_modules/@ember/template-compiler/index.ts", | ||
"@glimmer/tracking": "./node_modules/@glimmer/tracking/index.ts", | ||
"tracked-built-ins": "./node_modules/tracked-built-ins/dist/index.js", | ||
"@ember/template-compilation": "./src/precompile.ts", | ||
"@/": "./src/" | ||
} | ||
} | ||
</script> | ||
<script> | ||
process = { env: {} }; | ||
</script> | ||
<script type="module" src="./src/main.ts"></script> | ||
</head> | ||
<body> | ||
<div id="overlay"></div> | ||
</body> | ||
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
{ | ||
"name": "@ember/embedded-demo", | ||
"private": true, | ||
"type": "module", | ||
"dependencies": { | ||
"@ember/-internals": "workspace:*", | ||
"@ember/component": "workspace:*", | ||
"@ember/template-compilation": "workspace:*", | ||
"@ember/template-compiler": "workspace:*", | ||
"@ember/template-factory": "workspace:*", | ||
"@glimmer/tracking": "workspace:*", | ||
"@swc/wasm-web": "^1.7.28", | ||
"@swc/plugin-transform-imports": "^3.0.3", | ||
"tracked-built-ins": "^3.3.0" | ||
}, | ||
"devDependencies": { | ||
"@glimmer/compiler": "^0.92.4", | ||
"@glimmer/syntax": "^0.92.3", | ||
"content-tag": "^2.0.2", | ||
"vite": "^5.4.8", | ||
"vite-plugin-node-polyfills": "^0.22.0" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
interface ModuleTag { | ||
[Symbol.toStringTag]: 'Module'; | ||
} | ||
type ModuleObject = Record<string, unknown> & ModuleTag; | ||
|
||
export async function asModule<T = ModuleObject>( | ||
source: string | ||
// { at, name = 'template.js' }: { at: { url: URL | string }; name?: string } | ||
): Promise<T & ModuleTag> { | ||
const blob = new Blob([source], { type: 'application/javascript' }); | ||
|
||
return import(URL.createObjectURL(blob)); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
import type { ASTv1 } from '@glimmer/syntax'; | ||
import initSwc, { transformSync, type Output } from '@swc/wasm-web'; | ||
import type { PreprocessorOptions as ContentTagOptions } from 'content-tag'; | ||
import { Preprocessor } from 'content-tag'; | ||
|
||
await initSwc({}); | ||
|
||
export class GjsCompiler { | ||
readonly #contentTagPreprocessor = new Preprocessor(); | ||
|
||
#contentTag(source: string, options?: ContentTagOptions): string { | ||
return this.#contentTagPreprocessor.process(source, options); | ||
} | ||
|
||
compile = async (source: string, options?: ContentTagOptions): Promise<{ code: string }> => { | ||
let output = this.#contentTag(source, { inline_source_map: true, ...options }); | ||
|
||
const result = transformSync(output, { | ||
filename: options?.filename ?? 'unknown', | ||
sourceMaps: options?.inline_source_map ? 'inline' : false, | ||
inlineSourcesContent: Boolean(options?.inline_source_map), | ||
jsc: { | ||
parser: { | ||
syntax: 'typescript', | ||
decorators: true, | ||
}, | ||
transform: { | ||
legacyDecorator: true, | ||
useDefineForClassFields: false, | ||
}, | ||
}, | ||
}); | ||
|
||
// In real life, do something better than this | ||
if (typeof result?.code !== 'string') { | ||
throw new Error('Unable to compile'); | ||
} | ||
|
||
result.code = result.code.replace( | ||
/"moduleName":\s"[^"]+"/u, | ||
`"moduleName": "${options?.filename ?? 'unknown'}"` | ||
); | ||
|
||
return Promise.resolve(result as Output); | ||
}; | ||
} | ||
|
||
const GJS_COMPILER = new GjsCompiler(); | ||
|
||
export const compile = GJS_COMPILER.compile; | ||
|
||
export interface PrinterOptions { | ||
entityEncoding: ASTv1.EntityEncodingState; | ||
|
||
/** | ||
* Used to override the mechanism of printing a given AST.Node. | ||
* | ||
* This will generally only be useful to source -> source codemods | ||
* where you would like to specialize/override the way a given node is | ||
* printed (e.g. you would like to preserve as much of the original | ||
* formatting as possible). | ||
* | ||
* When the provided override returns undefined, the default built in printing | ||
* will be done for the AST.Node. | ||
* | ||
* @param ast the ast node to be printed | ||
* @param options the options specified during the print() invocation | ||
*/ | ||
override?(ast: ASTv1.Node, options: PrinterOptions): void | string; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
import { ComponentRenderer } from '@ember/-internals/strict-renderer'; | ||
import { compile } from './compiler'; | ||
import { asModule } from './as-module'; | ||
import { TrackedObject } from 'tracked-built-ins'; | ||
|
||
const owner = {}; | ||
const renderer = new ComponentRenderer(owner, document, { | ||
isInteractive: true, | ||
hasDOM: true, | ||
}); | ||
|
||
const componentModule = await compile(/*ts*/ ` | ||
import { TrackedObject } from 'tracked-built-ins'; | ||
import { tracked } from '@glimmer/tracking'; | ||
class Hello { | ||
@tracked greeting: string; | ||
} | ||
export const hello = new Hello() | ||
export const object = new TrackedObject({ | ||
object: 'world', | ||
}); | ||
class MyComponent { | ||
<template>Hi</template> | ||
} | ||
<template> | ||
{{~#let hello.greeting object.object as |greeting object|~}} | ||
<p><Paragraph @greeting={{greeting}} @kind={{@kind}} @object={{object}} /></p> | ||
{{~/let~}} | ||
</template> | ||
const Paragraph = <template> | ||
<p> | ||
<Word @word={{@greeting}} /> | ||
<Word @word={{@kind}} /> | ||
<Word @word={{@object}} /> | ||
</p> | ||
</template> | ||
const Word = <template> | ||
<span>{{@word}}</span> | ||
</template> | ||
`); | ||
|
||
const { | ||
default: component, | ||
hello, | ||
object, | ||
} = await asModule<{ | ||
default: object; | ||
hello: { greeting: string }; | ||
object: { object: string }; | ||
}>(componentModule.code); | ||
|
||
hello.greeting = 'hello'; | ||
object.object = 'world'; | ||
const args = new TrackedObject({ kind: 'great' }); | ||
|
||
const element = document.createElement('div'); | ||
document.body.appendChild(element); | ||
|
||
renderer.render(component, { element, args }); | ||
|
||
await delay(1000); | ||
|
||
hello.greeting = 'goodbye'; | ||
|
||
await delay(1000); | ||
|
||
args.kind = 'cruel'; | ||
|
||
function delay(ms: number) { | ||
return new Promise((resolve) => setTimeout(resolve, ms)); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
import { precompile } from '@glimmer/compiler'; | ||
|
||
export const precompileTemplate = precompile; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,100 @@ | ||
import type * as Babel from '@babel/core'; | ||
import type { NodePath } from '@babel/core'; | ||
import type { ImportDeclaration } from '@babel/types'; | ||
|
||
export interface ImportRewrite { | ||
to?: string; | ||
specifier?: RewriteSpecifier | RewriteSpecifier[]; | ||
} | ||
|
||
export interface RewriteSpecifier { | ||
/** | ||
* The name of the export to rename. The name `default` is | ||
* legal here, and will apply to `import Default from "..."` | ||
* syntax. | ||
*/ | ||
from: string; | ||
to: string; | ||
} | ||
|
||
export type Rewrites = Record<string, ImportRewrite | ImportRewrite[]>; | ||
|
||
export function rewrite( | ||
t: (typeof Babel)['types'], | ||
path: NodePath<ImportDeclaration>, | ||
rewrites: Rewrites | ||
) { | ||
for (const [matchSource, rules] of Object.entries(rewrites)) { | ||
for (const rule of intoArray(rules)) { | ||
path = rewriteOne(t, matchSource, path, rule); | ||
} | ||
} | ||
|
||
return path; | ||
} | ||
|
||
export function rewriteOne( | ||
t: (typeof Babel)['types'], | ||
matchSource: string, | ||
path: NodePath<ImportDeclaration>, | ||
rewrite: ImportRewrite | ||
): NodePath<ImportDeclaration> { | ||
const source = path.node.source.value; | ||
|
||
if (source !== matchSource) { | ||
return path; | ||
} | ||
|
||
if (rewrite.to) { | ||
path.node.source = t.stringLiteral(rewrite.to); | ||
} | ||
|
||
const renameSpecifiers = rewrite.specifier; | ||
|
||
if (!renameSpecifiers) { | ||
return path; | ||
} | ||
|
||
path.node.specifiers = path.node.specifiers.map((specifier) => { | ||
for (const rewrite of intoArray(renameSpecifiers)) { | ||
specifier = rewriteSpecifier(t, rewrite, specifier); | ||
} | ||
|
||
return specifier; | ||
}); | ||
|
||
return path; | ||
} | ||
|
||
function rewriteSpecifier( | ||
t: (typeof Babel)['types'], | ||
rewrite: RewriteSpecifier, | ||
specifier: ImportDeclaration['specifiers'][number] | ||
) { | ||
if (rewrite.from === 'default') { | ||
if (t.isImportDefaultSpecifier(specifier)) { | ||
// Intentionally keep the original name around so we don't have to adjust | ||
// the scope. | ||
return t.importSpecifier(specifier.local, t.identifier(rewrite.to)); | ||
} | ||
|
||
// if the import didn't use default import syntax, we might still find a `default` | ||
// named specifier, so don't return yet. | ||
} | ||
|
||
if (t.isImportSpecifier(specifier) && t.isIdentifier(specifier.imported)) { | ||
const importedName = specifier.imported.name; | ||
|
||
if (importedName === rewrite.from) { | ||
// Intentionally keep the original name around so we don't have to adjust | ||
// the scope. | ||
return t.importSpecifier(specifier.local, t.identifier(rewrite.to)); | ||
} | ||
} | ||
|
||
return specifier; | ||
} | ||
|
||
function intoArray<T>(value: T | T[]): T[] { | ||
return Array.isArray(value) ? value : [value]; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
import { consumeTag, createTag, dirtyTag } from '@glimmer/validator'; | ||
|
||
export class Cell<T> { | ||
#tag = createTag(); | ||
#value: T; | ||
|
||
constructor(value: T) { | ||
this.#value = value; | ||
} | ||
|
||
get current() { | ||
consumeTag(this.#tag); | ||
return this.#value; | ||
} | ||
|
||
set current(value: T) { | ||
this.#value = value; | ||
dirtyTag(this.#tag); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
{ | ||
"extends": "../tsconfig/compiler-options.json", | ||
"compilerOptions": { | ||
"noEmit": true, | ||
"baseUrl": ".", | ||
"rootDir": ".", | ||
"target": "esnext", | ||
"module": "esnext", | ||
"moduleResolution": "bundler" | ||
}, | ||
"include": [ | ||
"src/**/*.ts", | ||
], | ||
"exclude": [ | ||
"dist", | ||
"node_modules", | ||
"tmp", | ||
"types" | ||
] | ||
} |
Oops, something went wrong.