diff --git a/packages/ember-repl/addon/package.json b/packages/ember-repl/addon/package.json index 094908e95..e5f300f88 100644 --- a/packages/ember-repl/addon/package.json +++ b/packages/ember-repl/addon/package.json @@ -84,6 +84,7 @@ "broccoli-file-creator": "^2.1.1", "change-case": "^5.1.2", "common-tags": "^1.8.2", + "content-tag": "github:NullVoxPopuli/content-tag#browser-support-dist", "line-column": "^1.0.2", "magic-string": "^0.30.5", "mdast": "^3.0.0", diff --git a/packages/ember-repl/addon/src/browser/cjs/index.ts b/packages/ember-repl/addon/src/browser/cjs/index.ts index c4746fa9e..9e8916f7e 100644 --- a/packages/ember-repl/addon/src/browser/cjs/index.ts +++ b/packages/ember-repl/addon/src/browser/cjs/index.ts @@ -31,7 +31,7 @@ export async function compileJS(code: string, extraModules?: ExtraModules): Prom } async function compileGJS({ code: input, name }: Info) { - let preprocessed = preprocess(input, name); + let preprocessed = await preprocess(input, name); let result = await transform(preprocessed, name); if (!result) { diff --git a/packages/ember-repl/addon/src/browser/esm/index.ts b/packages/ember-repl/addon/src/browser/esm/index.ts index 60ff9c97f..5195591cf 100644 --- a/packages/ember-repl/addon/src/browser/esm/index.ts +++ b/packages/ember-repl/addon/src/browser/esm/index.ts @@ -65,7 +65,7 @@ async function evalSnippet(code: string) { } async function compileGJS({ code: input, name }: Info) { - let preprocessed = preprocess(input, name); + let preprocessed = await preprocess(input, name); let result = await transform(preprocessed, name, { modules: false, }); diff --git a/packages/ember-repl/addon/src/browser/eti/babel-plugin.ts b/packages/ember-repl/addon/src/browser/eti/babel-plugin.ts deleted file mode 100644 index 75a09caed..000000000 --- a/packages/ember-repl/addon/src/browser/eti/babel-plugin.ts +++ /dev/null @@ -1,105 +0,0 @@ -import { ImportUtil } from 'babel-import-util'; - -import { transformTemplateTag } from './template-tag-transform.ts'; -import * as util from './util.ts'; - -import type { NodePath } from '@babel/traverse'; -import type { CallExpression, Class, Program } from '@babel/types'; - -/** - * This Babel plugin takes parseable code emitted by the string-based - * preprocessor plugin in this package and converts it into calls to - * the standardized `precompileTemplate` macro from `@ember/template-compilation`. - * - * Its goal is to convert code like this: - * - * ```js - * import { hbs } from 'ember-template-imports'; - * - * const A = hbs(`A`, {...}); - * const B = [__GLIMMER_TEMPLATE(`B`, {...})]; - * class C { - * template = hbs(`C`, {...}); - * } - * - * [__GLIMMER_TEMPLATE(`default`, {...})]; - * - * class D { - * [__GLIMMER_TEMPLATE(`D`, {...})] - * } - * ``` - * - * Into this: - * - * ```js - * import { precompileTemplate } from '@ember/template-compilation'; - * import { setComponentTemplate } from '@ember/component'; - * import templateOnlyComponent from '@ember/component/template-only'; - * - * const A = setComponentTemplate( - * precompileTemplate(`A`, {...}), - * templateOnlyComponent('this-module.js', 'A') - * ); - * const B = setComponentTemplate( - * precompileTemplate(`B`, {...}), - * templateOnlyComponent('this-module.js', 'B') - * ); - * class C {} - * setComponentTemplate(precompileTemplate(`C`, {...}), C); - * - * export default setComponentTemplate( - * precompileTemplate(`default`, {...}), - * templateOnlyComponent('this-module.js', '_thisModule') - * ); - * - * class D {} - * setComponentTemplate(precompileTemplate(`D`, {...}), D); - * ``` - */ -export default function (babel: any) { - let t = babel.types; - - let visitor: any = { - Program: { - enter(path: NodePath, state: any) { - state.importUtil = new ImportUtil(t, path); - }, - }, - - // Process class bodies before things like class properties get transformed - // into imperative constructor code that we can't recognize. Taken directly - // from babel-plugin-htmlbars-inline-precompile https://git.io/JMi1G - Class(path: NodePath, state: any) { - let bodyPath = path.get('body.body'); - - if (!Array.isArray(bodyPath)) return; - - bodyPath.forEach((path) => { - if (path.type !== 'ClassProperty') return; - - let keyPath = path.get('key'); - let valuePath = path.get('value'); - - if (Array.isArray(keyPath)) return; - - if (keyPath && visitor[keyPath.type]) { - visitor[keyPath.type](keyPath, state); - } - - if (Array.isArray(valuePath)) return; - - if (valuePath && visitor[valuePath.type]) { - visitor[valuePath.type](valuePath, state); - } - }); - }, - - CallExpression(path: NodePath, state: any) { - if (util.isTemplateTag(path)) { - transformTemplateTag(t, path, state); - } - }, - }; - - return { visitor }; -} diff --git a/packages/ember-repl/addon/src/browser/eti/debug.ts b/packages/ember-repl/addon/src/browser/eti/debug.ts deleted file mode 100644 index c5a485ad1..000000000 --- a/packages/ember-repl/addon/src/browser/eti/debug.ts +++ /dev/null @@ -1,7 +0,0 @@ -export function expect(value: T | null | undefined, message: string): T { - if (value === undefined || value === null) { - throw new Error(`LIBRARY BUG: ${message}`); - } - - return value; -} diff --git a/packages/ember-repl/addon/src/browser/eti/parse-templates.ts b/packages/ember-repl/addon/src/browser/eti/parse-templates.ts deleted file mode 100644 index 0479d3ff7..000000000 --- a/packages/ember-repl/addon/src/browser/eti/parse-templates.ts +++ /dev/null @@ -1,284 +0,0 @@ -import { expect } from './debug.ts'; -import { TEMPLATE_TAG_NAME } from './util.ts'; - -export type TemplateMatch = TemplateTagMatch; - -export interface TemplateTagMatch { - type: 'template-tag'; - tagName: string; - start: RegExpMatchArray; - end: RegExpMatchArray; - contents: string; -} - -/** - * Represents a static import of a template literal. - */ -export interface StaticImportConfig { - /** - * The path to the package from which we want to import the template literal - * (e.g.: 'ember-cli-htmlbars') - */ - importPath: string; - /** - * The name of the template literal (e.g.: 'hbs') or 'default' if this package - * exports a default function - */ - importIdentifier: string; -} - -/** - * The input options to instruct parseTemplates on how to parse the input. - * - * @param templateTag - */ -export interface ParseTemplatesOptions { - /** Tag to use, if parsing template tags is enabled. */ - templateTag?: string; -} - -const escapeChar = '\\'; -const stringDelimiter = /['"]/; - -const singleLineCommentStart = /\/\//; -const newLine = /\n/; -const multiLineCommentStart = /\/\*/; -const multiLineCommentEnd = /\*\//; - -const templateLiteralStart = /([$a-zA-Z_][0-9a-zA-Z_$]*)?`/; -const templateLiteralEnd = /`/; - -const dynamicSegmentStart = /\${/; -const blockStart = /{/; -const dynamicSegmentEnd = /}/; - -function isEscaped(template: string, _offset: number | undefined) { - let offset = expect(_offset, 'Expected an index to check escaping'); - - let count = 0; - - while (template[offset - 1] === escapeChar) { - count++; - offset--; - } - - return count % 2 === 1; -} - -export const DEFAULT_PARSE_TEMPLATES_OPTIONS = { - templateTag: TEMPLATE_TAG_NAME, -}; - -/** - * Parses a template to find all possible valid matches for an embedded template. - * Supported syntaxes are template literals: - * - * hbs`Hello, world!` - * - * And template tags - * - * - * - * The parser excludes any values found within strings recursively, and also - * excludes any string literals with dynamic segments (e.g `${}`) since these - * cannot be valid templates. - * - * @param template The template to parse - * @param relativePath Relative file path for the template (for errors) - * @param options optional configuration options for how to parse templates - * @returns - */ -export function parseTemplates( - template: string, - relativePath: string, - options: ParseTemplatesOptions = DEFAULT_PARSE_TEMPLATES_OPTIONS -): TemplateMatch[] { - const results: TemplateMatch[] = []; - const templateTag = options?.templateTag; - - const templateTagStart = new RegExp(`<${templateTag}[^<]*>`); - const templateTagEnd = new RegExp(``); - const argumentsMatchRegex = new RegExp(`<${templateTag}[^<]*\\S[^<]*>`); - - const allTokens = new RegExp( - [ - singleLineCommentStart.source, - newLine.source, - multiLineCommentStart.source, - multiLineCommentEnd.source, - stringDelimiter.source, - templateLiteralStart.source, - templateLiteralEnd.source, - dynamicSegmentStart.source, - dynamicSegmentEnd.source, - blockStart.source, - templateTagStart.source, - templateTagEnd.source, - ].join('|'), - 'g' - ); - - const tokens = Array.from(template.matchAll(allTokens)); - - while (tokens.length > 0) { - const currentToken = tokens.shift()!; // eslint-disable-line @typescript-eslint/no-non-null-assertion - - parseToken(results, template, currentToken, tokens, true); - } - - /** - * Parse the current token. If top level, then template tags can be parsed. - * Else, we are nested within a dynamic segment, which is currently unsupported. - */ - function parseToken( - results: TemplateMatch[], - template: string, - token: RegExpMatchArray, - tokens: RegExpMatchArray[], - isTopLevel = false - ) { - if (token[0].match(multiLineCommentStart)) { - parseMultiLineComment(results, template, token, tokens); - } else if (token[0].match(singleLineCommentStart)) { - parseSingleLineComment(results, template, token, tokens); - } else if (token[0].match(templateLiteralStart)) { - parseTemplateLiteral(template, tokens); - } else if ( - isTopLevel && - templateTag !== undefined && - templateTagStart && - token[0].match(templateTagStart) - ) { - parseTemplateTag(results, template, token, tokens, templateTag); - } else if (token[0].match(stringDelimiter)) { - parseString(results, template, token, tokens); - } - } - - /** - * Parse a template literal. If a dynamic segment is found, enters the dynamic - * segment and parses it recursively. If no dynamic segments are found and the - * literal is top level (e.g. not nested within a dynamic segment) and has a - * tag, pushes it into the list of results. - */ - function parseTemplateLiteral(template: string, tokens: RegExpMatchArray[]) { - while (tokens.length > 0) { - let currentToken = expect(tokens.shift(), 'expected token'); - - if (isEscaped(template, currentToken.index)) continue; - - if (currentToken[0].match(templateLiteralEnd)) { - return; - } - } - } - - /** - * Parse a string. All tokens within a string are ignored - * since there are no dynamic segments within these. - */ - function parseString( - _results: TemplateMatch[], - template: string, - startToken: RegExpMatchArray, - tokens: RegExpMatchArray[] - ) { - while (tokens.length > 0) { - const currentToken = expect(tokens.shift(), 'expected token'); - - if (currentToken[0] === startToken[0] && !isEscaped(template, currentToken.index)) { - return; - } - } - } - - /** - * Parse a single-line comment. All tokens within a single-line comment are ignored - * since there are no dynamic segments within them. - */ - function parseSingleLineComment( - _results: TemplateMatch[], - _template: string, - _startToken: RegExpMatchArray, - tokens: RegExpMatchArray[] - ) { - while (tokens.length > 0) { - const currentToken = expect(tokens.shift(), 'expected token'); - - if (currentToken[0] === '\n') { - return; - } - } - } - - /** - * Parse a multi-line comment. All tokens within a multi-line comment are ignored - * since there are no dynamic segments within them. - */ - function parseMultiLineComment( - _results: TemplateMatch[], - _template: string, - _startToken: RegExpMatchArray, - tokens: RegExpMatchArray[] - ) { - while (tokens.length > 0) { - const currentToken = expect(tokens.shift(), 'expected token'); - - if (currentToken[0] === '*/') { - return; - } - } - } - - /** - * Parses a template tag. Continues parsing until the template tag has closed, - * accounting for nested template tags. - */ - function parseTemplateTag( - results: TemplateMatch[], - _template: string, - startToken: RegExpMatchArray, - tokens: RegExpMatchArray[], - templateTag: string - ) { - let stack = 1; - - if (argumentsMatchRegex && startToken[0].match(argumentsMatchRegex)) { - throw new Error( - `embedded template preprocessing currently does not support passing arguments, found args in: ${relativePath}` - ); - } - - while (tokens.length > 0) { - const currentToken = expect(tokens.shift(), 'expected token'); - - if (currentToken[0].match(templateTagStart)) { - stack++; - } else if (currentToken[0].match(templateTagEnd)) { - stack--; - } - - if (stack === 0) { - let contents = ''; - - if (startToken.index !== undefined) { - const templateStart = startToken.index + startToken[0].length; - - contents = template.slice(templateStart, currentToken.index); - } - - results.push({ - type: 'template-tag', - tagName: templateTag, - contents: contents, - start: startToken, - end: currentToken, - }); - - return; - } - } - } - - return results; -} diff --git a/packages/ember-repl/addon/src/browser/eti/preprocess.ts b/packages/ember-repl/addon/src/browser/eti/preprocess.ts deleted file mode 100644 index 6f025c042..000000000 --- a/packages/ember-repl/addon/src/browser/eti/preprocess.ts +++ /dev/null @@ -1,187 +0,0 @@ -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-ignore -import { getTemplateLocals } from '@glimmer/syntax'; - -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-ignore -import lineColumn from 'line-column'; -import MagicString from 'magic-string'; - -import { expect } from './debug.ts'; -import { parseTemplates } from './parse-templates.ts'; - -import type { ParseTemplatesOptions, TemplateMatch } from './parse-templates.ts'; - -interface PreprocessOptionsEager { - importIdentifier?: string; - importPath?: string; - templateTag?: string; - templateTagReplacement?: string; - - relativePath: string; - includeSourceMaps: boolean; - includeTemplateTokens: boolean; -} - -interface PreprocessOptionsLazy { - importIdentifier?: string; - importPath?: string; - templateTag?: string; - templateTagReplacement?: string; - - relativePath: string; - includeSourceMaps: boolean; - includeTemplateTokens: boolean; -} - -type PreprocessOptions = PreprocessOptionsLazy | PreprocessOptionsEager; - -interface PreprocessedOutput { - output: string; - replacements: Replacement[]; -} - -interface Replacement { - type: 'start' | 'end'; - index: number; - oldLength: number; - newLength: number; - originalLine: number; - originalCol: number; -} - -function getMatchStartAndEnd(match: RegExpMatchArray) { - return { - start: expect(match.index, 'Expected regular expression match to have an index'), - end: - expect(match.index, 'Expected regular expression match to have an index') + match[0].length, - }; -} - -function replacementFrom( - template: string, - index: number, - oldLength: number, - newLength: number, - type: 'start' | 'end' -): Replacement { - const loc = expect( - lineColumn(template).fromIndex(index), - 'BUG: expected to find a line/column based on index' - ); - - return { - type, - index, - oldLength, - newLength, - originalCol: loc.col, - originalLine: loc.line, - }; -} - -function replaceMatch( - s: MagicString, - match: TemplateMatch, - startReplacement: string, - endReplacement: string, - template: string, - includeTemplateTokens: boolean -): Replacement[] { - const { start: openStart, end: openEnd } = getMatchStartAndEnd(match.start); - const { start: closeStart, end: closeEnd } = getMatchStartAndEnd(match.end); - - let options = ''; - - if (includeTemplateTokens) { - const tokensString = getTemplateLocals(template.slice(openEnd, closeStart)) - .filter((local: string) => local.match(/^[$A-Z_][0-9A-Z_$]*$/i)) - .join(','); - - if (tokensString.length > 0) { - options = `, scope: () => ({${tokensString}})`; - } - } - - const newStart = `${startReplacement}\``; - const newEnd = `\`, { strictMode: true${options} }${endReplacement}`; - - s.overwrite(openStart, openEnd, newStart); - s.overwrite(closeStart, closeEnd, newEnd); - ensureBackticksEscaped(s, openEnd + 1, closeStart - 1); - - return [ - replacementFrom(template, openStart, openEnd - openStart, newStart.length, 'start'), - replacementFrom(template, closeStart, closeEnd - closeStart, newEnd.length, 'end'), - ]; -} - -/** - * Preprocesses all embedded templates within a JavaScript or TypeScript file. - * This function replaces all embedded templates that match our template syntax - * with valid, parseable JS. Optionally, it can also include a source map, and - * it can also include all possible values used within the template. - * - * Input: - * - *