Skip to content

Commit

Permalink
Merge pull request #1632 from chancancode/colocation-bug
Browse files Browse the repository at this point in the history
Fix co-located components regressions (#1619)
  • Loading branch information
ef4 authored Oct 10, 2023
2 parents 77ed393 + e51940a commit 157dd62
Show file tree
Hide file tree
Showing 2 changed files with 425 additions and 59 deletions.
111 changes: 81 additions & 30 deletions packages/compat/src/synthesize-template-only-components.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,29 @@
import Plugin from 'broccoli-plugin';
import type { Node } from 'broccoli-node-api';
import { join, basename } from 'path';
import walkSync from 'walk-sync';
import { remove, outputFileSync, pathExistsSync } from 'fs-extra';
import { join, basename, extname } from 'path';
import walkSync, { type Entry } from 'walk-sync';
import { removeSync, outputFileSync, pathExistsSync, readFileSync } from 'fs-extra';

const source = `import templateOnlyComponent from '@ember/component/template-only';
export default templateOnlyComponent();`;

const jsExtensions = ['.js', '.ts', '.mjs', '.mts'];

type Emitted =
| { type: 'template-only-component'; outputPath: string }
| { type: 'template-import'; outputPath: string; mtime: number };

type TemplateOnly = { template: Entry; javascript: undefined };
type JavaScriptOnly = { template: undefined; javascript: Entry };
type Colocated = { template: Entry; javascript: Entry };
type ComponentFiles = TemplateOnly | JavaScriptOnly | Colocated;

function importTemplate(files: { template: Entry }): string {
return `/* import __COLOCATED_TEMPLATE__ from './${basename(files.template.relativePath)}'; */\n`;
}

export default class SynthesizeTemplateOnlyComponents extends Plugin {
private emitted = new Set() as Set<string>;
private emitted = new Map() as Map<string, Emitted>;
private allowedPaths: string[];
private templateExtensions: string[];

Expand All @@ -25,54 +38,92 @@ export default class SynthesizeTemplateOnlyComponents extends Plugin {
}

async build() {
let unneeded = new Set(this.emitted.keys());
for (let dir of this.allowedPaths) {
let { needed, seen } = this.crawl(join(this.inputPaths[0], dir));
for (let file of needed) {
let fullName = join(this.outputPath, dir, file);
if (seen.has(file)) {
this.remove(fullName);
let entries = this.crawl(join(this.inputPaths[0], dir));
for (let [name, files] of entries) {
let fullName = join(this.outputPath, dir, name);
unneeded.delete(fullName);
if (files.javascript && files.template) {
this.addTemplateImport(fullName, files);
} else if (files.template) {
this.addTemplateOnlyComponent(fullName, files);
} else {
this.add(fullName);
this.remove(fullName);
}
}
}
for (let fullName of unneeded) {
this.remove(fullName);
}
}
private add(filename: string) {
if (!this.emitted.has(filename)) {

private addTemplateOnlyComponent(filename: string, files: TemplateOnly) {
const emitted = this.emitted.get(filename);

if (emitted?.type !== 'template-only-component') {
// special case: ember-cli doesn't allow template-only components named
// "template.hbs" because there are too many people doing a "pods-like"
// layout that happens to match that pattern.🤮
if (basename(filename) !== 'template') {
outputFileSync(filename + '.js', source, 'utf8');
const outputPath = filename + '.js';
outputFileSync(outputPath, importTemplate(files) + source, 'utf8');
this.emitted.set(filename, { type: 'template-only-component', outputPath });

if (emitted && emitted.outputPath !== outputPath) {
removeSync(emitted.outputPath);
}
}
}
}

private addTemplateImport(filename: string, files: Colocated) {
const emitted = this.emitted.get(filename);
const mtime = files.javascript.mtime;

if (!(emitted?.type === 'template-import' && emitted.mtime === mtime)) {
const inputSource = readFileSync(files.javascript.fullPath, { encoding: 'utf8' });
const outputPath = filename + extname(files.javascript.relativePath);
// If we are ok with appending instead, copy + append maybe more efficient?
outputFileSync(outputPath, importTemplate(files) + inputSource, 'utf8');
this.emitted.set(filename, { type: 'template-import', outputPath, mtime });

if (emitted && emitted.outputPath !== outputPath) {
removeSync(emitted.outputPath);
}
this.emitted.add(filename);
}
}

private remove(filename: string) {
if (this.emitted.has(filename)) {
remove(filename + '.js');
const emitted = this.emitted.get(filename);

if (emitted) {
removeSync(emitted.outputPath);
this.emitted.delete(filename);
}
}

private crawl(dir: string) {
const needed = new Set<string>();
const seen = new Set<string>();
private crawl(dir: string): Map<string, ComponentFiles> {
const entries = new Map<string, ComponentFiles>();

if (pathExistsSync(dir)) {
for (let file of walkSync(dir, { directories: false })) {
for (const templateExtension of this.templateExtensions) {
if (file.endsWith(templateExtension)) {
needed.add(file.slice(0, -1 * templateExtension.length));
} else {
const jsExtension = jsExtensions.find(ext => file.endsWith(ext));
if (jsExtension) {
seen.add(file.slice(0, -1 * jsExtension.length));
}
}
for (let entry of walkSync.entries(dir, { directories: false })) {
const templateExtension = this.templateExtensions.find(ext => entry.relativePath.endsWith(ext));
if (templateExtension) {
const key = entry.relativePath.slice(0, -1 * templateExtension.length);
entries.set(key, { template: entry, javascript: entries.get(key)?.javascript });
continue;
}

const jsExtension = jsExtensions.find(ext => entry.relativePath.endsWith(ext));
if (jsExtension) {
const key = entry.relativePath.slice(0, -1 * jsExtension.length);
entries.set(key, { template: entries.get(key)?.template, javascript: entry });
continue;
}
}
}
return { needed, seen };

return entries;
}
}
Loading

0 comments on commit 157dd62

Please sign in to comment.