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

Start implementing the runtime compiler #20774

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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
4 changes: 2 additions & 2 deletions .github/actions/setup/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ runs:
- uses: pnpm/action-setup@v4
name: Install pnpm
with:
version: 8
version: 9
run_install: false
- name: Install Node.js
uses: actions/setup-node@v4
Expand All @@ -29,7 +29,7 @@ runs:
path: .puppeteer-cache
key: ${{ runner.os }}-puppeteer-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-puppeteer-
${{ runner.os }}-puppeteer-

- run: pnpm install ${{ fromJSON('{"false":"--no-lockfile", "true":"--frozen-lockfile"}')[inputs.use_lockfile] }}
shell: bash
22 changes: 22 additions & 0 deletions demo/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<!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",
"@/": "./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));
}
37 changes: 37 additions & 0 deletions demo/src/compiler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import initSwc, { transform } 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 });

return transform(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,
},
},
});
};
}

const GJS_COMPILER = new GjsCompiler();

export const compile = GJS_COMPILER.compile;
80 changes: 80 additions & 0 deletions demo/src/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
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';

await delay(1000);

function delay(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
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/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"
]
}
7 changes: 7 additions & 0 deletions demo/vite.config.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { defineConfig } from 'vite';

export default defineConfig({
optimizeDeps: {
exclude: ['content-tag', '@swc/wasm-web'],
},
});
Loading
Loading