Skip to content
Open
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
5 changes: 3 additions & 2 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
{
"typescript.tsdk": "node_modules/typescript/lib"
}
"typescript.tsdk": "node_modules/typescript/lib",
"glint.libraryPath": "./ember-async-data"
}
6 changes: 1 addition & 5 deletions ember-async-data/.eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,7 @@ module.exports = {
ecmaVersion: 'latest',
},
plugins: ['ember'],
extends: [
'eslint:recommended',
'plugin:ember/recommended',
'plugin:prettier/recommended',
],
extends: ['eslint:recommended', 'plugin:ember/recommended', 'prettier'],
env: {
browser: true,
},
Expand Down
1 change: 1 addition & 0 deletions ember-async-data/.prettierrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ module.exports = {
},
},
],
plugins: ['prettier-plugin-ember-template-tag'],
};
1 change: 1 addition & 0 deletions ember-async-data/babel.config.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"presets": [["@babel/preset-typescript"]],
"plugins": [
"ember-template-imports/src/babel-plugin",
"@embroider/addon-dev/template-colocation-plugin",
["@babel/plugin-transform-typescript", { "allowDeclareFields": true }],
["@babel/plugin-proposal-decorators", { "version": "legacy" }],
Expand Down
14 changes: 9 additions & 5 deletions ember-async-data/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,10 @@
"lint:fix": "concurrently 'npm:lint:*:fix' --names 'fix:'",
"lint:hbs": "ember-template-lint . --no-error-on-unmatched-pattern",
"lint:js": "eslint . --cache",
"lint:prettier": "prettier . --check",
"lint:hbs:fix": "ember-template-lint . --fix --no-error-on-unmatched-pattern",
"lint:js:fix": "eslint . --fix",
"lint:prettier:fix": "prettier . --write",
"lint:types": "glint",
"start": "concurrently 'npm:start:*'",
"start:js": "rollup --config --watch --no-watch.clearScreen",
Expand All @@ -47,24 +49,26 @@
"@embroider/addon-dev": "^4.1.0",
"@glimmer/component": "^1.1.2",
"@glimmer/tracking": "^1.1.2",
"@glint/core": "^1.0.2",
"@glint/environment-ember-loose": "^1.0.2",
"@glint/template": "^1.0.2",
"@glint/core": "^1.1.0",
"@glint/environment-ember-loose": "^1.1.0",
"@glint/environment-ember-template-imports": "^1.1.0",
"@glint/template": "^1.1.0",
"@rollup/plugin-babel": "^6.0.3",
"@rollup/plugin-node-resolve": "^15.1.0",
"@tsconfig/ember": "^2.0.0",
"@typescript-eslint/eslint-plugin": "^6.2.1",
"@typescript-eslint/parser": "^6.2.1",
"concurrently": "^8.2.0",
"ember-source": "^4.12.3",
"ember-source": "^5.2.0",
"ember-template-lint": "^5.11.2",
"eslint": "^8.46.0",
"eslint-config-prettier": "^8.8.0",
"eslint-plugin-ember": "^11.10.0",
"eslint-plugin-n": "^16.1.0",
"eslint-plugin-prettier": "^4.2.1",
"expect-type": "^0.16.0",
"prettier": "^2.8.8",
"prettier": "^3.0.3",
"prettier-plugin-ember-template-tag": "^1.1.0",
"rollup": "^3.28.0",
"typescript": "~5.2.2"
},
Expand Down
75 changes: 75 additions & 0 deletions ember-async-data/rollup-plugin-template-tag.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import fs from "node:fs/promises";
import path from "node:path";
import { preprocessEmbeddedTemplates } from "ember-template-imports/lib/preprocess-embedded-templates.js";
import {
TEMPLATE_TAG_NAME,
TEMPLATE_TAG_PLACEHOLDER,
} from "ember-template-imports/lib/util.js";

export default function firstClassComponentTemplates() {
return {
name: "preprocess-fccts",
async resolveId(source, importer, options) {
if (source.endsWith(".hbs")) return;

for (let ext of ["", ".gjs", ".gts"]) {
let result = await this.resolve(source + ext, importer, {
...options,
skipSelf: true,
});

if (result?.external) {
return;
}

if (FCCT_EXTENSION.test(result?.id)) {
return resolutionFor(result.id);
}
}
},

async load(id) {
let originalId = this.getModuleInfo(id)?.meta?.fccts?.originalId ?? id;

if (originalId !== id) {
this.addWatchFile(originalId);
}

if (FCCT_EXTENSION.test(originalId)) {
return await preprocessTemplates(originalId);
}
},
};
}

const FCCT_EXTENSION = /\.g([jt]s)$/;

function resolutionFor(originalId) {
return {
id: originalId.replace(FCCT_EXTENSION, ".$1"),
meta: {
fccts: { originalId },
},
};
}

