├── test ├── fixtures │ ├── Nested.svelte │ ├── template.custom │ ├── template.pug │ ├── style.css │ ├── template.html │ ├── script.coffee │ ├── script.js │ ├── style.less │ ├── script.ts │ ├── style.scss │ ├── style.sass │ ├── style.styl │ ├── script.babel.js │ ├── tsconfig.json │ ├── tsconfig.semantic.json │ ├── tsconfig.es2021target.json │ ├── tsconfig.extends2.json │ ├── TypeScriptTypesOnly.svelte │ ├── tsconfig.extends1.json │ ├── tsconfig.outdir.json │ ├── postcss.config.js │ ├── types.ts │ ├── TypeScriptES2021.svelte │ ├── PreserveValueImports.svelte │ ├── TypeScriptImportsModule.svelte │ └── TypeScriptImports.svelte ├── node_modules │ └── scss-package │ │ └── main.scss ├── transformers │ ├── less.test.ts │ ├── stylus.test.ts │ ├── babel.test.ts │ ├── pug.test.ts │ ├── scss.test.ts │ ├── replace.test.ts │ ├── postcss.test.ts │ ├── typescript.test.ts │ └── globalStyle.test.ts ├── processors │ ├── postcss.test.ts │ ├── babel.test.ts │ ├── pug.test.ts │ ├── less.test.ts │ ├── stylus.test.ts │ ├── coffeescript.test.ts │ ├── typescript.test.ts │ └── scss.test.ts ├── autoProcess │ ├── script.test.ts │ ├── style.test.ts │ ├── markup.test.ts │ ├── sourceMaps.test.ts │ ├── externalFiles.test.ts │ └── autoProcess.test.ts ├── utils.ts └── modules │ ├── modules.test.ts │ └── globalifySelector.test.ts ├── .prettierrc ├── .eslintignore ├── .prettierignore ├── .gitignore ├── tsconfig.build.json ├── .editorconfig ├── src ├── types │ ├── modules.d.ts │ ├── options.ts │ └── index.ts ├── modules │ ├── errors.ts │ ├── prepareContent.ts │ ├── globalifySelector.ts │ ├── tagInfo.ts │ ├── utils.ts │ ├── markup.ts │ └── language.ts ├── transformers │ ├── replace.ts │ ├── less.ts │ ├── coffeescript.ts │ ├── stylus.ts │ ├── babel.ts │ ├── globalStyle.ts │ ├── postcss.ts │ ├── pug.ts │ ├── scss.ts │ └── typescript.ts ├── processors │ ├── replace.ts │ ├── globalStyle.ts │ ├── pug.ts │ ├── postcss.ts │ ├── babel.ts │ ├── less.ts │ ├── typescript.ts │ ├── stylus.ts │ ├── coffeescript.ts │ └── scss.ts ├── index.ts └── autoProcess.ts ├── scripts.js ├── tsconfig.json ├── .eslintrc ├── .github ├── PULL_REQUEST_TEMPLATE.md ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── workflows │ └── ci.yml └── ISSUE_TEMPLATE.md ├── CONTRIBUTING.md ├── LICENSE ├── package.json ├── docs ├── migration-guide.md ├── usage.md ├── getting-started.md └── preprocessing.md └── README.md /test/fixtures/Nested.svelte: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/template.custom: -------------------------------------------------------------------------------- 1 | foo -------------------------------------------------------------------------------- /test/fixtures/template.pug: -------------------------------------------------------------------------------- 1 | div Hey -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | "@kiwi/prettier-config" -------------------------------------------------------------------------------- /test/fixtures/style.css: -------------------------------------------------------------------------------- 1 | div{color:red} -------------------------------------------------------------------------------- /test/fixtures/template.html: -------------------------------------------------------------------------------- 1 |
Hey
-------------------------------------------------------------------------------- /test/fixtures/script.coffee: -------------------------------------------------------------------------------- 1 | export hello = 'world'; 2 | -------------------------------------------------------------------------------- /test/fixtures/script.js: -------------------------------------------------------------------------------- 1 | export var hello = 'world'; 2 | -------------------------------------------------------------------------------- /test/fixtures/style.less: -------------------------------------------------------------------------------- 1 | @color: red; 2 | div{color:@color} -------------------------------------------------------------------------------- /test/fixtures/script.ts: -------------------------------------------------------------------------------- 1 | export var hello: string = 'world'; 2 | -------------------------------------------------------------------------------- /test/fixtures/style.scss: -------------------------------------------------------------------------------- 1 | $color: red; 2 | 3 | div{color:$color} -------------------------------------------------------------------------------- /test/node_modules/scss-package/main.scss: -------------------------------------------------------------------------------- 1 | div { color: pink; } -------------------------------------------------------------------------------- /test/fixtures/style.sass: -------------------------------------------------------------------------------- 1 | $color: red 2 | 3 | div 4 | color: $color -------------------------------------------------------------------------------- /test/fixtures/style.styl: -------------------------------------------------------------------------------- 1 | $color = red; 2 | div 3 | color: $color -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | /test/fixtures 3 | examples/ 4 | dist/ -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | fixtures/ 3 | dist/ 4 | examples/ 5 | coverage/ -------------------------------------------------------------------------------- /test/fixtures/script.babel.js: -------------------------------------------------------------------------------- 1 | export var hello = {}; 2 | export var world = hello?.value; 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | !/test/node_modules 3 | *.log 4 | coverage 5 | dist/ 6 | .idea 7 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": ["src/**/*.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /test/fixtures/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "es2015", 4 | "skipLibCheck": true, 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /test/fixtures/tsconfig.semantic.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "es2010", 4 | "skipLibCheck": true, 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /test/fixtures/tsconfig.es2021target.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2021", 4 | "skipLibCheck": true 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /test/fixtures/tsconfig.extends2.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "skipLibCheck": false, 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /test/fixtures/TypeScriptTypesOnly.svelte: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /test/fixtures/tsconfig.extends1.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.extends2.json", 3 | "compilerOptions": { 4 | "esModuleInterop": true, 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /test/fixtures/tsconfig.outdir.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "es2015", 4 | "skipLibCheck": true, 5 | "outDir": "dist" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /test/fixtures/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | require('autoprefixer')({ 4 | overrideBrowserslist: 'Safari >= 5.1', 5 | }), 6 | ], 7 | } 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = false -------------------------------------------------------------------------------- /src/types/modules.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'svelte/package.json'; 2 | declare module 'coffeescript'; 3 | declare module 'postcss-load-config'; 4 | declare module 'less'; 5 | declare module 'sorcery'; 6 | -------------------------------------------------------------------------------- /test/fixtures/types.ts: -------------------------------------------------------------------------------- 1 | export type AType = 'test1' | 'test2'; 2 | export interface AInterface { 3 | test: string; 4 | } 5 | export const AValue: string = 'test'; 6 | 7 | export default 'String'; 8 | -------------------------------------------------------------------------------- /test/fixtures/TypeScriptES2021.svelte: -------------------------------------------------------------------------------- 1 | 10 | -------------------------------------------------------------------------------- /src/modules/errors.ts: -------------------------------------------------------------------------------- 1 | export const throwError = (msg: string) => { 2 | throw new Error(`[svelte-preprocess] ${msg}`); 3 | }; 4 | 5 | export const throwTypescriptError = () => { 6 | throwError(`Encountered type error`); 7 | }; 8 | -------------------------------------------------------------------------------- /test/fixtures/PreserveValueImports.svelte: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /test/fixtures/TypeScriptImportsModule.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 | 9 | 10 | {val} {aValue} -------------------------------------------------------------------------------- /scripts.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | 3 | /** 4 | * Cross-platform way to do `rm -rf` on a dir 5 | * @param {string} path 6 | */ 7 | function rimraf(path) { 8 | (fs.rmSync || fs.rmdirSync)(path, { recursive: true, force: true }); 9 | } 10 | 11 | switch (process.argv[2]) { 12 | case 'rmrf': 13 | rimraf(process.argv[3]); 14 | break; 15 | default: 16 | console.error('no valid script command given'); 17 | break; 18 | } 19 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "sourceMap": false, 5 | "module": "commonjs", 6 | "moduleResolution": "node", 7 | "esModuleInterop": true, 8 | "target": "es2018", 9 | "lib": ["es2018"], 10 | "declaration": true, 11 | "skipLibCheck": true, 12 | "types": ["node"], 13 | "outDir": "dist" 14 | }, 15 | "include": ["**/*.ts"], 16 | "exclude": ["node_modules/**/*", "dist", "examples"] 17 | } 18 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["@kiwi"], 3 | "env": { 4 | "node": true, 5 | "jest": true, 6 | }, 7 | "rules": { 8 | "@typescript-eslint/no-explicit-any": "off", 9 | "@typescript-eslint/ban-types": "off", 10 | "@typescript-eslint/prefer-nullish-coalescing": "off", 11 | "@typescript-eslint/no-non-null-assertion": "off", 12 | "no-console": "off", 13 | "line-comment-position": "off", 14 | "import/order": "off", 15 | "@typescript-eslint/ban-ts-comment": "off", 16 | }, 17 | } 18 | -------------------------------------------------------------------------------- /src/transformers/replace.ts: -------------------------------------------------------------------------------- 1 | import type { Transformer, Options } from '../types'; 2 | 3 | const transformer: Transformer = async ({ 4 | content, 5 | options, 6 | }) => { 7 | let newContent = content; 8 | 9 | if (options == null) { 10 | return { code: content }; 11 | } 12 | 13 | for (const [regex, replacer] of options) { 14 | newContent = newContent.replace(regex, replacer as any); 15 | } 16 | 17 | return { 18 | code: newContent, 19 | }; 20 | }; 21 | 22 | export { transformer }; 23 | -------------------------------------------------------------------------------- /src/processors/replace.ts: -------------------------------------------------------------------------------- 1 | import type { PreprocessorGroup, Options } from '../types'; 2 | 3 | const replace = (options: Options.Replace): PreprocessorGroup => ({ 4 | async markup({ content, filename }) { 5 | const { transformer } = await import('../transformers/replace'); 6 | 7 | return transformer({ content, filename, options }); 8 | }, 9 | }); 10 | 11 | // both for backwards compat with old svelte-preprocess versions 12 | // (was only default export once, now is named export because of transpilation causing node not to detect the named exports of 'svelte-preprocess' otherwise) 13 | export default replace; 14 | export { replace }; 15 | -------------------------------------------------------------------------------- /test/transformers/less.test.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'path'; 2 | import { describe, it, expect } from 'vitest'; 3 | import { sveltePreprocess } from '../../src'; 4 | import { preprocess } from '../utils'; 5 | 6 | describe('transformer - less', () => { 7 | it('should return @imported files as dependencies', async () => { 8 | const template = ``; 9 | const opts = sveltePreprocess(); 10 | const preprocessed = await preprocess(template, opts); 11 | 12 | expect(preprocessed.dependencies).toContain( 13 | resolve(__dirname, '..', 'fixtures', 'style.less'), 14 | ); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Before submitting the PR, please make sure you do the following 2 | 3 | - [ ] It's really useful if your PR relates to an outstanding issue, so please reference it in your PR, or create an explanatory one for discussion. In many cases features are absent for a reason. 4 | - [ ] This message body should clearly illustrate what problems it solves. If there are related issues, remember to reference them. 5 | - [ ] Ideally, include a test that fails without this PR but passes with it. PRs will only be merged once they pass CI. (Remember to run `pnpm lint`!) 6 | 7 | ### Tests 8 | 9 | - [ ] Run the tests with `npm test` or `pnpm test` 10 | -------------------------------------------------------------------------------- /test/transformers/stylus.test.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'path'; 2 | import { describe, it, expect } from 'vitest'; 3 | import { sveltePreprocess } from '../../src'; 4 | import { preprocess } from '../utils'; 5 | 6 | describe('transformer - stylus', () => { 7 | it('should return @imported files as dependencies', async () => { 8 | const template = ``; 9 | const opts = sveltePreprocess(); 10 | const preprocessed = await preprocess(template, opts); 11 | 12 | expect(preprocessed.dependencies).toContain( 13 | resolve(__dirname, '..', 'fixtures', 'style.styl'), 14 | ); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | PRs for issues are welcome. Before pushing the code, make sure that linting and tests pass. 4 | 5 | Commit pattern: conventional commit (`docs/feat/fix/chore: ..`) (https://www.conventionalcommits.org/en/v1.0.0/) 6 | Changelog pattern: conventional-changelog tool to generate the changelog (https://github.com/conventional-changelog/conventional-changelog) 7 | 8 | The publishing process is manual right now, so the current workflow is: 9 | 10 | - Merge to main 11 | - Checkout to main 12 | - run npm run patch | minor | major - this will update the changelog, bump the package.json and create the release tag 13 | - run npm publish (maybe 2fa will be asked here) 14 | -------------------------------------------------------------------------------- /src/processors/globalStyle.ts: -------------------------------------------------------------------------------- 1 | import type { PreprocessorGroup } from '../types'; 2 | 3 | const globalStyle = (): PreprocessorGroup => { 4 | return { 5 | async style({ content, attributes, filename }) { 6 | const { transformer } = await import('../transformers/globalStyle'); 7 | 8 | if (!attributes.global) { 9 | return { code: content }; 10 | } 11 | 12 | return transformer({ content, filename, attributes }); 13 | }, 14 | }; 15 | }; 16 | 17 | // both for backwards compat with old svelte-preprocess versions 18 | // (was only default export once, now is named export because of transpilation causing node not to detect the named exports of 'svelte-preprocess' otherwise) 19 | export default globalStyle; 20 | export { globalStyle }; 21 | -------------------------------------------------------------------------------- /src/transformers/less.ts: -------------------------------------------------------------------------------- 1 | import { isAbsolute, join } from 'path'; 2 | 3 | import less from 'less'; 4 | 5 | import { getIncludePaths } from '../modules/utils'; 6 | 7 | import type { Transformer, Options } from '../types'; 8 | 9 | const transformer: Transformer = async ({ 10 | content, 11 | filename, 12 | options = {}, 13 | }) => { 14 | options = { 15 | paths: getIncludePaths(filename, options.paths), 16 | ...options, 17 | }; 18 | 19 | const { css, map, imports } = await less.render(content, { 20 | sourceMap: {}, 21 | filename, 22 | ...options, 23 | }); 24 | 25 | const dependencies = imports.map((path: string) => 26 | isAbsolute(path) ? path : join(process.cwd(), path), 27 | ); 28 | 29 | return { 30 | code: css, 31 | map, 32 | dependencies, 33 | }; 34 | }; 35 | 36 | export { transformer }; 37 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: 'New Feature' 6 | assignees: '' 7 | --- 8 | 9 | **Is your feature request related to a problem? Please describe.** 10 | A clear and concise description of what the problem is. For example: I'm always frustrated when [...] 11 | 12 | **Describe the solution you'd like** 13 | A clear and concise description of what you want to happen. 14 | 15 | **Describe alternatives you've considered** 16 | A clear and concise description of any alternative solutions or features you've considered. 17 | 18 | **How important is this feature to you?** 19 | Note: the more honest and specific you are here the more we will take you seriously. 20 | 21 | **Additional context** 22 | Add any other context or screenshots about the feature request here. 23 | -------------------------------------------------------------------------------- /src/processors/pug.ts: -------------------------------------------------------------------------------- 1 | import { prepareContent } from '../modules/prepareContent'; 2 | import { transformMarkup } from '../modules/markup'; 3 | 4 | import type { Options, PreprocessorGroup } from '../types/index'; 5 | 6 | const pug = (options?: Options.Pug): PreprocessorGroup => ({ 7 | async markup({ content, filename }) { 8 | const { transformer } = await import('../transformers/pug'); 9 | 10 | content = prepareContent({ 11 | options: { 12 | ...options, 13 | stripIndent: true, 14 | }, 15 | content, 16 | }); 17 | 18 | return transformMarkup({ content, filename }, transformer, options); 19 | }, 20 | }); 21 | 22 | // both for backwards compat with old svelte-preprocess versions 23 | // (was only default export once, now is named export because of transpilation causing node not to detect the named exports of 'svelte-preprocess' otherwise) 24 | export default pug; 25 | export { pug }; 26 | -------------------------------------------------------------------------------- /test/processors/postcss.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest'; 2 | import { postcss } from '../../src'; 3 | import { CSS_PATTERN, preprocess, spyConsole } from '../utils'; 4 | 5 | spyConsole({ silent: true }); 6 | 7 | describe(`processor - postcss`, () => { 8 | it('should support external src files', async () => { 9 | const template = `
`; 10 | const preprocessed = await preprocess(template, [postcss()]); 11 | 12 | expect(preprocessed.toString?.()).toMatch(CSS_PATTERN); 13 | }); 14 | 15 | it('should support prepended data', async () => { 16 | const template = `
`; 17 | const options = { prependData: '/* potato */' }; 18 | const preprocessed = await preprocess(template, [postcss(options as any)]); 19 | 20 | expect(preprocessed.toString?.()).toContain('/* potato */'); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | Lint: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v4 10 | - uses: pnpm/action-setup@v4 11 | - uses: actions/setup-node@v4 12 | with: 13 | node-version: 18 14 | cache: 'pnpm' 15 | - run: pnpm install 16 | - run: pnpm lint 17 | 18 | Tests: 19 | runs-on: ${{ matrix.os }} 20 | strategy: 21 | matrix: 22 | node-version: [18, 20, 22] 23 | os: [ubuntu-latest, windows-latest, macOS-latest] 24 | 25 | steps: 26 | - run: git config --global core.autocrlf false 27 | - uses: actions/checkout@v4 28 | - uses: pnpm/action-setup@v4 29 | - uses: actions/setup-node@v4 30 | with: 31 | node-version: ${{ matrix.node-version }} 32 | cache: 'pnpm' 33 | - run: pnpm install 34 | - run: pnpm test 35 | env: 36 | CI: true 37 | -------------------------------------------------------------------------------- /test/processors/babel.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest'; 2 | import { babel } from '../../src'; 3 | import { preprocess } from '../utils'; 4 | 5 | describe(`processor - babel`, () => { 6 | it('should support external src files', async () => { 7 | const template = `
`; 8 | const preprocessed = await preprocess(template, [ 9 | babel({ 10 | presets: [ 11 | [ 12 | '@babel/preset-env', 13 | { 14 | loose: true, 15 | modules: false, 16 | targets: { 17 | esmodules: true, 18 | }, 19 | }, 20 | ], 21 | ], 22 | }), 23 | ]); 24 | 25 | expect(preprocessed.toString?.()).toMatchInlineSnapshot(` 26 | "
" 28 | `); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /test/autoProcess/script.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest'; 2 | import { sveltePreprocess } from '../../src'; 3 | import { preprocess, getFixtureContent } from '../utils'; 4 | 5 | const SCRIPT_LANGS: Array<[string, string, any?]> = [ 6 | ['coffeescript', 'coffee'], 7 | [ 8 | 'typescript', 9 | 'ts', 10 | { tsconfigFile: false, compilerOptions: { module: 'es2015' } }, 11 | ], 12 | ]; 13 | 14 | const EXPECTED_SCRIPT = getFixtureContent('script.js'); 15 | 16 | SCRIPT_LANGS.forEach(([lang, ext, langOptions]) => { 17 | describe(`script - preprocessor - ${lang}`, () => { 18 | const template = `
`; 21 | 22 | it(`should parse ${lang}`, async () => { 23 | const opts = sveltePreprocess({ 24 | [lang]: langOptions, 25 | }); 26 | 27 | const preprocessed = await preprocess(template, opts); 28 | 29 | expect(preprocessed.toString?.()).toContain(EXPECTED_SCRIPT); 30 | }); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | --- 2 | Before filing an issue we'd appreciate it if you could take a moment to ensure 3 | there isn't already an open issue or pull-request. 4 | --- 5 | 6 | If there's an existing issue, please add a :+1: reaction to the description of 7 | the issue. One way we prioritize issues is by the number of :+1: reactions on 8 | their descriptions. Please DO NOT add `+1` or :+1: comments. 9 | 10 | ### Feature requests and proposals 11 | 12 | We're excited to hear how we can make Svelte better. Please add as much detail 13 | as you can on your use case. 14 | 15 | ### Bugs 16 | 17 | If you're filing an issue about a bug please include as much information 18 | as you can including the following. 19 | 20 | - Your browser and the version: (e.x. Chrome 52.1, Firefox 48.0, IE 10) 21 | - Your operating system: (e.x. OS X 10, Windows XP, etc) 22 | - `svelte-preprocess` version (Please check you can reproduce the issue with the latest release!) 23 | - Whether your project uses Webpack or Rollup 24 | 25 | - _Repeatable steps to reproduce the issue_ 26 | 27 | ## Thanks for being part of Svelte! 28 | -------------------------------------------------------------------------------- /src/processors/postcss.ts: -------------------------------------------------------------------------------- 1 | import { getTagInfo } from '../modules/tagInfo'; 2 | import { concat } from '../modules/utils'; 3 | import { prepareContent } from '../modules/prepareContent'; 4 | 5 | import type { PreprocessorGroup, Options } from '../types'; 6 | 7 | /** Adapted from https://github.com/TehShrike/svelte-preprocess-postcss */ 8 | const postcss = (options?: Options.Postcss): PreprocessorGroup => ({ 9 | async style(svelteFile) { 10 | const { transformer } = await import('../transformers/postcss'); 11 | let { content, filename, attributes, dependencies } = 12 | await getTagInfo(svelteFile); 13 | 14 | content = prepareContent({ options, content }); 15 | 16 | /** If manually passed a plugins array, use it as the postcss config */ 17 | const transformed = await transformer({ 18 | content, 19 | filename, 20 | attributes, 21 | options, 22 | }); 23 | 24 | return { 25 | ...transformed, 26 | dependencies: concat(dependencies, transformed.dependencies), 27 | }; 28 | }, 29 | }); 30 | 31 | export { postcss }; 32 | export default postcss; 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016-20 [these people](https://github.com/sveltejs/svelte-preprocess/graphs/contributors) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /src/modules/prepareContent.ts: -------------------------------------------------------------------------------- 1 | // todo: could use magic-string and generate some sourcemaps 🗺 2 | export function prepareContent({ 3 | options, 4 | content, 5 | }: { 6 | options: any; 7 | content: string; 8 | }) { 9 | if (typeof options !== 'object') { 10 | return content; 11 | } 12 | 13 | if (options.stripIndent) { 14 | content = stripIndent(content); 15 | } 16 | 17 | if (options.prependData) { 18 | content = `${options.prependData}\n${content}`; 19 | } 20 | 21 | return content; 22 | } 23 | 24 | /** Get the shortest leading whitespace from lines in a string */ 25 | function minIndent(s: string) { 26 | const match = s.match(/^[ \t]*(?=\S)/gm); 27 | 28 | if (!match) { 29 | return 0; 30 | } 31 | 32 | return match.reduce((r, a) => Math.min(r, a.length), Infinity); 33 | } 34 | 35 | /** Strip leading whitespace from each line in a string */ 36 | function stripIndent(s: string) { 37 | const indent = minIndent(s); 38 | 39 | if (indent === 0) { 40 | return s; 41 | } 42 | 43 | const regex = new RegExp(`^[ \\t]{${indent}}`, 'gm'); 44 | 45 | return s.replace(regex, ''); 46 | } 47 | -------------------------------------------------------------------------------- /src/processors/babel.ts: -------------------------------------------------------------------------------- 1 | import { concat } from '../modules/utils'; 2 | import { getTagInfo } from '../modules/tagInfo'; 3 | import { prepareContent } from '../modules/prepareContent'; 4 | 5 | import type { PreprocessorGroup, Options } from '../types'; 6 | 7 | const babel = (options?: Options.Babel): PreprocessorGroup => ({ 8 | async script(svelteFile) { 9 | const { transformer } = await import('../transformers/babel'); 10 | 11 | let { content, filename, dependencies, attributes } = 12 | await getTagInfo(svelteFile); 13 | 14 | content = prepareContent({ options, content }); 15 | 16 | const transformed = await transformer({ 17 | content, 18 | filename, 19 | attributes, 20 | options, 21 | }); 22 | 23 | return { 24 | ...transformed, 25 | dependencies: concat(dependencies, transformed.dependencies), 26 | }; 27 | }, 28 | }); 29 | 30 | // both for backwards compat with old svelte-preprocess versions 31 | // (was only default export once, now is named export because of transpilation causing node not to detect the named exports of 'svelte-preprocess' otherwise) 32 | export default babel; 33 | export { babel }; 34 | -------------------------------------------------------------------------------- /test/processors/pug.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest'; 2 | import { pug } from '../../src'; 3 | import { getFixtureContent, preprocess } from '../utils'; 4 | 5 | const EXPECTED_TEMPLATE = getFixtureContent('template.html'); 6 | 7 | describe(`processor - pug`, () => { 8 | it('should preprocess the whole file', async () => { 9 | const template = getFixtureContent('template.pug'); 10 | const preprocessed = await preprocess(template, [pug()]); 11 | 12 | expect(preprocessed.toString?.()).toContain(EXPECTED_TEMPLATE); 13 | }); 14 | 15 | it('should support prepended data', async () => { 16 | const template = ``; 17 | const options = { prependData: `// potato` }; 18 | const preprocessed = await preprocess(template, [pug(options)]); 19 | 20 | expect(preprocessed.toString?.()).toContain(``); 21 | }); 22 | 23 | it('should support template tag wrapper', async () => { 24 | const template = ` 25 | h1 HEY 26 | `; 27 | 28 | const options = { markupTagName: `markup` }; 29 | const preprocessed = await preprocess(template, [pug(options)]); 30 | 31 | expect(preprocessed.toString?.()).toContain(`

