Skip to content

Commit

Permalink
Merge pull request #2121 from patricklx/addon-dev-incremental-updates…
Browse files Browse the repository at this point in the history
…-to-output

backport #1855 addon-dev: incremental updates to output
  • Loading branch information
ef4 authored Sep 24, 2024
2 parents 41d1e94 + 47e1e03 commit 5051e05
Show file tree
Hide file tree
Showing 9 changed files with 386 additions and 125 deletions.
3 changes: 1 addition & 2 deletions packages/addon-dev/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@
"fs-extra": "^10.0.0",
"minimatch": "^3.0.4",
"rollup-plugin-copy-assets": "^2.0.3",
"rollup-plugin-delete": "^2.0.0",
"walk-sync": "^3.0.0",
"yargs": "^17.0.1"
},
Expand All @@ -52,7 +51,7 @@
"@types/yargs": "^17.0.3",
"rollup": "^3.23.0",
"tmp": "^0.1.0",
"typescript": "^5.1.6"
"typescript": "^5.4.5"
},
"engines": {
"node": "12.* || 14.* || >= 16"
Expand Down
4 changes: 1 addition & 3 deletions packages/addon-dev/src/rollup-gjs-plugin.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { createFilter } from '@rollup/pluginutils';
import type { Plugin } from 'rollup';
import { readFileSync } from 'fs';
import { Preprocessor } from 'content-tag';

const PLUGIN_NAME = 'rollup-gjs-plugin';
Expand All @@ -14,11 +13,10 @@ export default function rollupGjsPlugin(
return {
name: PLUGIN_NAME,

load(id: string) {
transform(input: string, id: string) {
if (!gjsFilter(id)) {
return null;
}
let input = readFileSync(id, 'utf8');
let code = processor.process(input, {
filename: id,
inline_source_map,
Expand Down
144 changes: 75 additions & 69 deletions packages/addon-dev/src/rollup-hbs-plugin.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
import type { Plugin, PluginContext } from 'rollup';
import { createFilter } from '@rollup/pluginutils';
import type { Plugin, PluginContext, CustomPluginOptions } from 'rollup';
import { readFileSync } from 'fs';
import { correspondingTemplate, hbsToJS } from '@embroider/core';
import minimatch from 'minimatch';
import {
hbsToJS,
templateOnlyComponentSource,
needsSyntheticComponentJS,
syntheticJStoHBS,
} from '@embroider/core';
import { extname } from 'path';

const hbsFilter = createFilter('**/*.hbs?([?]*)');

export default function rollupHbsPlugin({
excludeColocation,
Expand All @@ -12,48 +19,88 @@ export default function rollupHbsPlugin({
return {
name: 'rollup-hbs-plugin',
async resolveId(source: string, importer: string | undefined, options) {
if (options.custom?.embroider?.isExtensionSearch) {
return null;
}

let resolution = await this.resolve(source, importer, {
skipSelf: true,
...options,
});

if (resolution) {
return resolution;
} else {
return maybeSynthesizeComponentJS(
this,
source,
importer,
options,
excludeColocation
if (!resolution && extname(source) === '') {
resolution = await this.resolve(source + '.hbs', importer, {
skipSelf: true,
});
}

if (!resolution) {
let hbsSource = syntheticJStoHBS(source);
if (hbsSource) {
resolution = await this.resolve(hbsSource, importer, {
skipSelf: true,
custom: {
embroider: {
isExtensionSearch: true,
},
},
});
}

if (!resolution) {
return null;
}
}

if (resolution && resolution.id.endsWith('.hbs')) {
let isExcluded = excludeColocation?.some((glob) =>
minimatch(resolution!.id, glob)
);
if (isExcluded) {
return resolution;
}
}

let syntheticId = needsSyntheticComponentJS(source, resolution.id);
if (syntheticId) {
this.addWatchFile(source);
return {
id: syntheticId,
meta: {
'rollup-hbs-plugin': {
type: 'template-only-component-js',
},
},
};
}
},

load(id: string) {
if (hbsFilter(id)) {
return getHbsToJSCode(id);
}
let meta = getMeta(this, id);
if (meta) {
if (meta?.type === 'template-js') {
const hbsFile = id.replace(/\.js$/, '.hbs');
return getHbsToJSCode(hbsFile);
}
if (getMeta(this, id)?.type === 'template-only-component-js') {
this.addWatchFile(id);
return {
code: templateOnlyComponent,
code: templateOnlyComponentSource(),
};
}
},

transform(code: string, id: string) {
let hbsFilename = id.replace(/\.\w{1,3}$/, '') + '.hbs';
if (hbsFilename !== id) {
this.addWatchFile(hbsFilename);
if (getMeta(this, id)?.type === 'template-only-component-js') {
this.addWatchFile(id);
}
}
if (!hbsFilter(id)) {
return null;
}
return hbsToJS(code);
},
};
}

const templateOnlyComponent =
`import templateOnly from '@ember/component/template-only';\n` +
`export default templateOnly();\n`;

type Meta = {
type: 'template-only-component-js' | 'template-js';
type: 'template-only-component-js';
};

function getMeta(context: PluginContext, id: string): Meta | null {
Expand All @@ -64,44 +111,3 @@ function getMeta(context: PluginContext, id: string): Meta | null {
return null;
}
}

function getHbsToJSCode(file: string): { code: string } {
let input = readFileSync(file, 'utf8');
let code = hbsToJS(input);
return {
code,
};
}

async function maybeSynthesizeComponentJS(
context: PluginContext,
source: string,
importer: string | undefined,
options: { custom?: CustomPluginOptions; isEntry: boolean },
excludeColocation: string[] | undefined
) {
let hbsFilename = correspondingTemplate(source);
let templateResolution = await context.resolve(hbsFilename, importer, {
skipSelf: true,
...options,
});
if (!templateResolution) {
return null;
}
let type = excludeColocation?.some((glob) => minimatch(hbsFilename, glob))
? 'template-js'
: 'template-only-component-js';
// we're trying to resolve a JS module but only the corresponding HBS
// file exists. Synthesize the JS. The meta states if the hbs corresponds
// to a template-only component or a simple template like a route template.
return {
id: templateResolution.id.replace(/\.hbs$/, '.js'),
meta: {
'rollup-hbs-plugin': {
type,
},
},
};
}

const hbsFilter = createFilter('**/*.hbs');
79 changes: 79 additions & 0 deletions packages/addon-dev/src/rollup-incremental-plugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import walkSync from 'walk-sync';
import { rmSync } from 'fs';
import { join } from 'path';
import type { Plugin } from 'rollup';
import { existsSync } from 'fs-extra';

export default function incremental(): Plugin {
const generatedAssets = new Map();
const generatedFiles = new Set<string>();

function isEqual(v1: string | Uint8Array, v2: string | Uint8Array): boolean {
if (typeof v1 === 'string' && typeof v2 === 'string') {
return v1 === v2;
}
if (Buffer.isBuffer(v1) && Buffer.isBuffer(v2)) {
return v1.equals(v2);
}
return false;
}

let firstTime = true;

function initGeneratedFiles(outDir: string) {
if (existsSync(outDir)) {
const files = walkSync(outDir, {
globs: ['*/**'],
directories: false,
});
for (const file of files) {
generatedFiles.add(file);
}
}
}

function deleteRemovedFiles(bundle: Record<string, any>, outDir: string) {
for (const file of generatedFiles) {
if (!bundle[file]) {
generatedAssets.delete(file);
rmSync(join(outDir, file));
}
}
generatedFiles.clear();
for (const file of Object.keys(bundle)) {
generatedFiles.add(file);
}
}

function syncFiles(bundle: Record<string, any>) {
for (const checkKey of Object.keys(bundle)) {
if (bundle[checkKey]) {
let module = bundle[checkKey] as any;
let code = module.source || module.code;
if (
generatedAssets.has(checkKey) &&
isEqual(code, generatedAssets.get(checkKey))
) {
delete bundle[checkKey];
} else {
generatedAssets.set(checkKey, code);
}
}
}
}

return {
name: 'incremental',
generateBundle(options, bundle) {
if (firstTime) {
firstTime = false;
initGeneratedFiles(options.dir!);
}
if (existsSync(options.dir!)) {
deleteRemovedFiles(bundle, options.dir!);
}

syncFiles(bundle);
},
};
}
12 changes: 6 additions & 6 deletions packages/addon-dev/src/rollup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,13 @@ import { default as hbs } from './rollup-hbs-plugin';
import { default as gjs } from './rollup-gjs-plugin';
import { default as publicEntrypoints } from './rollup-public-entrypoints';
import { default as appReexports } from './rollup-app-reexports';
import type { Options as DelOptions } from 'rollup-plugin-delete';
import { default as clean } from 'rollup-plugin-delete';
import { default as keepAssets } from './rollup-keep-assets';
import { default as dependencies } from './rollup-addon-dependencies';
import {
default as publicAssets,
type PublicAssetsOptions,
} from './rollup-public-assets';
import { default as clean } from './rollup-incremental-plugin';
import type { Plugin } from 'rollup';

export class Addon {
Expand Down Expand Up @@ -64,10 +63,11 @@ export class Addon {
return gjs(options);
}

// By default rollup does not clear the output directory between builds. This
// does that.
clean(options: DelOptions) {
return clean({ targets: `${this.#destDir}/*`, ...options });
// this does incremental updates to the dist files and also deletes files that are not part of the generated bundle
// rollup already supports incremental transforms of files,
// this extends it to the dist files
clean() {
return clean();
}

// V2 Addons are allowed to contain imports of .css files. This tells rollup
Expand Down
39 changes: 39 additions & 0 deletions packages/shared-internals/src/colocation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { existsSync } from 'fs-extra';
import { cleanUrl } from './paths';
import { sep } from 'path';

export function syntheticJStoHBS(source: string): string | null {
// explicit js is the only case we care about here. Synthetic template JS is
// only ever JS (never TS or anything else). And extensionless imports are
// handled by the default resolving system doing extension search.
if (cleanUrl(source).endsWith('.js')) {
return source.replace(/.js(\?.*)?/, '.hbs$1');
}

return null;
}

export function needsSyntheticComponentJS(requestedSpecifier: string, foundFile: string): string | null {
requestedSpecifier = cleanUrl(requestedSpecifier);
foundFile = cleanUrl(foundFile);
if (
discoveredImplicitHBS(requestedSpecifier, foundFile) &&
!foundFile.split(sep).join('/').endsWith('/template.hbs') &&
!correspondingJSExists(foundFile)
) {
return foundFile.slice(0, -3) + 'js';
}
return null;
}

function discoveredImplicitHBS(source: string, id: string): boolean {
return !source.endsWith('.hbs') && id.endsWith('.hbs');
}

function correspondingJSExists(id: string): boolean {
return ['js', 'ts'].some(ext => existsSync(id.slice(0, -3) + ext));
}

export function templateOnlyComponentSource() {
return `import templateOnly from '@ember/component/template-only';\nexport default templateOnly();\n`;
}
1 change: 1 addition & 0 deletions packages/shared-internals/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,4 @@ export {
export { locateEmbroiderWorkingDir } from './working-dir';

export * from './dep-validation';
export * from './colocation';
Loading

0 comments on commit 5051e05

Please sign in to comment.