async function preprocessTemplates(id) {
let ember = (await import("ember-source")).default;
let contents = await fs.readFile(id, "utf-8");

// This is basically taken directly from `ember-template-imports`
let result = preprocessEmbeddedTemplates(contents, {
relativePath: path.relative(".", id),

getTemplateLocalsRequirePath: ember.absolutePaths.templateCompiler,
getTemplateLocalsExportPath: "_GlimmerSyntax.getTemplateLocals",

templateTag: TEMPLATE_TAG_NAME,
templateTagReplacement: TEMPLATE_TAG_PLACEHOLDER,

includeSourceMaps: true,
includeTemplateTokens: true,
});

return result.output;
}
29 changes: 16 additions & 13 deletions ember-async-data/rollup.config.mjs
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import { babel } from '@rollup/plugin-babel';
import { nodeResolve } from '@rollup/plugin-node-resolve';
import { Addon } from '@embroider/addon-dev/rollup';
import { babel } from "@rollup/plugin-babel";
import { nodeResolve } from "@rollup/plugin-node-resolve";
import { Addon } from "@embroider/addon-dev/rollup";
import templateTag from "./rollup-plugin-template-tag.mjs";

const addon = new Addon({
srcDir: 'src',
destDir: 'dist',
srcDir: "src",
destDir: "dist",
});

// Add extensions here, such as ts, gjs, etc that you may import
const extensions = ['.js', '.ts'];
const extensions = [".js", ".ts", ".gjs", ".gts"];

export default {
// This provides defaults that work well alongside `publicEntrypoints` below.
Expand All @@ -19,16 +20,18 @@ export default {
// These are the modules that users should be able to import from your
// addon. Anything not listed here may get optimized away.
addon.publicEntrypoints([
'helpers/**/*.js',
'index.js',
'template-registry.js',
'tracked-async-data.js',
"helpers/**/*.js",
"index.js",
"template-registry.js",
"tracked-async-data.js",
]),

// These are the modules that should get reexported into the traditional
// "app" tree. Things in here should also be in publicEntrypoints above, but
// not everything in publicEntrypoints necessarily needs to go here.
addon.appReexports(['helpers/**/*.js']),
addon.appReexports(["helpers/**/*.js"]),

templateTag(),

// Follow the V2 Addon rules about dependencies. Your code can import from
// `dependencies` and `peerDependencies` as well as standard Ember-provided
Expand All @@ -43,7 +46,7 @@ export default {
// babel.config.json.
babel({
extensions,
babelHelpers: 'bundled',
babelHelpers: "bundled",
}),

// Allows rollup to resolve imports of files with the specified extensions
Expand All @@ -54,7 +57,7 @@ export default {

// addons are allowed to contain imports of .css files, which we want rollup
// to leave alone and keep in the published output.
addon.keepAssets(['**/*.css']),
addon.keepAssets(["**/*.css"]),

// Remove leftover build artifacts when starting a new build.
addon.clean(),
Expand Down
81 changes: 81 additions & 0 deletions ember-async-data/src/components/async.gts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import Component from '@glimmer/component';
import { cached } from '@glimmer/tracking';
import TrackedAsyncData from '../tracked-async-data';

export interface AsyncSignature<T> {
Args: {
/** The data to `await`. */
data: T | Promise<T>;
};
Blocks: {
/** To render while waiting on the promise */
pending: [];
/** To render once the promise has resolved, with the resolved value. */
resolved: [value: T];
/** To render if the promise has rejected, with the associated reason. */
rejected: [reason: unknown];
};
}

/**
Render a `TrackedAsyncData` in a template, with named blocks for each of the
states the value may be in. This is somewhat nicer than doing the same with
an `if`/`else if` chain, and composes nicely with other template primitives.

It is also less error prone, in that you *cannot* access the value of the data
in the `pending` or `rejected` blocks: it is only yielded by the `resolved`
named block.

```ts
import { Async } from 'ember-async-data'
import LoadingSpinner from './loading-spinner';

function dumpError(error: unknown) {
return JSON.stringify(error);
}

let example = new TrackedAsyncData("hello");

<template>
<Async @data={{example}}>
<:pending>
<LoadingSpinner />
</:pending>

<:resolved as |value|>
<SomeComponent @arg={{value}} />
</:resolved>

<:rejected as |reason|>
<p>Whoops, something went wrong!</p>
<pre><code>
{{dumpErr reason}}
</code></pre>
</:rejected>
</Async>
</template>
```
*/
// IMPLEMENTATION NOTE: yes, future maintainer, this *does* have to be a class,
// not a template-only component. This is because it must be generic over the
// type passed in so that the yielded type can preserve that ("parametricity").
// That is: if we pass in a `TrackedAsyncData<string>`, the value yielded from
// the `resolved` named block should be a string. The only things which can
// preserve the type parameter that way are functions and classes, and we do not
// presently have a function-based way to define components, so we have to use
// a class instead to preserve the type!
export default class Async<T> extends Component<AsyncSignature<T>> {
@cached get data() {
return new TrackedAsyncData(this.args.data);
}

<template>
{{#if this.data.isResolved}}
{{yield this.data.value to='resolved'}}
{{else if this.data.isRejected}}
{{yield this.data.error to='rejected'}}
{{else}}
{{yield to='pending'}}
{{/if}}
</template>
}
1 change: 1 addition & 0 deletions ember-async-data/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export { default as TrackedAsyncData } from './tracked-async-data';
export { load } from './helpers/load';
export { default as Async } from './components/async';
2 changes: 2 additions & 0 deletions ember-async-data/src/template-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
// See https://typed-ember.gitbook.io/glint/using-glint/ember/authoring-addons

import type Load from './helpers/load';
import type Async from './components/async';

export default interface Registry {
load: typeof Load;
Async: typeof Async;
}
Loading