Skip to content

Commit

Permalink
Start implementing the runtime compiler
Browse files Browse the repository at this point in the history
  • Loading branch information
wycats committed Oct 10, 2024
1 parent 0f9b3fc commit 694858a
Show file tree
Hide file tree
Showing 64 changed files with 17,570 additions and 9,838 deletions.
10 changes: 9 additions & 1 deletion .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,14 @@ module.exports = {

settings: {
'import/core-modules': ['require', 'backburner', 'router', '@glimmer/interfaces'],
'import/parsers': {
'@typescript-eslint/parser': ['.ts'],
},
'import/resolver': {
typescript: {
alwaysTryTypes: true,
project: ['./tsconfig.json', './demo/tsconfig.json'],
},
node: {
extensions: ['.js', '.ts', '.d.ts'],
paths: [path.resolve('./packages/')],
Expand All @@ -46,7 +53,7 @@ module.exports = {

parserOptions: {
sourceType: 'module',
project: './tsconfig.json',
project: ['./tsconfig.json', './demo/tsconfig.json'],
tsconfigRootDir: __dirname,
},

Expand Down Expand Up @@ -121,6 +128,7 @@ module.exports = {
'packages/@ember/*/tests/**/*.[jt]s',
'packages/@ember/-internals/*/tests/**/*.[jt]s',
'packages/internal-test-helpers/**/*.[jt]s',
'demo/**/*.[jt]s',
],
env: {
qunit: true,
Expand Down
23 changes: 23 additions & 0 deletions demo/index.html
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>
23 changes: 23 additions & 0 deletions demo/package.json
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"
}
}
13 changes: 13 additions & 0 deletions demo/src/as-module.ts
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));
}
70 changes: 70 additions & 0 deletions demo/src/compiler.ts
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;
}
78 changes: 78 additions & 0 deletions demo/src/main.ts
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));
}
3 changes: 3 additions & 0 deletions demo/src/precompile.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { precompile } from '@glimmer/compiler';

export const precompileTemplate = precompile;
100 changes: 100 additions & 0 deletions demo/src/rewrite.ts
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];
}
20 changes: 20 additions & 0 deletions demo/src/utils/cell.ts
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);
}
}
20 changes: 20 additions & 0 deletions demo/tsconfig.json
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"
]
}
Loading

0 comments on commit 694858a

Please sign in to comment.