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

Replace content-for using a Vite plugin #1836

Merged
merged 9 commits into from
Apr 17, 2024
Merged
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
9 changes: 9 additions & 0 deletions packages/compat/src/compat-app-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ import assertNever from 'assert-never';
import { Memoize } from 'typescript-memoize';
import { join, dirname } from 'path';
import resolve from 'resolve';
import type ContentForConfig from './content-for-config';
import type { V1Config } from './v1-config';
import type { AddonMeta, Package, PackageInfo } from '@embroider/core';
import { ensureDirSync, copySync, readdirSync, pathExistsSync } from 'fs-extra';
Expand All @@ -74,6 +75,7 @@ export class CompatAppBuilder {
private options: Required<Options>,
private compatApp: CompatApp,
private configTree: V1Config,
private contentForTree: ContentForConfig,
private synthVendor: Package,
private synthStyles: Package
) {}
Expand Down Expand Up @@ -896,6 +898,7 @@ export class CompatAppBuilder {

let resolverConfig = this.resolverConfig(appFiles);
this.addResolverConfig(resolverConfig);
this.addContentForConfig(this.contentForTree.readContents());
let babelConfig = await this.babelConfig(resolverConfig);
this.addBabelConfig(babelConfig);
writeFileSync(
Expand Down Expand Up @@ -993,6 +996,12 @@ export class CompatAppBuilder {
outputJSONSync(join(locateEmbroiderWorkingDir(this.compatApp.root), 'resolver.json'), config, { spaces: 2 });
}

private addContentForConfig(contentForConfig: any) {
outputJSONSync(join(locateEmbroiderWorkingDir(this.compatApp.root), 'content-for.json'), contentForConfig, {
spaces: 2,
});
}

private shouldSplitRoute(routeName: string) {
return (
!this.options.splitAtRoutes ||
Expand Down
38 changes: 34 additions & 4 deletions packages/compat/src/compat-app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import buildFunnel from 'broccoli-funnel';
import mergeTrees from 'broccoli-merge-trees';
import { WatchedDir } from 'broccoli-source';
import resolve from 'resolve';
import ContentForConfig from './content-for-config';
import { V1Config, WriteV1Config } from './v1-config';
import { WriteV1AppBoot, ReadV1AppBoot } from './v1-appboot';
import type { AddonMeta, EmberAppInstance, OutputFileToInputFileMap, PackageInfo } from '@embroider/core';
Expand Down Expand Up @@ -147,6 +148,19 @@ export default class CompatApp {
}
}

@Memoize()
private get contentFor(): ContentForConfig {
const configPaths = [
{ file: '/index.html', path: join('environments', `${this.legacyEmberAppInstance.env}.json`) },
];
if (this.shouldBuildTests) configPaths.push({ file: '/tests/index.html', path: join('environments', `test.json`) });
return new ContentForConfig(this.configTree, {
availableContentForTypes: this.options.availableContentForTypes,
configPaths,
pattern: this.filteredPatternsByContentFor.contentFor,
});
}

get autoRun(): boolean {
return this.legacyEmberAppInstance.options.autoRun;
}
Expand Down Expand Up @@ -180,6 +194,14 @@ export default class CompatApp {
});
}

private get filteredPatternsByContentFor() {
const filter = '/{{content-for [\'"](.+?)["\']}}/g';
return {
contentFor: this.configReplacePatterns.find((pattern: any) => filter.includes(pattern.match.toString())),
others: this.configReplacePatterns.filter((pattern: any) => !filter.includes(pattern.match.toString())),
};
}

private get htmlTree() {
if (this.legacyEmberAppInstance.tests) {
return mergeTrees([this.indexTree, this.testIndexTree]);
Expand All @@ -199,7 +221,7 @@ export default class CompatApp {
return new this.configReplace(index, this.configTree, {
configPath: join('environments', `${this.legacyEmberAppInstance.env}.json`),
files: [indexFilePath],
patterns: this.configReplacePatterns,
patterns: this.filteredPatternsByContentFor.others,
annotation: 'ConfigReplace/indexTree',
});
}
Expand All @@ -214,7 +236,7 @@ export default class CompatApp {
return new this.configReplace(index, this.configTree, {
configPath: join('environments', `test.json`),
files: ['tests/index.html'],
patterns: this.configReplacePatterns,
patterns: this.filteredPatternsByContentFor.others,
annotation: 'ConfigReplace/testIndexTree',
});
}
Expand Down Expand Up @@ -780,6 +802,7 @@ export default class CompatApp {
private inTrees(prevStageTree: BroccoliNode) {
let publicTree = this.publicTree;
let configTree = this.config;
let contentForTree = this.contentFor;

if (this.options.extraPublicTrees.length > 0) {
publicTree = mergeTrees([publicTree, ...this.options.extraPublicTrees].filter(Boolean) as BroccoliNode[]);
Expand All @@ -790,6 +813,7 @@ export default class CompatApp {
htmlTree: this.htmlTree,
publicTree,
configTree,
contentForTree,
appBootTree: this.appBoot,
prevStageTree,
};
Expand All @@ -811,7 +835,12 @@ export default class CompatApp {
}
}

private async instantiate(root: string, packageCache: RewrittenPackageCache, configTree: V1Config) {
private async instantiate(
root: string,
packageCache: RewrittenPackageCache,
configTree: V1Config,
contentForTree: ContentForConfig
) {
let origAppPkg = this.appPackage();
let movedAppPkg = packageCache.withRewrittenDeps(origAppPkg);
let workingDir = locateEmbroiderWorkingDir(this.root);
Expand All @@ -822,6 +851,7 @@ export default class CompatApp {
this.options,
this,
configTree,
contentForTree,
packageCache.get(join(workingDir, 'rewritten-packages', '@embroider', 'synthesized-vendor')),
packageCache.get(join(workingDir, 'rewritten-packages', '@embroider', 'synthesized-styles'))
);
Expand All @@ -837,7 +867,7 @@ export default class CompatApp {
if (!this.active) {
let { outputPath } = await prevStage.ready();
let packageCache = RewrittenPackageCache.shared('embroider', this.root);
this.active = await this.instantiate(outputPath, packageCache, inTrees.configTree);
this.active = await this.instantiate(outputPath, packageCache, inTrees.configTree, inTrees.contentForTree);
resolve({ outputPath });
}
await this.active.build(treePaths);
Expand Down
67 changes: 67 additions & 0 deletions packages/compat/src/content-for-config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import Plugin from 'broccoli-plugin';
import type { Node } from 'broccoli-node-api';
import { readFileSync } from 'fs-extra';
import { join } from 'path';

export default class ContentForConfig extends Plugin {
// The object keys are the content types and each value is the HTML
// code that should replace the corresponding {{content-for}}
// Example: { body: '<p>This snippet replaces content-for \"body\" in the app index.html</p>' }
private contentFor: any;

private defaultContentForTypes = [
'head',
'test-head',
'head-footer',
'test-head-footer',
'body',
'test-body',
'body-footer',
'test-body-footer',
'config-module',
'app-boot',
];

constructor(configTree: Node, private options: any) {
super([configTree], {
annotation: 'embroider:content-for-config',
persistentOutput: true,
needsCache: false,
});
}

readContents() {
if (!this.contentFor) {
throw new Error(`ContentForConfig not available until after the build`);
}
return this.contentFor;
}

build() {
if (!this.contentFor) this.contentFor = {};
const availableContentForTypes = this.options.availableContentForTypes ?? [];
const extendedContentTypes = new Set([...this.defaultContentForTypes, ...availableContentForTypes]);

let appConfig = this.getAppConfig();
appConfig.forEach((configPath: { file: string; json: any }) => {
extendedContentTypes.forEach(contentType => {
const matchExp = this.options.pattern.match;
if (!this.contentFor[configPath.file]) this.contentFor[configPath.file] = {};
if (!this.contentFor[configPath.file][contentType]) {
let contents = this.options.pattern.replacement.call(null, configPath.json, matchExp, contentType);
this.contentFor[configPath.file][contentType] = contents;
}
});
});
}

getAppConfig() {
return this.options.configPaths.map((configPath: { file: string; path: string }) => {
let config = readFileSync(join(this.inputPaths[0], configPath.path), { encoding: 'utf8' });
return {
file: configPath.file,
json: JSON.parse(config),
};
});
}
}
8 changes: 8 additions & 0 deletions packages/compat/src/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,13 @@ export default interface Options extends CoreOptions {
// it on in production. But it can be helpful when testing how much of your
// app is able to work with staticComponents enabled.
allowUnsafeDynamicComponents?: boolean;

// Allows you to customize the list of content types addons use to provide HTML
// to {{content-for}}. By default, the following content types are expected:
// 'head', 'test-head', 'head-footer', 'test-head-footer', 'body', 'test-body',
// 'body-footer', 'test-body-footer'. You need to use this config only to extend
// this list.
availableContentForTypes?: string[];
}

const defaults = Object.assign(coreWithDefaults(), {
Expand All @@ -106,6 +113,7 @@ const defaults = Object.assign(coreWithDefaults(), {
workspaceDir: null,
packageRules: [],
allowUnsafeDynamicComponents: false,
availableContentForTypes: [],
});

export function optionsWithDefaults(options?: Options): Required<Options> {
Expand Down
1 change: 1 addition & 0 deletions packages/vite/index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ export * from './src/template-tag.js';
export * from './src/optimize-deps.js';
export * from './src/build.js';
export * from './src/assets.js';
export * from './src/content-for.js';
19 changes: 19 additions & 0 deletions packages/vite/src/content-for.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import type { Plugin } from 'vite';
import { readJSONSync } from 'fs-extra';
import { join } from 'path';
import { locateEmbroiderWorkingDir } from '@embroider/core';

export function contentFor(): Plugin {
return {
name: 'embroider-content-for',

transformIndexHtml(html, { path }) {
let config: any = readJSONSync(join(locateEmbroiderWorkingDir(process.cwd()), 'content-for.json'));
let contentsForConfig = config[path];
for (const [contentType, htmlContent] of Object.entries(contentsForConfig)) {
html = html.replace(`{{content-for "${contentType}"}}`, `${htmlContent}`);
}
return html;
},
};
}
Loading
Loading