HEY

`); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /src/transformers/coffeescript.ts: -------------------------------------------------------------------------------- 1 | import coffeescript from 'coffeescript'; 2 | 3 | import type { Transformer, Options } from '../types'; 4 | 5 | const transformer: Transformer = ({ 6 | content, 7 | filename, 8 | options, 9 | }) => { 10 | const coffeeOptions = { 11 | filename, 12 | /* 13 | * Since `coffeescript` transpiles variables to `var` definitions, it uses a safety mechanism to prevent variables from bleeding to outside contexts. This mechanism consists of wrapping your `coffeescript` code inside an IIFE which, unfortunately, prevents `svelte` from finding your variables. To bypass this behavior, `svelte-preprocess` sets the [`bare` coffeescript compiler option](https://coffeescript.org/#lexical-scope) to `true`. 14 | */ 15 | bare: true, 16 | ...options, 17 | } as Omit; 18 | 19 | if (coffeeOptions.sourceMap) { 20 | const { js: code, v3SourceMap } = coffeescript.compile( 21 | content, 22 | coffeeOptions, 23 | ); 24 | 25 | const map = JSON.parse(v3SourceMap); 26 | 27 | return { code, map }; 28 | } 29 | 30 | return { code: coffeescript.compile(content, coffeeOptions) }; 31 | }; 32 | 33 | export { transformer }; 34 | -------------------------------------------------------------------------------- /src/processors/less.ts: -------------------------------------------------------------------------------- 1 | import { getTagInfo } from '../modules/tagInfo'; 2 | import { concat } from '../modules/utils'; 3 | import { prepareContent } from '../modules/prepareContent'; 4 | 5 | import type { PreprocessorGroup, Options } from '../types'; 6 | 7 | const less = (options?: Options.Less): PreprocessorGroup => ({ 8 | async style(svelteFile) { 9 | const { transformer } = await import('../transformers/less'); 10 | let { content, filename, attributes, lang, dependencies } = 11 | await getTagInfo(svelteFile); 12 | 13 | if (lang !== 'less') { 14 | return { code: content }; 15 | } 16 | 17 | content = prepareContent({ options, content }); 18 | 19 | const transformed = await transformer({ 20 | content, 21 | filename, 22 | attributes, 23 | options, 24 | }); 25 | 26 | return { 27 | ...transformed, 28 | dependencies: concat(dependencies, transformed.dependencies), 29 | }; 30 | }, 31 | }); 32 | 33 | // both for backwards compat with old svelte-preprocess versions 34 | // (was only default export once, now is named export because of transpilation causing node not to detect the named exports of 'svelte-preprocess' otherwise) 35 | export default less; 36 | export { less }; 37 | -------------------------------------------------------------------------------- /src/processors/typescript.ts: -------------------------------------------------------------------------------- 1 | import { getTagInfo } from '../modules/tagInfo'; 2 | import { concat } from '../modules/utils'; 3 | import { prepareContent } from '../modules/prepareContent'; 4 | 5 | import type { Options, PreprocessorGroup } from '../types'; 6 | 7 | const typescript = (options?: Options.Typescript): PreprocessorGroup => ({ 8 | async script(svelteFile) { 9 | const { transformer } = await import('../transformers/typescript'); 10 | let { content, markup, filename, attributes, lang, dependencies } = 11 | await getTagInfo(svelteFile); 12 | 13 | if (lang !== 'typescript') { 14 | return { code: content }; 15 | } 16 | 17 | content = prepareContent({ options, content }); 18 | 19 | const transformed = await transformer({ 20 | content, 21 | markup, 22 | filename, 23 | attributes, 24 | options, 25 | }); 26 | 27 | return { 28 | ...transformed, 29 | dependencies: concat(dependencies, transformed.dependencies), 30 | }; 31 | }, 32 | }); 33 | 34 | // both for backwards compat with old svelte-preprocess versions 35 | // (was only default export once, now is named export because of transpilation causing node not to detect the named exports of 'svelte-preprocess' otherwise) 36 | export default typescript; 37 | export { typescript }; 38 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { sveltePreprocess } from './autoProcess'; 2 | 3 | // default auto processor 4 | // crazy es6/cjs export mix for backward compatibility 5 | /** @deprecated Use the named export instead: `import { sveltePreprocess } from 'svelte-preprocess'` */ 6 | // eslint-disable-next-line no-multi-assign 7 | export default exports = module.exports = sveltePreprocess; 8 | 9 | // also export auto preprocessor as named export to sidestep default export type issues with "module": "NodeNext" in tsconfig. 10 | // Don't just do export { sveltePreprocess } because the transpiled output is wrong then. 11 | export { sveltePreprocess } from './autoProcess'; 12 | 13 | // stand-alone processors to be included manually, use their named exports for better transpilation or else node will not detect the named exports properly 14 | export { pug } from './processors/pug'; 15 | export { coffeescript } from './processors/coffeescript'; 16 | export { typescript } from './processors/typescript'; 17 | export { less } from './processors/less'; 18 | export { scss, sass } from './processors/scss'; 19 | export { stylus } from './processors/stylus'; 20 | export { postcss } from './processors/postcss'; 21 | export { globalStyle } from './processors/globalStyle'; 22 | export { babel } from './processors/babel'; 23 | export { replace } from './processors/replace'; 24 | -------------------------------------------------------------------------------- /src/processors/stylus.ts: -------------------------------------------------------------------------------- 1 | import { getTagInfo } from '../modules/tagInfo'; 2 | import { concat } from '../modules/utils'; 3 | import { prepareContent } from '../modules/prepareContent'; 4 | 5 | import type { Options, PreprocessorGroup } from '../types'; 6 | 7 | const stylus = (options?: Options.Stylus): PreprocessorGroup => ({ 8 | async style(svelteFile) { 9 | const { transformer } = await import('../transformers/stylus'); 10 | let { content, filename, attributes, lang, dependencies } = 11 | await getTagInfo(svelteFile); 12 | 13 | if (lang !== 'stylus') { 14 | return { code: content }; 15 | } 16 | 17 | content = prepareContent({ 18 | options: { 19 | ...options, 20 | stripIndent: true, 21 | }, 22 | content, 23 | }); 24 | 25 | const transformed = await transformer({ 26 | content, 27 | filename, 28 | attributes, 29 | options, 30 | }); 31 | 32 | return { 33 | ...transformed, 34 | dependencies: concat(dependencies, transformed.dependencies), 35 | }; 36 | }, 37 | }); 38 | 39 | // both for backwards compat with old svelte-preprocess versions 40 | // (was only default export once, now is named export because of transpilation causing node not to detect the named exports of 'svelte-preprocess' otherwise) 41 | export default stylus; 42 | export { stylus }; 43 | -------------------------------------------------------------------------------- /src/transformers/stylus.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import stylus from 'stylus'; 3 | import { getIncludePaths } from '../modules/utils'; 4 | import type { Transformer, Options } from '../types'; 5 | 6 | type StylusRendererWithSourceMap = ReturnType & { 7 | sourcemap: any; 8 | }; 9 | 10 | const transformer: Transformer = ({ 11 | content, 12 | filename, 13 | options = {}, 14 | }) => { 15 | options = { 16 | paths: getIncludePaths(filename, options.paths), 17 | ...options, 18 | }; 19 | 20 | return new Promise((resolve, reject) => { 21 | const style = stylus(content, { 22 | filename, 23 | ...options, 24 | }).set('sourcemap', options.sourcemap) as StylusRendererWithSourceMap; 25 | 26 | style.render((err, css) => { 27 | // istanbul ignore next 28 | if (err) reject(err); 29 | if (style.sourcemap?.sources) { 30 | style.sourcemap.sources = style.sourcemap.sources.map((source: any) => 31 | path.resolve(source), 32 | ); 33 | } 34 | 35 | resolve({ 36 | code: css, 37 | map: style.sourcemap, 38 | // .map() necessary for windows compatibility 39 | dependencies: style 40 | .deps(filename as string) 41 | .map((filePath) => path.resolve(filePath)), 42 | }); 43 | }); 44 | }); 45 | }; 46 | 47 | export { transformer }; 48 | -------------------------------------------------------------------------------- /src/processors/coffeescript.ts: -------------------------------------------------------------------------------- 1 | import { getTagInfo } from '../modules/tagInfo'; 2 | import { concat } from '../modules/utils'; 3 | import { prepareContent } from '../modules/prepareContent'; 4 | 5 | import type { PreprocessorGroup, Options } from '../types'; 6 | 7 | const coffeescript = (options?: Options.Coffeescript): PreprocessorGroup => ({ 8 | async script(svelteFile) { 9 | const { transformer } = await import('../transformers/coffeescript'); 10 | 11 | let { content, filename, attributes, lang, dependencies } = 12 | await getTagInfo(svelteFile); 13 | 14 | if (lang !== 'coffeescript') { 15 | return { code: content }; 16 | } 17 | 18 | content = prepareContent({ 19 | options: { 20 | ...options, 21 | stripIndent: true, 22 | }, 23 | content, 24 | }); 25 | 26 | const transformed = await transformer({ 27 | content, 28 | filename, 29 | attributes, 30 | options, 31 | }); 32 | 33 | return { 34 | ...transformed, 35 | dependencies: concat(dependencies, transformed.dependencies), 36 | }; 37 | }, 38 | }); 39 | 40 | // both for backwards compat with old svelte-preprocess versions 41 | // (was only default export once, now is named export because of transpilation causing node not to detect the named exports of 'svelte-preprocess' otherwise) 42 | export default coffeescript; 43 | export { coffeescript }; 44 | -------------------------------------------------------------------------------- /src/processors/scss.ts: -------------------------------------------------------------------------------- 1 | import { getTagInfo } from '../modules/tagInfo'; 2 | import { concat } from '../modules/utils'; 3 | import { prepareContent } from '../modules/prepareContent'; 4 | 5 | import type { PreprocessorGroup, Options } from '../types'; 6 | 7 | const scss = (options?: Options.Sass): PreprocessorGroup => ({ 8 | async style(svelteFile) { 9 | const { transformer } = await import('../transformers/scss'); 10 | let { content, filename, attributes, lang, alias, dependencies } = 11 | await getTagInfo(svelteFile); 12 | 13 | if (alias === 'sass') { 14 | options = { 15 | ...options, 16 | stripIndent: true, 17 | indentedSyntax: true, 18 | }; 19 | } 20 | 21 | if (lang !== 'scss') { 22 | return { code: content }; 23 | } 24 | 25 | content = prepareContent({ options, content }); 26 | 27 | const transformed = await transformer({ 28 | content, 29 | filename, 30 | attributes, 31 | options, 32 | }); 33 | 34 | return { 35 | ...transformed, 36 | dependencies: concat(dependencies, transformed.dependencies), 37 | }; 38 | }, 39 | }); 40 | 41 | // both for backwards compat with old svelte-preprocess versions 42 | // (was only default export once, now is named export because of transpilation causing node not to detect the named exports of 'svelte-preprocess' otherwise) 43 | export default scss; 44 | export { scss, scss as sass }; 45 | -------------------------------------------------------------------------------- /test/processors/less.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest'; 2 | import { less } from '../../src'; 3 | import { CSS_PATTERN, preprocess } from '../utils'; 4 | 5 | describe(`processor - less`, () => { 6 | it('should ignore other languages', async () => { 7 | const template = ``; 8 | const options = {}; 9 | 10 | const preprocessed = await preprocess(template, [less(options)]); 11 | 12 | expect(preprocessed.toString?.()).toBe(template); 13 | }); 14 | 15 | it('should leave other languages untouched', async () => { 16 | const template = ``; 17 | const options = { prependData: '/* potato */' }; 18 | 19 | const preprocessed = await preprocess(template, [less(options)]); 20 | 21 | expect(preprocessed.toString?.()).toBe(template); 22 | }); 23 | 24 | it('should support external src files', async () => { 25 | const template = `
`; 26 | const preprocessed = await preprocess(template, [less()]); 27 | 28 | expect(preprocessed.toString?.()).toMatch(CSS_PATTERN); 29 | }); 30 | 31 | it('should support prepended data', async () => { 32 | const template = `
`; 33 | const options = { prependData: '/* potato */' }; 34 | const preprocessed = await preprocess(template, [less(options)]); 35 | 36 | expect(preprocessed.toString?.()).toContain('/* potato */'); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /test/processors/stylus.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest'; 2 | import { stylus } from '../../src'; 3 | import { CSS_PATTERN, preprocess } from '../utils'; 4 | 5 | describe(`processor - stylus`, () => { 6 | it('should ignore other languages', async () => { 7 | const template = ``; 8 | const options = {}; 9 | 10 | const preprocessed = await preprocess(template, [stylus(options)]); 11 | 12 | expect(preprocessed.toString?.()).toBe(template); 13 | }); 14 | 15 | it('should leave other languages untouched', async () => { 16 | const template = ``; 17 | const options = { prependData: '/* potato */' }; 18 | 19 | const preprocessed = await preprocess(template, [stylus(options)]); 20 | 21 | expect(preprocessed.toString?.()).toBe(template); 22 | }); 23 | 24 | it('should support external src files', async () => { 25 | const template = `
`; 26 | const preprocessed = await preprocess(template, [stylus()]); 27 | 28 | expect(preprocessed.toString?.()).toMatch(CSS_PATTERN); 29 | }); 30 | 31 | it('should support prepended data', async () => { 32 | const template = `
`; 33 | const options = { prependData: '/* potato */' }; 34 | const preprocessed = await preprocess(template, [stylus(options)]); 35 | 36 | expect(preprocessed.toString?.()).toContain('/* potato */'); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /src/transformers/babel.ts: -------------------------------------------------------------------------------- 1 | import { transformAsync } from '@babel/core'; 2 | 3 | import type { TransformOptions } from '@babel/core'; 4 | import type { Transformer, Options } from '../types'; 5 | 6 | const transformer: Transformer = async ({ 7 | content, 8 | filename, 9 | options, 10 | map = undefined, 11 | }) => { 12 | const babelOptions = { 13 | ...options, 14 | inputSourceMap: 15 | typeof map === 'string' ? JSON.parse(map) : map ?? undefined, 16 | sourceType: 'module', 17 | // istanbul ignore next 18 | sourceMaps: !!options?.sourceMaps, 19 | filename, 20 | minified: false, 21 | ast: false, 22 | code: true, 23 | caller: { 24 | name: 'svelte-preprocess', 25 | supportsStaticESM: true, 26 | supportsDynamicImport: true, 27 | // this isn't supported by Svelte but let it error with a good error on this syntax untouched 28 | supportsTopLevelAwait: true, 29 | // todo: this can be enabled once all "peer deps" understand this 30 | // this syntax is supported since rollup@1.26.0 and webpack@5.0.0-beta.21 31 | // supportsExportNamespaceFrom: true, 32 | ...options?.caller, 33 | }, 34 | } as TransformOptions; 35 | 36 | const result = await transformAsync(content, babelOptions); 37 | 38 | if (result == null) { 39 | return { code: content }; 40 | } 41 | 42 | const { code, map: sourcemap } = result; 43 | 44 | return { 45 | code: code as string, 46 | map: sourcemap ?? undefined, 47 | }; 48 | }; 49 | 50 | export { transformer }; 51 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: 'Bug' 6 | assignees: '' 7 | --- 8 | 9 | **Describe the bug** 10 | A clear and concise description of what the bug is. 11 | 12 | **Logs** 13 | Please include browser console and server logs around the time this bug occurred. 14 | 15 | **To Reproduce** 16 | To help us help you, if you've found a bug please consider the following: 17 | 18 | - Please create a small repo that illustrates the problem. 19 | - Reproductions should be small, self-contained, correct examples – http://sscce.org. 20 | 21 | Occasionally, this won't be possible, and that's fine – we still appreciate you raising the issue. But please understand that `svelte-preprocess` is run by unpaid volunteers in their free time, and issues that follow these instructions will get fixed faster. 22 | 23 | **Expected behavior** 24 | A clear and concise description of what you expected to happen. 25 | 26 | **Stacktraces** 27 | If you have a stack trace to include, we recommend putting inside a `
` block for the sake of the thread's readability: 28 | 29 |
30 | Stack trace 31 | 32 | Stack trace goes here... 33 | 34 |
35 | 36 | **Information about your project:** 37 | 38 | - Your browser and the version: (e.x. Chrome 52.1, Firefox 48.0, IE 10) 39 | 40 | - Your operating system: (e.x. OS X 10, Ubuntu Linux 19.10, Windows XP, etc) 41 | 42 | - `svelte-preprocess` version (Please check you can reproduce the issue with the latest release!) 43 | 44 | - Whether your project uses Webpack or Rollup 45 | 46 | **Additional context** 47 | Add any other context about the problem here. 48 | -------------------------------------------------------------------------------- /test/processors/coffeescript.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest'; 2 | import { coffeescript } from '../../src'; 3 | import { getFixtureContent, preprocess } from '../utils'; 4 | 5 | const EXPECTED_SCRIPT = getFixtureContent('script.js'); 6 | 7 | describe(`processor - coffeescript`, () => { 8 | it('should ignore other languages', async () => { 9 | const template = ``; 10 | const options = {}; 11 | 12 | const preprocessed = await preprocess(template, [coffeescript(options)]); 13 | 14 | expect(preprocessed.toString?.()).toBe(template); 15 | }); 16 | 17 | it('should leave other languages untouched', async () => { 18 | const template = ``; 19 | const options = { 20 | stripIndent: true, 21 | prependData: '/* potato */', 22 | }; 23 | 24 | const preprocessed = await preprocess(template, [coffeescript(options)]); 25 | 26 | expect(preprocessed.toString?.()).toBe(template); 27 | }); 28 | 29 | it('should support external src files', async () => { 30 | const template = ``; 31 | const options = {}; 32 | 33 | const preprocessed = await preprocess(template, [coffeescript(options)]); 34 | 35 | expect(preprocessed.toString?.()).toContain(EXPECTED_SCRIPT); 36 | }); 37 | 38 | it('should support prepended data', async () => { 39 | const template = ``; 40 | const options = { 41 | prependData: '### potato ###', 42 | }; 43 | 44 | const preprocessed = await preprocess(template, [coffeescript(options)]); 45 | 46 | expect(preprocessed.toString?.()).toContain('/* potato */'); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /src/modules/globalifySelector.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable line-comment-position */ 2 | /* 3 | * Split a selector string (ex: div > foo ~ .potato) by 4 | * separators: space, >, +, ~ and comma (maybe not needed) 5 | * We use a negative lookbehind assertion to prevent matching 6 | * escaped combinators like `\~`. 7 | */ 8 | // TODO: maybe replace this ugly pattern with an actual selector parser? (https://github.com/leaverou/parsel, 2kb) 9 | const combinatorPattern = 10 | /(?+~,]\s*)(?![^(]*\))(?![^[]+\]|\d)/g; 11 | 12 | export function globalifySelector(selector: string) { 13 | const parts = selector.trim().split(combinatorPattern); 14 | 15 | const newSelector = []; 16 | 17 | for (let i = 0; i < parts.length; i++) { 18 | const part = parts[i]; 19 | 20 | // if this is a separator or a :global 21 | if (i % 2 !== 0 || part === '' || part.startsWith(':global')) { 22 | newSelector.push(part); 23 | continue; 24 | } 25 | 26 | // :local() with scope 27 | if (part.startsWith(':local(')) { 28 | newSelector.push(part.replace(/:local\((.+?)\)/g, '$1')); 29 | continue; 30 | } 31 | 32 | // :local inlined in a selector 33 | if (part.startsWith(':local')) { 34 | // + 2 to ignore the :local and space combinator 35 | const startIndex = i + 2; 36 | let endIndex = parts.findIndex( 37 | (p, idx) => idx > startIndex && p.startsWith(':global'), 38 | ); 39 | 40 | endIndex = endIndex === -1 ? parts.length - 1 : endIndex; 41 | 42 | newSelector.push(...parts.slice(startIndex, endIndex + 1)); 43 | 44 | i = endIndex; 45 | 46 | continue; 47 | } 48 | 49 | newSelector.push(`:global(${part})`); 50 | } 51 | 52 | return newSelector.join(''); 53 | } 54 | -------------------------------------------------------------------------------- /test/processors/typescript.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest'; 2 | import { typescript } from '../../src'; 3 | import { getFixtureContent, preprocess } from '../utils'; 4 | 5 | const EXPECTED_SCRIPT = getFixtureContent('script.js'); 6 | 7 | describe(`processor - typescript`, () => { 8 | it('should ignore other languages', async () => { 9 | const template = ``; 10 | const options = {}; 11 | 12 | const preprocessed = await preprocess(template, [typescript(options)]); 13 | 14 | expect(preprocessed.toString?.()).toBe(template); 15 | }); 16 | 17 | it('should leave other languages untouched', async () => { 18 | const template = ``; 19 | const options = { prependData: '/* potato */' }; 20 | 21 | const preprocessed = await preprocess(template, [typescript(options)]); 22 | 23 | expect(preprocessed.toString?.()).toBe(template); 24 | }); 25 | 26 | it('should support external src files', async () => { 27 | const template = `
`; 28 | const options = { 29 | tsconfigFile: false, 30 | compilerOptions: { module: 'es2015' }, 31 | prependData: '// potato', 32 | }; 33 | 34 | const preprocessed = await preprocess(template, [typescript(options)]); 35 | 36 | expect(preprocessed.toString?.()).toContain(EXPECTED_SCRIPT); 37 | }); 38 | 39 | it('should support prepended data', async () => { 40 | const template = `
`; 41 | const options = { 42 | tsconfigFile: false, 43 | compilerOptions: { module: 'es2015' }, 44 | prependData: '// potato', 45 | }; 46 | 47 | const preprocessed = await preprocess(template, [typescript(options)]); 48 | 49 | expect(preprocessed.toString?.()).toContain('// potato'); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /test/transformers/babel.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest'; 2 | import { sveltePreprocess } from '../../src'; 3 | import { preprocess } from '../utils'; 4 | 5 | const BABEL_CONFIG = { 6 | presets: [ 7 | [ 8 | '@babel/preset-env', 9 | { 10 | loose: true, 11 | modules: false, 12 | targets: { 13 | esmodules: true, 14 | }, 15 | }, 16 | ], 17 | ], 18 | }; 19 | 20 | // beforeAll(() => jest.setTimeout(10000)); 21 | // afterAll(() => jest.setTimeout(5000)); 22 | 23 | describe('transformer - babel', () => { 24 | it('transpiles with babel', async () => { 25 | const template = ``; 29 | 30 | const opts = sveltePreprocess({ 31 | babel: BABEL_CONFIG, 32 | }); 33 | 34 | const preprocessed = await preprocess(template, opts); 35 | 36 | expect(preprocessed.code).toBe(``); 39 | }); 40 | 41 | it('should not transpile import/export syntax with preset-env', async () => { 42 | const template = ``; 46 | 47 | const opts = sveltePreprocess({ 48 | babel: { 49 | presets: [ 50 | [ 51 | '@babel/preset-env', 52 | { 53 | loose: true, 54 | targets: { 55 | esmodules: true, 56 | }, 57 | }, 58 | ], 59 | ], 60 | }, 61 | }); 62 | 63 | const preprocessed = await preprocess(template, opts); 64 | 65 | expect(preprocessed.code).toBe( 66 | ``, 69 | ); 70 | }); 71 | }); 72 | -------------------------------------------------------------------------------- /test/utils.ts: -------------------------------------------------------------------------------- 1 | import { readFileSync } from 'fs'; 2 | import { resolve } from 'path'; 3 | import { vi, afterAll, afterEach } from 'vitest'; 4 | 5 | import { 6 | compile as svelteCompile, 7 | preprocess as sveltePreprocess, 8 | } from 'svelte/compiler'; 9 | 10 | export const CSS_PATTERN = 11 | /div(\.svelte-\w{4,7})?\s*\{\s*color:\s*(red|#f00);?\s*\}/; 12 | 13 | export const getTestAppFilename = () => resolve(__dirname, 'App.svelte'); 14 | 15 | export const preprocess = async (input: string, opts: any) => 16 | sveltePreprocess(input, opts, { filename: getTestAppFilename() }); 17 | 18 | const compile = async (input: string, opts: any) => { 19 | const preprocessed = await exports.preprocess(input, opts); 20 | const { js, css } = svelteCompile(preprocessed.toString?.(), { 21 | css: 'injected', 22 | }); 23 | 24 | return { js, css }; 25 | }; 26 | 27 | export const doesCompileThrow = async (input: string, opts: any) => { 28 | let didThrow = false; 29 | 30 | try { 31 | await compile(input, opts); 32 | } catch (err) { 33 | didThrow = true; 34 | } 35 | 36 | return didThrow; 37 | }; 38 | 39 | export const getFixturePath = (file: string) => 40 | resolve(__dirname, 'fixtures', file); 41 | 42 | export const getFixtureContent = (file: string) => 43 | readFileSync(exports.getFixturePath(file)).toString().trim(); 44 | 45 | export function spyConsole({ silent = true } = {}) { 46 | const warnSpy = vi.spyOn(global.console, 'warn'); 47 | const errorSpy = vi.spyOn(global.console, 'error'); 48 | const logSpy = vi.spyOn(global.console, 'log'); 49 | 50 | if (silent) { 51 | warnSpy.mockImplementation(() => {}); 52 | errorSpy.mockImplementation(() => {}); 53 | logSpy.mockImplementation(() => {}); 54 | } 55 | 56 | afterAll(() => { 57 | warnSpy.mockRestore(); 58 | errorSpy.mockRestore(); 59 | logSpy.mockRestore(); 60 | }); 61 | 62 | afterEach(() => { 63 | warnSpy.mockClear(); 64 | errorSpy.mockClear(); 65 | logSpy.mockClear(); 66 | }); 67 | 68 | return { warnSpy, errorSpy, logSpy }; 69 | } 70 | -------------------------------------------------------------------------------- /test/autoProcess/style.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest'; 2 | import { sveltePreprocess } from '../../src'; 3 | import { preprocess, getFixtureContent, CSS_PATTERN } from '../utils'; 4 | 5 | const STYLE_LANGS: Array<[string, string]> = [ 6 | ['sass', 'sass'], 7 | ['less', 'less'], 8 | ['scss', 'scss'], 9 | ['stylus', 'styl'], 10 | ]; 11 | 12 | STYLE_LANGS.forEach(([lang, ext]) => { 13 | describe(`style - preprocessor - ${lang}`, () => { 14 | it(`should parse ${lang}`, async () => { 15 | const template = `
`; 18 | 19 | const opts = sveltePreprocess(); 20 | const preprocessed = await preprocess(template, opts); 21 | 22 | expect(preprocessed.toString?.()).toMatch(CSS_PATTERN); 23 | }); 24 | 25 | it(`should parse external ${lang}`, async () => { 26 | const templateExternal = `
`; 27 | const opts = sveltePreprocess(); 28 | const preprocessed = await preprocess(templateExternal, opts); 29 | 30 | expect(preprocessed.toString?.()).toMatch(CSS_PATTERN); 31 | }); 32 | 33 | it(`should parse external ${lang}`, async () => { 34 | const templateExternal = `
`; 35 | const opts = sveltePreprocess(); 36 | const preprocessed = await preprocess(templateExternal, opts); 37 | 38 | expect(preprocessed.toString?.()).toMatch(CSS_PATTERN); 39 | }); 40 | 41 | it(`should return empty if content is empty`, async () => { 42 | const templateExternal = `
`; 43 | const opts = sveltePreprocess({ 44 | [lang]: { 45 | sourceMap: false, 46 | sourcemap: false, 47 | map: false, 48 | }, 49 | }); 50 | 51 | const preprocessed = await preprocess(templateExternal, opts); 52 | 53 | expect(preprocessed.toString?.()).toMatch(templateExternal); 54 | }); 55 | }); 56 | }); 57 | -------------------------------------------------------------------------------- /src/modules/tagInfo.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable node/prefer-promises/fs */ 2 | import { readFile, access } from 'fs'; 3 | import { resolve, dirname } from 'path'; 4 | 5 | import { getLanguage } from './language'; 6 | import { isValidLocalPath } from './utils'; 7 | 8 | import type { PreprocessorArgs } from '../types'; 9 | 10 | const resolveSrc = (importerFile: string, srcPath: string) => 11 | resolve(dirname(importerFile), srcPath); 12 | 13 | const getSrcContent = (file: string): Promise => { 14 | return new Promise((resolve, reject) => { 15 | readFile(file, (error, data) => { 16 | // istanbul ignore if 17 | if (error) reject(error); 18 | else resolve(data.toString()); 19 | }); 20 | }); 21 | }; 22 | 23 | async function doesFileExist(file: string) { 24 | return new Promise((resolve) => access(file, 0, (err) => resolve(!err))); 25 | } 26 | 27 | export const getTagInfo = async ({ 28 | attributes, 29 | filename, 30 | content, 31 | markup, 32 | }: PreprocessorArgs) => { 33 | const dependencies = []; 34 | // catches empty content and self-closing tags 35 | const isEmptyContent = content == null || content.trim().length === 0; 36 | 37 | /** only include src file if content of tag is empty */ 38 | if (attributes.src && isEmptyContent) { 39 | // istanbul ignore if 40 | if (typeof attributes.src !== 'string') { 41 | throw new Error('src attribute must be string'); 42 | } 43 | 44 | let path = attributes.src; 45 | 46 | /** Only try to get local files (path starts with ./ or ../) */ 47 | if (isValidLocalPath(path) && filename) { 48 | path = resolveSrc(filename, path); 49 | if (await doesFileExist(path)) { 50 | content = await getSrcContent(path); 51 | dependencies.push(path); 52 | } else { 53 | console.warn(`[svelte-preprocess] The file "${path}" was not found.`); 54 | } 55 | } 56 | } 57 | 58 | const { lang, alias } = getLanguage(attributes); 59 | 60 | return { 61 | filename, 62 | attributes, 63 | content, 64 | lang, 65 | alias, 66 | dependencies, 67 | markup, 68 | }; 69 | }; 70 | -------------------------------------------------------------------------------- /src/modules/utils.ts: -------------------------------------------------------------------------------- 1 | import { existsSync } from 'fs'; 2 | import { dirname, join, parse } from 'path'; 3 | 4 | export function concat(...arrs: any[]): any[] { 5 | return arrs.reduce((acc: [], a) => { 6 | if (a) return acc.concat(a); 7 | 8 | return acc; 9 | }, []); 10 | } 11 | 12 | /** Paths used by preprocessors to resolve @imports */ 13 | export function getIncludePaths(fromFilename?: string, base: string[] = []) { 14 | if (fromFilename == null) return []; 15 | 16 | return [ 17 | ...new Set([...base, 'node_modules', process.cwd(), dirname(fromFilename)]), 18 | ]; 19 | } 20 | 21 | const depCheckCache: Record = {}; 22 | 23 | /** 24 | * Checks if a package is installed. 25 | * 26 | * @export 27 | * @param {string} dep 28 | * @returns boolean 29 | */ 30 | export async function hasDepInstalled(dep: string) { 31 | if (depCheckCache[dep] != null) { 32 | return depCheckCache[dep]; 33 | } 34 | 35 | let result = false; 36 | 37 | try { 38 | await import(dep); 39 | 40 | result = true; 41 | } catch (e) { 42 | result = false; 43 | } 44 | 45 | return (depCheckCache[dep] = result); 46 | } 47 | 48 | export function isValidLocalPath(path: string) { 49 | return path.startsWith('.'); 50 | } 51 | 52 | // finds a existing path up the tree 53 | export function findUp({ what, from }: { what: string; from: string }) { 54 | const { root, dir } = parse(from); 55 | let cur = dir; 56 | 57 | try { 58 | while (cur !== root) { 59 | const possiblePath = join(cur, what); 60 | 61 | if (existsSync(possiblePath)) { 62 | return possiblePath; 63 | } 64 | 65 | cur = dirname(cur); 66 | } 67 | } catch (e) { 68 | console.error(e); 69 | } 70 | 71 | return null; 72 | } 73 | 74 | // set deep property in object 75 | export function setProp(obj: any, keyList: string[], value: any) { 76 | let i = 0; 77 | 78 | for (; i < keyList.length - 1; i++) { 79 | const key = keyList[i]; 80 | 81 | if (typeof obj[key] !== 'object') { 82 | obj[key] = {}; 83 | } 84 | 85 | obj = obj[key]; 86 | } 87 | 88 | obj[keyList[i]] = value; 89 | } 90 | -------------------------------------------------------------------------------- /src/modules/markup.ts: -------------------------------------------------------------------------------- 1 | import type { Transformer, Preprocessor } from '../types'; 2 | 3 | /** Create a tag matching regexp. */ 4 | export function createTagRegex(tagName: string, flags?: string): RegExp { 5 | return new RegExp( 6 | `/|<${tagName}(\\s[^]*?)?(?:>([^]*?)<\\/${tagName}>|\\/>)`, 7 | flags, 8 | ); 9 | } 10 | 11 | /** Transform an attribute string into a key-value object */ 12 | export function parseAttributes(attributesStr: string): Record { 13 | return attributesStr 14 | .split(/\s+/) 15 | .filter(Boolean) 16 | .reduce((acc: Record, attr) => { 17 | const [name, value] = attr.split('='); 18 | 19 | // istanbul ignore next 20 | acc[name] = value ? value.replace(/['"]/g, '') : true; 21 | 22 | return acc; 23 | }, {}); 24 | } 25 | 26 | export async function transformMarkup( 27 | { content, filename }: { content: string; filename?: string }, 28 | transformer: Preprocessor | Transformer, 29 | options: Record = {}, 30 | ) { 31 | let { markupTagName = 'template' } = options; 32 | 33 | markupTagName = markupTagName.toLocaleLowerCase(); 34 | 35 | const markupPattern = createTagRegex(markupTagName); 36 | 37 | const templateMatch = content.match(markupPattern); 38 | 39 | /** If no