Skip to content

Commit

Permalink
Merge pull request #1836 from BlueCutOfficial/replace-content-for-wit…
Browse files Browse the repository at this point in the history
…h-valid-html

Replace content-for using a Vite plugin
  • Loading branch information
mansona authored Apr 17, 2024
2 parents d740ef0 + 1b662fe commit fad2387
Show file tree
Hide file tree
Showing 10 changed files with 277 additions and 5 deletions.
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 @@ -881,6 +883,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 @@ -978,6 +981,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;
},
};
}
2 changes: 2 additions & 0 deletions tests/addon-template/vite.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
optimizeDeps,
compatPrebuild,
assets,
contentFor,
} from "@embroider/vite";
import { resolve } from "path";
import { babel } from "@rollup/plugin-babel";
Expand All @@ -26,6 +27,7 @@ export default defineConfig(({ mode }) => {
resolver(),
compatPrebuild(),
assets(),
contentFor(),

babel({
babelHelpers: "runtime",
Expand Down
2 changes: 2 additions & 0 deletions tests/app-template/vite.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
optimizeDeps,
compatPrebuild,
assets,
contentFor,
} from "@embroider/vite";
import { resolve } from "path";
import { babel } from "@rollup/plugin-babel";
Expand All @@ -26,6 +27,7 @@ export default defineConfig(({ mode }) => {
resolver(),
compatPrebuild(),
assets(),
contentFor(),

babel({
babelHelpers: "runtime",
Expand Down
Loading

0 comments on commit fad2387

Please sign in to comment.