Skip to content

Commit

Permalink
Add the template-plugin from #174
Browse files Browse the repository at this point in the history
  • Loading branch information
NullVoxPopuli committed May 22, 2024
1 parent 80859f5 commit 056af8a
Show file tree
Hide file tree
Showing 7 changed files with 249 additions and 0 deletions.
1 change: 1 addition & 0 deletions ember-scoped-css/build.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const buildFiles = [
'src/build/app-dependency-loader.js',
'src/build/ember-classic-support.js',
'src/build/babel-plugin.js',
'src/build/template-plugin.js',
];

const external = [...Object.keys(require('./package.json').dependencies)];
Expand Down
4 changes: 4 additions & 0 deletions ember-scoped-css/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@
"./babel-plugin": {
"import": "./src/build/babel-plugin.js",
"require": "./dist/cjs/babel-plugin.cjs"
},
"./template-plugin": {
"import": "./src/build/template-plugin.js",
"require": "./dist/cjs/template-plugin.cjs"
}
},
"scripts": {
Expand Down
76 changes: 76 additions & 0 deletions ember-scoped-css/src/build/template-plugin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/**
* @typedef {import('@glimmer/syntax').ASTPlugin} ASTPlugin
* @typedef {import('@glimmer/syntax').ASTPluginEnvironment} ASTPluginEnvironment
*
*/

import { getCSSInfo } from '../lib/css/utils.js';
import { fixFilename } from '../lib/path/template-transform-paths.js';
import {
appPath,
cssPathFor,
hashFromModulePath,
isRelevantFile,
} from '../lib/path/utils.js';
import { templatePlugin } from '../lib/rewriteHbs.js';

const noopPlugin = {
name: 'ember-scoped-css:noop',
visitor: {},
};

/**
* @returns {ASTPlugin}
*/
export function createPlugin(config) {
/**
*
* @param {ASTPluginEnvironment} env
*/
return function scopedCss(env) {
let isRelevant = isRelevantFile(env.filename, config.additionalRoots);

if (!isRelevant) {
return noopPlugin;
}

let absolutePath = fixFilename(env.filename);
let modulePath = appPath(absolutePath);

let cssPath = cssPathFor(absolutePath);
let info = getCSSInfo(cssPath);
let postfix = hashFromModulePath(modulePath);

if (!info) {
return noopPlugin;
}

let visitors = templatePlugin({
classes: info.classes,
tags: info.tags,
postfix,
});

return {
name: 'ember-scoped-css:template-plugin',
visitor: {
// Stack Manager
...visitors,
// Visitors broken out like this so we can conditionally
// debug based on file path.
AttrNode(...args) {
return visitors.AttrNode(...args);
},
ElementNode(...args) {
return visitors.ElementNode(...args);
},
MustacheStatement(...args) {
return visitors.MustacheStatement(...args);
},
SubExpression(...args) {
return visitors.SubExpression(...args);
},
},
};
};
}
17 changes: 17 additions & 0 deletions ember-scoped-css/src/lib/css/utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { existsSync, readFileSync } from 'fs';

import getClassesTagsFromCss from '../getClassesTagsFromCss';

/**
* @param {string} cssPath path to a CSS file
*/
export function getCSSInfo(cssPath) {
if (!existsSync(cssPath)) {
return null;
}

let css = readFileSync(cssPath, 'utf8');
let result = getClassesTagsFromCss(css);

return result;
}
103 changes: 103 additions & 0 deletions ember-scoped-css/src/lib/path/template-transform-paths.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import path from 'node:path';

import { existsSync } from 'fs';

import { findWorkspacePath, withoutExtension } from './utils';

/**
* template plugins do not hand us the correct file path.
* additionally, we may not be able to rely on this data in the future,
* so this functions acts as a means of normalizing _whatever_ we're given
* in the future.
*
* @param {string} filename
* @returns {string} the absolute path to the file
*/
export function fixFilename(filename) {
let fileName = filename;
let workspace = findWorkspacePath(fileName);

/**
* ember-source 5.8:
* - the filename looks like an absolute path, but swapped out the 'app' part of the path
* with the module name, so the file paths never exist on disk
*/
if (!fileName.includes('/app/')) {
let maybeModule = fileName.replace(workspace, '');
let [maybeScope, ...rest] = maybeModule.split('/').filter(Boolean);
let parts = rest;

if (maybeScope.startsWith('@')) {
let [, ...rester] = rest;

parts = rester;
}

let relative = path.join(...parts);

/**
* We don't actually know if this file is an app.
* it could be an addon (v1 or v2)
*
* So here we log to see if we have unhandled situations.
*/
let candidatePath = path.join(workspace, 'app', relative);

let resolved = findCandidate(candidatePath);

if (resolved) {
return resolved;
}
}

/**
* under embroider@3, the fileName will be the path to the rewritten file.
* we don't want this.
* we want the path to the original source.
* Through the powers of ✨ convention ✨, we can map back to source.
*/
if (fileName.includes('/node_modules/.embroider/rewritten-app/')) {
let candidatePath = fileName.replace(
'/node_modules/.embroider/rewritten-app/',
'/app/',
);

let resolved = findCandidate(candidatePath);

if (resolved) {
return resolved;
}
}

// TODO: why are we passed files to other projects?
if (!fileName.includes(workspace)) {
return fileName;
}

console.debug(`[ScopedCSS]: Failed to handle ${fileName}`);

// Fallback to what the plugin system gives us.
// This may be wrong, and if wrong, reveals
// unhandled scenarios with the file names in the plugin infra
return fileName;
}

const COMPILES_TO_JS = ['.hbs', '.gjs', '.gts'];

function findCandidate(filePath) {
if (existsSync(filePath)) {
return filePath;
}

let withoutExt = withoutExtension(filePath);

for (let ext of COMPILES_TO_JS) {
let candidatePath = withoutExt + ext;

if (existsSync(candidatePath)) {
return candidatePath;
}
}

return null;
}
47 changes: 47 additions & 0 deletions ember-scoped-css/src/lib/path/template-transform-paths.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import path from 'node:path';

import { describe, expect, it } from 'vitest';

import { fixFilename } from './template-transform-paths.js';
import { paths } from './utils.paths.test.js';

describe('fixFilename()', () => {
it('works with embroider paths', () => {
let file = path.join(
paths.embroiderApp,
paths.rewritten,
'components/template-only.hbs',
);
let corrected = fixFilename(file);

expect(corrected).to.equal(
path.join(paths.embroiderApp, 'app/components/template-only.hbs'),
);
});

it('works with the real path', () => {
let file = path.join(
paths.classicApp,
'app',
'components/template-only.hbs',
);
let corrected = fixFilename(file);

expect(corrected).to.equal(
path.join(paths.classicApp, 'app/components/template-only.hbs'),
);
});

it('works with classic paths (w/ module name)', () => {
let file = path.join(
paths.classicApp,
'classic-app',
'components/template-only.hbs',
);
let corrected = fixFilename(file);

expect(corrected).to.equal(
path.join(paths.classicApp, 'app/components/template-only.hbs'),
);
});
});
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"start:addon": "pnpm --filter v2-addon start --no-watch.clearScreen",
"start:embroider-app": "pnpm --filter embroider-app start",
"test": "pnpm --filter '*' test",
"test:ember": "pnpm --filter './test-apps/*' test:ember",
"test:fixture": "pnpm --filter './test-apps/*' test:fixture"
},
"devDependencies": {
Expand Down

0 comments on commit 056af8a

Please sign in to comment.