",
36 | "license": "MIT",
37 | "bugs": {
38 | "url": "https://github.com/privatenumber/vue-import-loader/issues"
39 | },
40 | "homepage": "https://github.com/privatenumber/vue-import-loader#readme",
41 | "devDependencies": {
42 | "eslint": "^6.8.0",
43 | "eslint-config-airbnb-base": "^14.1.0",
44 | "eslint-plugin-import": "^2.20.2",
45 | "husky": "^4.2.5",
46 | "jest": "^26.0.1",
47 | "lint-staged": "^10.2.6",
48 | "memfs": "^3.2.0",
49 | "unionfs": "^4.4.0",
50 | "vue": "^2.6.11",
51 | "vue-loader": "^15.9.2",
52 | "webpack": "^4.43.0"
53 | },
54 | "dependencies": {
55 | "acorn": "^7.2.0",
56 | "acorn-jsx": "^5.2.0",
57 | "acorn-walk": "^7.1.1",
58 | "loader-utils": "^2.0.0",
59 | "outdent": "^0.7.1",
60 | "vue-template-compiler": "^2.6.11"
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/test/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "jest": true,
4 | "browser": true
5 | }
6 | }
--------------------------------------------------------------------------------
/test/index.spec.js:
--------------------------------------------------------------------------------
1 | const outdent = require('outdent');
2 | const Vue = require('vue');
3 | const { httpServer, build, mount } = require('./utils');
4 |
5 | const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
6 |
7 | Vue.config.productionTip = false;
8 | Vue.config.devtools = false;
9 |
10 | beforeAll(() => httpServer.start());
11 | afterAll(() => httpServer.stop());
12 | afterEach(() => {
13 | window.webpackJsonp = null;
14 | });
15 |
16 | describe('Error handling', () => {
17 | test('No options', async () => {
18 | const built = await build({
19 | '/index.vue': outdent`
20 |
21 | Hello
22 |
23 | `,
24 | '/CustomComp.vue': outdent`
25 |
26 | world
27 |
28 | `,
29 | });
30 |
31 | const warnHandler = jest.fn();
32 | Vue.config.warnHandler = warnHandler;
33 |
34 | const vm = mount(Vue, built);
35 |
36 | expect(warnHandler).toBeCalled();
37 | expect(vm.$el.outerHTML).toBe('Hello
');
38 | });
39 |
40 | test('No options.components', async () => {
41 | const built = await build({
42 | '/index.vue': outdent`
43 |
44 | Hello
45 |
46 | `,
47 | '/CustomComp.vue': outdent`
48 |
49 | world
50 |
51 | `,
52 | }, {});
53 |
54 | const warnHandler = jest.fn();
55 | Vue.config.warnHandler = warnHandler;
56 |
57 | const vm = mount(Vue, built);
58 |
59 | expect(warnHandler).toBeCalled();
60 | expect(vm.$el.outerHTML).toBe('Hello
');
61 | });
62 | });
63 |
64 | describe('Component Registration', () => {
65 | test('Single component', async () => {
66 | const built = await build({
67 | '/index.vue': outdent`
68 |
69 | Hello
70 |
71 | `,
72 | '/CustomComp.vue': outdent`
73 |
74 | world
75 |
76 | `,
77 | }, {
78 | components: {
79 | CustomComp: '/CustomComp.vue',
80 | },
81 | });
82 |
83 | const vm = mount(Vue, built);
84 |
85 | expect(vm.$el.outerHTML).toBe('Hello world
');
86 | });
87 |
88 | test('Multiple components', async () => {
89 | const built = await build({
90 | '/index.vue': outdent`
91 |
92 |
93 |
Hello
94 |
95 |
96 |
97 | `,
98 | '/world.vue': outdent`
99 |
100 | world
101 |
102 | `,
103 | '/goodbyeWorld.vue': outdent`
104 |
105 | goodbye world
106 |
107 | `,
108 | }, {
109 | components: {
110 | world: '/world.vue',
111 | 'goodbye-world': '/goodbyeWorld.vue',
112 | UnusedComp: '/should-not-exist.vue',
113 | },
114 | });
115 |
116 | const vm = mount(Vue, built);
117 | expect(vm.$el.outerHTML).toBe('Hello world
goodbye world
');
118 | });
119 |
120 | test('Multiple files', async () => {
121 | const built = await build({
122 | '/index.vue': outdent`
123 |
124 |
125 |
Hello
126 |
127 |
128 |
129 | `,
130 | '/world.vue': outdent`
131 |
132 | world
133 |
134 | `,
135 | '/goodbyeWorld.vue': outdent`
136 |
137 | goodbye
138 |
139 | `,
140 | }, {
141 | components: {
142 | 'world-el': '/world.vue',
143 | 'goodbye-world': '/goodbyeWorld.vue',
144 | UnusedComp: '/should-not-exist.vue',
145 | },
146 | });
147 |
148 | const vm = mount(Vue, built);
149 | expect(vm.$el.outerHTML).toBe('Hello world
goodbye world
');
150 | });
151 |
152 | test('Component collision w/ Pascal', async () => {
153 | const built = await build({
154 | '/index.vue': outdent`
155 |
156 |
157 |
158 |
159 |
160 |
169 | `,
170 | '/hello-world-real.vue': outdent`
171 |
172 | hello world real
173 |
174 | `,
175 | '/hello-world-fake.vue': outdent`
176 |
177 | hello world fake
178 |
179 | `,
180 | }, {
181 | components: {
182 | 'hello-world': '/hello-world-fake.vue',
183 | },
184 | });
185 |
186 | const vm = mount(Vue, built);
187 | expect(vm.$el.outerHTML).toBe('hello world real
');
188 | });
189 |
190 | test('Component collision w/ kebab', async () => {
191 | const built = await build({
192 | '/index.vue': outdent`
193 |
194 |
195 |
196 |
197 |
198 |
207 | `,
208 | '/hello-world-real.vue': outdent`
209 |
210 | hello world real
211 |
212 | `,
213 | '/hello-world-fake.vue': outdent`
214 |
215 | hello world fake
216 |
217 | `,
218 | }, {
219 | components: {
220 | 'hello-world': '/hello-world-fake.vue',
221 | },
222 | });
223 |
224 | const vm = mount(Vue, built);
225 | expect(vm.$el.outerHTML).toBe('hello world real
');
226 | });
227 |
228 | test('Dynamic component', async () => {
229 | const built = await build({
230 | '/index.vue': outdent`
231 |
232 |
233 |
234 |
235 |
236 |
237 | `,
238 | '/world.vue': outdent`
239 |
240 | world
241 |
242 | `,
243 | '/goodbyeWorld.vue': outdent`
244 |
245 | goodbye
246 |
247 | `,
248 | }, {
249 | components: {
250 | world: '/world.vue',
251 | 'goodbye-world': '/goodbyeWorld.vue',
252 | UnusedComp: '/should-not-exist.vue',
253 | },
254 | });
255 |
256 | const vm = mount(Vue, built);
257 | expect(vm.$el.outerHTML).toBe('world
goodbye world
');
258 | });
259 |
260 | test('Alias', async () => {
261 | const built = await build({
262 | '/index.vue': outdent`
263 |
264 |
265 |
Hello
266 |
267 |
268 |
269 | `,
270 | '/components/world.vue': outdent`
271 |
272 | world
273 |
274 | `,
275 | '/components/goodbyeWorld.vue': outdent`
276 |
277 | goodbye
278 |
279 | `,
280 | }, {
281 | components: {
282 | world: '@/components/world.vue',
283 | 'goodbye-world': '@/components/goodbyeWorld.vue',
284 | UnusedComp: '@/components/should-not-exist.vue',
285 | },
286 | });
287 |
288 | const vm = mount(Vue, built);
289 | expect(vm.$el.outerHTML).toBe('Hello world
goodbye world
');
290 | });
291 |
292 | test('Resolve function', async () => {
293 | const built = await build({
294 | '/index.vue': outdent`
295 |
296 |
297 |
Hello
298 |
299 |
300 |
301 | `,
302 | '/components/world.vue': outdent`
303 |
304 | world
305 |
306 | `,
307 | '/components/goodbye-world.vue': outdent`
308 |
309 | goodbye
310 |
311 | `,
312 | }, {
313 | components({ kebab }) {
314 | if (['world', 'goodbye-world'].includes(kebab)) {
315 | return `/components/${kebab}.vue`;
316 | }
317 | return undefined;
318 | },
319 | });
320 |
321 | const vm = mount(Vue, built);
322 | expect(vm.$el.outerHTML).toBe('Hello world
goodbye world
');
323 | });
324 | });
325 |
326 |
327 | describe('Async components', () => {
328 | test('Basic async component', async () => {
329 | const built = await build({
330 | '/index.vue': outdent`
331 |
332 |
333 |
334 |
335 |
336 | `,
337 | '/HelloWorld.vue': outdent`
338 |
339 | Hello world
340 |
341 | `,
342 | '/LoadingComponent.vue': outdent`
343 |
344 | Loading
345 |
346 | `,
347 | '/ErrorComponent.vue': outdent`
348 |
349 | Error
350 |
351 | `,
352 | }, {
353 | components: {
354 | HelloWorld: {
355 | component: '/HelloWorld.vue',
356 | loading: '/LoadingComponent.vue',
357 | error: '/ErrorComponent.vue',
358 | delay: 0,
359 | timeout: 3000,
360 | },
361 | },
362 | });
363 |
364 | const vm = mount(Vue, built);
365 |
366 | expect(vm.$el.outerHTML).toBe('Loading
');
367 |
368 | await sleep(300);
369 | expect(vm.$el.outerHTML).toBe('Hello world
');
370 | }, 2000);
371 |
372 | test('Magic comments', async () => {
373 | const built = await build({
374 | '/index.vue': outdent`
375 |
376 |
377 |
378 |
379 |
380 | `,
381 | '/HelloWorlds.vue': outdent`
382 |
383 | Hello worlds!!!!!!
384 |
385 | `,
386 | '/LoadingComponent.vue': outdent`
387 |
388 | Loading
389 |
390 | `,
391 | '/ErrorComponent.vue': outdent`
392 |
393 | Error
394 |
395 | `,
396 | }, {
397 | components: {
398 | HelloWorld: {
399 | magicComments: [
400 | 'webpackChunkName: "my-chunk-name"',
401 | 'webpackPrefetch: true',
402 | 'webpackPreload: true',
403 | ],
404 | component: '/HelloWorlds.vue',
405 | loading: '/LoadingComponent.vue',
406 | error: '/ErrorComponent.vue',
407 | },
408 | },
409 | });
410 |
411 | expect(built).toMatch('my-chunk-name');
412 |
413 | const vm = mount(Vue, built);
414 |
415 | expect(vm.$el.outerHTML).toBe('');
416 |
417 | await sleep(200);
418 | expect(vm.$el.outerHTML).toBe('Loading
');
419 |
420 | await sleep(200);
421 | expect(vm.$el.outerHTML).toBe('Hello worlds!!!!!!
');
422 | }, 2000);
423 |
424 | test('Magic comments - eager', async () => {
425 | const built = await build({
426 | '/index.vue': outdent`
427 |
428 |
429 |
430 |
431 |
432 | `,
433 | '/HelloWorlds.vue': outdent`
434 |
435 | Hello worlds!!!!!!
436 |
437 | `,
438 | '/LoadingComponent.vue': outdent`
439 |
440 | Loading
441 |
442 | `,
443 | '/ErrorComponent.vue': outdent`
444 |
445 | Error
446 |
447 | `,
448 | }, {
449 | components: {
450 | HelloWorld: {
451 | magicComments: ['webpackMode: "eager"'],
452 | component: '/HelloWorlds.vue',
453 | loading: '/LoadingComponent.vue',
454 | error: '/ErrorComponent.vue',
455 | },
456 | },
457 | });
458 |
459 | const vm = mount(Vue, built);
460 |
461 | await sleep(100);
462 | expect(vm.$el.outerHTML).toBe('Hello worlds!!!!!!
');
463 | }, 2000);
464 |
465 | test('Resolver', async () => {
466 | const built = await build({
467 | '/index.vue': outdent`
468 |
469 |
470 |
471 |
472 |
473 | `,
474 | '/HelloWorlds.vue': outdent`
475 |
476 | Hello worlds!!!!!!
477 |
478 | `,
479 | '/LoadingComponent.vue': outdent`
480 |
481 | Loading
482 |
483 | `,
484 | '/ErrorComponent.vue': outdent`
485 |
486 | Error
487 |
488 | `,
489 | }, {
490 | components({ pascal }) {
491 | if (pascal === 'HelloWorld') {
492 | return {
493 | magicComments: ['webpackMode: "eager"'],
494 | component: '/HelloWorlds.vue',
495 | loading: '/LoadingComponent.vue',
496 | error: '/ErrorComponent.vue',
497 | };
498 | }
499 | return undefined;
500 | },
501 | });
502 |
503 | const vm = mount(Vue, built);
504 |
505 | await sleep(100);
506 | expect(vm.$el.outerHTML).toBe('Hello worlds!!!!!!
');
507 | }, 2000);
508 | });
509 |
510 | describe('Functional components', () => {
511 | test('Functional mode disabled', async () => {
512 | const built = await build({
513 | '/index.vue': outdent`
514 |
515 |
516 |
517 | `,
518 | '/components/goodbye-world.vue': outdent`
519 |
520 |
521 |
522 | `,
523 | '/components/goodbye.vue': outdent`
524 |
525 | good
526 |
527 | `,
528 | '/components/bye.vue': outdent`
529 |
530 | bye
531 |
532 | `,
533 | '/components/world.vue': outdent`
534 |
535 | world
536 |
537 | `,
538 | }, {
539 | components: {
540 | GoodbyeWorld: '/components/goodbye-world.vue',
541 | Goodbye: '/components/goodbye.vue',
542 | Bye: '/components/bye.vue',
543 | World: '/components/world.vue',
544 | },
545 | });
546 |
547 | const vm = mount(Vue, built);
548 | expect(vm.$el.outerHTML).toBe('
');
549 | });
550 |
551 | test('Functional mode enabled', async () => {
552 | const built = await build({
553 | '/index.vue': outdent`
554 |
555 |
556 |
557 | `,
558 | '/components/goodbye-world.vue': outdent`
559 |
560 |
561 |
562 | `,
563 | '/components/goodbye.vue': outdent`
564 |
565 | good
566 |
567 | `,
568 | '/components/bye.vue': outdent`
569 |
570 | bye
571 |
572 | `,
573 | '/components/world.vue': outdent`
574 |
575 | world
576 |
577 | `,
578 | }, {
579 | functional: true,
580 | components: {
581 | GoodbyeWorld: '/components/goodbye-world.vue',
582 | Goodbye: '/components/goodbye.vue',
583 | Bye: '/components/bye.vue',
584 | World: '/components/world.vue',
585 | },
586 | });
587 |
588 | const vm = mount(Vue, built);
589 | expect(vm.$el.outerHTML).toBe('goodbye world
');
590 | });
591 | });
592 |
593 | describe('Unused component detection', () => {
594 | test('Don\'t import fake component - Pascal', async () => {
595 | const built = await build({
596 | '/index.vue': outdent`
597 |
598 |
599 |
600 |
601 |
602 |
613 | `,
614 | '/hello-world-real.vue': outdent`
615 |
616 | hello world real
617 |
618 | `,
619 | }, {
620 | components: {
621 | 'hello-world': '/hello-world-non-existent.vue',
622 | },
623 | });
624 |
625 | const vm = mount(Vue, built);
626 | expect(vm.$el.outerHTML).toBe('hello world real
');
627 | });
628 |
629 | test('Don\'t import fake component - kebab', async () => {
630 | const built = await build({
631 | '/index.vue': outdent`
632 |
633 |
634 |
635 |
636 |
637 |
646 | `,
647 | '/hello-world-real.vue': outdent`
648 |
649 | hello world real
650 |
651 | `,
652 | }, {
653 | components: {
654 | 'hello-world': '/hello-world-non-existent.vue',
655 | },
656 | });
657 |
658 | const vm = mount(Vue, built);
659 | expect(vm.$el.outerHTML).toBe('hello world real
');
660 | });
661 |
662 | test('Won\'t detect registered', (cb) => {
663 | build({
664 | '/index.vue': outdent`
665 |
666 |
667 |
668 |
669 |
670 |
681 | `,
682 | '/hello-world-real.vue': outdent`
683 |
684 | hello world real
685 |
686 | `,
687 | }, {
688 | components: {
689 | 'hello-world': '/hello-world-trap.vue',
690 | },
691 | }).catch(() => {
692 | // Expect error
693 | cb();
694 | });
695 | });
696 | });
697 |
--------------------------------------------------------------------------------
/test/utils.js:
--------------------------------------------------------------------------------
1 | const http = require('http');
2 | const path = require('path');
3 | const webpack = require('webpack');
4 | const { Volume } = require('memfs');
5 | const fs = require('fs');
6 | const { ufs } = require('unionfs');
7 | const VueLoaderPlugin = require('vue-loader/lib/plugin');
8 |
9 | const loaderPath = require.resolve('..');
10 |
11 | const httpServer = (() => {
12 | let builtFs;
13 |
14 | const server = http.createServer((req, res) => {
15 | res.write(builtFs.readFileSync(req.url));
16 | setTimeout(() => res.end(), 200);
17 | });
18 |
19 | return {
20 | setFs(_builtFs) {
21 | builtFs = _builtFs;
22 | },
23 | start() {
24 | return new Promise((resolve) => {
25 | this.listening = server.listen(0, resolve);
26 | });
27 | },
28 | get port() {
29 | return this.listening.address().port;
30 | },
31 | stop() {
32 | return new Promise((resolve) => {
33 | server.close(resolve);
34 | });
35 | },
36 | };
37 | })();
38 |
39 | function build(volJson, loaderConfig = {}) {
40 | return new Promise((resolve, reject) => {
41 | const mfs = Volume.fromJSON(volJson);
42 | mfs.join = path.join.bind(path);
43 |
44 | const compiler = webpack({
45 | mode: 'production',
46 | optimization: {
47 | minimize: false,
48 | providedExports: false,
49 | },
50 | resolve: {
51 | alias: {
52 | '@': '/',
53 | },
54 | },
55 | resolveLoader: {
56 | alias: {
57 | 'vue-import-loader': loaderPath,
58 | },
59 | },
60 | module: {
61 | rules: [
62 | {
63 | test: /\.vue$/,
64 | use: [
65 | {
66 | loader: 'vue-import-loader',
67 | options: loaderConfig,
68 | },
69 | {
70 | loader: 'vue-loader',
71 | options: {
72 | productionMode: true,
73 | },
74 | },
75 | ],
76 | },
77 | ],
78 | },
79 | plugins: [
80 | new VueLoaderPlugin(),
81 | ],
82 | entry: '/index.vue',
83 | output: {
84 | path: '/dist-[hash]',
85 | publicPath: `http://localhost:${httpServer.port}/dist-[hash]/`,
86 | },
87 | });
88 |
89 | compiler.inputFileSystem = ufs.use(fs).use(mfs);
90 | compiler.outputFileSystem = mfs;
91 |
92 | compiler.run((err, stats) => {
93 | if (err) {
94 | reject(err);
95 | return;
96 | }
97 |
98 | if (stats.compilation.errors.length > 0) {
99 | reject(stats.compilation.errors);
100 | return;
101 | }
102 |
103 | httpServer.setFs(mfs);
104 | resolve(mfs.readFileSync(`/dist-${stats.hash}/main.js`).toString());
105 | });
106 | });
107 | }
108 |
109 | function mount(Vue, src) {
110 | // eslint-disable-next-line no-eval
111 | const { default: Component } = eval(src);
112 | const vm = new Vue(Component);
113 | vm.$mount();
114 | return vm;
115 | }
116 |
117 | module.exports = {
118 | httpServer,
119 | build,
120 | mount,
121 | };
122 |
--------------------------------------------------------------------------------